Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/command_sandbox_trampoline.rs
T
AIGameCreator App 689082901f 为持久进程增加可信启动握手
新增 PTY 外私有 bridge 与 sandbox ready/commit/exec 协议
升级 process record v3 并收紧恢复、幂等与 final/idle 门禁
完善 target 前台进程组、graceful 后代收束和审计失败处理
补齐跨平台回归测试及 Runtime 文档
2026-07-14 09:41:18 +08:00

1187 lines
48 KiB
Rust

#[cfg(target_os = "linux")]
mod linux {
use serde::{Deserialize, Serialize};
use std::ffi::OsString;
use std::fs::File;
use std::io::{self, Read, Write};
use std::net::Shutdown;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::os::unix::net::UnixStream;
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
pub(crate) const TRAMPOLINE_MODE_ARG: &str = "--command-sandbox-trampoline";
pub(crate) const PROCESS_SESSION_TRAMPOLINE_MODE_ARG: &str =
"--command-sandbox-process-session-trampoline";
pub(crate) const TRAMPOLINE_PATH: &str = "/run/genarrative-launch/trampoline";
pub(crate) const CONTROL_FD: RawFd = 3;
pub(crate) const BWRAP_STATUS_FD: RawFd = 4;
pub(crate) const BWRAP_BLOCK_FD: RawFd = 5;
pub(crate) const TRAMPOLINE_SOURCE_FD: RawFd = 6;
const SANDBOX_CONTROL_FD: RawFd = libc::STDIN_FILENO;
const CONTROL_FRAME_LIMIT: usize = 64 * 1024;
const NONCE_BYTES: usize = 32;
const PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS: u64 = 800;
#[cfg(test)]
const TEST_FIXTURE_ENV: &str = "GENARRATIVE_COMMAND_SANDBOX_TRAMPOLINE_FIXTURE";
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum ControlFrame {
Prepare {
nonce: Vec<u8>,
},
SandboxReady {
nonce: Vec<u8>,
},
CommitExec {
nonce: Vec<u8>,
executable: Vec<u8>,
arguments: Vec<Vec<u8>>,
},
ExecEstablished {
nonce: Vec<u8>,
},
TargetExecFailed {
nonce: Vec<u8>,
errno: i32,
},
TerminateTarget {
nonce: Vec<u8>,
},
TargetExited {
nonce: Vec<u8>,
code: i32,
},
TargetSignaled {
nonce: Vec<u8>,
signal: i32,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GatePhase {
Prepared,
ChildConfigured,
LauncherSpawned,
ChildCreated,
SandboxReady,
CommitPersisted,
ExecEstablished,
TargetExecFailed,
Terminal,
Aborted,
Failed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GateEvent {
ChildConfigured,
LauncherSpawned,
ChildCreated,
SandboxReady,
CommitPersisted,
ExecEstablished,
TargetExecFailed,
Terminal,
Abort,
Fail,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum TargetStdinMode {
Null,
DuplicateStdout,
}
impl GatePhase {
fn apply(&mut self, event: GateEvent) -> Result<(), String> {
let next = match (*self, event) {
(Self::Prepared, GateEvent::ChildConfigured) => Self::ChildConfigured,
(Self::ChildConfigured, GateEvent::LauncherSpawned) => Self::LauncherSpawned,
(Self::LauncherSpawned, GateEvent::ChildCreated) => Self::ChildCreated,
(Self::ChildCreated, GateEvent::SandboxReady) => Self::SandboxReady,
(Self::SandboxReady, GateEvent::CommitPersisted) => Self::CommitPersisted,
(Self::CommitPersisted, GateEvent::ExecEstablished) => Self::ExecEstablished,
(Self::CommitPersisted, GateEvent::TargetExecFailed) => Self::TargetExecFailed,
(Self::ExecEstablished, GateEvent::Terminal) => Self::Terminal,
(
Self::Prepared
| Self::ChildConfigured
| Self::LauncherSpawned
| Self::ChildCreated
| Self::SandboxReady,
GateEvent::Abort,
) => Self::Aborted,
(
Self::Prepared
| Self::ChildConfigured
| Self::LauncherSpawned
| Self::ChildCreated
| Self::SandboxReady
| Self::CommitPersisted,
GateEvent::Fail,
) => Self::Failed,
(Self::Failed, GateEvent::Abort) => Self::Aborted,
_ => return Err("command sandbox launch gate 状态迁移非法".to_string()),
};
*self = next;
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum TargetExecState {
Established,
Failed { errno: i32 },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum TargetTerminalState {
Exited { code: i32 },
Signaled { signal: i32 },
}
#[derive(Debug)]
pub(crate) struct LaunchGate {
control: UnixStream,
child_control: Option<UnixStream>,
bwrap_status: Option<UnixStream>,
bwrap_status_child: Option<UnixStream>,
bwrap_block: Option<UnixStream>,
bwrap_block_child: Option<UnixStream>,
trampoline_source: Option<File>,
inherited_fds: Vec<OwnedFd>,
nonce: Vec<u8>,
executable: Vec<u8>,
arguments: Vec<Vec<u8>>,
child_control_fd: RawFd,
phase: GatePhase,
}
impl LaunchGate {
pub(crate) fn new(executable: &Path, arguments: &[OsString]) -> Result<Self, String> {
Self::new_with_control_fd(executable, arguments, CONTROL_FD)
}
pub(crate) fn new_for_sandbox_stdin(
executable: &Path,
arguments: &[OsString],
) -> Result<Self, String> {
Self::new_for_staged_sandbox(executable, arguments, SANDBOX_CONTROL_FD)
}
fn new_for_staged_sandbox(
executable: &Path,
arguments: &[OsString],
child_control_fd: RawFd,
) -> Result<Self, String> {
let trampoline_source = File::open("/proc/self/exe")
.map_err(|error| format!("打开 command sandbox trampoline source 失败:{error}"))?;
let (bwrap_status, bwrap_status_child) = UnixStream::pair()
.map_err(|error| format!("创建 bwrap status 通道失败:{error}"))?;
let (bwrap_block, bwrap_block_child) = UnixStream::pair()
.map_err(|error| format!("创建 bwrap block 通道失败:{error}"))?;
let mut gate = Self::new_with_control_fd(executable, arguments, child_control_fd)?;
gate.trampoline_source = Some(trampoline_source);
gate.bwrap_status = Some(bwrap_status);
gate.bwrap_status_child = Some(bwrap_status_child);
gate.bwrap_block = Some(bwrap_block);
gate.bwrap_block_child = Some(bwrap_block_child);
Ok(gate)
}
fn new_with_control_fd(
executable: &Path,
arguments: &[OsString],
child_control_fd: RawFd,
) -> Result<Self, String> {
if !executable.is_absolute() {
return Err("command sandbox target executable 必须是绝对路径".to_string());
}
let (control, child_control) = UnixStream::pair()
.map_err(|error| format!("创建 command sandbox 私有控制通道失败:{error}"))?;
Ok(Self {
control,
child_control: Some(child_control),
bwrap_status: None,
bwrap_status_child: None,
bwrap_block: None,
bwrap_block_child: None,
trampoline_source: None,
inherited_fds: Vec::new(),
nonce: random_nonce()?,
executable: executable.as_os_str().as_bytes().to_vec(),
arguments: arguments
.iter()
.map(|argument| argument.as_os_str().as_bytes().to_vec())
.collect(),
child_control_fd,
phase: GatePhase::Prepared,
})
}
/// Installs the private launch descriptors. The caller must invoke
/// `child_created` immediately after a successful outer spawn.
pub(crate) fn install_on_command(&mut self, command: &mut Command) -> Result<(), String> {
if self.phase != GatePhase::Prepared {
return Err("command sandbox launch gate 已配置".to_string());
}
let child_fd = self
.child_control
.as_ref()
.ok_or_else(|| "command sandbox child 控制通道缺失".to_string())?
.as_raw_fd();
let target_fd = self.child_control_fd;
let bwrap_status_fd = self.bwrap_status_child.as_ref().map(AsRawFd::as_raw_fd);
let bwrap_block_fd = self.bwrap_block_child.as_ref().map(AsRawFd::as_raw_fd);
let trampoline_source_fd = self.trampoline_source.as_ref().map(AsRawFd::as_raw_fd);
let sources = [
(Some(child_fd), target_fd),
(bwrap_status_fd, BWRAP_STATUS_FD),
(bwrap_block_fd, BWRAP_BLOCK_FD),
(trampoline_source_fd, TRAMPOLINE_SOURCE_FD),
];
let mut mappings = [(None, 0); 4];
let mut inherited_fds = Vec::new();
for (index, (source, target)) in sources.into_iter().enumerate() {
let Some(source) = source else {
mappings[index] = (None, target);
continue;
};
let inherited = duplicate_fd_high(source)
.map_err(|error| format!("准备 command sandbox inherited fd 失败:{error}"))?;
mappings[index] = (Some(inherited.as_raw_fd()), target);
inherited_fds.push(inherited);
}
self.inherited_fds = inherited_fds;
unsafe {
command.pre_exec(move || install_fixed_fds(mappings));
}
self.phase.apply(GateEvent::ChildConfigured)
}
pub(crate) fn child_created(&mut self) -> Result<(), String> {
self.phase.apply(GateEvent::LauncherSpawned)?;
self.child_control.take();
self.bwrap_status_child.take();
self.bwrap_block_child.take();
self.trampoline_source.take();
self.inherited_fds.clear();
if let Some(status) = self.bwrap_status.as_mut() {
read_bwrap_child_created(status, Duration::from_secs(3)).map_err(|error| {
self.fail(format!("等待 bwrap child-created 失败:{error}"))
})?;
}
self.phase.apply(GateEvent::ChildCreated)?;
if let Some(mut block) = self.bwrap_block.take() {
block
.write_all(&[1])
.and_then(|()| block.flush())
.map_err(|error| self.fail(format!("放行 bwrap block-fd 失败:{error}")))?;
}
write_frame(
&mut self.control,
&ControlFrame::Prepare {
nonce: self.nonce.clone(),
},
)
.map_err(|error| self.fail(format!("发送 command sandbox prepare 失败:{error}")))
}
pub(crate) fn spawn_failed(&mut self) {
self.child_control.take();
self.bwrap_status.take();
self.bwrap_status_child.take();
self.bwrap_block.take();
self.bwrap_block_child.take();
self.trampoline_source.take();
self.inherited_fds.clear();
let _ = self.phase.apply(GateEvent::Fail);
}
pub(crate) fn wait_sandbox_ready(&mut self, timeout: Duration) -> Result<(), String> {
if self.phase != GatePhase::ChildCreated {
return Err("command sandbox 尚未进入 child-created".to_string());
}
let frame = read_frame_with_timeout(&mut self.control, timeout)
.map_err(|error| self.fail(format!("等待 command sandbox ready 失败:{error}")))?;
match frame {
ControlFrame::SandboxReady { nonce } if nonce == self.nonce => {
self.phase.apply(GateEvent::SandboxReady)
}
_ => Err(self.fail("command sandbox ready 帧无效".to_string())),
}
}
/// Must only be called after the Runtime durable commit succeeds.
pub(crate) fn commit_exec(&mut self) -> Result<(), String> {
self.phase.apply(GateEvent::CommitPersisted)?;
write_frame(
&mut self.control,
&ControlFrame::CommitExec {
nonce: self.nonce.clone(),
executable: self.executable.clone(),
arguments: self.arguments.clone(),
},
)
.map_err(|error| self.fail(format!("发送 command sandbox commit 失败:{error}")))
}
pub(crate) fn wait_target_exec(
&mut self,
timeout: Duration,
) -> Result<TargetExecState, String> {
if self.phase != GatePhase::CommitPersisted {
return Err("command sandbox 尚未完成 durable commit".to_string());
}
let frame = read_frame_with_timeout(&mut self.control, timeout)
.map_err(|error| self.fail(format!("等待 target exec 失败:{error}")))?;
match frame {
ControlFrame::ExecEstablished { nonce } if nonce == self.nonce => {
self.phase.apply(GateEvent::ExecEstablished)?;
Ok(TargetExecState::Established)
}
ControlFrame::TargetExecFailed { nonce, errno } if nonce == self.nonce => {
self.phase.apply(GateEvent::TargetExecFailed)?;
Ok(TargetExecState::Failed { errno })
}
_ => Err(self.fail("command sandbox target exec 帧无效".to_string())),
}
}
pub(crate) fn wait_terminal(
&mut self,
timeout: Duration,
) -> Result<TargetTerminalState, String> {
if self.phase != GatePhase::ExecEstablished {
return Err("command sandbox target 尚未 exec-established".to_string());
}
let frame = read_frame_with_timeout(&mut self.control, timeout)
.map_err(|error| self.fail(format!("等待 target terminal 失败:{error}")))?;
let terminal = match frame {
ControlFrame::TargetExited { nonce, code } if nonce == self.nonce => {
TargetTerminalState::Exited { code }
}
ControlFrame::TargetSignaled { nonce, signal } if nonce == self.nonce => {
TargetTerminalState::Signaled { signal }
}
_ => return Err(self.fail("command sandbox target terminal 帧无效".to_string())),
};
self.phase.apply(GateEvent::Terminal)?;
Ok(terminal)
}
pub(crate) fn terminate_target(&mut self) -> Result<(), String> {
if self.phase != GatePhase::ExecEstablished {
return Err("command sandbox target 尚未 exec-established".to_string());
}
write_frame(
&mut self.control,
&ControlFrame::TerminateTarget {
nonce: self.nonce.clone(),
},
)
.map_err(|error| {
self.fail(format!(
"发送 command sandbox target terminate 失败:{error}"
))
})
}
pub(crate) fn abort(&mut self, child: &mut Child) -> Result<(), String> {
child
.kill()
.map_err(|error| format!("终止 command sandbox child 失败:{error}"))?;
child
.wait()
.map_err(|error| format!("回收 command sandbox child 失败:{error}"))?;
let _ = self.control.shutdown(Shutdown::Both);
self.phase.apply(GateEvent::Abort)
}
fn fail(&mut self, message: String) -> String {
let _ = self.phase.apply(GateEvent::Fail);
message
}
}
pub(crate) fn is_trampoline_mode(arguments: &[String]) -> bool {
arguments == [TRAMPOLINE_MODE_ARG]
}
pub(crate) fn is_process_session_trampoline_mode(arguments: &[String]) -> bool {
arguments == [PROCESS_SESSION_TRAMPOLINE_MODE_ARG]
}
pub(crate) fn sandbox_trampoline_arguments() -> Vec<OsString> {
#[cfg(not(test))]
{
vec![OsString::from(TRAMPOLINE_MODE_ARG)]
}
#[cfg(test)]
{
vec![
OsString::from("--exact"),
OsString::from(
"command_sandbox_trampoline::linux::tests::trampoline_child_fixture",
),
OsString::from("--ignored"),
OsString::from("--nocapture"),
]
}
}
pub(crate) fn process_session_trampoline_arguments() -> Vec<OsString> {
#[cfg(not(test))]
{
vec![OsString::from(PROCESS_SESSION_TRAMPOLINE_MODE_ARG)]
}
#[cfg(test)]
{
vec![
OsString::from("--exact"),
OsString::from(
"command_sandbox_trampoline::linux::tests::trampoline_child_fixture",
),
OsString::from("--ignored"),
OsString::from("--nocapture"),
]
}
}
#[cfg(test)]
pub(crate) fn sandbox_trampoline_test_environment() -> (&'static str, &'static str) {
(TEST_FIXTURE_ENV, "stdin")
}
#[cfg(test)]
pub(crate) fn process_session_trampoline_test_environment() -> (&'static str, &'static str) {
(TEST_FIXTURE_ENV, "process-session")
}
pub(crate) fn run_trampoline() -> Result<i32, String> {
run_trampoline_from_fd(SANDBOX_CONTROL_FD, TargetStdinMode::Null)
}
pub(crate) fn run_process_session_trampoline() -> Result<i32, String> {
run_trampoline_from_fd(SANDBOX_CONTROL_FD, TargetStdinMode::DuplicateStdout)
}
fn run_trampoline_from_fd(
control_fd: RawFd,
target_stdin_mode: TargetStdinMode,
) -> Result<i32, String> {
let mut control = unsafe { UnixStream::from_raw_fd(control_fd) };
set_close_on_exec(control.as_raw_fd())
.map_err(|error| format!("保护 command sandbox 控制 fd 失败:{error}"))?;
let nonce = match read_frame(&mut control)
.map_err(|error| format!("读取 command sandbox prepare 失败:{error}"))?
{
ControlFrame::Prepare { nonce } if nonce.len() == NONCE_BYTES => nonce,
_ => return Err("command sandbox prepare 帧无效".to_string()),
};
write_frame(
&mut control,
&ControlFrame::SandboxReady {
nonce: nonce.clone(),
},
)
.map_err(|error| format!("发送 command sandbox ready 失败:{error}"))?;
let (executable, arguments) = match read_frame(&mut control)
.map_err(|error| format!("读取 command sandbox commit 失败:{error}"))?
{
ControlFrame::CommitExec {
nonce: commit_nonce,
executable,
arguments,
} if commit_nonce == nonce => (executable, arguments),
_ => return Err("command sandbox commit 帧无效".to_string()),
};
let executable = PathBuf::from(OsString::from_vec(executable));
if !executable.is_absolute() {
return Err("command sandbox target executable 非绝对路径".to_string());
}
let arguments = arguments
.into_iter()
.map(OsString::from_vec)
.collect::<Vec<_>>();
let target_stdin = match target_stdin_mode {
TargetStdinMode::Null => Stdio::null(),
TargetStdinMode::DuplicateStdout => Stdio::from(
duplicate_pty_stdin_from_stdout()
.map_err(|error| format!("复制 process-session PTY stdin 失败:{error}"))?,
),
};
let mut target_command = Command::new(&executable);
target_command.args(&arguments).stdin(target_stdin);
if target_stdin_mode == TargetStdinMode::DuplicateStdout {
unsafe {
target_command.pre_exec(|| {
let mut blocked = std::mem::zeroed::<libc::sigset_t>();
let mut previous = std::mem::zeroed::<libc::sigset_t>();
if libc::sigemptyset(&mut blocked) != 0
|| libc::sigaddset(&mut blocked, libc::SIGTTOU) != 0
|| libc::sigprocmask(libc::SIG_BLOCK, &blocked, &mut previous) != 0
{
return Err(io::Error::last_os_error());
}
if libc::setpgid(0, 0) != 0 {
let error = io::Error::last_os_error();
libc::sigprocmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut());
return Err(error);
}
if libc::tcsetpgrp(libc::STDOUT_FILENO, libc::getpid()) != 0 {
let error = io::Error::last_os_error();
libc::sigprocmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut());
return Err(error);
}
if libc::sigprocmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut()) != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
}
let mut target = match target_command.spawn() {
Ok(target) => target,
Err(error) => {
write_frame(
&mut control,
&ControlFrame::TargetExecFailed {
nonce,
errno: error.raw_os_error().unwrap_or(0),
},
)
.map_err(|write_error| {
format!("发送 command sandbox target exec 失败帧失败:{write_error}")
})?;
return Ok(126);
}
};
write_frame(
&mut control,
&ControlFrame::ExecEstablished {
nonce: nonce.clone(),
},
)
.map_err(|error| format!("发送 command sandbox exec-established 失败:{error}"))?;
let status = if target_stdin_mode == TargetStdinMode::DuplicateStdout {
let process_group = i32::try_from(target.id())
.map_err(|_| "command sandbox target 进程组身份无效".to_string())?;
let mut target_status = None;
let mut graceful_deadline = None;
loop {
if target_status.is_none() {
target_status = target
.try_wait()
.map_err(|error| format!("检查 command sandbox target 失败:{error}"))?;
}
if let Some(status) = target_status {
let group_alive = unsafe { libc::kill(-process_group, 0) } == 0
|| io::Error::last_os_error().raw_os_error() == Some(libc::EPERM);
if !group_alive {
break status;
}
let deadline = *graceful_deadline.get_or_insert_with(|| {
Instant::now()
+ Duration::from_millis(PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS)
});
if Instant::now() >= deadline {
break status;
}
}
let Some(frame) = read_frame_if_available(&mut control, Duration::from_millis(25))?
else {
continue;
};
match frame {
ControlFrame::TerminateTarget {
nonce: terminate_nonce,
} if terminate_nonce == nonce => {
let result = unsafe { libc::kill(-process_group, libc::SIGTERM) };
if result != 0
&& io::Error::last_os_error().raw_os_error() != Some(libc::ESRCH)
{
return Err(format!(
"发送 command sandbox target SIGTERM 失败:{}",
io::Error::last_os_error()
));
}
graceful_deadline = Some(
Instant::now()
+ Duration::from_millis(PROCESS_SESSION_TARGET_TERMINATE_GRACE_MS),
);
}
_ => return Err("command sandbox target control 帧无效".to_string()),
}
}
} else {
target
.wait()
.map_err(|error| format!("等待 command sandbox target 失败:{error}"))?
};
if let Some(code) = status.code() {
write_frame(&mut control, &ControlFrame::TargetExited { nonce, code })
.map_err(|error| format!("发送 command sandbox target exit 失败:{error}"))?;
Ok(code)
} else {
let signal = status.signal().unwrap_or(0);
write_frame(
&mut control,
&ControlFrame::TargetSignaled { nonce, signal },
)
.map_err(|error| format!("发送 command sandbox target signal 失败:{error}"))?;
Ok(128 + signal)
}
}
fn duplicate_fd_high(source: RawFd) -> io::Result<OwnedFd> {
let duplicated = unsafe { libc::fcntl(source, libc::F_DUPFD_CLOEXEC, 64) };
if duplicated < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(duplicated) })
}
fn duplicate_pty_stdin_from_stdout() -> io::Result<OwnedFd> {
if unsafe { libc::isatty(libc::STDOUT_FILENO) } != 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"process-session stdout 不是 PTY",
));
}
duplicate_fd_high(libc::STDOUT_FILENO)
}
fn install_fixed_fds(mappings: [(Option<RawFd>, RawFd); 4]) -> io::Result<()> {
for (source, target) in mappings {
let Some(source) = source else {
continue;
};
if unsafe { libc::dup2(source, target) } < 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn set_close_on_exec(fd: RawFd) -> io::Result<()> {
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
if flags < 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn random_nonce() -> Result<Vec<u8>, String> {
let mut nonce = vec![0_u8; NONCE_BYTES];
File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(&mut nonce))
.map_err(|error| format!("读取 command sandbox nonce 失败:{error}"))?;
Ok(nonce)
}
fn read_bwrap_child_created(stream: &mut UnixStream, timeout: Duration) -> io::Result<()> {
stream.set_read_timeout(Some(timeout))?;
let result = (|| {
let mut line = Vec::new();
let mut byte = [0_u8; 1];
loop {
let read = stream.read(&mut byte)?;
if read == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"bwrap status 在 child-created 前关闭",
));
}
if byte[0] == b'\n' {
let status = serde_json::from_slice::<serde_json::Value>(&line)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if status
.get("child-pid")
.and_then(serde_json::Value::as_u64)
.is_some_and(|pid| pid > 0)
{
return Ok(());
}
line.clear();
continue;
}
line.push(byte[0]);
if line.len() > CONTROL_FRAME_LIMIT {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"bwrap status 帧超过大小上限",
));
}
}
})();
let reset = stream.set_read_timeout(None);
match (result, reset) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), _) => Err(error),
(Ok(()), Err(error)) => Err(error),
}
}
fn write_frame(stream: &mut UnixStream, frame: &ControlFrame) -> io::Result<()> {
let payload = serde_json::to_vec(frame)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
if payload.is_empty() || payload.len() > CONTROL_FRAME_LIMIT {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"command sandbox 控制帧大小无效",
));
}
stream.write_all(&(payload.len() as u32).to_be_bytes())?;
stream.write_all(&payload)?;
stream.flush()
}
fn read_frame(stream: &mut UnixStream) -> io::Result<ControlFrame> {
let mut length = [0_u8; 4];
stream.read_exact(&mut length)?;
let length = u32::from_be_bytes(length) as usize;
if length == 0 || length > CONTROL_FRAME_LIMIT {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"command sandbox 控制帧大小无效",
));
}
let mut payload = vec![0_u8; length];
stream.read_exact(&mut payload)?;
serde_json::from_slice(&payload)
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
}
fn read_frame_if_available(
stream: &mut UnixStream,
timeout: Duration,
) -> Result<Option<ControlFrame>, String> {
let timeout_ms = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
let mut descriptor = libc::pollfd {
fd: stream.as_raw_fd(),
events: libc::POLLIN | libc::POLLHUP,
revents: 0,
};
let result = unsafe { libc::poll(&mut descriptor, 1, timeout_ms) };
if result < 0 {
return Err(format!(
"等待 command sandbox target control 失败:{}",
io::Error::last_os_error()
));
}
if result == 0 {
return Ok(None);
}
if descriptor.revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
return Err("command sandbox target control 通道失效".to_string());
}
read_frame(stream)
.map(Some)
.map_err(|error| format!("读取 command sandbox target control 失败:{error}"))
}
fn read_frame_with_timeout(
stream: &mut UnixStream,
timeout: Duration,
) -> io::Result<ControlFrame> {
stream.set_read_timeout(Some(timeout))?;
let result = read_frame(stream);
let reset = stream.set_read_timeout(None);
match (result, reset) {
(Ok(frame), Ok(())) => Ok(frame),
(Err(error), _) => Err(error),
(Ok(_), Err(error)) => Err(error),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::{Duration, Instant};
const FIXTURE_TEST: &str =
"command_sandbox_trampoline::linux::tests::trampoline_child_fixture";
#[test]
#[ignore]
fn trampoline_child_fixture() {
let Some(mode) = std::env::var_os(TEST_FIXTURE_ENV) else {
return;
};
let (control_fd, target_stdin_mode) = match mode.to_str() {
Some("stdin") => (SANDBOX_CONTROL_FD, TargetStdinMode::Null),
Some("process-session") => (SANDBOX_CONTROL_FD, TargetStdinMode::DuplicateStdout),
_ => (CONTROL_FD, TargetStdinMode::Null),
};
match run_trampoline_from_fd(control_fd, target_stdin_mode) {
Ok(exit_code) => std::process::exit(exit_code),
Err(_) => std::process::exit(125),
}
}
fn spawn_trampoline(gate: &mut LaunchGate) -> Child {
let mut command = Command::new(std::env::current_exe().expect("current test binary"));
command
.arg("--exact")
.arg(FIXTURE_TEST)
.arg("--ignored")
.arg("--nocapture")
.env(TEST_FIXTURE_ENV, "fd3");
gate.install_on_command(&mut command).expect("install gate");
match command.spawn() {
Ok(child) => {
gate.child_created().expect("mark child-created");
child
}
Err(error) => {
gate.spawn_failed();
panic!("spawn trampoline fixture: {error}");
}
}
}
fn wait_child(child: &mut Child, timeout: Duration) -> std::process::ExitStatus {
let deadline = Instant::now() + timeout;
loop {
match child.try_wait().expect("poll child") {
Some(status) => return status,
None if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
None => {
let _ = child.kill();
let _ = child.wait();
panic!("trampoline fixture timed out");
}
}
}
}
#[test]
fn target_is_blocked_until_commit_and_control_fd_is_not_inherited() {
let directory = tempfile::tempdir().expect("temp directory");
let marker = directory.path().join("target-ran");
let script = format!(
"if [ -e /proc/self/fd/{CONTROL_FD} ]; then exit 90; fi; printf committed > '{}'",
marker.display()
);
let mut gate = LaunchGate::new(
Path::new("/bin/sh"),
&[OsString::from("-c"), OsString::from(script)],
)
.expect("create gate");
let mut child = spawn_trampoline(&mut gate);
gate.wait_sandbox_ready(Duration::from_secs(2))
.expect("sandbox ready");
thread::sleep(Duration::from_millis(120));
assert!(!marker.exists(), "target ran before durable commit");
gate.commit_exec().expect("commit target exec");
assert_eq!(
gate.wait_target_exec(Duration::from_secs(2))
.expect("target exec result"),
TargetExecState::Established
);
assert_eq!(
gate.wait_terminal(Duration::from_secs(2))
.expect("target terminal"),
TargetTerminalState::Exited { code: 0 }
);
assert!(wait_child(&mut child, Duration::from_secs(2)).success());
assert_eq!(
std::fs::read_to_string(marker).expect("target marker"),
"committed"
);
}
#[test]
fn stdin_modes_keep_one_shot_null_and_process_session_pty_input() {
let mut one_shot_gate = LaunchGate::new_with_control_fd(
Path::new("/bin/sh"),
&[
OsString::from("-c"),
OsString::from("test \"$(readlink /proc/self/fd/0)\" = /dev/null"),
],
SANDBOX_CONTROL_FD,
)
.expect("create one-shot gate");
let mut one_shot_command =
Command::new(std::env::current_exe().expect("current test binary"));
one_shot_command
.arg("--exact")
.arg(FIXTURE_TEST)
.arg("--ignored")
.arg("--nocapture")
.env(TEST_FIXTURE_ENV, "stdin")
.stdin(Stdio::null());
one_shot_gate
.install_on_command(&mut one_shot_command)
.expect("install one-shot gate");
let mut one_shot_child = one_shot_command.spawn().expect("spawn one-shot fixture");
one_shot_gate
.child_created()
.expect("mark one-shot child-created");
one_shot_gate
.wait_sandbox_ready(Duration::from_secs(2))
.expect("one-shot sandbox ready");
one_shot_gate.commit_exec().expect("commit one-shot exec");
assert_eq!(
one_shot_gate
.wait_target_exec(Duration::from_secs(2))
.expect("one-shot target exec"),
TargetExecState::Established
);
assert_eq!(
one_shot_gate
.wait_terminal(Duration::from_secs(2))
.expect("one-shot target terminal"),
TargetTerminalState::Exited { code: 0 }
);
assert!(wait_child(&mut one_shot_child, Duration::from_secs(2)).success());
let mut process_session_gate = LaunchGate::new_with_control_fd(
Path::new("/bin/sh"),
&[
OsString::from("-c"),
OsString::from(
"for fd in 3 4 5 6; do test ! -e /proc/self/fd/$fd || exit $((80 + fd)); done; IFS= read -r value; test \"$value\" = PTY_INPUT",
),
],
SANDBOX_CONTROL_FD,
)
.expect("create process-session gate");
let mut pty_master = 0;
let mut pty_slave = 0;
let open_result = unsafe {
libc::openpty(
&mut pty_master,
&mut pty_slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
};
assert_eq!(open_result, 0, "open process-session test PTY");
let mut pty_master = unsafe { File::from_raw_fd(pty_master) };
let pty_slave = unsafe { OwnedFd::from_raw_fd(pty_slave) };
set_close_on_exec(pty_master.as_raw_fd()).expect("protect PTY master");
set_close_on_exec(pty_slave.as_raw_fd()).expect("protect PTY slave");
let mut process_session_command =
Command::new(std::env::current_exe().expect("current test binary"));
process_session_command
.arg("--exact")
.arg(FIXTURE_TEST)
.arg("--ignored")
.arg("--nocapture")
.env(TEST_FIXTURE_ENV, "process-session")
.stdin(Stdio::null())
.stdout(Stdio::from(
duplicate_fd_high(pty_slave.as_raw_fd()).expect("duplicate PTY stdout"),
))
.stderr(Stdio::from(
duplicate_fd_high(pty_slave.as_raw_fd()).expect("duplicate PTY stderr"),
));
unsafe {
process_session_command.pre_exec(|| {
if libc::setsid() < 0 {
return Err(io::Error::last_os_error());
}
if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCSCTTY, 0) < 0 {
return Err(io::Error::last_os_error());
}
Ok(())
});
}
drop(pty_slave);
process_session_gate
.install_on_command(&mut process_session_command)
.expect("install process-session gate");
let mut process_session_child = process_session_command
.spawn()
.expect("spawn process-session fixture");
process_session_gate
.child_created()
.expect("mark process-session child-created");
process_session_gate
.wait_sandbox_ready(Duration::from_secs(2))
.expect("process-session sandbox ready");
process_session_gate
.commit_exec()
.expect("commit process-session exec");
assert_eq!(
process_session_gate
.wait_target_exec(Duration::from_secs(2))
.expect("process-session target exec"),
TargetExecState::Established
);
pty_master
.write_all(b"PTY_INPUT\n")
.expect("write process-session PTY input");
assert_eq!(
process_session_gate
.wait_terminal(Duration::from_secs(2))
.expect("process-session target terminal"),
TargetTerminalState::Exited { code: 0 }
);
assert!(wait_child(&mut process_session_child, Duration::from_secs(2)).success());
}
#[test]
fn abort_before_commit_reaps_child_without_running_target() {
let directory = tempfile::tempdir().expect("temp directory");
let marker = directory.path().join("target-ran");
let script = format!("printf forbidden > '{}'", marker.display());
let mut gate = LaunchGate::new(
Path::new("/bin/sh"),
&[OsString::from("-c"), OsString::from(script)],
)
.expect("create gate");
let mut child = spawn_trampoline(&mut gate);
gate.wait_sandbox_ready(Duration::from_secs(2))
.expect("sandbox ready");
gate.abort(&mut child).expect("abort and reap child");
assert!(!marker.exists(), "target ran before commit");
assert!(child.try_wait().expect("reaped child").is_some());
}
#[test]
fn target_exec_failure_is_distinct_from_sandbox_ready() {
let mut gate =
LaunchGate::new(Path::new("/definitely/missing/genarrative-target"), &[])
.expect("create gate");
let mut child = spawn_trampoline(&mut gate);
gate.wait_sandbox_ready(Duration::from_secs(2))
.expect("sandbox ready");
gate.commit_exec().expect("commit target exec");
assert!(matches!(
gate.wait_target_exec(Duration::from_secs(2))
.expect("target exec result"),
TargetExecState::Failed { errno } if errno != 0
));
assert_eq!(
wait_child(&mut child, Duration::from_secs(2)).code(),
Some(126)
);
}
#[test]
fn control_eof_before_commit_never_runs_target() {
let directory = tempfile::tempdir().expect("temp directory");
let marker = directory.path().join("target-ran");
let script = format!("printf forbidden > '{}'", marker.display());
let mut gate = LaunchGate::new(
Path::new("/bin/sh"),
&[OsString::from("-c"), OsString::from(script)],
)
.expect("create gate");
let mut child = spawn_trampoline(&mut gate);
gate.wait_sandbox_ready(Duration::from_secs(2))
.expect("sandbox ready");
drop(gate);
assert!(!wait_child(&mut child, Duration::from_secs(2)).success());
assert!(!marker.exists(), "target ran after pre-commit EOF");
}
#[test]
fn wrong_commit_nonce_never_runs_target() {
let directory = tempfile::tempdir().expect("temp directory");
let marker = directory.path().join("target-ran");
let script = format!("printf forbidden > '{}'", marker.display());
let mut gate = LaunchGate::new(
Path::new("/bin/sh"),
&[OsString::from("-c"), OsString::from(script)],
)
.expect("create gate");
let mut child = spawn_trampoline(&mut gate);
gate.wait_sandbox_ready(Duration::from_secs(2))
.expect("sandbox ready");
gate.nonce[0] ^= 0xff;
gate.commit_exec().expect("send mismatched commit nonce");
assert!(gate.wait_target_exec(Duration::from_secs(2)).is_err());
assert!(!wait_child(&mut child, Duration::from_secs(2)).success());
assert!(!marker.exists(), "target ran after wrong commit nonce");
}
#[test]
fn gate_state_rejects_duplicate_and_out_of_order_events() {
let mut phase = GatePhase::Prepared;
assert!(phase.apply(GateEvent::SandboxReady).is_err());
phase.apply(GateEvent::ChildConfigured).expect("configured");
assert!(phase.apply(GateEvent::ChildConfigured).is_err());
phase
.apply(GateEvent::LauncherSpawned)
.expect("launcher spawned");
phase.apply(GateEvent::ChildCreated).expect("created");
phase.apply(GateEvent::SandboxReady).expect("ready");
assert!(phase.apply(GateEvent::ExecEstablished).is_err());
}
#[test]
fn staged_sandbox_gate_variants_construct_fd0_control_resources() {
let stdin_gate = LaunchGate::new_for_sandbox_stdin(Path::new("/usr/bin/true"), &[])
.expect("create stdin sandbox gate");
let process_session_gate =
crate::command_sandbox::stage_command_sandbox_launch_for_process_session(
crate::command_sandbox::CommandSandboxLaunch {
executable: PathBuf::from("/usr/bin/bwrap"),
arguments: vec![OsString::from("--"), OsString::from("/usr/bin/true")],
cwd: PathBuf::from("/workspace/project"),
environment: Vec::new(),
metadata: crate::command_sandbox::command_sandbox_platform_metadata(),
},
Path::new("/usr/bin/true"),
&[],
)
.expect("stage process-session sandbox gate")
.gate;
for gate in [stdin_gate, process_session_gate] {
assert_eq!(gate.child_control_fd, SANDBOX_CONTROL_FD);
assert_eq!(gate.phase, GatePhase::Prepared);
assert!(gate.child_control.is_some());
assert!(gate.bwrap_status.is_some());
assert!(gate.bwrap_status_child.is_some());
assert!(gate.bwrap_block.is_some());
assert!(gate.bwrap_block_child.is_some());
assert!(gate.trampoline_source.is_some());
}
assert!(is_process_session_trampoline_mode(&[
PROCESS_SESSION_TRAMPOLINE_MODE_ARG.to_string()
]));
assert!(!is_trampoline_mode(&[
PROCESS_SESSION_TRAMPOLINE_MODE_ARG.to_string()
]));
}
}
}
#[cfg(target_os = "linux")]
pub(crate) use linux::{
is_process_session_trampoline_mode, is_trampoline_mode, process_session_trampoline_arguments,
run_process_session_trampoline, run_trampoline, sandbox_trampoline_arguments, LaunchGate,
TargetExecState, TargetTerminalState, BWRAP_BLOCK_FD, BWRAP_STATUS_FD, TRAMPOLINE_PATH,
TRAMPOLINE_SOURCE_FD,
};
#[cfg(all(test, target_os = "linux"))]
pub(crate) use linux::{
process_session_trampoline_test_environment, sandbox_trampoline_test_environment,
};