From 256e40ec05109e72e7aba72c4db35056efe47305 Mon Sep 17 00:00:00 2001 From: wangyining Date: Thu, 13 Aug 2026 17:54:01 +0800 Subject: [PATCH 1/3] fix(process): release process-owned resources before publishing exit --- core/ksyscall/src/io_mpx/select.rs | 2 +- core/ksyscall/src/task/pidfd.rs | 2 +- fs/filesystems/procfs/src/task_nodes/root.rs | 6 +- posix/fs/src/fd_ops.rs | 6 +- posix/process/docs/design.md | 8 +- posix/process/docs/security.md | 8 +- posix/process/src/init_process.rs | 1 + posix/process/src/runtime.rs | 14 +- process/kfd/docs/design.md | 33 ++-- process/kfd/docs/security.md | 18 +-- process/kfd/src/fd_table.rs | 26 ++-- process/kfd/src/lib.rs | 40 ++++- process/kprocess/docs/design.md | 40 +++-- process/kprocess/docs/security.md | 5 + .../kprocess/src/process/runtime_access.rs | 83 ++++++++--- process/kprocess/src/process_exit.rs | 3 + process/kprocess/src/process_runtime/mod.rs | 77 ++++++---- .../src/process_runtime/runtime_state.rs | 4 +- process/kprocess/src/tests.rs | 101 +++++++++++-- process/kresources/src/lib.rs | 141 +++++++++++++----- 20 files changed, 452 insertions(+), 166 deletions(-) diff --git a/core/ksyscall/src/io_mpx/select.rs b/core/ksyscall/src/io_mpx/select.rs index e94e068bd..86f5a9a67 100644 --- a/core/ksyscall/src/io_mpx/select.rs +++ b/core/ksyscall/src/io_mpx/select.rs @@ -71,7 +71,7 @@ fn do_select( ); let resources = kprocess::current_resources(); - let fd_table = resources.fd_table(); + let fd_table = resources.fd_table()?; let fd_table = fd_table.read(); let mut fds = Vec::with_capacity(nfds); let mut fd_indices = Vec::with_capacity(nfds); diff --git a/core/ksyscall/src/task/pidfd.rs b/core/ksyscall/src/task/pidfd.rs index 0bacde16b..6a531d728 100644 --- a/core/ksyscall/src/task/pidfd.rs +++ b/core/ksyscall/src/task/pidfd.rs @@ -37,7 +37,7 @@ pub fn sys_pidfd_getfd(pidfd: i32, target_fd: i32, flags: u32) -> KResult pidfd .live_process()? .resources()? - .fd_table() + .fd_table()? .read() .get(target_fd as usize) .ok_or(KError::BadFileDescriptor) diff --git a/fs/filesystems/procfs/src/task_nodes/root.rs b/fs/filesystems/procfs/src/task_nodes/root.rs index 1cc3095e2..382067b57 100644 --- a/fs/filesystems/procfs/src/task_nodes/root.rs +++ b/fs/filesystems/procfs/src/task_nodes/root.rs @@ -450,8 +450,10 @@ impl SimpleDirOps for ThreadFdDir { let Ok(resources) = task.as_thread().process().resources() else { return Box::new(iter::empty()); }; - let ids = resources - .fd_table() + let Ok(fd_table) = resources.fd_table() else { + return Box::new(iter::empty()); + }; + let ids = fd_table .read() .ids() .map(|id| Cow::Owned(id.to_string())) diff --git a/posix/fs/src/fd_ops.rs b/posix/fs/src/fd_ops.rs index 833ec4484..9602a9478 100644 --- a/posix/fs/src/fd_ops.rs +++ b/posix/fs/src/fd_ops.rs @@ -48,13 +48,13 @@ pub fn sys_close_range(first: i32, last: i32, flags: u32) -> KResult { let resources = kprocess::current_user_process().resources()?; if flags.contains(CloseRangeFlags::UNSHARE) { - resources.unshare_fd_table(); + resources.unshare_fd_table()?; } if flags.contains(CloseRangeFlags::CLOEXEC) { - resources.set_cloexec_range(first, last); + resources.set_cloexec_range(first, last)?; } else { - resources.close_range(first, last); + resources.close_range(first, last)?; } Ok(0) diff --git a/posix/process/docs/design.md b/posix/process/docs/design.md index 2577cde24..f154f89b5 100644 --- a/posix/process/docs/design.md +++ b/posix/process/docs/design.md @@ -60,14 +60,18 @@ entry / ksyscall 1. 清理 `clear_child_tid` 并唤醒 futex。 2. 遍历 robust futex list,标记 owner-dead。 3. 从进程线程集合中摘除当前线程。 -4. 若为最后线程,关闭 fd、通知父进程、清理共享内存和可选 TEE 私有状态。 -5. 若触发 group exit,向线程组广播 `SIGKILL`。 +4. 若为最后线程,释放 mm owner,再清理共享内存和可选 TEE 私有状态。 +5. 依次释放 files、filesystem context 和 namespace owner。 +6. owner 全部释放后发布 process exit 并通知父进程。 +7. 若触发 group exit,向线程组广播 `SIGKILL`。 ## 并发模型 - 线程/进程基础状态由 `kprocess` 和其内部锁保护。 - 本 crate 负责组织退出与信号路径的调用顺序,不重复持有额外全局状态。 - robust futex owner-dead 标志通过原子位和等待队列协作。 +- owner slot 的 `take()` 与 capability 查询由各自 runtime 锁串行化;取出的对象在 + slot 锁外 drop,避免资源析构重入 owner 锁。 ## 设计决策 diff --git a/posix/process/docs/security.md b/posix/process/docs/security.md index 3fbb43f17..417b4dffa 100644 --- a/posix/process/docs/security.md +++ b/posix/process/docs/security.md @@ -27,7 +27,8 @@ `new_user(...)` 在 task 构造时一次性装入 `UserRuntimeSlot`,再经 `publish_user_task(...).commit(...)` 发布到 process registry 后才激活,不存在 runnable 后补装 runtime 的路径。 -- 最后线程退出前必须先关闭 fd,再标记进程退出,避免外部等待者持有悬挂资源语义。 +- 最后线程必须先取走 mm、fd table、`FsStruct` 和 `NsProxy` owner,再发布进程退出; + 已退出进程的 capability 查询必须返回 `NoSuchProcess`。 - `SHM_MANAGER` 清理仅针对已退出进程 PID。 ## 线程安全 @@ -48,7 +49,10 @@ ## 故障模式与影响分析(FMEA) -- 退出路径漏关 fd:会破坏 pipe EOF / wait 语义;当前实现先 `close_all_fds()`。 +- 退出路径漏放 files owner:会破坏 pipe EOF / wait 语义;当前实现用 `exit_files()` + 取走本进程 owner,共享 fd table 在最后 owner 释放时关闭。 +- 退出路径漏放 `FsStruct`/`NsProxy`:会让 `Path -> Mount` 跨 zombie 生命周期存活; + 当前最后线程在父进程可观察退出前完成两者 detach。 - 父进程通知丢失:通过退出信号和 `child_exit_event()` 双路径通知。 - group-exit 未广播:会留下残余线程;当前实现遍历线程组发 `SIGKILL`。 - init 进程启动前缺少 user runtime:会导致用户线程 runtime 前提失效;当前 PID 1 路径在进入用户态前校验 identity、安装 runtime、发布 process/task 可见性,并同步当前页表。 diff --git a/posix/process/src/init_process.rs b/posix/process/src/init_process.rs index 0d1ffd1c4..5bd1585d0 100644 --- a/posix/process/src/init_process.rs +++ b/posix/process/src/init_process.rs @@ -126,6 +126,7 @@ pub fn spawn_init_process( .resources() .expect("init process must have live resources") .fd_table() + .expect("init process must have a live fd table") .write(), &fs_context, ) diff --git a/posix/process/src/runtime.rs b/posix/process/src/runtime.rs index 371e7f4e9..d3026309e 100644 --- a/posix/process/src/runtime.rs +++ b/posix/process/src/runtime.rs @@ -334,8 +334,8 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { process_exit::record_exited_thread_cpu_time(process, thread_utime, thread_stime); if is_last_thread { - if let Err(err) = process.close_all_fds() { - error!("close_all_fds on process exit failed: {err:?}"); + if let Err(err) = process.exit_mm() { + error!("exit_mm on process exit failed: {err:?}"); } // Detach shared memory before waking the parent, so that @@ -347,8 +347,14 @@ pub fn do_exit(exit_code: i32, group_exit: bool) { error!("clear_tee_runtime_private on process exit failed: {err:?}"); } - if let Err(err) = process.clear_exclusive_address_space() { - error!("clear address space on process exit failed: {err:?}"); + if let Err(err) = process.exit_files() { + error!("exit_files on process exit failed: {err:?}"); + } + if let Err(err) = process.exit_fs() { + error!("exit_fs on process exit failed: {err:?}"); + } + if let Err(err) = process.exit_namespaces() { + error!("exit_namespaces on process exit failed: {err:?}"); } process_exit::complete_process_exit(process); diff --git a/process/kfd/docs/design.md b/process/kfd/docs/design.md index f759aeea6..a6f4eb27c 100644 --- a/process/kfd/docs/design.md +++ b/process/kfd/docs/design.md @@ -45,7 +45,7 @@ posix/fs, posix/net, posix/mm, io-mpx v ┌─────────────────────────────────────────────┐ │ kresources │ -│ Arc> │ +│ Option>> │ └──────────────────┬──────────────────────────┘ │ read/write lock v @@ -90,7 +90,7 @@ Open(updated cloexec) │ duplicate_to ├──────────────► Open at new fd (Arc cloned) │ - │ file_close_fd_locked / remove_range / remove_cloexec_files / remove_all_if_unshared + │ file_close_fd_locked / remove_range / remove_cloexec_files / final table drop v Free ``` @@ -121,23 +121,23 @@ Open 再逐个删除。 这样可以避免边遍历 `FlattenObjects` 边修改同一结构。 -### fd table 共享和关闭 +### fd table 共享和退出关闭 ```text -Arc strong_count > 1 - │ remove_all_if_unshared +Process A files ─┐ + ├──> Arc> +Process B files ─┘ + │ first exit_files(): take this process owner v -unchanged - -Arc strong_count == 1 - │ remove_all_if_unshared +table remains live for Process B + │ final exit_files() v -all descriptors removed, lock dropped before descriptor close +FdTable::drop(): remove and close all descriptors ``` -该路径用于进程资源释放。 -当 fd table 仍被其他进程或线程共享时, -不会关闭所有 fd。 +进程退出只释放自己的 files owner,不根据瞬时 `Arc::strong_count` 猜测是否应该 +关闭共享表。最后一个 owner 消失时,`FdTable::drop()` 关闭剩余 descriptor;已经 +取得的临时 table capability 也按同一所有权规则延迟 final close。 ## 算法流程 @@ -210,9 +210,10 @@ snapshot 创建后,原 fd 可以被关闭或复用, - 查找 fd、读取 `cloexec`:调用方持读锁即可。 - 创建 `FdSnapshot`:调用方持读锁,克隆 `Arc` 后释放锁。 - 添加、删除、dup、设置 `cloexec`、close range:调用方必须持写锁。 -- `remove_all_if_unshared` 在持写锁时移除 descriptor, - 把移除结果暂存在 `Vec` 中, - 然后由 `ProcessResources` 在表锁外关闭 `FileDescriptor`。 +- 普通 close/exec/range 路径在持写锁时只移除 descriptor,再由 + `ProcessResources` 在表锁外关闭。 +- final `FdTable::drop()` 只会在外层 `RwLock` 的所有 `Arc` 都已释放后运行; + 此时表已经不可访问,可以直接 drain 并关闭剩余 descriptor。 这样可以避免 `FileLike::drop` 或底层对象释放路径在持有 fd table 写锁时重入 fd 表。 diff --git a/process/kfd/docs/security.md b/process/kfd/docs/security.md index ad56a3186..ddb3ded91 100644 --- a/process/kfd/docs/security.md +++ b/process/kfd/docs/security.md @@ -16,7 +16,7 @@ syscall layer │ v kresources - │ owns Arc> + │ owns Option>> │ v ┌─────────────────────────────────────────────┐ @@ -114,9 +114,9 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; snapshot 不是 fd table 的实时视图。 4. **fd table 修改独占**: 所有插入、删除和 flag 修改都要求调用方持有 `RwLock` 写锁。 -5. **drop 不在表锁内执行**: - `remove_all_if_unshared` 先从表中取出 descriptor, - 释放写锁后再关闭,降低释放路径重入风险。 +5. **关闭不持有仍可访问的表锁**: + 普通 close 路径先从表中取出 descriptor,释放写锁后再关闭;final table drop + 只在外层 `RwLock` 的所有 `Arc` 已消失后处理剩余 descriptor。 6. **ABI 结构不泄露未初始化数据**: `stat` / `statx` 转换先全零初始化, 再填充字段。 @@ -147,7 +147,7 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; | T-08 | `stat` / `statx` 未初始化字段泄露内核内存 | 高 | 直接构造 ABI 结构但未清零 reserved 字段 | 转换先 `zeroed()`,保留字段保持 0 | | T-09 | 低层槽位 API 被外部绕过资源策略或 descriptor flag 规则 | 中 | `add`、`add_at`、`remove`、`get_mut` 作为跨 crate API 暴露 | 已将这些 helper 收窄为 `pub(crate)`;外部路径使用高层 API | | T-10 | `FileLike` 默认方法返回值掩盖不支持操作 | 低 | 具体实现未覆盖 read/write/ioctl/mmap | 默认返回 `InvalidInput`、`NotATty` 或 `NoSuchDevice`;调用者按 errno 处理 | -| T-11 | `Arc::strong_count` 判断期间出现新的共享者 | 中 | `remove_all_if_unshared` 与 fd table 引用复制并发 | 调用方的进程资源替换路径应串行化 fd table Arc 的发布;函数只在 strong count 为 1 时关闭 | +| T-11 | 共享 fd table 永远不执行 final close | 中 | 退出进程保留自己的 table owner,仅在瞬时非共享时尝试清空 | `exit_files()` 无条件取走当前进程 owner;最后一个 `Arc` 释放时由 `FdTable::drop()` 关闭剩余 descriptor | | T-12 | procfs 或 exec 路径把 `FdSnapshot` 当成 live fd 权限 | 中 | snapshot 创建后原 fd 被关闭、复用或 flag 改变 | snapshot 只表示创建时的 open object;需要 live fd 状态的 syscall 必须重新查 fd table | 影响等级定义: @@ -208,9 +208,9 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; 3. **`FileLike` errno 语义由实现者补充**: trait 默认方法只描述通用不支持操作, 具体对象仍需在自身 crate 中说明 read/write/ioctl/mmap 的 errno 细节。 -4. **`remove_all_if_unshared` 依赖发布侧串行化**: - `Arc::strong_count` 只能表达当前引用数, - 不能替代进程资源层对 fd table 替换的同步。 +4. **外部 table capability 可延迟 final close**: + procfs、select 等路径在退出前取得的 `Arc>` 会保持表存活, + 但退出后的新查询会返回 `NoSuchProcess`,且 capability 释放后仍会执行 final close。 ## 其它说明(模板章节) @@ -234,4 +234,4 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; - [ ] 新增 `FileLike` 默认方法不会把不支持操作伪装成成功。 - [ ] `stat` / `statx` ABI 结构新增字段时保留字段仍清零。 - [ ] exec 路径继续调用 `close_cloexec_files`。 -- [ ] 资源层替换 fd table 时保持 `Arc>` 发布同步。 +- [ ] 资源层替换或释放 fd table 时保持 owner slot 原子切换,且不能在退出后重新安装。 diff --git a/process/kfd/src/fd_table.rs b/process/kfd/src/fd_table.rs index 9ee15bfe1..786f66f7f 100644 --- a/process/kfd/src/fd_table.rs +++ b/process/kfd/src/fd_table.rs @@ -217,21 +217,19 @@ impl FdTable { } removed } +} - /// Removes all descriptors when the table is not shared. - pub fn remove_all_if_unshared(fd_table: &Arc>) -> Vec { - if Arc::strong_count(fd_table) > 1 { - return Vec::new(); - } - - let mut table = fd_table.write(); - let ids: Vec = table.ids().collect(); - let mut removed = Vec::with_capacity(ids.len()); - for id in ids { - if let Some(descriptor) = table.remove(id) { - removed.push(descriptor); - } +impl Drop for FdTable { + fn drop(&mut self) { + // Final owner teardown cannot return close errors. Keep closing so one + // failed flush does not retain the remaining descriptors. + loop { + let next_fd = self.ids().next(); + let Some(fd) = next_fd else { + break; + }; + let descriptor = self.remove(fd).expect("descriptor id must remain present"); + let _ = descriptor.close(); } - removed } } diff --git a/process/kfd/src/lib.rs b/process/kfd/src/lib.rs index 8d21affb4..ab80838b6 100644 --- a/process/kfd/src/lib.rs +++ b/process/kfd/src/lib.rs @@ -20,9 +20,12 @@ pub use self::{ #[cfg(unittest)] mod tests { + use alloc::sync::Arc; + use core::sync::atomic::{AtomicUsize, Ordering}; + use kpoll::IoEvents; use ktime_types::SystemTime; - use kvfs::{AnonInodeFs, DeviceId, FMode, FileOperations, OpenFlags, VfsFile}; + use kvfs::{AnonInodeFs, DeviceId, FMode, FileOperations, OpenFlags, VfsFile, VfsResult}; use linux_raw_sys::general::stat; use unittest::def_test; @@ -36,6 +39,15 @@ mod tests { } } + struct FlushCountFops(Arc); + + impl FileOperations for FlushCountFops { + fn flush(&self, _file: &VfsFile) -> VfsResult<()> { + self.0.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + } + fn snapshot_test_file() -> alloc::sync::Arc { AnonInodeFs::global() .get_file( @@ -49,6 +61,19 @@ mod tests { .expect("snapshot test anon inode file opens") } + fn flush_count_test_file(flushes: Arc) -> Arc { + AnonInodeFs::global() + .get_file( + "[fd-table-drop-test]", + Arc::new(FlushCountFops(flushes)), + Arc::new(()), + FMode::READ, + OpenFlags::empty(), + kcred::initial_cred(), + ) + .expect("fd table drop test file opens") + } + #[def_test] fn test_kstat_default() { let kstat = Kstat::default(); @@ -123,4 +148,17 @@ mod tests { Err(kerrno::KError::BadFileDescriptor) )); } + + #[def_test] + fn test_fd_table_drop_closes_remaining_descriptors() { + let flushes = Arc::new(AtomicUsize::new(0)); + let mut table = FdTable::default(); + table + .add_file(16, flush_count_test_file(flushes.clone()), false) + .unwrap(); + + drop(table); + + assert_eq!(flushes.load(Ordering::Relaxed), 1); + } } diff --git a/process/kprocess/docs/design.md b/process/kprocess/docs/design.md index d732bb612..1e1986350 100644 --- a/process/kprocess/docs/design.md +++ b/process/kprocess/docs/design.md @@ -91,12 +91,21 @@ Process │ └─ group / member_slot ├─ pid_publication_slot └─ runtime_ref: Option> - │ - ├─ Thread - │ ├─ real_cred: RwLock> - │ └─ cred: RwLock> │ v + ProcessRuntime + ├─ mm_user: Option + ├─ resources.fd_table: Option>> + ├─ fs_context: Option>> + └─ nsproxy: Option> + ^ + │ + Thread + ├─ process: Arc + ├─ runtime: Arc + ├─ real_cred: RwLock> + └─ cred: RwLock> + ProcessGroup ├─ processes: BTreeMap> └─ session: Arc @@ -141,10 +150,12 @@ Created ──publish_task──> Running ──exit_thread(last)──> Exiting |----|----|----------| | `Created` | `Running` | publication 阶段把已准备好的 task 发布到 `Process` 自有线程成员表 | | `Running` | `Running` | 非最后一个线程调用 `exit_thread` | -| `Running` | `Zombie` | 最后一个线程退出后调用 `Process::exit`,且 child-exit 策略要求父进程 wait | -| `Running` | `Dead/Reaped` | 最后一个线程退出后 child-exit 策略要求 autoreap | +| `Running` | `Exiting` | 最后一个线程离开线程成员表,开始释放进程 owner | +| `Exiting` | `Zombie` | owner 已释放,且 child-exit 策略要求父进程 wait | +| `Exiting` | `Dead/Reaped` | owner 已释放,且 child-exit 策略要求 autoreap | | `Zombie` | `Dead/Reaped` | 父进程 wait 路径调用 `free` 或 `wait_reap` 获得单赢家 | +`Exiting` 是最后一个线程移除后、退出状态发布前的控制流阶段,不增加独立的状态字段。 `Process::exit` 对 init 进程直接返回。 普通进程退出时设置退出状态,并把子进程 reparent 到 init 进程。 `free` 只允许已退出进程调用,并从父进程 children 表移除当前进程。 @@ -231,15 +242,22 @@ path + 旧 argv 的混合状态。 之前,外部 `lookup::task(tid)`、`tgkill`、按 TID 的定时器投递都会看到 `NoSuchProcess`;这是有意的可见性取舍,避免退出尾段仍被当作可命中目标, 并防止 Published(dead Weak) TID 槽位滞留。 -2. 最后一个线程退出后,`posix-process` runtime glue 经 `process_exit` 语义模块触发稳定 `Process` 的 exited-state 转换。 -3. `Process` 内部在 process-domain 临界区内设置 `Zombie` 或 `Dead` 退出状态, +2. 最后一个线程先释放 `MmUserHandle`,完成共享内存和进程私有状态清理,再依次 + 从 runtime 取走 fd table、`FsStruct` 和 `NsProxy` owner。每个 owner slot 都可 + 独立置空,重复释放安全返回;后续 capability 查询返回 `NoSuchProcess`。 +3. owner 释放完成后,`posix-process` 才通过 `process_exit` 发布稳定 `Process` 的 + exited state。`Process` 内部在 process-domain 临界区内设置 `Zombie` 或 `Dead`, 将所有子进程 reparent 到 init,并同时更新旧 parent children、新 parent children、child parent link 和 orphan 的退出通知 signal。reparent 会把 orphan 的退出通知 signal 重置为 `SIGCHLD`,避免 init 继承非 `SIGCHLD` clone-child 通知语义。 4. 默认 SIGCHLD 语义下,waitable zombie 之后该 `Process` 仍保持 published 身份,继续承担 wait/pidfd/reap 语义;与此同时,外部 `live` 查询必须开始把它视为不可操作对象。 -5. 弱 runtime 引用不在 zombie 转换时主动清除,而是允许当前退出线程在尾段继续通过稳定 `Process` 访问其已持有的运行态资源;其生命周期最终由 `Thread -> Arc` 强引用自然结束。 -6. 最后一个线程在发布 zombie 前释放当前 runtime 持有的 `memspace::process_lifetime::MmUserHandle`;当这是最后一个 active mm user 时同步释放 VMA 和用户页资源。共享 VM 场景通过从父 runtime 的 handle 派生新 user 继续持有,普通 `Arc` observer 或 `MmPin` 不参与该判定。 +5. 弱 runtime 引用不在 zombie 转换时主动清除,但 runtime 中的 mm/files/fs/ns + owner 已经置空。当前退出线程可继续持有 runtime 壳完成尾段,zombie/reaper identity + 不会因此继续固定 VFS path 或 mount。 +6. 当退出线程释放的是最后一个 active mm user 时,同步释放 VMA 和用户页资源。 + 共享 VM 场景通过从父 runtime 的 handle 派生新 user 继续持有,普通 + `Arc` observer 或 `MmPin` 不参与该判定。 7. 退出路径在父进程可观察前,先把当前线程最终 CPU time 累计到 `ProcessLifecycleState`,并通过本进程 `exit_event` 通知 pidfd/poll 观察者。 8. child-exit 通知对齐 Linux `do_notify_parent()`:默认忽略的 SIGCHLD 仍会排队;显式 `SIG_IGN` 或 `SA_NOCLDWAIT` 请求 autoreap。`SA_NOCLDWAIT` 保持 Linux 行为,除非同时显式 `SIG_IGN`,否则仍发送 SIGCHLD。发送给父进程的 signal 使用 child-exit `siginfo_t` layout,而不是普通 `SI_KERNEL`:`si_code` 从 wait status 映射为 `CLD_EXITED`、`CLD_KILLED` 或 `CLD_DUMPED`,`si_status` 携带退出码或终止信号,并填充 child PID、real UID、用户态/内核态 CPU clock ticks。非 SIGCHLD 的 clone exit signal 也沿用同一 child-exit payload,只替换 `si_signo`。 9. 对 SIGCHLD 退出,运行时先在 process-domain read side 采样 @@ -421,6 +439,8 @@ subreaper 尚未实现,代码保留 TODO。 - `Process` 没有自定义 `Drop`,生命周期由 `Arc` 引用计数控制。 - 父进程 children 表持有子进程强引用,`free` 或 autoreap 从父表移除已退出子进程后释放这条所有权边。 - 用户进程的大块地址空间资源不依赖 `ktask` GC;最后一个 runtime `MmUserHandle` 释放时会同步清理 `MmSpace` 的用户映射。 +- fd table、`FsStruct` 和 `NsProxy` 不等待整个 `ProcessRuntime` drop;最后线程在 + exited-state 发布前取走对应 owner。共享对象由最后一个 `Arc` owner 自然释放。 - 需要访问 live 用户映射的路径通过 `Process::address_space()` 进入,该入口要求 runtime 仍能派生 active `MmUserHandle`;退出清理后需要观察稳定 mm identity 或 空 VMA 状态的内部路径必须显式使用 teardown-observation pinned address-space 入口。 diff --git a/process/kprocess/docs/security.md b/process/kprocess/docs/security.md index 9a77c12c0..7f524673d 100644 --- a/process/kprocess/docs/security.md +++ b/process/kprocess/docs/security.md @@ -43,6 +43,8 @@ kprocess / posix/process / ksyscall / ktty completion 归属于 `Process`,不依赖 `ProcessRuntime` 是否仍可升级。 7. **弱 runtime 引用非拥有**:`Process` 只保存 `Weak`, 不延长 runtime 生命周期;upgrade 失败时由上层折叠为 `NoSuchProcess` 等语义错误。 + runtime 内的 files、`FsStruct` 和 `NsProxy` owner 可在 runtime 对象仍存活时独立置空, + capability accessor 必须同时检查对应 owner 是否存在。 8. **live 语义独立于弱 runtime 引用**:外部 `live process` 以 exited state 为准, 不允许把“runtime 还没释放”误判成“进程仍然活着”。 9. **publication 原子可见性**:task/process/group/session 目录在同一 publication 锁下更新, @@ -90,6 +92,7 @@ kprocess / posix/process / ksyscall / ktty | T-18 | 退出进程的大块用户内存释放依赖普通 GC 任务调度 | 高 | fork/exec 风暴中 GC 任务迟迟不运行,已退出进程的地址空间资源堆积 | runtime 持有 `memspace::process_lifetime::MmUserHandle`;最后一个 handle 释放时同步清理 `MmSpace` 的用户映射,普通 `Arc` observer 或 `MmPin` 不保留映射 | | T-19 | 父进程显式忽略 SIGCHLD 后 zombie 泄漏或被 wait 抢先回收 | 中 | 父进程设置 `SIGCHLD` 为 `SIG_IGN` 或 `SA_NOCLDWAIT`,child exit 与 parent wait / signal handler 并发 | child-exit 通知先准备 autoreap/queue 决策;autoreap child 跳过 waitable zombie 状态,先撤销 children/PID 身份,再提交 typed SIGCHLD payload,并在提交时按当前线程 mask 选择唤醒目标 | | T-20 | 失效 PID/TID 目录槽位无限保留 | 高 | wait/exit 只 retire slot 却不从 `BTreeMap` 删除,fork 密集工作负载累积数百 MiB RustHeap | `unpublish_task_if_matches`/`unpublish_process_if_matches` 在 retire 前用 `Arc::ptr_eq` 校验发布身份,再删除仍指向同一 cleanable slot 的目录项;复用后的 Reserved/Published 新身份不会被旧退出路径误退休 | +| T-21 | zombie 或 reaper identity 继续固定 VFS mount | 高 | exited-state 已发布,但 runtime 的 fd table、`FsStruct` 或 `NsProxy` owner 仍存在 | 最后线程先取走 mm/files/fs/ns owner,再发布 exited state;空 owner 的 accessor 返回 `NoSuchProcess` | 影响等级定义: @@ -159,6 +162,8 @@ signal、scheduler 和 job-control 路径读取,调用者需要在上层执行 - 新增凭据修改是否遵循 prepare/check/commit,且失败时不替换 committed `Arc`。 - 需要文件权限的调用是否在 syscall 入口取得一次快照,而不是让下层反向查询 current task。 - 新增 current-thread 尾段路径是否仍可通过稳定 `Process` 访问所需 runtime capability,且不会把已退出进程重新暴露为 live。 +- 新增退出 capability 是否在 exited-state 发布前取走;files、fs、namespace accessor + 是否在 owner 已空时拒绝访问,且 owner drop 是否发生在对应 slot 锁外。 - 新增地址空间退出清理是否只在最后一个 runtime `MmUserHandle` 释放时发生,且不得被普通 `Arc` observer 或 `MmPin` 阻塞或破坏 `CLONE_VM` 共享方。 - 新增用户映射访问路径是否走 live address-space 入口;退出后仅需观察 mm 对象的路径是否显式使用 teardown-observation pinned 入口,避免把 `MmPin` 当成 live user capability。 - 新增 controlling terminal 行为是否保持 set-once 和 pointer-match unset 语义。 diff --git a/process/kprocess/src/process/runtime_access.rs b/process/kprocess/src/process/runtime_access.rs index ddef0bad7..77b80a7a2 100644 --- a/process/kprocess/src/process/runtime_access.rs +++ b/process/kprocess/src/process/runtime_access.rs @@ -145,20 +145,37 @@ impl Process { self.runtime().map(|runtime| runtime.resources().clone()) } - /// Returns the filesystem context while runtime remains attached. + /// Returns the attached filesystem context. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the runtime or filesystem owner + /// has been released. pub fn fs_context(&self) -> KResult>> { - self.runtime().map(|runtime| runtime.fs_context()) + self.runtime()?.fs_context().ok_or(KError::NoSuchProcess) } - /// Returns the process UTS namespace while runtime remains attached. + /// Returns the process UTS namespace while its namespace owner is attached. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the runtime or namespace owner + /// has been released. pub fn uts_ns(&self) -> KResult> { - self.runtime().map(|runtime| runtime.uts_ns()) + self.runtime()?.uts_ns().ok_or(KError::NoSuchProcess) } - /// Returns the process mount namespace while runtime remains attached. + /// Returns the process mount namespace while its namespace owner is attached. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the runtime or namespace owner + /// has been released. pub fn mnt_ns(&self) -> KResult> { - self.runtime() - .map(|runtime| runtime.nsproxy().mnt_ns().clone()) + self.runtime()? + .nsproxy() + .map(|nsproxy| nsproxy.mnt_ns().clone()) + .ok_or(KError::NoSuchProcess) } /// Returns a live address-space capability while runtime remains attached. @@ -184,13 +201,17 @@ impl Process { self.runtime().map(|runtime| runtime.mm_id()) } - /// Releases this process runtime's address-space user. + /// Releases this process runtime's address-space owner. /// /// Returns `true` when this was the last runtime user and user mappings /// were cleared. - pub fn clear_exclusive_address_space(&self) -> KResult { - self.runtime() - .map(|runtime| runtime.clear_exclusive_address_space()) + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the process runtime is no longer + /// reachable. + pub fn exit_mm(&self) -> KResult { + self.runtime().map(|runtime| runtime.exit_mm()) } /// Returns the signal manager while runtime remains attached. @@ -393,7 +414,7 @@ impl Process { runtime.set_heap_top(update.heap_top); runtime.reset_signal_actions(); runtime.clear_posix_timers(); - runtime.resources().close_cloexec_files(); + runtime.resources().close_cloexec_files()?; #[cfg(feature = "tipc")] runtime.with_tipc_handles(|handles| handles.write().uctx_handle_close_all()); #[cfg(feature = "tee")] @@ -412,16 +433,42 @@ impl Process { self.runtime().map(|runtime| runtime.clear_posix_timers()) } - /// Closes all process file descriptors. - pub fn close_all_fds(&self) -> KResult<()> { - self.runtime() - .map(|runtime| runtime.resources().close_all_fds()) + /// Releases this process's file descriptor table owner. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the process runtime is no longer + /// reachable. + pub fn exit_files(&self) -> KResult<()> { + self.runtime()?.resources().exit_files(); + Ok(()) + } + + /// Releases this process's filesystem context owner. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the process runtime is no longer + /// reachable. + pub fn exit_fs(&self) -> KResult<()> { + self.runtime()?.exit_fs(); + Ok(()) + } + + /// Releases this process's namespace owner. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] when the process runtime is no longer + /// reachable. + pub fn exit_namespaces(&self) -> KResult<()> { + self.runtime()?.exit_namespaces(); + Ok(()) } /// Closes all process file descriptors marked `FD_CLOEXEC`. pub fn close_cloexec_files(&self) -> KResult<()> { - self.runtime() - .map(|runtime| runtime.resources().close_cloexec_files()) + self.runtime()?.resources().close_cloexec_files() } /// Closes every TIPC handle before this process starts its new executable. diff --git a/process/kprocess/src/process_exit.rs b/process/kprocess/src/process_exit.rs index 7c3cfd98f..d44e178e5 100644 --- a/process/kprocess/src/process_exit.rs +++ b/process/kprocess/src/process_exit.rs @@ -45,6 +45,9 @@ pub fn finalize_process_exit_with_publication( /// Completes process exit after the last thread has finished runtime cleanup. /// +/// The caller must release process-owned mm, files, filesystem context, and +/// namespace capabilities before calling this function. +/// /// The parent wait event is the tail publication step: a parent woken by /// `wait*()` should only observe a fully resolved child-exit state, either a /// waitable zombie or an already-detached autoreaped child. diff --git a/process/kprocess/src/process_runtime/mod.rs b/process/kprocess/src/process_runtime/mod.rs index 985965a78..626ed7f69 100644 --- a/process/kprocess/src/process_runtime/mod.rs +++ b/process/kprocess/src/process_runtime/mod.rs @@ -121,10 +121,10 @@ pub enum ForkFdTable { pub(crate) struct ProcessRuntime { process: Arc, resources: Arc, - fs_context: Arc>, + fs_context: RwLock>>>, posix_state: ProcessPosixState, runtime_state: ProcessRuntimeState, - nsproxy: RwLock>, + nsproxy: RwLock>>, signal_manager: Arc, #[cfg(feature = "tee")] tee_ta_ctx: RwLock, @@ -158,9 +158,17 @@ impl AttachedForkProcess { } } - fn into_process(mut self) -> Arc { + fn commit(mut self) { + drop( + self.process + .take() + .expect("attached fork process must be present"), + ); + } + + fn process(&self) -> &Arc { self.process - .take() + .as_ref() .expect("attached fork process must be present") } } @@ -249,10 +257,10 @@ impl ProcessRuntime { #[cfg(feature = "tipc")] tipc_handles: RwLock::new(TipcHandleTable::new()), resources: ProcessResources::new(config.user_stack_size), - fs_context, + fs_context: RwLock::new(Some(fs_context)), posix_state, runtime_state, - nsproxy: RwLock::new(nsproxy), + nsproxy: RwLock::new(Some(nsproxy)), signal_manager: Arc::new(ProcessSignalManager::new( signal_actions, config.signal_trampoline, @@ -347,23 +355,33 @@ impl ProcessRuntime { self.runtime_state.mm_id() } - pub(crate) fn clear_exclusive_address_space(&self) -> bool { - self.runtime_state.clear_exclusive_address_space() + pub(crate) fn exit_mm(&self) -> bool { + self.runtime_state.exit_mm() } - /// Returns the process-owned filesystem context. - pub fn fs_context(&self) -> Arc> { - self.fs_context.clone() + /// Returns the process-owned filesystem context while it remains attached. + pub fn fs_context(&self) -> Option>> { + self.fs_context.read().clone() } - /// Returns the namespace proxy. - pub fn nsproxy(&self) -> Arc { + /// Returns the namespace proxy while it remains attached. + pub fn nsproxy(&self) -> Option> { self.nsproxy.read().clone() } - /// Returns the UTS namespace. - pub fn uts_ns(&self) -> Arc { - self.nsproxy.read().uts_ns().clone() + /// Returns the UTS namespace while the namespace proxy remains attached. + pub fn uts_ns(&self) -> Option> { + self.nsproxy().map(|nsproxy| nsproxy.uts_ns().clone()) + } + + pub(crate) fn exit_fs(&self) { + let fs_context = self.fs_context.write().take(); + drop(fs_context); + } + + pub(crate) fn exit_namespaces(&self) { + let nsproxy = self.nsproxy.write().take(); + drop(nsproxy); } /// Returns the top address of the user heap. @@ -441,15 +459,15 @@ pub(crate) fn fork_process_runtime( let address_space = prepare_fork_address_space(parent, config.address_space)?; let signal_actions = prepare_fork_signal_actions(parent, config.signal_actions); - let process = attached_process.into_process(); let process_runtime = finish_fork_runtime( parent, - process, + attached_process.process().clone(), fs_namespaces, address_space, signal_actions, config, - ); + )?; + attached_process.commit(); Ok((process_runtime, leader_task_number)) } @@ -458,7 +476,7 @@ fn prepare_fork_fs_and_namespaces( parent: &Arc, config: &ProcessForkConfig, ) -> KResult { - let parent_fs_context = parent.fs_context(); + let parent_fs_context = parent.fs_context().ok_or(KError::NoSuchProcess)?; let fs_context = if matches!(config.fs, ForkFs::Shared) { if parent_fs_context.lock().in_exec() { return Err(KError::WouldBlock); @@ -468,15 +486,12 @@ fn prepare_fork_fs_and_namespaces( Arc::new(Mutex::new(parent_fs_context.lock().clone_for_process())) }; + let parent_nsproxy = parent.nsproxy().ok_or(KError::NoSuchProcess)?; let nsproxy_result = if matches!(config.fs, ForkFs::Shared) { - parent - .nsproxy() - .clone_for_child(config.namespace_flags, NamespaceFsContext::Shared) + parent_nsproxy.clone_for_child(config.namespace_flags, NamespaceFsContext::Shared) } else { let mut fs = fs_context.lock(); - parent - .nsproxy() - .clone_for_child(config.namespace_flags, NamespaceFsContext::Private(&mut fs)) + parent_nsproxy.clone_for_child(config.namespace_flags, NamespaceFsContext::Private(&mut fs)) }; let nsproxy = nsproxy_result.map_err(|err| match err { kns::CloneNsError::InvalidFlagCombination => KError::InvalidInput, @@ -529,7 +544,7 @@ fn finish_fork_runtime( address_space: ForkAddressSpaceState, signal_actions: Arc>, config: ProcessForkConfig, -) -> Arc { +) -> KResult> { let exec_metadata = parent.exec_metadata(); let process_runtime = ProcessRuntime::new_with_nsproxy_and_mm_user( process, @@ -547,11 +562,11 @@ fn finish_fork_runtime( if matches!(config.fd_table, ForkFdTable::Shared) { process_runtime .resources() - .replace_fd_table(parent.resources().fd_table()); + .replace_fd_table(parent.resources().fd_table()?)?; } else { - let fd_table = kfd::FdTable::clone_shared_from(&parent.resources().fd_table()); - process_runtime.resources().replace_fd_table(fd_table); + let fd_table = kfd::FdTable::clone_shared_from(&parent.resources().fd_table()?); + process_runtime.resources().replace_fd_table(fd_table)?; } - process_runtime + Ok(process_runtime) } diff --git a/process/kprocess/src/process_runtime/runtime_state.rs b/process/kprocess/src/process_runtime/runtime_state.rs index 96106f9a5..01dd7d882 100644 --- a/process/kprocess/src/process_runtime/runtime_state.rs +++ b/process/kprocess/src/process_runtime/runtime_state.rs @@ -91,7 +91,7 @@ impl ProcessRuntimeState { } /// Releases this runtime's address-space user and clears mappings for the last user. - pub(super) fn clear_exclusive_address_space(&self) -> bool { + pub(super) fn exit_mm(&self) -> bool { let mm_user = self.mm_user.lock().take(); mm_user.is_some_and(MmUserHandle::release_and_clear_if_last) } @@ -131,6 +131,6 @@ impl ProcessRuntimeState { impl Drop for ProcessRuntimeState { fn drop(&mut self) { - self.clear_exclusive_address_space(); + self.exit_mm(); } } diff --git a/process/kprocess/src/tests.rs b/process/kprocess/src/tests.rs index 52dc2a480..db1daa6ea 100644 --- a/process/kprocess/src/tests.rs +++ b/process/kprocess/src/tests.rs @@ -757,8 +757,7 @@ fn test_exit_cleanup_clears_exclusive_address_space() { let (proc, _task) = process_with_address_space(8_130, mapped_test_address_space()); assert!( - proc.clear_exclusive_address_space() - .expect("runtime must be attached"), + proc.exit_mm().expect("runtime must be attached"), "exclusive address space should be cleared during process exit" ); assert!( @@ -778,14 +777,59 @@ fn test_exit_cleanup_clears_exclusive_address_space() { wait_reap::assert_reap_zombie_process(&proc); } +#[def_test(serial)] +fn test_exit_owners_detach_before_process_exit_publication() { + let (proc, _task) = process_with_address_space(8_137, mapped_test_address_space()); + let resources = proc.resources().expect("runtime must expose resources"); + + assert!(proc.address_space().is_ok()); + assert!(resources.fd_table().is_ok()); + assert!(proc.fs_context().is_ok()); + assert!(proc.mnt_ns().is_ok()); + assert!(proc.uts_ns().is_ok()); + + proc.exit_mm().expect("runtime must be attached"); + proc.exit_files().expect("runtime must be attached"); + proc.exit_fs().expect("runtime must be attached"); + proc.exit_namespaces().expect("runtime must be attached"); + + assert!(!proc.is_exited()); + assert_eq!( + proc.address_space().err(), + Some(kerrno::KError::NoSuchProcess) + ); + assert_eq!( + resources.fd_table().err(), + Some(kerrno::KError::NoSuchProcess) + ); + assert_eq!(proc.fs_context().err(), Some(kerrno::KError::NoSuchProcess)); + assert_eq!(proc.mnt_ns().err(), Some(kerrno::KError::NoSuchProcess)); + assert_eq!(proc.uts_ns().err(), Some(kerrno::KError::NoSuchProcess)); + + assert!(!proc.exit_mm().expect("owner release must be idempotent")); + proc.exit_files().expect("owner release must be idempotent"); + proc.exit_fs().expect("owner release must be idempotent"); + proc.exit_namespaces() + .expect("owner release must be idempotent"); + + process_exit::finalize_process_exit(&proc); + assert!(proc.is_exited()); + assert_eq!( + resources.fd_table().err(), + Some(kerrno::KError::NoSuchProcess) + ); + assert_eq!(proc.fs_context().err(), Some(kerrno::KError::NoSuchProcess)); + assert_eq!(proc.mnt_ns().err(), Some(kerrno::KError::NoSuchProcess)); + wait_reap::assert_reap_zombie_process(&proc); +} + #[def_test(serial)] fn test_exit_cleanup_ignores_non_runtime_address_space_refs() { let observed_address_space = mapped_test_address_space(); let (proc, _task) = process_with_address_space(8_131, observed_address_space.clone()); assert!( - proc.clear_exclusive_address_space() - .expect("runtime must be attached"), + proc.exit_mm().expect("runtime must be attached"), "temporary address-space references must not suppress final mm cleanup" ); assert_eq!( @@ -817,9 +861,7 @@ fn test_exit_cleanup_preserves_clone_vm_address_space_until_last_runtime_user() let child_process = child.process().clone(); assert!( - !parent - .clear_exclusive_address_space() - .expect("runtime must be attached"), + !parent.exit_mm().expect("runtime must be attached"), "shared VM mappings must remain while another process runtime still uses the mm" ); assert_eq!( @@ -830,7 +872,7 @@ fn test_exit_cleanup_preserves_clone_vm_address_space_until_last_runtime_user() assert!( child_process - .clear_exclusive_address_space() + .exit_mm() .expect("child runtime must be attached"), "last shared VM runtime user should clear mappings" ); @@ -895,9 +937,7 @@ fn test_failed_shared_vm_fork_rolls_back_tree_relation() { let child_count_before = parent.children().len(); assert!( - parent - .clear_exclusive_address_space() - .expect("runtime must be attached"), + parent.exit_mm().expect("runtime must be attached"), "test setup must release the last active address-space user" ); @@ -934,9 +974,7 @@ fn test_failed_private_vm_fork_after_parent_mm_teardown_rolls_back_tree_relation let child_count_before = parent.children().len(); assert!( - parent - .clear_exclusive_address_space() - .expect("runtime must be attached"), + parent.exit_mm().expect("runtime must be attached"), "test setup must release the last active address-space user" ); @@ -966,6 +1004,41 @@ fn test_failed_private_vm_fork_after_parent_mm_teardown_rolls_back_tree_relation wait_reap::assert_reap_zombie_process(&parent); } +#[def_test(serial)] +fn test_failed_fork_after_parent_files_exit_rolls_back_tree_relation() { + let (parent, parent_task) = process_with_address_space(8_138, mapped_test_address_space()); + let child_count_before = parent.children().len(); + parent.exit_files().expect("runtime must be attached"); + + let err = match parent_task + .as_thread() + .prepare_process_fork(ProcessForkConfig { + parent: ForkParent::Caller, + address_space: ForkAddressSpace::Private, + fs: ForkFs::Private, + signal_actions: ForkSignalActions::Private, + fd_table: ForkFdTable::Private, + namespace_flags: kns::NamespaceFlags::empty(), + exit_signal: Some(ksignal::Signo::SIGCHLD), + }) { + Ok(_) => panic!("fork must fail once the parent files owner is released"), + Err(err) => err, + }; + + assert_eq!(err, kerrno::KError::NoSuchProcess); + assert_eq!( + parent.children().len(), + child_count_before, + "failed fork must roll back the unpublished child relation" + ); + + parent.exit_mm().expect("runtime must be attached"); + parent.exit_fs().expect("runtime must be attached"); + parent.exit_namespaces().expect("runtime must be attached"); + process_exit::finalize_process_exit(&parent); + wait_reap::assert_reap_zombie_process(&parent); +} + #[def_test(serial)] fn test_group_exit_prevents_late_thread_exit_from_overwriting_exit_code() { let init = ensure_init(); diff --git a/process/kresources/src/lib.rs b/process/kresources/src/lib.rs index 0ddb60ec1..baee8137d 100644 --- a/process/kresources/src/lib.rs +++ b/process/kresources/src/lib.rs @@ -20,13 +20,13 @@ use linux_raw_sys::general::{RLIM_NLIMITS, RLIMIT_NOFILE}; /// Process-owned resource state. /// -/// This is the first owner boundary for process resources. More resource handles -/// can move here later; for now it owns the rlimit set explicitly. +/// The resource set owns limits and the detachable file descriptor table used +/// by one process runtime. pub struct ProcessResources { /// Per-process resource limits. pub rlimits: RwLock, /// The process-owned file descriptor table handle. - fd_table: RwLock>>, + fd_table: RwLock>>>, } impl ProcessResources { @@ -34,7 +34,7 @@ impl ProcessResources { pub fn new(user_stack_size: usize) -> Arc { Arc::new(Self { rlimits: RwLock::new(Rlimits::new(user_stack_size)), - fd_table: RwLock::new(FdTable::new_shared()), + fd_table: RwLock::new(Some(FdTable::new_shared())), }) } @@ -83,14 +83,23 @@ impl ProcessResources { self.set_rlimit(resource, Rlimit::new(current, max)) } - /// Returns the current file descriptor table handle. - pub fn fd_table(&self) -> Arc> { - self.fd_table.read().clone() + /// Returns the attached file descriptor table. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] after the process has released its + /// files owner. + pub fn fd_table(&self) -> KResult>> { + self.fd_table.read().clone().ok_or(KError::NoSuchProcess) } - fn with_fd_table(&self, access_fn: impl FnOnce(&RwLock) -> R) -> R { - let fd_table = self.fd_table.read(); - access_fn((*fd_table).as_ref()) + fn with_fd_table( + &self, + access_fn: impl FnOnce(&RwLock) -> KResult, + ) -> KResult { + let owner = self.fd_table.read(); + let fd_table = owner.as_ref().ok_or(KError::NoSuchProcess)?; + access_fn(fd_table) } fn close_descriptors(descriptors: impl IntoIterator) { @@ -161,54 +170,82 @@ impl ProcessResources { } /// Closes all descriptors in the given inclusive range. - pub fn close_range(&self, first_fd: c_int, last_fd: c_int) { + pub fn close_range(&self, first_fd: c_int, last_fd: c_int) -> KResult<()> { let descriptors = - self.with_fd_table(|fd_table| fd_table.write().remove_range(first_fd, last_fd)); + self.with_fd_table(|fd_table| Ok(fd_table.write().remove_range(first_fd, last_fd)))?; Self::close_descriptors(descriptors); + Ok(()) } /// Marks all descriptors in the given inclusive range close-on-exec. - pub fn set_cloexec_range(&self, first_fd: c_int, last_fd: c_int) { - self.with_fd_table(|fd_table| fd_table.write().set_cloexec_range(first_fd, last_fd)); + pub fn set_cloexec_range(&self, first_fd: c_int, last_fd: c_int) -> KResult<()> { + self.with_fd_table(|fd_table| { + fd_table.write().set_cloexec_range(first_fd, last_fd); + Ok(()) + }) } /// Closes all descriptors marked close-on-exec. - pub fn close_cloexec_files(&self) { - let descriptors = self.with_fd_table(|fd_table| fd_table.write().remove_cloexec_files()); + pub fn close_cloexec_files(&self) -> KResult<()> { + let descriptors = + self.with_fd_table(|fd_table| Ok(fd_table.write().remove_cloexec_files()))?; Self::close_descriptors(descriptors); + Ok(()) } - /// Closes all file descriptors when the table is not shared. - pub fn close_all_fds(&self) { - // Must NOT call self.fd_table() — that clones the inner Arc and - // bumps strong_count, tricking remove_all_if_unshared into returning - // early without closing any descriptors. - let descriptors = { - let guard = self.fd_table.read(); - FdTable::remove_all_if_unshared(&guard) - }; - Self::close_descriptors(descriptors); + /// Releases this process's file descriptor table owner. + /// + /// A table shared by multiple processes closes its descriptors when its + /// final owner is released. + pub fn exit_files(&self) { + let fd_table = self.fd_table.write().take(); + drop(fd_table); } - /// Replaces the current fd table with an unshared clone. - pub fn unshare_fd_table(&self) { - let old_table = self.fd_table(); - let new_table = FdTable::clone_shared_from(&old_table); - self.replace_fd_table(new_table); + /// Replaces a shared fd table with a private clone. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] after the files owner is released. + pub fn unshare_fd_table(&self) -> KResult<()> { + let old_table = { + let mut owner = self.fd_table.write(); + let table = owner.as_ref().ok_or(KError::NoSuchProcess)?; + if Arc::strong_count(table) == 1 { + return Ok(()); + } + + let new_table = FdTable::clone_shared_from(table); + owner + .replace(new_table) + .expect("fd table owner was checked") + }; + drop(old_table); + Ok(()) } - /// Replaces the file descriptor table handle. - pub fn replace_fd_table(&self, table: Arc>) -> Arc> { - core::mem::replace(&mut *self.fd_table.write(), table) + /// Replaces the attached file descriptor table. + /// + /// # Errors + /// + /// Returns [`KError::NoSuchProcess`] after the files owner is released; + /// an exited process cannot acquire a new table. + pub fn replace_fd_table(&self, table: Arc>) -> KResult>> { + let mut owner = self.fd_table.write(); + let old_table = owner.take().ok_or(KError::NoSuchProcess)?; + *owner = Some(table); + Ok(old_table) } } #[cfg(unittest)] mod tests { + use alloc::sync::Arc; + use krlimit::Rlimit; use unittest::def_test; - use super::ProcessResources; + use super::{FdTable, ProcessResources}; #[def_test] fn test_process_resources_default_limits() { @@ -218,7 +255,7 @@ mod tests { resources.rlimits.read()[linux_raw_sys::general::RLIMIT_STACK].current, 0x80000 ); - assert_eq!(resources.fd_table().read().count(), 0); + assert_eq!(resources.fd_table().unwrap().read().count(), 0); } #[def_test] @@ -283,4 +320,36 @@ mod tests { kerrno::KError::OperationNotPermitted ); } + + #[def_test] + fn test_exit_files_detaches_fd_table() { + let resources = ProcessResources::new(0x80000); + + resources.exit_files(); + + assert_eq!( + resources.fd_table().err(), + Some(kerrno::KError::NoSuchProcess) + ); + assert_eq!( + resources.replace_fd_table(FdTable::new_shared()).err(), + Some(kerrno::KError::NoSuchProcess) + ); + resources.exit_files(); + } + + #[def_test] + fn test_shared_fd_table_lives_until_last_files_owner_exits() { + let first = ProcessResources::new(0x80000); + let second = ProcessResources::new(0x80000); + let shared = first.fd_table().unwrap(); + let weak = Arc::downgrade(&shared); + drop(second.replace_fd_table(shared).unwrap()); + + first.exit_files(); + assert!(weak.upgrade().is_some()); + + second.exit_files(); + assert!(weak.upgrade().is_none()); + } } -- Gitee From 06db3bb671628b4e437b1110c6d4d2735abf7af4 Mon Sep 17 00:00:00 2001 From: wangyining Date: Thu, 13 Aug 2026 18:09:05 +0800 Subject: [PATCH 2/3] test(kfd): cover final close after flush failure --- posix/fs/docs/security.md | 2 +- process/kfd/src/lib.rs | 44 +++++++++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/posix/fs/docs/security.md b/posix/fs/docs/security.md index 064706ee2..f68b7496b 100644 --- a/posix/fs/docs/security.md +++ b/posix/fs/docs/security.md @@ -188,7 +188,7 @@ kfd resources / kvfs / device and pipe implementations | F-08 | `syncfs` 目标不是文件或目录 | fd 指向 pipe/socket/设备 | 返回 `InvalidInput` | 当前同步请求失败 | 4 | downcast 后只 flush 文件系统对象 | | F-09 | `copy_file_range` 语义不完整 | 重叠和普通文件检查 TODO | 可能出现与 Linux 不一致的数据结果 | 相关应用复制行为异常 | 2 | 非零 flags 显式拒绝;其余限制实现前需要补充测试 | | F-10 | `fcntl` unsupported cmd 返回成功 | 兼容占位 | 应用误判某些控制操作已生效 | 可能产生行为差异 | 2 | warning 记录;有安全影响的命令应显式实现或拒绝 | -| F-11 | `close_range(UNSHARE)` 资源复制失败 | 下层 unshare 实现异常 | 当前 API 没有错误承载 | fd 表隔离语义不完整 | 2 | `unshare_fd_table` 当前按不可失败路径使用 | +| F-11 | `close_range(UNSHARE)` 无法取得 files owner | 进程退出已脱离 fd table owner | `unshare_fd_table` 返回 `NoSuchProcess` | 当前 syscall 失败,已脱离的 fd table 不会被重新安装 | 3 | `sys_close_range` 通过 `?` 将资源层错误传播为用户态 syscall 错误 | | F-12 | FIEMAP 输出容量不足或中途遇到坏用户页 | 调用者提供较小数组或不可写地址 | 返回已统计数量或 `BadAddress` | 当前查询失败,文件系统状态不变 | 3 | `FiemapExtentInfo` 达到容量后正常停止;writer 每项通过 `UserPtr` 写入并传播 copy fault | 严重度定义: diff --git a/process/kfd/src/lib.rs b/process/kfd/src/lib.rs index ab80838b6..0512a2323 100644 --- a/process/kfd/src/lib.rs +++ b/process/kfd/src/lib.rs @@ -39,12 +39,15 @@ mod tests { } } - struct FlushCountFops(Arc); + struct FlushCountFops { + flushes: Arc, + result: VfsResult<()>, + } impl FileOperations for FlushCountFops { fn flush(&self, _file: &VfsFile) -> VfsResult<()> { - self.0.fetch_add(1, Ordering::Relaxed); - Ok(()) + self.flushes.fetch_add(1, Ordering::Relaxed); + self.result } } @@ -61,11 +64,14 @@ mod tests { .expect("snapshot test anon inode file opens") } - fn flush_count_test_file(flushes: Arc) -> Arc { + fn flush_count_test_file( + flushes: Arc, + result: VfsResult<()>, + ) -> Arc { AnonInodeFs::global() .get_file( "[fd-table-drop-test]", - Arc::new(FlushCountFops(flushes)), + Arc::new(FlushCountFops { flushes, result }), Arc::new(()), FMode::READ, OpenFlags::empty(), @@ -154,11 +160,37 @@ mod tests { let flushes = Arc::new(AtomicUsize::new(0)); let mut table = FdTable::default(); table - .add_file(16, flush_count_test_file(flushes.clone()), false) + .add_file(16, flush_count_test_file(flushes.clone(), Ok(())), false) .unwrap(); drop(table); assert_eq!(flushes.load(Ordering::Relaxed), 1); } + + #[def_test] + fn test_fd_table_drop_continues_after_flush_error() { + let failed_flushes = Arc::new(AtomicUsize::new(0)); + let successful_flushes = Arc::new(AtomicUsize::new(0)); + let mut table = FdTable::default(); + table + .add_file( + 16, + flush_count_test_file(failed_flushes.clone(), Err(kerrno::KError::Io)), + false, + ) + .unwrap(); + table + .add_file( + 16, + flush_count_test_file(successful_flushes.clone(), Ok(())), + false, + ) + .unwrap(); + + drop(table); + + assert_eq!(failed_flushes.load(Ordering::Relaxed), 1); + assert_eq!(successful_flushes.load(Ordering::Relaxed), 1); + } } -- Gitee From e2203b1a9fdec663338aad5842bc92e86f778efd Mon Sep 17 00:00:00 2001 From: wangyining Date: Thu, 13 Aug 2026 18:14:04 +0800 Subject: [PATCH 3/3] fmt --- process/kfd/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/process/kfd/src/lib.rs b/process/kfd/src/lib.rs index 0512a2323..34813680b 100644 --- a/process/kfd/src/lib.rs +++ b/process/kfd/src/lib.rs @@ -64,10 +64,7 @@ mod tests { .expect("snapshot test anon inode file opens") } - fn flush_count_test_file( - flushes: Arc, - result: VfsResult<()>, - ) -> Arc { + fn flush_count_test_file(flushes: Arc, result: VfsResult<()>) -> Arc { AnonInodeFs::global() .get_file( "[fd-table-drop-test]", -- Gitee