diff --git a/Cargo.lock b/Cargo.lock index bf33d010ccfc105684e3f28eefe27f6db5a5e654..6da0c33974a00e7dad17c8ea189bc42eb91d0f69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,7 @@ dependencies = [ "kcpu_id_map", "kcred", "kernel-image-metadata", + "kexec", "kfeat", "khal", "kiface", diff --git a/arch/khal/docs/design.md b/arch/khal/docs/design.md index 934c7fce03fc36095d3bf78cf1c7d6b96f5ee283..77520734b84ea6299ea65c8263da9946e4471743 100644 --- a/arch/khal/docs/design.md +++ b/arch/khal/docs/design.md @@ -35,6 +35,13 @@ kcpu trap dispatch 普通 IRQ API 调用路径应直接进入 `kirq`,不经过 `khal::irq` facade。 +## 平台终止操作 + +`khal::power` 将平台 `SysCtrl` 的 `halt()` 与 `power_off()` 暴露给运行时,并用 +`ShutdownMode` 表达 orderly shutdown 完成后的最终平台状态。`halt()` 停止 CPU +但保持供电,`power_off()` 请求平台移除电源;用户态请求到文件系统、设备与平台 +teardown 的顺序由 `kruntime` 管理,不属于 HAL。 + ## 调用约束 / 执行上下文 - `irq_handler()` 与 `nmi_handler()` 只能由架构 trap dispatch 调用。 diff --git a/arch/khal/docs/security.md b/arch/khal/docs/security.md index 716517ae071d050d11f5c24fd473fadf6e77116f..4931863c8666615061c6726663238d58f42a5b16 100644 --- a/arch/khal/docs/security.md +++ b/arch/khal/docs/security.md @@ -18,6 +18,10 @@ generic `kirq` API。 本模块不直接处理用户指针、DMA buffer、设备 MMIO 或文件/网络数据。 +`khal::power` 是最终的平台控制边界。正常 halt/power-off 只能由 runtime supervisor +在用户态、mount namespace 和设备 teardown 完成后调用;panic 等不可恢复路径可直接 +调用 `power_off()`,但不提供 orderly shutdown 保证。 + `arch/khal/src/irq` 当前没有本地 `unsafe` 代码块。相关 unsafe 边界位于: - `kcpu` trap/汇编入口; @@ -28,6 +32,7 @@ generic `kirq` API。 - `khal::irq` 不保存 `IRQ_STATE` 或 `NMI_TABLE`,避免与 `kirq` 形成重复活跃 state。 - `NoPreempt` guard 必须覆盖整个 `kirq::handle_irq()` / `kirq::handle_nmi()` 调用。 +- `halt()` 与 `power_off()` 是不返回的最终平台操作,不能合并其外部可见语义。 ## 线程安全 @@ -45,6 +50,7 @@ generic `kirq` API。 | T-02 | generic IRQ API 重新经由 `khal::irq` 导出 | 调用方把 HAL adapter 当公共 IRQ core | crate 边界退化,后续扩展回到 HAL | `khal::irq` 不 re-export `kirq::*`,调用方直接依赖 `kirq` | | T-03 | `IPI_IRQ` 移入 `kirq` | generic core 依赖 `kbuild_config` | crate 边界污染、特性耦合 | IPI 调用方直接使用 `kbuild_config::IPI_IRQ` | | T-04 | x86 helper 回流到 `khal::irq` | HAL adapter 再次暴露 APIC vector policy | crate 边界退化,驱动绕过 `kirq` MSI resource | `khal::irq` 不提供 MSI-X/APIC helper;x86 APIC 只实现 `kirq::MsiBackendIf` | +| T-05 | 正常关机路径直接调用平台终止操作 | filesystem 或设备 owner 尚未释放 | 持久化状态不完整、设备仍在工作 | runtime supervisor 完成 teardown 后才调用 `halt()` / `power_off()` | ## 故障模式与影响分析(FMEA) diff --git a/arch/khal/src/dummy.rs b/arch/khal/src/dummy.rs index 10451f0a78b5e44b6a686cab790164d45bb159f6..d61393b6ad57ad668d2ad8c62fa9802dd68d7f3a 100644 --- a/arch/khal/src/dummy.rs +++ b/arch/khal/src/dummy.rs @@ -80,7 +80,11 @@ impl SysCtrl { Ok(()) } - fn shutdown() -> ! { + fn halt() -> ! { + unimplemented!() + } + + fn power_off() -> ! { unimplemented!() } } diff --git a/arch/khal/src/lib.rs b/arch/khal/src/lib.rs index f824c69d00ff6664919bb5fee3536c43cb26a23e..417c604fb6c61da37305fdffc7bc74b5f069e3bc 100644 --- a/arch/khal/src/lib.rs +++ b/arch/khal/src/lib.rs @@ -58,7 +58,16 @@ pub use firmware::cmdline; pub mod power { #[cfg(feature = "smp")] pub use kplat::sys::boot_ap; - pub use kplat::sys::shutdown; + pub use kplat::sys::{halt, power_off}; + + /// Terminal system state selected after orderly shutdown. + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub enum ShutdownMode { + /// Stop the system while leaving power applied. + Halt, + /// Remove system power. + PowerOff, + } } /// Trap handling. diff --git a/core/kruntime/docs/design.md b/core/kruntime/docs/design.md index ce463975c535b4882ceaf53cbb551c756b8aba6c..0bc27bbe3540932d003d3ca7ac2e979a4831bbfd 100644 --- a/core/kruntime/docs/design.md +++ b/core/kruntime/docs/design.md @@ -2,7 +2,7 @@ ## 定位 -`kruntime` 是 x-kernel 的**运行时编排层**:在平台引导代码将控制权交给内核后,负责主核/从核的初始化顺序、子系统装配,以及跳转到系统初始化入口(`entry` crate 提供的 `SystemInitEntry`)。它不实现具体设备驱动或 syscall,而是把 `khal`、`memspace`、`ktask`、`kdriver` 等按固定阶段串联起来。 +`kruntime` 是 x-kernel 的**运行时编排层**:在平台引导代码将控制权交给内核后,负责主核/从核的初始化顺序、子系统装配以及系统终止顺序。`entry` crate 通过 `SystemUserspace` 提供用户态生命周期策略。`kruntime` 不实现具体设备驱动或 syscall,只按固定阶段串联所属子系统。 目标读者:需要理解启动链、SMP 屏障、或在此 crate 增加初始化钩子的内核开发者。 @@ -10,11 +10,11 @@ x-kernel 将**引导**(`kernel-boot`、平台 `platconfig`)与**运行时**(内存、调度、驱动、用户态 init)分离。引导层通过 `BootInfo` 和 `kiface` 单实现入口把主核/从核控制权交给运行时;`kruntime` 提供这些入口的实现(`rust_main` / `rust_main_secondary`)。 -启动拓扑遵循 FreeBSD `proc0` 模型:boot/current task 是调度器内部任务(`Internal` 身份,不进入普通 PID allocator)。`rust_main` 激活一个 PID-less 的 late-init 内核线程跑完所有 late init(drivers/fs/net/SMP 等),等待全部 CPU 就绪后调用 `SystemInitEntry::enter()`。`entry` **spawn 出一个全新的 PID 1 用户任务**(init),随后 late-init 线程退出。init 不是"原地变身"——它是被 spawn 的新任务,走与 fork 相同的 `new_user` + `publish_user_task().commit()` 标准路径。 +启动拓扑遵循现有 PID-less bootstrap 模型:boot/current task 是调度器内部任务(`Internal` 身份,不进入普通 PID allocator)。`rust_main` 激活 late-init 内核线程跑完所有 late init,等待全部 CPU 就绪后调用 `SystemUserspace::start()`。生产 provider 创建全新的 PID 1 用户任务并返回 task handle;unittest provider 返回测试 runner task。late-init 线程随后转为系统生命周期 supervisor,等待该终端 workload 退出,再依次 teardown 用户态 owner、初始 mount namespace、设备和平台。生产 init 走与 fork 相同的 `new_user` + `publish_user_task().commit()` 路径,不改写 bootstrap task 身份。 -关键设计:late-init 线程以及 late init 期间 spawn 的所有后台 worker(网络 poller、TTY、watchdog 等)都是 PID-less 的 `Internal` 内核线程(对应 FreeBSD `kthread_add`),不进入普通 PID 空间。因此 init 的分配是 root namespace 的第一笔普通分配,PID 1 是唯一的启动期固定 PID。 +关键设计:late-init 线程以及 late init 期间 spawn 的所有后台 worker(网络 poller、TTY、watchdog 等)都是 PID-less 的 `Internal` 内核线程,不进入普通 PID 空间。因此 init 的分配是 root namespace 的第一笔普通分配,PID 1 是唯一的启动期固定 PID。 -应用逻辑(例如 `entry::runtime::init_runtime`)放在 `entry` crate;初始用户进程组装则由 `posix-process::spawn_init_process` 承接,避免 `kruntime` 反向依赖高层服务。 +应用逻辑(例如 `entry::runtime::init_runtime`)放在 `entry` crate;初始用户进程组装和用户态 owner 终止则由 `posix-process` 承接,避免 `kruntime` 反向依赖进程、signal 或 exec 实现。 ## 范围 @@ -54,9 +54,9 @@ kernel-boot (汇编 / MMU / BootInfo) │ late_init (Internal, PID-less bootstrap thread) │ │ drivers / fs / net / SMP / IPI / init_cb │ │ 等待 INITED_CPUS == nr_cpus │ -│ SystemInitEntry::enter() ─────────────► entry crate │ -│ entry spawns and activates fresh PID 1 init task │ -│ return, then exit │ +│ SystemUserspace::start() ─────────────► entry crate │ +│ entry returns PID 1 or unittest runner task │ +│ join task -> fs teardown -> device teardown -> terminal op │ └───────────────────────────────────────────────────────────────┘ │ │ boot_ap → SecondaryKernelEntry::enter(logical_cpu_id) @@ -81,7 +81,7 @@ kernel-boot (汇编 / MMU / BootInfo) | `mp::start_secondary_cpus` | 为 AP 准备栈、调用 `boot_ap`、等待 AP 进入 runtime | | `init_setup` | 执行链接段 `.init_array` 中的 `register_init` 回调 | | `dma_integration` | 将 DMA 页表属性更新委托给 `memspace::kernel_layout` | -| `lang_items` | Panic 时打印 backtrace 并 `shutdown` | +| `lang_items` | Panic 时打印 backtrace 并直接 `power_off` | ## 启动流程(主核) @@ -117,18 +117,59 @@ late-init thread (Internal, PID-less): → INITED_CPUS += 1 → spin until is_init_ok() → [vsock_tipc_bridge] start_vsock_bridge (依赖跨 CPU 调度,必须在屏障后) - → SystemInitEntry::enter() - (entry; spawn 出全新 PID 1 用户任务 init,返回) - → late-init thread exits (task_entry 自动调 ktask::exit) + → SystemUserspace::start() + (entry; spawn 出全新 PID 1 用户任务 init,返回 task handle) + → join(PID 1 leader task) + → SystemUserspace::shutdown() + (等待 PID 1 process completion,再终止余下用户进程并返回 shutdown mode) + → fs_boot::shutdown_namespace + → kdriver bus quiesce/remove + → khal::power::halt / power_off init (PID 1, 全新 spawn 的用户任务): → 调度器 switch-in 时自动激活用户页表 (switch_page_table_root) → run_user_thread_loop → 进入用户态 + → do_exit: mm → files → fs_struct → nsproxy + → ktask::exit: publish Exited and wake supervisor ``` +### 正常关机约定 + +正常关机只有一个平台终止点,归 `kruntime` 的 PID-less supervisor 所有: + +```text +PID 1 正常退出 ───────────────────────────────────────────────┐ +reboot(HALT/POWER_OFF) -> request_shutdown(mode) -> SIGKILL PID 1 ─┤ + v +supervisor join PID 1 leader task + -> SystemUserspace::shutdown + -> wait PID 1 process completion + -> return first requested mode (default POWER_OFF) + -> fs_boot::shutdown_namespace + -> MntNamespace::unmount_all + -> Mount drop -> SuperBlock final shutdown + -> kdriver quiesce/remove + -> HALT: send remote stop callbacks + -> wait until each accepted callback disables local IRQs and enters halt + -> khal::power::halt + POWER_OFF: khal::power::power_off +``` + +PID 1 自身不执行 sync、unmount 或 HAL terminal operation。`do_exit()` 先取走它的 +files/`FsStruct`/`NsProxy` owner。supervisor 醒来后由 provider 终止其余用户进程, +等待它们各自完成 `do_exit()`,再清空会持有 `VfsFile` 的 exec cache。`fs_boot` 随后 +释放静态 `INIT_FS` 并撤销 mount topology。KVFS 继续通过最后一个 active +`Mount` 进入已有的“第一次 sync → dcache eviction → 第二次 sync”,runtime +不复制 filesystem 算法。panic handler 仍是 emergency path,直接平台断电, +不承诺 orderly teardown。 + +HALT 路径对每个成功入队的远端 stop callback 等待进入确认,再终止当前 CPU; +IPI 投递失败会记录目标 CPU 并继续进入平台 halt。确认只表达远端 CPU 已关闭本地中断 +并进入终止循环,不引入额外的 CPU 生命周期对象。 + ### PID 1 启动约定 -`ktask::init_scheduler()` 创建的 boot/current task 与 idle/gc task 都是 scheduler 内部身份(`Internal`/`Idle`),不调用 `kidentity::allocate_root_pid_handle()`。late-init 线程也是 `Internal`,且 late init 期间 spawn 的所有后台 worker(`ktask::spawn` 走 `new_pidless_kthread`)同样是 PID-less 的 `Internal`。因此 `SystemInitEntry::enter()` 创建 init 时,root namespace 的第一笔普通 PID 分配必须得到 PID 1。 +`ktask::init_scheduler()` 创建的 boot/current task 与 idle/gc task 都是 scheduler 内部身份(`Internal`/`Idle`),不调用 `kidentity::allocate_root_pid_handle()`。late-init 线程也是 `Internal`,且 late init 期间 spawn 的所有后台 worker(`ktask::spawn` 走 `new_pidless_kthread`)同样是 PID-less 的 `Internal`。因此 `SystemUserspace::start()` 创建 init 时,root namespace 的第一笔普通 PID 分配必须得到 PID 1。 这个顺序把 root namespace 的早期 PID 语义固定为: @@ -138,7 +179,7 @@ PID 1 = init 用户进程(late-init 线程 spawn 的全新用户任务) PID >=2 = 后续 fork 出的用户进程或显式 Linux-visible task;普通内核 worker 不占号 ``` -init 不再经历"原地变身":late-init 线程通过 `SystemInitEntry::enter()` → `posix-process::spawn_init_process` 构造一个全新的 `User` 身份任务,runtime 在 `new_user` 构造时一次性就绪(`UserRuntimeSlot::ready`),经 `publish_user_task().commit()` 发布并激活,页表由调度器在首次 switch-in 时通过 `switch_page_table_root` 自动写入。没有 `install_user_runtime`、空槽状态机或 `activate_current_user_page_table` 这类"事后补装"机制。 +init 不再经历"原地变身":late-init 线程通过 `SystemUserspace::start()` → `posix-process::spawn_init_process` 构造一个全新的 `User` 身份任务,runtime 在 `new_user` 构造时一次性就绪(`UserRuntimeSlot::ready`),经 `publish_user_task().commit()` 发布并激活,页表由调度器在首次 switch-in 时通过 `switch_page_table_root` 自动写入。没有 `install_user_runtime`、空槽状态机或 `activate_current_user_page_table` 这类"事后补装"机制。 ### 全局就绪屏障 `is_init_ok` @@ -148,11 +189,11 @@ INITED_CPUS.load(Acquire) == kcpu_id_map::nr_cpus() - 主核在完成自身初始化后对 `INITED_CPUS` 执行 `fetch_add(1, Release)`。 - 每个从核在 `rust_main_secondary` 末尾同样 `fetch_add(1, Release)`。 -- PID-less late-init 线程在调用 `SystemInitEntry::enter()` 前自旋等待计数达到 `nr_cpus()`(运行时从设备树/ACPI 发现的实际核数,而非编译期 `NR_CPUS` 上限),保证系统初始化入口启动时所有逻辑 CPU 已完成 runtime 初始化。QEMU `-smp` 与 `NR_CPUS` 不再强耦合:给少于 `NR_CPUS` 的核不会死锁,给多于 `NR_CPUS` 的核会被告警截断。 +- PID-less late-init 线程在调用 `SystemUserspace::start()` 前自旋等待计数达到 `nr_cpus()`(运行时从设备树/ACPI 发现的实际核数,而非编译期 `NR_CPUS` 上限),保证系统初始化入口启动时所有逻辑 CPU 已完成 runtime 初始化。QEMU `-smp` 与 `NR_CPUS` 不再强耦合:给少于 `NR_CPUS` 的核不会死锁,给多于 `NR_CPUS` 的核会被告警截断。 - 依赖跨 CPU task spawn 的 late-start worker,例如 vsock-TIPC bridge, - 在该屏障之后、`SystemInitEntry::enter()` 之前启动,避免调度到尚未注册的 secondary run queue。 + 在该屏障之后、`SystemUserspace::start()` 之前启动,避免调度到尚未注册的 secondary run queue。 -从核在屏障之后开启本地 IRQ(及可选 watchdog),进入 `ktask::run_idle()`,**不**执行 `SystemInitEntry`。 +从核在屏障之后开启本地 IRQ(及可选 watchdog),进入 `ktask::run_idle()`,**不**执行 `SystemUserspace`。 ## 启动流程(从核) @@ -190,7 +231,7 @@ SecondaryKernelEntry::enter(logical_cpu_id) | `kernel_boot::PrimaryKernelEntry` | `lib.rs` | 主核从 boot 层进入 `rust_main` | | `kernel_boot::SecondaryKernelEntry` | `mp.rs` | 从核从 boot 层进入 `rust_main_secondary` | | `fs_block::RootFileSystem` | Kconfig 所选 `kext4_vfs` / `fat` crate | 提供 root block filesystem mount,避免 boot 按实现分支 | -| `kruntime::SystemInitEntry` | `entry/src/main.rs` | runtime 就绪后进入系统级 init 策略层 | +| `kruntime::SystemUserspace` | `entry/src/main.rs` | 返回终端 workload task,并在关机时释放对应 filesystem owner 与 exec-cache 资源、选择 terminal mode | 均为链接期 exactly-one 单实现,非运行时注册。 ## Cargo Features @@ -210,13 +251,13 @@ SecondaryKernelEntry::enter(logical_cpu_id) ## 设计决策 -### 为何用 `SystemInitEntry` 而非直接依赖 `entry` +### 为何用 `SystemUserspace` 而非直接依赖 `entry` -`kruntime` 被 `entry` 依赖。若 `kruntime` 再依赖 `entry` 会形成环。`SystemInitEntry` 把 handoff 变成一个 `kiface` 单实现接口:`kruntime` 拥有调用契约,`entry` 提供策略实现,同时避免依赖裸符号名和 `extern "C"` 调用。 +`kruntime` 被 `entry` 依赖。若 `kruntime` 再依赖 `entry` 会形成环。`SystemUserspace` 是一个 `kiface` 单实现接口:`kruntime` 拥有 supervisor 与 teardown 顺序,provider 只负责用户态的 start/shutdown 操作。这使 `kruntime` 无需依赖 `entry`、`kprocess`、`ksignal` 或 `kexec`,也不需要新的全局 callback 字段。 ### 为何主核才跑 `init_cb` -`.init_array` 回调假定中断子系统已注册、且尚未进入多任务应用阶段;放在 `init_interrupt` 之后、调用 `SystemInitEntry::enter()` 之前,与 C 运行时 constructor 时机相近,但由内核显式控制调用点。 +`.init_array` 回调假定中断子系统已注册、且尚未进入多任务应用阶段;放在 `init_interrupt` 之后、调用 `SystemUserspace::start()` 之前,与 C 运行时 constructor 时机相近,但由内核显式控制调用点。 ### 为何 DMA 接线放在独立模块 diff --git a/core/kruntime/docs/security.md b/core/kruntime/docs/security.md index 86d26788943b66fbf795bed49cfe070384bf2d59..ecd9c46bf98bdb58ea7e5c2d333ad47b26f7bd7f 100644 --- a/core/kruntime/docs/security.md +++ b/core/kruntime/docs/security.md @@ -2,7 +2,7 @@ ## 信任模型 -`kruntime` 是平台引导层和内核运行时之间的可信编排层。它不直接解析 syscall 参数或用户指针,但它接收 boot 层传入的 `BootInfo`,安装早期内存、调度、中断和驱动运行环境,并在所有已发现 CPU 完成 runtime 初始化后调用 `SystemInitEntry::enter()`。 +`kruntime` 是平台引导层和内核运行时之间的可信编排层。它不直接解析 syscall 参数或用户指针,但它接收 boot 层传入的 `BootInfo`,安装早期内存、调度、中断和驱动运行环境,并在所有已发现 CPU 完成 runtime 初始化后调用 `SystemUserspace::start()`。 ``` kernel-boot / 平台固件 @@ -16,7 +16,9 @@ kernel-boot / 平台固件 │ late_init_main │ │ drivers / fs / net / SMP / .init_array │ │ INITED_CPUS barrier │ -│ SystemInitEntry::enter() -> spawn PID 1 │ +│ SystemUserspace::start() -> terminal task │ +│ join task -> userspace/fs/device teardown │ +│ -> halt or power-off │ │ rust_main_secondary │ │ AP runtime init -> INITED_CPUS -> run_idle │ └──────────────────────────────────────────────────┘ @@ -24,8 +26,11 @@ kernel-boot / 平台固件 - **boot 层 / 固件**必须提供可读且语义正确的 `BootInfo`、CPU 拓扑、内存区域和可选 boot console MMIO 描述。 - **链接脚本和注册宏**必须提供有效的 `_stext` / `_etext`、`__init_array_start` / `__init_array_end`,并保证 `.init_array` 只包含 `extern "C" fn()` 条目。 -- **`kiface` exactly-one provider**保证 `PrimaryKernelEntry`、`SecondaryKernelEntry`、`LoggerAdapter`、`DmaPageTableIf` 和 `SystemInitEntry` 在链接期只有一个实现。 -- **`entry` provider**负责策略层启动:`SystemInitEntry::enter()` 必须发布并激活 PID 1 init 任务后返回。 +- **`kiface` exactly-one provider**保证 `PrimaryKernelEntry`、`SecondaryKernelEntry`、`LoggerAdapter`、`DmaPageTableIf` 和 `SystemUserspace` 在链接期只有一个实现。 +- **`entry` provider**负责终端 workload 策略:生产构建的 `SystemUserspace::start()` + 必须发布并激活 PID 1 init 任务,unittest 构建返回测试 runner;`shutdown()` 必须在 + mount teardown 前释放相应 exec cache capability,并返回 terminal mode。生产路径还 + 必须等待余下进程完成自身退出。 ## 外部边界 / 攻击面 @@ -99,7 +104,7 @@ SMP TLB shootdown 单测使用 volatile 读写验证远端 CPU 是否看到页 ### panic handler(`src/lang_items.rs`) -panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕获 backtrace 可能再次失败。当前策略是打印 panic 信息和 backtrace,然后调用 `khal::power::shutdown()` 终止系统。 +panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕获 backtrace 可能再次失败。当前策略是打印 panic 信息和 backtrace,然后调用 `khal::power::power_off()` 终止系统。 ## 内存安全不变量 @@ -108,14 +113,19 @@ panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕 - `.init_array` 边界必须位于已映射内核镜像中,并且不会被可写数据覆盖。 - 从核 boot stack 数组必须至少覆盖被实际启动的 AP 数量;超过 `NR_CPUS - 1` 的 present CPU 不会被 `kruntime` 启动。 - `memspace::init_memory_management()` 之后,`DmaPageTableIf::protect()` 只能通过 `memspace` 持有内核地址空间锁来修改映射属性。 -- PID-less late-init 线程和普通内核 worker 不进入 root PID allocator;PID 1 只能由 `SystemInitEntry::enter()` 中的 init spawn 消耗并断言。 +- PID-less late-init 线程和普通内核 worker 不进入 root PID allocator;PID 1 只能由 `SystemUserspace::start()` 中的 init spawn 消耗并断言。 +- 正常平台 halt/power-off 只能发生在 supervisor 已观察 terminal task `Exited`、provider + 已完成对应 owner cleanup、初始 mount namespace teardown 和设备 quiesce/remove 之后; + PID 1 和 unittest runner 都不得直接调用 HAL terminal operation。 ## 线程安全 -- `INITED_CPUS` 使用 `Release`/`Acquire` 作为全局 runtime-ready 屏障。主核 late-init 线程和每个从核完成本地 runtime 初始化后递增;`SystemInitEntry::enter()` 和依赖跨 CPU 调度的 late-start worker 只在屏障满足后运行。 +- `INITED_CPUS` 使用 `Release`/`Acquire` 作为全局 runtime-ready 屏障。主核 late-init 线程和每个从核完成本地 runtime 初始化后递增;`SystemUserspace::start()` 和依赖跨 CPU 调度的 late-start worker 只在屏障满足后运行。 - `ENTERED_CPUS` 只表达 AP 已进入 `rust_main_secondary`,不代表 AP 初始化完成;它与 `INITED_CPUS` 分离,避免把“可启动下一个 AP”和“全局 runtime ready”混在一起。 - `LoggerAdapter::cpu_id()` 和 `task_id()` 在 `is_init_ok()` 前避免读取可能未就绪的 current task / CPU-local 运行时状态;非 SMP 构建下 CPU ID 固定为 0。 - 从核在 `INITED_CPUS` 屏障后才启用 IPI/PMU IRQ、本地 IRQ 和 watchdog,并最终进入 `ktask::run_idle()`。 +- 正常 HALT 对每个已接受的远端 stop callback 使用 Release/Acquire 确认;远端先关闭 + 本地 IRQ 并发布已进入状态,当前 CPU 观察确认后才调用本地平台 halt。 - `DmaPageTableIf::protect()` 通过 `memspace::kernel_layout().lock()` 串行化内核页表属性更新。 ## 威胁分析 @@ -126,12 +136,13 @@ panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕 | T-02 | `BootInfo` 内存表或 CPU 拓扑错误 | 高 | boot 层 bug、固件表异常、虚拟化环境错误 | 由 `khal::firmware` / `khal::mem` / `kcpu_id_map` 建立运行时视图;后续初始化只消费该视图 | | T-03 | AP 未完成初始化时进入系统 init 或跨 CPU worker | 中 | `INITED_CPUS` 计数错误、CPU 数来源错误 | 等待 `INITED_CPUS == kcpu_id_map::nr_cpus()`;计数使用 Release/Acquire | | T-04 | 从核使用错误或重叠 boot stack | 高 | AP 启动循环越界、并发启动复用槽位 | `secondary_cpu_index < NR_CPUS - 1`;串行 `boot_ap` + `ENTERED_CPUS` 握手;每 AP 独立槽 | -| T-05 | PID 1 被提前消耗 | 中 | late-init 线程或后台 worker 获得 Linux-visible identity;`SystemInitEntry` 未先创建 init | boot、idle、late-init 和普通内核 worker 使用 PID-less identity;init 创建路径断言 root PID 为 1 | +| T-05 | PID 1 被提前消耗 | 中 | late-init 线程或后台 worker 获得普通进程身份;`SystemUserspace` 未先创建 init | boot、idle、late-init 和普通内核 worker 使用 PID-less identity;init 创建路径断言 root PID 为 1 | | T-06 | IRQ 在 handler 注册前启用 | 高 | `init_interrupt` 顺序被改坏、平台提前开 IRQ | 主核先安装 softirq hardirq-exit runner,再注册 timer/IPI/PMU handler,最后开本地 IRQ;从核初始化完成后再开 IRQ | | T-07 | per-CPU timer deadline raw access 在可迁移上下文运行 | 中 | handler 被改为普通线程调用或抢占语义变化 | unsafe 注释要求 IRQ/preemption 禁止迁移;审计 raw percpu 调用点 | | T-08 | `DmaPageTableIf::protect` 修改非法内核 VA 范围 | 高 | `kdma` 调用方传入错误地址或长度 | 委托 `memspace` 的内核 layout 锁和页表校验 | | T-09 | panic handler 在损坏栈上再次失败 | 中 | 栈溢出、页表损坏、backtrace 范围错误 | panic 后只做诊断并关机,不尝试恢复 | | T-10 | 启动日志泄露物理地址和设备映射 | 低 | 开启启动日志 | 限于内核日志;不处理用户数据,但部署时需按日志策略限制可见性 | +| T-11 | 正常关机绕过进程和 mount 生命周期 | 高 | PID 1、unittest runner 或 reboot syscall 直接调用 HAL terminal operation | provider 返回 terminal task;PID-less supervisor `join` 后依次调用 userspace、namespace、device 和 platform owner | ## 故障模式与影响分析(FMEA) @@ -142,15 +153,18 @@ panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕 | F-03 | 某从核未递增 `ENTERED_CPUS` | `boot_ap` 失败、固件未唤醒 AP、AP trap | 主核卡在 AP bring-up | late init 无法继续 | 2 | `start_secondary_cpus()` 返回 `boot_ap` 错误;进入 runtime 失败时依赖平台日志/调试 | | F-04 | 某 CPU 未递增 `INITED_CPUS` | 从核 init hang、主核 late init panic | 全局 ready 屏障不满足 | 系统 init 不启动或从核不进 idle | 2 | 启动日志;watchdog feature 或外部复位 | | F-05 | `register_init` 回调 panic | 子系统 init bug | `.init_array` 遍历中断 | 后续 init 不执行,panic 后关机 | 2 | 回调保持短小、无阻塞;新增回调需审计 panic 路径 | -| F-06 | `SystemInitEntry` provider 缺失或重复 | `entry` 未提供实现或多个实现冲突 | 链接失败 | 无法生成镜像 | 2 | `kiface` exactly-one provider 构建期发现 | +| F-06 | `SystemUserspace` provider 缺失或重复 | `entry` 未提供实现或多个实现冲突 | 链接失败 | 无法生成镜像 | 2 | `kiface` exactly-one provider 构建期发现 | | F-07 | PID 1 断言失败 | Linux-visible task 在 init 前抢先消耗 root PID | `assert_eq!` panic | 启动中止 | 2 | boot、idle、late-init 和普通内核 worker 使用 PID-less identity | | F-08 | region 日志摘要数组溢出 | distinct region name/flags 超过 64 | `expect("too many region log summaries")` panic | 启动日志阶段失败 | 3 | `MAX_REGION_LOG_SUMMARIES = 64`;异常平台需扩大上限或调整汇总策略 | | F-09 | timer handler 重复立刻触发或 deadline 倒退 | 时间源异常或 deadline 更新逻辑被改坏 | tick 过密、调度异常 | 性能下降或 hang | 2 | deadline 取当前记录与 `now + interval` 的较大值,再写入下一 tick | +| F-10 | 初始 mount namespace teardown 失败 | mount registry/topology 不变量被破坏 | 部分 superblock final shutdown 不可达 | 断电后文件系统需恢复 | 1 | supervisor 记录明确错误;teardown 复用 KVFS 校验与 final active-mount owner 路径 | +| F-11 | PID 1 leader task 已退出但 sibling thread 仍存活 | init 为多线程且 leader 先完成 | 活跃 runtime owner 固定 mount | namespace teardown 阻塞或与活跃用户态并发 | 1 | `SystemUserspace::shutdown()` 等待 PID 1 process completion;completion 在最后线程释放 runtime owner 后发布 | +| F-12 | 远端 CPU stop callback 无法投递 | 目标 CPU 的 IPI queue 未就绪或已满 | 无法取得该 CPU 的 halt 确认 | 平台 halt 时仍可能有残留 CPU | 2 | 记录明确 CPU ID 和 IPI 错误;成功投递的 callback 必须确认进入,硬件异常依赖 watchdog 或外部复位 | ## 故障管理 - **启动期错误快速失败**:关键路径使用 `assert!`、`expect` 或 `panic!`,避免在未完整初始化的内核中降级运行。 -- **panic 后关机**:panic handler 打印 panic 信息和 backtrace 后调用 `khal::power::shutdown()`;本 crate 不尝试恢复。 +- **panic 后关机**:panic handler 打印 panic 信息和 backtrace 后调用 `khal::power::power_off()`;本 crate 不尝试恢复。 - **SMP hang 无本地超时**:`ENTERED_CPUS` / `INITED_CPUS` 等待使用自旋,若 AP 或主核 late init 卡死,需要 watchdog feature、平台日志或外部复位介入。 - **feature 关闭是配置选择**:如 `fs`、`net`、`watchdog`、`pmu` 未启用时跳过对应初始化,不作为运行时故障处理。 @@ -170,8 +184,8 @@ panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕 1. **`.init_array` 无单条隔离**:一个回调 panic 会中断后续回调;没有 per-callback recovery。 2. **SMP 等待无超时**:AP 启动或 init 屏障失败会导致自旋等待。 -3. **系统 init 仅由主核 late-init 线程触发**:从核初始化完成后只进入 idle,不执行 `SystemInitEntry`。 -4. **PID 1 语义依赖 `SystemInitEntry` 合约**:`enter()` 必须创建、发布并激活 root PID 1 init。 +3. **系统 init 仅由主核 late-init/supervisor 线程触发**:从核初始化完成后只进入 idle,不执行 `SystemUserspace`。 +4. **PID 1 语义依赖生产 `SystemUserspace` 合约**:生产 `start()` 必须创建、发布并激活 root PID 1 init,并返回对应 task handle;unittest provider 返回独立 runner task。 5. **启动日志摘要是启发式**:只有名称以 `uefi ` 开头且数量达到阈值的区域会汇总,可能隐藏单个区域的细节。 ## 审计清单 @@ -180,10 +194,14 @@ panic handler 本身是 safe Rust,但在已损坏栈或已损坏页表上捕 - [ ] 新增或移动 `unsafe` 块时,同步说明真实不变量、调用上下文和失败影响。 - [ ] 调整 `rust_main` 顺序时,重新检查 allocator、memspace、driver resource provider、logger、scheduler、IRQ 的依赖。 -- [ ] 调整 `late_init_main` 顺序时,重新检查 `.init_array`、SMP ready 屏障、vsock/TIPC bridge 和 `SystemInitEntry` 的顺序。 +- [ ] 调整 `late_init_main` 顺序时,重新检查 `.init_array`、SMP ready 屏障、vsock/TIPC bridge 和 `SystemUserspace` 的顺序。 - [ ] 修改 `INITED_CPUS` / `ENTERED_CPUS` 时,区分“AP 已进入 runtime”和“所有 CPU runtime ready”,并检查 Release/Acquire 配对。 - [ ] 新增 early/late kernel thread 时,确认它是否应为 PID-less `Internal`,不能抢先消耗 PID 1。 -- [ ] 修改 `SystemInitEntry` provider 时,确认它 spawn 并激活 PID 1 后返回。 +- [ ] 修改 `SystemUserspace` provider 时,确认 `start()` 返回的 handle 正是 supervisor + 应等待的 terminal task;生产 `shutdown()` 等待 PID 1 process completion,所有构建都在 + namespace teardown 前释放对应 filesystem owner。 +- [ ] 新增正常 halt/power-off 入口时,确认它只请求 supervisor 路径,不直接调用 HAL;panic、 + fatal boot failure 等 emergency path 必须与 orderly contract 明确区分。 - [ ] 新增 `register_init` 回调时,确认它不依赖用户态 init、避免长时间阻塞,并接受 panic 会中止启动。 - [ ] 修改 timer/IPI/PMU handler 时,确认 handler 注册先于 IRQ enable,raw per-CPU 访问仍只发生在不可迁移上下文。 - [ ] 修改 DMA protect 接线时,确认仍由 `memspace` 统一持锁和校验内核页表范围。 diff --git a/core/kruntime/src/lang_items.rs b/core/kruntime/src/lang_items.rs index 65466b76a16f17d669d07ab9bd83b74348422d60..ee6f2edee721b25e3a91fde8cbf1d0feee5f1ec6 100644 --- a/core/kruntime/src/lang_items.rs +++ b/core/kruntime/src/lang_items.rs @@ -9,5 +9,5 @@ use core::panic::PanicInfo; fn panic(info: &PanicInfo) -> ! { kprintln!("{}", info); kprintln!("{}", backtrace::Backtrace::capture()); - khal::power::shutdown() + khal::power::power_off() } diff --git a/core/kruntime/src/lib.rs b/core/kruntime/src/lib.rs index f0156774071b18a008818d5776501c33c9f77fbe..aa95f4ed76c1739748f719a4816f383c4989e9a3 100644 --- a/core/kruntime/src/lib.rs +++ b/core/kruntime/src/lib.rs @@ -3,8 +3,8 @@ // See LICENSES for license details. //! Runtime orchestration for x-kernel: primary/secondary CPU bring-up, subsystem -//! init ordering, and handoff to the system-init entry supplied by the `entry` -//! crate. +//! init ordering, and handoff to the userspace lifecycle provider supplied by +//! the `entry` crate. //! //! For architecture, boot flow, and security analysis, see `docs/design.md` and //! `docs/security.md` in the crate source directory. @@ -111,19 +111,90 @@ impl klogger::LoggerAdapter { } } +#[cfg(feature = "ipi")] +use core::sync::atomic::AtomicBool; use core::sync::atomic::{AtomicUsize, Ordering}; static INITED_CPUS: AtomicUsize = AtomicUsize::new(0); -/// High-level system-init handoff performed after runtime bring-up. +#[cfg(feature = "ipi")] +fn stop_current_cpu(has_entered_halt: alloc::sync::Arc) { + karch::disable_local_irq(); + has_entered_halt.store(true, Ordering::Release); + loop { + karch::stop_cpu(); + } +} + +/// Terminal workload lifecycle handoff performed by the runtime supervisor. /// -/// The provider owns policy-level startup such as runtime feature init and -/// spawning the first user program (PID 1) as a fresh user task. The provider -/// returns once init has been published and activated. +/// Production providers create PID 1; unit-test providers create their test +/// runner. The provider owns workload-specific terminal cleanup, while the +/// runtime retains mount, device, and platform shutdown ordering. #[kiface::interface] -pub trait SystemInitEntry { - /// Spawn PID 1 and return once it is runnable. - fn enter(); +pub trait SystemUserspace { + /// Starts the terminal userspace workload and returns its runnable task. + /// + /// Production providers spawn PID 1. Unit-test providers return the test + /// runner whose completion begins the same runtime-owned teardown path. + fn start() -> ktask::KtaskRef; + + /// Stops userspace and selects the platform terminal state. + /// + /// The provider releases executable-loader owners before returning so the + /// runtime can tear down mounts and devices before applying the result. + fn shutdown() -> khal::power::ShutdownMode; +} + +fn halt_system() -> ! { + #[cfg(feature = "ipi")] + { + let current_cpu_id = khal::percpu::this_cpu_id(); + kcpu_id_map::for_each_present_logical_cpu(|_, cpu_id, _| { + if cpu_id == current_cpu_id { + return; + } + + let has_entered_halt = alloc::sync::Arc::new(AtomicBool::new(false)); + let remote_state = has_entered_halt.clone(); + match kipi::run_on_cpu(cpu_id, move || stop_current_cpu(remote_state)) { + Ok(()) => { + while !has_entered_halt.load(Ordering::Acquire) { + core::hint::spin_loop(); + } + } + Err(err) => { + warn!("failed to halt CPU {}: {err}", cpu_id.as_usize()); + } + } + }); + } + + khal::power::halt(); +} + +fn orderly_shutdown() -> ! { + let shutdown_mode = SystemUserspace::shutdown(); + + #[cfg(feature = "fs")] + if let Err(err) = fs_boot::shutdown_namespace() { + warn!("initial mount namespace teardown failed: {err:?}"); + } + + let device_manager = kdriver::device_manager(); + device_manager.quiesce_buses(); + device_manager.remove_buses(); + + match shutdown_mode { + khal::power::ShutdownMode::Halt => { + info!("Orderly shutdown complete, halting"); + halt_system() + } + khal::power::ShutdownMode::PowerOff => { + info!("Orderly shutdown complete, powering off"); + khal::power::power_off() + } + } } const MAX_REGION_LOG_SUMMARIES: usize = 64; @@ -166,9 +237,11 @@ fn is_init_ok() -> bool { /// Run late subsystem initialization on the PID-less late-init bootstrap thread. /// /// The thread starts in kernel context, brings up secondary CPUs and late -/// subsystems, then crosses [`SystemInitEntry`], which spawns PID 1 init, before -/// returning. Because the late-init thread holds an `Internal` identity and -/// allocates no root PID itself, init receives the system's first root PID. +/// subsystems, then starts the terminal workload through [`SystemUserspace`]. +/// It remains alive as the lifecycle supervisor until userspace and kernel +/// owners have been released in shutdown order. Because this thread holds an +/// `Internal` identity and allocates no root PID itself, production init +/// receives the system's first root PID. fn late_init_main(cpu_id: kcpu_id_map::LogicalCpuId) { #[cfg(feature = "smp")] { @@ -232,19 +305,24 @@ fn late_init_main(cpu_id: kcpu_id_map::LogicalCpuId) { #[cfg(feature = "vsock_tipc_bridge")] knet::start_vsock_bridge(); - // Spawn PID 1 after all PID-less late initialization has completed. - // `enter()` returns after the init task has been published and activated. - SystemInitEntry::enter(); + // Keep the PID-less bootstrap task as the system-lifecycle supervisor. PID + // 1 exits through the ordinary process/task cleanup path before mount and + // device teardown begin here. + let terminal_task = SystemUserspace::start(); + terminal_task.join(); + drop(terminal_task); + info!("Terminal workload has exited"); - // The late-init thread has finished its job; `task_entry` will exit it. + orderly_shutdown(); } /// Prepare the PID-less late-init bootstrap thread on the boot CPU. /// -/// This task runs all late subsystem initialization, spawns PID 1 init, then -/// exits. It carries an `Internal` identity, so it consumes no root PID number. -/// Secondary run queues are not registered at activation time, so the thread -/// is pinned to the boot CPU via a one-shot CPU mask. +/// This task runs all late subsystem initialization, starts the terminal +/// workload, and remains as the system-lifecycle supervisor until the terminal +/// platform operation. It carries an `Internal` identity, so it consumes no +/// root PID number. Secondary run queues are not registered at activation time, +/// so the thread is pinned to the boot CPU via a one-shot CPU mask. fn prepare_late_init_on_primary(cpu_id: kcpu_id_map::LogicalCpuId) -> ktask::KtaskRef { let task = ktask::TaskInner::new_pidless_kthread( move || late_init_main(cpu_id), diff --git a/core/ksyscall/docs/design.md b/core/ksyscall/docs/design.md index 1565e00bbfbb2a09312c2ef67f97352cef300978..4c54e9856aecd87f2a838784efb9b56ef368cb6b 100644 --- a/core/ksyscall/docs/design.md +++ b/core/ksyscall/docs/design.md @@ -93,7 +93,7 @@ ksyscall::dispatch_irq_syscall ├─ task adapter ───────────> kprocess / posix-process / kprocess / kcred ├─ io_mpx adapter ─────────> kfd_objects::Epoll ├─ sync adapter ───────────> kfutex / kprocess - └─ misc adapter ───────────> posix-mm / posix-net / ... + └─ misc adapter ───────────> posix-process / posix-mm / posix-net / ... ``` ## 设计原则 @@ -118,7 +118,9 @@ ksyscall::dispatch_irq_syscall - owner 在 `kfd_objects::EventFd` - `sys.rs` - `sethostname` 路由到当前 UTS namespace - - `reboot` 校验 Linux magic/command 后路由到平台 power 接口 + - `reboot` 校验 Linux magic/command 后通过 `posix-process` 请求 PID 1 退出;请求与 + init 退出竞争时不向已获授权的调用者返回错误,调用进程仍进入普通退出路径; + adapter 不直接 sync、卸载或调用平台 power - `time/timerfd.rs` - `timerfd_*` - owner 在 `kfd_objects::TimerFd` diff --git a/core/ksyscall/docs/security.md b/core/ksyscall/docs/security.md index 34e8123d9e8fcc017c0d8ffaddb2269f44e2b9e7..d8b61fabe3a43efdd35448b73cd17ee65dcdb15e 100644 --- a/core/ksyscall/docs/security.md +++ b/core/ksyscall/docs/security.md @@ -55,7 +55,7 @@ resource owners | T-06 | `setpriority` 通过进程代表线程漏检目标身份 | 高 | per-thread credential 下仍按 process representative 授权 | `PRIO_*` 选择和扫描均落到具体 task,逐 task 比较 caller euid 与 target real/effective UID,并单独检查提高优先级权限 | | T-06a | 非特权进程改写任意任务 affinity | 高 | `sched_setaffinity` 对非 current 直接 `set_cpumask` | 解析目标后按 `check_same_owner` 语义比较 caller euid 与 target ruid/euid;root(近似 `CAP_SYS_NICE`)可绕过,否则 `EPERM` | | T-07 | 非特权进程修改主机名 | 高 | `sethostname` 直接写 UTS namespace | syscall 边界检查 privileged credential,并限制 nodename 长度与用户缓冲区访问 | -| T-08 | 非特权进程改变电源状态 | 高 | `reboot` 直接进入平台 power 接口 | 检查 privileged credential、Linux magic 和受支持命令集合 | +| T-08 | 非特权进程改变电源状态或绕过正常 teardown | 高 | `reboot` 直接进入平台 power 接口 | 检查 privileged credential、Linux magic 和命令集合后,请求终止 PID 1 并退出调用进程;signal 与 init 退出竞争时只记内核日志,不把已授权 power-off 变成 syscall 错误;进程、mount、设备生命周期由 supervisor 继续执行 | | T-09 | 非特权进程修改墙钟 | 高 | wall-clock setter 直接更新 realtime 时钟关联 | `settimeofday` 与 `clock_settime` 在共享 setter 中检查 privileged credential,并拒绝把墙钟移到 CLOCK_MONOTONIC 之前 | | T-10 | `PR_SET_KEEPCAPS` 传入非法值或绕过锁定位 | 中 | `ctl.rs` 拒绝大于 1 的设置值,`kcred::Cred::keep_caps_enable()` / `keep_caps_disable()` 校验锁定位并通过 prepared credential 一次提交 | diff --git a/core/ksyscall/src/io_mpx/select.rs b/core/ksyscall/src/io_mpx/select.rs index e94e068bd5c96c54949f3e9c43de7ebc88087f84..86f5a9a677eaee7d637fa2b08dfec12aff70f770 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/sys.rs b/core/ksyscall/src/sys.rs index 26c00e133abe14f9c373399a94857bfc3c0a8a19..496d2dc6cafd881d21f4dbd1ece638ca64864bcc 100644 --- a/core/ksyscall/src/sys.rs +++ b/core/ksyscall/src/sys.rs @@ -163,6 +163,9 @@ pub fn sys_sethostname(name: UserConstPtr, len: usize) -> KResult { /// toggles are handled here; other commands (`RESTART`, `RESTART2`, `KEXEC`, /// `SW_SUSPEND`) are rejected with `EINVAL`, since `reboot(2)` returns /// `EINVAL` — not `ENOSYS` — for an unsupported command. +/// +/// Terminal commands request the system lifecycle supervisor; this adapter +/// does not perform filesystem or platform teardown in the calling process. pub fn sys_reboot( magic1: u32, magic2: u32, @@ -189,10 +192,16 @@ pub fn sys_reboot( // CAD-state variable yet, so these are accepted as an intentional stub. LINUX_REBOOT_CMD_CAD_ON | LINUX_REBOOT_CMD_CAD_OFF => Ok(0), LINUX_REBOOT_CMD_HALT | LINUX_REBOOT_CMD_POWER_OFF => { - // TODO: flush/sync filesystems (e.g. sys_sync) before pulling the - // plug; `shutdown()` never returns, so cleanup must happen first. - warn!("reboot: initiating platform shutdown (command {command:#x})"); - khal::power::shutdown() + let shutdown_mode = if command == LINUX_REBOOT_CMD_HALT { + khal::power::ShutdownMode::Halt + } else { + khal::power::ShutdownMode::PowerOff + }; + warn!("reboot: requesting orderly {shutdown_mode:?}"); + let process = kprocess::current_user_process(); + posix_process::request_shutdown(shutdown_mode, &process); + posix_process::do_exit(0, true); + Ok(0) } _ => Err(KError::InvalidInput), } diff --git a/core/ksyscall/src/task/pidfd.rs b/core/ksyscall/src/task/pidfd.rs index 0bacde16b3fa7ac774a497dc203a336d77ff2a8b..6a531d728fe4101241181b5f2e3c71270f482e9c 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/entry/Cargo.toml b/entry/Cargo.toml index 99d542051e391ef075444b11f9e16c918afce6ce..c804bd80cf4944a02e9c7847510be00bfe34f40b 100644 --- a/entry/Cargo.toml +++ b/entry/Cargo.toml @@ -23,6 +23,7 @@ kiface.workspace = true klogger.workspace = true kruntime.workspace = true ktask.workspace = true +kexec.workspace = true kprocess.workspace = true kuaccess.workspace = true ksyscall.workspace = true diff --git a/entry/src/main.rs b/entry/src/main.rs index bfd9e2f648dfb3b87b30a8a9a111037c08a37252..0ca9fcc4b70fe19ad1ab6444935a90b69a0b9c5a 100644 --- a/entry/src/main.rs +++ b/entry/src/main.rs @@ -20,10 +20,23 @@ mod runtime; mod unittest_simple; #[kiface::provide] -impl kruntime::SystemInitEntry { - fn enter() { +impl kruntime::SystemUserspace { + fn start() -> ktask::KtaskRef { kernel_main() } + + fn shutdown() -> khal::power::ShutdownMode { + #[cfg(not(feature = "unittest"))] + { + posix_process::shutdown_userspace() + } + + #[cfg(feature = "unittest")] + { + kexec::clear_elf_cache(); + khal::power::ShutdownMode::PowerOff + } + } } #[cfg(feature = "unittest")] @@ -70,7 +83,7 @@ fn print_boot_info() { } #[cfg(not(feature = "unittest"))] -fn kernel_main() { +fn kernel_main() -> ktask::KtaskRef { use alloc::{borrow::ToOwned, vec::Vec}; print_boot_info(); @@ -96,33 +109,13 @@ fn kernel_main() { .collect::>(); let envs = []; - // Spawn PID 1 as a fresh user task and return. This runs on the PID-less - // late-init bootstrap thread, which is not transformed into init. - posix_process::spawn_init_process(&args, &envs, ksyscall::dispatch_irq_syscall, || { - if let Err(err) = kvfs::sync_filesystems() { - warn!("sync filesystems after init exit failed: {err:?}"); - } - if let Ok(namespace) = kvfs::MntNamespace::initial() { - let root = namespace.visible_root_path(); - if let Err(err) = namespace.detach_tree(&root) { - warn!("unmount all filesystems failed: {err:?}"); - } - if let Err(err) = root.sync_filesystem() { - warn!("flush rootfs failed: {err:?}"); - } - } - info!("Init process finished, powering off..."); - khal::power::shutdown(); - }); + // Spawn PID 1 as a fresh user task and return its lifecycle handle to the + // PID-less runtime supervisor. + posix_process::spawn_init_process(&args, &envs, ksyscall::dispatch_irq_syscall) } #[cfg(feature = "unittest")] -fn kernel_main() { - use alloc::{sync::Arc, vec::Vec}; - use core::sync::atomic::{AtomicBool, Ordering}; - - use ktask::spawn; - +fn kernel_main() -> ktask::KtaskRef { print_boot_info(); runtime::init_runtime(); @@ -146,65 +139,122 @@ fn kernel_main() { } } - let finished = Arc::new(AtomicBool::new(false)); - let finished_clone = finished.clone(); + ktask::spawn(|| { + run_unit_tests(); + write_unittest_coverage(); + info!("Unit tests completed"); + }) +} + +#[cfg(feature = "unittest")] +fn run_unit_tests() { + use alloc::{sync::Arc, vec::Vec}; + use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use ktask::spawn as task_spawn; + use unittest::{TestResult, Testable}; + + let crate_filter = unittest_crate_filter(); + if let Some(crate_filter) = crate_filter { + unittest::ktest_println!("Running unit tests with crate filter: {}", crate_filter); + } + + let grouped = unittest::collect_tests(crate_filter); - spawn(move || { - use core::sync::atomic::AtomicUsize; + if grouped.is_empty() { + unittest::ktest_println!("No tests found!"); + unittest::ktest_println!("=== UNITTEST_STATUS: TESTS_FAILED ==="); + return; + } - use ktask::spawn as task_spawn; - use unittest::{TestResult, Testable}; + let total_tests: usize = grouped.values().map(|v| v.len()).sum(); + unittest::ktest_println!("================================"); + unittest::ktest_println!("Starting unit tests [unittest] (parallel)..."); + unittest::ktest_println!(" {} module(s), {} test(s)", grouped.len(), total_tests); + unittest::ktest_println!("================================"); - let crate_filter = unittest_crate_filter(); - if let Some(crate_filter) = crate_filter { - unittest::ktest_println!("Running unit tests with crate filter: {}", crate_filter); - } + let passed = Arc::new(AtomicUsize::new(0)); + let failed = Arc::new(AtomicUsize::new(0)); + let ignored = Arc::new(AtomicUsize::new(0)); - let grouped = unittest::collect_tests(crate_filter); + // Flatten to owned descriptors so test execution never reads the + // writable linker registration section after discovery. + let flat: Vec = grouped + .iter() + .flat_map(|(_, tests)| tests.iter().copied()) + .collect(); - if grouped.is_empty() { - unittest::ktest_println!("No tests found!"); - unittest::ktest_println!("=== UNITTEST_STATUS: TESTS_FAILED ==="); - finished_clone.store(true, Ordering::Release); - return; - } + for (module, tests) in &grouped { + unittest::ktest_println!(" [{}] ({} tests)", module, tests.len()); + } - let total_tests: usize = grouped.values().map(|v| v.len()).sum(); - unittest::ktest_println!("================================"); - unittest::ktest_println!("Starting unit tests [unittest] (parallel)..."); - unittest::ktest_println!(" {} module(s), {} test(s)", grouped.len(), total_tests); - unittest::ktest_println!("================================"); - - let passed = Arc::new(AtomicUsize::new(0)); - let failed = Arc::new(AtomicUsize::new(0)); - let ignored = Arc::new(AtomicUsize::new(0)); - - // Flatten to owned descriptors so test execution never reads the - // writable linker registration section after discovery. - let flat: Vec = grouped - .iter() - .flat_map(|(_, tests)| tests.iter().copied()) - .collect(); - - for (module, tests) in &grouped { - unittest::ktest_println!(" [{}] ({} tests)", module, tests.len()); + // Split into serial and parallel tests. + // Serial: explicitly marked serial OR user execution mode. + // Only Standard-mode tests without serial flag run in parallel. + let (serial_tests, parallel_tests): ( + Vec, + Vec, + ) = flat + .into_iter() + .partition(|t| t.serial || t.execution_mode != unittest::TestExecutionMode::Standard); + let mut failed_serial_tests = Vec::new(); + + // Run serial tests sequentially first + if !serial_tests.is_empty() { + unittest::ktest_println!(" Running {} serial test(s)...", serial_tests.len()); + for test in &serial_tests { + let module_name = test.module; + let test_name = test.name(); + print_test_start(module_name, test_name); + let result = test.run(); + print_test_result(module_name, test_name, result); + match result { + TestResult::Ok => { + passed.fetch_add(1, Ordering::Relaxed); + } + TestResult::Failed => { + failed.fetch_add(1, Ordering::Relaxed); + failed_serial_tests.push(*test); + } + TestResult::Ignored => { + ignored.fetch_add(1, Ordering::Relaxed); + } + } } + } + + // Run parallel tests on a bounded worker set. Spawning every test at + // once makes the runner sensitive to task-lifetime bugs and obscures + // the failing descriptor when a worker faults before printing a result. + let parallel_tests = Arc::new(parallel_tests); + let parallel_test_failed = Arc::new( + (0..parallel_tests.len()) + .map(|_| AtomicBool::new(false)) + .collect::>(), + ); + let next_test = Arc::new(AtomicUsize::new(0)); + let worker_count = core::cmp::min( + parallel_tests.len(), + core::cmp::max(1, kcpu_id_map::nr_cpus() * 4), + ); + let mut tasks: Vec = Vec::with_capacity(worker_count); + + for _ in 0..worker_count { + let tests = parallel_tests.clone(); + let next = next_test.clone(); + let p = passed.clone(); + let f = failed.clone(); + let ig = ignored.clone(); + let test_failed = parallel_test_failed.clone(); + + tasks.push(task_spawn(move || { + loop { + let test_idx = next.fetch_add(1, Ordering::Relaxed); + if test_idx >= tests.len() { + break; + } - // Split into serial and parallel tests. - // Serial: explicitly marked serial OR user execution mode. - // Only Standard-mode tests without serial flag run in parallel. - let (serial_tests, parallel_tests): ( - Vec, - Vec, - ) = flat - .into_iter() - .partition(|t| t.serial || t.execution_mode != unittest::TestExecutionMode::Standard); - let mut failed_serial_tests = Vec::new(); - - // Run serial tests sequentially first - if !serial_tests.is_empty() { - unittest::ktest_println!(" Running {} serial test(s)...", serial_tests.len()); - for test in &serial_tests { + let test = &tests[test_idx]; let module_name = test.module; let test_name = test.name(); print_test_start(module_name, test_name); @@ -212,115 +262,59 @@ fn kernel_main() { print_test_result(module_name, test_name, result); match result { TestResult::Ok => { - passed.fetch_add(1, Ordering::Relaxed); + p.fetch_add(1, Ordering::Relaxed); } TestResult::Failed => { - failed.fetch_add(1, Ordering::Relaxed); - failed_serial_tests.push(*test); + f.fetch_add(1, Ordering::Relaxed); + test_failed[test_idx].store(true, Ordering::Release); } TestResult::Ignored => { - ignored.fetch_add(1, Ordering::Relaxed); + ig.fetch_add(1, Ordering::Relaxed); } } } - } - - // Run parallel tests on a bounded worker set. Spawning every test at - // once makes the runner sensitive to task-lifetime bugs and obscures - // the failing descriptor when a worker faults before printing a result. - let parallel_tests = Arc::new(parallel_tests); - let parallel_test_failed = Arc::new( - (0..parallel_tests.len()) - .map(|_| AtomicBool::new(false)) - .collect::>(), - ); - let next_test = Arc::new(AtomicUsize::new(0)); - let worker_count = core::cmp::min( - parallel_tests.len(), - core::cmp::max(1, kcpu_id_map::nr_cpus() * 4), - ); - let mut tasks: Vec = Vec::with_capacity(worker_count); - - for _ in 0..worker_count { - let tests = parallel_tests.clone(); - let next = next_test.clone(); - let p = passed.clone(); - let f = failed.clone(); - let ig = ignored.clone(); - let test_failed = parallel_test_failed.clone(); - - tasks.push(task_spawn(move || { - loop { - let test_idx = next.fetch_add(1, Ordering::Relaxed); - if test_idx >= tests.len() { - break; - } + })); + } - let test = &tests[test_idx]; - let module_name = test.module; - let test_name = test.name(); - print_test_start(module_name, test_name); - let result = test.run(); - print_test_result(module_name, test_name, result); - match result { - TestResult::Ok => { - p.fetch_add(1, Ordering::Relaxed); - } - TestResult::Failed => { - f.fetch_add(1, Ordering::Relaxed); - test_failed[test_idx].store(true, Ordering::Release); - } - TestResult::Ignored => { - ig.fetch_add(1, Ordering::Relaxed); - } - } - } - })); - } + for task in tasks { + task.join(); + } - for task in tasks { - task.join(); + let p = passed.load(Ordering::Relaxed); + let f = failed.load(Ordering::Relaxed); + let ig = ignored.load(Ordering::Relaxed); + let total = p + f + ig; + + unittest::ktest_println!(); + unittest::ktest_println!( + " >>> Test results: {} passed, {} failed, {} ignored, {} total", + p, + f, + ig, + total + ); + + let test_passed = f == 0; + if test_passed { + unittest::ktest_println!("=== UNITTEST_STATUS: ALL_TESTS_PASSED ==="); + } else { + unittest::ktest_println!("=== UNITTEST_STATUS: TESTS_FAILED ==="); + unittest::ktest_println!("=== FAILED_TESTS: {} ===", f); + for test in &failed_serial_tests { + unittest::ktest_println!(" {}::{}", test.module, test.name()); } - - let p = passed.load(Ordering::Relaxed); - let f = failed.load(Ordering::Relaxed); - let ig = ignored.load(Ordering::Relaxed); - let total = p + f + ig; - - unittest::ktest_println!(); - unittest::ktest_println!( - " >>> Test results: {} passed, {} failed, {} ignored, {} total", - p, - f, - ig, - total - ); - - let test_passed = f == 0; - if test_passed { - unittest::ktest_println!("=== UNITTEST_STATUS: ALL_TESTS_PASSED ==="); - } else { - unittest::ktest_println!("=== UNITTEST_STATUS: TESTS_FAILED ==="); - unittest::ktest_println!("=== FAILED_TESTS: {} ===", f); - for test in &failed_serial_tests { + for (test_idx, test) in parallel_tests.iter().enumerate() { + if parallel_test_failed[test_idx].load(Ordering::Acquire) { unittest::ktest_println!(" {}::{}", test.module, test.name()); } - for (test_idx, test) in parallel_tests.iter().enumerate() { - if parallel_test_failed[test_idx].load(Ordering::Acquire) { - unittest::ktest_println!(" {}::{}", test.module, test.name()); - } - } - unittest::ktest_println!("=== FAILED_TESTS_END ==="); } - - finished_clone.store(true, Ordering::Release); - }); - - // Loop until tests are finished. - // We use yield_now() to let the scheduler run the test task. - while !finished.load(Ordering::Acquire) { - ktask::yield_now(); + unittest::ktest_println!("=== FAILED_TESTS_END ==="); } +} + +#[cfg(feature = "unittest")] +fn write_unittest_coverage() { + use alloc::vec::Vec; info!("Writing LLVM coverage data to /.llvm-cov/default.profraw ..."); let mut cov = Vec::new(); @@ -367,7 +361,4 @@ fn kernel_main() { } else { info!("No coverage data to write."); } - - info!("Unit tests completed, shutting down..."); - khal::power::shutdown(); } diff --git a/fs/boot/docs/design.md b/fs/boot/docs/design.md index 01a197c76d8e86d4a8e211ab33055d72b0e6b150..dd65d31f2cf775a969a31588f35f2362679a6ddc 100644 --- a/fs/boot/docs/design.md +++ b/fs/boot/docs/design.md @@ -37,6 +37,12 @@ visible root。bootstrap rootfs 保留在覆盖层下,对应 Linux 初始 root 用户态启动前,真实 root 上再挂同一个共享 devtmpfs 以及 tmpfs、procfs、sysfs 和可选 bpffs。devtmpfs/sysfs 的 internal mount 保证可见 mount 卸载后内核维护的树仍然存活。 +关机路径与上述构造对称:`shutdown_namespace()` 先把静态 `INIT_FS` 的 root/pwd 成对退回 +initial namespace 的 structural nullfs root,释放全局 `Path -> Mount` owner;随后调用 +`MntNamespace::unmount_all()` 逐层摘除真实 root、bootstrap rootfs 及其子树。boot 层只拥有 +初始 namespace policy,不调用 `sync_fs()` 或文件系统私有清理;最后一个 `Mount` 释放后 +由 KVFS 的 superblock active 生命周期执行 writeback、dcache eviction 和第二次 sync。 + ## 算法流程 1. 注册 root filesystem type 与内建 nodev filesystem types。 @@ -48,9 +54,17 @@ bpffs。devtmpfs/sysfs 的 internal mount 保证可见 mount 卸载后内核维 6. 把真实 root overmount 到 bootstrap root,并成对更新 init root/pwd。 7. 在真实 root 上安装启动期虚拟文件系统。 +终止流程: + +1. 系统生命周期 supervisor 等待 PID 1 完成普通进程退出。 +2. 把 `INIT_FS` root/pwd 切到 structural root。 +3. 从 visible root 开始递归 detach,每次恢复被覆盖的下一层,直到只剩 structural root。 +4. 由 `Mount`/`SuperBlock` owner drop 自然完成最终 filesystem shutdown。 + ## 所有权与并发 -initial namespace 由 boot CPU 串行建立。`block::BlockDevice` 的发布与 `dev_t` lookup 由 +initial namespace 由 boot CPU 串行建立,并由同一个 PID-less runtime supervisor 在 +用户态退出后串行拆除。`block::BlockDevice` 的发布与 `dev_t` lookup 由 block core 拥有;boot 只持有选择和 mount 期间的 `Arc`。root filesystem implementation selection 只有链接期 provider,运行时 type identity 只有 KVFS registry。 @@ -64,3 +78,5 @@ selection 只有链接期 provider,运行时 type identity 只有 KVFS registr - boot 不引用 ext4/FAT crate,也不维护 filesystem name switch。 - 空容量 loop disk 不参加 root fallback,但仍是正常 block-core 对象。 - 固定虚拟文件系统路径属于 initial namespace policy,保持显式。 +- 关机只撤销 initial namespace 和 `INIT_FS` 的 owner;sync/eviction 算法仍唯一归属 + `SuperBlock` final active-mount shutdown,不在 boot 层复制。 diff --git a/fs/boot/docs/security.md b/fs/boot/docs/security.md index a23b09e4f9812358c26e2e14cd31e41b401a625a..4005409a802c9c5ea2201b7027b8ae446316f029 100644 --- a/fs/boot/docs/security.md +++ b/fs/boot/docs/security.md @@ -23,6 +23,8 @@ root block device 名称来自内核构建配置;设备枚举和磁盘内容 - bootstrap mount namespace 初始化成功后才能取得 initial root path。 - root device handle 在 `FsContext::get_tree` 和真实 root graft 完成前保持有效。 - init `FsStruct` 的 root/pwd 在 bootstrap 和真实 root 两次切换中都成对更新。 +- 终止卸载前必须把 init `FsStruct` 的 root/pwd 成对切到 structural root;否则静态 + `INIT_FS` 会固定真实 root mount,使 superblock final shutdown 不可达。 - 所有内建 filesystem type 必须在安装用户态可见的 procfs 和开放 mount syscall 前 完成唯一注册。 - `nosuid`、`nodev`、`noexec`、`relatime` 属于具体 `Mount`;不得写入共享 @@ -46,6 +48,7 @@ root block device 名称来自内核构建配置;设备枚举和磁盘内容 | T-03 | 已挂载 backing device 被移除 | 中 | 热移除 root 或 9P 设备 | mount 持有 resident object;后续 I/O 传播设备错误,9P 移除另行告警 | | T-04 | filesystem type 列表与用户 mount 分派漂移 | 中 | boot、procfs 和 syscall 各自维护类型名 | boot 只向 KVFS 注册描述符;procfs 和 POSIX mount 都读取该注册表 | | T-05 | 启动 mount flags 错误作用到共享 superblock 或禁用 `/dev` | 高 | 把 per-mount flags 填入 statfs 后端状态,或给 `/dev` 设置 `nodev` | boot 通过 namespace attach 设置 `MountFlags`;superblock 构造器只接收 filesystem-wide flags | +| T-06 | 正常关机只做 sync、未释放初始 namespace owner | 高 | `INIT_FS` 仍持有真实 root `Path`,或 boot 层直接调用平台断电 | `shutdown_namespace()` 先 retarget `INIT_FS`,再递归 detach;实际 sync/eviction 由最后 `Mount` drop 驱动 | ## 故障模式与影响分析(FMEA) @@ -55,6 +58,7 @@ root block device 名称来自内核构建配置;设备枚举和磁盘内容 | F-02 | root mount 失败 | source lookup、I/O、格式或 feature 错误 | 无真实 root superblock | boot panic | 1 | 记录 `FsContext/get_tree` 错误后停止启动 | | F-03 | 首选设备名不存在 | 配置与硬件不一致 | 无法选择 root | boot panic | 2 | 输出配置名和候选设备,不回退到其他介质 | | F-04 | filesystem type 重名或重复注册 | 启动接线错误 | 注册返回 `ResourceBusy` | boot 在 namespace 对用户态开放前停止 | 2 | 每个配置启用的 canonical type 恰好注册一次 | +| F-05 | initial namespace teardown 失败 | mount registry/topology 不变量被破坏 | 部分 mount 未 detach | 平台关机可能丢失文件系统状态 | 1 | 返回 `VfsResult` 给 runtime supervisor 并记录失败;正常结构下 commit 前完整校验 | ## 故障管理 @@ -75,3 +79,5 @@ root/关键虚拟文件系统失败会停止启动,避免在不完整 namespac - 新启动 mount 的 flags 是否写入 `Mount`;`/dev` 是否保持可访问设备节点? - 真实 root mount 失败是否在启动用户态前终止,并保留明确错误? - 设备移除回调是否避免持锁执行 I/O? +- 关机路径是否先释放 `INIT_FS` 的 visible-root owner,且没有在 `fs_boot` 复制 + superblock sync/eviction 算法? diff --git a/fs/boot/src/lib.rs b/fs/boot/src/lib.rs index 56ad311d3a8f8198961e2f0737bf0b8a7c309280..80f0a59798eafe074a1d2eab826fec8d8b81ef9f 100644 --- a/fs/boot/src/lib.rs +++ b/fs/boot/src/lib.rs @@ -107,6 +107,21 @@ pub fn mount_virtual_filesystems() { BootVfs::initial().mount_virtual_filesystems(); } +/// Tears down the initial mount namespace after terminal userspace cleanup. +/// +/// The global init `fs_struct` is first retargeted to the namespace's private +/// structural root so it no longer pins a user-visible mount. Detaching the +/// remaining mount tree then lets `Mount` ownership drive the existing +/// superblock shutdown sequence. +pub fn shutdown_namespace() -> kvfs::VfsResult<()> { + let namespace = MntNamespace::initial()?; + let structural_root = namespace.root_path(); + fs_context::init_fs() + .lock() + .replace_root_and_pwd(structural_root.clone(), structural_root)?; + namespace.unmount_all() +} + struct BootVfs { namespace: Arc, root: Path, diff --git a/fs/filesystems/procfs/src/task_nodes/root.rs b/fs/filesystems/procfs/src/task_nodes/root.rs index 1cc3095e262396f11b41bbcf5e74b53374452138..382067b57b12f6dfda6846911b392dfbf1ddc9a2 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/fs/kvfs/docs/design.md b/fs/kvfs/docs/design.md index e7e39c4d568b1e183014a91bdf2b05f9f509040d..c3733c866668b739f976478e93e5670ba123b0e5 100644 --- a/fs/kvfs/docs/design.md +++ b/fs/kvfs/docs/design.md @@ -386,6 +386,11 @@ checkpoint 和设备状态,对应 `generic_shutdown_super()` 与 block-device `Path::unmount()` 不在 topology 层调用 `sync_fs()`,所以打开文件或其它 `Path` 持有 detached mount 时会像 Linux 一样推迟 final shutdown。shutdown 的 sync 错误被记录但不回滚已提交的 topology;清理和最终 flush 仍继续执行。 +initial namespace 的终止路径使用 `MntNamespace::unmount_all()`:其私有 structural nullfs +root 永久留作 namespace 容器,算法反复取得当前 visible root、递归 detach 整棵子树并 +恢复下一层 overmount,直到只剩 structural root。该方法只修改 mount topology/registry, +不调用 sync;收集到的 `Arc` 在 registry mutex 外释放,继续复用上述唯一的 final +active-mount shutdown。 卸载在 parent mountpoint 的 inode namespace lock 下移除 parent child 索引、恢复被覆盖的 mount,并清空 detached mount 的 parent location,使仍持有该 mount 的打开路径不能沿旧 parent 返回已经离开的 namespace。 diff --git a/fs/kvfs/docs/security.md b/fs/kvfs/docs/security.md index 0545241df982a1c477069e454077e012847711f3..cd7d08f9aa7a978af3a79501bea4dbb141e85ab9 100644 --- a/fs/kvfs/docs/security.md +++ b/fs/kvfs/docs/security.md @@ -220,6 +220,7 @@ lower filesystem lock;在推广此类嵌套前还需要明确的跨文件系 | T-38 | `listxattr` 为不可见名称或属性值建立中间副本,造成不必要的内存放大 | 中 | 后端先返回拥有 value 的属性向量,KVFS 再过滤 `trusted.*` | `InodeOperations::list_xattrs` 通过 borrowed name sink 输出;`Path` 在流中先过滤 `trusted.*`,调用者只接收可见名称 | | T-39 | immutable 或 append-only inode 的 xattr 仍可被修改 | 中 | namespace 特例在通用 inode 状态检查前直接授权,或文件系统未把磁盘 inode flags 映射到 KVFS | `check_xattr_permission()` 在所有 namespace 分支前检查 `NodeFlags::IMMUTABLE/APPEND_ONLY`,set/remove 统一返回 `EPERM`;具体 bridge 必须在建立 VFS inode identity 时映射后端 flags | | T-40 | inode 初始化或驱逐竞争产生第二个 resident identity,或全 cache 唤醒形成惊群 | 高 | cache miss 在构造后才占 slot、Weak upgrade 失败立即重建,或所有 inode 共用一个等待队列 | cache 先发布 `New`;并发 initializer 在该 slot 等待;最后引用 drop 发布 `Freeing` 后再调用 hook;同号 lookup 等待 entry 删除并重试;每 slot 队列只唤醒同号等待者,后端不接收 `EINVAL` 风格的竞争错误 | +| T-41 | initial namespace 终止卸载遗漏 hidden root 层 | 高 | 只 detach 一次 visible root,未继续处理 covers stack | `MntNamespace::unmount_all()` 反复 detach visible root tree,直到 mount identity 等于 structural root;每轮复用完整 subtree registry 校验 | ## 故障模式与影响分析(FMEA) @@ -298,6 +299,8 @@ slot 在 callback 前已经存在,commit 只交换 location 和原位替换 sl - 每个 `VfsMount` 是否恰好取得和释放一次 superblock active 引用,非最后 mount 是否避免 teardown,最后一个引用是否只执行一次 shutdown。 - 递归 detach 是否包含 child overmount stack,并在修改 topology 前完成完整 registry 校验。 +- initial namespace 终止路径是否循环处理 root covers stack,且只通过 `Mount` owner drop + 进入 superblock shutdown,未从 topology 层强制调用 sync/shutdown。 - 每个 namespace mutation 是否获取父目录 exclusive lock。 - 新文件系统是否只注册一个 canonical name,且 mount lookup 与 `/proc/filesystems` 没有新增平行分支。 diff --git a/fs/kvfs/src/mount.rs b/fs/kvfs/src/mount.rs index 58fdc5172369ceba7d739ebea362d195b84cc0a0..ccb697a76673ea4e0ea5330a98df9056b648cb24 100644 --- a/fs/kvfs/src/mount.rs +++ b/fs/kvfs/src/mount.rs @@ -324,6 +324,22 @@ impl MntNamespace { drop(mounts); Ok(()) } + + /// Unmounts every user-visible filesystem from this namespace. + /// + /// The initial namespace retains its private structural root, so terminal + /// teardown repeatedly removes the visible root tree and reveals the + /// covered layer beneath it. Releasing each detached [`Mount`] naturally + /// drives final superblock deactivation after external paths are gone. + pub fn unmount_all(&self) -> VfsResult<()> { + loop { + let visible_root = self.visible_root_path(); + if Arc::ptr_eq(visible_root.mount(), &self.root) { + return Ok(()); + } + self.detach_tree(&visible_root)?; + } + } } /// Result of cloning a mount namespace and retargeting filesystem context paths. @@ -1347,6 +1363,7 @@ mod tests { extern crate alloc; use alloc::{string::String, sync::Arc}; + use core::sync::atomic::{AtomicUsize, Ordering}; use ktime_types::SystemTime; use unittest::{assert, assert_eq, def_test}; @@ -1382,6 +1399,7 @@ mod tests { struct MockFilesystem { mount_flags: StatFsFlags, + sync_count: Option>, } impl SuperBlockOperations for MockFilesystem { @@ -1401,6 +1419,13 @@ mod tests { } Ok(()) } + + fn sync_fs(&self) -> VfsResult<()> { + if let Some(sync_count) = &self.sync_count { + sync_count.fetch_add(1, Ordering::Relaxed); + } + Ok(()) + } } struct MockDirOps { @@ -1572,7 +1597,10 @@ mod tests { } } - fn mock_filesystem(mount_flags: StatFsFlags) -> Arc { + fn mock_filesystem_with_sync_count( + mount_flags: StatFsFlags, + sync_count: Option>, + ) -> Arc { let inode = VfsInode::new_openable_dir(Arc::new(MockDirOps::new(mount_flags, 1)), inode_init(1)); let root = Dentry::new_dir_from_inode(inode, None, String::new()); @@ -1581,12 +1609,19 @@ mod tests { superblock_flags.insert(SuperBlockFlags::RDONLY); } SuperBlock::new_with_flags( - Arc::new(MockFilesystem { mount_flags }), + Arc::new(MockFilesystem { + mount_flags, + sync_count, + }), root, superblock_flags, ) } + fn mock_filesystem(mount_flags: StatFsFlags) -> Arc { + mock_filesystem_with_sync_count(mount_flags, None) + } + fn statfs() -> VfsResult { Ok(StatFs { fs_type: 0, @@ -1985,6 +2020,68 @@ mod tests { assert_eq!(grandchild_mount.children().len(), 0); } + #[def_test] + fn test_namespace_unmount_all_reveals_structural_root() { + let structural_fs = mock_filesystem(StatFsFlags::empty()); + let root_fs = mock_filesystem(StatFsFlags::empty()); + let overmount_fs = mock_filesystem(StatFsFlags::empty()); + let bootstrap_child_fs = mock_filesystem(StatFsFlags::empty()); + let visible_child_fs = mock_filesystem(StatFsFlags::empty()); + + let namespace = MntNamespace::new_root(&structural_fs, kcred::initial_user_namespace()); + let root = namespace.attach(&namespace.root_path(), &root_fs).unwrap(); + let bootstrap_mountpoint = lookup_child_in_mount(&root.root_path(), "mnt").unwrap(); + let bootstrap_child = namespace + .attach(&bootstrap_mountpoint, &bootstrap_child_fs) + .unwrap(); + let overmount = namespace + .attach(&namespace.visible_root_path(), &overmount_fs) + .unwrap(); + let mountpoint = lookup_child_in_mount(&overmount.root_path(), "mnt").unwrap(); + let visible_child = namespace.attach(&mountpoint, &visible_child_fs).unwrap(); + + namespace.unmount_all().unwrap(); + + assert!(Arc::ptr_eq( + namespace.visible_root_path().mount(), + namespace.root_mount() + )); + assert_eq!(namespace.mounts.lock().len(), 1); + assert!(root.location().is_none()); + assert!(bootstrap_child.location().is_none()); + assert!(overmount.location().is_none()); + assert!(visible_child.location().is_none()); + } + + #[def_test] + fn test_namespace_unmount_all_accepts_structural_root_only() { + let structural_fs = mock_filesystem(StatFsFlags::empty()); + let namespace = MntNamespace::new_root(&structural_fs, kcred::initial_user_namespace()); + + namespace.unmount_all().unwrap(); + + assert!(Arc::ptr_eq( + namespace.visible_root_path().mount(), + namespace.root_mount() + )); + assert_eq!(namespace.mounts.lock().len(), 1); + } + + #[def_test] + fn test_namespace_unmount_all_runs_existing_superblock_shutdown() { + let structural_fs = mock_filesystem(StatFsFlags::empty()); + let sync_count = Arc::new(AtomicUsize::new(0)); + let root_fs = + mock_filesystem_with_sync_count(StatFsFlags::empty(), Some(sync_count.clone())); + let namespace = MntNamespace::new_root(&structural_fs, kcred::initial_user_namespace()); + let mounted_root = namespace.attach(&namespace.root_path(), &root_fs).unwrap(); + drop(mounted_root); + + namespace.unmount_all().unwrap(); + + assert_eq!(sync_count.load(Ordering::Relaxed), 2); + } + #[def_test] fn test_hidden_mount_cannot_be_unmounted_while_overmounted() { let root_fs = mock_filesystem(StatFsFlags::empty()); diff --git a/platforms/kplat-aarch64/src/peripherals/psci.rs b/platforms/kplat-aarch64/src/peripherals/psci.rs index 3b4e5a621e7671380f021755e6ea7bccc30580ff..f644efb65b1466a4dc9856d07a514703599e4e01 100644 --- a/platforms/kplat-aarch64/src/peripherals/psci.rs +++ b/platforms/kplat-aarch64/src/peripherals/psci.rs @@ -79,7 +79,7 @@ pub fn init(method: &str) { smccc::init_conduit(method); } /// Power off the system via PSCI. -pub fn shutdown() -> ! { +pub fn power_off() -> ! { info!("Shutting down..."); psci_call(PSCI_0_2_FN_SYSTEM_OFF, 0, 0, 0).ok(); warn!("It should shutdown!"); diff --git a/platforms/kplat-aarch64/src/power.rs b/platforms/kplat-aarch64/src/power.rs index 7e48aee8ea8b3681ebb943fe573828ba61ec34c9..d261a9c93ee27e6b3a09b15ea6b9b10c383f7da9 100644 --- a/platforms/kplat-aarch64/src/power.rs +++ b/platforms/kplat-aarch64/src/power.rs @@ -25,7 +25,14 @@ impl SysCtrl { crate::peripherals::psci::cpu_on(raw_cpu_id, entry_paddr.as_usize(), 0).map_err(Into::into) } - fn shutdown() -> ! { - crate::peripherals::psci::shutdown() + fn halt() -> ! { + info!("System halted"); + loop { + karch::stop_cpu(); + } + } + + fn power_off() -> ! { + crate::peripherals::psci::power_off() } } diff --git a/platforms/kplat-loongarch64/src/power.rs b/platforms/kplat-loongarch64/src/power.rs index 2c9ab9644a1e75180dcfb41c64758d5ac629028e..b6b982a4393c60eb1bdf054ec89b25fdbbf15bfc 100644 --- a/platforms/kplat-loongarch64/src/power.rs +++ b/platforms/kplat-loongarch64/src/power.rs @@ -27,7 +27,14 @@ impl SysCtrl { Ok(()) } - fn shutdown() -> ! { + fn halt() -> ! { + info!("System halted"); + loop { + karch::stop_cpu(); + } + } + + fn power_off() -> ! { let halt_addr = memspace::iomap_device(PhysAddr::from_usize(GED_PADDR), 0x1000, "ged") .unwrap_or_else(|err| panic!("failed to iomap ged: {err:?}")) .as_mut_ptr(); diff --git a/platforms/kplat-riscv64/src/power.rs b/platforms/kplat-riscv64/src/power.rs index 7aaddfb1453ee2704f8c4cb56edc044485697630..bd7744e2144f6604ff4f79b204a81c7bfe69e95e 100644 --- a/platforms/kplat-riscv64/src/power.rs +++ b/platforms/kplat-riscv64/src/power.rs @@ -35,7 +35,14 @@ impl SysCtrl { Ok(()) } - fn shutdown() -> ! { + fn halt() -> ! { + info!("System halted"); + loop { + karch::stop_cpu(); + } + } + + fn power_off() -> ! { info!("Shutting down..."); sbi_rt::system_reset(sbi_rt::Shutdown, sbi_rt::NoReason); warn!("It should shutdown!"); diff --git a/platforms/kplat-x86_64/src/power.rs b/platforms/kplat-x86_64/src/power.rs index a7e7d32b1e297b71743a6b8b9978906708924de0..e941ca292ed9aa6d3107595657316cce8d8369ab 100644 --- a/platforms/kplat-x86_64/src/power.rs +++ b/platforms/kplat-x86_64/src/power.rs @@ -25,7 +25,14 @@ impl SysCtrl { Ok(()) } - fn shutdown() -> ! { + fn halt() -> ! { + info!("System halted"); + loop { + karch::stop_cpu(); + } + } + + fn power_off() -> ! { info!("Shutting down..."); if cfg!(feature = "reboot-on-system-off") { khal::kprintln!("System will reboot, press any key to continue ..."); diff --git a/platforms/kplat/src/sys.rs b/platforms/kplat/src/sys.rs index f6844630d7ae35ed9857e11c564cc627428c451a..889baeba63dd4863ef5d661e51be86510fa883a4 100644 --- a/platforms/kplat/src/sys.rs +++ b/platforms/kplat/src/sys.rs @@ -20,6 +20,9 @@ pub trait SysCtrl { /// actual boot command. fn boot_ap(logical_cpu_id: LogicalCpuId, stack_top: usize) -> KResult; - /// Shuts down the system. - fn shutdown() -> !; + /// Halts the system without removing power. + fn halt() -> !; + + /// Powers off the system. + fn power_off() -> !; } diff --git a/posix/fs/docs/design.md b/posix/fs/docs/design.md index c6ee8298b4b61fab6b999081313223153a0e402c..725200ebb824e3357fb9fe98f52609e2dab8fcb8 100644 --- a/posix/fs/docs/design.md +++ b/posix/fs/docs/design.md @@ -444,7 +444,8 @@ pathname DAC。这对应 Linux `vfs_truncate()` 与 `do_ftruncate()` 的语义 `posix-fs` 没有自定义 `Drop` 类型。 资源释放依赖下层对象: -- `close`/`close_range` 从当前进程 fd 表移除 `Arc`; +- `close` 从当前进程 fd 表移除 `Arc`;`close_range(UNSHARE)` 在 + files owner 写锁覆盖的单次事务中完成必要复制和 range 修改; - fd 复制和打开路径通过 `Arc` 共享文件、目录、pipe 或设备对象; - 临时 `Vec`、路径 `String`、`CString` 和中间 I/O 缓冲区在函数返回时释放; - mount/unmount 的 topology 由 `kvfs::Path` 和 `Mount` 管理;`VfsMount` 最后释放时归还 diff --git a/posix/fs/docs/security.md b/posix/fs/docs/security.md index 064706ee27328271268eb5f9b48191569d613557..80b69fb1a5d9519be1db8f78cebad9309b7749cf 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)` 在进程退出后访问已 detach fd table | exit 与外部 fd capability 并发 | 当前 syscall 返回失败 | 不会重新安装或误改退出进程 files | 3 | range close/CLOEXEC 事务在同一个 owner 写锁下完成可用性检查、必要 unshare 和修改,并传播 `NoSuchProcess` | | F-12 | FIEMAP 输出容量不足或中途遇到坏用户页 | 调用者提供较小数组或不可写地址 | 返回已统计数量或 `BadAddress` | 当前查询失败,文件系统状态不变 | 3 | `FiemapExtentInfo` 达到容量后正常停止;writer 每项通过 `UserPtr` 写入并传播 copy fault | 严重度定义: diff --git a/posix/fs/src/fd_ops.rs b/posix/fs/src/fd_ops.rs index 833ec448471c2375ecd4b0ac04030e5bc92133d8..af2e001118b9829b455521ab92cbc3e81a9812c4 100644 --- a/posix/fs/src/fd_ops.rs +++ b/posix/fs/src/fd_ops.rs @@ -47,14 +47,12 @@ pub fn sys_close_range(first: i32, last: i32, flags: u32) -> KResult { debug!("sys_close_range <= fds: [{first}, {last}], flags: {flags:?}"); let resources = kprocess::current_user_process().resources()?; - if flags.contains(CloseRangeFlags::UNSHARE) { - resources.unshare_fd_table(); - } + let should_unshare = flags.contains(CloseRangeFlags::UNSHARE); if flags.contains(CloseRangeFlags::CLOEXEC) { - resources.set_cloexec_range(first, last); + resources.set_cloexec_range(first, last, should_unshare)?; } else { - resources.close_range(first, last); + resources.close_range(first, last, should_unshare)?; } Ok(0) diff --git a/posix/process/docs/design.md b/posix/process/docs/design.md index 2577cde240aa27fa9bb84f2ef6faf689d523235f..2100aba57ce797754abd0f98b40e4666fcef7219 100644 --- a/posix/process/docs/design.md +++ b/posix/process/docs/design.md @@ -7,6 +7,7 @@ - clone / exit / signal-return 需要共享的用户态 trap 主循环; - 初始用户进程的地址空间、`Process`/`Thread` runtime、TTY 和 stdio 组装; - 线程退出时的 robust futex 清理、group-exit 和父进程通知; +- 系统关机时的用户态文件系统 owner 释放; - 保持这些逻辑依赖 `kprocess` 原语,但不把它们塞回 `kprocess` 本体。 纯 syscall adapter(`getpid`、`getrusage`、`umask`、job control、rlimit 等) @@ -18,6 +19,7 @@ - `src/runtime.rs` - `src/init_process.rs` +- `src/system_lifecycle.rs` - `src/lib.rs` ## 架构 @@ -40,7 +42,17 @@ entry / ksyscall - 先构造 matching `Thread`,再通过 `new_user_task(..., thread, ...)` 一次性构造 task 与 `UserTaskRuntime` - 调用 `start_user_task(...)` - `kprocess` 内部先完成 publish,再使 task runnable -- `spawn_init_process()` 依赖 rootfs、TTY 和默认 stdio 初始化路径可用;它由 PID-less 的 late-init 线程调用,分配 PID 1 并构造一个全新的 `User` 身份用户任务(走与 fork 相同的 `new_user` + `publish_user_task().commit()` 路径),不再原地转换 current task。PID 1 只继承 console stdio,不预先绑定 controlling TTY 或设置 foreground process group,后续 getty 通过标准 session/TTY ioctl 获取控制终端。 +- `spawn_init_process()` 依赖 rootfs、TTY 和默认 stdio 初始化路径可用;它由 PID-less 的 late-init 线程调用,分配 PID 1 并构造一个全新的 `User` 身份用户任务(走与 fork 相同的 `new_user` + `publish_user_task().commit()` 路径),再把 task handle 返回给系统生命周期 supervisor。PID 1 只继承 console stdio,不预先绑定 controlling TTY 或设置 foreground process group,后续 getty 通过标准 session/TTY ioctl 获取控制终端。 +- `request_shutdown()` 记录第一个 HALT/POWER_OFF 请求,并在请求者不是 PID 1 时对 + PID 1 投递 fatal signal;它不在 syscall 或调用进程上执行文件系统 teardown。PID 1 + 若已进入退出尾段,signal 投递失败记录 error 日志;发起 reboot 的进程仍完成自身退出, + 不把关机竞争转换为 syscall 返回。 +- `shutdown_userspace()` 只由已经 `join()` PID 1 leader task 的 supervisor 调用。 + 它先等待 PID 1 的 process exit completion,保证所有 sibling thread 已完成退出; + 再向余下进程发出终止信号并等待自身退出,重复检查直到没有新产生的进程,最后 + 清空 `kexec` ELF cache 持有的 `VfsFile`,然后把首个请求的 terminal mode 返回给 + runtime;没有 reboot 请求时默认 POWER_OFF。PID 1 的 `Process` 仍作为全局 reaper + identity 保留到终止,因此不进入余下进程集合。监督者不替其他任务释放 runtime owner。 - `do_exit()`、`check_signals()` 依赖 current task 是携带 `UserTaskRuntime` 的用户 task。 - 这些接口会访问地址空间、信号状态、fd 表和共享内存管理器,可阻塞,不适用于中断上下文。 @@ -60,20 +72,36 @@ entry / ksyscall 1. 清理 `clear_child_tid` 并唤醒 futex。 2. 遍历 robust futex list,标记 owner-dead。 3. 从进程线程集合中摘除当前线程。 -4. 若为最后线程,关闭 fd、通知父进程、清理共享内存和可选 TEE 私有状态。 -5. 若触发 group exit,向线程组广播 `SIGKILL`。 +4. 若为最后线程,完成共享内存和可选 TEE 私有状态清理。 +5. 通过 `Process::release_runtime_owners()` 依次释放 mm user、fd table、 + `FsStruct` 和 `NsProxy` owner。 +6. 发布进程退出并通知父进程。 +7. 若触发 group exit,向线程组广播 `SIGKILL`。 ## 并发模型 -- 线程/进程基础状态由 `kprocess` 和其内部锁保护。 +- 线程/进程基础状态由 `kprocess` 和其内部锁保护;一个原子请求槽保存首个 terminal + mode,使异步 supervisor 在用户进程全部退出后仍能区分 HALT 与 POWER_OFF。 - 本 crate 负责组织退出与信号路径的调用顺序,不重复持有额外全局状态。 - robust futex owner-dead 标志通过原子位和等待队列协作。 +- shutdown 不在退出中的 PID 1 task 上执行;PID 1 也先走普通 `do_exit()`,由持有其 + leader task handle 的 PID-less supervisor 在 `join()` 后等待整个 PID 1 process + completion,再进入系统 teardown。 +- 终止阶段通过 `Process::wait_for_exit()` 等待每个服务进程的最后线程完成 + `do_exit()`;因此 mm、files、`FsStruct` 和 `NsProxy` 都在任务自身停止用户态执行后 + 释放,监督者不会与其他 CPU 上的地址空间访问并发。 ## 设计决策 - 该逻辑放在 `posix-process`,因为它围绕进程/线程生命周期状态机,不应该污染 `kprocess` 的基础职责。 - `posix-process` 可以自然承接这类面向进程生命周期的上层 owner 逻辑,并避免 `kprocess <-> posix-ipc` 环依赖。 - 纯 adapter 迁回 `ksyscall/task` 后,本 crate 只保留真正依赖进程生命周期状态机的 owner 逻辑。 +- init bootstrap 与系统终止分别位于 `init_process.rs` 和 `system_lifecycle.rs`; + 两者共享 process 原语,但不混合创建与终止职责。 +- ELF cache 是进程 fd table 之外的 open-file owner;它已由 `kexec` 统一管理, + 因此关机只调用现有 `clear_elf_cache()`,不增加 TEE 或 filesystem 专用旁路。 +- 可阻塞 syscall 必须使用已有的 interruptible future 包装,使 fatal signal 能让 + 任务返回 runtime 信号检查;系统关机因此只依赖普通进程退出,不关闭特定子系统句柄。 - 用户态 runtime 直接消费 `MmSpace::handle_page_fault()` 的结构化结果, 因此架构 trap glue 不需要理解 file-backed fault 细节,同时 runtime 可以把 file-backed EOF 等对象级 fault 转换为 `SIGBUS`,把普通权限或缺页错误转换为 diff --git a/posix/process/docs/security.md b/posix/process/docs/security.md index 3fbb43f17c0d580048f5b45b1d25a235c84cb164..2bfdd04108754dd1fdd8d87ecec91bdcb86e90b4 100644 --- a/posix/process/docs/security.md +++ b/posix/process/docs/security.md @@ -27,13 +27,28 @@ `new_user(...)` 在 task 构造时一次性装入 `UserRuntimeSlot`,再经 `publish_user_task(...).commit(...)` 发布到 process registry 后才激活,不存在 runnable 后补装 runtime 的路径。 -- 最后线程退出前必须先关闭 fd,再标记进程退出,避免外部等待者持有悬挂资源语义。 +- 最后线程在标记进程退出前必须依次取走 fd table、`FsStruct` 和 `NsProxy` owner,避免 + wait 返回后退出进程仍通过 root/pwd 固定 mount。 +- 系统终止阶段先 `join()` PID 1 leader task,再等待 PID 1 process exit completion; + 后者只在所有 sibling thread 退出并释放 runtime owner 后发布。完成后返回方向只能 + 通往 namespace/device/platform teardown,不能重新进入用户态。 +- 系统监督者不得释放仍存活任务的 mm user;地址空间只能由任务自身的退出路径 + 在停止用户态执行后释放。 +- mount teardown 前所有已发出终止信号的进程都必须完成自身退出;否则 executable + mapping、fd 或 fs context 仍可能固定 root mount。 +- `kexec` ELF cache 必须在 mount teardown 前清空,因为每个 cache entry 都会 + 独立持有 `VfsFile -> Path -> Mount`。 - `SHM_MANAGER` 清理仅针对已退出进程 PID。 ## 线程安全 -- 本 crate 不自建额外共享状态,依赖 `kprocess::Process`/`Thread` runtime 内部同步。 +- 进程状态依赖 `kprocess::Process`/`Thread` runtime 内部同步;系统级仅有一个原子 + shutdown request 槽,首个请求通过 compare-exchange 决定 HALT/POWER_OFF,后续请求 + 不覆盖它。 - group-exit 广播和父进程唤醒都基于当前可见线程/进程集合执行。 +- terminal userspace shutdown 对余下进程取得快照、投递信号并逐个等待 process exit + completion,随后重复该过程直到快照为空,覆盖退出并发期间创建 child 的情况。 + owner slot 只由各进程的最后线程退出路径取走。 - 纯 syscall adapter 已迁到 `ksyscall/task`,不再扩大本 crate 的 ABI 暴露面。 ## 威胁分析 @@ -48,9 +63,23 @@ ## 故障模式与影响分析(FMEA) -- 退出路径漏关 fd:会破坏 pipe EOF / wait 语义;当前实现先 `close_all_fds()`。 +- 退出路径漏放 files owner:会破坏 pipe EOF / wait 语义;当前实现用 `exit_files()` 取走 + 本进程 owner,共享 fd table 在最后 owner 释放时关闭。 +- 退出路径漏放 `FsStruct`/`NsProxy`:会让 `Path -> Mount` 跨 zombie 生命周期存活,阻止 + mount final teardown;当前最后线程在父进程可观察退出前完成两者 detach。 +- 漏清 ELF cache:即使所有进程 fd 和 `FsStruct` 已释放,cache 中的 open file + 仍会阻止最后一个 root mount drop;`shutdown_userspace()` 在 namespace teardown 前清空它。 +- 监督者释放存活任务地址空间:可能与其他 CPU 的取指、缺页或用户内存访问并发; + 监督者只等待退出,mm 和 filesystem owner 均由任务自身退出路径处理。 +- 阻塞 syscall 不响应任务 interrupt:fatal signal 无法到达 runtime 检查,监督者会等待; + poll/future 型阻塞入口使用 `ktask::future::interruptible()` 包装。 - 父进程通知丢失:通过退出信号和 `child_exit_event()` 双路径通知。 - group-exit 未广播:会留下残余线程;当前实现遍历线程组发 `SIGKILL`。 +- 只等待 PID 1 leader task:多线程 init 的 sibling 可能仍持有 mm/files/fs owner; + `shutdown_userspace()` 还会等待 PID 1 process exit completion。 +- shutdown 请求与 PID 1 退出竞争:terminal mode 在投递 signal 前发布;signal 投递可能 + 返回目标已退出,请求入口记录错误,发起者仍执行 `do_exit()`,由已经运行的 supervisor + 路径继续关机。 - init 进程启动前缺少 user runtime:会导致用户线程 runtime 前提失效;当前 PID 1 路径在进入用户态前校验 identity、安装 runtime、发布 process/task 可见性,并同步当前页表。 - init 进程预占 controlling TTY:会阻止 OpenRC getty 建立新 session 并获取 console;初始进程只安装 stdio,控制终端所有权留给 getty 的 `setsid`/TTY ioctl 流程。 @@ -63,3 +92,5 @@ - `Stop` / `CoreDump` 默认动作仍是简化实现。 - 多线程 `execve` 仍未完整支持。 +- orderly shutdown 不以超时强拆仍被活跃任务持有的 mm 或 filesystem owner;阻塞入口 + 若不能响应 fatal signal,监督者会等待,需要在对应等待原语修复可中断语义。 diff --git a/posix/process/src/init_process.rs b/posix/process/src/init_process.rs index 0d1ffd1c43d16dfdfab87f574d650e513ea29e07..1e3d5e66b3cae4569f5a0a00bb92c9c1891b7b97 100644 --- a/posix/process/src/init_process.rs +++ b/posix/process/src/init_process.rs @@ -28,15 +28,9 @@ use crate::runtime::run_user_thread_loop; /// all-online-CPU affinity. The task enters user space on its own kernel stack /// through the normal scheduler switch-in path, the same one fork uses. /// -/// Unlike the old in-place "transform current into init" model, this does **not** -/// touch the caller: it follows the FreeBSD-style "the bootstrap thread forks -/// init" model. The caller is expected to be a kernel thread (typically the -/// late-init bootstrap thread) and remains free to continue or exit after the -/// spawn. -/// -/// `after_init_exit` runs on the spawned init task after its user loop exits and -/// is expected to perform system shutdown or another non-returning terminal -/// action. +/// This does not transform the caller into init. The caller remains a kernel +/// thread, and the returned task handle is its capability for supervising init +/// exit. /// /// # Panics /// @@ -47,8 +41,7 @@ pub fn spawn_init_process( args: &[String], envs: &[String], dispatch_syscall: impl FnMut(&mut UserContext) -> UserThreadRuntimeAction + Send + 'static, - after_init_exit: impl FnOnce() + Send + 'static, -) { +) -> ktask::KtaskRef { // PID 1: must be the first root PID allocation in the system. The late-init // bootstrap thread holds an `Internal` identity and allocates none, so this // call naturally receives root_nr 1. @@ -126,6 +119,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, ) @@ -134,10 +128,10 @@ pub fn spawn_init_process( // Build a fresh user task carrying the runtime from construction (no // in-place install). The entry closure runs the user loop on the spawned - // task's own kernel stack, then performs the post-init shutdown. + // task's own kernel stack. System lifecycle policy waits on the returned + // task handle rather than running shutdown inside the exiting process. let entry = move || { run_user_thread_loop(uctx, 0, dispatch_syscall); - after_init_exit(); ktask::exit(0); }; let mut task = ktask::TaskInner::new_user(entry, name, pid_handle, thread); @@ -152,5 +146,5 @@ pub fn spawn_init_process( // Publish and activate through the standard fork path (caller-agnostic). publish_user_task(task) .commit(|_| Ok(())) - .expect("Failed to publish init process"); + .expect("Failed to publish init process") } diff --git a/posix/process/src/lib.rs b/posix/process/src/lib.rs index c4f1d807d3ab3fb88cfb0a47f6822ae3fc6cb8ba..2e92f934eb58949834fe6cbf5e108f8f0fc6e752 100644 --- a/posix/process/src/lib.rs +++ b/posix/process/src/lib.rs @@ -13,5 +13,7 @@ extern crate klogger; mod init_process; mod runtime; +mod system_lifecycle; pub use init_process::spawn_init_process; pub use runtime::{check_signals, do_exit, new_user_task, raise_signal_fatal}; +pub use system_lifecycle::{request_shutdown, shutdown_userspace}; diff --git a/posix/process/src/runtime.rs b/posix/process/src/runtime.rs index 371e7f4e946e9b26145198369a6efd665dd33363..3b970474c9e94d6d8c176a6c9fc4b38f507ccbf4 100644 --- a/posix/process/src/runtime.rs +++ b/posix/process/src/runtime.rs @@ -334,10 +334,6 @@ 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:?}"); - } - // Detach shared memory before waking the parent, so that // waitpid() returns only after segments marked IPC_RMID // have been destroyed. @@ -347,8 +343,10 @@ 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:?}"); + // Release every process-owned capability before publishing exit; + // mount lifetime then decides when a filesystem can shut down. + if let Err(err) = process.release_runtime_owners() { + error!("release runtime owners on process exit failed: {err:?}"); } process_exit::complete_process_exit(process); diff --git a/posix/process/src/system_lifecycle.rs b/posix/process/src/system_lifecycle.rs new file mode 100644 index 0000000000000000000000000000000000000000..37974846f1e8d39c36a4ab4f238fa02315d42c0b --- /dev/null +++ b/posix/process/src/system_lifecycle.rs @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 KylinSoft Co., Ltd. +// See LICENSES for license details. + +//! Userspace transitions owned by the system lifecycle supervisor. + +use alloc::sync::Arc; +use core::sync::atomic::{AtomicU8, Ordering}; + +use khal::power::ShutdownMode; +use kprocess::Process; +use ksignal::{SignalInfo, Signo}; + +const NO_SHUTDOWN_REQUEST: u8 = 0; +const HALT_REQUEST: u8 = 1; +const POWER_OFF_REQUEST: u8 = 2; + +static SHUTDOWN_REQUEST: AtomicU8 = AtomicU8::new(NO_SHUTDOWN_REQUEST); + +fn record_shutdown_request(mode: ShutdownMode) { + let request = match mode { + ShutdownMode::Halt => HALT_REQUEST, + ShutdownMode::PowerOff => POWER_OFF_REQUEST, + }; + if let Err(selected_request) = SHUTDOWN_REQUEST.compare_exchange( + NO_SHUTDOWN_REQUEST, + request, + Ordering::AcqRel, + Ordering::Acquire, + ) { + debug!("shutdown request {request} did not replace selected request {selected_request}"); + } +} + +fn shutdown_mode() -> ShutdownMode { + match SHUTDOWN_REQUEST.load(Ordering::Acquire) { + HALT_REQUEST => ShutdownMode::Halt, + NO_SHUTDOWN_REQUEST | POWER_OFF_REQUEST => ShutdownMode::PowerOff, + _ => unreachable!("invalid shutdown request"), + } +} + +/// Requests a terminal system state through PID 1's normal process lifecycle. +/// +/// The first request selects the terminal state. A request made by PID 1 only +/// records that state because the caller exits itself. Signal delivery for any +/// other requester is best-effort because PID 1 may already be completing exit. +pub fn request_shutdown(mode: ShutdownMode, requester: &Arc) { + record_shutdown_request(mode); + if requester.is_init() { + return; + } + + let init = kprocess::init_proc(); + if init.exit_event().is_completed() { + return; + } + if let Err(err) = kprocess::process_signals::send_to_process_ref( + &init, + Some(SignalInfo::new_kernel(Signo::SIGKILL)), + ) { + error!("failed to signal init during shutdown request: {err:?}"); + } +} + +/// Terminates userspace before mount teardown. +/// +/// The supervisor has joined PID 1's leader task before calling this function. +/// This function first waits for the complete PID 1 thread group, then signals +/// remaining service processes and waits for their process-exit paths before +/// emptying the executable cache. Waiting for process completion keeps address- +/// space and filesystem owner release in each exiting process lifecycle. PID 1 +/// remains the global reaper identity until the terminal platform operation. +/// Repeating the snapshot catches a child created while an earlier snapshot +/// was being terminated. +/// +/// Returns the terminal state selected by the first reboot request, or +/// [`ShutdownMode::PowerOff`] when PID 1 exited without one. +pub fn shutdown_userspace() -> ShutdownMode { + kprocess::init_proc().wait_for_exit(); + + let sigkill = SignalInfo::new_kernel(Signo::SIGKILL); + loop { + let processes = kprocess::scheduler::processes() + .into_iter() + .filter(|process| !process.is_init()) + .collect::>(); + if processes.is_empty() { + break; + } + + for process in &processes { + if let Err(err) = + kprocess::process_signals::send_to_process_ref(process, Some(sigkill.clone())) + { + warn!( + "failed to signal process {} during shutdown: {err:?}", + process.pid() + ); + } + } + + for process in processes { + process.wait_for_exit(); + } + } + + kexec::clear_elf_cache(); + shutdown_mode() +} diff --git a/process/kexec/docs/design.md b/process/kexec/docs/design.md index 41a9dc7f7794a2c8e1b6427c524190f2512a2f61..46d7c4e7fdebaa0ec3b6e857234d108edde9887d 100644 --- a/process/kexec/docs/design.md +++ b/process/kexec/docs/design.md @@ -201,5 +201,8 @@ procfs 路径字符串,也不单独实现 magic-link 修正。 ## Drop / 资源释放 - ELF header/cache 数据跟随 `ElfCacheEntry` 和 LRU cache 生命周期释放。 +- 每个 `ElfCacheEntry` 拥有一个 executable `VfsFile`;其 `Path` 会延长对应 + mount 的生命期。系统终止必须在 namespace teardown 前调用 + `clear_elf_cache()`,不能假定进程 fd table 退出就能释放该 owner。 - 用户地址空间资源由 `MmSpace` 拥有,`process/kexec` 不在 drop 路径中释放 VMA、page table 或 anonymous/file-backed object。 diff --git a/process/kexec/docs/security.md b/process/kexec/docs/security.md index f3ad49a5dd63eda1271507f249cd8cddaffef00c..bf4c1896da9a18ea00a0ffdf8847843dae95b5f3 100644 --- a/process/kexec/docs/security.md +++ b/process/kexec/docs/security.md @@ -60,8 +60,9 @@ VFS/namei 解析,可以通过 `ExecSource::Resolved { location, display_path } - 全局 ELF loader 由 `Mutex` 保护。 - 单次装载期间对目标 `MmSpace` 的修改由调用方以可变引用形式传入。 -- LRU cache 中的 executable `File` 引用共享 inode-owned page cache,但不拥有 - MM 映射状态。 +- LRU cache 中的 executable `VfsFile` 共享 inode-owned page cache,但不拥有 + MM 映射状态。它仍通过 `Path` 持有 mount,因此终止编排必须在 + mount teardown 前调用 `clear_elf_cache()`。 ## 威胁分析 @@ -88,6 +89,7 @@ VFS/namei 解析,可以通过 `ExecSource::Resolved { location, display_path } | 映射安装失败 | 地址冲突、OOM、页表错误 | 部分地址空间构造失败 | exec 失败,调用方处理错误 | MM API 返回 `KResult` | | point-of-no-return 后元数据更新失败 | 地址空间已清空后继续执行可失败路径 | 旧用户镜像不可恢复 | 不能把错误返回旧用户态 | 可失败解析前移;后处理使用预解析数据或 best-effort | | cache 内容过期 | 底层文件变化但 LRU 仍持有旧 header | 可能使用旧解析结果 | 可执行文件更新可见性延迟 | 当前 ELF cache 未接入文件失效通知 | +| 终止时未清空 cache | LRU entry 跨 namespace teardown 持有 `VfsFile` | root mount 的最后 owner 无法释放 | superblock shutdown 不可达 | `posix-process` userspace shutdown 调用现有 `clear_elf_cache()` | ## 故障管理 @@ -115,3 +117,4 @@ VFS/namei 解析,可以通过 `ExecSource::Resolved { location, display_path } - `PT_LOAD` 的 VMA start 和 file offset 是否同时按页向下对齐? - `PT_INTERP` 路径读取是否处理了无效字符串和文件系统错误? - 新增 loader 逻辑是否仍保持 `process/kexec` 作为 MM client,而不是 MM owner? +- 新增长期 cache entry 是否持有 `VfsFile`/`Path`,并在系统终止阶段有明确的释放点? diff --git a/process/kexec/src/loader.rs b/process/kexec/src/loader.rs index c592423d8038698ae11f02a9188b2cbcaa2fe331..8b1337ea3471fd0a18c574f428a0181fa407f054 100644 --- a/process/kexec/src/loader.rs +++ b/process/kexec/src/loader.rs @@ -458,9 +458,10 @@ static_lock! { static ELF_LOADER: Mutex = Mutex::new(ElfLoader::new()); } -/// Clear the ELF cache. +/// Drops all cached executable images and their open-file ownership. /// -/// Useful for removing noise during memory leak detection. +/// Terminal userspace teardown calls this before mount teardown. It is also +/// useful for removing cache noise during memory leak detection. pub fn clear_elf_cache() { ELF_LOADER.lock().0.flush(); #[cfg(feature = "tee_ta_sign")] diff --git a/process/kfd/docs/design.md b/process/kfd/docs/design.md index f759aeea618a025f75da3404c568f6137874397b..1d7273f0afcf04baec03f752f19e35f5667b8ce8 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,38 @@ Open 再逐个删除。 这样可以避免边遍历 `FlattenObjects` 边修改同一结构。 -### fd table 共享和关闭 +### fd table 共享和退出关闭 ```text -Arc strong_count > 1 - │ remove_all_if_unshared +Process A files ─┐ + ├──> Arc> +Process B files ─┘ + │ exit_files(): take Option owner v -unchanged - -Arc strong_count == 1 - │ remove_all_if_unshared +the table remains live for the other owner + │ final owner exits v -all descriptors removed, lock dropped before descriptor close +FdTable::drop(): drain descriptors and close them ``` -该路径用于进程资源释放。 -当 fd table 仍被其他进程或线程共享时, -不会关闭所有 fd。 +进程退出不读取瞬时 `Arc::strong_count`,只从自己的 files slot 取走 +owner。共享表由最后一个 owner 的 `Arc` 释放自然触发关闭;已取得的 +临时表 capability 也按同一所有权规则延迟 final close。 + +`close_range(UNSHARE)` 由 `ProcessResources` 在 files owner 写锁下处理。 +共享判断、必要的 descriptor table 复制、owner 替换和 range 修改属于同一个事务, +因此 fork 不能插入在“完成 unshare”和“开始 range 修改”之间。若表只有当前 owner, +则直接修改原表;存在其它 owner 或已取得的 table capability 时,先复制再修改新表。 +被替换的旧 owner 在资源锁外释放,避免 final close 重入资源层临界区。 + +`Arc` 的强引用数同时表达 fd table owner 和已取得 capability 的存活关系,承担 +files table 引用计数的职责。短期 capability 与 fork owner 都必须阻止调用方把旧表 +当作唯一所有,因此无需再维护一份容易失配的进程共享计数。短期 capability 可能使 +`UNSHARE` 做一次保守复制,但不会让 range 修改泄漏到旧表。 + +final owner 释放发生在 `Drop` 中,已经没有可接收错误的 syscall 调用者。 +因此单个 descriptor 的 flush 错误不会中止其余 descriptor 的关闭;显式 +`close(2)` 仍通过普通返回路径报告自己的错误。 ## 算法流程 @@ -210,9 +225,12 @@ snapshot 创建后,原 fd 可以被关闭或复用, - 查找 fd、读取 `cloexec`:调用方持读锁即可。 - 创建 `FdSnapshot`:调用方持读锁,克隆 `Arc` 后释放锁。 - 添加、删除、dup、设置 `cloexec`、close range:调用方必须持写锁。 -- `remove_all_if_unshared` 在持写锁时移除 descriptor, - 把移除结果暂存在 `Vec` 中, - 然后由 `ProcessResources` 在表锁外关闭 `FileDescriptor`。 +- 普通 close/exec/range 路径在持写锁时只移除 descriptor,再由 + `ProcessResources` 在表锁外关闭。 +- `close_range(UNSHARE)` 持有 files owner 写锁完成共享判断、复制和 range 修改; + fork 取得 table capability 需要 files owner 读锁,二者按该锁线性化。 +- final `FdTable::drop()` 发生在外层 `RwLock` 和全部共享 capability 已经消失后;它先 + drain 槽位,再逐项关闭,不会在仍可访问的 fd table 锁内重入释放路径。 这样可以避免 `FileLike::drop` 或底层对象释放路径在持有 fd table 写锁时重入 fd 表。 diff --git a/process/kfd/docs/security.md b/process/kfd/docs/security.md index ad56a318632c155418de5589d1be016fd237197f..8b4c3aabcdb935b29090769deb655eabe284be5b 100644 --- a/process/kfd/docs/security.md +++ b/process/kfd/docs/security.md @@ -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. **drop 不在可访问的表锁内执行**: + 普通 close 路径先从表中取出 descriptor,释放写锁后再关闭;final table drop 只在 + 外层 `RwLock` 的所有 `Arc` 已消失后 drain descriptor。 6. **ABI 结构不泄露未初始化数据**: `stat` / `statx` 转换先全零初始化, 再填充字段。 @@ -147,8 +147,10 @@ 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 的一个进程退出时误关其它进程 fd | 高 | 退出路径按瞬时引用数清空共享表 | `ProcessResources::exit_files()` 只取走本进程 owner;`FdTable::drop()` 仅由最后一个 `Arc` owner 触发 | | T-12 | procfs 或 exec 路径把 `FdSnapshot` 当成 live fd 权限 | 中 | snapshot 创建后原 fd 被关闭、复用或 flag 改变 | snapshot 只表示创建时的 open object;需要 live fd 状态的 syscall 必须重新查 fd table | +| T-13 | 私有 fd table 执行 unshare 时误触发全表 close | 高 | unshare 无条件复制并替换唯一 owner | range 事务在 owner 锁下判断共享状态,唯一 owner 保持原表;旧表 owner 在锁外释放 | +| T-14 | fork 插入 unshare 与 range 修改之间并继续共享被修改的旧表 | 高 | unshare 和 range 修改是两个独立资源操作 | 共享判断、必要复制、owner 替换和 range 修改都在同一次 files owner 写锁事务中完成;fork 的 owner 读取与之线性化 | 影响等级定义: @@ -169,6 +171,8 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; | F-07 | `statx` regular-file atomic write 字段误报 | `mode` 类型位错误 | 用户态看到错误 attribute | 应用可能选择错误 I/O 策略 | 4 | 仅当 `mode & S_IFMT == S_IFREG` 时设置 | | F-08 | FileLike 实现忘记覆盖 `path` 以外的方法 | 默认方法被调用 | 返回不支持错误 | 功能降级 | 4 | trait 默认错误返回;实现者测试覆盖自身行为 | | F-09 | `snapshot` 返回 `BadFileDescriptor` | fd 不存在或已关闭 | procfd/fexecve/open 路径失败 | 应用收到 `EBADF` 或上层映射后的 errno | 4 | snapshot 前查表,失败不保留对象引用 | +| F-10 | final fd table teardown 的 descriptor flush 失败 | 底层文件或设备在最终关闭时报告错误 | 对应 flush 未完成 | 退出调用者已不存在,错误无法返回 | 3 | `FdTable::drop()` 继续关闭其余 descriptor;残余风险是该 flush 错误没有可返回的 syscall 调用者 | +| F-11 | `close_range(UNSHARE)` 因短期 table capability 复制 fd table | 并发读者在 range 事务开始前取得 `Arc` | 发生一次保守的 O(nfd) 复制 | syscall 延迟增加,语义不变 | 4 | `Arc` 统一承担 owner/capability 存活计数;避免增加可能失配的第二套共享计数 | 严重度定义: @@ -208,9 +212,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. **final close 可被合法 capability 延迟**: + 进程退出后,已取得的 fd-table owner 仍可延迟 `FdTable::drop()`; + 调用者不得把退出通知误解为对象已析构。 ## 其它说明(模板章节) @@ -234,4 +238,5 @@ let mut statx: statx = unsafe { core::mem::zeroed() }; - [ ] 新增 `FileLike` 默认方法不会把不支持操作伪装成成功。 - [ ] `stat` / `statx` ABI 结构新增字段时保留字段仍清零。 - [ ] exec 路径继续调用 `close_cloexec_files`。 -- [ ] 资源层替换 fd table 时保持 `Arc>` 发布同步。 +- [ ] 资源层替换/取走 fd table 时保持 `Option>>` 发布同步,退出后不再 + 重新安装 table。 diff --git a/process/kfd/src/fd_table.rs b/process/kfd/src/fd_table.rs index 9ee15bfe1266c8c47f4fcd92ffa2d300465b577c..ffe0afc288e27092dd09e7a11d642cf5d5fbfe5a 100644 --- a/process/kfd/src/fd_table.rs +++ b/process/kfd/src/fd_table.rs @@ -218,20 +218,24 @@ 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(); + fn remove_all(&mut self) -> Vec { + let ids: Vec = self.ids().collect(); let mut removed = Vec::with_capacity(ids.len()); for id in ids { - if let Some(descriptor) = table.remove(id) { + if let Some(descriptor) = self.remove(id) { removed.push(descriptor); } } removed } } + +impl Drop for FdTable { + fn drop(&mut self) { + // Final owner teardown cannot report close errors. Continue closing the + // remaining descriptors so one failed flush cannot retain the rest. + for descriptor in self.remove_all() { + let _ = descriptor.close(); + } + } +} diff --git a/process/kidentity/docs/design.md b/process/kidentity/docs/design.md index 2f2677413e17d3b24a1b461f8df275413273c8cf..7fde77cb7023bca14b28238328579a46213f38ff 100644 --- a/process/kidentity/docs/design.md +++ b/process/kidentity/docs/design.md @@ -109,7 +109,7 @@ PidHandle ### PID 1 由 boot lifecycle 保证 root namespace 的普通分配器从 1 开始。boot、idle、late-init 和普通内核 -worker 都使用 PID-less identity,因此 `SystemInitEntry` 创建 init 时的第一笔 +worker 都使用 PID-less identity,因此 `SystemUserspace` 创建 init 时的第一笔 普通分配必须得到 PID 1。后续 PID 不承载启动期固定角色,按正常分配顺序产生。 如果未来某条早期路径在 init 创建前启动 Linux-visible task,init 侧的 PID 1 断言会暴露启动顺序破坏;`kidentity` 本身不保存 init 专用全局 handle。 diff --git a/process/kprocess/docs/design.md b/process/kprocess/docs/design.md index d732bb612289ced3ce7c1304f40ade576a60c3c8..870e754d10a50a3e18e73d8fc280b2de97739ab8 100644 --- a/process/kprocess/docs/design.md +++ b/process/kprocess/docs/design.md @@ -91,6 +91,12 @@ Process │ └─ group / member_slot ├─ pid_publication_slot └─ runtime_ref: Option> + │ + v + ProcessRuntime + ├─ resources.files: Option>> + ├─ fs_context: Option>> + └─ nsproxy: Option> │ ├─ Thread │ ├─ real_cred: RwLock> @@ -145,7 +151,9 @@ Created ──publish_task──> Running ──exit_thread(last)──> Exiting | `Running` | `Dead/Reaped` | 最后一个线程退出后 child-exit 策略要求 autoreap | | `Zombie` | `Dead/Reaped` | 父进程 wait 路径调用 `free` 或 `wait_reap` 获得单赢家 | -`Process::exit` 对 init 进程直接返回。 +`Process::exit` 不把 init 进程转换为 zombie/dead,使它在断电前继续作为 +全局 reaper identity;init 最后一个线程完成 runtime owner 清理后仍发布已有的 +process exit completion,系统生命周期监督者以该 completion 作为 teardown 屏障。 普通进程退出时设置退出状态,并把子进程 reparent 到 init 进程。 `free` 只允许已退出进程调用,并从父进程 children 表移除当前进程。 @@ -231,15 +239,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. 最后一个线程先释放 active mm user,再经 `Process::release_runtime_owners()` + 依次取走 files、`FsStruct` 和 `NsProxy` owner。已有 owner slot 用 `Option` + 表达 attached/detached,退出后 capability accessor 返回 `NoSuchProcess`,不另建 + shutdown 状态机。 + 系统终止监督者通过 `Process::wait_for_exit()` 等待这一转换完成,不替存活任务 + 释放任何 owner。 +3. `posix-process` runtime glue 经 `process_exit` 语义模块触发稳定 `Process` 的 exited-state 转换。 +4. `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. 默认 SIGCHLD 语义下,waitable zombie 之后该 `Process` 仍保持 published 身份,继续承担 wait/pidfd/reap 语义;与此同时,外部 `live` 查询必须开始把它视为不可操作对象。 +6. 弱 runtime 引用不在 zombie 转换时主动清除,但 files/fs/ns capability 已在前一阶段 + 置空;zombie 只能保留身份、rlimit、signal/lifecycle 等仍有观察语义的状态,不能继续 + 固定 mount。runtime 对象最终由 `Thread -> Arc` 强引用自然结束。 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 采样 @@ -348,7 +363,8 @@ path + 旧 argv 的混合状态。 - `kidentity` 当前按 namespace 线性分配 `PidHandle`;当前阶段不做回收,后续如引入 pid reuse,应仍保持 “publish before runnable” 不变量。 - `PidHandle` 已能携带 namespace 链,但 `kprocess` 对外 `pid()/tid()` 仍固定返回 root/global 编号;在 wait、kill、procfs、registry 全部 namespace-aware 之前,不把 namespace-visible 编号暴露到对外主语义。 - `ProcessLifecycleState` 的 `child_exit_event` 通过 `Arc` 表达父进程可连续观察的 child-exit 事件流;单个 process 自身的 - `exit_event` 通过 `Arc` 表达 sticky completion,供 pidfd 和其它 late observer 注册。已退出线程和已回收 child CPU time + `exit_event` 通过 `Arc` 表达 sticky completion,供 pidfd、 + `Process::wait_for_exit()` 和其它 late observer 注册。已退出线程和已回收 child CPU time 对外统一使用 `TimeSpan`;`ProcessCpuTotals` 仅在内部以 relaxed `u64` 纳秒原子保存表示,加载后立即恢复为语义类型。 - `ProcessGroup::processes` 和 `Session::process_groups` 使用 `WeakMap`,避免 group/session 成员表延长成员生命周期。 - `Thread::real_cred` 与 `Thread::cred` 分别使用 `RwLock>`;读路径只克隆 `Arc`,写路径按固定顺序同时替换两个指针。 @@ -414,6 +430,9 @@ process group 和 session 是查询索引,不拥有成员进程或成员进程 当前实现把退出进程的子进程统一 reparent 到 init。 这满足基本 wait 和 orphan child 处理需求。 +init 的 leader task 退出只表示该 task 已停止;多线程 init 的 process exit +completion 要等最后一个线程完成 mm、files、`FsStruct` 和 namespace owner 清理后 +才发布。completion 不改变 init 的 reaper identity,也不增加额外关机状态。 subreaper 尚未实现,代码保留 TODO。 ## Drop / 资源释放 @@ -421,6 +440,9 @@ subreaper 尚未实现,代码保留 TODO。 - `Process` 没有自定义 `Drop`,生命周期由 `Arc` 引用计数控制。 - 父进程 children 表持有子进程强引用,`free` 或 autoreap 从父表移除已退出子进程后释放这条所有权边。 - 用户进程的大块地址空间资源不依赖 `ktask` GC;最后一个 runtime `MmUserHandle` 释放时会同步清理 `MmSpace` 的用户映射。 +- files、`FsStruct` 和 `NsProxy` 不等待 `ProcessRuntime` 本身 drop;最后线程退出时从各自 + `Option` owner 中取走。共享 fd table 在最后 files owner drop 时逐项 close,共享 + `FsStruct`/namespace 也由最后一个 `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 9a77c12c08003f1748349d7a2d739502f0a4f293..9e05fd4504e559e3f103ffa4855d4c3850aba6ee 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 锁下更新, @@ -72,7 +74,7 @@ kprocess / posix/process / ksyscall / ktty |------|----------|----------|----------|----------| | T-01 | PID、PGID 或 SID 冲突导致关系图错误 | 中 | 调用者用已存在 ID 创建进程、group 或 session | `create_session` 和 `create_group` 文档要求调用者先做冲突检查;`posix/process` 通过 registry 检查 group | | T-02 | 跨 session 移动进程破坏 job-control 隔离 | 中 | 调用 `move_to_group` 时目标 group 属于其他 session | `move_to_group` 比较 `Arc`,不同 session 返回 `false` | -| T-03 | init 进程退出导致 reaper 缺失 | 中 | 调用者对 init 进程调用 `exit` | `Process::exit` 对 init 进程直接返回 | +| T-03 | init 进程退出导致 reaper 缺失 | 中 | 调用者对 init 进程调用 `exit` | init 不转换为 zombie/dead,保留全局 reaper identity;最后线程仍发布 process exit completion 供系统 teardown 等待 | | T-04 | 未退出进程被提前回收 | 中 | 调用者对运行中进程调用 `free` | `free` 断言进程已经 exited,错误调用触发 panic | | T-05 | 控制终端被重复绑定 | 中 | 多个 TTY 尝试设置同一 session terminal | `set_terminal` 返回 `SetTerminalResult::Occupied`;TTY 侧只在短临界区安装已构造 terminal,并在失败时回滚 job-control session | | T-06 | 错误终端对象清除当前绑定 | 中 | 调用者传入非当前 terminal 的对象调用 `unset_terminal` | `unset_terminal` 使用 `Arc::ptr_eq` 校验对象一致性 | @@ -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 runtime 继续固定文件系统 mount | 高 | 只等待整个 `ProcessRuntime` drop,未取走 `FsStruct`/`NsProxy` | 最后线程在 exited-state 发布前执行 `exit_fs()`/`exit_namespaces()`;accessor 对空 owner 返回 `NoSuchProcess` | 影响等级定义: @@ -110,6 +113,7 @@ kprocess / posix/process / ksyscall / ktty | F-07 | 线程集合统计不准 | 调用者漏调 `add_thread` 或 `exit_thread` | `threads()`、CPU time 和 rusage 统计错误 | procfs、wait、timer 逻辑受影响 | 3 | clone 和 exit 路径集中调用对应 API | | F-08 | 中断上下文执行进程关系修改 | IRQ 路径误调用 `fork`、`exit`、`create_session` 或 group mutation | 关中断持锁时间变长 | 调度延迟上升,严重时影响系统响应 | 2 | 进程关系修改限定在启动、clone、exit、wait 和 syscall job-control 路径 | | F-09 | PID/TID publication 目录泄漏 | exit/wait 路径只逻辑失效 slot | RustHeap 随累计 fork 线性增长,buddy 外部碎片 | spawn 类压力测试 OOM | 2 | 热路径按发布身份精确 retire/删除匹配 PID/TID 槽;`cleanup()` 仅作 group/session 兜底 | +| F-10 | 只等待 init leader task | 多线程 init 尚有 sibling 存活 | runtime owner 仍可访问 mm/files/fs namespace | mount teardown 与活跃引用并发,文件系统无法完成 final shutdown | 1 | init 最后线程完成 owner 清理后发布 process exit completion;系统监督者在 namespace teardown 前等待该 completion | 严重度定义: @@ -159,6 +163,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 已空时拒绝访问,且 drop 是否发生在对应锁外。 - 新增地址空间退出清理是否只在最后一个 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/lifecycle.rs b/process/kprocess/src/lifecycle.rs index 9bef641874227c59b8dbe4e671b0e7cb5bbcdbd8..54f8e7a1119a75182fc97fc38799b3413f729dbf 100644 --- a/process/kprocess/src/lifecycle.rs +++ b/process/kprocess/src/lifecycle.rs @@ -62,9 +62,9 @@ impl ProcessLifecycleState { self.events.exit_event() } - /// Wakes waiters that are blocked on this process exiting. - pub(crate) fn notify_exit(&self) { - self.events.notify_exit(); + /// Completes process exit while deferring waiter wakeup to the caller. + pub(crate) fn complete_exit_defer_wake(&self) -> PollSet { + self.events.complete_exit_defer_wake() } /// Adds exited-thread CPU time to the accumulated counters. @@ -113,8 +113,8 @@ impl ProcessEvents { &self.exit_event } - fn notify_exit(&self) { - self.exit_event.complete_all(); + fn complete_exit_defer_wake(&self) -> PollSet { + self.exit_event.complete_all_defer_wake() } } diff --git a/process/kprocess/src/process/exit.rs b/process/kprocess/src/process/exit.rs index 5bf5bb12460fc900a6aa9dd2415e95b1fb1dd1c0..402c54cb78d1a53661bc02340437fb2596b438a8 100644 --- a/process/kprocess/src/process/exit.rs +++ b/process/kprocess/src/process/exit.rs @@ -197,6 +197,11 @@ impl Process { self.exit_state() != ProcessExitState::Running } + /// Waits until every thread in this process has completed process exit. + pub fn wait_for_exit(&self) { + ktask::future::wait_until_completed(self.exit_event()); + } + /// Returns `true` if the process can currently be consumed by `wait*()`. pub fn is_waitable_zombie(&self) -> bool { self.exit_state() == ProcessExitState::Zombie @@ -229,7 +234,11 @@ impl Process { self.lifecycle.notify_child_exit(); } - /// Returns the process-exit event for this process. + /// Returns the completion published after the final process-exit cleanup. + /// + /// Init keeps its running reaper identity after this completion is + /// published; callers that need the cleanup barrier should observe this + /// event rather than [`Self::is_exited`]. pub fn exit_event(&self) -> &Arc { self.lifecycle.exit_event() } @@ -288,7 +297,7 @@ impl Process { .is_some_and(|(_, should_autoreap)| *should_autoreap); let prepared_sigchld = prepared.map(|(prepared, _)| prepared); - let (transition, notify_exit, retry) = { + let (transition, exit_waiters, retry) = { let domain = process_domain::write_lock(); let current = self.exit_parent_snapshot_locked(); if !snapshot.same_contract(¤t) @@ -302,7 +311,7 @@ impl Process { autoreaped: false, reparented_zombie_parent: None, }, - false, + None, true, ) } else { @@ -313,6 +322,12 @@ impl Process { } else { None }; + // Init retains its global reaper identity, but its final + // cleanup still completes process-lifecycle waiters. + let process_completed = + state_changed || (self.is_init() && !self.exit_event().is_completed()); + let exit_waiters = + process_completed.then(|| self.lifecycle.complete_exit_defer_wake()); ( ProcessExitTransition { @@ -322,14 +337,14 @@ impl Process { autoreaped: autoreap && state_changed, reparented_zombie_parent, }, - state_changed, + exit_waiters, false, ) } }; - if notify_exit { - self.lifecycle.notify_exit(); + if let Some(exit_waiters) = exit_waiters { + exit_waiters.wake(); } if !retry { return transition; diff --git a/process/kprocess/src/process/runtime_access.rs b/process/kprocess/src/process/runtime_access.rs index ddef0bad7059f9fe90e6edf713b4f15c2183ca96..a4d6657e5e54a829dd63e641f7dacdc7f3c4de3c 100644 --- a/process/kprocess/src/process/runtime_access.rs +++ b/process/kprocess/src/process/runtime_access.rs @@ -147,18 +147,20 @@ impl Process { /// Returns the filesystem context while runtime remains attached. 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. 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. 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. @@ -393,7 +395,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 +414,23 @@ 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 process-owned capabilities during final process exit. + /// + /// Address-space users are released before files, filesystem context, and + /// namespace ownership. Keeping the sequence on `Process` makes the owner + /// transition explicit at the process lifecycle boundary. + pub fn release_runtime_owners(&self) -> KResult<()> { + let runtime = self.runtime()?; + runtime.clear_exclusive_address_space(); + runtime.resources().exit_files(); + runtime.exit_fs(); + 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_runtime/mod.rs b/process/kprocess/src/process_runtime/mod.rs index 985965a785c20adff7f2d8f8a6ccdd7b6795e1e1..a7bd94e300703b0b3cfaacbba9e2a3103f89569c 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, @@ -249,10 +249,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, @@ -351,19 +351,31 @@ impl ProcessRuntime { self.runtime_state.clear_exclusive_address_space() } - /// 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()) + } + + /// Detaches this process from its filesystem context. + pub(crate) fn exit_fs(&self) { + let fs_context = self.fs_context.write().take(); + drop(fs_context); + } + + /// Detaches this process from its namespace proxy. + pub(crate) fn exit_namespaces(&self) { + let nsproxy = self.nsproxy.write().take(); + drop(nsproxy); } /// Returns the top address of the user heap. @@ -449,7 +461,7 @@ pub(crate) fn fork_process_runtime( address_space, signal_actions, config, - ); + )?; Ok((process_runtime, leader_task_number)) } @@ -458,7 +470,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 +480,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 +538,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 +556,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/tests.rs b/process/kprocess/src/tests.rs index 52dc2a480e225f5248a65182f90d4412730ab7a5..cc1b096bc7c84f513c312c19a807b6584e630b72 100644 --- a/process/kprocess/src/tests.rs +++ b/process/kprocess/src/tests.rs @@ -778,6 +778,72 @@ fn test_exit_cleanup_clears_exclusive_address_space() { wait_reap::assert_reap_zombie_process(&proc); } +#[def_test(serial)] +fn test_release_runtime_owners_detaches_exit_capabilities() { + let (proc, _task) = process_with_address_space(8_137, mapped_test_address_space()); + + assert!(proc.fs_context().is_ok()); + assert!(proc.mnt_ns().is_ok()); + assert!(proc.resources().unwrap().fd_table().is_ok()); + + proc.release_runtime_owners() + .expect("runtime must remain reachable during owner release"); + + assert!(proc.address_space().is_err()); + assert!(proc.fs_context().is_err()); + assert!(proc.mnt_ns().is_err()); + assert!(proc.resources().unwrap().fd_table().is_err()); + proc.release_runtime_owners() + .expect("owner release must be idempotent while runtime remains reachable"); + + process_exit::finalize_process_exit(&proc); + wait_reap::assert_reap_zombie_process(&proc); +} + +#[def_test(serial)] +fn test_range_unshare_linearizes_concurrent_fd_table_acquisition() { + let resources = kresources::ProcessResources::new(0x80000); + let original_table = resources.fd_table().expect("fd table should be installed"); + let original_table_guard = original_table.write(); + + let range_resources = resources.clone(); + let range_task = ktask::spawn(move || { + range_resources + .set_cloexec_range(0, 0, true) + .expect("range unshare should succeed"); + }); + while range_task.state() != ktask::TaskState::Blocked { + ktask::yield_now(); + } + + let acquired_table_ptr = Arc::new(AtomicUsize::new(0)); + let acquisition_resources = resources.clone(); + let observed_ptr = acquired_table_ptr.clone(); + let acquisition_task = ktask::spawn(move || { + let table = acquisition_resources + .fd_table() + .expect("fd table should remain installed"); + observed_ptr.store(Arc::as_ptr(&table) as usize, Ordering::Release); + }); + while acquisition_task.state() != ktask::TaskState::Blocked { + ktask::yield_now(); + } + + drop(original_table_guard); + range_task.join(); + acquisition_task.join(); + + let current_table = resources + .fd_table() + .expect("fd table should remain installed"); + assert!(!Arc::ptr_eq(&original_table, ¤t_table)); + assert_eq!( + acquired_table_ptr.load(Ordering::Acquire), + Arc::as_ptr(¤t_table) as usize, + "a concurrent owner acquisition must observe the post-unshare table" + ); +} + #[def_test(serial)] fn test_exit_cleanup_ignores_non_runtime_address_space_refs() { let observed_address_space = mapped_test_address_space(); @@ -976,6 +1042,10 @@ fn test_group_exit_prevents_late_thread_exit_from_overwriting_exit_code() { let not_last = proc.exit_thread(&leader_task, 11); assert!(!not_last, "first exiting thread should not be last"); + assert!( + !proc.exit_event().is_completed(), + "process completion must wait for every thread" + ); assert_eq!( proc.exit_code(), 11, @@ -985,6 +1055,10 @@ fn test_group_exit_prevents_late_thread_exit_from_overwriting_exit_code() { proc.group_exit(); let is_last = proc.exit_thread(&sibling_task, 22); assert!(is_last, "second exiting thread should be the last thread"); + assert!( + !proc.exit_event().is_completed(), + "last-thread removal alone must not publish process completion" + ); assert_eq!( proc.exit_code(), 11, @@ -992,9 +1066,42 @@ fn test_group_exit_prevents_late_thread_exit_from_overwriting_exit_code() { ); proc.exit_with_publication(ProcessExitPublication::WaitableZombie); + assert!( + proc.exit_event().is_completed(), + "final process-exit publication must complete waiters" + ); proc.free(); } +#[def_test(serial)] +fn test_wait_for_exit_blocks_until_process_completion() { + let init = ensure_init(); + let process = init.fork(502); + let is_waiting = Arc::new(AtomicBool::new(false)); + let has_returned = Arc::new(AtomicBool::new(false)); + let waiter_process = process.clone(); + let waiter_is_waiting = is_waiting.clone(); + let waiter_has_returned = has_returned.clone(); + + let waiter = ktask::spawn(move || { + waiter_is_waiting.store(true, Ordering::Release); + waiter_process.wait_for_exit(); + waiter_has_returned.store(true, Ordering::Release); + }); + + while !is_waiting.load(Ordering::Acquire) || waiter.state() != ktask::TaskState::Blocked { + ktask::yield_now(); + } + assert!(!has_returned.load(Ordering::Acquire)); + + process.exit_with_publication(ProcessExitPublication::WaitableZombie); + waiter.join(); + assert!(has_returned.load(Ordering::Acquire)); + + process.wait_for_exit(); + process.free(); +} + #[def_test(serial)] fn test_process_exit_notifies_pidfd_and_parent_waiters() { let init = ensure_init(); diff --git a/process/kresources/src/lib.rs b/process/kresources/src/lib.rs index 0ddb60ec135378f4be3f1eeb812bd8b41cf8d61c..8b4d931b5ce0614e912ce0094b33453ed7f23c93 100644 --- a/process/kresources/src/lib.rs +++ b/process/kresources/src/lib.rs @@ -26,7 +26,7 @@ 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())), }) } @@ -84,13 +84,46 @@ impl ProcessResources { } /// Returns the current file descriptor table handle. - pub fn fd_table(&self) -> Arc> { - self.fd_table.read().clone() + 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 with_fd_table_for_range( + &self, + should_unshare: bool, + access_fn: impl FnOnce(&mut FdTable) -> R, + ) -> KResult { + let (result, old_table) = { + let mut owner = self.fd_table.write(); + let table = owner.as_ref().ok_or(KError::NoSuchProcess)?; + let old_table = if should_unshare && Arc::strong_count(table) > 1 { + let private_table = FdTable::clone_shared_from(table); + Some( + owner + .replace(private_table) + .expect("fd table owner was checked"), + ) + } else { + None + }; + + let mut table = owner.as_ref().expect("fd table owner was checked").write(); + let result = access_fn(&mut table); + drop(table); + (result, old_table) + }; + + drop(old_table); + Ok(result) } fn close_descriptors(descriptors: impl IntoIterator) { @@ -161,54 +194,70 @@ impl ProcessResources { } /// Closes all descriptors in the given inclusive range. - pub fn close_range(&self, first_fd: c_int, last_fd: c_int) { - let descriptors = - self.with_fd_table(|fd_table| fd_table.write().remove_range(first_fd, last_fd)); + /// + /// When `should_unshare` is true, a shared table is detached before the + /// range mutation as one operation on the process's table owner. + pub fn close_range( + &self, + first_fd: c_int, + last_fd: c_int, + should_unshare: bool, + ) -> KResult<()> { + let descriptors = self.with_fd_table_for_range(should_unshare, |fd_table| { + fd_table.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)); + /// + /// When `should_unshare` is true, a shared table is detached before the + /// range mutation as one operation on the process's table owner. + pub fn set_cloexec_range( + &self, + first_fd: c_int, + last_fd: c_int, + should_unshare: bool, + ) -> KResult<()> { + self.with_fd_table_for_range(should_unshare, |fd_table| { + fd_table.set_cloexec_range(first_fd, last_fd); + }) } /// 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()); - Self::close_descriptors(descriptors); - } - - /// 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) - }; + 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(()) } - /// 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); + /// Detaches the process from its file descriptor table. + /// + /// A shared table is closed when its final process owner releases it. + pub fn exit_files(&self) { + let fd_table = self.fd_table.write().take(); + drop(fd_table); } /// Replaces the file descriptor table handle. - pub fn replace_fd_table(&self, table: Arc>) -> Arc> { - core::mem::replace(&mut *self.fd_table.write(), 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 +267,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 +332,62 @@ 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) + ); + assert_eq!( + resources.fd_table().err(), + Some(kerrno::KError::NoSuchProcess) + ); + resources.exit_files(); + } + + #[def_test] + fn test_range_unshare_keeps_private_fd_table() { + let resources = ProcessResources::new(0x80000); + let before = { + let owner = resources.fd_table.read(); + Arc::as_ptr(owner.as_ref().expect("fd table should be installed")) + }; + + resources + .set_cloexec_range(0, 0, true) + .expect("private fd table should remain usable"); + + let after = { + let owner = resources.fd_table.read(); + Arc::as_ptr(owner.as_ref().expect("fd table should remain installed")) + }; + assert_eq!(before, after, "private fd table must not be replaced"); + } + + #[def_test] + fn test_range_unshare_clones_shared_fd_table() { + let resources = ProcessResources::new(0x80000); + let shared_table = resources.fd_table().expect("fd table should be installed"); + + resources + .set_cloexec_range(0, 0, true) + .expect("shared fd table should be cloned"); + + let private_table = resources + .fd_table() + .expect("fd table should remain installed"); + assert!( + !Arc::ptr_eq(&shared_table, &private_table), + "unshare must detach a shared fd table" + ); + } } diff --git a/task/ktask/docs/design.md b/task/ktask/docs/design.md index 6196145742f07adaa02a79f032ea912e94d5e188..5f550422f8e353af84e421f5cf5a6f2c96b962b1 100644 --- a/task/ktask/docs/design.md +++ b/task/ktask/docs/design.md @@ -206,13 +206,19 @@ active exception context 恢复到当前 CPU。否则旧 CPU 会一直认为自 - `WaitQueue` 基于 `event_listener` 封装等待与通知,支持超时与条件等待。 - `TaskInner::join()` 使用 `kpoll::Completion` 作为 per-task exit wait source。任务退出时先发布 `exit_code`,再把 state 切到 `Exited` 并 `complete_all()`;joiner 的真实完成条件仍是 - `TaskState::Exited`,completion 只负责避免丢失 wake 并支持 late joiner。 + `TaskState::Exited`,completion 只负责避免丢失 wake 并支持 late joiner。等待算法由 + `future::wait_until_completed()` 提供,进程退出屏障复用同一套注册失败 yield/retry 语义。 - `ktask` 通过 `kirq::IrqSyncWaitIf` 提供 completion-backed blocking wait,使 `kirq::synchronize_irq()` / `free_irq()` 能阻塞当前 task;IRQ descriptor 生命周期和 `in_flight` predicate 仍由 `kirq` 拥有。 ### 6) 退出回收(GC task) +`exit_current()` 对所有非-idle task 使用同一状态转换:发布 exit code、把 task 置为 +`Exited`、唤醒 joiner、进入 per-CPU `EXITED_TASKS`,然后 reschedule。scheduler 的 boot +current/idle 标记不承担系统电源策略,也不能从任务退出路径直接调用 HAL terminal operation;PID 1 +是否触发系统关机由 `kruntime` 中持有其 task handle 的 supervisor 决定。 + 每 CPU run queue 在创建时自动加入一个 `gc` 任务,循环执行 `poll_gc`: - 消费 `EXITED_TASKS` 列表 diff --git a/task/ktask/docs/security.md b/task/ktask/docs/security.md index 1fb40e1536443239cebc46860e3208060f5b96e9..febc91f16d3bb465e1ec638baecb1ec2bba9cd51 100644 --- a/task/ktask/docs/security.md +++ b/task/ktask/docs/security.md @@ -103,6 +103,8 @@ ksched algorithms / karch context switch / allocator 3. **SMP blocked 唤醒保护**:Blocked 任务重新入队前(`smp`)等待 `task.on_cpu()==false`,防止与远端 CPU `switch_to` 并发。 4. **唤醒抢占请求分离**:waker 只把任务转为 `Ready` 并设置本地或远端 `need_resched` 请求,真实切换仍发生在抢占安全点。 5. **退出回收隔离**:退出任务先进入 `EXITED_TASKS`,由每 CPU `gc_task` 延迟回收,避免切换路径直接 drop。 + 所有非-idle task 都必须先发布 `Exited` 并唤醒 joiner;`exit_current()` 不拥有平台电源 + 策略,不得因 scheduler-internal init 标记直接断电。 6. **idle 任务特判**:idle 不入普通调度实体路径,不参与 `task_tick`,避免算法元数据污染。 7. **发布先于 runnable**:需要额外注册对象图的调用方必须先 `prepare_task()`, 完成外部 publish 后再 `activate_task()`,避免 task 先运行、后补注册。PID 1 同样 @@ -127,6 +129,7 @@ ksched algorithms / karch context switch / allocator | T-09 | 远端唤醒后未及时调度 | 中 | 任务入远端 run queue 但远端 CPU 未到抢占安全点 | `ipi + preempt` 下请求远端 `need_resched`;无 IPI 时仍依赖 tick/安全点 | | T-09b | IRQ teardown 等待在错误上下文阻塞当前任务 | 高 | hardirq/softirq/BH-disabled 路径间接调用 `IrqSyncWaitIf` provider | `kirq` 在进入 provider 前执行 context gate;`ktask` 只提供阻塞机制,不放宽 IRQ 同步 API 约束 | | T-10 | 绝对 timer deadline 在整数转换时截断 | 高 | `TimeSpan::as_nanos()` 的 `u128` 结果直接窄化为 `u64` | HAL 接口保持 `MonotonicInstant`;仅 timer backend 在寄存器边界执行钳制转换 | +| T-11 | 特殊 task 退出绕过 joiner 和资源 owner | 高 | `exit_current()` 识别 init 标记后直接平台断电 | 所有非-idle task 统一发布 `Exited`、进入 GC 并 reschedule;系统关机由 `kruntime` supervisor 编排 | ## 故障模式与影响分析(FMEA) diff --git a/task/ktask/src/future/mod.rs b/task/ktask/src/future/mod.rs index 79f33dcd03c8977b34294b10320332fe00e9fd80..2d68a03974cecc58f5e56e2177c1a1cbc9c91b66 100644 --- a/task/ktask/src/future/mod.rs +++ b/task/ktask/src/future/mod.rs @@ -13,10 +13,10 @@ use core::{ }; use kerrno::KError; -use kpoll::{PollRegisterError, PollRegistrations}; +use kpoll::{Completion, PollRegisterError, PollRegistrations}; use kspin::{NoPreemptIrqSave, SpinNoIrq}; -use crate::{KtaskRef, WeakKtaskRef, current, current_run_queue, select_wake_run_queue}; +use crate::{KtaskRef, WeakKtaskRef, current, current_run_queue, select_wake_run_queue, yield_now}; mod poll; pub use poll::*; @@ -98,6 +98,34 @@ pub fn block_on(f: F) -> F::Output { } } +/// Blocks until a terminal completion has been published. +/// +/// This observes rather than consumes the completion state, so it is intended +/// for lifecycle barriers completed with [`Completion::complete_all`]. If poll +/// registration cannot allocate, the current task yields and retries without +/// treating the failure as completion. +pub fn wait_until_completed(completion: &Completion) { + let mut registrations = PollRegistrations::new(); + block_on(poll_fn(|cx| { + loop { + if completion.is_completed() { + return Poll::Ready(()); + } + let mut context = registrations.context(cx); + if completion.register(&mut context).is_err() { + drop(context); + yield_now(); + continue; + } + drop(context); + if completion.is_completed() { + return Poll::Ready(()); + } + return Poll::Pending; + } + })); +} + /// Error returned by [`interruptible`]. #[derive(Debug, PartialEq, Eq)] pub struct Interrupted(InterruptCause); diff --git a/task/ktask/src/run_queue.rs b/task/ktask/src/run_queue.rs index 357d981be9c899931d8f5500527830aa108b81de..9ca34f0d1268dfa5e76fd68655c105e6ebd61000 100644 --- a/task/ktask/src/run_queue.rs +++ b/task/ktask/src/run_queue.rs @@ -890,36 +890,27 @@ impl CurrentRunQueueRef<'_, G> { debug!("task exit: {}, exit_code={}", curr.id_name(), exit_code); assert!(curr.is_running(), "task is not running: {:?}", curr.state()); assert!(!curr.is_idle()); - if curr.is_init() { - // SAFETY: `exit_current` runs under - // `current_run_queue::()`, so IRQs and - // preemption are disabled while touching the current CPU's percpu - // exited-task list. - current_exited_tasks_mut().clear(); - khal::power::shutdown(); - } else { - // Notify the joiner task. - curr.notify_exit(exit_code); - - // SAFETY: `exit_current` runs under - // `current_run_queue::()`, so IRQs and - // preemption are disabled while touching current-CPU percpu - // scheduler queues and wakers. - // Push current task to the `EXITED_TASKS` list, which will be consumed by the GC task. - current_exited_tasks_mut().push_back(curr.clone()); - // Wake up the GC task to drop the exited tasks. - current_wait_for_exit().wake(); - - // Exited list owns the task; `leave_current(Exit)` clears scheduler - // current accounting (e.g. EEVDF `curr`) and must not arm PLACE_LAG. - self.inner - .scheduler - .lock() - .leave_current(curr.clone(), CurrentDisposition::Exit); + // Notify the joiner task. + curr.notify_exit(exit_code); + + // SAFETY: `exit_current` runs under + // `current_run_queue::()`, so IRQs and + // preemption are disabled while touching current-CPU percpu + // scheduler queues and wakers. + // Push current task to the `EXITED_TASKS` list, which will be consumed by the GC task. + current_exited_tasks_mut().push_back(curr.clone()); + // Wake up the GC task to drop the exited tasks. + current_wait_for_exit().wake(); + + // Exited list owns the task; `leave_current(Exit)` clears scheduler + // current accounting (e.g. EEVDF `curr`) and must not arm PLACE_LAG. + self.inner + .scheduler + .lock() + .leave_current(curr.clone(), CurrentDisposition::Exit); - // Schedule to next task. - self.inner.resched(); - } + // Schedule to next task. + self.inner.resched(); unreachable!("task exited!"); } diff --git a/task/ktask/src/task.rs b/task/ktask/src/task.rs index 03a5aaaae956d138164cd65feceac8f33fb2c485..f4a735e3a8f21974ac718aee973fd2c593b03a5e 100644 --- a/task/ktask/src/task.rs +++ b/task/ktask/src/task.rs @@ -12,7 +12,6 @@ use core::{ any::Any, cell::{Cell, UnsafeCell}, fmt, - future::poll_fn, mem::ManuallyDrop, ops::Deref, ptr::NonNull, @@ -27,11 +26,11 @@ use kerrno::KResult; use khal::context::TaskContext; #[cfg(feature = "tls")] use khal::tls::TlsArea; -use kpoll::{Completion, PollRegistrations, PollSet}; +use kpoll::{Completion, PollSet}; use kspin::SpinNoIrq; use memaddr::{VirtAddr, align_up_4k}; -use crate::{KCpuMask, KTask, KtaskRef, future::block_on, yield_now}; +use crate::{KCpuMask, KTask, KtaskRef}; enum TaskIdentity { Idle, @@ -415,27 +414,10 @@ impl TaskInner { /// /// It will return immediately if the task has already exited (but not dropped). pub fn join(&self) -> i32 { - let mut registrations = PollRegistrations::new(); - block_on(poll_fn(|cx| { - loop { - if self.state() == TaskState::Exited { - return Poll::Ready(self.exit_code.load(Ordering::Acquire)); - } - let mut context = registrations.context(cx); - if self.wait_for_exit.register(&mut context).is_err() { - drop(context); - // Under memory pressure, yield and retry rather than - // busy-spinning on `wake_by_ref` without a registration. - yield_now(); - continue; - } - drop(context); - if self.state() == TaskState::Exited { - return Poll::Ready(self.exit_code.load(Ordering::Acquire)); - } - return Poll::Pending; - } - })) + if self.state() != TaskState::Exited { + crate::future::wait_until_completed(&self.wait_for_exit); + } + self.exit_code.load(Ordering::Acquire) } /// Returns the runtime attached to this task, if any. diff --git a/tee/tipc/docs/design.md b/tee/tipc/docs/design.md index 0b8020f1b33b3b11ed902c73dbc3965b411b8d38..f6e3cb3c4b5e56e36c538a6271d8405a1d96794d 100644 --- a/tee/tipc/docs/design.md +++ b/tee/tipc/docs/design.md @@ -114,10 +114,12 @@ process/kprocess ProcessRuntime owns per-process HandleTable - 本 crate 是 `no_std` 内核 crate,依赖 `alloc`、`kspin`、`kpoll` 和 `ktask`。 - API 面向 task/syscall 生命周期路径,不应在中断上下文调用。 -- `IpcChan::wait_connected` 会通过 `ktask::future::block_on` 等待连接完成,内部 +- `IpcChan::wait_connected` 会通过 interruptible future 等待连接完成,内部 `PollRegistrations` 跨越 `Pending`;调用方必须处在允许阻塞的上下文。 - `tipc_handle::Handle::register` 使用 `PollContext` 并可返回注册错误;poll/wait 调用方需要位于调度器可用之后,并在每轮 poll 刷新 registration context。 +- `tipc_wait`、`tipc_wait_any` 和同步 connect 都注册 task interrupt;收到 signal + interrupt 时返回 `Interrupted`,由用户 runtime 继续处理 pending signal。 - `tipc_handle::HandleTable` 本身不持锁,调用方负责把它放入 process-local 锁保护中。 - `syscall` 模块依赖 `kprocess`、`khal`、`linux_sysno` 和 `posix-types`,只能在用户线程 syscall 上下文使用。 - `PortRegistry` 是全局命名空间,路径、端口发布和 waiting client 操作由内部 `SpinNoIrq` 串行化。 diff --git a/tee/tipc/docs/security.md b/tee/tipc/docs/security.md index 3dc2075f3c2e5e1229ff35d63c32e704989e79f2..34cacf63f680abede30df01ceae936612709def3 100644 --- a/tee/tipc/docs/security.md +++ b/tee/tipc/docs/security.md @@ -128,6 +128,8 @@ tipc-handle / ktask / kspin / alloc - 队列暂不可用使用 `WouldBlock`,由调用方结合 poll/wait 处理。 - poll source 注册失败映射为 `NoMemory` 并终止当前 wait,避免在没有 wakeup registration 的情况下进入 `Pending`。 +- 阻塞 wait/connect 使用 interruptible future;task interrupt 会结束当前等待并返回 + `Interrupted`,避免 fatal signal 永久滞留在阻塞 syscall 内。 - 对端关闭或 peer weak upgrade 失败返回 `NotConnected`。 - stale registry entry 不 panic,publish/connect 路径会忽略或清理。 - close/drop 路径唤醒等待者,使 poll/wait 能观察 ERROR/HUP。 diff --git a/tee/tipc/src/channel.rs b/tee/tipc/src/channel.rs index c99dea75c53f6ea800f38d8ae9364cd2315eb5e3..2dac121d70007122d7996ccfb043aa32f0624014 100644 --- a/tee/tipc/src/channel.rs +++ b/tee/tipc/src/channel.rs @@ -346,23 +346,29 @@ impl IpcChan { _ => { // Register through a short-lived poll future to avoid a // separate wait primitive in the core object. - ktask::future::block_on(core::future::poll_fn(|cx| { - let mut context = registrations.context(cx); - if self - .register(&mut context, HandleEventMask::READY | HandleEventMask::HUP) - .is_err() - { - return core::task::Poll::Ready(Err(KError::NoMemory)); - } - if matches!( - self.state(), - IpcChanState::Connected | IpcChanState::Disconnecting - ) { - core::task::Poll::Ready(Ok(())) - } else { - core::task::Poll::Pending - } - }))?; + ktask::future::block_on(ktask::future::interruptible(core::future::poll_fn( + |cx| { + let mut context = registrations.context(cx); + if self + .register( + &mut context, + HandleEventMask::READY | HandleEventMask::HUP, + ) + .is_err() + { + return core::task::Poll::Ready(Err(KError::NoMemory)); + } + if matches!( + self.state(), + IpcChanState::Connected | IpcChanState::Disconnecting + ) { + core::task::Poll::Ready(Ok(())) + } else { + core::task::Poll::Pending + } + }, + ))) + .map_err(KError::from)??; } } } diff --git a/tee/tipc/src/syscall.rs b/tee/tipc/src/syscall.rs index 4ef05a7d5e66934c5f78bd36e71f65c9bf9c1d6e..39024185ad21a171378c04dc2277939f3d4c22ee 100644 --- a/tee/tipc/src/syscall.rs +++ b/tee/tipc/src/syscall.rs @@ -151,13 +151,14 @@ fn wait_for_event( mut wait: impl FnMut(&mut PollContext<'_>) -> Poll>, ) -> KResult { let mut registrations = PollRegistrations::new(); - ktask::future::block_on(ktask::future::timeout( + ktask::future::block_on(ktask::future::interruptible(ktask::future::timeout( timeout_duration(timeout_ms), poll_fn(move |cx| { let mut context = registrations.context(cx); wait(&mut context) }), - )) + ))) + .map_err(KError::from)? .map_err(KError::from)? }