diff --git a/arch/kcpu/docs/security.md b/arch/kcpu/docs/security.md index a2aad29535f52148a00bc349a244c3b4b52f788f..4a024572d9091b616cee8c69ac7e35b33d73c883 100644 --- a/arch/kcpu/docs/security.md +++ b/arch/kcpu/docs/security.md @@ -359,7 +359,7 @@ $f0–$f31 的寄存器编号。 - [ ] 修改 GDT 段选择子常量后验证 `UserContext::new` 中的 `cs`/`ss` 值。 - [ ] 新增异常向量处理时检查是否需要更新异常表或注册新 handler。 - [ ] LoongArch64 新增指令模拟时验证操作码掩码和寄存器编号范围。 -- [ ] 修改 `copy_user.S` 后确认每条访存指令均有对应的 `_asm_extable` 条目。 +- [ ] 修改 `copy_user.S` / `atomic_user.S` 后确认每条访存指令均有对应的 `_asm_extable` 条目。 - [ ] 新增 per-CPU 变量时验证初始化顺序(percpu init → `init_trap`)。 - [ ] 修改页表切换逻辑后验证 TLB 刷新语义是否正确。 - [ ] 修改 `fp-simd` / `tls` feature 门控代码后验证所有架构的一致性。 diff --git a/arch/kcpu/src/aarch64/atomic_user.S b/arch/kcpu/src/aarch64/atomic_user.S new file mode 100644 index 0000000000000000000000000000000000000000..59c90eb0054736f0dc39bfeb4a1d40e2625c045b --- /dev/null +++ b/arch/kcpu/src/aarch64/atomic_user.S @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. +// +// Atomically compare-exchange a 32-bit word at a (possibly user) address. +// +// Prototype: +// size_t user_atomic_cmpxchg_u32(u32 *addr, u32 old, u32 new, u32 *old_out); +// Args: x0=addr, w1=old, w2=new, x3=old_out +// Returns: 0 if no fault occurred; non-zero if a data abort occurred. +// On success, *old_out receives the value observed at *addr. If that value +// equals `old`, `new` was stored. + +.section .text +.global user_atomic_cmpxchg_u32 +.type user_atomic_cmpxchg_u32, @function + +user_atomic_cmpxchg_u32: +.Lcmpxchg_retry: +1: ldxr w4, [x0] + // old_out is a kernel pointer; no exception table needed. + str w4, [x3] + cmp w4, w1 + b.ne .Lcmpxchg_done +2: stxr w5, w2, [x0] + cbnz w5, .Lcmpxchg_retry + dmb ish +.Lcmpxchg_done: + clrex + mov x0, #0 + ret + +.Lcmpxchg_fault: + clrex + mov x0, #1 + ret + + _asm_extable 1b, .Lcmpxchg_fault + _asm_extable 2b, .Lcmpxchg_fault + +.size user_atomic_cmpxchg_u32, .-user_atomic_cmpxchg_u32 diff --git a/arch/kcpu/src/aarch64/copy_user.S b/arch/kcpu/src/aarch64/copy_user.S index b475c32d74b0f1aaa979924e3f98dbad805c7564..99004e51aa37f61b16774456b65ce697f2dd9f4b 100644 --- a/arch/kcpu/src/aarch64/copy_user.S +++ b/arch/kcpu/src/aarch64/copy_user.S @@ -3,14 +3,6 @@ // Arguments: x0=dst, x1=src, x2=size // Returns: 0 on success; remaining bytes (not copied) if a data abort occurs -.macro _asm_extable, from, to - .pushsection __ex_table, "a" - .balign 8 - .quad \from - .quad \to - .popsection -.endm - .section .text .global raw_copy_from_user @@ -20,7 +12,7 @@ // 3. Bulk copy 64 bytes per iteration using 8x LDP/STP pairs // 4. Copy remaining 8-byte chunks // 5. Tail copy bytes -// All load/store pairs are covered by exception table entries pointing to .Lfault +// All load/store pairs are covered by exception table entries pointing to .Lcopy_fault // so that on fault we compute remaining = original_end - current_dst. raw_copy_from_user: @@ -90,22 +82,22 @@ raw_copy_from_user: // Fault handler: x0 currently points just past last successfully written byte. // Remaining = dst_end - current_dst (x3 - x0) -.Lfault: +.Lcopy_fault: sub x0, x3, x0 ret // Exception table entries for every faultable memory access. - _asm_extable 1b, .Lfault - _asm_extable 2b, .Lfault - _asm_extable 3b, .Lfault - _asm_extable 4b, .Lfault - _asm_extable 5b, .Lfault - _asm_extable 6b, .Lfault - _asm_extable 7b, .Lfault - _asm_extable 8b, .Lfault - _asm_extable 9b, .Lfault - _asm_extable 10b, .Lfault - _asm_extable 11b, .Lfault - _asm_extable 12b, .Lfault - _asm_extable 13b, .Lfault - _asm_extable 14b, .Lfault + _asm_extable 1b, .Lcopy_fault + _asm_extable 2b, .Lcopy_fault + _asm_extable 3b, .Lcopy_fault + _asm_extable 4b, .Lcopy_fault + _asm_extable 5b, .Lcopy_fault + _asm_extable 6b, .Lcopy_fault + _asm_extable 7b, .Lcopy_fault + _asm_extable 8b, .Lcopy_fault + _asm_extable 9b, .Lcopy_fault + _asm_extable 10b, .Lcopy_fault + _asm_extable 11b, .Lcopy_fault + _asm_extable 12b, .Lcopy_fault + _asm_extable 13b, .Lcopy_fault + _asm_extable 14b, .Lcopy_fault diff --git a/arch/kcpu/src/aarch64/instrs.rs b/arch/kcpu/src/aarch64/instrs.rs index 904501e4ba19c1f1732cecc8521eebb65757cb70..bae9af52698a3dc757f052be097e833892523408 100644 --- a/arch/kcpu/src/aarch64/instrs.rs +++ b/arch/kcpu/src/aarch64/instrs.rs @@ -4,7 +4,11 @@ //! Wrapper functions for assembly instructions. -core::arch::global_asm!(include_str!("copy_user.S")); +core::arch::global_asm!( + include_str!("../asm/extable.inc"), + include_str!("copy_user.S"), + include_str!("atomic_user.S"), +); unsafe extern "C" { /// Copies data from source to destination, where addresses may be in user @@ -17,6 +21,22 @@ unsafe extern "C" { /// Returns the number of bytes not copied. This means 0 indicates success, /// while a value > 0 indicates failure. pub fn raw_copy_from_user(dst: *mut u8, src: *const u8, size: usize) -> usize; + + /// Atomically compare-exchanges a 32-bit word at `addr`. + /// + /// On success (return `0`), writes the previous `*addr` value into + /// `old_out`. If that previous value equals `old`, stores `new`. + /// + /// # Safety + /// + /// `addr` must be a 4-byte-aligned address that is currently accessible in + /// the active address space under the user-access window. `old_out` must + /// point to writable kernel memory. + /// + /// # Returns + /// + /// `0` if no fault occurred; non-zero if a data abort occurred. + pub fn user_atomic_cmpxchg_u32(addr: *mut u32, old: u32, new: u32, old_out: *mut u32) -> usize; } /// Alias for compatibility with other architectures diff --git a/arch/kcpu/src/asm/extable.inc b/arch/kcpu/src/asm/extable.inc new file mode 100644 index 0000000000000000000000000000000000000000..e3965e799785e0337b9861d7eb8ac3e5f197e5ed --- /dev/null +++ b/arch/kcpu/src/asm/extable.inc @@ -0,0 +1,15 @@ +// Shared exception-table macro for user-access assembly helpers. +// Include once per global_asm! block before any .S file that uses _asm_extable. + +.ifndef _ASM_EXTABLE_DEFINED +.equ _ASM_EXTABLE_DEFINED, 1 + +.macro _asm_extable, from, to + .pushsection __ex_table, "a" + .balign 8 + .quad \from + .quad \to + .popsection +.endm + +.endif diff --git a/arch/kcpu/src/loongarch64/atomic_user.S b/arch/kcpu/src/loongarch64/atomic_user.S new file mode 100644 index 0000000000000000000000000000000000000000..77dd5d5ee80834dc3b1118549de53d387e2c2ad6 --- /dev/null +++ b/arch/kcpu/src/loongarch64/atomic_user.S @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. +// +// Atomically compare-exchange a 32-bit word at a (possibly user) address. +// +// Prototype: +// size_t user_atomic_cmpxchg_u32(u32 *addr, u32 old, u32 new, u32 *old_out); +// Args: a0=addr, a1=old, a2=new, a3=old_out +// Returns: 0 if no fault occurred; non-zero if a fault occurred. +// On success, *old_out receives the value observed at *addr. If that value +// equals `old`, `new` was stored. +// +// LoongArch `sc.w rd, rj, si12` overwrites `rd` with 1 on success / 0 on fail, +// so the desired store value must be reloaded into `rd` on each retry. + +.section .text +.global user_atomic_cmpxchg_u32 +.type user_atomic_cmpxchg_u32, @function + +user_atomic_cmpxchg_u32: +.Lcmpxchg_retry: +1: ll.w $t0, $a0, 0 + st.w $t0, $a3, 0 + bne $t0, $a1, .Lcmpxchg_done + move $t1, $a2 +2: sc.w $t1, $a0, 0 + beqz $t1, .Lcmpxchg_retry + dbar 0 +.Lcmpxchg_done: + move $a0, $zero + jr $ra + +.Lcmpxchg_fault: + ori $a0, $zero, 1 + jr $ra + + _asm_extable 1b, .Lcmpxchg_fault + _asm_extable 2b, .Lcmpxchg_fault + +.size user_atomic_cmpxchg_u32, .-user_atomic_cmpxchg_u32 diff --git a/arch/kcpu/src/loongarch64/instrs.rs b/arch/kcpu/src/loongarch64/instrs.rs index 5005f16f43becefa8b078fb4590724bd88c95d1e..8aa9b5a495ab36c971789ff418b09cb716931b3a 100644 --- a/arch/kcpu/src/loongarch64/instrs.rs +++ b/arch/kcpu/src/loongarch64/instrs.rs @@ -4,7 +4,11 @@ //! Wrapper functions for assembly instructions. -core::arch::global_asm!(include_asm_macros!(), include_str!("copy_user.S")); +core::arch::global_asm!( + include_asm_macros!(), + include_str!("copy_user.S"), + include_str!("atomic_user.S"), +); unsafe extern "C" { /// Copies data from source to destination, where addresses may be in user @@ -17,4 +21,20 @@ unsafe extern "C" { /// Returns the number of bytes not copied. This means 0 indicates success, /// while a value > 0 indicates failure. pub fn user_copy(dst: *mut u8, src: *const u8, size: usize) -> usize; + + /// Atomically compare-exchanges a 32-bit word at `addr`. + /// + /// On success (return `0`), writes the previous `*addr` value into + /// `old_out`. If that previous value equals `old`, stores `new`. + /// + /// # Safety + /// + /// `addr` must be a 4-byte-aligned address that is currently accessible in + /// the active address space under the user-access window. `old_out` must + /// point to writable kernel memory. + /// + /// # Returns + /// + /// `0` if no fault occurred; non-zero if a fault occurred. + pub fn user_atomic_cmpxchg_u32(addr: *mut u32, old: u32, new: u32, old_out: *mut u32) -> usize; } diff --git a/arch/kcpu/src/riscv/atomic_user.S b/arch/kcpu/src/riscv/atomic_user.S new file mode 100644 index 0000000000000000000000000000000000000000..7e0a7a0967ce04a909c0a0614b17a82e2f74340d --- /dev/null +++ b/arch/kcpu/src/riscv/atomic_user.S @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. +// +// Atomically compare-exchange a 32-bit word at a (possibly user) address. +// +// Prototype: +// size_t user_atomic_cmpxchg_u32(u32 *addr, u32 old, u32 new, u32 *old_out); +// Args: a0=addr, a1=old, a2=new, a3=old_out +// Returns: 0 if no fault occurred; non-zero if a fault occurred. +// On success, *old_out receives the value observed at *addr. If that value +// equals `old`, `new` was stored. + +.section .text +.global user_atomic_cmpxchg_u32 +.type user_atomic_cmpxchg_u32, @function + +user_atomic_cmpxchg_u32: +.Lcmpxchg_retry: +1: lr.w t0, (a0) + sw t0, 0(a3) + bne t0, a1, .Lcmpxchg_done +2: sc.w t1, a2, (a0) + bnez t1, .Lcmpxchg_retry +.Lcmpxchg_done: + fence rw, rw + li a0, 0 + ret + +.Lcmpxchg_fault: + li a0, 1 + ret + + _asm_extable 1b, .Lcmpxchg_fault + _asm_extable 2b, .Lcmpxchg_fault + +.size user_atomic_cmpxchg_u32, .-user_atomic_cmpxchg_u32 diff --git a/arch/kcpu/src/riscv/instrs.rs b/arch/kcpu/src/riscv/instrs.rs index 5005f16f43becefa8b078fb4590724bd88c95d1e..8aa9b5a495ab36c971789ff418b09cb716931b3a 100644 --- a/arch/kcpu/src/riscv/instrs.rs +++ b/arch/kcpu/src/riscv/instrs.rs @@ -4,7 +4,11 @@ //! Wrapper functions for assembly instructions. -core::arch::global_asm!(include_asm_macros!(), include_str!("copy_user.S")); +core::arch::global_asm!( + include_asm_macros!(), + include_str!("copy_user.S"), + include_str!("atomic_user.S"), +); unsafe extern "C" { /// Copies data from source to destination, where addresses may be in user @@ -17,4 +21,20 @@ unsafe extern "C" { /// Returns the number of bytes not copied. This means 0 indicates success, /// while a value > 0 indicates failure. pub fn user_copy(dst: *mut u8, src: *const u8, size: usize) -> usize; + + /// Atomically compare-exchanges a 32-bit word at `addr`. + /// + /// On success (return `0`), writes the previous `*addr` value into + /// `old_out`. If that previous value equals `old`, stores `new`. + /// + /// # Safety + /// + /// `addr` must be a 4-byte-aligned address that is currently accessible in + /// the active address space under the user-access window. `old_out` must + /// point to writable kernel memory. + /// + /// # Returns + /// + /// `0` if no fault occurred; non-zero if a fault occurred. + pub fn user_atomic_cmpxchg_u32(addr: *mut u32, old: u32, new: u32, old_out: *mut u32) -> usize; } diff --git a/arch/kcpu/src/riscv/macros.rs b/arch/kcpu/src/riscv/macros.rs index bce1046102b76f91ec45865dec49e39c29cc5f29..12c00f92bf48ebdb3cbefa68105f43b27efb4251 100644 --- a/arch/kcpu/src/riscv/macros.rs +++ b/arch/kcpu/src/riscv/macros.rs @@ -26,6 +26,11 @@ macro_rules! __asm_macros { sw \rs2, \off*XLENB(\rs1) .endm + .endif + + .ifndef ASM_EXTABLE_FLAG + .equ ASM_EXTABLE_FLAG, 1 + .macro _asm_extable, from, to .pushsection __ex_table, "a" .balign 4 @@ -52,6 +57,11 @@ macro_rules! __asm_macros { sd \rs2, \off*XLENB(\rs1) .endm + .endif + + .ifndef ASM_EXTABLE_FLAG + .equ ASM_EXTABLE_FLAG, 1 + .macro _asm_extable, from, to .pushsection __ex_table, "a" .balign 8 diff --git a/arch/kcpu/src/x86_64/atomic_user.S b/arch/kcpu/src/x86_64/atomic_user.S new file mode 100644 index 0000000000000000000000000000000000000000..3d61900c80b85efd1079742d5614701aaebc76c1 --- /dev/null +++ b/arch/kcpu/src/x86_64/atomic_user.S @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. +// +// Atomically compare-exchange a 32-bit word at a (possibly user) address. +// +// Prototype: +// size_t user_atomic_cmpxchg_u32(u32 *addr, u32 old, u32 new, u32 *old_out); +// Args: rdi=addr, esi=old, edx=new, rcx=old_out +// Returns: 0 if no fault occurred; non-zero if a fault occurred. +// On success, *old_out receives the value observed at *addr. If that value +// equals `old`, `new` was stored. + +.section .text +.global user_atomic_cmpxchg_u32 +.type user_atomic_cmpxchg_u32, @function + +user_atomic_cmpxchg_u32: + mov eax, esi +1: lock cmpxchg dword ptr [rdi], edx + // After cmpxchg, eax always holds the previous *addr value. + mov dword ptr [rcx], eax + xor eax, eax + ret + +.Lcmpxchg_fault: + mov eax, 1 + ret + + _asm_extable 1b, .Lcmpxchg_fault + +.size user_atomic_cmpxchg_u32, .-user_atomic_cmpxchg_u32 diff --git a/arch/kcpu/src/x86_64/copy_user.S b/arch/kcpu/src/x86_64/copy_user.S index 6b748a75d3abe6d573a963645e9e76e95789f593..9df869d699b8881180f412a9dec5ece839d5bb06 100644 --- a/arch/kcpu/src/x86_64/copy_user.S +++ b/arch/kcpu/src/x86_64/copy_user.S @@ -1,11 +1,3 @@ -.macro _asm_extable, from, to - .pushsection __ex_table, "a" - .balign 8 - .quad \from - .quad \to - .popsection -.endm - .section .text .global user_copy user_copy: diff --git a/arch/kcpu/src/x86_64/instrs.rs b/arch/kcpu/src/x86_64/instrs.rs index 604c7088ebcc653f164942dc8fc062a7fac41010..4917debddb3caf70d273687c282d0329ba97e760 100644 --- a/arch/kcpu/src/x86_64/instrs.rs +++ b/arch/kcpu/src/x86_64/instrs.rs @@ -4,7 +4,11 @@ //! Wrapper functions for assembly instructions. -core::arch::global_asm!(include_str!("copy_user.S")); +core::arch::global_asm!( + include_str!("../asm/extable.inc"), + include_str!("copy_user.S"), + include_str!("atomic_user.S"), +); unsafe extern "C" { /// Copies data from source to destination, where addresses may be in user @@ -17,4 +21,20 @@ unsafe extern "C" { /// Returns the number of bytes not copied. This means 0 indicates success, /// while a value > 0 indicates failure. pub fn user_copy(dst: *mut u8, src: *const u8, size: usize) -> usize; + + /// Atomically compare-exchanges a 32-bit word at `addr`. + /// + /// On success (return `0`), writes the previous `*addr` value into + /// `old_out`. If that previous value equals `old`, stores `new`. + /// + /// # Safety + /// + /// `addr` must be a 4-byte-aligned address that is currently accessible in + /// the active address space under the user-access window. `old_out` must + /// point to writable kernel memory. + /// + /// # Returns + /// + /// `0` if no fault occurred; non-zero if a fault occurred. + pub fn user_atomic_cmpxchg_u32(addr: *mut u32, old: u32, new: u32, old_out: *mut u32) -> usize; } diff --git a/core/ksyscall/src/sync/futex.rs b/core/ksyscall/src/sync/futex.rs index 1bb711a650287c60e0eb9a1f2032ef24bc9f8d67..d427020c7b2f9de5b3d44f098c6e049d94f1e853 100644 --- a/core/ksyscall/src/sync/futex.rs +++ b/core/ksyscall/src/sync/futex.rs @@ -9,6 +9,7 @@ use core::{mem::size_of, sync::atomic::Ordering}; use kerrno::{KError, KResult, LinuxError}; use kfutex::FutexKey; use kprocess::{AsThread, current_futex_key}; +use kuaccess::atomic_u32_eq; use linux_raw_sys::general::{ FUTEX_CMD_MASK, FUTEX_CMP_REQUEUE, FUTEX_PRIVATE_FLAG, FUTEX_REQUEUE, FUTEX_WAIT, FUTEX_WAIT_BITSET, FUTEX_WAKE, FUTEX_WAKE_BITSET, robust_list_head, timespec, @@ -57,9 +58,10 @@ pub fn sys_futex( let futex_table = process.futex_state()?.table_for(&key); let command = futex_op & (FUTEX_CMD_MASK as u32); + let uaddr_usize = uaddr.as_ptr() as usize; match command { FUTEX_WAIT | FUTEX_WAIT_BITSET => { - if uaddr.read_vm()? != value { + if !atomic_u32_eq(uaddr_usize, value)? { return Err(KError::WouldBlock); } @@ -79,9 +81,9 @@ pub fn sys_futex( u32::MAX }; - let wait_result = futex - .wq - .wait_if(bitset, timeout, || uaddr.read_vm() == Ok(value)); + let wait_result = futex.wq.wait_if(bitset, timeout, || { + atomic_u32_eq(uaddr_usize, value).unwrap_or(false) + }); match wait_result { Ok(false) => { return Err(KError::WouldBlock); @@ -118,7 +120,7 @@ pub fn sys_futex( } FUTEX_REQUEUE | FUTEX_CMP_REQUEUE => { validate_non_negative(value)?; - if command == FUTEX_CMP_REQUEUE && uaddr.read_vm()? != value3 { + if command == FUTEX_CMP_REQUEUE && !atomic_u32_eq(uaddr_usize, value3)? { return Err(KError::WouldBlock); } let value2 = validate_non_negative(timeout_or_value2 as u32)?; diff --git a/core/kuaccess/Cargo.toml b/core/kuaccess/Cargo.toml index 40b089e0bb70fa106c654c11beebbc2c78a8e740..50fc65f191b09d5c8865a125c2be6bfc8da9687a 100644 --- a/core/kuaccess/Cargo.toml +++ b/core/kuaccess/Cargo.toml @@ -21,3 +21,4 @@ memaddr.workspace = true memspace.workspace = true osvm.workspace = true unittest.workspace = true +unittest_support.workspace = true diff --git a/core/kuaccess/docs/design.md b/core/kuaccess/docs/design.md index 5f93c3a5e84993984dbb8cf68165907bf7914868..ecff9c7b6e1a6368b6a8172c064789281ae9d21e 100644 --- a/core/kuaccess/docs/design.md +++ b/core/kuaccess/docs/design.md @@ -6,7 +6,8 @@ - 将 `osvm` 的通用虚拟内存访问入口接到当前线程地址空间; - 处理“内核在访问用户地址时允许页错误回填”的 trap 路径; -- 提供少量高频的用户态字符串装载辅助函数。 +- 提供少量高频的用户态字符串装载辅助函数; +- 提供用户态 32-bit 原子 cmpxchg 原语(供 futex 等路径使用)。 调用方包括 syscall 实现、用户线程 runtime,以及依赖 `osvm` 指针包装的其他 crate。 @@ -25,6 +26,7 @@ syscall / runtime kuaccess | \ | \-- vm_load_string* -> osvm load helpers + | \-- atomic_cmpxchg_u32 -> user_atomic_cmpxchg_u32 | \-- VirtMemIo(Vm) -> user_copy + current thread address space ``` @@ -47,9 +49,19 @@ syscall / runtime 5. `kuaccess` 消费 `MmSpace::handle_page_fault()` 返回的 typed fault outcome: `Resolved` / retry-class outcome 让 fault 指令重试;unmapped、permission、bus、 OOM、no-progress 和 generic failure 都返回 false,交给架构 exception-table - fixup 使 `user_copy` 返回失败。 + fixup 使 `user_copy` / `user_atomic_cmpxchg_u32` 返回失败。 6. 访问结束后恢复线程标志,并将失败映射为 `MemError` / `KError`。 +### 用户态原子 cmpxchg + +1. 校验 4 字节对齐与用户地址范围。 +2. 在 `IrqSave` 保护下、于 `access_user_memory()` 窗口内调用架构原语 + `user_atomic_cmpxchg_u32`。 +3. 成功时返回 `(exchanged, observed)`;fault 时由 `__ex_table` fixup 返回非零, + 映射为 `MemError::NoAccess`。 + +该原语供 futex / robust-list 等路径对用户态 futex word 做无 TOCTOU 的更新。 + ### 字符串装载 1. 从用户地址读取字节向量或 NUL 终止字节流。 @@ -58,7 +70,8 @@ syscall / runtime ## 并发模型 -- `Vm` 使用 `IrqSave` 建立访问窗口,避免访问期间的本地中断干扰。 +- `Vm` 与 `atomic_cmpxchg_u32` 使用 `IrqSave` 建立访问窗口,避免本地中断在 + exclusive/atomic 序列期间清除 monitor。 - 不维护全局共享状态;真正的并发控制由线程状态和地址空间锁负责。 ## 设计决策 diff --git a/core/kuaccess/docs/security.md b/core/kuaccess/docs/security.md index 75afd35c41238faa7c5df9f9e0811af3040cc908..bb340857d72432b88445c15c7bee3db0959a9902 100644 --- a/core/kuaccess/docs/security.md +++ b/core/kuaccess/docs/security.md @@ -15,15 +15,18 @@ - `user_copy(...)`: 依赖调用前的用户地址范围检查,以及当前线程 `accessing_user_memory` 标志保证 trap handler 只在受控窗口内接管。 +- `user_atomic_cmpxchg_u32(...)`: + 依赖 4 字节对齐检查、用户地址范围检查,以及同一用户访问窗口与 exception-table fixup。 ## 内存安全不变量 - `check_access()` 只允许访问用户空间有效区间。 +- `atomic_cmpxchg_u32()` / `atomic_load_u32()` / `atomic_u32_eq()` 额外要求地址 4 字节对齐。 - `dispatch_irq_page_fault()` 仅在当前线程显式进入用户内存访问窗口时处理 fault。 - `kuaccess` 必须保留 `MmSpace::handle_page_fault()` 的 typed outcome 分类: 只有 `Resolved` 和 retry-class outcome 可以转换为 trap handled;unmapped、 permission denied、bus error、OOM、no-progress 和 generic failure 必须转换为 - user-copy failure,而不是继续重试。 + user-copy / atomic-user 失败,而不是继续重试。 - 字符串 helper 只在成功读取完整字节流后再做 UTF-8 解释。 ## 线程安全 @@ -52,4 +55,5 @@ ## 已知限制 -- 当前 helper 只覆盖字符串装载,复杂结构体解析仍由各调用方自行组织。 +- 当前 helper 只覆盖字符串装载与 `u32` 原子 cmpxchg;复杂结构体解析仍由各调用方自行组织。 +- 原子原语目前仅提供 32-bit cmpxchg;更大宽度或其它 RMW 操作按需再加。 diff --git a/core/kuaccess/src/lib.rs b/core/kuaccess/src/lib.rs index f2cc9a642363285100b77b5be37b8ba3747ee15e..b3dc35a676b143a42b9adfca7233db0e5886e8f1 100644 --- a/core/kuaccess/src/lib.rs +++ b/core/kuaccess/src/lib.rs @@ -17,7 +17,7 @@ use extern_trait::extern_trait; use kaddr_layout::{USER_SPACE_BASE, USER_SPACE_SIZE}; use kerrno::{KError, KResult}; use khal::{ - asm::user_copy, + asm::{user_atomic_cmpxchg_u32, user_copy}, paging::MappingFlags, trap::{PAGE_FAULT, register_trap_handler}, }; @@ -102,6 +102,61 @@ pub fn vm_load_string_with_len(ptr: *const c_char, len: usize) -> KResult MemResult { + let (_, observed) = atomic_cmpxchg_u32(addr, 0, 0)?; + Ok(observed) +} + +/// Atomically tests whether `*addr == expected` without changing the word. +pub fn atomic_u32_eq(addr: usize, expected: u32) -> MemResult { + let (exchanged, _) = atomic_cmpxchg_u32(addr, expected, expected)?; + Ok(exchanged) +} + +/// Atomically compare-exchanges a 32-bit word in user memory. +/// +/// Returns `Ok((true, old))` when `*addr == old` and the store of `new` +/// succeeded. Returns `Ok((false, observed))` when the word differed from +/// `old` (no store). Returns `Err` on misalignment, out-of-range address, or +/// page fault that could not be resolved inside the user-access window. +/// +/// This is the primitive futex / robust-list paths need for race-free updates +/// of userspace futex words. +pub fn atomic_cmpxchg_u32(addr: usize, old: u32, new: u32) -> MemResult<(bool, u32)> { + if !addr.is_multiple_of(core::mem::align_of::()) { + return Err(MemError::InvalidAddr); + } + check_access(addr, core::mem::size_of::())?; + + // Match `VirtMemIo`: keep IRQs masked for the exclusive/atomic sequence so + // a local interrupt cannot clear the monitor between load and store. + let _irq = IrqSave::new(); + let mut observed = 0u32; + let failed = access_user_memory(|| { + // SAFETY: `check_access` validated the 4-byte user range, `addr` is + // 4-byte aligned, and `observed` is a live kernel stack slot. The + // call runs inside `access_user_memory`, so page faults are handled + // by the exception-table fixup on `user_atomic_cmpxchg_u32`. + unsafe { + user_atomic_cmpxchg_u32( + addr as *mut u32, + old, + new, + core::ptr::addr_of_mut!(observed), + ) + } + }); + if unlikely(failed != 0) { + Err(MemError::NoAccess) + } else { + Ok((observed == old, observed)) + } +} + #[extern_trait] // SAFETY: `Vm` validates the user range up front and performs raw copies only // inside the temporary user-access window established by `access_user_memory`. @@ -144,8 +199,12 @@ mod tests { use memspace::PageFaultOutcome; use osvm::MemError; use unittest::def_test; + use unittest_support::TestUserValue; - use super::{USER_SPACE_BASE, USER_SPACE_SIZE, check_access, fault_outcome_to_trap_result}; + use super::{ + USER_SPACE_BASE, USER_SPACE_SIZE, atomic_cmpxchg_u32, atomic_load_u32, atomic_u32_eq, + check_access, fault_outcome_to_trap_result, + }; #[def_test] fn test_check_access_valid() { @@ -177,6 +236,62 @@ mod tests { assert!(matches!(res, Err(MemError::NoAccess))); } + #[def_test] + fn test_atomic_cmpxchg_u32_rejects_misaligned() { + let res = atomic_cmpxchg_u32(USER_SPACE_BASE + 1, 0, 1); + assert!(matches!(res, Err(MemError::InvalidAddr))); + } + + #[def_test] + fn test_atomic_cmpxchg_u32_rejects_out_of_range() { + let res = atomic_cmpxchg_u32(USER_SPACE_BASE - 4, 0, 1); + assert!(matches!(res, Err(MemError::NoAccess))); + } + + #[def_test(custom)] + fn test_atomic_cmpxchg_u32_match_updates_user_word() { + let word = TestUserValue::::from_value(10).unwrap(); + let addr = word.as_user_ptr() as usize; + + let res = atomic_cmpxchg_u32(addr, 10, 20).unwrap(); + assert_eq!(res, (true, 10)); + assert_eq!(word.read(), 20); + } + + #[def_test(custom)] + fn test_atomic_cmpxchg_u32_mismatch_leaves_user_word() { + let word = TestUserValue::::from_value(10).unwrap(); + let addr = word.as_user_ptr() as usize; + + let res = atomic_cmpxchg_u32(addr, 5, 20).unwrap(); + assert_eq!(res, (false, 10)); + assert_eq!(word.read(), 10); + } + + #[def_test(custom)] + fn test_atomic_load_u32_reads_user_word() { + let word = TestUserValue::::from_value(42).unwrap(); + let addr = word.as_user_ptr() as usize; + + assert_eq!(atomic_load_u32(addr).unwrap(), 42); + } + + #[def_test(custom)] + fn test_atomic_u32_eq_matches_user_word() { + let word = TestUserValue::::from_value(7).unwrap(); + let addr = word.as_user_ptr() as usize; + + assert!(atomic_u32_eq(addr, 7).unwrap()); + assert!(!atomic_u32_eq(addr, 8).unwrap()); + } + + #[def_test(custom)] + fn test_atomic_cmpxchg_u32_faults_on_unmapped_user_addr() { + let unmapped = USER_SPACE_BASE + 0x1000; + let res = atomic_cmpxchg_u32(unmapped, 0, 1); + assert!(matches!(res, Err(MemError::NoAccess))); + } + #[def_test] fn test_check_access_rejects_far_above_user_space() { let res = check_access(USER_SPACE_BASE + USER_SPACE_SIZE + 0x1000, 1); diff --git a/posix/process/Cargo.toml b/posix/process/Cargo.toml index e3a54feeffbec616c0f5214470e2a5d71757f7f8..8b2611e5aa6d7f5189f51102c74c600066016217 100644 --- a/posix/process/Cargo.toml +++ b/posix/process/Cargo.toml @@ -23,6 +23,7 @@ khal.workspace = true kidentity.workspace = true klogger.workspace = true kprocess.workspace = true +kuaccess.workspace = true ksignal.workspace = true ksync.workspace = true ktask.workspace = true diff --git a/posix/process/src/runtime.rs b/posix/process/src/runtime.rs index 1c5e0393c61219d712f0915ccf7a9235079b6673..9c71194784ea2513230cb0d83535df01610b10e4 100644 --- a/posix/process/src/runtime.rs +++ b/posix/process/src/runtime.rs @@ -13,12 +13,13 @@ use kerrno::{KError, KResult, LinuxError}; use khal::uspace::{ExceptionKind, ReturnReason, UserContext}; use kidentity::PidHandle; use kprocess::{ - CpuTimeState, Pid, Thread, UserThreadRuntimeAction, current_futex_key, current_user_process, - current_user_thread, poll_cpu_timers, process_exit, process_signals, + CpuTimeState, Pid, Thread, Tid, UserThreadRuntimeAction, current_futex_key, + current_user_process, current_user_thread, poll_cpu_timers, process_exit, process_signals, }; use ksignal::{SignalInfo, SignalOSAction, SignalSet, Signo}; use ktask::{TaskInner, current}; -use linux_raw_sys::general::ROBUST_LIST_LIMIT; +use kuaccess::{atomic_cmpxchg_u32, atomic_load_u32}; +use linux_raw_sys::general::{FUTEX_OWNER_DIED, FUTEX_TID_MASK, FUTEX_WAITERS, ROBUST_LIST_LIMIT}; use linux_sysno::Sysno; use memspace::PageFaultOutcome; use osvm::{VirtMutPtr, VirtPtr}; @@ -155,11 +156,63 @@ struct RobustListHead { list_op_pending: *mut RobustList, } -fn dispatch_irq_futex_death(entry: *mut RobustList, offset: i64) -> KResult<()> { +/// Userspace robust-mutex lock word (`u32` futex value). +/// +/// Layout matches the Linux robust-futex ABI: TID in the low bits, plus +/// `FUTEX_OWNER_DIED` / `FUTEX_WAITERS` in the high bits. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(transparent)] +struct RobustFutexWord(u32); + +impl RobustFutexWord { + fn from_bits(bits: u32) -> Self { + Self(bits) + } + + fn bits(self) -> u32 { + self.0 + } + + fn tid(self) -> Tid { + self.0 & FUTEX_TID_MASK + } + + fn is_owner_died(self) -> bool { + self.0 & FUTEX_OWNER_DIED != 0 + } + + /// Preserve `WAITERS`, clear the TID, and set `OWNER_DIED`. + fn with_owner_died(self) -> Self { + Self((self.0 & FUTEX_WAITERS) | FUTEX_OWNER_DIED) + } +} + +fn mark_futex_owner_died(address: usize, tid: Tid) -> KResult { + loop { + let observed = RobustFutexWord::from_bits(atomic_load_u32(address).map_err(KError::from)?); + if observed.tid() != tid { + return Ok(false); + } + if observed.is_owner_died() { + return Ok(true); + } + let newval = observed.with_owner_died(); + let (exchanged, _) = + atomic_cmpxchg_u32(address, observed.bits(), newval.bits()).map_err(KError::from)?; + if exchanged { + return Ok(true); + } + } +} + +fn dispatch_irq_futex_death(entry: *mut RobustList, offset: i64, tid: Tid) -> KResult<()> { let address = (entry as u64) .checked_add_signed(offset) .ok_or(KError::InvalidInput)?; let address: usize = address.try_into().map_err(|_| KError::InvalidInput)?; + if !mark_futex_owner_died(address, tid)? { + return Ok(()); + } let key = current_futex_key(address); let futex_state = current_user_process().futex_state()?; @@ -174,7 +227,7 @@ fn dispatch_irq_futex_death(entry: *mut RobustList, offset: i64) -> KResult<()> } /// Process robust futex list on thread exit and wake waiting threads. -fn exit_robust_list(head: *const RobustListHead) { +fn exit_robust_list(head: *const RobustListHead, tid: Tid) { let mut limit = ROBUST_LIST_LIMIT; // SAFETY: `head` comes from the task's registered robust-list head, and we @@ -191,7 +244,7 @@ fn exit_robust_list(head: *const RobustListHead) { let Some(next_entry) = entry.read_vm().map(|node| node.next).ok() else { return; }; - if entry != pending && dispatch_irq_futex_death(entry, offset).is_err() { + if entry != pending && dispatch_irq_futex_death(entry, offset, tid).is_err() { return; } entry = next_entry; @@ -204,7 +257,7 @@ fn exit_robust_list(head: *const RobustListHead) { } if !pending.is_null() { - let _ = dispatch_irq_futex_death(pending, offset); + let _ = dispatch_irq_futex_death(pending, offset, tid); } } @@ -229,7 +282,7 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { let head = thr.robust_list_head() as *const RobustListHead; if !head.is_null() { - exit_robust_list(head); + exit_robust_list(head, thr.tid()); } // Per-thread TEE session cleanup when this thread holds a session context.