feat(sandbox): enforce launcher lifecycle ordering

This commit is contained in:
2026-10-06 11:02:55 +08:00
parent 1f45266517
commit 957135a112
5 changed files with 526 additions and 1 deletions
+16
View File
@@ -78,6 +78,22 @@ FD、栈和其他信号仍按原退出状态记录,缺少专门证据时不猜
工件只能导出规格已申请的路径,单文件与已导出总量均受限;这些字段检查不能替代实际文件安全检查。
准备失败和清理失败分别保留,真实进程树终止、资源回收与 VM 验收仍待后续阶段实现。
## 启动生命周期顺序
`launcher/lifecycle.rs` 提供独立的状态约束:已校验 → 准备 → cgroup/限额确认 → namespace
确认 → 运行根确认 → 身份确认 → seccomp 确认 → exec 放行 → exec 确认 → 等待。
放行与 exec 成功确认分别记录;主进程退出只进入终止阶段,直到整个任务树为空且启动器/init
子进程已回收,才能进行最终计量、输出/工件收集和清理。清理成功后状态才结束。
取消、限额或操作失败停止推进初始化;乱序/重复阶段消息记录协议错误并进入终止流程。
即使调用方忽略错误,也无法继续这次执行的 exec 放行。计量阶段的错误不恢复已回收的任务,
也不跳过剩余收集;启动/监督故障与清理故障独立保留,重试清理成功不抹掉原始错误。
最先停止的原因只用于调度诊断,最终执行分类仍由结果模型依据全部观察确定。
事件仅由可信监督器在核验同一任务的实际操作后产生,不可从执行请求直接反序列化。
该模块不执行系统调用、不证明隔离、不产生执行许可;有界 IPC、单线程启动器、真实终止与
清理仍待实现和独立 VM 验证。当前 CLI `run` 保持拒绝执行。
## 可信运行配置与启动前检查
```bash
@@ -0,0 +1,244 @@
//! Monotone lifecycle ordering for the future trusted supervisor.
//!
//! Callers must produce events from verified operations on the same task. These
//! in-memory types cannot authenticate OS observations and are deliberately not
//! deserializable. In particular, reaching FilterReady is no execution permit.
use crate::outcome::{ErrorCode, ExecutionError, ExecutionStage, LimitKind};
use crate::spec::ValidatedSpec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum State {
Validated,
Preparing,
CgroupReady,
NamespacesReady,
RootReady,
IdentityReady,
FilterReady,
ExecReleased,
Waiting,
Terminating,
Accounting,
Cleaning,
Finished,
}
impl State {
/// Operation currently outstanding; cleanup failures have their own record.
pub fn stage(self) -> Option<ExecutionStage> {
match self {
Self::Validated => Some(ExecutionStage::Preparation),
Self::Preparing => Some(ExecutionStage::Cgroup),
Self::CgroupReady => Some(ExecutionStage::Namespaces),
Self::NamespacesReady => Some(ExecutionStage::RootFilesystem),
Self::RootReady => Some(ExecutionStage::Identity),
Self::IdentityReady => Some(ExecutionStage::Seccomp),
Self::FilterReady | Self::ExecReleased => Some(ExecutionStage::Exec),
Self::Waiting | Self::Terminating => Some(ExecutionStage::Wait),
Self::Accounting => Some(ExecutionStage::Accounting),
Self::Cleaning => Some(ExecutionStage::Cleanup),
Self::Finished => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Event {
BeginPreparation,
/// Child remains blocked; task enrollment and limits are verified.
CgroupConfirmed,
NamespacesConfirmed,
RootConfirmed,
/// UID/GID, supplementary groups, capabilities and no_new_privs checked.
IdentityConfirmed,
FilterConfirmed,
/// Trusted control transport has released the exec gate, not exec success.
ExecGateReleased,
/// The exec error channel confirmed success, distinct from gate release.
ExecConfirmed,
/// Does not imply that descendants have exited or output reached EOF.
MainExited,
Cancel,
LimitObserved(LimitKind),
Failed {
code: ErrorCode,
errno: Option<u16>,
},
/// Entire task tree empty and launcher/init children reaped.
TreeReaped,
/// Final accounting and bounded output/artifact collection settled,
/// including explicit unavailable/error records when collection failed.
CollectionFinished,
CleanupSucceeded,
CleanupFailed {
errno: Option<u16>,
},
}
/// First reason to stop scheduling execution, not a judge/result classifier.
/// Later cancellation and faults remain independently observable.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StopReason {
MainExited,
Cancelled,
Limit(LimitKind),
Failure,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct TransitionError {
pub state: State,
pub event: Event,
}
impl std::fmt::Display for TransitionError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"invalid lifecycle event {:?} in {:?}",
self.event, self.state
)
}
}
impl std::error::Error for TransitionError {}
/// Private fields and no Clone/Deserialize keep each attempt's transitions monotone.
/// Retains at most one primary fault and one independent cleanup fault.
#[derive(Debug)]
pub struct Lifecycle {
run_id: String,
state: State,
stop_reason: Option<StopReason>,
exec_released: bool,
cancel_requested: bool,
failure: Option<ExecutionError>,
cleanup_failure: Option<ExecutionError>,
}
impl Lifecycle {
pub fn new(spec: &ValidatedSpec) -> Self {
Self {
run_id: spec.spec().run_id.clone(),
state: State::Validated,
stop_reason: None,
exec_released: false,
cancel_requested: false,
failure: None,
cleanup_failure: None,
}
}
pub fn run_id(&self) -> &str {
&self.run_id
}
pub fn state(&self) -> State {
self.state
}
pub fn stop_reason(&self) -> Option<StopReason> {
self.stop_reason
}
pub fn exec_released(&self) -> bool {
self.exec_released
}
pub fn cancel_requested(&self) -> bool {
self.cancel_requested
}
pub fn failure(&self) -> Option<&ExecutionError> {
self.failure.as_ref()
}
pub fn cleanup_failure(&self) -> Option<&ExecutionError> {
self.cleanup_failure.as_ref()
}
/// An invalid live event latches a protocol fault and requests termination;
/// ignoring the returned error cannot subsequently advance to exec.
/// This only records ordering: the caller must still perform OS termination.
pub fn apply(&mut self, event: Event) -> Result<(), TransitionError> {
let previous = self.state;
if previous == State::Finished {
return Err(TransitionError {
state: previous,
event,
});
}
match event {
Event::Cancel => {
self.cancel_requested = true;
self.request_stop(StopReason::Cancelled);
return Ok(());
}
Event::LimitObserved(kind) => {
self.request_stop(StopReason::Limit(kind));
return Ok(());
}
Event::Failed { code, errno } if valid_errno(errno) => {
self.record_failure(code, errno);
return Ok(());
}
Event::CleanupFailed { errno } if previous == State::Cleaning && valid_errno(errno) => {
self.record_failure(ErrorCode::CleanupIncomplete, errno);
return Ok(());
}
_ => {}
}
let next = match (previous, event) {
(State::Validated, Event::BeginPreparation) => Some(State::Preparing),
(State::Preparing, Event::CgroupConfirmed) => Some(State::CgroupReady),
(State::CgroupReady, Event::NamespacesConfirmed) => Some(State::NamespacesReady),
(State::NamespacesReady, Event::RootConfirmed) => Some(State::RootReady),
(State::RootReady, Event::IdentityConfirmed) => Some(State::IdentityReady),
(State::IdentityReady, Event::FilterConfirmed) => Some(State::FilterReady),
(State::FilterReady, Event::ExecGateReleased) => Some(State::ExecReleased),
(State::ExecReleased, Event::ExecConfirmed) => Some(State::Waiting),
(State::Waiting, Event::MainExited) => {
self.request_stop(StopReason::MainExited);
return Ok(());
}
(State::Terminating, Event::TreeReaped) => Some(State::Accounting),
(State::Accounting, Event::CollectionFinished) => Some(State::Cleaning),
(State::Cleaning, Event::CleanupSucceeded) => Some(State::Finished),
_ => None,
};
if let Some(next) = next {
self.state = next;
if next == State::ExecReleased {
self.exec_released = true;
}
Ok(())
} else {
self.record_failure(ErrorCode::ProtocolFailed, None);
Err(TransitionError {
state: previous,
event,
})
}
}
fn request_stop(&mut self, reason: StopReason) {
self.stop_reason.get_or_insert(reason);
match self.state {
State::Terminating | State::Accounting | State::Cleaning | State::Finished => {}
_ => self.state = State::Terminating,
}
}
fn record_failure(&mut self, code: ErrorCode, errno: Option<u16>) {
if let Some(stage) = self.state.stage() {
let error = ExecutionError { stage, code, errno };
if self.state == State::Cleaning {
self.cleanup_failure.get_or_insert(error);
} else {
self.failure.get_or_insert(error);
}
// Accounting failures do not skip remaining collection. Once the
// tree is reaped, later faults must never restore a live-task phase.
self.request_stop(StopReason::Failure);
}
}
}
fn valid_errno(errno: Option<u16>) -> bool {
errno.is_none_or(|value| (1..=4095).contains(&value))
}
+7
View File
@@ -0,0 +1,7 @@
//! Trusted supervisor/launcher coordination, separate from user stdio.
//!
//! Lifecycle events describe completed operations, not commands received from
//! an untrusted request. OS operations and authenticated bounded IPC are pending;
//! this module grants no execution authority and does not change CLI refusal.
pub mod lifecycle;
+2 -1
View File
@@ -1,8 +1,9 @@
//! Linux execution boundary: policy/contract checks and read-only observations.
//!
//! JS-01 defines execution contracts; JS-02..04 add the launcher, isolation and limits.
//! Execution contracts and lifecycle ordering precede OS launcher/isolation work.
//! Until that work is verified, the CLI cannot execute any user program.
pub mod launcher;
pub mod outcome;
pub mod probe;
pub mod runtime;
@@ -0,0 +1,257 @@
use kf_sandbox::launcher::lifecycle::{Event, Lifecycle, State, StopReason};
use kf_sandbox::outcome::{ErrorCode, ExecutionStage, LimitKind};
use kf_sandbox::spec::{ExecutionSpec, SpecPolicy};
const STARTUP: [Event; 8] = [
Event::BeginPreparation,
Event::CgroupConfirmed,
Event::NamespacesConfirmed,
Event::RootConfirmed,
Event::IdentityConfirmed,
Event::FilterConfirmed,
Event::ExecGateReleased,
Event::ExecConfirmed,
];
fn lifecycle(prefix: usize) -> Lifecycle {
let spec: ExecutionSpec =
serde_json::from_str(include_str!("fixtures/spec.valid.json")).unwrap();
let spec = spec.validate(&SpecPolicy::default()).unwrap();
let mut task = Lifecycle::new(&spec);
assert_eq!(task.run_id(), spec.spec().run_id);
for event in &STARTUP[..prefix] {
task.apply(*event).unwrap();
}
task
}
fn settle(task: &mut Lifecycle) {
task.apply(Event::TreeReaped).unwrap();
task.apply(Event::CollectionFinished).unwrap();
task.apply(Event::CleanupSucceeded).unwrap();
assert_eq!(task.state(), State::Finished);
}
#[test]
fn initialization_exec_confirmation_and_tree_reaping_are_distinct() {
let mut task = lifecycle(6);
assert_eq!(task.state(), State::FilterReady);
assert!(!task.exec_released());
task.apply(Event::ExecGateReleased).unwrap();
assert_eq!(task.state(), State::ExecReleased);
assert!(task.exec_released());
task.apply(Event::ExecConfirmed).unwrap();
assert_eq!(task.state(), State::Waiting);
task.apply(Event::MainExited).unwrap();
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.stop_reason(), Some(StopReason::MainExited));
settle(&mut task);
assert!(task.failure().is_none());
assert!(task.cleanup_failure().is_none());
}
#[test]
fn every_incomplete_startup_prefix_rejects_and_latches_early_exec() {
for prefix in 0..6 {
let mut task = lifecycle(prefix);
assert!(task.apply(Event::ExecGateReleased).is_err());
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.failure().unwrap().code, ErrorCode::ProtocolFailed);
// Ignoring the error and submitting the omitted confirmations cannot
// revive this attempt or open its exec gate.
for event in &STARTUP[prefix..] {
assert!(task.apply(*event).is_err());
}
assert!(!task.exec_released());
settle(&mut task);
}
}
#[test]
fn duplicated_or_out_of_order_security_confirmation_aborts() {
for (prefix, event) in [
(1, Event::FilterConfirmed),
(2, Event::CgroupConfirmed),
(3, Event::IdentityConfirmed),
(4, Event::RootConfirmed),
(6, Event::FilterConfirmed),
] {
let mut task = lifecycle(prefix);
assert!(task.apply(event).is_err());
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.stop_reason(), Some(StopReason::Failure));
assert!(!task.exec_released());
}
}
#[test]
fn faults_preserve_the_outstanding_stage_and_cannot_restart_execution() {
let stages = [
ExecutionStage::Preparation,
ExecutionStage::Cgroup,
ExecutionStage::Namespaces,
ExecutionStage::RootFilesystem,
ExecutionStage::Identity,
ExecutionStage::Seccomp,
ExecutionStage::Exec,
ExecutionStage::Exec,
ExecutionStage::Wait,
];
for (prefix, stage) in stages.into_iter().enumerate() {
let mut task = lifecycle(prefix);
task.apply(Event::Failed {
code: ErrorCode::OperationFailed,
errno: Some(13),
})
.unwrap();
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.failure().unwrap().stage, stage);
assert_eq!(task.failure().unwrap().errno, Some(13));
assert!(task.apply(Event::ExecGateReleased).is_err());
assert_eq!(task.failure().unwrap().code, ErrorCode::OperationFailed);
settle(&mut task);
}
}
#[test]
fn cancellation_at_every_pre_exec_boundary_is_idempotent_and_never_execs() {
for prefix in 0..=6 {
let mut task = lifecycle(prefix);
task.apply(Event::Cancel).unwrap();
task.apply(Event::Cancel).unwrap();
assert_eq!(task.state(), State::Terminating);
assert!(task.cancel_requested());
assert_eq!(task.stop_reason(), Some(StopReason::Cancelled));
assert!(!task.exec_released());
settle(&mut task);
assert!(task.failure().is_none());
}
}
#[test]
fn cancellation_after_release_still_requires_tree_reaping() {
for prefix in [7, 8] {
let mut task = lifecycle(prefix);
task.apply(Event::Cancel).unwrap();
assert!(task.exec_released());
assert_eq!(task.state(), State::Terminating);
settle(&mut task);
}
}
#[test]
fn main_exit_does_not_authorize_early_accounting_or_cleanup() {
for event in [Event::CollectionFinished, Event::CleanupSucceeded] {
let mut task = lifecycle(8);
task.apply(Event::MainExited).unwrap();
assert!(task.apply(event).is_err());
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.failure().unwrap().stage, ExecutionStage::Wait);
settle(&mut task);
}
}
#[test]
fn cleanup_fault_is_independent_and_retry_does_not_erase_diagnostics() {
let mut task = lifecycle(3);
task.apply(Event::Failed {
code: ErrorCode::OperationFailed,
errno: Some(1),
})
.unwrap();
task.apply(Event::TreeReaped).unwrap();
task.apply(Event::CollectionFinished).unwrap();
task.apply(Event::CleanupFailed { errno: Some(16) })
.unwrap();
assert_eq!(task.state(), State::Cleaning);
assert_eq!(
task.failure().unwrap().stage,
ExecutionStage::RootFilesystem
);
assert_eq!(task.failure().unwrap().errno, Some(1));
assert_eq!(
task.cleanup_failure().unwrap().stage,
ExecutionStage::Cleanup
);
assert_eq!(
task.cleanup_failure().unwrap().code,
ErrorCode::CleanupIncomplete
);
assert_eq!(task.cleanup_failure().unwrap().errno, Some(16));
task.apply(Event::CleanupSucceeded).unwrap();
assert_eq!(task.state(), State::Finished);
assert!(task.cleanup_failure().is_some());
}
#[test]
fn accounting_faults_cannot_restore_live_task_phases_or_skip_collection() {
let mut task = lifecycle(8);
task.apply(Event::MainExited).unwrap();
task.apply(Event::TreeReaped).unwrap();
task.apply(Event::Failed {
code: ErrorCode::MeasurementUnavailable,
errno: None,
})
.unwrap();
task.apply(Event::Cancel).unwrap();
assert_eq!(task.state(), State::Accounting);
assert_eq!(task.failure().unwrap().stage, ExecutionStage::Accounting);
assert!(task.cancel_requested());
assert_eq!(task.stop_reason(), Some(StopReason::MainExited));
assert!(task.apply(Event::CleanupSucceeded).is_err());
assert_eq!(task.state(), State::Accounting);
task.apply(Event::CollectionFinished).unwrap();
task.apply(Event::CleanupSucceeded).unwrap();
}
#[test]
fn malformed_errno_becomes_a_protocol_fault_without_storing_invalid_values() {
for errno in [0, 4096, u16::MAX] {
let mut task = lifecycle(1);
assert!(
task.apply(Event::Failed {
code: ErrorCode::OperationFailed,
errno: Some(errno),
})
.is_err()
);
assert_eq!(task.state(), State::Terminating);
assert_eq!(task.failure().unwrap().code, ErrorCode::ProtocolFailed);
assert_eq!(task.failure().unwrap().errno, None);
}
}
#[test]
fn limit_stop_and_late_cancel_preserve_the_first_stop_reason() {
let mut task = lifecycle(8);
task.apply(Event::LimitObserved(LimitKind::CpuTime))
.unwrap();
task.apply(Event::Cancel).unwrap();
task.apply(Event::LimitObserved(LimitKind::Memory)).unwrap();
assert_eq!(
task.stop_reason(),
Some(StopReason::Limit(LimitKind::CpuTime))
);
assert!(task.cancel_requested());
settle(&mut task);
}
#[test]
fn finished_tasks_reject_all_events_without_reopening_or_mutating_results() {
let mut task = lifecycle(0);
task.apply(Event::Cancel).unwrap();
settle(&mut task);
for event in STARTUP.into_iter().chain([
Event::Cancel,
Event::LimitObserved(LimitKind::Memory),
Event::CleanupFailed { errno: Some(1) },
Event::CleanupSucceeded,
]) {
assert!(task.apply(event).is_err());
assert_eq!(task.state(), State::Finished);
assert_eq!(task.stop_reason(), Some(StopReason::Cancelled));
assert!(!task.exec_released());
assert!(task.failure().is_none());
assert!(task.cleanup_failure().is_none());
}
}