use std::io::Write;
use std::num::NonZero;
#[cfg(unix)]
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[cfg(unix)]
use std::sync::OnceLock;
use std::time::Duration;
use microsandbox_db::DbWriteConnection;
use microsandbox_db::entity::run as run_entity;
#[cfg(unix)]
use microsandbox_filesystem::{BindIdentityMap, BindIdentityMapHandle, DynFileSystem};
use microsandbox_filesystem::{
HostPermissions, PassthroughConfig, PassthroughFs, SingleFileFs, StatVirtualization,
};
use microsandbox_metrics::{ActivateSlot, MetricsRegistry, ReleaseMode};
#[cfg(feature = "net")]
use microsandbox_network::{ResolvedNetworkConfig, network::SmoltcpNetwork};
use microsandbox_protocol::{
bootstrap::{BootstrapBlockRoot, GuestBootstrap},
codec,
message::{Message, MessageType},
};
use microsandbox_types::CpuPlacement;
#[cfg(feature = "net")]
use microsandbox_types::DeploymentProfile;
#[cfg(windows)]
use microsandbox_vsock::WindowsNamedPipePortBackend;
#[cfg(unix)]
use microsandbox_vsock::{UnixDatagramPortBackend, UnixStreamPortBackend};
use msb_krun::VmBuilder;
use sea_orm::{ColumnTrait, EntityTrait, Set};
use serde::Serialize;
#[cfg(windows)]
use crate::bootstrap_fs::AgentBootstrapFs;
#[cfg(windows)]
use crate::console::AgentConsolePipeBridge;
use crate::console::{AgentConsoleBackend, ConsoleSharedState};
use crate::heartbeat::{self, HeartbeatDecision, HeartbeatReader};
use crate::launch::FileMountConfig;
#[cfg(unix)]
pub use crate::launch::LIFECYCLE_LOCK_FD;
pub use crate::launch::{
BRANCH_MEMORY_FD, CONFIG_FD, MetricsSlotHandoff, PARENT_WATCH_DETACH, PARENT_WATCH_FD,
STARTUP_FD, StartupCommand,
};
use crate::logging::LogLevel;
use crate::metrics::run_metrics_sampler;
use crate::relay::{self, AgentRelay};
use crate::{RuntimeError, RuntimeResult};
const EXIT_REASON_COMPLETED: u8 = 0;
const EXIT_REASON_IDLE_TIMEOUT: u8 = 1;
const EXIT_REASON_MAX_DURATION: u8 = 2;
const EXIT_REASON_SIGNAL: u8 = 3;
const EXIT_REASON_PARENT_EXIT: u8 = 4;
const EXIT_REASON_AGENT_UNRESPONSIVE: u8 = 5;
const EXIT_REASON_SHUTDOWN_REQUESTED: u8 = 6;
const EXIT_REASON_STARTUP_COMMAND_FAILED: u8 = 7;
const WRITEBACK_PRESSURE_REFRESH_INTERVAL: Duration = Duration::from_millis(250);
const AGENT_CONTROL_QUEUE_SIZE: u16 = 32;
const AGENT_BULK_QUEUE_SIZE: u16 = 256;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[doc(hidden)]
#[non_exhaustive]
pub enum AgentTransportProfile {
#[default]
Auto,
Combined,
DualPortV1,
}
struct AgentConsoleBackends {
control: AgentConsoleBackend,
bulk: Option<AgentConsoleBackend>,
}
#[derive(Debug)]
pub struct Config {
#[doc(hidden)]
pub agent_transport: AgentTransportProfile,
pub sandbox_name: String,
pub sandbox_id: i32,
pub log_level: Option<LogLevel>,
pub sandbox_db_path: PathBuf,
pub sandbox_db_connect_timeout_secs: u64,
pub log_dir: PathBuf,
pub runtime_dir: PathBuf,
pub sandboxes_dir: PathBuf,
pub run_dir: PathBuf,
pub lifecycle_guard: crate::ipc::SandboxLifecycleGuard,
pub cpu_lease_dir: PathBuf,
pub writeback_lease_dir: PathBuf,
pub block_writeback_pool_bytes: Option<u64>,
pub agent_sock_path: PathBuf,
pub startup_command: Option<StartupCommand>,
#[cfg(unix)]
pub startup_fd: Option<OwnedFd>,
#[cfg(windows)]
pub startup_pipe: Option<String>,
#[cfg(unix)]
pub parent_watchdog: Option<OwnedFd>,
pub forward_output: bool,
pub idle_timeout_secs: Option<u64>,
pub max_duration_secs: Option<u64>,
pub metrics_sample_interval_ms: Option<NonZero<u64>>,
pub metrics_slot: Option<MetricsSlotHandoff>,
pub vm: VmConfig,
}
#[cfg(unix)]
#[derive(Debug, Eq, PartialEq)]
enum ParentWatchdogSignal {
ParentExited,
Detached,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UpperLayerSpec {
pub path: PathBuf,
pub format: msb_krun::DiskImageFormat,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct UpperSpec {
pub layers: Vec<UpperLayerSpec>,
pub read_only: bool,
}
#[derive(Debug, Clone)]
pub struct DiskMountSpec {
pub id: String,
pub host: PathBuf,
pub layers: Vec<UpperLayerSpec>,
pub guest: String,
pub format: msb_krun::DiskImageFormat,
pub fstype: Option<String>,
pub readonly: bool,
pub snapshot_owned: bool,
pub lifecycle_owned: bool,
}
pub struct VmConfig {
pub libkrunfw_path: PathBuf,
pub thp: microsandbox_types::TransparentHugePagePolicy,
pub memory_cache_dir: Option<PathBuf>,
pub vcpus: u8,
pub memory_mib: u32,
pub max_cpus: u8,
pub max_memory_mib: u32,
pub cpu_placement: CpuPlacement,
pub placement_profile_name: Option<String>,
pub placement_profile: Option<microsandbox_types::PlacementProfile>,
pub block_writeback_limit_bytes: Option<u64>,
pub rootfs_path: Option<PathBuf>,
pub rootfs_follow_root_symlinks: bool,
pub rootfs_disk: Option<PathBuf>,
pub rootfs_disk_format: Option<String>,
pub rootfs_disk_readonly: bool,
pub rootfs_disk_spec: Option<UpperSpec>,
pub rootfs_disk_runtime_owned: bool,
pub rootfs_vmdk: Option<PathBuf>,
pub rootfs_upper: Option<PathBuf>,
pub rootfs_upper_spec: Option<UpperSpec>,
pub mounts: Vec<String>,
pub owned_volumes: Vec<microsandbox_types::VolumeMount>,
pub file_mounts: Vec<FileMountConfig>,
pub disks: Vec<DiskMountSpec>,
pub vsock: Vec<microsandbox_types::VsockRouteSpec>,
#[cfg(unix)]
pub backends: Vec<(String, Box<dyn DynFileSystem + Send + Sync>)>,
pub init_path: Option<PathBuf>,
pub bootstrap: GuestBootstrap,
pub exec_path: Option<PathBuf>,
pub exec_args: Vec<String>,
#[cfg(feature = "net")]
pub network: ResolvedNetworkConfig,
#[cfg(feature = "net")]
pub deployment_profile: DeploymentProfile,
#[cfg(feature = "net")]
pub sandbox_slot: u16,
pub checkpoint_restore: Option<crate::launch::CheckpointRestoreConfig>,
}
#[derive(Debug, Serialize)]
struct StartupInfo {
pid: u32,
startup_events: bool,
}
struct BindIdentityMapRegistration {
#[cfg(unix)]
handle: Option<BindIdentityMapHandle>,
#[cfg(unix)]
mount_count: usize,
}
#[cfg(feature = "net")]
struct KrunNetworkRateLimiters {
rx: Option<msb_krun::RateLimiterConfig>,
tx: Option<msb_krun::RateLimiterConfig>,
}
#[cfg(feature = "net")]
type NetworkTerminationHandle = microsandbox_network::network::TerminationHandle;
#[cfg(not(feature = "net"))]
type NetworkTerminationHandle = ();
#[cfg(feature = "net")]
type NetworkMetricsHandle = microsandbox_network::network::MetricsHandle;
#[cfg(not(feature = "net"))]
type NetworkMetricsHandle = ();
#[cfg(feature = "net")]
type NetworkSecretsHandle = microsandbox_network::secrets::handle::SecretsHandle;
#[cfg(not(feature = "net"))]
type NetworkSecretsHandle = ();
#[cfg(feature = "net")]
type NetworkActivationHandle = microsandbox_network::network::NetworkActivationHandle;
#[cfg(not(feature = "net"))]
type NetworkActivationHandle = ();
type VmBuildOutput = (
msb_krun::Vm,
Option<NetworkTerminationHandle>,
Option<NetworkMetricsHandle>,
Option<NetworkSecretsHandle>,
Option<NetworkActivationHandle>,
Option<Vec<u8>>,
GuestBootstrap,
BindIdentityMapRegistration,
Option<crate::checkpoint::RestoredAgentState>,
std::collections::BTreeMap<String, microsandbox_filesystem::OwnedDirectoryCheckpoint>,
);
struct RestoreEndpointPublication {
agent_sock_path: PathBuf,
run_dir: PathBuf,
sandbox_name: String,
control: super::control::ControlContext,
}
impl AgentTransportProfile {
fn offers_dual_port(self) -> bool {
matches!(self, Self::Auto | Self::DualPortV1)
}
}
impl BindIdentityMapRegistration {
fn new() -> Self {
Self {
#[cfg(unix)]
handle: None,
#[cfg(unix)]
mount_count: 0,
}
}
}
impl RestoreEndpointPublication {
fn publish(self, relay: &mut AgentRelay) -> RuntimeResult<()> {
relay.bind_public_endpoint()?;
#[cfg(unix)]
if let Err(error) = crate::ipc::publish_legacy_agent_link(
&self.run_dir,
&self.sandbox_name,
&self.agent_sock_path,
) {
let _ =
crate::ipc::remove_canonical_socket_artifacts(&self.run_dir, &self.sandbox_name);
return Err(error.into());
}
if let Err(error) = publish_control_endpoint(
crate::control::control_socket_path_for(&self.agent_sock_path),
self.control,
&self.run_dir,
&self.sandbox_name,
) {
let _ =
crate::ipc::remove_canonical_socket_artifacts(&self.run_dir, &self.sandbox_name);
return Err(error);
}
Ok(())
}
}
impl std::fmt::Debug for VmConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug = f.debug_struct("VmConfig");
debug
.field("libkrunfw_path", &self.libkrunfw_path)
.field("thp", &self.thp)
.field("vcpus", &self.vcpus)
.field("memory_mib", &self.memory_mib)
.field("max_cpus", &self.max_cpus)
.field("max_memory_mib", &self.max_memory_mib)
.field("placement_profile_name", &self.placement_profile_name)
.field(
"block_writeback_limit_bytes",
&self.block_writeback_limit_bytes,
)
.field("rootfs_path", &self.rootfs_path)
.field("rootfs_vmdk", &self.rootfs_vmdk)
.field("rootfs_upper", &self.rootfs_upper)
.field("rootfs_upper_spec", &self.rootfs_upper_spec)
.field("rootfs_disk", &self.rootfs_disk)
.field("rootfs_disk_format", &self.rootfs_disk_format)
.field("rootfs_disk_readonly", &self.rootfs_disk_readonly)
.field("rootfs_disk_spec", &self.rootfs_disk_spec)
.field("rootfs_disk_runtime_owned", &self.rootfs_disk_runtime_owned)
.field("mounts", &self.mounts)
.field("disks", &self.disks);
#[cfg(unix)]
debug.field("backends", &format!("[{} backend(s)]", self.backends.len()));
debug
.field("init_path", &self.init_path)
.field("bootstrap", &self.bootstrap)
.field("exec_path", &self.exec_path)
.field("exec_args", &self.exec_args)
.field("checkpoint_restore", &self.checkpoint_restore)
.finish()
}
}
pub fn enter(config: Config) -> ! {
let log_dir = config.log_dir.clone();
let metrics_slot = config.metrics_slot.clone();
let failure_channel = super::progress::StartupFailureChannel::default();
let result = run(config, &failure_channel);
match result {
Ok(infallible) => match infallible {},
Err(e) => {
release_reserved_metrics_slot(metrics_slot.as_ref());
let boot_err = crate::boot_error::BootError::from_runtime_error(&e);
if let Err(write_err) = boot_err.write_atomic(&log_dir) {
eprintln!("failed to write boot-error.json: {write_err}");
}
eprintln!("sandbox error: {e}");
failure_channel.release();
std::process::exit(1);
}
}
}
fn run(
mut config: Config,
failure_channel: &super::progress::StartupFailureChannel,
) -> RuntimeResult<std::convert::Infallible> {
#[cfg(unix)]
raise_nofile_limit();
let pid = std::process::id();
#[cfg(unix)]
let startup_events = config.startup_fd.is_some();
#[cfg(windows)]
let startup_events = config.startup_pipe.is_some();
let startup = StartupInfo {
pid,
startup_events,
};
let startup_json = serde_json::to_string(&startup)
.map_err(|e| RuntimeError::Custom(format!("serialize startup: {e}")))?;
#[cfg(unix)]
let startup_writer = write_startup_info(config.startup_fd.as_ref(), &startup_json)?;
#[cfg(unix)]
drop(config.startup_fd.take()); #[cfg(windows)]
let startup_writer = write_startup_info(config.startup_pipe.as_deref(), &startup_json)?;
let startup_writer = startup_writer
.map(|writer| failure_channel.retain(writer))
.transpose()?;
setup_log_capture(&config.log_dir, config.forward_output)?;
tracing::info!(sandbox = %config.sandbox_name, "sandbox starting");
let shutdown_flush_timeout = guest_shutdown_flush_timeout(config.vm.init_path.is_some());
let shared = Arc::new(ConsoleSharedState::new());
let bulk_shared = config
.agent_transport
.offers_dual_port()
.then(|| Arc::new(ConsoleSharedState::new()));
let console_backends = AgentConsoleBackends {
control: AgentConsoleBackend::new(Arc::clone(&shared)),
bulk: bulk_shared
.as_ref()
.map(|shared| AgentConsoleBackend::new(Arc::clone(shared))),
};
let tokio_rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.map_err(|e| RuntimeError::Custom(format!("tokio runtime: {e}")))?;
let startup_progress: crate::startup_progress::StartupProgressCallback = match startup_writer {
Some(writer) => super::progress::start(
writer,
&tokio_rt,
if config.vm.checkpoint_restore.is_some() {
crate::startup_progress::StartupPhase::PreparingSnapshot
} else {
crate::startup_progress::StartupPhase::Activating
},
failure_channel.clone(),
),
None => Arc::new(|_| {}),
};
std::fs::create_dir_all(&config.runtime_dir)?;
std::fs::create_dir_all(config.runtime_dir.join("scripts"))?;
crate::checkpoint::recover_runtime_owned_root(&config.runtime_dir, &mut config.vm).map_err(
|error| RuntimeError::Custom(format!("recover runtime-owned root disk: {error}")),
)?;
recover_owned_disk_layers(&config.runtime_dir, &mut config.vm.disks)?;
heartbeat::clear_stale(&config.runtime_dir)?;
if config.vm.checkpoint_restore.is_some() {
prepare_runtime_restore_namespace(&config.runtime_dir, config.vm.rootfs_vmdk.is_some())?;
}
#[cfg(unix)]
crate::ipc::prepare_canonical_socket_dir(
&config.run_dir,
&config.sandbox_name,
&config.agent_sock_path,
)?;
let (mut relay, db, run_db_id) = tokio_rt.block_on(async {
let relay = async {
if config.vm.checkpoint_restore.is_some() {
Ok(AgentRelay::new_deferred(
&config.agent_sock_path,
Arc::clone(&shared),
bulk_shared.as_ref().map(Arc::clone),
))
} else {
AgentRelay::new_with_bulk(
&config.agent_sock_path,
Arc::clone(&shared),
bulk_shared.as_ref().map(Arc::clone),
)
.await
}
};
let db = connect_db(
&config.sandbox_db_path,
config.sandbox_db_connect_timeout_secs,
);
let (relay, db) = tokio::try_join!(relay, db)?;
let run_db_id = insert_run(&db, config.sandbox_id, pid).await?;
Ok::<_, RuntimeError>((relay, db, run_db_id))
})?;
let writeback_disk_paths = match writeback_limited_disk_paths(&config.vm) {
Ok(disk_paths) => disk_paths,
Err(error) => {
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
return Err(error);
}
};
let cpu_guard = match tokio_rt.block_on(crate::cpu::acquire(
&db,
run_db_id,
&config.cpu_lease_dir,
crate::cpu::PlacementRequest {
policy: config.vm.cpu_placement,
max_vcpus: config.vm.max_cpus.max(config.vm.vcpus),
boot_memory_mib: config.vm.memory_mib,
max_memory_mib: config.vm.max_memory_mib.max(config.vm.memory_mib),
profile: config.vm.placement_profile,
},
)) {
Ok(guard) => Arc::new(guard),
Err(error) => {
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
return Err(error);
}
};
let writeback_guard = match tokio_rt.block_on(crate::writeback::acquire(
&db,
run_db_id,
&config.writeback_lease_dir,
config.block_writeback_pool_bytes,
config.vm.block_writeback_limit_bytes,
&writeback_disk_paths,
)) {
Ok(guard) => guard,
Err(error) => {
if let Err(release_error) = tokio_rt.block_on(cpu_guard.release(&db)) {
tracing::warn!(%release_error, "release CPU placement after writeback pressure setup failure");
}
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
return Err(error);
}
};
let writeback_guard = Arc::new(writeback_guard);
let writeback_limit = writeback_guard.limit();
if writeback_guard.is_managed() {
let pressure_guard = Arc::clone(&writeback_guard);
let pressure_db = db.clone();
tokio_rt.spawn(async move {
monitor_writeback_pressure(pressure_guard, pressure_db).await;
});
}
#[cfg(unix)]
if config.vm.checkpoint_restore.is_none()
&& let Err(error) = crate::ipc::publish_legacy_agent_link(
&config.run_dir,
&config.sandbox_name,
&config.agent_sock_path,
)
{
if let Err(release_error) = tokio_rt.block_on(writeback_guard.release(&db)) {
tracing::warn!(%release_error, "release writeback admission after legacy endpoint publication failure");
}
if let Err(release_error) = tokio_rt.block_on(cpu_guard.release(&db)) {
tracing::warn!(%release_error, "release CPU placement after legacy endpoint publication failure");
}
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
let _ =
crate::ipc::remove_canonical_socket_artifacts(&config.run_dir, &config.sandbox_name);
return Err(error.into());
}
let exec_log_writer: Option<Arc<crate::exec_log::LogWriter>> =
match crate::exec_log::LogWriter::open(&config.log_dir) {
Ok(writer) => {
let arc = Arc::new(writer);
relay = relay.with_log_writer(Arc::clone(&arc));
Some(arc)
}
Err(err) => {
tracing::warn!(error = %err, "exec_log: open failed, capture disabled");
None
}
};
let exit_reason: Arc<std::sync::atomic::AtomicU8> =
Arc::new(std::sync::atomic::AtomicU8::new(EXIT_REASON_COMPLETED));
let metrics_writer = activate_metrics_writer(
config.metrics_slot.as_ref(),
config.metrics_sample_interval_ms,
run_db_id,
pid,
);
if metrics_writer.is_none()
&& config.metrics_slot.is_some()
&& config.metrics_sample_interval_ms.is_some()
{
release_reserved_metrics_slot(config.metrics_slot.as_ref());
}
let rt_handle = tokio_rt.handle().clone();
let exit_db = db.clone();
let exit_sandbox_id = config.sandbox_id;
let exit_run_id = run_db_id;
let exit_reason_for_observer = Arc::clone(&exit_reason);
let exit_sock_path = config.agent_sock_path.clone();
let exit_run_dir = config.run_dir.clone();
let exit_sandbox_name = config.sandbox_name.clone();
let exit_sandboxes_dir = config.sandboxes_dir.clone();
let exit_log_writer = exec_log_writer.clone();
let exit_metrics_writer = metrics_writer.clone();
let exit_cpu_guard = Arc::clone(&cpu_guard);
let exit_writeback_guard = Arc::clone(&writeback_guard);
let placement_rt_handle = tokio_rt.handle().clone();
let placement_db = db.clone();
let placement_cpu_guard = Arc::clone(&cpu_guard);
let resolved_numa_topology = cpu_guard.numa_topology();
let placement_required = cpu_guard.placement_required();
#[cfg(windows)]
let _agent_console_pipe_bridge = AgentConsolePipeBridge::spawn(
agent_console_pipe_name(config.sandbox_id),
Arc::clone(&shared),
tokio_rt.handle(),
)
.map_err(|e| RuntimeError::Custom(format!("agent console pipe bridge: {e}")))?;
#[cfg(windows)]
let _agent_bulk_console_pipe_bridge = match bulk_shared.as_ref() {
Some(bulk_shared) => Some(
AgentConsolePipeBridge::spawn(
agent_bulk_console_pipe_name(config.sandbox_id),
Arc::clone(bulk_shared),
tokio_rt.handle(),
)
.map_err(|e| RuntimeError::Custom(format!("agent bulk console pipe bridge: {e}")))?,
),
None => None,
};
let host_placement = HostPlacement {
vcpu_targets: cpu_guard.vcpu_targets(),
required: placement_required,
numa_topology: resolved_numa_topology,
};
let build_result = build_vm(
&config,
console_backends,
move |exit_code: i32| {
use microsandbox_db::entity::sandbox as sandbox_entity;
use sea_orm::QueryFilter;
use sea_orm::sea_query::Expr;
let reason_tag = exit_reason_for_observer.load(std::sync::atomic::Ordering::SeqCst);
let reason = match reason_tag {
EXIT_REASON_IDLE_TIMEOUT => run_entity::TerminationReason::IdleTimeout,
EXIT_REASON_AGENT_UNRESPONSIVE => run_entity::TerminationReason::AgentUnresponsive,
EXIT_REASON_SHUTDOWN_REQUESTED => run_entity::TerminationReason::ShutdownRequested,
EXIT_REASON_STARTUP_COMMAND_FAILED => run_entity::TerminationReason::Failed,
EXIT_REASON_MAX_DURATION => run_entity::TerminationReason::MaxDurationExceeded,
EXIT_REASON_PARENT_EXIT => run_entity::TerminationReason::Signal,
EXIT_REASON_SIGNAL => run_entity::TerminationReason::Signal,
_ if exit_code == 0 => run_entity::TerminationReason::Completed,
_ => run_entity::TerminationReason::Failed,
};
rt_handle.block_on(async {
let now = chrono::Utc::now().naive_utc();
if let Err(error) = exit_writeback_guard.release(&exit_db).await {
tracing::warn!(%error, "release writeback pressure membership at VM exit");
}
if let Err(error) = exit_cpu_guard.release(&exit_db).await {
tracing::warn!(%error, "release CPU placement at VM exit");
}
let bound_result = crate::ipc::remove_socket_pair(&exit_sock_path);
let owned_result =
crate::ipc::remove_sandbox_socket_artifacts(&exit_run_dir, &exit_sandbox_name);
if let Err(error) = bound_result.and(owned_result) {
tracing::warn!(
sandbox = %exit_sandbox_name,
error = %error,
"runtime exit socket cleanup failed; leaving lifecycle active for reaping"
);
return;
}
let _ = run_entity::Entity::update_many()
.col_expr(
run_entity::Column::Status,
Expr::value(run_entity::RunStatus::Terminated),
)
.col_expr(run_entity::Column::TerminationReason, Expr::value(reason))
.col_expr(run_entity::Column::ExitCode, Expr::value(exit_code))
.col_expr(run_entity::Column::TerminatedAt, Expr::value(now))
.filter(run_entity::Column::Id.eq(exit_run_id))
.exec(&exit_db)
.await;
match crate::maintenance::terminal_sandbox_update(&exit_db).await {
Ok(update) => {
let _ = update
.col_expr(
sandbox_entity::Column::Status,
Expr::value(sandbox_entity::SandboxStatus::Stopped),
)
.col_expr(sandbox_entity::Column::UpdatedAt, Expr::value(now))
.filter(sandbox_entity::Column::Id.eq(exit_sandbox_id))
.exec(&exit_db)
.await;
}
Err(error) => tracing::warn!(%error, "terminal sandbox update failed"),
}
match crate::maintenance::cleanup_terminal_ephemeral_sandbox_owned(
&exit_db,
&exit_sandboxes_dir,
&exit_run_dir,
exit_sandbox_id,
)
.await
{
Ok(outcome) => {
tracing::debug!(?outcome, "ephemeral exit self-clean")
}
Err(err) => {
tracing::warn!(error = %err, "ephemeral exit self-clean failed")
}
}
});
if let Some(ref writer) = exit_log_writer {
writer.write_system("--- sandbox stopped ---");
}
if let Some(ref writer) = exit_metrics_writer
&& let Err(err) = writer.clone().release(ReleaseMode::Stale)
{
tracing::debug!(error = %err, slot = writer.slot(), "metrics slot release at exit");
}
},
move |report: &msb_krun::PlacementReport| {
let pinned = report
.vcpus
.iter()
.filter(|result| matches!(result, msb_krun::VcpuPlacementResult::Pinned { .. }))
.count();
if let Err(error) =
placement_rt_handle.block_on(placement_cpu_guard.reconcile(&placement_db, report))
{
tracing::warn!(%error, "record effective host placement");
}
tracing::info!(
pinned_vcpus = pinned,
inherited_vcpus = report.vcpus.len().saturating_sub(pinned),
memory = ?report.memory,
"host placement acknowledged before guest execution"
);
},
VmBuildRuntime {
tokio_handle: tokio_rt.handle().clone(),
startup_progress: startup_progress.clone(),
},
host_placement,
writeback_limit.as_ref(),
);
let (
vm,
_network_termination_handle,
network_metrics_handle,
_network_secrets_handle,
network_activation_handle,
bootstrap_frame,
resolved_bootstrap,
bind_identity_map,
mut restored_agent,
owned_directory_checkpoints,
) = match build_result {
Ok(vm) => vm,
Err(e) => {
if let Err(error) = tokio_rt.block_on(writeback_guard.release(&db)) {
tracing::warn!(%error, "release writeback pressure membership after VM build failure");
}
if let Err(error) = tokio_rt.block_on(cpu_guard.release(&db)) {
tracing::warn!(%error, "release CPU placement after VM build failure");
}
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
if let Some(writer) = metrics_writer.clone() {
let _ = writer.release(ReleaseMode::Free);
} else {
release_reserved_metrics_slot(config.metrics_slot.as_ref());
}
let _ = crate::ipc::remove_socket_pair(&config.agent_sock_path);
let _ =
crate::ipc::remove_sandbox_socket_artifacts(&config.run_dir, &config.sandbox_name);
return Err(e);
}
};
if restored_agent.is_none() {
startup_progress(crate::startup_progress::StartupProgress::phase(
crate::startup_progress::StartupPhase::Activating,
));
}
if let Some(bootstrap_frame) = bootstrap_frame {
relay::push_guest_frame_blocking(&shared, bootstrap_frame)?;
}
#[cfg(unix)]
{
relay =
relay.with_bind_identity_map(bind_identity_map.handle, bind_identity_map.mount_count);
}
#[cfg(windows)]
{
let _ = bind_identity_map;
}
let krun_metrics_handle = vm.metrics_handle();
let exit_handle = vm.exit_handle();
let upper_host_path = oci_upper_host_path(&config.vm);
let restore_endpoint_publication = {
let control = vm.control_handle();
#[cfg(feature = "net")]
let secrets = _network_secrets_handle.clone();
#[cfg(not(feature = "net"))]
let secrets: Option<()> = None;
let control_sock_path = crate::control::control_socket_path_for(&config.agent_sock_path);
let executor = super::control::RuntimeControlExecutor::new(
control,
#[cfg(feature = "net")]
secrets,
&config.runtime_dir,
&config.vm,
&resolved_bootstrap,
tokio_rt.handle().clone(),
&config.agent_sock_path,
Arc::clone(&shared.workload_control),
Arc::clone(&shared.resident_paused),
restored_agent
.as_mut()
.and_then(|agent| agent.inherited_memory.take()),
owned_directory_checkpoints,
);
let context = super::control::ControlContext {
executor: match executor {
Ok(executor) => Arc::new(executor),
Err(error) => {
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
return Err(RuntimeError::Custom(format!(
"publish runtime control identity: {error}"
)));
}
},
};
if restored_agent.is_some() {
Some(RestoreEndpointPublication {
agent_sock_path: config.agent_sock_path.clone(),
run_dir: config.run_dir.clone(),
sandbox_name: config.sandbox_name.clone(),
control: context,
})
} else {
if let Err(error) = publish_control_endpoint(
control_sock_path,
context,
&config.run_dir,
&config.sandbox_name,
) {
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
let _ = crate::ipc::remove_canonical_socket_artifacts(
&config.run_dir,
&config.sandbox_name,
);
return Err(error);
}
None
}
};
#[cfg(unix)]
{
if let Some(parent_watchdog) = config.parent_watchdog
&& let Err(e) = spawn_parent_watchdog(
parent_watchdog,
Arc::clone(&shared),
Arc::clone(&exit_reason),
exit_handle.clone(),
config.sandbox_name.clone(),
shutdown_flush_timeout,
)
{
let _ = tokio_rt.block_on(mark_run_failed(&db, run_db_id));
if let Some(writer) = metrics_writer.clone() {
let _ = writer.release(ReleaseMode::Free);
} else {
release_reserved_metrics_slot(config.metrics_slot.as_ref());
}
let _ = crate::ipc::remove_socket_pair(&config.agent_sock_path);
let _ =
crate::ipc::remove_sandbox_socket_artifacts(&config.run_dir, &config.sandbox_name);
return Err(e);
}
}
#[cfg(feature = "net")]
if let Some(network_termination_handle) = _network_termination_handle {
let network_exit_handle = exit_handle.clone();
let network_reason = Arc::clone(&exit_reason);
network_termination_handle.set_hook(Arc::new(move || {
tracing::warn!("secret violation requested sandbox termination");
network_reason.store(EXIT_REASON_SIGNAL, std::sync::atomic::Ordering::SeqCst);
network_exit_handle.trigger();
}));
}
let metrics_sampler = match (config.metrics_sample_interval_ms, metrics_writer.clone()) {
(None, _) => {
tracing::debug!(
sandbox = %config.sandbox_name,
"metrics sampling disabled; not spawning sampler"
);
None
}
(Some(_), None) => {
if config.metrics_slot.is_some() {
tracing::warn!(
sandbox = %config.sandbox_name,
"metrics activation failed; slot was released and sampler not spawned"
);
} else {
tracing::warn!(
sandbox = %config.sandbox_name,
"metrics sampling enabled but no slot was reserved by the host; not spawning sampler"
);
}
None
}
(Some(interval_ms), Some(writer)) => Some((
writer,
interval_ms,
krun_metrics_handle,
network_metrics_handle
.map(|handle| Box::new(handle) as Box<dyn crate::metrics::NetworkMetrics>),
upper_host_path,
)),
};
let metrics_sandbox_id = config.sandbox_id;
let metrics_sandbox_name = config.sandbox_name.clone();
let metrics_pid = pid;
let metrics_max_cpus = config.vm.max_cpus.max(config.vm.vcpus);
{
let maintenance_db = db.clone();
let maintenance_dir = config.sandboxes_dir.clone();
let maintenance_run_dir = config.run_dir.clone();
tokio_rt.spawn(async move {
crate::maintenance::run_startup_maintenance(
&maintenance_db,
&maintenance_dir,
&maintenance_run_dir,
)
.await;
});
}
let (_relay_shutdown_tx, relay_shutdown_rx) = tokio::sync::watch::channel(false);
let (relay_drain_tx, mut relay_drain_rx) = tokio::sync::mpsc::channel::<()>(1);
let relay_exit_handle = exit_handle.clone();
let relay_exit_reason = Arc::clone(&exit_reason);
let restore_control = restored_agent.as_ref().map(|_| vm.control_handle());
let restore_runtime_dir = config.runtime_dir.clone();
let relay_boot_log_dir = config.log_dir.clone();
let restore_startup_progress = startup_progress.clone();
tokio_rt.spawn(async move {
let ready_result = tokio::task::spawn_blocking(move || {
if let (Some(restored), Some(control)) =
(restored_agent.as_ref(), restore_control.as_ref())
{
relay.activate_restored(
control,
restored,
&restore_runtime_dir,
&restore_startup_progress,
)?;
} else {
relay.wait_ready()?;
}
Ok::<_, RuntimeError>(relay)
})
.await;
match ready_result {
Ok(Ok(mut relay)) => {
if let Some(publication) = restore_endpoint_publication
&& let Err(error) = publication.publish(&mut relay)
{
tracing::error!(%error, "publish restored runtime endpoints");
super::progress::publish_failure(&relay_boot_log_dir, &error);
relay_exit_reason.store(
EXIT_REASON_AGENT_UNRESPONSIVE,
std::sync::atomic::Ordering::SeqCst,
);
relay_exit_handle.trigger();
return;
}
#[cfg(feature = "net")]
if let Some(network_activation) = network_activation_handle {
network_activation.activate();
}
#[cfg(not(feature = "net"))]
let _ = network_activation_handle;
if let Some((
writer,
interval_ms,
krun_metrics_handle,
network_metrics_handle,
upper_host_path,
)) = metrics_sampler
{
tracing::debug!(
sandbox = %metrics_sandbox_name,
interval_ms = interval_ms.get(),
"starting metrics sampler after agent ready"
);
tokio::spawn(run_metrics_sampler(crate::metrics::MetricsSamplerSpec {
writer,
sandbox_id: metrics_sandbox_id,
pid: metrics_pid,
interval_ms,
max_cpus: metrics_max_cpus,
krun_metrics: krun_metrics_handle,
network_metrics: network_metrics_handle,
upper_host_path,
}));
}
if let Err(e) = relay.run(relay_shutdown_rx, relay_drain_tx).await {
tracing::error!("agent relay error: {e}");
relay_exit_reason.store(
EXIT_REASON_AGENT_UNRESPONSIVE,
std::sync::atomic::Ordering::SeqCst,
);
relay_exit_handle.trigger();
}
}
Ok(Err(e)) => {
tracing::error!("agent relay wait_ready failed: {e}");
super::progress::publish_failure(&relay_boot_log_dir, &e);
relay_exit_reason.store(
EXIT_REASON_AGENT_UNRESPONSIVE,
std::sync::atomic::Ordering::SeqCst,
);
relay_exit_handle.trigger();
}
Err(e) => {
tracing::error!("agent relay wait_ready task panicked: {e}");
super::progress::publish_failure(
&relay_boot_log_dir,
&RuntimeError::Custom(format!("agent readiness task failed: {e}")),
);
relay_exit_reason.store(
EXIT_REASON_AGENT_UNRESPONSIVE,
std::sync::atomic::Ordering::SeqCst,
);
relay_exit_handle.trigger();
}
}
});
{
let shutdown_reason = Arc::clone(&exit_reason);
tokio_rt.spawn(async move {
if relay_drain_rx.recv().await.is_some() {
shutdown_reason.store(
EXIT_REASON_SHUTDOWN_REQUESTED,
std::sync::atomic::Ordering::SeqCst,
);
tracing::info!("graceful shutdown requested; waiting for guest poweroff");
}
});
}
if let Some(startup_command) = config.startup_command.clone() {
let startup_agent_sock_path = config.agent_sock_path.clone();
let startup_shared = Arc::clone(&shared);
let startup_exit_handle = exit_handle.clone();
let startup_reason = Arc::clone(&exit_reason);
let startup_shutdown_flush_timeout = shutdown_flush_timeout;
tokio_rt.spawn(async move {
tracing::info!(
cmd = %startup_command.cmd,
args = ?startup_command.args,
"starting startup command"
);
match crate::startup::run_startup_command(&startup_agent_sock_path, startup_command)
.await
{
Ok(crate::startup::StartupCommandExit::Exited(0)) => {
tracing::info!("startup command exited successfully");
}
Ok(crate::startup::StartupCommandExit::Exited(code)) => {
startup_reason.store(
EXIT_REASON_STARTUP_COMMAND_FAILED,
std::sync::atomic::Ordering::SeqCst,
);
tracing::warn!(code, "startup command exited with non-zero status");
}
Ok(crate::startup::StartupCommandExit::Failed(failed)) => {
startup_reason.store(
EXIT_REASON_STARTUP_COMMAND_FAILED,
std::sync::atomic::Ordering::SeqCst,
);
tracing::warn!(error = %failed.message, "startup command failed to spawn");
}
Err(err) => {
startup_reason.store(
EXIT_REASON_STARTUP_COMMAND_FAILED,
std::sync::atomic::Ordering::SeqCst,
);
tracing::warn!(error = %err, "startup command failed");
}
}
if startup_shared
.resident_paused
.load(std::sync::atomic::Ordering::Acquire)
{
startup_exit_handle.trigger();
return;
}
match request_guest_shutdown_async(&startup_shared).await {
Ok(()) => {
tokio::time::sleep(startup_shutdown_flush_timeout).await;
tracing::info!("startup command shutdown flush window elapsed");
}
Err(err) => {
tracing::warn!(
error = %err,
"startup command shutdown request failed, triggering host exit"
);
}
}
startup_exit_handle.trigger();
});
}
{
let mut heartbeat_reader = HeartbeatReader::new(&config.runtime_dir);
let idle_timeout = config.idle_timeout_secs.map(Duration::from_secs);
let heartbeat_exit_handle = exit_handle.clone();
let heartbeat_reason = Arc::clone(&exit_reason);
let heartbeat_shared = Arc::clone(&shared);
let heartbeat_shutdown_flush_timeout = shutdown_flush_timeout;
tokio_rt.spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
loop {
interval.tick().await;
if heartbeat_shared
.resident_paused
.load(std::sync::atomic::Ordering::Acquire)
{
continue;
}
let decision = heartbeat_reader.check(idle_timeout);
match decision {
HeartbeatDecision::Idle(status) => {
let idle_secs = idle_timeout.map(|timeout| timeout.as_secs()).unwrap_or(0);
tracing::info!(
idle_secs,
heartbeat_seq = ?status.heartbeat_seq,
activity_seq = ?status.activity_seq,
idle_for = ?status.idle_for,
active_exec_sessions = status.active_exec_sessions,
active_fs_streams = status.active_fs_streams,
active_tcp_streams = status.active_tcp_streams,
"sandbox idle, requesting guest shutdown"
);
heartbeat_reason.store(
EXIT_REASON_IDLE_TIMEOUT,
std::sync::atomic::Ordering::SeqCst,
);
match request_guest_shutdown_async(&heartbeat_shared).await {
Ok(()) => {
tokio::time::sleep(heartbeat_shutdown_flush_timeout).await;
tracing::info!(
"idle shutdown flush window elapsed, triggering host exit"
);
}
Err(err) => {
tracing::warn!(
error = %err,
"idle shutdown request failed, triggering host exit"
);
}
}
heartbeat_exit_handle.trigger();
break;
}
HeartbeatDecision::PendingBoot(_) | HeartbeatDecision::Active(_) => {}
}
}
});
}
if let Some(max_secs) = config.max_duration_secs {
let max_exit_handle = exit_handle.clone();
let max_reason = Arc::clone(&exit_reason);
tokio_rt.spawn(async move {
tokio::time::sleep(Duration::from_secs(max_secs)).await;
tracing::info!("max duration {max_secs}s exceeded, triggering exit");
max_reason.store(
EXIT_REASON_MAX_DURATION,
std::sync::atomic::Ordering::SeqCst,
);
max_exit_handle.trigger();
});
}
let cleanup_rt_handle = tokio_rt.handle().clone();
std::mem::forget(tokio_rt);
tracing::info!(sandbox = %config.sandbox_name, "entering VM");
match vm.enter() {
Ok(infallible) => Ok(infallible),
Err(e) => {
if let Err(error) = cleanup_rt_handle.block_on(writeback_guard.release(&db)) {
tracing::warn!(%error, "release writeback pressure membership after VM enter failure");
}
if let Err(error) = cleanup_rt_handle.block_on(cpu_guard.release(&db)) {
tracing::warn!(%error, "release CPU placement after VM enter failure");
}
if let Some(writer) = metrics_writer {
let _ = writer.release(ReleaseMode::Free);
}
Err(RuntimeError::Custom(format!("VM enter: {e}")))
}
}
}
fn oci_upper_host_path(vm: &VmConfig) -> Option<PathBuf> {
if vm.rootfs_vmdk.is_some() {
return vm
.rootfs_upper_spec
.as_ref()
.and_then(|spec| spec.layers.last())
.map(|layer| layer.path.clone())
.or_else(|| vm.rootfs_upper.clone());
}
vm.rootfs_disk_runtime_owned.then(|| {
vm.rootfs_disk_spec
.as_ref()
.and_then(|spec| spec.layers.last())
.map(|layer| layer.path.clone())
.or_else(|| vm.rootfs_disk.clone())
})?
}
#[cfg(windows)]
fn agent_console_pipe_name(sandbox_id: i32) -> String {
format!(
r"\\.\pipe\msb-agent-console-{sandbox_id}-{}",
std::process::id()
)
}
#[cfg(windows)]
fn agent_bulk_console_pipe_name(sandbox_id: i32) -> String {
format!(
r"\\.\pipe\msb-agent-bulk-console-{sandbox_id}-{}",
std::process::id()
)
}
fn apply_block_writeback_limit(
mut disk: msb_krun::DiskBuilder,
format: msb_krun::DiskImageFormat,
read_only: bool,
limit: Option<&msb_krun::WritebackLimit>,
) -> msb_krun::DiskBuilder {
if !read_only
&& matches!(format, msb_krun::DiskImageFormat::Raw)
&& let Some(limit) = limit
{
disk = disk.writeback_limit(limit.clone());
}
disk
}
fn attach_upper_layers(
disk: msb_krun::DiskBuilder,
layers: Vec<UpperLayerSpec>,
) -> msb_krun::DiskBuilder {
if layers.len() == 1 && matches!(layers[0].format, msb_krun::DiskImageFormat::Raw) {
disk.path(&layers[0].path)
} else {
disk.layers(
layers
.into_iter()
.map(|layer| msb_krun::DiskLayer::new(layer.path, layer.format)),
)
}
}
fn validate_upper_layers(spec: &UpperSpec) -> RuntimeResult<Vec<UpperLayerSpec>> {
if spec.layers.is_empty() {
return Err(RuntimeError::Custom(
"upper block chain must contain at least one layer".into(),
));
}
let mut paths = std::collections::BTreeSet::new();
for (index, layer) in spec.layers.iter().enumerate() {
if layer.path.as_os_str().is_empty() {
return Err(RuntimeError::Custom(format!(
"upper block chain layer {index} has an empty path"
)));
}
if !paths.insert(layer.path.clone()) {
return Err(RuntimeError::Custom(format!(
"upper block chain repeats path {}",
layer.path.display()
)));
}
match layer.format {
msb_krun::DiskImageFormat::Raw if index > 0 => {
return Err(RuntimeError::Custom(format!(
"upper block chain layer {index} is raw but has a predecessor"
)));
}
msb_krun::DiskImageFormat::Vmdk => {
return Err(RuntimeError::Custom(format!(
"upper block chain layer {index} uses unsupported VMDK format"
)));
}
msb_krun::DiskImageFormat::Raw | msb_krun::DiskImageFormat::Qcow2 => {}
}
}
Ok(spec.layers.clone())
}
fn recover_owned_disk_layers(runtime_dir: &Path, disks: &mut [DiskMountSpec]) -> RuntimeResult<()> {
for disk in disks.iter_mut().filter(|disk| disk.lifecycle_owned) {
let chain = crate::checkpoint::load_runtime_owned_disk_chain(runtime_dir, &disk.id)
.map_err(|error| {
RuntimeError::Custom(format!("recover owned disk {}: {error}", disk.id))
})?;
let Some(chain) = chain else { continue };
if chain.device_id != disk.id {
return Err(RuntimeError::Custom(format!(
"owned disk {} journal has a different device identity",
disk.id
)));
}
disk.layers = chain
.layers
.into_iter()
.map(|layer| {
Ok(UpperLayerSpec {
path: layer.path,
format: validate_disk_format(Some(&layer.format)).map_err(|error| {
RuntimeError::Custom(format!(
"owned disk {} layer format: {error}",
disk.id
))
})?,
})
})
.collect::<RuntimeResult<Vec<_>>>()?;
if disk.layers.is_empty() {
return Err(RuntimeError::Custom(format!(
"owned disk {} journal has no layers",
disk.id
)));
}
validate_disk_mount_layers(disk)?;
}
Ok(())
}
fn validate_disk_mount_layers(disk: &DiskMountSpec) -> RuntimeResult<Option<Vec<UpperLayerSpec>>> {
if disk.layers.is_empty() {
if !disk.host.exists() {
return Err(RuntimeError::Custom(format!(
"disk {}: host path not found: {}",
disk.id,
disk.host.display()
)));
}
return Ok(None);
}
if !disk.lifecycle_owned || !disk.snapshot_owned {
return Err(RuntimeError::Custom(format!(
"disk {}: explicit mount chains require owned storage",
disk.id
)));
}
let layers = validate_upper_layers(&UpperSpec {
layers: disk.layers.clone(),
read_only: disk.readonly,
})?;
for layer in &layers {
if !std::fs::symlink_metadata(&layer.path)?
.file_type()
.is_file()
{
return Err(RuntimeError::Custom(format!(
"owned disk {}: layer is not a regular file: {}",
disk.id,
layer.path.display()
)));
}
}
Ok(Some(layers))
}
fn writeback_limited_disk_paths(vm: &VmConfig) -> RuntimeResult<Vec<PathBuf>> {
if vm.block_writeback_limit_bytes.is_none() {
return Ok(Vec::new());
}
let mut paths = Vec::new();
if vm.rootfs_path.is_some() {
} else if vm.rootfs_vmdk.is_some() {
if let Some(spec) = &vm.rootfs_upper_spec {
if let Some(head) = spec.layers.last()
&& is_writeback_limited_disk(head.format, spec.read_only)
{
paths.push(head.path.clone());
}
} else if let Some(upper) = &vm.rootfs_upper {
paths.push(upper.clone());
}
} else if let Some(spec) = &vm.rootfs_disk_spec {
if let Some(head) = spec.layers.last()
&& is_writeback_limited_disk(head.format, spec.read_only)
{
paths.push(head.path.clone());
}
} else if let Some(rootfs_disk) = &vm.rootfs_disk {
let format = validate_disk_format(vm.rootfs_disk_format.as_deref())
.map_err(|error| RuntimeError::Custom(format!("disk format: {error}")))?;
if is_writeback_limited_disk(format, vm.rootfs_disk_readonly) {
paths.push(rootfs_disk.clone());
}
}
paths.extend(vm.disks.iter().filter_map(|disk| {
let (path, format) = disk
.layers
.last()
.map_or((&disk.host, disk.format), |head| (&head.path, head.format));
is_writeback_limited_disk(format, disk.readonly).then(|| path.clone())
}));
Ok(paths)
}
fn is_writeback_limited_disk(format: msb_krun::DiskImageFormat, read_only: bool) -> bool {
!read_only && matches!(format, msb_krun::DiskImageFormat::Raw)
}
fn agent_primary_queue_size(has_dedicated_bulk_port: bool) -> u16 {
if has_dedicated_bulk_port {
AGENT_CONTROL_QUEUE_SIZE
} else {
AGENT_BULK_QUEUE_SIZE
}
}
struct HostPlacement<'a> {
vcpu_targets: Option<&'a [crate::cpu::LogicalCpuId]>,
required: bool,
numa_topology: Option<msb_krun::NumaTopology>,
}
struct VmBuildRuntime {
tokio_handle: tokio::runtime::Handle,
startup_progress: crate::startup_progress::StartupProgressCallback,
}
fn build_vm(
config: &Config,
console_backends: AgentConsoleBackends,
on_exit: impl Fn(i32) + Send + 'static,
on_placement: impl FnOnce(&msb_krun::PlacementReport) + Send + 'static,
runtime: VmBuildRuntime,
host_placement: HostPlacement<'_>,
writeback_limit: Option<&msb_krun::WritebackLimit>,
) -> RuntimeResult<VmBuildOutput> {
let VmBuildRuntime {
tokio_handle,
startup_progress,
} = runtime;
let AgentConsoleBackends {
control: console_backend,
bulk: bulk_console_backend,
} = console_backends;
let agent_queue_size = agent_primary_queue_size(bulk_console_backend.is_some());
let vm = &config.vm;
let mut owned_directory_checkpoints = std::collections::BTreeMap::new();
let prepared_restore = vm
.checkpoint_restore
.as_ref()
.map(|restore| {
let prepared = if restore.local_branch {
crate::checkpoint::PreparedCheckpointRestore::open_local(
restore.closure.clone(),
&restore.checkpoint_id,
restore.memory_descriptor,
)
} else {
crate::checkpoint::PreparedCheckpointRestore::open(
restore.closure.clone(),
&restore.checkpoint_root,
)
}
.map_err(|error| {
RuntimeError::Custom(format!("prepare checkpoint restore: {error}"))
})?;
prepared
.validate_geometry(vm)
.map_err(RuntimeError::Custom)?;
Ok::<_, RuntimeError>(prepared)
})
.transpose()?;
let mut bootstrap = vm.bootstrap.clone();
let balloon_stats_interval = config
.metrics_sample_interval_ms
.map(|interval_ms| Duration::from_millis(interval_ms.get()));
#[cfg(unix)]
let mut bind_identity_map = BindIdentityMapRegistration::new();
#[cfg(windows)]
let bind_identity_map = BindIdentityMapRegistration::new();
let kernel_cmdline = agent_kernel_cmdline(vm.thp, config.agent_transport);
let mut builder = VmBuilder::new()
.machine(|m| {
let mut m = m
.vcpus(vm.vcpus)
.memory_mib(vm.memory_mib as usize)
.max_vcpus(vm.max_cpus.max(vm.vcpus))
.max_memory_mib((vm.max_memory_mib.max(vm.memory_mib)) as usize)
.balloon_stats_interval(balloon_stats_interval);
if let Some(targets) = host_placement.vcpu_targets {
let affinity = targets
.iter()
.copied()
.map(|cpu| msb_krun::HostCpuId::in_group(cpu.group, cpu.index))
.collect();
m = if host_placement.required {
m.vcpu_affinity(affinity)
} else {
m.try_vcpu_affinity(affinity)
};
}
if let Some(topology) = host_placement.numa_topology {
m = m.numa_topology(topology);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
m.split_irqchip(true)
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
m
}
})
.kernel(|k| {
let k = k.krunfw_path(&vm.libkrunfw_path).cmdline(&kernel_cmdline);
if let Some(ref init_path) = vm.init_path {
k.init_path(init_path)
} else {
k
}
});
if let Some(ref rootfs_path) = vm.rootfs_path {
let backend = bind_rootfs_backend(rootfs_path, vm.rootfs_follow_root_symlinks)?;
builder = builder.fs(move |fs| fs.tag("/dev/root").custom(Box::new(backend)));
} else if let Some(ref vmdk_path) = vm.rootfs_vmdk {
#[cfg(unix)]
{
let backend = bootstrap_trampoline_backend()?;
builder = builder.fs(move |fs| fs.tag("/dev/root").custom(Box::new(backend)));
}
#[cfg(windows)]
{
let backend = AgentBootstrapFs::new()
.map_err(|e| RuntimeError::Custom(format!("bootstrap rootfs: {e}")))?;
builder = builder.fs(move |fs| fs.tag("/dev/root").custom(Box::new(backend)));
}
let vmdk = vmdk_path.clone();
builder = builder.disk(move |d| {
d.path(&vmdk)
.format(msb_krun::DiskImageFormat::Vmdk)
.read_only(true)
});
if let Some(ref spec) = vm.rootfs_upper_spec {
let layers = validate_upper_layers(spec)?;
let format = layers.last().expect("validated non-empty chain").format;
let read_only = spec.read_only;
let direct_io =
cfg!(target_os = "linux") && matches!(format, msb_krun::DiskImageFormat::Qcow2);
let writeback_limit = writeback_limit.cloned();
builder = builder.disk(move |d| {
let d = attach_upper_layers(d, layers)
.format(format)
.read_only(read_only)
.direct_io(direct_io);
apply_block_writeback_limit(d, format, read_only, writeback_limit.as_ref())
});
} else if let Some(ref upper) = vm.rootfs_upper {
let upper = upper.clone();
let format = msb_krun::DiskImageFormat::Raw;
let writeback_limit = writeback_limit.cloned();
builder = builder.disk(move |d| {
let d = d.path(&upper).format(format).read_only(false);
apply_block_writeback_limit(d, format, false, writeback_limit.as_ref())
});
}
} else if vm.rootfs_disk_spec.is_some() || vm.rootfs_disk.is_some() {
#[cfg(unix)]
{
let backend = bootstrap_trampoline_backend()?;
builder = builder.fs(move |fs| fs.tag("/dev/root").custom(Box::new(backend)));
}
#[cfg(windows)]
{
let backend = AgentBootstrapFs::new()
.map_err(|e| RuntimeError::Custom(format!("bootstrap rootfs: {e}")))?;
builder = builder.fs(move |fs| fs.tag("/dev/root").custom(Box::new(backend)));
}
if let Some(ref spec) = vm.rootfs_disk_spec {
let layers = validate_upper_layers(spec)?;
let format = layers.last().expect("validated non-empty chain").format;
let read_only = spec.read_only;
let direct_io =
cfg!(target_os = "linux") && matches!(format, msb_krun::DiskImageFormat::Qcow2);
let writeback_limit = writeback_limit.cloned();
builder = builder.disk(move |d| {
let d = attach_upper_layers(d, layers)
.format(format)
.read_only(read_only)
.direct_io(direct_io);
apply_block_writeback_limit(d, format, read_only, writeback_limit.as_ref())
});
} else if let Some(ref disk_path) = vm.rootfs_disk {
let format = validate_disk_format(vm.rootfs_disk_format.as_deref())
.map_err(|e| RuntimeError::Custom(format!("disk format: {e}")))?;
let disk_path = disk_path.clone();
let readonly = vm.rootfs_disk_readonly;
let writeback_limit = writeback_limit.cloned();
builder = builder.disk(move |d| {
let d = d.path(&disk_path).format(format).read_only(readonly);
apply_block_writeback_limit(d, format, readonly, writeback_limit.as_ref())
});
}
if bootstrap.block_root.is_none() {
bootstrap.block_root = Some(BootstrapBlockRoot::DiskImage {
device: "/dev/vda".to_string(),
fstype: None,
});
}
}
{
let runtime_tag = microsandbox_protocol::RUNTIME_FS_TAG.to_string();
let cfg = PassthroughConfig {
root_dir: canonicalize_owned_mount_root(&config.runtime_dir)?,
inject_init: false,
quota_bytes: Some(microsandbox_protocol::RUNTIME_FS_QUOTA_BYTES),
no_symlink_root: true,
..Default::default()
};
let backend = PassthroughFs::new(cfg)
.map_err(|e| RuntimeError::Custom(format!("runtime mount: {e}")))?;
builder = builder.fs(move |fs| fs.tag(&runtime_tag).custom(Box::new(backend)));
}
let mut external_mount_reports = Vec::new();
let relaxed = vm.checkpoint_restore.as_ref().is_some_and(|restore| {
restore.external_mount_policy == microsandbox_types::ExternalMountRestorePolicy::Relaxed
});
let file_inputs = if let Some(restore) = &vm.checkpoint_restore {
let mut inputs = Vec::new();
for (index, binding) in restore
.external_mounts
.iter()
.filter(|binding| binding.filename.is_some())
.enumerate()
{
if binding.device_id != format!("virtio_fs{}", 2 + index) {
return Err(RuntimeError::Custom(
"external file transport topology differs".into(),
));
}
let spec = vm.file_mounts.iter().find(|spec| {
spec.mount
.split_once(':')
.is_some_and(|(tag, _)| tag == binding.mount.tag)
});
if spec.is_none() && !binding.unavailable {
return Err(RuntimeError::Custom(format!(
"file mount {} has no trusted launch binding",
binding.mount.guest_path
)));
}
inputs.push((spec, Some(binding)));
}
if vm.file_mounts.len() != inputs.iter().filter(|(spec, _)| spec.is_some()).count() {
return Err(RuntimeError::Custom(
"restore cannot add uncaptured file transports".into(),
));
}
inputs
} else {
vm.file_mounts
.iter()
.map(|spec| (Some(spec), None))
.collect()
};
let captured_file_count = file_inputs.len();
for (file_mount, restore_binding) in file_inputs {
let Some(file_mount) = file_mount else {
let binding = restore_binding.expect("only restore omits backing");
let tag = binding.mount.tag.clone();
builder = builder.fs(move |fs| {
fs.tag(&tag)
.custom(Box::new(microsandbox_filesystem::UnavailableFs::default()))
});
external_mount_reports.push(crate::checkpoint::ExternalMountReport {
guest_path: binding.mount.guest_path.clone(),
unavailable: Some(
"no trusted destination mapping is available; filesystem operations return EIO"
.into(),
),
stale_inodes: Default::default(),
});
continue;
};
let parsed = parse_mount_spec(&file_mount.mount)
.map_err(|e| RuntimeError::Custom(format!("file mount {:?}: {e}", file_mount.mount)))?;
let tag = parsed.tag;
let host_path = PathBuf::from(&parsed.host_path);
let override_owner = match (parsed.override_uid, parsed.override_gid) {
(Some(uid), Some(gid)) => Some((uid, gid)),
_ => None,
};
#[cfg(unix)]
let mount_bind_identity_map = bind_identity_map_for_mount(
&mut bind_identity_map,
parsed.stat_virtualization,
override_owner,
);
let external_options = microsandbox_filesystem::ExternalCheckpointOptions {
relaxed,
remapped: restore_binding.is_some_and(|binding| binding.remapped),
..Default::default()
};
if let Some(binding) = restore_binding {
external_mount_reports.push(crate::checkpoint::ExternalMountReport {
guest_path: binding.mount.guest_path.clone(),
unavailable: None,
stale_inodes: external_options.invalid_inodes.clone(),
});
}
let cfg = PassthroughConfig {
external_checkpoint: Some(external_options),
stat_virtualization: parsed.stat_virtualization,
host_permissions: parsed.host_permissions,
readonly: parsed.readonly,
quota_bytes: parsed.quota_bytes,
#[cfg(unix)]
bind_identity_map: mount_bind_identity_map,
#[cfg(windows)]
default_owner: override_owner,
..Default::default()
};
let backend =
match SingleFileFs::new(host_path.clone(), file_mount.filename.clone(), cfg) {
Err(error)
if relaxed
&& restore_binding.is_some()
&& matches!(
error.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::NotADirectory
| std::io::ErrorKind::IsADirectory
) =>
{
external_mount_reports
.last_mut()
.expect("restore report")
.unavailable = Some(format!(
"external file cannot be opened: {error}; filesystem operations return EIO"
));
builder = builder.fs(move |fs| {
fs.tag(&tag)
.custom(Box::new(microsandbox_filesystem::UnavailableFs::default()))
});
continue;
}
result => result,
}
.map_err(|e| {
RuntimeError::Custom(format!(
"file mount {tag}: failed to open host file {}: {e}",
host_path.display()
))
})?;
builder = builder.fs(move |fs| fs.tag(&tag).custom(Box::new(backend)));
}
let mount_inputs = if let Some(restore) = &vm.checkpoint_restore {
let mut inputs = Vec::new();
for (index, binding) in restore
.external_mounts
.iter()
.filter(|binding| binding.filename.is_none())
.enumerate()
{
if binding.device_id != format!("virtio_fs{}", 2 + captured_file_count + index) {
return Err(RuntimeError::Custom(
"external mount transport topology differs".into(),
));
}
let spec = vm.mounts.iter().find(|spec| {
spec.split_once(':')
.is_some_and(|(tag, _)| tag == binding.mount.tag)
});
if spec.is_none() && !binding.unavailable {
return Err(RuntimeError::Custom(format!(
"mount {} has no trusted launch binding",
binding.mount.guest_path
)));
}
inputs.push((spec.map(String::as_str), Some(binding)));
}
if vm.mounts.len() != inputs.iter().filter(|(spec, _)| spec.is_some()).count() {
return Err(RuntimeError::Custom(
"restore cannot add uncaptured filesystem transports".into(),
));
}
inputs
} else {
vm.mounts
.iter()
.map(|spec| (Some(spec.as_str()), None))
.collect()
};
for (mount_spec, restore_binding) in mount_inputs {
let relaxed = vm.checkpoint_restore.as_ref().is_some_and(|restore| {
restore.external_mount_policy == microsandbox_types::ExternalMountRestorePolicy::Relaxed
});
let Some(mount_spec) = mount_spec else {
let binding = restore_binding.expect("only restores have unavailable bindings");
{
let tag = binding.mount.tag.clone();
builder = builder.fs(move |fs| {
fs.tag(&tag)
.custom(Box::new(microsandbox_filesystem::UnavailableFs::default()))
});
external_mount_reports.push(crate::checkpoint::ExternalMountReport {
guest_path: binding.mount.guest_path.clone(),
unavailable: Some("no trusted destination mapping is available; filesystem operations return EIO".into()),
stale_inodes: Default::default(),
});
continue;
}
};
let parsed = parse_mount_spec(mount_spec)
.map_err(|e| RuntimeError::Custom(format!("--mount {mount_spec:?}: {e}")))?;
let tag = parsed.tag;
let host_path = PathBuf::from(&parsed.host_path);
let owned_mount = vm.owned_volumes.iter().find(|mount| {
matches!(
mount,
microsandbox_types::VolumeMount::Owned {
storage: microsandbox_types::OwnedVolumeStorage::Directory { .. },
..
}
) && microsandbox_types::owned_volume_mount_id(mount.guest()) == tag
&& vm
.bootstrap
.dir_mounts
.iter()
.any(|binding| binding.tag == tag && binding.guest_path == mount.guest())
});
let owned_checkpoint =
owned_mount.map(|_| microsandbox_filesystem::OwnedDirectoryCheckpoint::default());
if let Some(checkpoint) = &owned_checkpoint {
if let Some(restore) = &vm.checkpoint_restore {
checkpoint
.set_restore(&restore.closure.join("owned").join(&tag))
.map_err(|error| RuntimeError::Custom(format!("owned mount {tag}: {error}")))?;
}
owned_directory_checkpoints.insert(tag.clone(), checkpoint.clone());
}
let override_owner = match (parsed.override_uid, parsed.override_gid) {
(Some(uid), Some(gid)) => Some((uid, gid)),
_ => None,
};
#[cfg(unix)]
let mount_bind_identity_map = bind_identity_map_for_mount(
&mut bind_identity_map,
parsed.stat_virtualization,
override_owner,
);
let external_options = microsandbox_filesystem::ExternalCheckpointOptions {
relaxed,
remapped: restore_binding.is_some_and(|binding| binding.remapped),
..Default::default()
};
if let Some(binding) = restore_binding {
external_mount_reports.push(crate::checkpoint::ExternalMountReport {
guest_path: binding.mount.guest_path.clone(),
unavailable: None,
stale_inodes: external_options.invalid_inodes.clone(),
});
}
let cfg = PassthroughConfig {
root_dir: host_path.clone(),
external_checkpoint: owned_checkpoint.is_none().then_some(external_options),
owned_checkpoint,
inject_init: false,
stat_virtualization: parsed.stat_virtualization,
host_permissions: parsed.host_permissions,
readonly: parsed.readonly,
no_symlink_root: !parsed.follow_root_symlinks,
#[cfg(unix)]
bind_identity_map: mount_bind_identity_map,
#[cfg(windows)]
default_owner: override_owner,
quota_bytes: parsed.quota_bytes,
..Default::default()
};
let backend = match PassthroughFs::new(cfg) {
Err(error) if owned_mount.is_none() && relaxed && restore_binding.is_some()
&& matches!(error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::NotADirectory) => {
external_mount_reports.last_mut().expect("restore report").unavailable = Some(format!("external export cannot be opened: {error}; filesystem operations return EIO"));
builder = builder.fs(move |fs| fs.tag(&tag).custom(Box::new(microsandbox_filesystem::UnavailableFs::default())));
continue;
}
result => result,
}.map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
#[cfg(target_os = "macos")]
let platform_hint =
" On macOS, grant access in System Settings > Privacy & Security.";
#[cfg(not(target_os = "macos"))]
let platform_hint = "";
let policy_hint = if matches!(
parsed.stat_virtualization,
StatVirtualization::Strict
) {
" For a foreign-owned path, use stat-virt=relaxed if full metadata virtualization is not required."
} else {
""
};
RuntimeError::Custom(format!(
"mount {tag}: permission denied accessing host folder {} ({e}).{platform_hint}{policy_hint}",
host_path.display(),
))
} else {
RuntimeError::Custom(format!("mount {tag}: {e}"))
}
})?;
builder = builder.fs(move |fs| fs.tag(&tag).custom(Box::new(backend)));
}
let disk_inputs = if let Some(prepared) = &prepared_restore {
let captured = prepared.additional_blocks();
if vm
.disks
.iter()
.any(|disk| !captured.iter().any(|(id, _)| *id == disk.id))
{
return Err(RuntimeError::Custom(
"restore cannot add uncaptured block devices".into(),
));
}
captured
.into_iter()
.map(|(id, state)| {
(
vm.disks.iter().find(|disk| disk.id == id),
Some((id, state)),
)
})
.collect::<Vec<_>>()
} else {
vm.disks.iter().map(|disk| (Some(disk), None)).collect()
};
for (disk, captured) in disk_inputs {
let Some(disk) = disk else {
let (id, state) = captured.expect("only restored devices omit backing");
let guest = vm
.checkpoint_restore
.as_ref()
.and_then(|restore| restore.unavailable_disks.get(id))
.ok_or_else(|| {
RuntimeError::Custom(format!("block {id} has no explicit unavailable binding"))
})?;
if state.device.id != id {
return Err(RuntimeError::Custom(
"captured block identity mismatch".into(),
));
}
builder = builder.disk(|disk| disk.unavailable(state.clone()));
external_mount_reports.push(crate::checkpoint::ExternalMountReport {
guest_path: guest.clone(),
unavailable: Some("additional disk was not mapped; disk I/O returns EIO and the guest filesystem may abort its journal or become read-only".into()),
stale_inodes: Default::default(),
});
continue;
};
let layers = validate_disk_mount_layers(disk)?;
tracing::debug!(
id = %disk.id,
guest = %disk.guest,
host = %disk.host.display(),
?disk.format,
fstype = ?disk.fstype,
readonly = disk.readonly,
"attaching disk-image volume",
);
let id = disk.id.clone();
let host = disk.host.clone();
let format = layers
.as_ref()
.and_then(|layers| layers.last())
.map_or(disk.format, |head| head.format);
let readonly = disk.readonly;
let direct_io = layers.is_some()
&& cfg!(target_os = "linux")
&& matches!(format, msb_krun::DiskImageFormat::Qcow2);
let writeback_limit = writeback_limit.cloned();
builder = builder.disk(move |d| {
let d = d.id(&id);
let d = match layers {
Some(layers) => attach_upper_layers(d, layers).direct_io(direct_io),
None => d.path(&host),
};
let mut d = d.format(format).read_only(readonly);
if readonly {
d = d
.cache(msb_krun::CacheMode::Unsafe)
.sync(msb_krun::SyncMode::None);
}
apply_block_writeback_limit(d, format, readonly, writeback_limit.as_ref())
});
}
let mut network_termination_handle = None;
let mut network_metrics_handle = None;
let mut network_secrets_handle = None;
#[cfg(feature = "net")]
let mut network_activation_handle = None;
#[cfg(not(feature = "net"))]
let network_activation_handle: Option<NetworkActivationHandle> = None;
#[cfg(unix)]
if !vm.vsock.is_empty() {
#[cfg(feature = "net")]
if vm.deployment_profile == DeploymentProfile::MultiTenant {
return Err(RuntimeError::Custom(
"host vsock routes are disabled for multi-tenant deployments".to_string(),
));
}
let mut streams: Vec<(u32, Arc<dyn msb_krun::backends::vsock::VsockPortBackend>)> =
Vec::new();
let mut datagrams: Vec<(
u32,
Arc<dyn msb_krun::backends::vsock::VsockDatagramPortBackend>,
)> = Vec::new();
for route in &vm.vsock {
match route.socket_type {
microsandbox_types::VsockSocketType::Stream => {
let backend =
UnixStreamPortBackend::new(&route.host_socket).map_err(|err| {
RuntimeError::Custom(format!(
"initialize stream vsock route {}:{}: {err}",
route.host_socket.display(),
route.port
))
})?;
streams.push((route.port, Arc::new(backend)));
}
microsandbox_types::VsockSocketType::Dgram => {
let backend =
UnixDatagramPortBackend::new(&route.host_socket).map_err(|err| {
RuntimeError::Custom(format!(
"initialize datagram vsock route {}:{}: {err}",
route.host_socket.display(),
route.port
))
})?;
datagrams.push((route.port, Arc::new(backend)));
}
}
}
builder = builder.vsock(move |mut vsock| {
for (port, backend) in streams {
vsock = vsock.custom(port, backend);
}
for (port, backend) in datagrams {
vsock = vsock.custom_dgram(port, backend);
}
vsock
});
}
#[cfg(windows)]
if !vm.vsock.is_empty() {
#[cfg(feature = "net")]
if vm.deployment_profile == DeploymentProfile::MultiTenant {
return Err(RuntimeError::Custom(
"host vsock routes are disabled for multi-tenant deployments".to_string(),
));
}
let mut streams: Vec<(u32, Arc<dyn msb_krun::backends::vsock::VsockPortBackend>)> =
Vec::new();
for route in &vm.vsock {
if route.socket_type == microsandbox_types::VsockSocketType::Dgram {
return Err(RuntimeError::Custom(
"vsock datagram routes are not supported on Windows".to_string(),
));
}
let backend = WindowsNamedPipePortBackend::new(&route.host_socket).map_err(|err| {
RuntimeError::Custom(format!(
"initialize stream vsock route {}:{}: {err}",
route.host_socket.display(),
route.port
))
})?;
streams.push((route.port, Arc::new(backend)));
}
builder = builder.vsock(move |mut vsock| {
for (port, backend) in streams {
vsock = vsock.custom(port, backend);
}
vsock
});
}
#[cfg(feature = "net")]
if vm.network.config().enabled {
let _ = rustls::crypto::ring::default_provider().install_default();
vm.network
.config()
.secrets
.validate()
.map_err(|err| RuntimeError::Custom(format!("invalid network secrets: {err}")))?;
let rate_limiters = to_krun_network_rate_limiters(vm.network.config());
let mut network =
SmoltcpNetwork::new(vm.network.clone(), vm.sandbox_slot, vm.deployment_profile)
.map_err(|err| RuntimeError::Custom(format!("initialize network: {err}")))?;
if let Some(restore) = &vm.checkpoint_restore {
let gateway = restore.network_gateway_mac.ok_or_else(|| {
RuntimeError::Custom(
"full restore lacks captured gateway MAC; recapture the development snapshot"
.into(),
)
})?;
network = network
.with_captured_gateway_mac(gateway)
.map_err(|err| RuntimeError::Custom(format!("restore network: {err}")))?;
}
network_termination_handle = Some(network.termination_handle());
network_metrics_handle = Some(network.metrics_handle());
if !vm.network.config().secrets.secrets.is_empty() {
network_secrets_handle = Some(network.secrets_handle());
}
if vm.checkpoint_restore.is_some() {
network_activation_handle = Some(network.defer_activation());
}
network.start(tokio_handle.clone());
let guest_mac = network.guest_mac();
let net_backend = network.take_backend();
{
let tls_dir = config.runtime_dir.join("tls");
let _ = std::fs::create_dir_all(&tls_dir);
if let Some(ca_pem) = network.ca_cert_pem() {
let _ = std::fs::write(tls_dir.join("ca.pem"), &ca_pem);
}
if let Some(host_cas_pem) = network.host_cas_cert_pem() {
let _ = std::fs::write(tls_dir.join("host-cas.pem"), &host_cas_pem);
}
}
bootstrap.network = Some(network.guest_bootstrap_network());
bootstrap.host_alias = Some(network.guest_host_alias().to_string());
bootstrap.default_env.extend(network.guest_secret_env());
builder = builder.net(move |mut n| {
n = n.mac(guest_mac);
if let Some(config) = rate_limiters.rx {
n = n.rx_rate_limiter(config);
}
if let Some(config) = rate_limiters.tx {
n = n.tx_rate_limiter(config);
}
n.custom(net_backend)
});
}
builder = builder.exec(|mut e| {
if let Some(ref path) = vm.exec_path {
e = e.path(path);
}
if !vm.exec_args.is_empty() {
e = e.args(&vm.exec_args);
}
e
});
let kernel_log_path = config.log_dir.join("kernel.log");
#[cfg(unix)]
{
builder = builder.console(|c| {
let c = c.output(&kernel_log_path).custom_with_options(
microsandbox_protocol::AGENT_PORT_NAME,
Box::new(console_backend),
msb_krun::ConsolePortOptions::new().queue_size(agent_queue_size),
);
match bulk_console_backend {
Some(backend) => c.custom_with_options(
microsandbox_protocol::AGENT_BULK_PORT_NAME,
Box::new(backend),
msb_krun::ConsolePortOptions::new().queue_size(AGENT_BULK_QUEUE_SIZE),
),
None => c,
}
});
}
#[cfg(windows)]
{
let _ = (console_backend, bulk_console_backend);
let agent_pipe = agent_console_pipe_name(config.sandbox_id);
let bulk_pipe = config
.agent_transport
.offers_dual_port()
.then(|| agent_bulk_console_pipe_name(config.sandbox_id));
builder = builder.console(|c| {
let c = c
.disable_implicit()
.virtio_output(&kernel_log_path)
.named_pipe_with_options(
microsandbox_protocol::AGENT_PORT_NAME,
agent_pipe,
msb_krun::ConsolePortOptions::new().queue_size(agent_queue_size),
);
match bulk_pipe {
Some(pipe) => c.named_pipe_with_options(
microsandbox_protocol::AGENT_BULK_PORT_NAME,
pipe,
msb_krun::ConsolePortOptions::new().queue_size(AGENT_BULK_QUEUE_SIZE),
),
None => c,
}
});
}
builder = builder.on_placement(on_placement).on_exit(on_exit);
let mut vm = builder
.build()
.map_err(|e| RuntimeError::Custom(format!("build VM: {e}")))?;
let restored_agent = if let Some(restore) = &config.vm.checkpoint_restore {
let prepared = prepared_restore.expect("restore was admitted before device construction");
prepared
.seed_root_disk(&config.runtime_dir, &config.vm)
.map_err(RuntimeError::Custom)?;
let cache_root = restore
.forked
.then(|| {
config.vm.memory_cache_dir.clone().ok_or_else(|| {
RuntimeError::Custom(
"CoW memory requires its backend-resolved cache directory".into(),
)
})
})
.transpose()?;
let mut restored = prepared
.install(&mut vm, cache_root, startup_progress)
.map_err(RuntimeError::Custom)?;
restored.external_mount_reports = external_mount_reports;
Some(restored)
} else {
None
};
let bootstrap_frame = if restored_agent.is_none() {
Some(encode_bootstrap_frame(&bootstrap)?)
} else {
None
};
Ok((
vm,
network_termination_handle,
network_metrics_handle,
network_secrets_handle,
network_activation_handle,
bootstrap_frame,
bootstrap,
bind_identity_map,
restored_agent,
owned_directory_checkpoints,
))
}
fn encode_bootstrap_frame(bootstrap: &GuestBootstrap) -> RuntimeResult<Vec<u8>> {
let message = Message::with_payload(MessageType::Bootstrap, 0, bootstrap)
.map_err(|e| RuntimeError::Custom(format!("encode guest bootstrap: {e}")))?;
let mut frame = Vec::new();
codec::encode_to_buf(&message, &mut frame)
.map_err(|e| RuntimeError::Custom(format!("encode guest bootstrap frame: {e}")))?;
Ok(frame)
}
fn publish_control_endpoint(
control_sock_path: PathBuf,
context: super::control::ControlContext,
run_dir: &Path,
sandbox_name: &str,
) -> RuntimeResult<()> {
#[cfg(not(unix))]
let _ = (run_dir, sandbox_name);
match super::control::spawn_control_listener(control_sock_path.clone(), context) {
Ok(()) => {
#[cfg(unix)]
if let Err(error) =
crate::ipc::publish_legacy_control_link(run_dir, sandbox_name, &control_sock_path)
{
if error.kind() == std::io::ErrorKind::InvalidInput {
tracing::warn!(
"legacy runtime control endpoint is unavailable for {sandbox_name}: {error}"
);
} else {
return Err(error.into());
}
}
}
Err(error) => {
tracing::warn!(
"failed to start runtime control listener at {}: {error}",
control_sock_path.display()
);
}
}
Ok(())
}
#[cfg(feature = "net")]
fn to_krun_network_rate_limiters(
config: µsandbox_network::config::NetworkConfig,
) -> KrunNetworkRateLimiters {
let rate_limiter = config.rate_limiter.as_ref();
KrunNetworkRateLimiters {
rx: rate_limiter
.and_then(|rate_limiter| rate_limiter.ingress.as_ref())
.map(to_krun_rate_limiter),
tx: rate_limiter
.and_then(|rate_limiter| rate_limiter.egress.as_ref())
.map(to_krun_rate_limiter),
}
}
#[cfg(feature = "net")]
fn to_krun_rate_limiter(
config: µsandbox_types::RateLimiterConfig,
) -> msb_krun::RateLimiterConfig {
fn bucket(config: µsandbox_types::TokenBucketConfig) -> msb_krun::TokenBucketConfig {
msb_krun::TokenBucketConfig {
size: config.size,
refill_time: Duration::from_millis(config.refill_time_ms),
one_time_burst: config.one_time_burst,
}
}
msb_krun::RateLimiterConfig {
bandwidth: config.bandwidth.as_ref().map(bucket),
ops: config.ops.as_ref().map(bucket),
}
}
async fn monitor_writeback_pressure(
guard: Arc<crate::writeback::WritebackPressureGuard>,
db: DbWriteConnection,
) {
let mut interval = tokio::time::interval(WRITEBACK_PRESSURE_REFRESH_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
interval.tick().await;
let mut coordination_failed = false;
loop {
interval.tick().await;
match guard.refresh(&db).await {
Ok(()) if coordination_failed => {
tracing::info!("writeback pressure coordination recovered");
coordination_failed = false;
}
Ok(()) => {}
Err(error) => {
guard.fail_closed();
if !coordination_failed {
tracing::warn!(%error, "writeback pressure coordination failed closed");
coordination_failed = true;
}
}
}
}
}
#[cfg(unix)]
fn raise_nofile_limit() {
const TARGET: libc::rlim_t = 1_048_576;
let mut lim = libc::rlimit {
rlim_cur: 0,
rlim_max: 0,
};
if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) } != 0 {
tracing::warn!(
error = %std::io::Error::last_os_error(),
"getrlimit(RLIMIT_NOFILE) failed; keeping inherited fd limit"
);
return;
}
let want = lim.rlim_max.min(TARGET);
#[cfg(target_os = "macos")]
let want = macos_maxfilesperproc().map_or(want, |max| want.min(max));
if want <= lim.rlim_cur {
return;
}
let new = libc::rlimit {
rlim_cur: want,
rlim_max: lim.rlim_max,
};
if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &new) } != 0 {
tracing::warn!(
error = %std::io::Error::last_os_error(),
soft = lim.rlim_cur,
wanted = want,
"setrlimit(RLIMIT_NOFILE) failed; keeping inherited fd limit"
);
} else {
tracing::debug!(from = lim.rlim_cur, to = want, "raised RLIMIT_NOFILE");
}
}
#[cfg(target_os = "macos")]
fn macos_maxfilesperproc() -> Option<libc::rlim_t> {
let mut maxfiles: libc::c_int = 0;
let mut len = std::mem::size_of::<libc::c_int>();
let ret = unsafe {
libc::sysctlbyname(
c"kern.maxfilesperproc".as_ptr(),
&mut maxfiles as *mut _ as *mut libc::c_void,
&mut len,
std::ptr::null_mut(),
0,
)
};
(ret == 0 && maxfiles > 0).then_some(maxfiles as libc::rlim_t)
}
#[cfg(unix)]
fn bootstrap_trampoline_backend() -> RuntimeResult<PassthroughFs> {
let trampoline = tempfile::tempdir()?;
for directory in ["dev", "sys", "proc", ".msb", "newroot"] {
std::fs::create_dir(trampoline.path().join(directory)).map_err(|error| {
RuntimeError::Custom(format!("create bootstrap mountpoint {directory}: {error}"))
})?;
}
let cfg = PassthroughConfig {
root_dir: canonicalize_owned_mount_root(trampoline.path())?,
no_symlink_root: true,
..Default::default()
};
let backend = PassthroughFs::new(cfg)
.map_err(|error| RuntimeError::Custom(format!("trampoline rootfs: {error}")))?;
let _ = trampoline.keep();
Ok(backend)
}
fn prepare_runtime_restore_namespace(runtime_dir: &Path, oci_root: bool) -> RuntimeResult<()> {
if oci_root {
for directory in ["rootfs/lower", "rootfs/upperfs"] {
std::fs::create_dir_all(runtime_dir.join(directory)).map_err(|error| {
RuntimeError::Custom(format!(
"create restored runtime mountpoint {directory}: {error}"
))
})?;
}
}
let heartbeat = runtime_dir.join("heartbeat.json");
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&heartbeat)
{
Ok(_) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(error) => Err(RuntimeError::Custom(format!(
"create restored runtime heartbeat {}: {error}",
heartbeat.display()
))),
}
}
fn bind_rootfs_backend(
rootfs_path: &Path,
follow_root_symlinks: bool,
) -> RuntimeResult<PassthroughFs> {
let cfg = PassthroughConfig {
root_dir: rootfs_path.to_path_buf(),
no_symlink_root: !follow_root_symlinks,
..Default::default()
};
PassthroughFs::new(cfg).map_err(|e| RuntimeError::Custom(format!("rootfs: {e}")))
}
fn canonicalize_owned_mount_root(path: &Path) -> RuntimeResult<PathBuf> {
std::fs::canonicalize(path).map_err(|e| {
RuntimeError::Custom(format!("canonicalize mount root {}: {e}", path.display()))
})
}
fn activate_metrics_writer(
handoff: Option<&MetricsSlotHandoff>,
interval: Option<NonZero<u64>>,
run_id: i32,
pid: u32,
) -> Option<microsandbox_metrics::MetricsSlotWriter> {
interval?;
let handoff = handoff?;
let registry = match MetricsRegistry::open(&handoff.shm_name) {
Ok(reg) => reg,
Err(err) => {
tracing::warn!(error = %err, shm = %handoff.shm_name, "failed to open metrics registry");
return None;
}
};
let started_at = chrono::Utc::now();
match registry.activate_writer(ActivateSlot {
slot: handoff.slot,
generation: handoff.generation,
run_id,
pid: pid as i32,
started_at,
}) {
Ok(writer) => Some(writer),
Err(err) => {
tracing::warn!(error = %err, "failed to activate metrics slot");
None
}
}
}
fn release_reserved_metrics_slot(handoff: Option<&MetricsSlotHandoff>) {
let Some(handoff) = handoff else { return };
if let Ok(reg) = MetricsRegistry::open(&handoff.shm_name) {
let _ = reg.release_reserved(handoff.slot, handoff.generation);
}
}
#[cfg(unix)]
fn bind_identity_map_for_mount(
registration: &mut BindIdentityMapRegistration,
stat_virtualization: StatVirtualization,
override_owner: Option<(u32, u32)>,
) -> Option<BindIdentityMapHandle> {
if matches!(stat_virtualization, StatVirtualization::Off) {
return None;
}
if let Some((guest_uid, guest_gid)) = override_owner {
return Some(Arc::new(OnceLock::from(BindIdentityMap::fixed(
guest_uid, guest_gid,
))));
}
registration.mount_count += 1;
let handle = registration
.handle
.get_or_insert_with(|| Arc::new(OnceLock::new()));
Some(Arc::clone(handle))
}
#[cfg(unix)]
fn setup_log_capture(log_dir: &std::path::Path, forward: bool) -> RuntimeResult<()> {
let devnull = std::fs::OpenOptions::new().write(true).open("/dev/null")?;
unsafe {
libc::dup2(devnull.as_raw_fd(), libc::STDOUT_FILENO);
}
drop(devnull);
let (stderr_read, stderr_write) = create_pipe()?;
let orig_stderr: Option<std::fs::File> = if forward {
Some(unsafe { std::fs::File::from_raw_fd(libc::dup(libc::STDERR_FILENO)) })
} else {
None
};
unsafe {
libc::dup2(stderr_write.as_raw_fd(), libc::STDERR_FILENO);
}
drop(stderr_write);
spawn_log_thread("log-runtime", stderr_read, log_dir, "runtime", orig_stderr)?;
Ok(())
}
#[cfg(windows)]
fn setup_log_capture(_log_dir: &std::path::Path, _forward: bool) -> RuntimeResult<()> {
Ok(())
}
#[cfg(unix)]
fn write_startup_info(
startup_fd: Option<&OwnedFd>,
json: &str,
) -> RuntimeResult<Option<std::fs::File>> {
if let Some(fd) = startup_fd {
let dup = unsafe { libc::dup(fd.as_raw_fd()) };
if dup < 0 {
return Err(std::io::Error::last_os_error().into());
}
let mut file = unsafe { std::fs::File::from_raw_fd(dup) };
writeln!(file, "{json}")?;
file.flush()?;
return Ok(Some(file));
}
let mut stdout = std::io::stdout().lock();
writeln!(stdout, "{json}")?;
stdout.flush()?;
Ok(None)
}
#[cfg(windows)]
fn write_startup_info(
startup_pipe: Option<&str>,
json: &str,
) -> RuntimeResult<Option<std::fs::File>> {
if let Some(pipe) = startup_pipe {
let mut file = std::fs::OpenOptions::new()
.write(true)
.open(pipe)
.map_err(|err| RuntimeError::Custom(format!("open startup pipe {pipe}: {err}")))?;
writeln!(file, "{json}")?;
file.flush()?;
return Ok(Some(file));
}
let mut stdout = std::io::stdout().lock();
writeln!(stdout, "{json}")?;
stdout.flush()?;
Ok(None)
}
async fn connect_db(
db_path: &std::path::Path,
connect_timeout_secs: u64,
) -> RuntimeResult<DbWriteConnection> {
DbWriteConnection::open(
db_path,
Duration::from_secs(connect_timeout_secs),
Duration::from_secs(microsandbox_db::pool::DEFAULT_BUSY_TIMEOUT_SECS),
)
.await
.map_err(|e| RuntimeError::Custom(format!("database connect: {e}")))
}
async fn insert_run(db: &DbWriteConnection, sandbox_id: i32, pid: u32) -> RuntimeResult<i32> {
let now = chrono::Utc::now().naive_utc();
let record = run_entity::ActiveModel {
sandbox_id: Set(sandbox_id),
pid: Set(Some(pid as i32)),
status: Set(run_entity::RunStatus::Running),
started_at: Set(Some(now)),
..Default::default()
};
let result = run_entity::Entity::insert(record)
.exec(db)
.await
.map_err(|e| RuntimeError::Custom(format!("insert run: {e}")))?;
Ok(result.last_insert_id)
}
async fn mark_run_failed(db: &DbWriteConnection, run_id: i32) -> RuntimeResult<()> {
use sea_orm::QueryFilter;
use sea_orm::sea_query::Expr;
let now = chrono::Utc::now().naive_utc();
run_entity::Entity::update_many()
.col_expr(
run_entity::Column::Status,
Expr::value(run_entity::RunStatus::Terminated),
)
.col_expr(
run_entity::Column::TerminationReason,
Expr::value(run_entity::TerminationReason::InternalError),
)
.col_expr(run_entity::Column::TerminatedAt, Expr::value(now))
.filter(run_entity::Column::Id.eq(run_id))
.exec(db)
.await
.map_err(|e| RuntimeError::Custom(format!("mark run failed: {e}")))?;
Ok(())
}
#[cfg(any(unix, test))]
fn request_guest_shutdown(shared: &ConsoleSharedState) -> RuntimeResult<()> {
request_guest_shutdown_with_timeout(shared, Duration::from_secs(60))
}
async fn request_guest_shutdown_async(shared: &Arc<ConsoleSharedState>) -> RuntimeResult<()> {
relay::push_guest_frame_until_async(shared, guest_shutdown_frame()?, Duration::from_secs(60))
.await
}
#[cfg(any(unix, test))]
fn request_guest_shutdown_with_timeout(
shared: &ConsoleSharedState,
timeout: Duration,
) -> RuntimeResult<()> {
relay::push_guest_frame_until(shared, guest_shutdown_frame()?, timeout)
}
fn guest_shutdown_frame() -> RuntimeResult<Vec<u8>> {
let msg = Message::with_payload(MessageType::Shutdown, 0, &())
.map_err(|e| RuntimeError::Custom(format!("encode idle shutdown: {e}")))?;
let mut frame = Vec::new();
codec::encode_to_buf(&msg, &mut frame)
.map_err(|e| RuntimeError::Custom(format!("encode idle shutdown frame: {e}")))?;
Ok(frame)
}
fn guest_shutdown_flush_timeout(has_handoff_init: bool) -> Duration {
let override_ms = std::env::var("MSB_SHUTDOWN_FLUSH_TIMEOUT_MS").ok();
guest_shutdown_flush_timeout_with_override(has_handoff_init, override_ms.as_deref())
}
fn guest_shutdown_flush_timeout_with_override(
has_handoff_init: bool,
override_ms: Option<&str>,
) -> Duration {
if let Some(raw) = override_ms {
match raw.parse::<u64>() {
Ok(ms) => return Duration::from_millis(ms),
Err(error) => {
tracing::warn!(
value = raw,
error = %error,
"ignoring invalid MSB_SHUTDOWN_FLUSH_TIMEOUT_MS override"
);
}
}
}
if has_handoff_init {
microsandbox_protocol::HANDOFF_SHUTDOWN_FLUSH_TIMEOUT
} else {
microsandbox_protocol::NORMAL_SHUTDOWN_FLUSH_TIMEOUT
}
}
#[cfg(unix)]
fn spawn_parent_watchdog(
parent_watchdog: OwnedFd,
shared: Arc<ConsoleSharedState>,
exit_reason: Arc<std::sync::atomic::AtomicU8>,
exit_handle: msb_krun::ExitHandle,
sandbox_name: String,
shutdown_flush_timeout: Duration,
) -> RuntimeResult<()> {
std::thread::Builder::new()
.name(format!("msb-parent-watch-{sandbox_name}"))
.spawn(move || {
let mut file = std::fs::File::from(parent_watchdog);
match read_parent_watchdog_signal(&mut file) {
Ok(ParentWatchdogSignal::ParentExited) => {
tracing::info!("creator process exited; stopping attached sandbox");
exit_reason.store(EXIT_REASON_PARENT_EXIT, std::sync::atomic::Ordering::SeqCst);
if shared
.resident_paused
.load(std::sync::atomic::Ordering::Acquire)
{
exit_handle.trigger();
return;
}
if let Err(err) = request_guest_shutdown(&shared) {
tracing::warn!(error = %err, "parent-watch shutdown request failed");
} else {
std::thread::sleep(shutdown_flush_timeout);
}
exit_handle.trigger();
}
Ok(ParentWatchdogSignal::Detached) => {
tracing::debug!("attached-parent watchdog detached; leaving sandbox running");
}
Err(err) => {
tracing::warn!(error = %err, "parent-watch read failed; stopping sandbox");
exit_reason.store(EXIT_REASON_SIGNAL, std::sync::atomic::Ordering::SeqCst);
exit_handle.trigger();
}
}
})
.map_err(RuntimeError::Io)?;
Ok(())
}
#[cfg(unix)]
fn read_parent_watchdog_signal(file: &mut std::fs::File) -> std::io::Result<ParentWatchdogSignal> {
let mut buf = [0_u8; 1];
loop {
match std::io::Read::read(file, &mut buf) {
Ok(0) => return Ok(ParentWatchdogSignal::ParentExited),
Ok(_) if buf[0] == PARENT_WATCH_DETACH => return Ok(ParentWatchdogSignal::Detached),
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => continue,
Err(err) => return Err(err),
}
}
}
#[cfg(unix)]
fn create_pipe() -> RuntimeResult<(OwnedFd, OwnedFd)> {
let mut fds = [0i32; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
return Err(RuntimeError::Io(std::io::Error::last_os_error()));
}
Ok(unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) })
}
#[cfg(unix)]
fn spawn_log_thread(
name: &str,
pipe_read: OwnedFd,
log_dir: &std::path::Path,
log_prefix: &str,
forward: Option<std::fs::File>,
) -> RuntimeResult<()> {
use crate::logging::RotatingLog;
use std::io::Read;
const MAX_LOG_BYTES: u64 = 10 * 1024 * 1024;
let log_dir = log_dir.to_path_buf();
let log_prefix = log_prefix.to_string();
std::thread::Builder::new()
.name(name.into())
.spawn(move || {
let mut log = match RotatingLog::new(&log_dir, &log_prefix, MAX_LOG_BYTES) {
Ok(log) => log,
Err(e) => {
let _ = writeln!(std::io::stderr(), "failed to create {log_prefix} log: {e}");
return;
}
};
let mut reader = unsafe { std::fs::File::from_raw_fd(pipe_read.into_raw_fd()) };
let mut fwd = forward;
let mut buf = [0u8; 4096];
loop {
match reader.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
let _ = log.write(&buf[..n]);
if let Some(ref mut f) = fwd {
let _ = std::io::Write::write_all(f, &buf[..n]);
}
}
Err(_) => break,
}
}
})
.map_err(|e| RuntimeError::Custom(format!("spawn {name} thread: {e}")))?;
Ok(())
}
#[derive(Debug)]
struct ParsedMountSpec {
tag: String,
host_path: String,
stat_virtualization: StatVirtualization,
host_permissions: HostPermissions,
readonly: bool,
follow_root_symlinks: bool,
quota_bytes: Option<u64>,
override_uid: Option<u32>,
override_gid: Option<u32>,
}
fn parse_mount_spec(spec: &str) -> Result<ParsedMountSpec, String> {
let (tag, rest) = spec
.split_once(':')
.ok_or_else(|| format!("expected tag:host_path[:opts] shape, got {spec:?}"))?;
if tag.is_empty() {
return Err(format!("empty tag in mount spec {spec:?}"));
}
let (host_path, options) = split_mount_host_options(rest);
if host_path.is_empty() {
return Err(format!("empty host path in mount spec {spec:?}"));
}
if host_path.contains(',') {
return Err(format!(
"mount options must use tag:host_path:opts syntax, got comma in host path {host_path:?}"
));
}
let mut stat_virtualization = StatVirtualization::Strict;
let mut host_permissions = HostPermissions::Private;
let mut readonly = false;
let mut follow_root_symlinks = false;
let mut quota_bytes = None;
let mut override_uid = None;
let mut override_gid = None;
let mut seen_stat_virt = false;
let mut seen_host_perms = false;
let mut seen_access = false;
let mut seen_noexec = false;
let mut seen_nosuid = false;
let mut seen_nodev = false;
let mut seen_follow_root = false;
let mut seen_quota = false;
let mut seen_uid = false;
let mut seen_gid = false;
if let Some(opts) = options {
for opt in opts.split(',') {
let opt = opt.trim();
if opt.is_empty() {
continue;
}
match opt {
"ro" | "rw" => {
if seen_access {
return Err("mount option `ro`/`rw` specified more than once".to_string());
}
seen_access = true;
readonly = opt == "ro";
}
"noexec" => {
if seen_noexec {
return Err("mount option `noexec` specified more than once".to_string());
}
seen_noexec = true;
}
"nosuid" => {
if seen_nosuid {
return Err("mount option `nosuid` specified more than once".to_string());
}
seen_nosuid = true;
}
"nodev" => {
if seen_nodev {
return Err("mount option `nodev` specified more than once".to_string());
}
seen_nodev = true;
}
"follow-root-symlinks" => {
if seen_follow_root {
return Err(
"mount option `follow-root-symlinks` specified more than once"
.to_string(),
);
}
seen_follow_root = true;
follow_root_symlinks = true;
}
"suid" | "exec" | "dev" => {
return Err(format!("unsupported mount option {opt:?}"));
}
_ => {
let (key, value) = opt
.split_once('=')
.ok_or_else(|| format!("expected flag or key=value option, got {opt:?}"))?;
match key {
"stat-virt" => {
if seen_stat_virt {
return Err(
"mount option `stat-virt` specified more than once".to_string()
);
}
seen_stat_virt = true;
stat_virtualization = match value {
"strict" => StatVirtualization::Strict,
"relaxed" => StatVirtualization::Relaxed,
"off" => StatVirtualization::Off,
other => {
return Err(format!(
"invalid stat-virt {other:?} (expected strict|relaxed|off)"
));
}
}
}
"host-perms" => {
if seen_host_perms {
return Err("mount option `host-perms` specified more than once"
.to_string());
}
seen_host_perms = true;
host_permissions = match value {
"private" => HostPermissions::Private,
"mirror" => HostPermissions::Mirror,
other => {
return Err(format!(
"invalid host-perms {other:?} (expected private|mirror)"
));
}
}
}
"quota" => {
if seen_quota {
return Err(
"mount option `quota` specified more than once".to_string()
);
}
seen_quota = true;
let mib = value.parse::<u64>().map_err(|_| {
format!(
"invalid quota {value:?} (expected an integer count of MiB)"
)
})?;
quota_bytes = Some(mib.saturating_mul(1024 * 1024));
}
"uid" => {
if seen_uid {
return Err(
"mount option `uid` specified more than once".to_string()
);
}
seen_uid = true;
override_uid = Some(value.parse::<u32>().map_err(|_| {
format!("invalid uid {value:?} (expected an unsigned integer)")
})?);
}
"gid" => {
if seen_gid {
return Err(
"mount option `gid` specified more than once".to_string()
);
}
seen_gid = true;
override_gid = Some(value.parse::<u32>().map_err(|_| {
format!("invalid gid {value:?} (expected an unsigned integer)")
})?);
}
other => return Err(format!("unknown mount option {other:?}")),
}
}
}
}
}
if override_uid.is_some() != override_gid.is_some() {
return Err("mount options `uid` and `gid` must be specified together".to_string());
}
if override_uid.is_some() && matches!(stat_virtualization, StatVirtualization::Off) {
return Err(
"mount options `uid` and `gid` cannot be combined with stat-virt=off".to_string(),
);
}
Ok(ParsedMountSpec {
tag: tag.to_string(),
host_path: host_path.to_string(),
stat_virtualization,
host_permissions,
readonly,
follow_root_symlinks,
quota_bytes,
override_uid,
override_gid,
})
}
fn split_mount_host_options(rest: &str) -> (&str, Option<&str>) {
let search = if windows_drive_path_prefix_len(rest).is_some() {
&rest[2..]
} else {
rest
};
match search.rsplit_once(':') {
Some((_prefix, opts)) => {
let split_at = rest.len() - opts.len() - 1;
let host = &rest[..split_at];
(host, Some(opts))
}
None => (rest, None),
}
}
fn windows_drive_path_prefix_len(rest: &str) -> Option<usize> {
#[cfg(windows)]
{
microsandbox_utils::is_windows_drive_path_text(rest).then_some(2)
}
#[cfg(not(windows))]
{
let _ = rest;
None
}
}
pub fn validate_disk_format(format: Option<&str>) -> msb_krun::Result<msb_krun::DiskImageFormat> {
match format.unwrap_or("raw") {
"qcow2" => Ok(msb_krun::DiskImageFormat::Qcow2),
"raw" => Ok(msb_krun::DiskImageFormat::Raw),
"vmdk" => Ok(msb_krun::DiskImageFormat::Vmdk),
other => Err(msb_krun::Error::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("unknown disk image format: {other}"),
))),
}
}
pub fn append_block_root_env(env: &mut Vec<String>) {
let prefix = format!("{}=", microsandbox_protocol::ENV_BLOCK_ROOT);
if env.iter().any(|entry| entry.starts_with(&prefix)) {
return;
}
env.push(format!("{prefix}/dev/vda"));
}
pub fn prepend_scripts_path(env: &mut Vec<String>) {
let scripts = microsandbox_protocol::SCRIPTS_PATH;
let prefix = "PATH=";
if let Some(entry) = env.iter_mut().find(|entry| entry.starts_with(prefix)) {
let existing = &entry[prefix.len()..];
if !existing.split(':').any(|segment| segment == scripts) {
*entry = format!("{prefix}{scripts}:{existing}");
}
} else {
env.push(format!(
"{prefix}{scripts}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
));
}
}
fn thp_kernel_cmdline(policy: microsandbox_types::TransparentHugePagePolicy) -> String {
format!("transparent_hugepage={}", policy.as_str())
}
fn agent_kernel_cmdline(
thp: microsandbox_types::TransparentHugePagePolicy,
transport: AgentTransportProfile,
) -> String {
let mut cmdline = thp_kernel_cmdline(thp);
if transport.offers_dual_port() {
cmdline.push(' ');
cmdline.push_str(microsandbox_protocol::AGENT_TRANSPORT_DUAL_PORT_CMDLINE);
}
cmdline
}
#[cfg(test)]
mod tests {
#[cfg(feature = "net")]
use super::to_krun_network_rate_limiters;
use super::{
AGENT_BULK_QUEUE_SIZE, AGENT_CONTROL_QUEUE_SIZE, AgentTransportProfile, ConsoleSharedState,
HostPermissions, StatVirtualization, agent_kernel_cmdline, agent_primary_queue_size,
append_block_root_env, bind_rootfs_backend, encode_bootstrap_frame,
guest_shutdown_flush_timeout, guest_shutdown_flush_timeout_with_override, parse_mount_spec,
prepend_scripts_path, request_guest_shutdown, request_guest_shutdown_with_timeout,
thp_kernel_cmdline, validate_disk_format,
};
#[cfg(unix)]
use super::{
BindIdentityMapRegistration, PARENT_WATCH_DETACH, ParentWatchdogSignal,
bind_identity_map_for_mount, bootstrap_trampoline_backend, read_parent_watchdog_signal,
};
use super::{
DiskMountSpec, UpperLayerSpec, UpperSpec, prepare_runtime_restore_namespace,
recover_owned_disk_layers, validate_disk_mount_layers, validate_upper_layers,
};
use microsandbox_filesystem::{Context, DynFileSystem, FsOptions};
use microsandbox_protocol::{bootstrap::GuestBootstrap, codec, message::MessageType};
#[cfg(unix)]
use std::io::Write;
#[cfg(unix)]
use std::sync::Arc;
use std::time::Duration;
fn fs_context() -> Context {
Context {
uid: 0,
gid: 0,
pid: 1,
}
}
fn owned_disk_spec(directory: &std::path::Path, readonly: bool) -> DiskMountSpec {
DiskMountSpec {
id: microsandbox_types::owned_volume_mount_id("/data"),
host: directory.join("disk.raw"),
layers: Vec::new(),
guest: "/data".into(),
format: msb_krun::DiskImageFormat::Raw,
fstype: Some("ext4".into()),
readonly,
snapshot_owned: true,
lifecycle_owned: true,
}
}
#[test]
fn owned_disk_attachment_uses_explicit_layers_without_the_nominal_raw_file() {
let temp = tempfile::tempdir().unwrap();
let mut disk = owned_disk_spec(temp.path(), true);
for (name, format) in [
("sealed.raw", msb_krun::DiskImageFormat::Raw),
("head.qcow2", msb_krun::DiskImageFormat::Qcow2),
] {
let path = temp.path().join(name);
std::fs::write(&path, b"attachment path fixture").unwrap();
disk.layers.push(UpperLayerSpec { path, format });
}
assert!(!disk.host.exists());
assert_eq!(
validate_disk_mount_layers(&disk).unwrap(),
Some(disk.layers.clone())
);
assert!(disk.readonly);
disk.lifecycle_owned = false;
assert!(validate_disk_mount_layers(&disk).is_err());
disk.lifecycle_owned = true;
std::fs::remove_file(&disk.layers[0].path).unwrap();
assert!(validate_disk_mount_layers(&disk).is_err());
}
#[test]
fn owned_disk_start_recovers_journal_without_changing_readonly_or_falling_back() {
for readonly in [false, true] {
let home = tempfile::tempdir().unwrap();
let runtime = home.path().join("runtime");
let id = microsandbox_types::owned_volume_mount_id("/data");
let directory = home.path().join("owned-volumes").join(&id);
std::fs::create_dir_all(&directory).unwrap();
let base = directory.join("sealed.raw");
let head = directory.join("head.qcow2");
std::fs::write(&base, vec![37; 1024 * 1024]).unwrap();
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(microsandbox_image::checkpoint::create_qcow2_overlay(
&head,
1024 * 1024,
&base,
"raw",
))
.unwrap();
crate::checkpoint::seed_runtime_owned_disk_chain(
&runtime,
&id,
&directory.join("disk.raw"),
&[
crate::checkpoint::RuntimeOwnedRootLayer {
path: base.clone(),
format: "raw".into(),
},
crate::checkpoint::RuntimeOwnedRootLayer {
path: head.clone(),
format: "qcow2".into(),
},
],
readonly,
)
.unwrap();
let mut disks = vec![owned_disk_spec(&directory, readonly)];
recover_owned_disk_layers(&runtime, &mut disks).unwrap();
assert_eq!(disks[0].readonly, readonly);
assert_eq!(disks[0].layers.len(), 2);
assert_eq!(disks[0].layers[0].path, base);
assert_eq!(disks[0].layers[1].path, head);
assert!(!disks[0].host.exists());
std::fs::write(&disks[0].host, vec![99; 1024 * 1024]).unwrap();
std::fs::remove_file(&base).unwrap();
assert!(recover_owned_disk_layers(&runtime, &mut disks).is_err());
let mut external = owned_disk_spec(&directory, readonly);
external.lifecycle_owned = false;
recover_owned_disk_layers(&runtime, std::slice::from_mut(&mut external)).unwrap();
assert!(external.layers.is_empty());
}
}
#[test]
fn upper_chain_accepts_one_raw_base_followed_by_qcow2_heads() {
let spec = UpperSpec {
layers: vec![
UpperLayerSpec {
path: "upper.ext4".into(),
format: msb_krun::DiskImageFormat::Raw,
},
UpperLayerSpec {
path: "upper-2.qcow2".into(),
format: msb_krun::DiskImageFormat::Qcow2,
},
UpperLayerSpec {
path: "upper-3.qcow2".into(),
format: msb_krun::DiskImageFormat::Qcow2,
},
],
read_only: false,
};
assert_eq!(validate_upper_layers(&spec).unwrap(), spec.layers);
}
#[test]
fn upper_chain_rejects_raw_successors_and_repeated_paths() {
let raw_successor = UpperSpec {
layers: vec![
UpperLayerSpec {
path: "upper.ext4".into(),
format: msb_krun::DiskImageFormat::Raw,
},
UpperLayerSpec {
path: "upper-next.ext4".into(),
format: msb_krun::DiskImageFormat::Raw,
},
],
read_only: false,
};
assert!(validate_upper_layers(&raw_successor).is_err());
let repeated = UpperSpec {
layers: vec![
UpperLayerSpec {
path: "upper.ext4".into(),
format: msb_krun::DiskImageFormat::Raw,
},
UpperLayerSpec {
path: "upper.ext4".into(),
format: msb_krun::DiskImageFormat::Qcow2,
},
],
read_only: false,
};
assert!(validate_upper_layers(&repeated).is_err());
}
#[cfg(feature = "net")]
#[test]
fn network_rate_limiters_map_directions_without_losing_precision() {
let egress = microsandbox_types::RateLimiterConfig {
bandwidth: Some(microsandbox_types::TokenBucketConfig {
size: 1_048_576,
refill_time_ms: 1_234,
one_time_burst: 524_288,
}),
ops: Some(microsandbox_types::TokenBucketConfig {
size: 1_000,
refill_time_ms: 7,
one_time_burst: 12,
}),
};
let ingress = microsandbox_types::RateLimiterConfig {
bandwidth: Some(microsandbox_types::TokenBucketConfig {
size: 2_048,
refill_time_ms: 99,
one_time_burst: 256,
}),
ops: None,
};
let config = microsandbox_network::config::NetworkConfig {
rate_limiter: Some(microsandbox_types::NetworkRateLimiterConfig {
egress: Some(egress),
ingress: Some(ingress),
}),
..Default::default()
};
let mapped = to_krun_network_rate_limiters(&config);
assert_eq!(
mapped.tx.as_ref().unwrap().bandwidth.as_ref().unwrap(),
&msb_krun::TokenBucketConfig {
size: 1_048_576,
refill_time: Duration::from_millis(1_234),
one_time_burst: 524_288,
}
);
assert_eq!(
mapped.tx.unwrap().ops.unwrap(),
msb_krun::TokenBucketConfig {
size: 1_000,
refill_time: Duration::from_millis(7),
one_time_burst: 12,
}
);
assert_eq!(
mapped.rx.unwrap().bandwidth.unwrap(),
msb_krun::TokenBucketConfig {
size: 2_048,
refill_time: Duration::from_millis(99),
one_time_burst: 256,
}
);
}
#[test]
fn transparent_huge_page_policy_maps_to_kernel_boot_parameter() {
use microsandbox_types::TransparentHugePagePolicy;
assert_eq!(
thp_kernel_cmdline(TransparentHugePagePolicy::Always),
"transparent_hugepage=always"
);
assert_eq!(
thp_kernel_cmdline(TransparentHugePagePolicy::Madvise),
"transparent_hugepage=madvise"
);
assert_eq!(
thp_kernel_cmdline(TransparentHugePagePolicy::Never),
"transparent_hugepage=never"
);
}
#[test]
fn agent_transport_auto_offers_dual_port_with_combined_escape_hatch() {
use microsandbox_types::TransparentHugePagePolicy;
assert_eq!(
AgentTransportProfile::default(),
AgentTransportProfile::Auto
);
assert!(AgentTransportProfile::Auto.offers_dual_port());
assert!(AgentTransportProfile::DualPortV1.offers_dual_port());
assert!(!AgentTransportProfile::Combined.offers_dual_port());
let auto = agent_kernel_cmdline(
TransparentHugePagePolicy::Madvise,
AgentTransportProfile::Auto,
);
let forced_combined = agent_kernel_cmdline(
TransparentHugePagePolicy::Madvise,
AgentTransportProfile::Combined,
);
assert!(auto.contains(microsandbox_protocol::AGENT_TRANSPORT_DUAL_PORT_CMDLINE));
assert!(!forced_combined.contains("microsandbox.agent_transport="));
}
#[test]
fn test_bind_rootfs_backend_exposes_host_file_and_init() {
let rootfs = tempfile::tempdir().unwrap();
std::fs::write(rootfs.path().join("host.txt"), b"from host").unwrap();
let fs = bind_rootfs_backend(rootfs.path(), true).unwrap();
fs.init(FsOptions::empty()).unwrap();
let host = fs.lookup(fs_context(), 1, c"host.txt").unwrap();
let init = fs.lookup(fs_context(), 1, c"init.krun").unwrap();
assert_ne!(host.inode, init.inode);
assert_eq!(init.inode, 2);
}
#[cfg(unix)]
#[test]
fn bootstrap_trampoline_recreates_agent_mountpoints() {
let fs = bootstrap_trampoline_backend().unwrap();
fs.init(FsOptions::empty()).unwrap();
for directory in ["dev", "sys", "proc", ".msb", "newroot"] {
fs.lookup(fs_context(), 1, &std::ffi::CString::new(directory).unwrap())
.unwrap();
}
}
#[test]
fn restored_runtime_namespace_matches_root_kind_and_preserves_heartbeat() {
let oci_runtime = tempfile::tempdir().unwrap();
prepare_runtime_restore_namespace(oci_runtime.path(), true).unwrap();
assert!(oci_runtime.path().join("rootfs/lower").is_dir());
assert!(oci_runtime.path().join("rootfs/upperfs").is_dir());
assert_eq!(
std::fs::read(oci_runtime.path().join("heartbeat.json")).unwrap(),
b""
);
std::fs::write(
oci_runtime.path().join("heartbeat.json"),
b"destination heartbeat",
)
.unwrap();
prepare_runtime_restore_namespace(oci_runtime.path(), true).unwrap();
assert_eq!(
std::fs::read(oci_runtime.path().join("heartbeat.json")).unwrap(),
b"destination heartbeat"
);
let disk_runtime = tempfile::tempdir().unwrap();
prepare_runtime_restore_namespace(disk_runtime.path(), false).unwrap();
assert!(!disk_runtime.path().join("rootfs").exists());
assert!(disk_runtime.path().join("heartbeat.json").is_file());
}
#[test]
fn test_parse_mount_spec_minimal() {
let p = parse_mount_spec("foo:/host/data").unwrap();
assert_eq!(p.tag, "foo");
assert_eq!(p.host_path, "/host/data");
assert!(matches!(p.stat_virtualization, StatVirtualization::Strict));
assert!(matches!(p.host_permissions, HostPermissions::Private));
assert!(!p.readonly);
assert_eq!(p.override_uid, None);
assert_eq!(p.override_gid, None);
}
#[test]
fn test_parse_mount_spec_uid_gid() {
let p = parse_mount_spec("home:/host/home:uid=1000,gid=1000").unwrap();
assert_eq!(p.override_uid, Some(1000));
assert_eq!(p.override_gid, Some(1000));
let p = parse_mount_spec("home:/host/home:uid=0,gid=0").unwrap();
assert_eq!(p.override_uid, Some(0));
assert_eq!(p.override_gid, Some(0));
}
#[test]
fn test_parse_mount_spec_uid_gid_must_be_paired() {
assert!(parse_mount_spec("home:/host/home:uid=1000").is_err());
assert!(parse_mount_spec("home:/host/home:gid=1000").is_err());
}
#[test]
fn test_parse_mount_spec_uid_gid_reject_stat_virt_off() {
let err = parse_mount_spec("home:/host/home:uid=1000,gid=1000,stat-virt=off").unwrap_err();
assert!(
err.contains("cannot be combined with stat-virt=off"),
"{err}"
);
}
#[test]
fn test_parse_mount_spec_rejects_invalid_uid() {
assert!(parse_mount_spec("home:/host/home:uid=abc,gid=1000").is_err());
}
#[test]
fn test_parse_mount_spec_with_ro_and_policies() {
let p = parse_mount_spec("foo:/host/data:ro,noexec,stat-virt=relaxed,host-perms=mirror")
.unwrap();
assert_eq!(p.host_path, "/host/data");
assert!(matches!(p.stat_virtualization, StatVirtualization::Relaxed));
assert!(matches!(p.host_permissions, HostPermissions::Mirror));
assert!(p.readonly);
}
#[test]
fn test_parse_mount_spec_stat_virt_off() {
let p = parse_mount_spec("foo:/host/data:stat-virt=off").unwrap();
assert!(matches!(p.stat_virtualization, StatVirtualization::Off));
assert!(!p.readonly);
}
#[test]
fn test_parse_mount_spec_follow_root_symlinks_default_protected() {
let p = parse_mount_spec("foo:/host/data").unwrap();
assert!(!p.follow_root_symlinks);
}
#[test]
fn test_parse_mount_spec_follow_root_symlinks_opt_out() {
let p = parse_mount_spec("foo:/host/data:follow-root-symlinks").unwrap();
assert!(p.follow_root_symlinks);
let p = parse_mount_spec("foo:/host/data:ro,follow-root-symlinks,stat-virt=off").unwrap();
assert!(p.follow_root_symlinks);
assert!(p.readonly);
assert!(matches!(p.stat_virtualization, StatVirtualization::Off));
}
#[test]
fn test_parse_mount_spec_rejects_duplicate_follow_root_symlinks() {
let err = parse_mount_spec("foo:/host/data:follow-root-symlinks,follow-root-symlinks")
.unwrap_err();
assert!(err.contains("follow-root-symlinks"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_quota_in_mib() {
let p = parse_mount_spec("foo:/host/data:quota=2048").unwrap();
assert_eq!(p.quota_bytes, Some(2048 * 1024 * 1024));
}
#[test]
fn test_parse_mount_spec_quota_default_none() {
let p = parse_mount_spec("foo:/host/data:ro").unwrap();
assert_eq!(p.quota_bytes, None);
}
#[test]
fn test_parse_mount_spec_rejects_duplicate_quota() {
let err = parse_mount_spec("foo:/host/data:quota=1,quota=2").unwrap_err();
assert!(
err.contains("`quota` specified more than once"),
"got: {err}"
);
}
#[test]
fn test_parse_mount_spec_rejects_non_numeric_quota() {
let err = parse_mount_spec("foo:/host/data:quota=big").unwrap_err();
assert!(err.contains("invalid quota"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_unknown_key() {
let err = parse_mount_spec("foo:/host/data:bogus=1").unwrap_err();
assert!(err.contains("unknown mount option"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_invalid_stat_virt() {
let err = parse_mount_spec("foo:/host/data:stat-virt=nope").unwrap_err();
assert!(err.contains("invalid stat-virt"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_invalid_host_perms() {
let err = parse_mount_spec("foo:/host/data:host-perms=public").unwrap_err();
assert!(err.contains("invalid host-perms"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_missing_colon_errors() {
let err = parse_mount_spec("nopath").unwrap_err();
assert!(err.contains("expected tag:host_path"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_empty_tag_errors() {
let err = parse_mount_spec(":/host").unwrap_err();
assert!(err.contains("empty tag"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_with_flags_before_policies() {
let p = parse_mount_spec("foo:/host/data:ro,nosuid,stat-virt=relaxed").unwrap();
assert_eq!(p.host_path, "/host/data");
assert!(matches!(p.stat_virtualization, StatVirtualization::Relaxed));
}
#[test]
fn test_parse_mount_spec_rejects_duplicate_stat_virt() {
let err = parse_mount_spec("foo:/host:stat-virt=strict,stat-virt=off").unwrap_err();
assert!(err.contains("more than once"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_legacy_comma_options() {
let err = parse_mount_spec("foo:/host/data,stat-virt=off").unwrap_err();
assert!(err.contains("tag:host_path:opts"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_duplicate_flags() {
let err = parse_mount_spec("foo:/host:ro,rw").unwrap_err();
assert!(err.contains("ro`/`rw"), "got: {err}");
}
#[test]
fn test_parse_mount_spec_rejects_unsupported_flags() {
let err = parse_mount_spec("foo:/host:exec").unwrap_err();
assert!(err.contains("unsupported mount option"), "got: {err}");
}
#[test]
#[cfg(windows)]
fn test_parse_mount_spec_accepts_windows_drive_path() {
let p = parse_mount_spec(r"work:C:\Users\Stephen\data:ro,host-perms=mirror").unwrap();
assert_eq!(p.tag, "work");
assert_eq!(p.host_path, r"C:\Users\Stephen\data");
assert!(matches!(p.host_permissions, HostPermissions::Mirror));
assert!(p.readonly);
}
#[test]
#[cfg(unix)]
fn test_bind_identity_map_registration_separates_explicit_owners() {
let mut registration = BindIdentityMapRegistration {
handle: None,
mount_count: 0,
};
let first =
bind_identity_map_for_mount(&mut registration, StatVirtualization::Strict, None)
.unwrap();
let second =
bind_identity_map_for_mount(&mut registration, StatVirtualization::Relaxed, None)
.unwrap();
let fixed = bind_identity_map_for_mount(
&mut registration,
StatVirtualization::Relaxed,
Some((1000, 1001)),
)
.unwrap();
let off = bind_identity_map_for_mount(&mut registration, StatVirtualization::Off, None);
assert!(Arc::ptr_eq(&first, &second));
assert!(!Arc::ptr_eq(&first, &fixed));
let fixed = fixed.get().unwrap();
assert_eq!((fixed.guest_uid, fixed.guest_gid), (1000, 1001));
assert_eq!((fixed.overflow_uid, fixed.overflow_gid), (1000, 1001));
assert!(off.is_none());
assert_eq!(registration.mount_count, 2);
}
#[test]
fn test_request_guest_shutdown_enqueues_shutdown_frame() {
let shared = ConsoleSharedState::new();
request_guest_shutdown(&shared).unwrap();
let mut frame = shared.rx_ring.pop().unwrap().to_vec();
let msg = codec::try_decode_from_buf(&mut frame).unwrap().unwrap();
assert_eq!(msg.t, MessageType::Shutdown);
assert_eq!(msg.id, 0);
}
#[test]
fn test_bootstrap_frame_is_a_current_generation_control_message() {
let bootstrap = GuestBootstrap {
default_env: vec![microsandbox_protocol::bootstrap::BootstrapEnvVar {
key: "APP_CONFIG".to_string(),
value: "{\"message\":\"hello\"}".to_string(),
}],
..GuestBootstrap::default()
};
let mut frame = encode_bootstrap_frame(&bootstrap).unwrap();
let message = codec::try_decode_from_buf(&mut frame).unwrap().unwrap();
assert_eq!(message.t, MessageType::Bootstrap);
assert_eq!(message.id, 0);
assert_eq!(message.flags, 0);
assert_eq!(message.v, microsandbox_protocol::message::PROTOCOL_VERSION);
assert_eq!(message.payload::<GuestBootstrap>().unwrap(), bootstrap);
assert!(frame.is_empty());
}
#[test]
fn test_primary_agent_queue_tracks_whether_it_carries_bulk() {
assert_eq!(agent_primary_queue_size(false), AGENT_BULK_QUEUE_SIZE);
assert_eq!(agent_primary_queue_size(true), AGENT_CONTROL_QUEUE_SIZE);
}
#[test]
fn test_guest_shutdown_flush_timeout_tracks_handoff_mode() {
assert_eq!(
guest_shutdown_flush_timeout(false),
microsandbox_protocol::NORMAL_SHUTDOWN_FLUSH_TIMEOUT
);
assert_eq!(
guest_shutdown_flush_timeout(true),
microsandbox_protocol::HANDOFF_SHUTDOWN_FLUSH_TIMEOUT
);
}
#[test]
fn test_guest_shutdown_flush_timeout_accepts_ms_override() {
assert_eq!(
guest_shutdown_flush_timeout_with_override(false, Some("0")),
Duration::ZERO
);
assert_eq!(
guest_shutdown_flush_timeout_with_override(true, Some("125")),
Duration::from_millis(125)
);
}
#[test]
fn test_guest_shutdown_flush_timeout_ignores_invalid_override() {
assert_eq!(
guest_shutdown_flush_timeout_with_override(false, Some("nope")),
microsandbox_protocol::NORMAL_SHUTDOWN_FLUSH_TIMEOUT
);
assert_eq!(
guest_shutdown_flush_timeout_with_override(true, Some("nope")),
microsandbox_protocol::HANDOFF_SHUTDOWN_FLUSH_TIMEOUT
);
}
#[test]
fn test_request_guest_shutdown_with_timeout_fails_when_ring_full() {
let shared = ConsoleSharedState::with_capacity(8);
shared.rx_ring.push(b"occupied".to_vec()).unwrap();
let err = request_guest_shutdown_with_timeout(&shared, Duration::ZERO).unwrap_err();
assert!(
err.to_string()
.contains("timed out sending frame to agentd")
);
}
#[test]
#[cfg(unix)]
fn test_parent_watchdog_signal_reports_parent_exit_on_eof() {
let (read_fd, write_fd) = super::create_pipe().unwrap();
drop(write_fd);
let mut reader = std::fs::File::from(read_fd);
let signal = read_parent_watchdog_signal(&mut reader).unwrap();
assert_eq!(signal, ParentWatchdogSignal::ParentExited);
}
#[test]
#[cfg(unix)]
fn test_parent_watchdog_signal_reports_detach_byte() {
let (read_fd, write_fd) = super::create_pipe().unwrap();
let mut writer = std::fs::File::from(write_fd);
writer.write_all(&[PARENT_WATCH_DETACH]).unwrap();
let mut reader = std::fs::File::from(read_fd);
let signal = read_parent_watchdog_signal(&mut reader).unwrap();
assert_eq!(signal, ParentWatchdogSignal::Detached);
}
#[test]
fn test_validate_disk_format_rejects_unknown_values() {
let err = validate_disk_format(Some("iso")).unwrap_err();
assert!(err.to_string().contains("unknown disk image format"));
}
#[test]
fn test_append_block_root_env_adds_default_device() {
let mut env = vec!["FOO=bar".to_string()];
append_block_root_env(&mut env);
assert!(env.contains(&"FOO=bar".to_string()));
assert!(env.contains(&format!(
"{}=/dev/vda",
microsandbox_protocol::ENV_BLOCK_ROOT
)));
}
#[test]
fn test_append_block_root_env_preserves_existing_value() {
let existing = format!(
"{}=/dev/vdb,fstype=xfs",
microsandbox_protocol::ENV_BLOCK_ROOT
);
let mut env = vec![existing.clone()];
append_block_root_env(&mut env);
assert_eq!(env, vec![existing]);
}
#[test]
fn test_prepend_scripts_path_updates_existing_path() {
let mut env = vec!["PATH=/usr/bin:/bin".to_string()];
prepend_scripts_path(&mut env);
assert_eq!(env, vec!["PATH=/.msb/scripts:/usr/bin:/bin".to_string()]);
}
#[test]
fn test_prepend_scripts_path_adds_default_path_when_missing() {
let mut env = vec!["LANG=C.UTF-8".to_string()];
prepend_scripts_path(&mut env);
assert!(
env.contains(
&"PATH=/.msb/scripts:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
.to_string()
)
);
}
#[test]
fn test_prepend_scripts_path_avoids_duplicates() {
let mut env = vec!["PATH=/.msb/scripts:/usr/bin".to_string()];
prepend_scripts_path(&mut env);
assert_eq!(env, vec!["PATH=/.msb/scripts:/usr/bin".to_string()]);
}
}