689082901f
新增 PTY 外私有 bridge 与 sandbox ready/commit/exec 协议 升级 process record v3 并收紧恢复、幂等与 final/idle 门禁 完善 target 前台进程组、graceful 后代收束和审计失败处理 补齐跨平台回归测试及 Runtime 文档
1187 lines
48 KiB
Rust
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,
|
|
};
|