use std::{
path::Path,
process::{Child, Stdio},
};
use crate::jailer::{Jail, JailerBuilder};
use crate::runtime::layout::BoxFilesystemLayout;
use crate::runtime::options::BoxOptions;
use crate::util::configure_library_env;
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use super::watchdog;
pub struct SpawnedShim {
pub child: Child,
pub keepalive: Option<watchdog::Keepalive>,
}
pub struct ShimSpawner<'a> {
binary_path: &'a Path,
layout: &'a BoxFilesystemLayout,
box_id: &'a str,
options: &'a BoxOptions,
}
impl<'a> ShimSpawner<'a> {
pub fn new(
binary_path: &'a Path,
layout: &'a BoxFilesystemLayout,
box_id: &'a str,
options: &'a BoxOptions,
) -> Self {
Self {
binary_path,
layout,
box_id,
options,
}
}
pub fn spawn(&self, config_json: &str, detach: bool) -> BoxliteResult<SpawnedShim> {
let (keepalive, child_setup) = if !detach {
let (k, s) = watchdog::create()?;
(Some(k), Some(s))
} else {
(None, None)
};
let mut builder = JailerBuilder::new()
.with_box_id(self.box_id)
.with_layout(self.layout.clone())
.with_security(self.options.advanced.security.clone())
.with_volumes(self.options.volumes.clone())
.with_detach(detach);
if let Some(ref setup) = child_setup {
builder = builder.with_preserved_fd(setup.raw_fd(), watchdog::PIPE_FD);
}
let jail = builder.build()?;
jail.prepare()?;
let no_args: &[String] = &[];
let mut cmd = jail.command(self.binary_path, no_args);
self.configure_env(&mut cmd);
let stderr_file = self.create_stderr_file()?;
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::from(stderr_file));
let mut child = cmd.spawn().map_err(|e| {
let err_msg = format!(
"Failed to spawn VM subprocess at {}: {}",
self.binary_path.display(),
e
);
tracing::error!("{}", err_msg);
BoxliteError::Engine(err_msg)
})?;
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
stdin.write_all(config_json.as_bytes()).map_err(|e| {
BoxliteError::Engine(format!("Failed to write config to shim stdin: {e}"))
})?;
drop(stdin); }
drop(child_setup);
Ok(SpawnedShim { child, keepalive })
}
fn configure_env(&self, cmd: &mut std::process::Command) {
cmd.env("BOXLITE_BOX_ID", self.box_id);
if let Ok(rust_log) = std::env::var("RUST_LOG") {
cmd.env("RUST_LOG", rust_log);
}
if let Ok(rust_backtrace) = std::env::var("RUST_BACKTRACE") {
cmd.env("RUST_BACKTRACE", rust_backtrace);
}
if self.options.advanced.security.jailer_enabled
&& self.options.advanced.security.sandbox_profile.is_none()
{
let tmp_dir = self.layout.tmp_dir();
cmd.env("TMPDIR", &tmp_dir);
cmd.env("TMP", &tmp_dir);
cmd.env("TEMP", &tmp_dir);
}
configure_library_env(cmd, std::ptr::null());
}
fn create_stderr_file(&self) -> BoxliteResult<std::fs::File> {
let stderr_file_path = self.layout.stderr_file_path();
std::fs::File::create(&stderr_file_path).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create stderr file {}: {}",
stderr_file_path.display(),
e
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
#[test]
fn test_build_shim_args() {
use crate::runtime::layout::{BoxFilesystemLayout, FsLayoutConfig};
use std::path::PathBuf;
let layout = BoxFilesystemLayout::new(
PathBuf::from("/tmp/box"),
FsLayoutConfig::without_bind_mount(),
false,
);
let options = BoxOptions::default();
let spawner = ShimSpawner::new(
Path::new("/usr/bin/boxlite-shim"),
&layout,
"test-box",
&options,
);
assert_eq!(spawner.box_id, "test-box");
}
#[test]
fn test_configure_env_sets_box_scoped_temp_dir() {
use crate::runtime::advanced_options::{AdvancedBoxOptions, SecurityOptions};
use crate::runtime::layout::{BoxFilesystemLayout, FsLayoutConfig};
use std::path::PathBuf;
let layout = BoxFilesystemLayout::new(
PathBuf::from("/tmp/box"),
FsLayoutConfig::without_bind_mount(),
false,
);
let options = BoxOptions {
advanced: AdvancedBoxOptions {
security: SecurityOptions {
jailer_enabled: true,
..SecurityOptions::default()
},
..AdvancedBoxOptions::default()
},
..BoxOptions::default()
};
let spawner = ShimSpawner::new(
Path::new("/usr/bin/boxlite-shim"),
&layout,
"test-box",
&options,
);
let mut cmd = std::process::Command::new("/usr/bin/true");
spawner.configure_env(&mut cmd);
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
let expected = layout.tmp_dir();
assert_eq!(
envs.get(OsStr::new("BOXLITE_BOX_ID")).and_then(|v| *v),
Some(OsStr::new("test-box"))
);
assert_eq!(
envs.get(OsStr::new("TMPDIR")).and_then(|v| *v),
Some(expected.as_os_str())
);
assert_eq!(
envs.get(OsStr::new("TMP")).and_then(|v| *v),
Some(expected.as_os_str())
);
assert_eq!(
envs.get(OsStr::new("TEMP")).and_then(|v| *v),
Some(expected.as_os_str())
);
}
#[test]
fn test_configure_env_does_not_override_temp_for_custom_profile() {
use crate::runtime::advanced_options::{AdvancedBoxOptions, SecurityOptions};
use crate::runtime::layout::{BoxFilesystemLayout, FsLayoutConfig};
use std::path::PathBuf;
let layout = BoxFilesystemLayout::new(
PathBuf::from("/tmp/box"),
FsLayoutConfig::without_bind_mount(),
false,
);
let options = BoxOptions {
advanced: AdvancedBoxOptions {
security: SecurityOptions {
jailer_enabled: true,
sandbox_profile: Some(PathBuf::from("/tmp/custom.sbpl")),
..SecurityOptions::default()
},
..AdvancedBoxOptions::default()
},
..BoxOptions::default()
};
let spawner = ShimSpawner::new(
Path::new("/usr/bin/boxlite-shim"),
&layout,
"test-box",
&options,
);
let mut cmd = std::process::Command::new("/usr/bin/true");
spawner.configure_env(&mut cmd);
let envs: std::collections::HashMap<_, _> = cmd.get_envs().collect();
assert!(!envs.contains_key(OsStr::new("TMPDIR")));
assert!(!envs.contains_key(OsStr::new("TMP")));
assert!(!envs.contains_key(OsStr::new("TEMP")));
}
#[cfg(unix)]
#[test]
fn shim_spawner_detached_creates_new_session() {
use crate::runtime::advanced_options::SecurityOptions;
use crate::runtime::layout::{BoxFilesystemLayout, FsLayoutConfig};
use std::time::Duration;
use tempfile::TempDir;
let parent_sid = unsafe { libc::getsid(0) };
let tmp = TempDir::new_in("/tmp").expect("tempdir");
let box_dir = tmp.path().join("box");
std::fs::create_dir_all(&box_dir).expect("mkdir box");
let layout = BoxFilesystemLayout::new(box_dir, FsLayoutConfig::without_bind_mount(), false);
let mut options = BoxOptions::default();
options.advanced.security = SecurityOptions::development();
let spawner = ShimSpawner::new(
std::path::Path::new("/usr/bin/yes"),
&layout,
"shimspawnertest",
&options,
);
let spawned = spawner.spawn("", true).expect("spawn detached");
let pid = spawned.child.id();
std::thread::sleep(Duration::from_millis(100));
let child_sid = unsafe { libc::getsid(pid as i32) };
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
libc::waitpid(pid as i32, std::ptr::null_mut(), 0);
}
assert_eq!(
child_sid, pid as i32,
"detached ShimSpawner::spawn must produce a session-leader child. \
Got sid={child_sid}, expected {pid}. parent_sid={parent_sid}. \
Without setsid, a SIGHUP to the parent's controlling terminal \
would cascade into the detached shim."
);
assert_ne!(
child_sid, parent_sid,
"shim's session id must differ from parent's"
);
}
#[cfg(unix)]
#[test]
fn shim_spawner_non_detached_creates_new_pgroup() {
use crate::runtime::advanced_options::SecurityOptions;
use crate::runtime::layout::{BoxFilesystemLayout, FsLayoutConfig};
use std::time::Duration;
use tempfile::TempDir;
let parent_pgid = unsafe { libc::getpgid(0) };
let tmp = TempDir::new_in("/tmp").expect("tempdir");
let box_dir = tmp.path().join("box");
std::fs::create_dir_all(&box_dir).expect("mkdir box");
let layout = BoxFilesystemLayout::new(box_dir, FsLayoutConfig::without_bind_mount(), false);
let mut options = BoxOptions::default();
options.advanced.security = SecurityOptions::development();
let spawner = ShimSpawner::new(
std::path::Path::new("/usr/bin/yes"),
&layout,
"shimspawnertest",
&options,
);
let spawned = spawner.spawn("", false).expect("spawn non-detached");
let pid = spawned.child.id();
std::thread::sleep(Duration::from_millis(100));
let child_pgid = unsafe { libc::getpgid(pid as i32) };
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
libc::waitpid(pid as i32, std::ptr::null_mut(), 0);
}
assert_eq!(
child_pgid, pid as i32,
"non-detached ShimSpawner::spawn must produce a pgroup-leader child. \
Got pgid={child_pgid}, expected {pid}. parent_pgid={parent_pgid}. \
Without process_group(0), killpg(shim_pid) would target the \
parent's pgroup."
);
assert_ne!(
child_pgid, parent_pgid,
"shim's pgid must differ from parent's"
);
}
}