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.