use std::{path::PathBuf, process::Child, sync::Mutex, time::Instant};
use crate::{
BoxID,
runtime::layout::BoxFilesystemLayout,
vmm::{InstanceSpec, VmmKind},
};
use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use super::watchdog;
use super::{
VmmController, VmmHandler as VmmHandlerTrait, VmmMetrics,
spawn::{ShimSpawner, SpawnedShim},
};
pub struct ShimHandler {
pid: u32,
#[allow(dead_code)]
box_id: BoxID,
process: Option<Child>,
#[allow(dead_code)]
keepalive: Option<watchdog::Keepalive>,
metrics_sys: Mutex<sysinfo::System>,
}
impl ShimHandler {
pub fn from_spawned(spawned: SpawnedShim, box_id: BoxID) -> Self {
let pid = spawned.child.id();
Self {
pid,
box_id,
process: Some(spawned.child),
keepalive: spawned.keepalive,
metrics_sys: Mutex::new(sysinfo::System::new()),
}
}
pub fn from_pid(pid: u32, box_id: BoxID) -> Self {
Self {
pid,
box_id,
process: None,
keepalive: None,
metrics_sys: Mutex::new(sysinfo::System::new()),
}
}
}
impl VmmHandlerTrait for ShimHandler {
fn pid(&self) -> u32 {
self.pid
}
fn stop(&mut self) -> BoxliteResult<()> {
const GRACEFUL_SHUTDOWN_TIMEOUT_MS: u64 = 2000;
if let Some(mut process) = self.process.take() {
let pid = process.id();
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
let start = std::time::Instant::now();
loop {
match process.try_wait() {
Ok(Some(_)) => {
return Ok(());
}
Ok(None) => {
if start.elapsed().as_millis() > GRACEFUL_SHUTDOWN_TIMEOUT_MS as u128 {
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
Err(_) => {
let _ = process.kill();
let _ = process.wait();
return Ok(());
}
}
}
} else {
unsafe {
libc::kill(self.pid as i32, libc::SIGTERM);
}
let start = std::time::Instant::now();
loop {
let mut status: i32 = 0;
let result = unsafe { libc::waitpid(self.pid as i32, &mut status, libc::WNOHANG) };
if result > 0 {
return Ok(());
}
if result < 0 {
let exists = crate::util::is_process_alive(self.pid);
if !exists {
return Ok(()); }
}
if start.elapsed().as_millis() > GRACEFUL_SHUTDOWN_TIMEOUT_MS as u128 {
unsafe {
libc::kill(self.pid as i32, libc::SIGKILL);
}
return Ok(());
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[allow(unreachable_code)]
Ok(())
}
fn metrics(&self) -> BoxliteResult<VmmMetrics> {
use sysinfo::Pid;
let pid = Pid::from_u32(self.pid);
let mut sys = self
.metrics_sys
.lock()
.map_err(|e| BoxliteError::Internal(format!("metrics_sys lock poisoned: {}", e)))?;
sys.refresh_process(pid);
if let Some(proc_info) = sys.process(pid) {
return Ok(VmmMetrics {
cpu_percent: Some(proc_info.cpu_usage()),
memory_bytes: Some(proc_info.memory()),
disk_bytes: None, });
}
Ok(VmmMetrics::default())
}
fn is_running(&self) -> bool {
crate::util::is_process_alive(self.pid)
}
}
fn emit_redacted_box_config_trace(engine: VmmKind, box_id: &str, config_json: &str) {
tracing::trace!(
engine = ?engine,
box_id = %box_id,
json_bytes = config_json.len(),
"Box configuration prepared (raw config not logged; contains secrets)"
);
}
pub struct ShimController {
binary_path: PathBuf,
engine_type: VmmKind,
box_id: BoxID,
options: crate::runtime::options::BoxOptions,
layout: BoxFilesystemLayout,
}
impl ShimController {
pub fn new(
binary_path: PathBuf,
engine_type: VmmKind,
box_id: BoxID,
options: crate::runtime::options::BoxOptions,
layout: BoxFilesystemLayout,
) -> BoxliteResult<Self> {
if !binary_path.exists() {
return Err(BoxliteError::Engine(format!(
"Box runner binary not found: {}",
binary_path.display()
)));
}
Ok(Self {
binary_path,
engine_type,
box_id,
options,
layout,
})
}
}
#[async_trait::async_trait]
impl VmmController for ShimController {
async fn start(&mut self, config: &InstanceSpec) -> BoxliteResult<Box<dyn VmmHandlerTrait>> {
tracing::debug!(
"Preparing config: entrypoint.executable={}, entrypoint.args={:?}",
config.guest_entrypoint.executable,
config.guest_entrypoint.args
);
let mut env = config.guest_entrypoint.env.clone();
if let Ok(rust_log) = std::env::var("RUST_LOG") {
env.push(("RUST_LOG".to_string(), rust_log.clone()));
}
let mut guest_entrypoint = config.guest_entrypoint.clone();
guest_entrypoint.env = env;
let serializable_config = InstanceSpec {
engine: self.engine_type,
box_id: self.box_id.to_string(),
security: self.options.advanced.security.clone(),
cpus: config.cpus,
memory_mib: config.memory_mib,
fs_shares: config.fs_shares.clone(),
block_devices: config.block_devices.clone(),
guest_entrypoint,
transport: config.transport.clone(),
ready_transport: config.ready_transport.clone(),
guest_rootfs: config.guest_rootfs.clone(),
network_config: config.network_config.clone(), network_backend_endpoint: None, disable_network: config.disable_network,
home_dir: config.home_dir.clone(),
console_output: config.console_output.clone(),
exit_file: config.exit_file.clone(),
detach: config.detach,
};
let config_json = serde_json::to_string(&serializable_config)
.map_err(|e| BoxliteError::Engine(format!("Failed to serialize config: {}", e)))?;
if let boxlite_shared::Transport::Unix { socket_path } = &config.transport
&& socket_path.exists()
{
tracing::warn!("Removing stale Unix socket: {}", socket_path.display());
let _ = std::fs::remove_file(socket_path);
}
tracing::info!(
engine = ?self.engine_type,
transport = ?config.transport,
"Starting Box subprocess"
);
tracing::debug!(binary = %self.binary_path.display(), "Box runner binary");
emit_redacted_box_config_trace(self.engine_type, self.box_id.as_str(), &config_json);
let shim_spawn_start = Instant::now();
let spawner = ShimSpawner::new(
&self.binary_path,
&self.layout,
self.box_id.as_str(),
&self.options,
);
let spawned = spawner.spawn(&config_json, config.detach)?;
let shim_spawn_duration = shim_spawn_start.elapsed();
let pid = spawned.child.id();
tracing::info!(
box_id = %self.box_id,
pid = pid,
shim_spawn_duration_ms = shim_spawn_duration.as_millis(),
"boxlite-shim subprocess spawned"
);
let handler = ShimHandler::from_spawned(spawned, self.box_id.clone());
tracing::info!(
box_id = %self.box_id,
"VM subprocess started successfully"
);
Ok(Box::new(handler))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
struct BufWriter(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for BufWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufWriter {
type Writer = BufWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
#[test]
fn redacted_box_config_trace_does_not_emit_config_json() {
let buf = Arc::new(Mutex::new(Vec::<u8>::new()));
let subscriber = tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
.with_writer(BufWriter(buf.clone()))
.with_ansi(false)
.finish();
let key_sentinel = "PKCS8_PRIVATE_KEY_SENTINEL_DO_NOT_LEAK";
let secret_sentinel = "USER_SECRET_VALUE_SENTINEL_DO_NOT_LEAK";
let config_json = format!(
r#"{{"secrets":[{{"name":"db","value":"{}"}}],"ca_key_pem":"-----BEGIN PRIVATE KEY-----\n{}\n-----END PRIVATE KEY-----"}}"#,
secret_sentinel, key_sentinel
);
let expected_len = config_json.len();
tracing::subscriber::with_default(subscriber, || {
emit_redacted_box_config_trace(VmmKind::Libkrun, "test-box-id", &config_json);
});
let output = String::from_utf8(buf.lock().unwrap().clone()).expect("utf8 trace output");
assert!(
!output.contains(key_sentinel),
"CA private key sentinel leaked into trace output: {output}"
);
assert!(
!output.contains(secret_sentinel),
"user secret sentinel leaked into trace output: {output}"
);
assert!(
output.contains("test-box-id"),
"box_id (non-sensitive) should appear in trace output: {output}"
);
assert!(
output.contains(&format!("json_bytes={expected_len}")),
"json_bytes redacted summary should appear: {output}"
);
}
#[test]
fn no_config_json_in_tracing_sigil_workspace_wide() {
fn walk_rs(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
walk_rs(&p, out);
} else if p.extension().is_some_and(|e| e == "rs") {
out.push(p);
}
}
}
let workspace_src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("src");
let mut files = Vec::new();
walk_rs(&workspace_src, &mut files);
assert!(
!files.is_empty(),
"patrol found zero .rs files under {}",
workspace_src.display()
);
let forbidden = ["%config_json", "?config_json"];
let mut offenders = Vec::new();
for path in &files {
let src = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
let production = match src.find("#[cfg(test)]") {
Some(idx) => &src[..idx],
None => &src,
};
for needle in &forbidden {
if production.contains(needle) {
offenders.push(format!("{}: contains {needle:?}", path.display()));
}
}
}
assert!(
offenders.is_empty(),
"`config_json` reached a `tracing::*` field sigil in production code — \
that string carries secrets and a PKCS8 CA private key. Route the \
log through `emit_redacted_box_config_trace` instead.\n {}",
offenders.join("\n ")
);
}
}