use std::path::PathBuf;
use microsandbox_protocol::bootstrap::GuestBootstrap;
use microsandbox_types::{CpuPlacement, PlacementProfile, VsockRouteSpec};
use serde::{Deserialize, Serialize};
use microsandbox_types::TransparentHugePagePolicy;
#[cfg(feature = "net")]
use microsandbox_network::ResolvedNetworkConfig;
#[cfg(feature = "net")]
use microsandbox_types::DeploymentProfile;
pub const CONFIG_FD: i32 = 96;
pub const BRANCH_MEMORY_FD: i32 = 95;
pub const PARENT_WATCH_FD: i32 = 97;
pub const STARTUP_FD: i32 = 98;
#[cfg(unix)]
pub const LIFECYCLE_LOCK_FD: i32 = 99;
pub const PARENT_WATCH_DETACH: u8 = 1;
mod compatibility;
#[cfg(test)]
#[path = "launch/tests.rs"]
mod compatibility_tests;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct MetricsSlotHandoff {
pub shm_name: String,
pub slot: u32,
pub generation: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StartupCommand {
pub cmd: String,
pub args: Vec<String>,
pub env: Vec<String>,
pub cwd: Option<String>,
pub user: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LaunchConfig {
pub execution: ExecutionIntent,
pub db_path: PathBuf,
pub db_connect_timeout_secs: u64,
pub log_dir: PathBuf,
pub runtime_dir: PathBuf,
pub sandboxes_dir: PathBuf,
#[serde(default)]
pub run_dir: PathBuf,
pub cpu_lease_dir: PathBuf,
pub writeback_lease_dir: PathBuf,
pub cpu_placement: CpuPlacement,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placement_profile_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placement_profile: Option<PlacementProfile>,
pub agent_sock: PathBuf,
pub libkrunfw_path: PathBuf,
#[serde(default)]
pub thp: TransparentHugePagePolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_cache_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_writeback_limit_bytes: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_writeback_pool_bytes: Option<u64>,
pub startup: Option<StartupCommand>,
pub lifecycle: Lifecycle,
pub metrics: MetricsConfig,
pub rootfs: RootfsConfig,
pub mounts: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub owned_volumes: Vec<microsandbox_types::VolumeMount>,
#[serde(default)]
pub file_mounts: Vec<FileMountConfig>,
pub disks: Vec<String>,
pub init_path: Option<PathBuf>,
pub bootstrap: GuestBootstrap,
pub exec_path: Option<PathBuf>,
pub exec_args: Vec<String>,
#[cfg(feature = "net")]
pub network: Option<ResolvedNetworkConfig>,
#[cfg(feature = "net")]
#[serde(default)]
pub deployment_profile: DeploymentProfile,
#[cfg(feature = "net")]
pub sandbox_slot: u16,
#[serde(default)]
pub vsock: Vec<VsockRouteSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint_restore: Option<CheckpointRestoreConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CheckpointRestoreConfig {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub memory_descriptor: bool,
#[serde(default)]
pub network_gateway_mac: Option<[u8; 6]>,
#[serde(default)]
pub external_mount_policy: microsandbox_types::ExternalMountRestorePolicy,
#[serde(default)]
pub external_mounts: Vec<ExternalMountRestoreBinding>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub unavailable_disks: std::collections::BTreeMap<String, String>,
pub local_branch: bool,
pub forked: bool,
pub closure: PathBuf,
pub checkpoint_root: String,
pub checkpoint_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExternalMountRestoreBinding {
pub device_id: String,
pub mount: microsandbox_protocol::bootstrap::BootstrapDirMount,
#[serde(default)]
pub filename: Option<String>,
pub remapped: bool,
pub unavailable: bool,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionIntent {
#[default]
Boot,
Restore,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Lifecycle {
pub max_duration_secs: Option<u64>,
pub idle_timeout_secs: Option<u64>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct MetricsConfig {
pub sample_interval_ms: u64,
pub disabled: bool,
pub slot: Option<MetricsSlotHandoff>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct RootfsConfig {
pub path: Option<PathBuf>,
#[serde(default)]
pub follow_root_symlinks: bool,
pub disk: Option<PathBuf>,
pub disk_format: Option<String>,
pub disk_readonly: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub disk_layers: Vec<RootfsUpperLayerConfig>,
#[serde(default)]
pub disk_runtime_owned: bool,
pub upper: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upper_format: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub upper_layers: Vec<RootfsUpperLayerConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RootfsUpperLayerConfig {
pub path: PathBuf,
pub format: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMountConfig {
pub mount: String,
pub filename: String,
}
impl LaunchConfig {
pub fn decode(bytes: &[u8]) -> Result<Self, String> {
let config: Self = serde_json::from_slice(bytes)
.map_err(|error| format!("invalid launch config: {error}"))?;
match (config.execution, config.checkpoint_restore.as_ref()) {
(ExecutionIntent::Boot, None) => {}
(ExecutionIntent::Restore, Some(restore)) => {
if restore.closure.as_os_str().is_empty()
|| (!restore.local_branch && restore.checkpoint_root.is_empty())
|| restore.checkpoint_id.is_empty()
{
return Err("restore requires a complete checkpoint source".into());
}
if restore.local_branch && (!restore.forked || !restore.checkpoint_root.is_empty())
{
return Err(
"local branch requires private memory and no durable checkpoint root"
.into(),
);
}
if config.startup.is_some() {
return Err("restore cannot execute a fresh startup command".into());
}
if restore.memory_descriptor
&& (!restore.local_branch || !cfg!(target_os = "linux"))
{
return Err("memory descriptor requires a Linux local branch".into());
}
}
_ => return Err("execution intent and checkpoint restore source disagree".into()),
}
Ok(config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn memory_descriptor_cannot_be_used_as_a_durable_or_cold_restore() {
let mut value = restore_request();
value["checkpoint_restore"]["memory_descriptor"] = true.into();
assert!(decode(value.clone()).is_err());
value["checkpoint_restore"]["local_branch"] = true.into();
value["checkpoint_restore"]["checkpoint_root"] = "".into();
assert_eq!(decode(value.clone()).is_ok(), cfg!(target_os = "linux"));
value["execution"] = "boot".into();
assert!(decode(value).is_err());
}
fn restore_request() -> serde_json::Value {
serde_json::to_value(LaunchConfig {
execution: ExecutionIntent::Restore,
checkpoint_restore: Some(CheckpointRestoreConfig {
memory_descriptor: false,
network_gateway_mac: None,
external_mount_policy: Default::default(),
external_mounts: Vec::new(),
unavailable_disks: Default::default(),
local_branch: false,
forked: true,
closure: "/owned/child/restore".into(),
checkpoint_root: "blake3:captured-root".into(),
checkpoint_id: "captured".into(),
}),
..Default::default()
})
.unwrap()
}
fn decode(value: serde_json::Value) -> Result<LaunchConfig, String> {
LaunchConfig::decode(&serde_json::to_vec(&value).unwrap())
}
#[test]
fn matching_boot_and_restore_intents_are_accepted() {
assert!(decode(serde_json::to_value(LaunchConfig::default()).unwrap()).is_ok());
let restored = decode(restore_request()).unwrap();
assert!(restored.checkpoint_restore.unwrap().forked);
}
#[test]
fn unsupported_or_missing_restore_never_becomes_boot() {
for mutation in [
"missing",
"null",
"unknown_outer",
"unknown_nested",
"unknown_intent",
"boot",
] {
let mut request = restore_request();
match mutation {
"missing" => {
request
.as_object_mut()
.unwrap()
.remove("checkpoint_restore");
}
"null" => request["checkpoint_restore"] = serde_json::Value::Null,
"unknown_outer" => request["branch_restore"] = serde_json::json!({}),
"unknown_nested" => request["checkpoint_restore"]["unsupported"] = true.into(),
"unknown_intent" => request["execution"] = "future_restore".into(),
"boot" => request["execution"] = "boot".into(),
_ => unreachable!(),
}
assert!(decode(request).is_err(), "{mutation}");
}
}
#[test]
fn launch_requires_explicit_intent_and_memory_policy() {
let mut request = restore_request();
request.as_object_mut().unwrap().remove("execution");
assert!(decode(request).is_err());
let mut request = restore_request();
request["checkpoint_restore"]
.as_object_mut()
.unwrap()
.remove("forked");
assert!(decode(request).is_err());
}
#[test]
fn local_branch_requires_restore_and_private_memory_without_a_fake_root() {
let mut request = restore_request();
request["checkpoint_restore"]["local_branch"] = true.into();
assert!(decode(request.clone()).is_err());
request["checkpoint_restore"]["checkpoint_root"] = "".into();
assert!(decode(request.clone()).is_ok());
request["checkpoint_restore"]["forked"] = false.into();
assert!(decode(request.clone()).is_err());
request["checkpoint_restore"]["forked"] = true.into();
request["execution"] = "boot".into();
assert!(decode(request).is_err());
}
#[test]
fn isolated_file_mount_survives_the_client_runner_handoff() {
let config = LaunchConfig {
file_mounts: vec![FileMountConfig {
mount: "config:/host/secret.txt:ro,uid=1000,gid=1000".into(),
filename: "secret.txt".into(),
}],
..Default::default()
};
let encoded = serde_json::to_value(&config).unwrap();
let decoded: LaunchConfig = serde_json::from_value(encoded.clone()).unwrap();
assert_eq!(decoded.file_mounts[0].mount, config.file_mounts[0].mount);
assert_eq!(decoded.file_mounts[0].filename, "secret.txt");
assert!(decoded.mounts.is_empty());
let mut without_files = encoded;
without_files.as_object_mut().unwrap().remove("file_mounts");
let decoded: LaunchConfig = serde_json::from_value(without_files).unwrap();
assert!(decoded.file_mounts.is_empty());
}
#[cfg(feature = "net")]
#[test]
fn network_slot_handoff_rejects_out_of_range_values() {
let config = LaunchConfig {
sandbox_slot: u16::MAX,
..Default::default()
};
let mut encoded = serde_json::to_value(&config).unwrap();
let decoded: LaunchConfig = serde_json::from_value(encoded.clone()).unwrap();
assert_eq!(decoded.sandbox_slot, u16::MAX);
encoded["sandbox_slot"] = serde_json::json!(u32::from(u16::MAX) + 1);
assert!(serde_json::from_value::<LaunchConfig>(encoded).is_err());
}
}
impl LaunchConfig {
pub fn from_json(bytes: &[u8]) -> Result<Self, String> {
let config = compatibility::decode(bytes)?;
Self::decode(&serde_json::to_vec(&config).map_err(|e| e.to_string())?)
}
}