use std::collections::VecDeque;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::process::ChildStdin;
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use crate::assets::AssetPaths;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::pool::{PoolClientError, WarmPool, WarmPoolError};
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::resources::VmResources;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
use crate::vmm::runner::RunOptions;
#[non_exhaustive]
pub enum Error {
Image {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Vm {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Assets {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
Io(std::io::Error),
Network {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
CacheMiss { msg: String },
CacheInvalid { msg: String },
Bake {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
ImageNotFound {
image: String,
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
RegistryAuth {
image: String,
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
RegistryUnreachable {
msg: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
PoolExhausted { msg: String },
KernelPanic {
first_line: String,
stack: Vec<String>,
},
}
impl Error {
pub(crate) fn image_msg(msg: impl Into<String>) -> Self {
Error::Image {
msg: msg.into(),
source: None,
}
}
pub(crate) fn vm_msg(msg: impl Into<String>) -> Self {
Error::Vm {
msg: msg.into(),
source: None,
}
}
pub(crate) fn assets_msg(msg: impl Into<String>) -> Self {
Error::Assets {
msg: msg.into(),
source: None,
}
}
pub(crate) fn network_msg(msg: impl Into<String>) -> Self {
Error::Network {
msg: msg.into(),
source: None,
}
}
pub(crate) fn bake_msg(msg: impl Into<String>) -> Self {
Error::Bake {
msg: msg.into(),
source: None,
}
}
pub(crate) fn cache_miss(msg: impl Into<String>) -> Self {
Error::CacheMiss { msg: msg.into() }
}
pub(crate) fn cache_invalid(msg: impl Into<String>) -> Self {
Error::CacheInvalid { msg: msg.into() }
}
pub(crate) fn image_not_found(image: impl Into<String>, msg: impl Into<String>) -> Self {
Error::ImageNotFound {
image: image.into(),
msg: msg.into(),
source: None,
}
}
pub(crate) fn registry_auth(image: impl Into<String>, msg: impl Into<String>) -> Self {
Error::RegistryAuth {
image: image.into(),
msg: msg.into(),
source: None,
}
}
pub(crate) fn registry_unreachable(msg: impl Into<String>) -> Self {
Error::RegistryUnreachable {
msg: msg.into(),
source: None,
}
}
pub(crate) fn pool_exhausted(msg: impl Into<String>) -> Self {
Error::PoolExhausted { msg: msg.into() }
}
pub(crate) fn kernel_panic(first_line: impl Into<String>, stack: Vec<String>) -> Self {
Error::KernelPanic {
first_line: first_line.into(),
stack,
}
}
pub(crate) fn is_likely_multi_vcpu_restore_stall(&self) -> bool {
let Error::Vm { msg, .. } = self else {
return false;
};
msg.contains("Resource temporarily unavailable")
|| msg.contains("os error 35")
|| msg.contains("os error 60") || msg.contains("timed out")
|| (msg.contains("probe failed") && !msg.contains("speaks protocol v"))
}
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Image { msg, source } => f
.debug_struct("Image")
.field("msg", msg)
.field("source", source)
.finish(),
Error::Vm { msg, source } => f
.debug_struct("Vm")
.field("msg", msg)
.field("source", source)
.finish(),
Error::Assets { msg, source } => f
.debug_struct("Assets")
.field("msg", msg)
.field("source", source)
.finish(),
Error::Io(e) => f.debug_tuple("Io").field(e).finish(),
Error::Network { msg, source } => f
.debug_struct("Network")
.field("msg", msg)
.field("source", source)
.finish(),
Error::CacheMiss { msg } => f.debug_struct("CacheMiss").field("msg", msg).finish(),
Error::CacheInvalid { msg } => {
f.debug_struct("CacheInvalid").field("msg", msg).finish()
}
Error::Bake { msg, source } => f
.debug_struct("Bake")
.field("msg", msg)
.field("source", source)
.finish(),
Error::ImageNotFound { image, msg, source } => f
.debug_struct("ImageNotFound")
.field("image", image)
.field("msg", msg)
.field("source", source)
.finish(),
Error::RegistryAuth { image, msg, source } => f
.debug_struct("RegistryAuth")
.field("image", image)
.field("msg", msg)
.field("source", source)
.finish(),
Error::RegistryUnreachable { msg, source } => f
.debug_struct("RegistryUnreachable")
.field("msg", msg)
.field("source", source)
.finish(),
Error::PoolExhausted { msg } => {
f.debug_struct("PoolExhausted").field("msg", msg).finish()
}
Error::KernelPanic { first_line, stack } => f
.debug_struct("KernelPanic")
.field("first_line", first_line)
.field("stack_lines", &stack.len())
.finish(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::Image { msg, .. } => write!(f, "image: {msg}"),
Error::Vm { msg, .. } => write!(f, "vm: {msg}"),
Error::Assets { msg, .. } => write!(f, "assets: {msg}"),
Error::Io(e) => write!(f, "io: {e}"),
Error::Network { msg, .. } => write!(f, "network: {msg}"),
Error::CacheMiss { msg } => write!(f, "cache miss: {msg}"),
Error::CacheInvalid { msg } => write!(f, "cache invalid: {msg}"),
Error::Bake { msg, .. } => write!(f, "bake: {msg}"),
Error::ImageNotFound { image, msg, .. } => {
write!(f, "image not found ({image}): {msg}")
}
Error::RegistryAuth { image, msg, .. } => {
write!(f, "registry auth failed for {image}: {msg}")
}
Error::RegistryUnreachable { msg, .. } => {
write!(f, "registry unreachable: {msg}")
}
Error::PoolExhausted { msg } => write!(f, "pool exhausted: {msg}"),
Error::KernelPanic { first_line, stack } => {
writeln!(f, "kernel panic during bake: {first_line}")?;
for line in stack {
writeln!(f, " {line}")?;
}
Ok(())
}
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Image { source, .. }
| Error::Vm { source, .. }
| Error::Assets { source, .. }
| Error::Network { source, .. }
| Error::Bake { source, .. }
| Error::ImageNotFound { source, .. }
| Error::RegistryAuth { source, .. }
| Error::RegistryUnreachable { source, .. } => source
.as_ref()
.map(|s| s.as_ref() as &(dyn std::error::Error + 'static)),
Error::Io(e) => Some(e),
Error::CacheMiss { .. }
| Error::CacheInvalid { .. }
| Error::PoolExhausted { .. }
| Error::KernelPanic { .. } => None,
}
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
impl From<WarmPoolError> for Error {
fn from(e: WarmPoolError) -> Self {
Error::Vm {
msg: e.to_string(),
source: Some(Box::new(e)),
}
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
impl From<PoolClientError> for Error {
fn from(e: PoolClientError) -> Self {
Error::Vm {
msg: e.to_string(),
source: Some(Box::new(e)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PullPolicy {
Always,
Missing,
Never,
}
impl Default for PullPolicy {
fn default() -> Self {
Self::Missing
}
}
impl PullPolicy {
fn as_bake_str(self) -> &'static str {
match self {
Self::Always => "always",
Self::Missing => "missing",
Self::Never => "never",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct KvmImageParts {
pub(crate) kernel: Option<PathBuf>,
pub(crate) initrd: Option<PathBuf>,
pub(crate) disk: Option<PathBuf>,
pub(crate) snapshot: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub(crate) struct SentryImageParts {
pub(crate) rootfs: PathBuf,
pub(crate) has_workload: bool,
pub(crate) user: Option<(u32, u32)>,
pub(crate) image_env: Vec<(String, String)>,
pub(crate) cmd: Vec<String>,
pub(crate) warm_daemon: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct RunscImageParts {
pub(crate) rootfs: PathBuf,
pub(crate) memory_mib: u32,
pub(crate) vcpus: u32,
pub(crate) has_workload: bool,
pub(crate) user: Option<(u32, u32)>,
pub(crate) image_env: Vec<(String, String)>,
pub(crate) cmd: Vec<String>,
pub(crate) warm_daemon: bool,
pub(crate) checkpoint: Option<PathBuf>,
pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
pub(crate) agent_path: String,
pub(crate) agent_unix_env: String,
}
#[derive(Debug, Clone)]
pub(crate) struct KboxlikeImageParts {
pub(crate) rootfs: PathBuf,
pub(crate) memory_mib: u32,
pub(crate) vcpus: u32,
pub(crate) has_workload: bool,
pub(crate) user: Option<(u32, u32)>,
pub(crate) image_env: Vec<(String, String)>,
pub(crate) cmd: Vec<String>,
pub(crate) warm_daemon: bool,
pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
pub(crate) volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct RunscRuntime {
runsc_bin: PathBuf,
runsc_root: PathBuf,
network: String,
build_netns: Option<RunscBuildNetwork>,
container_id: String,
bundle_dir: PathBuf,
rootfs: PathBuf,
parts: RunscImageParts,
file_agent_exec: bool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
struct RunscPortForwardTarget {
runsc_bin: PathBuf,
runsc_root: PathBuf,
network: String,
container_id: String,
agent_path: String,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct RunscPoolInner {
parts: RunscImageParts,
vm_config: VmConfig,
min: usize,
max: usize,
warm_spares: usize,
runsc_bin: PathBuf,
runsc_root: PathBuf,
network: String,
build_netns: Option<RunscBuildNetwork>,
pool_dir: PathBuf,
base_bundle_dir: PathBuf,
base_rootfs: PathBuf,
checkpoint_dir: Option<PathBuf>,
admission: Admission,
checked_out: std::sync::atomic::AtomicUsize,
idle: Mutex<Vec<Vm>>,
restore_on_release: bool,
shut_down: std::sync::atomic::AtomicBool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct KboxlikePoolInner {
parts: KboxlikeImageParts,
vm_config: VmConfig,
min: usize,
admission: Admission,
idle: Mutex<Vec<Vm>>,
restore_on_release: bool,
refilling: std::sync::atomic::AtomicBool,
shut_down: std::sync::atomic::AtomicBool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct RunscBuildNetwork {
name: String,
host_veth: String,
subnet: String,
iface: String,
old_ip_forward: Option<String>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscRuntime {
fn cleanup(&self) {
let mut kill = Command::new(&self.runsc_bin);
crate::exec::add_runsc_global_flags_with_network(
&mut kill,
&self.runsc_root,
&self.network,
);
let _ = kill
.arg("kill")
.arg(&self.container_id)
.arg("KILL")
.status();
let mut delete = Command::new(&self.runsc_bin);
crate::exec::add_runsc_global_flags_with_network(
&mut delete,
&self.runsc_root,
&self.network,
);
let _ = delete
.arg("delete")
.arg("-force")
.arg(&self.container_id)
.status();
let _ = Command::new("umount").arg("-l").arg(&self.rootfs).status();
if let Some(dir) = self.bundle_dir.parent() {
let _ = std::fs::remove_dir_all(dir);
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscRuntime {
fn drop(&mut self) {
self.cleanup();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Debug)]
struct KboxlikeRuntime {
runtime_dir: PathBuf,
source_root_host_pid: Option<i32>,
restored_host_pids: Vec<i32>,
rootfs: PathBuf,
parts: KboxlikeImageParts,
mounts: Vec<(PathBuf, String)>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Clone, Debug)]
struct KboxlikePortForwardTarget {
restored_host_pids: Vec<i32>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikeRuntime {
fn cleanup(&mut self) {
if let Some(pid) = self.source_root_host_pid.take() {
crate::kboxlike::kill_and_reap_live_process_group(pid);
}
let mut pending: std::collections::BTreeSet<i32> =
self.restored_host_pids.drain(..).collect();
for pid in pending.iter().copied() {
unsafe {
libc::kill(pid, libc::SIGKILL);
}
}
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
while !pending.is_empty() && std::time::Instant::now() <= deadline {
let mut status = 0;
let waited = unsafe { libc::waitpid(-1, &mut status, libc::__WALL | libc::WNOHANG) };
if waited > 0 {
pending.remove(&waited);
} else if waited == 0 {
std::thread::sleep(std::time::Duration::from_millis(1));
} else {
break;
}
}
let _ = std::fs::remove_dir_all(&self.runtime_dir);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for KboxlikeRuntime {
fn drop(&mut self) {
self.cleanup();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_host_exe_path(parts: &KboxlikeImageParts, argv0: Option<&str>) -> String {
let Some(argv0) = argv0.filter(|value| !value.is_empty()) else {
return std::env::current_exe()
.ok()
.map(|path| path.to_string_lossy().into_owned())
.unwrap_or_else(|| "supermachine".to_owned());
};
if let Some(guest_abs) = argv0.strip_prefix('/') {
return parts.rootfs.join(guest_abs).to_string_lossy().into_owned();
}
argv0.to_owned()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_timing_enabled() -> bool {
matches!(
std::env::var("SUPERMACHINE_RUNSC_TIMINGS").as_deref(),
Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_timing(label: &str, start: Instant) {
if runsc_timing_enabled() {
eprintln!(
"supermachine(runsc timing): {label} {}ms",
start.elapsed().as_millis()
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_pool_spares(max: usize, restore_on_release: bool) -> usize {
if !restore_on_release {
return 0;
}
if let Ok(raw) = std::env::var("SUPERMACHINE_RUNSC_POOL_SPARES") {
if let Ok(n) = raw.parse::<usize>() {
return n;
}
}
((max + 1) / 2).clamp(1, 4).min(max.max(1))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscBuildNetwork {
fn create() -> Result<Self, Error> {
let iface = runsc_default_host_iface()?;
let suffix = format!("{:x}", unique_suffix());
let short = &suffix[..suffix.len().min(8)];
let name = format!("sm{short}");
let host_veth = format!("vh{short}");
let guest_veth = format!("vg{short}");
let octet = 32 + (std::process::id() % 160);
let subnet = format!("10.231.{octet}.0/24");
let host_ip = format!("10.231.{octet}.1");
let guest_ip = format!("10.231.{octet}.2");
let old_ip_forward = std::fs::read_to_string("/proc/sys/net/ipv4/ip_forward")
.ok()
.map(|s| s.trim().to_owned());
let net = Self {
name,
host_veth,
subnet,
iface,
old_ip_forward,
};
if let Err(e) = net.setup(&guest_veth, &host_ip, &guest_ip) {
net.cleanup();
return Err(e);
}
Ok(net)
}
fn path(&self) -> PathBuf {
PathBuf::from(format!("/run/netns/{}", self.name))
}
fn setup(&self, guest_veth: &str, host_ip: &str, guest_ip: &str) -> Result<(), Error> {
runsc_host_cmd("sysctl", &["-w", "net.ipv4.ip_forward=1"])?;
runsc_host_cmd("ip", &["netns", "add", &self.name])?;
runsc_host_cmd(
"ip",
&[
"link",
"add",
&self.host_veth,
"type",
"veth",
"peer",
"name",
guest_veth,
],
)?;
runsc_host_cmd("ip", &["link", "set", guest_veth, "netns", &self.name])?;
let host_cidr = format!("{host_ip}/24");
runsc_host_cmd("ip", &["addr", "add", &host_cidr, "dev", &self.host_veth])?;
runsc_host_cmd("ip", &["link", "set", &self.host_veth, "up"])?;
let guest_cidr = format!("{guest_ip}/24");
runsc_host_cmd(
"ip",
&[
"netns",
"exec",
&self.name,
"ip",
"addr",
"add",
&guest_cidr,
"dev",
guest_veth,
],
)?;
runsc_host_cmd(
"ip",
&["netns", "exec", &self.name, "ip", "link", "set", "lo", "up"],
)?;
runsc_host_cmd(
"ip",
&[
"netns", "exec", &self.name, "ip", "link", "set", guest_veth, "up",
],
)?;
runsc_host_cmd(
"ip",
&[
"netns", "exec", &self.name, "ip", "route", "add", "default", "via", host_ip,
],
)?;
runsc_host_cmd(
"iptables",
&[
"-t",
"nat",
"-A",
"POSTROUTING",
"-s",
&self.subnet,
"-o",
&self.iface,
"-j",
"MASQUERADE",
],
)
}
fn cleanup(&self) {
let _ = Command::new("iptables")
.args([
"-t",
"nat",
"-D",
"POSTROUTING",
"-s",
&self.subnet,
"-o",
&self.iface,
"-j",
"MASQUERADE",
])
.status();
let _ = Command::new("ip")
.args(["link", "del", &self.host_veth])
.status();
let _ = Command::new("ip")
.args(["netns", "del", &self.name])
.status();
if let Some(old) = &self.old_ip_forward {
let setting = format!("net.ipv4.ip_forward={old}");
let _ = Command::new("sysctl").args(["-w", &setting]).status();
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscBuildNetwork {
fn drop(&mut self) {
self.cleanup();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_host_cmd(program: &str, args: &[&str]) -> Result<(), Error> {
let out = Command::new(program)
.args(args)
.output()
.map_err(|e| Error::vm_msg(format!("{program} {}: {e}", args.join(" "))))?;
if out.status.success() {
Ok(())
} else {
Err(Error::vm_msg(format!(
"{program} {} failed with status {:?}: {}",
args.join(" "),
out.status.code(),
String::from_utf8_lossy(&out.stderr)
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_default_host_iface() -> Result<String, Error> {
let routes = std::fs::read_to_string("/proc/net/route").map_err(Error::Io)?;
for line in routes.lines().skip(1) {
let fields: Vec<_> = line.split_whitespace().collect();
if fields.len() > 1 && fields[1] == "00000000" {
return Ok(fields[0].to_owned());
}
}
Err(Error::vm_msg(
"runsc build network: host default route not found".to_owned(),
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_network_for_config(config: &VmConfig) -> &'static str {
if config.for_build {
crate::exec::RUNSC_NETWORK_SANDBOX
} else {
crate::exec::RUNSC_NETWORK_NONE
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl RunscPoolInner {
fn build(
image: &Image,
config: &VmConfig,
min: usize,
max: usize,
acquire_timeout: Option<Duration>,
restore_on_release: bool,
) -> Result<Arc<Self>, Error> {
let parts = image
.runsc
.as_ref()
.ok_or_else(|| Error::vm_msg("RunscPoolInner::build on a non-runsc image"))?
.clone();
let runsc_bin = runsc_bin_path();
let network = runsc_network_for_config(config).to_owned();
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
let pool_dir = dir.join(format!(
"supermachine-runsc-pool-{}-{}",
std::process::id(),
unique_suffix()
));
let base_bundle_dir = pool_dir.join("base-bundle");
let base_rootfs = base_bundle_dir.join("rootfs");
let runsc_root = pool_dir.join("runsc-root");
std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
let build_netns = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
Some(RunscBuildNetwork::create()?)
} else {
None
};
let netns_path = build_netns.as_ref().map(|net| net.path());
prepare_runsc_bundle(
&runsc_bin,
&parts,
&base_bundle_dir,
&base_rootfs,
&network,
netns_path.as_deref(),
)?;
let checkpoint_dir = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
None
} else {
let checkpoint_dir = pool_dir.join("checkpoint");
std::fs::create_dir_all(&checkpoint_dir).map_err(Error::Io)?;
Some(checkpoint_dir)
};
if let (Some(source_checkpoint), Some(checkpoint_dir)) =
(&parts.checkpoint, checkpoint_dir.as_ref())
{
if let Err(e) = copy_dir_recursive(source_checkpoint, &checkpoint_dir) {
let _ = std::fs::remove_dir_all(&pool_dir);
return Err(Error::vm_msg(format!(
"runsc checkpoint clone {} -> {}: {e}",
source_checkpoint.display(),
checkpoint_dir.display()
)));
}
} else if let Some(checkpoint_dir) = checkpoint_dir.as_ref() {
let warm_id = format!("sm-runsc-warm-{}-{}", std::process::id(), unique_suffix());
let pid_file = pool_dir.join("warm.pid");
if let Err(e) = runsc_run_detached(
&runsc_bin,
&runsc_root,
&network,
&base_bundle_dir,
&pid_file,
&warm_id,
) {
let _ = std::fs::remove_dir_all(&pool_dir);
return Err(e);
}
let checkpoint =
runsc_checkpoint(&runsc_bin, &runsc_root, &network, &checkpoint_dir, &warm_id);
let _ = runsc_delete_force(&runsc_bin, &runsc_root, &network, &warm_id);
if let Err(e) = checkpoint {
let _ = std::fs::remove_dir_all(&pool_dir);
return Err(e);
}
}
let max = max.max(1);
let min = min.min(max);
let warm_spares = if min > 0 {
runsc_pool_spares(max, restore_on_release)
} else {
0
};
let inner = Arc::new(Self {
parts,
vm_config: config.clone(),
min,
max,
warm_spares,
runsc_bin,
runsc_root,
network,
build_netns,
pool_dir,
base_bundle_dir,
base_rootfs,
checkpoint_dir,
admission: Admission::new(max.saturating_add(warm_spares), acquire_timeout),
checked_out: std::sync::atomic::AtomicUsize::new(0),
idle: Mutex::new(Vec::new()),
restore_on_release,
shut_down: std::sync::atomic::AtomicBool::new(false),
});
inner.prewarm_to_min();
Ok(inner)
}
fn warm_target(&self) -> usize {
self.min.saturating_add(self.warm_spares)
}
fn try_checkout(&self) -> bool {
use std::sync::atomic::Ordering::SeqCst;
loop {
let cur = self.checked_out.load(SeqCst);
if cur >= self.max {
return false;
}
if self
.checked_out
.compare_exchange(cur, cur + 1, SeqCst, SeqCst)
.is_ok()
{
return true;
}
}
}
fn release_checkout(&self) {
self.checked_out
.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
self.admission.notify();
}
fn acquire_vm(&self) -> Result<Vm, Error> {
let deadline = self
.admission
.acquire_timeout
.map(|d| std::time::Instant::now() + d);
loop {
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
return Err(Error::pool_exhausted("pool is shut down"));
}
if self.checked_out.load(std::sync::atomic::Ordering::SeqCst) < self.max {
let mut idle = self.idle.lock().unwrap_or_else(|e| e.into_inner());
if !idle.is_empty() && self.try_checkout() {
return Ok(idle.pop().expect("idle checked above"));
}
}
if self.try_checkout() {
if self.admission.try_reserve() {
return self.restore_one().inspect_err(|_| {
self.release_checkout();
self.admission.release();
});
}
self.release_checkout();
}
if !self.admission.wait_for_change(deadline)? {
return Err(Error::pool_exhausted(format!(
"runsc pool saturated: all {} checkout slots in use and none freed within \
acquire_timeout",
self.max
)));
}
}
}
fn restore_one(&self) -> Result<Vm, Error> {
let restore_total = Instant::now();
let runtime_dir = self.pool_dir.join(format!(
"restore-{}-{}",
std::process::id(),
unique_suffix()
));
let bundle_dir = runtime_dir.join("bundle");
let rootfs = bundle_dir.join("rootfs");
let phase = Instant::now();
std::fs::create_dir_all(&bundle_dir).map_err(Error::Io)?;
prepare_runsc_restore_rootfs(&self.base_rootfs, &runtime_dir, &rootfs)?;
runsc_timing("restore.prepare_rootfs", phase);
let netns_path = self.build_netns.as_ref().map(|net| net.path());
let phase = Instant::now();
copy_runsc_bundle_config(
&self.base_bundle_dir,
&bundle_dir,
&self.parts,
&rootfs,
&self.network,
netns_path.as_deref(),
)?;
runsc_timing("restore.bundle_config", phase);
let container_id = format!(
"sm-runsc-restored-{}-{}",
std::process::id(),
unique_suffix()
);
let pid_file = runtime_dir.join("container.pid");
if let Some(checkpoint_dir) = &self.checkpoint_dir {
let phase = Instant::now();
if let Err(e) = runsc_restore_detached(
&self.runsc_bin,
&self.runsc_root,
&self.network,
&bundle_dir,
checkpoint_dir,
&pid_file,
&container_id,
) {
let _ = std::fs::remove_dir_all(&runtime_dir);
return Err(e);
}
runsc_timing("restore.runsc_restore", phase);
} else {
let phase = Instant::now();
if let Err(e) = runsc_run_detached(
&self.runsc_bin,
&self.runsc_root,
&self.network,
&bundle_dir,
&pid_file,
&container_id,
) {
let _ = std::fs::remove_dir_all(&runtime_dir);
return Err(e);
}
runsc_timing("restore.runsc_run", phase);
}
runsc_timing("restore.total", restore_total);
Ok(Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: Some(RunscRuntime {
runsc_bin: self.runsc_bin.clone(),
runsc_root: self.runsc_root.clone(),
network: self.network.clone(),
build_netns: None,
container_id,
bundle_dir,
rootfs,
parts: self.parts.clone(),
file_agent_exec: false,
}),
kboxlike: None,
})
}
fn prewarm_to_min(&self) {
loop {
let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
if idle_len >= self.warm_target() || !self.admission.try_reserve() {
break;
}
match self.restore_one() {
Ok(vm) => {
self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
self.admission.notify();
}
Err(e) => {
eprintln!("supermachine(runsc): prewarm restore failed: {e}");
self.admission.release();
break;
}
}
}
}
fn release_vm(self: &Arc<Self>, vm: Vm) {
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
self.release_checkout();
drop(vm);
self.admission.release();
return;
}
if !self.restore_on_release {
self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
self.release_checkout();
self.admission.notify();
return;
}
self.release_checkout();
let inner = Arc::clone(self);
if let Err(e) = std::thread::Builder::new()
.name("supermachine-runsc-refill".into())
.spawn(move || inner.recycle_released_vm(vm))
{
eprintln!("supermachine(runsc): spawn refill thread failed: {e}");
self.admission.release();
}
}
fn recycle_released_vm(self: Arc<Self>, vm: Vm) {
let total = Instant::now();
drop(vm);
runsc_timing("release.drop_dirty", total);
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
self.admission.release();
return;
}
let keep_warm =
self.idle.lock().unwrap_or_else(|e| e.into_inner()).len() < self.warm_target();
if keep_warm {
match self.restore_one() {
Ok(fresh) => {
self.idle
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(fresh);
self.admission.notify();
}
Err(e) => {
eprintln!("supermachine(runsc): restore_on_release refill failed: {e}");
self.admission.release();
}
}
} else {
self.admission.release();
}
runsc_timing("release.total", total);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for RunscPoolInner {
fn drop(&mut self) {
self.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
self.admission.notify();
let drained: Vec<Vm> =
std::mem::take(&mut *self.idle.lock().unwrap_or_else(|e| e.into_inner()));
for vm in drained {
drop(vm);
}
let _ = std::fs::remove_dir_all(&self.pool_dir);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[derive(Default)]
pub struct SentryBakeConfig {
pub memory_mib: Option<u32>,
pub vcpus: Option<u32>,
pub cmd: Option<Vec<String>>,
pub env: Vec<(String, String)>,
pub working_dir: Option<String>,
pub user: Option<String>,
pub extra_files: Vec<(PathBuf, String)>,
pub mounts: Vec<(
PathBuf,
String,
crate::vmm::resources::SymlinkPolicy,
String,
)>,
pub volumes: Vec<(PathBuf, String, u64)>,
pub warmup: Option<Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>>,
pub warm_daemon: bool,
pub warmup_tag: Option<String>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_sha256_bytes(bytes: &[u8]) -> String {
let digest = ring::digest::digest(&ring::digest::SHA256, bytes);
let mut out = String::with_capacity(64);
for b in digest.as_ref() {
use std::fmt::Write as _;
let _ = write!(out, "{b:02x}");
}
out
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_sha256_file(path: &Path) -> Result<String, Error> {
let mut f = std::fs::File::open(path).map_err(|e| {
Error::bake_msg(format!(
"hash sentry bake input file {}: {e}",
path.display()
))
})?;
let mut ctx = ring::digest::Context::new(&ring::digest::SHA256);
let mut buf = [0u8; 64 * 1024];
loop {
let n = f.read(&mut buf).map_err(|e| {
Error::bake_msg(format!(
"read sentry bake input file {}: {e}",
path.display()
))
})?;
if n == 0 {
break;
}
ctx.update(&buf[..n]);
}
let digest = ctx.finish();
let mut out = String::with_capacity(64);
for b in digest.as_ref() {
use std::fmt::Write as _;
let _ = write!(out, "{b:02x}");
}
Ok(out)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_file_mode(path: &Path) -> Result<u32, Error> {
use std::os::unix::fs::PermissionsExt as _;
Ok(std::fs::metadata(path)
.map_err(|e| Error::bake_msg(format!("stat sentry bake input {}: {e}", path.display())))?
.permissions()
.mode()
& 0o7777)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_bake_cache_key(image_ref: &str, config: &SentryBakeConfig) -> Result<String, Error> {
let mut extra_files = Vec::with_capacity(config.extra_files.len());
for (host, guest) in &config.extra_files {
extra_files.push(serde_json::json!({
"guest": guest,
"mode": sentry_file_mode(host)?,
"sha256": sentry_sha256_file(host)?,
}));
}
let mounts: Vec<_> = config
.mounts
.iter()
.map(|(host, tag, policy, guest)| {
serde_json::json!({
"host": host.to_string_lossy(),
"tag": tag,
"policy": format!("{policy:?}"),
"guest": guest,
})
})
.collect();
let volumes: Vec<_> = config
.volumes
.iter()
.map(|(host, guest, size)| {
serde_json::json!({
"host": host.to_string_lossy(),
"guest": guest,
"size": size,
})
})
.collect();
let input = serde_json::json!({
"schema": 1,
"image": image_ref,
"memory_mib": config.memory_mib,
"vcpus": config.vcpus,
"cmd": &config.cmd,
"env": &config.env,
"working_dir": &config.working_dir,
"user": &config.user,
"extra_files": extra_files,
"mounts": mounts,
"volumes": volumes,
"has_warmup": config.warmup.is_some(),
"warmup_tag": &config.warmup_tag,
"warm_daemon": config.warm_daemon,
});
let bytes = serde_json::to_vec(&input)
.map_err(|e| Error::bake_msg(format!("encode sentry bake cache key: {e}")))?;
Ok(sentry_sha256_bytes(&bytes))
}
#[derive(Debug, Clone)]
pub struct Image {
snapshot_path: PathBuf,
pub(crate) memory_mib: u32,
pub(crate) vcpus: u32,
pub(crate) baker_runtime_sha16: Option<String>,
pub(crate) balloon_target_pages: Option<u32>,
pub(crate) tsi_token: Option<String>,
pub(crate) egress_policy: Option<String>,
pub(crate) pre_exec_sync: bool,
pub(crate) layers: Vec<PathBuf>,
pub(crate) delta_squashfs: Option<PathBuf>,
pub(crate) mounts: Vec<crate::vmm::resources::MountSpec>,
pub(crate) volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
pub(crate) bundled_kernel: Option<PathBuf>,
pub(crate) kvm: Option<KvmImageParts>,
pub(crate) sentry: Option<SentryImageParts>,
pub(crate) runsc: Option<RunscImageParts>,
pub(crate) kboxlike: Option<KboxlikeImageParts>,
pub(crate) hidden_pool: std::sync::OnceLock<Arc<HiddenPool>>,
pub(crate) warm_baked_worker: Arc<crate::bake::WarmStash>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) kvm_pool: std::sync::OnceLock<Arc<Pool>>,
}
impl Image {
#[doc(hidden)]
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn is_kvm_cold_boot(&self) -> bool {
self.kvm
.as_ref()
.map(|k| k.snapshot.is_none())
.unwrap_or(false)
}
pub fn is_sentry(&self) -> bool {
self.sentry.is_some()
}
pub fn is_runsc(&self) -> bool {
self.runsc.is_some()
}
#[doc(hidden)]
pub fn is_kboxlike(&self) -> bool {
self.kboxlike.is_some()
}
#[doc(hidden)]
pub fn _warm_handoff_present(&self) -> bool {
self.warm_baked_worker
.inner
.lock()
.map(|g| g.is_some())
.unwrap_or(false)
}
#[doc(hidden)]
pub fn _warm_handoff_pid(&self) -> Option<u32> {
self.warm_baked_worker
.inner
.lock()
.ok()
.and_then(|g| g.as_ref().map(|bw| bw.child.id()))
}
}
#[doc(hidden)]
pub struct HiddenPool {
state: Arc<Mutex<PoolState>>,
available: Arc<Condvar>,
dirty: Option<Arc<Mutex<VecDeque<Worker>>>>,
dirty_pending: Option<Arc<Condvar>>,
socks_dir: PathBuf,
shutting_down: Arc<AtomicBool>,
spawn_cfg: Arc<SpawnConfig>,
policy: PoolPolicy,
}
#[derive(Debug, Clone, Copy)]
struct PoolPolicy {
min: usize,
max: usize,
idle_timeout: Duration,
acquire_timeout: Option<Duration>,
restore_on_release: bool,
}
impl Default for PoolPolicy {
fn default() -> Self {
Self {
min: 0,
max: 64,
idle_timeout: Duration::from_secs(60),
acquire_timeout: Some(Duration::from_secs(60)),
restore_on_release: true,
}
}
}
struct PoolState {
idle: Vec<IdleEntry>,
alive: usize,
waiting: usize,
reaped: u64,
}
struct IdleEntry {
worker: Worker,
last_used: Instant,
}
struct Worker {
child: Child,
vsock_mux_path: PathBuf,
vsock_exec_path: PathBuf,
control_path: PathBuf,
control: Arc<Mutex<ControlChannel>>,
last_restore_path: PathBuf,
memory_mib: u32,
has_mounts: bool,
vcpus: u32,
volume_temp_files: Vec<PathBuf>,
volume_guest_paths: Vec<String>,
has_cow_volumes: bool,
_admission: crate::memory_admission::AdmissionGuard,
}
struct SnapshotStats {
bytes_written: u64,
capture_us: u64,
save_us: u64,
}
struct ControlChannel {
reader: std::io::BufReader<std::os::unix::net::UnixStream>,
writer: std::os::unix::net::UnixStream,
}
impl ControlChannel {
fn send_line(&mut self, line: &str) -> std::io::Result<()> {
use std::io::Write;
self.writer.write_all(line.as_bytes())?;
if !line.ends_with('\n') {
self.writer.write_all(b"\n")?;
}
self.writer.flush()
}
fn read_line(&mut self) -> std::io::Result<String> {
use std::io::BufRead;
loop {
let mut buf = String::new();
let n = self.reader.read_line(&mut buf)?;
if n == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"worker control socket closed",
));
}
let trimmed = buf.trim_start();
if trimmed.starts_with("SAVE_DONE ") || trimmed.starts_with("SAVE_FAIL ") {
tracing::debug!(line = %buf.trim_end(), "skipping orthogonal bg-save notification");
continue;
}
if trimmed.starts_with("MARKER_KCACHE_READY") {
tracing::debug!("skipping MARKER_KCACHE_READY notification");
continue;
}
return Ok(buf);
}
}
}
#[cfg(target_os = "macos")]
fn warm_baked_to_worker(
bw: crate::bake::BakedWorker,
memory_mib: u32,
has_mounts: bool,
volume_guest_paths: Vec<String>,
vcpus: u32,
use_once: bool,
) -> Worker {
let admission = crate::memory_admission::charge({
let overhead = crate::memory_admission::WORKER_OVERHEAD_MIB;
let observed = crate::memory_admission::phys_footprint_mib(bw.child.id()).unwrap_or(0);
learned_charge_mib(memory_mib, observed, overhead)
});
Worker {
child: bw.child,
vsock_mux_path: bw.vsock_mux_path,
vsock_exec_path: bw.vsock_exec_path,
control_path: bw.control_path,
control: Arc::new(Mutex::new(ControlChannel {
reader: std::io::BufReader::new(bw.control_reader),
writer: bw.control_writer,
})),
last_restore_path: bw.last_restore_path,
memory_mib,
has_mounts,
vcpus,
volume_temp_files: Vec::new(),
volume_guest_paths,
has_cow_volumes: use_once,
_admission: admission,
}
}
#[cfg(target_os = "macos")]
fn wait_for_volumes_mounted(exec_path: &Path, guest_paths: &[String]) -> Result<(), Error> {
let t0 = std::time::Instant::now();
let budget = std::time::Duration::from_secs(3);
loop {
let out = crate::exec::ExecBuilder::new(exec_path.to_path_buf())
.argv(["cat", "/proc/mounts"])
.timeout(std::time::Duration::from_secs(3))
.output()
.map_err(|e| {
Error::vm_msg(format!(
"post-restore volume-readiness exec failed in {:?}: {e} — \
worker will be respawned",
t0.elapsed()
))
})?;
if out.status.code() == Some(0) {
let mounts = String::from_utf8_lossy(&out.stdout);
let all_mounted = guest_paths.iter().all(|gp| {
mounts
.lines()
.any(|l| l.split(' ').nth(1) == Some(gp.as_str()))
});
if all_mounted {
return Ok(());
}
}
if t0.elapsed() >= budget {
return Err(Error::vm_msg(format!(
"post-restore volumes not mounted within {budget:?}: {guest_paths:?} \
absent from /proc/mounts — worker will be respawned"
)));
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}
impl Worker {
fn send_restore(&self, snapshot_path: &Path) -> Result<(), Error> {
let path_str = snapshot_path
.to_str()
.ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
let mut ctl = self
.control
.lock()
.map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
ctl.send_line(&format!("RESTORE {path_str}"))
.map_err(Error::Io)?;
let line = ctl.read_line().map_err(Error::Io)?;
if !line.starts_with("DONE") {
return Err(Error::vm_msg(format!(
"worker RESTORE: expected DONE response, got: {}",
line.trim()
)));
}
if self.vcpus > 1 {
ctl.send_line("SMPARK_UNPARK_DIRECT").map_err(Error::Io)?;
let _ = ctl.read_line().map_err(Error::Io)?;
}
drop(ctl);
let probe_enabled = std::env::var("SUPERMACHINE_POST_RESTORE_PROBE")
.map(|v| v != "0" && v != "false")
.unwrap_or(true);
if probe_enabled {
let per_attempt = std::time::Duration::from_millis(200);
let max_attempts = 10;
for attempt in 0..max_attempts {
let body = serde_json::json!({ "action": "probe" });
match crate::exec::send_control_with_ack(
&self.vsock_exec_path,
&body,
Some(per_attempt),
) {
Ok(_) => break,
Err(_) if attempt + 1 < max_attempts => {}
Err(e) => {
return Err(Error::vm_msg(format!(
"post-restore agent probe failed after {max_attempts} attempts: {e}"
)));
}
}
}
if self.vcpus > 1 {
let body = serde_json::json!({ "action": "smpark_unpark" });
let _ = crate::exec::send_control_with_ack(
&self.vsock_exec_path,
&body,
Some(std::time::Duration::from_millis(500)),
);
}
let liveness_enabled = std::env::var("SUPERMACHINE_POST_RESTORE_LIVENESS")
.map(|v| v != "0" && v != "false")
.unwrap_or(true);
if liveness_enabled {
let liveness_t0 = std::time::Instant::now();
let exec_res = crate::exec::ExecBuilder::new(self.vsock_exec_path.clone())
.argv(["/bin/true"])
.timeout(std::time::Duration::from_secs(3))
.output();
match exec_res {
Ok(out) if out.status.code() == Some(0) => {
}
Ok(out) => {
return Err(Error::vm_msg(format!(
"post-restore liveness exec returned non-zero in {:?}: exit={:?} \
stderr={:?} — worker will be respawned",
liveness_t0.elapsed(),
out.status.code(),
String::from_utf8_lossy(&out.stderr)
.chars()
.take(200)
.collect::<String>(),
)));
}
Err(e) => {
return Err(Error::vm_msg(format!(
"post-restore liveness exec failed in {:?}: {e} — worker will be respawned",
liveness_t0.elapsed(),
)));
}
}
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if !self.volume_guest_paths.is_empty() {
wait_for_volumes_mounted(&self.vsock_exec_path, &self.volume_guest_paths)?;
}
if self.has_mounts {
let _ = drop_vfs_caches_via_agent(&self.vsock_exec_path, self.memory_mib);
}
let _ = sync_time_via_agent(&self.vsock_exec_path);
Ok(())
}
fn send_snapshot(&self, out_path: &Path) -> Result<SnapshotStats, Error> {
let path_str = out_path
.to_str()
.ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
let mut ctl = self
.control
.lock()
.map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
ctl.send_line(&format!("SNAPSHOT {path_str}"))
.map_err(Error::Io)?;
let line = ctl.read_line().map_err(Error::Io)?;
if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT") {
let mut stats = SnapshotStats {
bytes_written: 0,
capture_us: 0,
save_us: 0,
};
for kv in rest.split_ascii_whitespace() {
if let Some(v) = kv.strip_prefix("bytes_written=") {
stats.bytes_written = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("capture_us=") {
stats.capture_us = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("save_us=") {
stats.save_us = v.parse().unwrap_or(0);
}
}
Ok(stats)
} else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
Err(Error::vm_msg(format!(
"worker SNAPSHOT failed: {}",
rest.trim()
)))
} else {
Err(Error::vm_msg(format!(
"worker SNAPSHOT: unexpected response: {}",
line.trim()
)))
}
}
fn send_snapshot_with_base(
&self,
out_path: &Path,
base_path: &Path,
inline: bool,
) -> Result<SnapshotStats, Error> {
let path_str = out_path
.to_str()
.ok_or_else(|| Error::vm_msg("snapshot out path is not valid UTF-8".to_owned()))?;
let base_str = base_path
.to_str()
.ok_or_else(|| Error::vm_msg("snapshot base path is not valid UTF-8".to_owned()))?;
let mut ctl = self
.control
.lock()
.map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
let line = if inline {
format!("SNAPSHOT {path_str} base={base_str} inline")
} else {
format!("SNAPSHOT {path_str} base={base_str}")
};
ctl.send_line(&line).map_err(Error::Io)?;
let line = ctl.read_line().map_err(Error::Io)?;
if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT") {
let mut stats = SnapshotStats {
bytes_written: 0,
capture_us: 0,
save_us: 0,
};
for kv in rest.split_ascii_whitespace() {
if let Some(v) = kv.strip_prefix("bytes_written=") {
stats.bytes_written = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("capture_us=") {
stats.capture_us = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("save_us=") {
stats.save_us = v.parse().unwrap_or(0);
}
}
Ok(stats)
} else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
Err(Error::vm_msg(format!(
"worker SNAPSHOT (with base) failed: {}",
rest.trim()
)))
} else {
Err(Error::vm_msg(format!(
"worker SNAPSHOT (with base): unexpected response: {}",
line.trim()
)))
}
}
#[allow(dead_code)]
fn send_snapshot_async(&self, out_path: &Path) -> Result<SnapshotStats, Error> {
let path_str = out_path
.to_str()
.ok_or_else(|| Error::vm_msg("snapshot path is not valid UTF-8".to_owned()))?;
let mut ctl = self
.control
.lock()
.map_err(|_| Error::vm_msg("worker control mutex poisoned".to_owned()))?;
ctl.send_line(&format!("SNAPSHOT_ASYNC {path_str}"))
.map_err(Error::Io)?;
let line = ctl.read_line().map_err(Error::Io)?;
if let Some(rest) = line.strip_prefix("DONE_SNAPSHOT_ASYNC") {
let mut stats = SnapshotStats {
bytes_written: 0,
capture_us: 0,
save_us: 0,
};
for kv in rest.split_ascii_whitespace() {
if let Some(v) = kv.strip_prefix("bytes_written=") {
stats.bytes_written = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("capture_us=") {
stats.capture_us = v.parse().unwrap_or(0);
} else if let Some(v) = kv.strip_prefix("save_us=") {
stats.save_us = v.parse().unwrap_or(0);
}
}
Ok(stats)
} else if let Some(rest) = line.strip_prefix("ERR_SNAPSHOT ") {
Err(Error::vm_msg(format!(
"worker SNAPSHOT_ASYNC failed: {}",
rest.trim()
)))
} else {
Err(Error::vm_msg(format!(
"worker SNAPSHOT_ASYNC: unexpected response: {}",
line.trim()
)))
}
}
#[cfg_attr(
not(all(target_os = "macos", target_arch = "aarch64")),
allow(dead_code)
)]
fn send_smpark_park(&self) -> Result<bool, Error> {
let body = serde_json::json!({ "action": "smpark_park" });
match crate::exec::send_control_with_ack(
&self.vsock_exec_path,
&body,
Some(Duration::from_secs(5)),
) {
Ok(_) => Ok(true),
Err(e) => {
tracing::debug!(error = %e, "smpark_park unavailable; skipping");
Ok(false)
}
}
}
#[cfg_attr(
not(all(target_os = "macos", target_arch = "aarch64")),
allow(dead_code)
)]
fn send_smpark_unpark(&self) -> Result<bool, Error> {
let body = serde_json::json!({ "action": "smpark_unpark" });
match crate::exec::send_control_with_ack(
&self.vsock_exec_path,
&body,
Some(Duration::from_secs(5)),
) {
Ok(_) => Ok(true),
Err(e) => {
tracing::debug!(error = %e, "smpark_unpark unavailable; skipping");
Ok(false)
}
}
}
fn shutdown(&mut self) {
if let Ok(mut ctl) = self.control.lock() {
let _ = ctl.send_line("QUIT");
}
let deadline = Instant::now() + Duration::from_millis(100);
loop {
match self.child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(2));
}
_ => {
let _ = self.child.kill();
let _ = self.child.wait();
break;
}
}
}
let _ = std::fs::remove_file(&self.vsock_mux_path);
let _ = std::fs::remove_file(&self.vsock_exec_path);
let _ = std::fs::remove_file(&self.control_path);
let mut h = self.vsock_mux_path.clone();
h.set_extension("handoff");
let _ = std::fs::remove_file(&h);
for path in self.volume_temp_files.drain(..) {
let _ = std::fs::remove_file(&path);
}
}
}
struct SpawnConfig {
observed_footprint_mib: std::sync::atomic::AtomicU64,
worker_bin: PathBuf,
snapshot_path: PathBuf,
layers: Vec<PathBuf>,
delta_squashfs: Option<PathBuf>,
mounts: Vec<crate::vmm::resources::MountSpec>,
volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)>,
memory_mib: u32,
vcpus: u32,
socks_dir: PathBuf,
name_prefix: String,
spawn_timeout: Duration,
baker_runtime_sha16: Option<String>,
balloon_target_pages: Option<u32>,
tsi_token: Option<String>,
egress_policy: Option<String>,
pre_exec_sync: bool,
restore_on_release: bool,
}
fn learned_charge_mib(memory_mib: u32, observed_mib: u64, overhead: u64) -> u64 {
let cap = memory_mib as u64 + overhead;
if observed_mib == 0 {
return cap;
}
let margined = observed_mib + observed_mib / 4 + overhead; margined.min(cap)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_admission_charge_mib(mem_mib: u64, cells: u64) -> u64 {
let per_cell = mem_mib.saturating_add(crate::memory_admission::WORKER_OVERHEAD_MIB);
per_cell.saturating_mul(cells.max(1))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_cell_mem_mib(image: &Image, config: &VmConfig) -> u64 {
if config.sentry_uncapped {
return 0;
}
config.memory_mib.unwrap_or(image.memory_mib) as u64
}
#[cfg(test)]
mod learned_charge_tests {
use super::learned_charge_mib;
const OH: u64 = 64;
#[test]
fn unmeasured_charges_full_cap() {
assert_eq!(learned_charge_mib(512, 0, OH), 512 + OH);
}
#[test]
fn measured_charges_observed_plus_margin() {
assert_eq!(learned_charge_mib(512, 300, OH), 375 + OH);
}
#[test]
fn never_exceeds_the_cap() {
assert_eq!(learned_charge_mib(512, 1000, OH), 512 + OH);
}
#[test]
fn cow_restored_pool_fits_under_budget_without_timeouts() {
let charge = learned_charge_mib(8256, 288, OH);
assert_eq!(charge, 288 + 72 + OH);
assert!(
charge * 8 < 19660,
"8 CoW-restored workers must fit the integrator's budget (charge {charge} MiB)"
);
}
}
#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod sentry_admission_tests {
use super::sentry_admission_charge_mib;
const OH: u64 = crate::memory_admission::WORKER_OVERHEAD_MIB;
#[test]
fn one_shot_charges_one_capped_cell_plus_overhead() {
assert_eq!(sentry_admission_charge_mib(512, 1), 512 + OH);
}
#[test]
fn pool_charge_scales_with_concurrency_cap() {
assert_eq!(sentry_admission_charge_mib(512, 4), 4 * (512 + OH));
assert!(sentry_admission_charge_mib(512, 8) > sentry_admission_charge_mib(512, 4));
}
#[test]
fn uncapped_cell_charges_overhead_only() {
assert_eq!(sentry_admission_charge_mib(0, 1), OH);
assert_eq!(sentry_admission_charge_mib(0, 4), 4 * OH);
}
#[test]
fn zero_cells_clamps_to_one() {
assert_eq!(sentry_admission_charge_mib(512, 0), 512 + OH);
}
#[test]
fn over_budget_pool_reserves_more_than_the_budget() {
let budget_mib = 4096u64;
let pool_charge = sentry_admission_charge_mib(512, 32);
assert!(
pool_charge > budget_mib,
"an over-budget pool must reserve > the budget so admission gates it \
(charge {pool_charge} MiB vs budget {budget_mib} MiB)"
);
assert!(sentry_admission_charge_mib(512, 4) <= budget_mib);
}
}
impl SpawnConfig {
fn admission_charge_mib(&self) -> u64 {
const UNMEASURED_RESTORE_BASELINE_MIB: u64 = 256;
let overhead = crate::memory_admission::WORKER_OVERHEAD_MIB;
let observed = self
.observed_footprint_mib
.load(std::sync::atomic::Ordering::Relaxed);
let effective = if observed == 0 {
UNMEASURED_RESTORE_BASELINE_MIB
} else {
observed
};
learned_charge_mib(self.memory_mib, effective, overhead)
}
fn spawn_one(&self) -> Result<Worker, Error> {
use std::os::unix::net::UnixListener;
let suffix = unique_suffix();
let vsock_mux_path = self
.socks_dir
.join(format!("{}-{:016x}.sock", self.name_prefix, suffix));
let vsock_exec_path = {
let mut p = vsock_mux_path.clone();
let mut name = p.file_name().unwrap().to_owned();
name.push("-exec");
p.set_file_name(name);
p
};
let control_path = {
let mut p = vsock_mux_path.clone();
let mut name = p.file_name().unwrap().to_owned();
name.push("-ctl");
p.set_file_name(name);
p
};
let _ = std::fs::remove_file(&vsock_mux_path);
let _ = std::fs::remove_file(&vsock_exec_path);
let _ = std::fs::remove_file(&control_path);
let ctl_listener = UnixListener::bind(&control_path).map_err(|e| {
Error::vm_msg(format!(
"bind control socket {}: {e}",
control_path.display()
))
})?;
if !self.snapshot_path.is_file() {
let poll_t0 = Instant::now();
let poll_deadline = poll_t0 + self.spawn_timeout;
let mut backoff = Duration::from_millis(2);
loop {
if self.snapshot_path.is_file() {
if crate::trace::enabled("timings") {
eprintln!(
"[spawn_one] waited {:?} for snapshot file to land at {}",
poll_t0.elapsed(),
self.snapshot_path.display(),
);
}
break;
}
if Instant::now() > poll_deadline {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: snapshot file {} did not appear within {:?} \
(bg save in flight from a pipelined bake — increase \
`VmConfig::restore_timeout` if your disk is slow)",
self.snapshot_path.display(),
self.spawn_timeout
)));
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_millis(50));
}
}
let mut cmd = Command::new(&self.worker_bin);
for layer in &self.layers {
cmd.arg("--virtio-blk").arg(layer);
}
if let Some(delta) = &self.delta_squashfs {
cmd.arg("--virtio-blk").arg(delta);
}
for m in &self.mounts {
cmd.arg("--mount")
.arg(format!("{}:{}:{}", m.host_path, m.guest_tag, m.guest_path));
}
let cow_volumes_enabled = self.volumes.iter().any(|(_, _, _, p)| p.is_some());
let mut volume_temp_files: Vec<PathBuf> = Vec::new();
for (i, (host_file, guest_path, size_bytes, pristine)) in self.volumes.iter().enumerate() {
let effective_host = if cow_volumes_enabled {
match pristine.as_ref() {
Some(pristine_path) => {
match cow_volume_for_spawn(pristine_path, i, suffix) {
Ok(temp) => {
volume_temp_files.push(temp.clone());
temp
}
Err(e) => {
for p in &volume_temp_files {
let _ = std::fs::remove_file(p);
}
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"spawn_one: COW volume {}: {e}",
pristine_path.display()
)));
}
}
}
None => {
warn_legacy_volume_without_pristine_once(host_file);
host_file.clone()
}
}
} else {
host_file.clone()
};
cmd.arg("--volume").arg(format!(
"{}:{}:{}",
effective_host.display(),
guest_path,
size_bytes
));
}
cmd.arg("--memory").arg(self.memory_mib.to_string());
cmd.arg("--vcpus").arg(self.vcpus.to_string());
cmd.arg("--restore-from").arg(&self.snapshot_path);
cmd.arg("--cow-restore");
cmd.arg("--vsock-mux").arg(&vsock_mux_path);
cmd.arg("--vsock-exec").arg(&vsock_exec_path);
cmd.arg("--pool-worker").arg(&control_path);
cmd.env(
"SUPERMACHINE_HOST_IPV6",
if crate::utils::net::host_ipv6_route() {
"1"
} else {
"0"
},
);
if let Some(pages) = self.balloon_target_pages {
cmd.arg("--balloon-target-pages").arg(pages.to_string());
}
if let Some(hex) = self.tsi_token.as_deref() {
cmd.arg("--tsi-token").arg(hex);
}
if let Some(policy) = self.egress_policy.as_deref() {
cmd.arg("--egress-policy").arg(policy);
}
if self.pre_exec_sync {
cmd.env("SUPERMACHINE_PRE_EXEC_SYNC_RESUME", "1");
}
let log_to_stdio = std::env::var("SUPERMACHINE_WORKER_LOG")
.map(|v| v == "1" || v == "true")
.unwrap_or(false);
let log_file_path = std::env::var("SUPERMACHINE_WORKER_LOG_FILE").ok().map(|p| {
if p.contains('%') {
p.replace('%', &format!("{suffix:016x}"))
} else {
p
}
});
if let Some(path) = log_file_path.as_deref() {
let f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.ok();
if let Some(f) = f {
let f2 = f.try_clone().ok();
cmd.stdout(Stdio::from(f));
if let Some(f2) = f2 {
cmd.stderr(Stdio::from(f2));
} else {
cmd.stderr(Stdio::null());
}
}
} else if !log_to_stdio {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
}
crate::memory_admission::await_pressure_relief();
let admission_guard = crate::memory_admission::admit_with(|| self.admission_charge_mib());
let _spawn_permit = crate::spawn_concurrency::acquire();
let __t0 = Instant::now();
let child = cmd.spawn().map_err(|e| {
Error::vm_msg(format!("spawn worker {}: {e}", self.worker_bin.display()))
})?;
let __t_spawned = __t0.elapsed();
struct SpawnCleanup {
child: Option<std::process::Child>,
cleanup_paths: Vec<PathBuf>,
armed: bool,
}
impl Drop for SpawnCleanup {
fn drop(&mut self) {
if !self.armed {
return;
}
if let Some(mut c) = self.child.take() {
let _ = c.kill();
let _ = c.wait();
}
for p in &self.cleanup_paths {
let _ = std::fs::remove_file(p);
}
}
}
let mut spawn_cleanup = {
let mut cleanup_paths = volume_temp_files.clone();
cleanup_paths.push(control_path.clone());
SpawnCleanup {
child: Some(child),
cleanup_paths,
armed: true,
}
};
ctl_listener
.set_nonblocking(true)
.map_err(|e| Error::vm_msg(format!("set control listener nonblocking: {e}")))?;
let deadline = Instant::now() + self.spawn_timeout;
let mut backoff = Duration::from_millis(1);
let stream = loop {
match ctl_listener.accept() {
Ok((s, _)) => break s,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
if Instant::now() > deadline {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: control connect did not arrive within {:?}",
self.spawn_timeout
)));
}
std::thread::sleep(backoff);
backoff = (backoff * 2).min(Duration::from_millis(10));
}
Err(e) => {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!("worker spawn: control accept: {e}")));
}
}
};
stream
.set_nonblocking(false)
.map_err(|e| Error::vm_msg(format!("set control stream blocking: {e}")))?;
let writer = stream
.try_clone()
.map_err(|e| Error::vm_msg(format!("clone control stream: {e}")))?;
let mut control = ControlChannel {
reader: std::io::BufReader::new(stream),
writer,
};
let ready = match control.read_line() {
Ok(l) => l,
Err(e) => {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!("worker spawn: read READY: {e}")));
}
};
if ready.trim() != "READY" {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: expected READY, got: {}",
ready.trim()
)));
}
let snap_path_str = self.snapshot_path.to_string_lossy().to_string();
if let Err(e) = control.send_line(&format!("RESTORE {snap_path_str}")) {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: send initial RESTORE: {e}"
)));
}
let done = match control.read_line() {
Ok(l) => l,
Err(e) => {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: read initial DONE: {e}"
)));
}
};
if !done.starts_with("DONE") {
let _ = std::fs::remove_file(&control_path);
return Err(Error::vm_msg(format!(
"worker spawn: expected initial DONE, got: {}",
done.trim()
)));
}
if crate::trace::enabled("timings") {
eprintln!(
"[spawn_one] spawn={:?} accept_to_done={:?} total={:?}",
__t_spawned,
__t0.elapsed() - __t_spawned,
__t0.elapsed()
);
}
if self.vcpus > 1 {
let body = serde_json::json!({ "action": "smpark_unpark" });
match crate::exec::send_control_with_ack(
&vsock_exec_path,
&body,
Some(Duration::from_secs(2)),
) {
Ok(_) => {}
Err(e) => {
tracing::debug!(
error = %e,
"post-restore smpark_unpark RPC error (continuing)"
);
}
}
}
if !self.mounts.is_empty() || !self.volumes.is_empty() {
let _ = drop_vfs_caches_via_agent(&vsock_exec_path, self.memory_mib);
}
let _ = sync_time_via_agent(&vsock_exec_path);
let probe_skip = self
.baker_runtime_sha16
.as_deref()
.and_then(|stored| {
current_worker_sha16(&self.worker_bin).map(|current| stored == current.as_str())
})
.unwrap_or(false);
if !probe_skip {
if let Err(e) = probe_agent_protocol(&vsock_exec_path, self.vcpus) {
if self.vcpus > 1 && e.is_likely_multi_vcpu_restore_stall() {
eprintln!(
"supermachine: WARNING multi-vCPU ({}vCPU) snapshot agent probe \
timed out — guest may be RCU-stalled after restore. \
Continuing (multi-vCPU is unsupported per design); \
workload exec will surface a real error if the guest is hung.",
self.vcpus
);
} else {
let _ = std::fs::remove_file(&control_path);
return Err(e);
}
}
}
if !self.volumes.is_empty() {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
let gps: Vec<String> = self
.volumes
.iter()
.map(|(_, gp, _, _)| gp.clone())
.collect();
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if let Err(e) = wait_for_volumes_mounted(&vsock_exec_path, &gps) {
let _ = std::fs::remove_file(&control_path);
return Err(e);
}
}
spawn_cleanup.armed = false;
let child = spawn_cleanup
.child
.take()
.expect("spawn_cleanup holds the child until success");
if let Some(fp) = crate::memory_admission::phys_footprint_mib(child.id()) {
let prev = self
.observed_footprint_mib
.fetch_max(fp, std::sync::atomic::Ordering::Relaxed);
if fp > prev {
eprintln!(
"[admission] observed worker footprint {fp} MiB (cap {} MiB) — \
new high-water-mark",
self.memory_mib
);
}
if prev == 0 {
crate::memory_admission::notify_waiters();
}
}
Ok(Worker {
child,
vsock_mux_path,
vsock_exec_path,
control_path,
control: Arc::new(Mutex::new(control)),
last_restore_path: self.snapshot_path.clone(),
memory_mib: self.memory_mib,
has_mounts: !self.mounts.is_empty() || !self.volumes.is_empty(),
volume_guest_paths: self
.volumes
.iter()
.map(|(_, gp, _, _)| gp.clone())
.collect(),
vcpus: self.vcpus,
has_cow_volumes: !volume_temp_files.is_empty(),
volume_temp_files,
_admission: admission_guard,
})
}
}
fn cow_volume_for_spawn(pristine: &Path, index: usize, suffix: u64) -> std::io::Result<PathBuf> {
let pid = std::process::id();
let dst =
std::env::temp_dir().join(format!("supermachine-vol-{pid}-{suffix:016x}-{index}.img"));
crate::bake::cow_or_copy(pristine, &dst)?;
Ok(dst)
}
fn sweep_volume_temps_once() {
static SWEPT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if SWEPT.get().is_some() {
return;
}
sweep_stranded_volume_temps();
let _ = SWEPT.set(());
}
fn sweep_stranded_volume_temps() {
let tmp = std::env::temp_dir();
let entries = match std::fs::read_dir(&tmp) {
Ok(e) => e,
Err(_) => return,
};
let me = std::process::id();
for entry in entries.flatten() {
let fname = entry.file_name();
let Some(name) = fname.to_str() else {
continue;
};
let Some(rest) = name.strip_prefix("supermachine-vol-") else {
continue;
};
if !name.ends_with(".img") {
continue;
}
let Some(dash) = rest.find('-') else {
continue;
};
let pid_str = &rest[..dash];
let Ok(pid) = pid_str.parse::<i32>() else {
continue;
};
if pid as u32 == me {
continue;
}
let alive = unsafe { libc::kill(pid, 0) };
if alive == 0 {
continue;
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() != Some(libc::ESRCH) {
continue;
}
let _ = std::fs::remove_file(entry.path());
}
}
fn warn_legacy_volume_without_pristine_once(host_file: &Path) {
static WARNED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if WARNED.get().is_some() {
return;
}
let _ = WARNED.set(());
eprintln!(
"supermachine: WARNING snapshot lacks pristine volume bytes (host_file={}); \
workers will write directly to the host file, leaking state across \
processes and across `restore_on_release=true` acquires — re-bake \
under 0.7.49+ for per-spawn COW reset semantics",
host_file.display()
);
}
const HOST_AGENT_PROTOCOL_MIN: u32 = 3;
fn current_worker_sha16(worker_bin: &Path) -> Option<String> {
use std::sync::Mutex;
static CACHE: Mutex<Option<(PathBuf, u64, u128, String)>> = Mutex::new(None);
let meta = std::fs::metadata(worker_bin).ok()?;
let len = meta.len();
let mtime_ns = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
if let Ok(g) = CACHE.lock() {
if let Some((p, l, m, sha)) = g.as_ref() {
if p == worker_bin && *l == len && *m == mtime_ns {
return Some(sha.clone());
}
}
}
use ring::digest::{Context, SHA256};
use std::io::Read;
let mut f = std::fs::File::open(worker_bin).ok()?;
let mut ctx = Context::new(&SHA256);
let mut buf = [0u8; 64 * 1024];
loop {
let n = f.read(&mut buf).ok()?;
if n == 0 {
break;
}
ctx.update(&buf[..n]);
}
let d = ctx.finish();
let mut digest = String::with_capacity(64);
for b in d.as_ref() {
use std::fmt::Write;
let _ = write!(digest, "{:02x}", b);
}
let sha16 = digest[..digest.len().min(16)].to_owned();
if let Ok(mut g) = CACHE.lock() {
*g = Some((worker_bin.to_path_buf(), len, mtime_ns, sha16.clone()));
}
Some(sha16)
}
fn drop_vfs_caches_via_agent(vsock_exec_path: &Path, memory_mib: u32) -> bool {
let setting = std::env::var("SUPERMACHINE_VFS_AUTO_INVALIDATE").ok();
let s = setting.as_deref();
if s == Some("0") {
return false;
}
let force = s == Some("force") || s == Some("FORCE");
if memory_mib < 512 && !force {
if crate::trace::enabled("vfs") {
eprintln!(
"[drop_vfs_caches] SKIP memory_mib={memory_mib} < 512 (set \
SUPERMACHINE_VFS_AUTO_INVALIDATE=force to override)"
);
}
return false;
}
let body = serde_json::json!({ "action": "drop_vfs_caches" });
let t0 = Instant::now();
let trace = crate::trace::enabled("vfs");
match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(5))) {
Ok(ack) => {
if trace {
eprintln!(
"[drop_vfs_caches] OK in {} ms ack={ack:?}",
t0.elapsed().as_millis()
);
}
true
}
Err(e) => {
if trace {
eprintln!(
"[drop_vfs_caches] FAIL in {} ms ({e})",
t0.elapsed().as_millis()
);
}
tracing::debug!(
error = %e,
"drop_vfs_caches RPC error (continuing with stale caches)"
);
false
}
}
}
fn sync_time_via_agent(vsock_exec_path: &Path) -> bool {
if std::env::var("SUPERMACHINE_TIME_AUTO_SYNC").as_deref() == Ok("0") {
return false;
}
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(d) => d.as_nanos(),
Err(_) => return false,
};
let host_unix_ns = u64::try_from(now).unwrap_or(u64::MAX);
let body = serde_json::json!({
"action": "sync_time",
"host_unix_ns": host_unix_ns,
});
let t0 = Instant::now();
let trace = crate::trace::enabled("time");
match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(5))) {
Ok(ack) => {
if trace {
eprintln!(
"[sync_time] OK in {} ms ack={ack:?}",
t0.elapsed().as_millis()
);
}
true
}
Err(e) => {
if trace {
eprintln!("[sync_time] FAIL in {} ms ({e})", t0.elapsed().as_millis());
}
tracing::debug!(
error = %e,
"sync_time RPC error (continuing with frozen wall-clock)"
);
false
}
}
}
pub(crate) fn guest_sync_via_agent(vsock_exec_path: &Path) -> bool {
let body = serde_json::json!({ "action": "sync" });
let t0 = Instant::now();
match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(Duration::from_secs(60)))
{
Ok(_) => {
if crate::trace::enabled("phases") {
eprintln!("[guest_sync] OK in {} ms", t0.elapsed().as_millis());
}
true
}
Err(e) => {
eprintln!(
"supermachine: guest sync before snapshot capture unavailable \
({e}); volume writes still dirty in the guest may be missing \
from the host backing file (rebake to refresh the in-guest agent)"
);
false
}
}
}
fn probe_agent_protocol(vsock_exec_path: &Path, vcpus: u32) -> Result<(), Error> {
let timeout = if vcpus > 1 {
Duration::from_secs(1)
} else {
Duration::from_secs(10)
};
let body = serde_json::json!({ "action": "probe" });
let ack = match crate::exec::send_control_with_ack(vsock_exec_path, &body, Some(timeout)) {
Ok(a) => a,
Err(e) => {
return Err(Error::vm_msg(format!(
"agent in this snapshot is from an older supermachine release \
(probe failed: {e}). Rebake the snapshot to pick up the new \
agent: rm -rf the snapshot dir and re-run your bake (e.g. \
`supermachine pull <image> --name <name>`)."
)));
}
};
let proto = ack.get("protocol").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
if proto < HOST_AGENT_PROTOCOL_MIN {
return Err(Error::vm_msg(format!(
"agent in this snapshot speaks protocol v{proto} but this \
supermachine library expects v{HOST_AGENT_PROTOCOL_MIN}+. The \
snapshot was baked against a previous release; rebake to pick up \
the new agent (rm -rf the snapshot dir and re-run your bake)."
)));
}
Ok(())
}
impl std::fmt::Debug for HiddenPool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.state.lock().ok();
f.debug_struct("HiddenPool")
.field("socks_dir", &self.socks_dir)
.field("alive", &s.as_ref().map(|s| s.alive).unwrap_or(usize::MAX))
.field(
"idle",
&s.as_ref().map(|s| s.idle.len()).unwrap_or(usize::MAX),
)
.finish()
}
}
impl HiddenPool {
fn shutdown_pool(&self) {
self.shutting_down.store(true, Ordering::SeqCst);
self.available.notify_all();
if let Some(c) = self.dirty_pending.as_ref() {
c.notify_all();
}
if let Ok(mut s) = self.state.lock() {
while let Some(mut e) = s.idle.pop() {
e.worker.shutdown();
s.alive = s.alive.saturating_sub(1);
}
if let Some(d) = self.dirty.as_ref() {
if let Ok(mut q) = d.lock() {
while let Some(mut w) = q.pop_front() {
w.shutdown();
s.alive = s.alive.saturating_sub(1);
}
}
}
}
}
}
impl Drop for HiddenPool {
fn drop(&mut self) {
self.shutdown_pool();
let _ = std::fs::remove_dir_all(&self.socks_dir);
}
}
impl HiddenPool {
fn acquire(&self) -> Result<Worker, Error> {
let acquire_t0 = Instant::now();
let acquire_timeout = self.policy.acquire_timeout;
let mut state = self
.state
.lock()
.map_err(|_| Error::vm_msg("pool mutex poisoned".to_owned()))?;
loop {
while let Some(mut entry) = state.idle.pop() {
match entry.worker.child.try_wait() {
Ok(None) => {
if !self.policy.restore_on_release {
let idle_for = entry.last_used.elapsed();
if idle_for > Duration::from_secs(1) {
let exec_path = entry.worker.vsock_exec_path.clone();
drop(state);
let _ = sync_time_via_agent(&exec_path);
return Ok(entry.worker);
}
}
return Ok(entry.worker); }
Ok(Some(_status)) => {
state.alive = state.alive.saturating_sub(1);
continue; }
Err(_) => {
state.alive = state.alive.saturating_sub(1);
continue;
}
}
}
if self.shutting_down.load(Ordering::SeqCst) {
return Err(Error::vm_msg("pool is shutting down".to_owned()));
}
if state.alive < self.policy.max {
state.alive += 1;
drop(state);
let spawned = self.spawn_cfg.spawn_one();
match spawned {
Ok(w) => return Ok(w),
Err(e) => {
if let Ok(mut s) = self.state.lock() {
s.alive = s.alive.saturating_sub(1);
}
self.available.notify_all();
return Err(e);
}
}
}
if let Some(total) = acquire_timeout {
if acquire_t0.elapsed() >= total {
return Err(Error::pool_exhausted(format!(
"acquire timed out after {total:?}; pool at max ({})",
self.policy.max
)));
}
}
state.waiting += 1;
let (new_state, timed_out) = match acquire_timeout {
None => match self.available.wait(state) {
Ok(s) => (s, false),
Err(_) => return Err(Error::vm_msg("pool condvar poisoned".to_owned())),
},
Some(total) => {
let remaining = total.saturating_sub(acquire_t0.elapsed());
match self.available.wait_timeout(state, remaining) {
Ok((s, r)) => (s, r.timed_out()),
Err(_) => return Err(Error::vm_msg("pool condvar poisoned".to_owned())),
}
}
};
state = new_state;
state.waiting = state.waiting.saturating_sub(1);
if timed_out {
return Err(Error::pool_exhausted(format!(
"acquire timed out after {:?}; pool at max ({})",
acquire_timeout.unwrap_or_default(),
self.policy.max
)));
}
}
}
fn release(&self, worker: Worker) {
if let Some(fp) = crate::memory_admission::phys_footprint_mib(worker.child.id()) {
let prev = self
.spawn_cfg
.observed_footprint_mib
.fetch_max(fp, std::sync::atomic::Ordering::Relaxed);
if fp > prev {
eprintln!(
"[admission] observed worker footprint {fp} MiB at release \
(cap {} MiB) — new high-water-mark",
self.spawn_cfg.memory_mib
);
}
}
if !self.policy.restore_on_release {
if let Ok(mut s) = self.state.lock() {
s.idle.push(IdleEntry {
worker,
last_used: Instant::now(),
});
self.available.notify_all();
}
return;
}
if worker.has_cow_volumes {
let mut w = worker;
w.shutdown();
if let Ok(mut s) = self.state.lock() {
s.alive = s.alive.saturating_sub(1);
}
self.available.notify_all();
return;
}
if let Some(d) = self.dirty.as_ref() {
if let Ok(mut q) = d.lock() {
q.push_back(worker);
}
if let Some(c) = self.dirty_pending.as_ref() {
c.notify_all();
}
} else {
let mut w = worker;
w.shutdown();
if let Ok(mut s) = self.state.lock() {
s.alive = s.alive.saturating_sub(1);
}
self.available.notify_all();
}
}
}
impl Image {
pub fn from_snapshot(path: impl Into<PathBuf>) -> Result<Self, Error> {
Self::from_snapshot_inner(path.into(), false)
}
pub(crate) fn from_snapshot_pending(path: impl Into<PathBuf>) -> Result<Self, Error> {
Self::from_snapshot_inner(path.into(), true)
}
fn from_snapshot_inner(path: PathBuf, allow_pending: bool) -> Result<Self, Error> {
let (snapshot_path, metadata_path) = if path.is_dir() {
(path.join("restore.snap"), path.join("metadata.json"))
} else if path.is_file() {
let parent = path.parent().ok_or_else(|| {
Error::image_msg(format!(
"snapshot path has no parent dir: {}",
path.display()
))
})?;
(path.clone(), parent.join("metadata.json"))
} else if path
.file_name()
.map(|n| n == "restore.snap")
.unwrap_or(false)
&& path
.parent()
.map(|p| p.join("metadata.json").is_file())
.unwrap_or(false)
{
let parent = path.parent().expect("checked parent has metadata.json");
(path.clone(), parent.join("metadata.json"))
} else if allow_pending {
return Err(Error::image_msg(format!(
"snapshot path not found: {}",
path.display()
)));
} else {
return Err(Error::image_msg(format!(
"snapshot path not found: {}",
path.display()
)));
};
if !metadata_path.is_file() {
return Err(Error::image_msg(format!(
"metadata.json not found alongside snapshot at {}",
metadata_path.display()
)));
}
let meta_text = std::fs::read_to_string(&metadata_path)
.map_err(|e| Error::image_msg(format!("read {}: {e}", metadata_path.display())))?;
let meta: serde_json::Value = serde_json::from_str(&meta_text)
.map_err(|e| Error::image_msg(format!("parse {}: {e}", metadata_path.display())))?;
let backend = meta
.get("backend")
.and_then(|v| v.as_str())
.unwrap_or("hvf")
.to_string();
if !allow_pending
&& backend != "kvm"
&& backend != "sentry"
&& backend != "runsc"
&& backend != "kboxlike"
&& !snapshot_path.is_file()
{
return Err(Error::image_msg(format!(
"snapshot file not found: {}",
snapshot_path.display()
)));
}
if let Some(baked) = snapshot_baked_under_other_version(&meta) {
let current = env!("CARGO_PKG_VERSION");
return Err(Error::cache_invalid(format!(
"snapshot at {} was baked under v{baked}; current binaries are \
v{current}. Restoring across versions can produce a silently \
broken VM (e.g. no egress). Re-bake: delete the snapshot dir \
and re-run the build/pull that produced it.",
snapshot_path.display()
)));
}
let memory_mib = meta
.get("memory_mib")
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(256);
let vcpus = meta
.get("vcpus")
.and_then(|v| v.as_u64())
.map(|v| v as u32)
.unwrap_or(1);
let metadata_dir = metadata_path
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let resolve_path = |s: &str| -> PathBuf {
let p = PathBuf::from(s);
if p.is_absolute() {
p
} else {
metadata_dir.join(p)
}
};
let layers: Vec<PathBuf> = meta
.get("layers")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(resolve_path))
.collect()
})
.unwrap_or_default();
let delta_squashfs = meta
.get("delta_squashfs")
.and_then(|v| v.as_str())
.map(resolve_path);
if !allow_pending {
for layer in &layers {
if !layer.is_file() {
return Err(Error::image_msg(format!(
"snapshot at {} references missing layer file {}: a sibling \
snapshot dir was likely deleted (e.g. the base dir of a \
`__warm__<tag>` variant). Delete this dir too and re-bake.",
snapshot_path.display(),
layer.display()
)));
}
}
if let Some(delta) = &delta_squashfs {
if !delta.is_file() {
return Err(Error::image_msg(format!(
"snapshot at {} references missing delta squashfs {}: a \
sibling snapshot dir was likely deleted (e.g. the base dir \
of a `__warm__<tag>` variant). Delete this dir too and re-bake.",
snapshot_path.display(),
delta.display()
)));
}
}
}
let mounts: Vec<crate::vmm::resources::MountSpec> = meta
.get("mounts")
.and_then(|v| v.as_array())
.map(|arr| -> Result<Vec<_>, Error> {
arr.iter()
.filter_map(|x| {
let host = x.get("host_path")?.as_str()?;
let tag = x.get("guest_tag")?.as_str()?;
let guest_path = match x.get("guest_path").and_then(|v| v.as_str()) {
Some(p) => p,
None => {
return Some(Err(Error::bake_msg(format!(
"snapshot mount entry for tag `{tag}` has no \
`guest_path` — this snapshot was baked by \
supermachine 0.7.27 or earlier, which used a \
legacy non-auto-mount API. Re-bake with \
`MountSpec::new(host, tag, guest_path)` to \
pin a guest mount point."
))));
}
};
let policy = x
.get("symlinks")
.and_then(|v| v.as_str())
.and_then(parse_symlink_policy)
.unwrap_or_default();
let m = crate::vmm::resources::MountSpec::new(host, tag, guest_path)
.with_symlinks(policy);
Some(Ok(m))
})
.collect()
})
.transpose()?
.unwrap_or_default();
if !allow_pending {
for m in &mounts {
if !Path::new(&m.host_path).is_dir() {
return Err(Error::image_msg(format!(
"snapshot at {} records virtio-fs mount tag `{}` from host \
dir {} which no longer exists. Restore the directory, or \
delete the snapshot dir to re-bake with the current mounts.",
snapshot_path.display(),
m.guest_tag,
m.host_path,
)));
}
}
}
let volumes: Vec<(PathBuf, String, u64, Option<PathBuf>)> = meta
.get("volumes")
.and_then(|v| v.as_array())
.map(|arr| -> Vec<(PathBuf, String, u64, Option<PathBuf>)> {
arr.iter()
.filter_map(|x| {
let host_file = x.get("host_file")?.as_str()?;
let guest_path = x.get("guest_path")?.as_str()?;
let size_bytes = x
.get("size_bytes")
.and_then(|v| v.as_u64())
.unwrap_or(crate::vmm::resources::VolumeSpec::DEFAULT_SIZE_BYTES);
let pristine = x
.get("pristine")
.and_then(|v| v.as_str())
.map(PathBuf::from)
.filter(|p| p.is_file());
Some((
PathBuf::from(host_file),
guest_path.to_owned(),
size_bytes,
pristine,
))
})
.collect()
})
.unwrap_or_default();
let bundled_kernel = {
let cand = metadata_dir.join("kernel");
if cand.is_file() {
Some(cand)
} else {
None
}
};
let baker_runtime_sha16 = meta
.get("runtime_sha16")
.and_then(|v| v.as_str())
.map(|s| s.to_owned());
let balloon_target_pages = meta
.get("balloon_target_pages")
.and_then(|v| v.as_u64())
.and_then(|n| u32::try_from(n).ok())
.filter(|n| *n > 0);
let tsi_token = meta
.get("tsi_token")
.and_then(|v| v.as_str())
.filter(|s| s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit()))
.map(|s| s.to_ascii_lowercase());
let egress_policy = meta
.get("egress_policy")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let pre_exec_sync = meta
.get("pre_exec_sync")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let kvm = if backend == "kvm" {
let kernel = meta
.get("kvm_kernel")
.and_then(|v| v.as_str())
.map(resolve_path);
let initrd = meta
.get("kvm_initrd")
.and_then(|v| v.as_str())
.map(resolve_path);
let disk = meta
.get("kvm_disk")
.and_then(|v| v.as_str())
.map(resolve_path);
let snapshot = meta
.get("kvm_snapshot")
.and_then(|v| v.as_str())
.map(resolve_path);
if snapshot.is_none() && (kernel.is_none() || initrd.is_none()) {
return Err(Error::image_msg(
"metadata.json \"backend\":\"kvm\" needs either \"kvm_snapshot\", \
or both \"kvm_kernel\" and \"kvm_initrd\""
.to_owned(),
));
}
Some(KvmImageParts {
kernel,
initrd,
disk,
snapshot,
})
} else {
None
};
let sentry = if backend == "sentry" {
let rootfs = meta
.get("sentry_rootfs")
.and_then(|v| v.as_str())
.map(resolve_path)
.ok_or_else(|| {
Error::image_msg(
"metadata.json \"backend\":\"sentry\" needs \"sentry_rootfs\"".to_owned(),
)
})?;
if !allow_pending && !rootfs.is_dir() {
return Err(Error::image_msg(format!(
"sentry rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
rootfs.display()
)));
}
let has_workload = meta
.get("sentry_has_workload")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let user = meta
.get("user")
.and_then(|v| v.as_str())
.and_then(parse_numeric_user);
let image_env = meta
.get("image_env")
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let str_array = |v: &serde_json::Value| -> Vec<String> {
v.as_array()
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
let cmd = meta
.get("cmd")
.map(str_array)
.filter(|c| !c.is_empty())
.or_else(|| meta.get("entrypoint").map(str_array))
.unwrap_or_default();
let warm_daemon = meta
.get("warm_daemon")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(SentryImageParts {
rootfs,
has_workload,
user,
image_env,
cmd,
warm_daemon,
})
} else {
None
};
let runsc = if backend == "runsc" {
let rootfs = meta
.get("runsc_rootfs")
.or_else(|| meta.get("sentry_rootfs"))
.and_then(|v| v.as_str())
.map(resolve_path)
.ok_or_else(|| {
Error::image_msg(
"metadata.json \"backend\":\"runsc\" needs \"runsc_rootfs\"".to_owned(),
)
})?;
if !allow_pending && !rootfs.is_dir() {
return Err(Error::image_msg(format!(
"runsc rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
rootfs.display()
)));
}
let has_workload = meta
.get("runsc_has_workload")
.or_else(|| meta.get("sentry_has_workload"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let user = meta
.get("user")
.and_then(|v| v.as_str())
.and_then(parse_numeric_user);
let image_env = meta
.get("image_env")
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let str_array = |v: &serde_json::Value| -> Vec<String> {
v.as_array()
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
let cmd = meta
.get("cmd")
.map(str_array)
.filter(|c| !c.is_empty())
.or_else(|| meta.get("entrypoint").map(str_array))
.unwrap_or_default();
let warm_daemon = meta
.get("warm_daemon")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let checkpoint = meta
.get("runsc_checkpoint")
.and_then(|v| v.as_str())
.map(resolve_path);
let agent_path = meta
.get("runsc_agent_path")
.and_then(|v| v.as_str())
.unwrap_or("/.supermachine/supermachine-agent")
.to_owned();
let agent_unix_env = meta
.get("runsc_agent_unix_env")
.and_then(|v| v.as_str())
.unwrap_or("SUPERMACHINE_AGENT_UNIX_SOCKET")
.to_owned();
Some(RunscImageParts {
rootfs,
memory_mib,
vcpus,
has_workload,
user,
image_env,
cmd,
warm_daemon,
checkpoint,
mounts: mounts.clone(),
agent_path,
agent_unix_env,
})
} else {
None
};
let kboxlike = if backend == "kboxlike" {
let rootfs = meta
.get("kboxlike_rootfs")
.or_else(|| meta.get("sentry_rootfs"))
.and_then(|v| v.as_str())
.map(resolve_path)
.ok_or_else(|| {
Error::image_msg(
"metadata.json \"backend\":\"kboxlike\" needs \"kboxlike_rootfs\""
.to_owned(),
)
})?;
if !allow_pending && !rootfs.is_dir() {
return Err(Error::image_msg(format!(
"kboxlike rootfs dir not found: {}. Delete the snapshot dir and re-bake.",
rootfs.display()
)));
}
let has_workload = meta
.get("kboxlike_has_workload")
.or_else(|| meta.get("sentry_has_workload"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let user = meta
.get("user")
.and_then(|v| v.as_str())
.and_then(parse_numeric_user);
let image_env = meta
.get("image_env")
.and_then(|v| v.as_object())
.map(|o| {
o.iter()
.filter_map(|(k, val)| val.as_str().map(|s| (k.clone(), s.to_string())))
.collect()
})
.unwrap_or_default();
let str_array = |v: &serde_json::Value| -> Vec<String> {
v.as_array()
.map(|a| {
a.iter()
.filter_map(|s| s.as_str().map(str::to_owned))
.collect()
})
.unwrap_or_default()
};
let cmd = meta
.get("cmd")
.map(str_array)
.filter(|c| !c.is_empty())
.or_else(|| meta.get("entrypoint").map(str_array))
.unwrap_or_default();
let warm_daemon = meta
.get("warm_daemon")
.and_then(|v| v.as_bool())
.unwrap_or(false);
Some(KboxlikeImageParts {
rootfs,
memory_mib,
vcpus,
has_workload,
user,
image_env,
cmd,
warm_daemon,
mounts: mounts.clone(),
volumes: volumes.clone(),
})
} else {
None
};
Ok(Self {
snapshot_path,
memory_mib,
vcpus,
baker_runtime_sha16,
balloon_target_pages,
tsi_token,
egress_policy,
pre_exec_sync,
layers,
delta_squashfs,
mounts,
volumes,
bundled_kernel,
kvm,
sentry,
runsc,
kboxlike,
hidden_pool: std::sync::OnceLock::new(),
warm_baked_worker: Arc::new(crate::bake::WarmStash::new(None)),
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
kvm_pool: std::sync::OnceLock::new(),
})
}
pub fn from_oci(image_ref: &str) -> Result<Self, Error> {
Self::from_oci_with_policy(image_ref, PullPolicy::default())
}
pub fn from_oci_with_policy(image_ref: &str, policy: PullPolicy) -> Result<Self, Error> {
let snapshots_dir = default_snapshots_dir();
Self::from_oci_to_dir(image_ref, policy, &snapshots_dir, None)
}
pub fn from_oci_to_dir(
image_ref: &str,
policy: PullPolicy,
snapshots_dir: &Path,
name: Option<&str>,
) -> Result<Self, Error> {
let derived = name
.map(|s| s.to_owned())
.unwrap_or_else(|| crate::bake::snapshot_name_for_image(image_ref));
let snap_dir = snapshots_dir.join(&derived);
let cache_loadable = Self::from_snapshot(&snap_dir).is_ok();
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
let cache_loadable = {
let mut cache_loadable = cache_loadable;
if cache_loadable {
let expected = linux_default_backend();
let cached = snap_dir_backend(&snap_dir);
if cached.as_deref() != Some(expected) {
if matches!(policy, PullPolicy::Never) {
return Err(Error::cache_invalid(format!(
"cached snapshot at {} was baked for backend {:?}, but this Linux \
build resolves to backend {expected:?} (PullPolicy::Never won't \
auto-rebake)",
snap_dir.display(),
cached
)));
}
eprintln!(
"supermachine: snapshot at {} was baked for backend {:?}; current Linux \
backend is {expected:?}. Auto-rebaking.",
snap_dir.display(),
cached
);
invalidate_stale_snapshot_tree(&snap_dir);
cache_loadable = false;
}
}
cache_loadable
};
let cache_loadable = if cache_loadable && !matches!(policy, PullPolicy::Never) {
if let Some(baked) = snap_dir_baked_under_other_version(&snap_dir) {
let current = env!("CARGO_PKG_VERSION");
eprintln!(
"supermachine: snapshot at {} was baked under v{baked}; \
current binaries are v{current}. Auto-rebaking.",
snap_dir.display()
);
invalidate_stale_snapshot_tree(&snap_dir);
false
} else {
true
}
} else {
cache_loadable
};
match policy {
PullPolicy::Never => {
if cache_loadable {
return Self::from_snapshot(&snap_dir);
}
let restore_snap = snap_dir.join("restore.snap");
if restore_snap.is_file() {
return Err(Error::cache_invalid(format!(
"snapshot present at {} but not loadable on this binary; \
rebake required (PullPolicy::Never won't auto-rebake)",
snap_dir.display()
)));
}
return Err(Error::cache_miss(format!(
"no cached snapshot for {image_ref} at {} (PullPolicy::Never)",
snap_dir.display()
)));
}
PullPolicy::Missing if cache_loadable => {
return Self::from_snapshot(&snap_dir);
}
_ => {}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
match linux_default_backend() {
"runsc" => return Self::bake_runsc_from_ref(image_ref, &snap_dir),
"sentry" => return Self::bake_sentry_from_ref(image_ref, &snap_dir),
"kboxlike" => return Self::bake_kboxlike_from_ref(image_ref, &snap_dir),
"kvm" => return Self::bake_kvm_auto(image_ref, &snap_dir),
other => {
return Err(Error::bake_msg(format!(
"unsupported SUPERMACHINE_BACKEND={other:?}; expected runsc, kvm, sentry, kboxlike, or auto"
)));
}
}
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
let root = repo_root_for_bake()?;
let request = crate::bake::BakeRequest {
image: image_ref.to_owned(),
name: name.map(|s| s.to_owned()),
runtime: "supermachine".to_owned(),
guest_port: 80,
memory_mib: 256,
vcpus: 1,
pull_policy: policy.as_bake_str().to_owned(),
snapshots_dir: snapshots_dir.to_path_buf(),
cmd_override: None,
extra_args: Vec::new(),
platform: "linux/arm64".to_owned(),
};
let bake_t0 = std::time::Instant::now();
crate::bake::run_push(&request, bake_t0, &root)
.map_err(|e| map_bake_error(&request.image, e))?;
Self::from_snapshot(&snap_dir)
}
}
pub fn builder(image_ref: impl Into<String>) -> OciImageBuilder {
OciImageBuilder::new(image_ref)
}
pub fn ensure_baked<F>(
name: impl Into<String>,
image_ref: impl Into<String>,
configure: F,
) -> Result<Image, Error>
where
F: FnOnce(OciImageBuilder) -> OciImageBuilder,
{
let builder = configure(OciImageBuilder::new(image_ref).with_name(name));
builder.build()
}
pub fn snapshot_path(&self) -> &Path {
&self.snapshot_path
}
pub fn memory_mib(&self) -> u32 {
self.memory_mib
}
pub fn vcpus(&self) -> u32 {
self.vcpus
}
#[doc(hidden)]
pub fn balloon_target_pages_for_test(&self) -> Option<u32> {
self.balloon_target_pages
}
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
pub fn start(&self, config: &VmConfig) -> Result<Vm, Error> {
Vm::start(self, config)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kvm(
layer_tars: &[PathBuf],
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
Self::bake_kvm_with_workload(layer_tars, kernel, agent_initrd, None, dest_dir)
}
pub fn bake_kvm_with_workload(
layer_tars: &[PathBuf],
kernel: &Path,
agent_initrd: &Path,
workload_script: Option<&str>,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
let dest = dest_dir.into();
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let rootfs = dest.join("rootfs.squashfs");
let stage = dest.join("rootfs-stage");
crate::bake::build_kvm_rootfs_squashfs(layer_tars, &stage, &rootfs, workload_script)
.map_err(Error::bake_msg)?;
let _ = std::fs::remove_dir_all(&stage);
let phys = std::fs::metadata(&rootfs).map(|m| m.len()).unwrap_or(0)
+ std::fs::metadata(kernel).map(|m| m.len()).unwrap_or(0)
+ std::fs::metadata(agent_initrd)
.map(|m| m.len())
.unwrap_or(0);
let metadata = serde_json::json!({
"backend": "kvm",
"kvm_kernel": kernel.to_string_lossy(),
"kvm_initrd": agent_initrd.to_string_lossy(),
"kvm_disk": rootfs.to_string_lossy(),
"memory_mib": 512,
"vcpus": 1,
"balloon_target_pages": crate::bake::compute_balloon_target_pages(512),
"baked_at": chrono_rfc3339_now(),
"snapshot_physical_bytes": phys,
});
std::fs::write(
dest.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Image::from_snapshot(&dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kvm_from_ref(
image_ref: &str,
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
let dest = dest_dir.into();
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let pull_work = dest.join("oci-pull");
let t_pull = std::time::Instant::now();
let (layers, workload, oci_config) =
crate::bake::pull_oci_layers(image_ref, "amd64", &pull_work)
.map_err(Error::bake_msg)?;
let pull_ms = t_pull.elapsed().as_millis();
let t_build = std::time::Instant::now();
let img = Self::bake_kvm_with_workload(
&layers,
kernel,
agent_initrd,
workload.as_deref(),
dest.clone(),
)?;
if crate::trace::enabled("run") {
eprintln!(
"supermachine: kvm bake phases pull_oci_ms={} build_rootfs_ms={}",
pull_ms,
t_build.elapsed().as_millis()
);
}
let meta_path = dest.join("metadata.json");
if let Ok(text) = std::fs::read_to_string(&meta_path) {
if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&text) {
if let Some(obj) = json.as_object_mut() {
obj.insert("image".to_string(), serde_json::json!(image_ref));
obj.insert(
"baked_by_version".to_string(),
serde_json::json!(env!("CARGO_PKG_VERSION")),
);
if let Some(cfg) = &oci_config {
let mut env_obj = serde_json::Map::new();
for kv in cfg
.get("Env")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
if let Some(eq) = kv.find('=') {
env_obj
.insert(kv[..eq].to_string(), serde_json::json!(&kv[eq + 1..]));
}
}
obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
for (oci_key, meta_key) in [
("Cmd", "cmd"),
("Entrypoint", "entrypoint"),
("WorkingDir", "working_dir"),
("User", "user"),
] {
if let Some(v) = cfg.get(oci_key) {
if !v.is_null() {
obj.insert(meta_key.to_string(), v.clone());
}
}
}
}
if let Ok(pretty) = serde_json::to_string_pretty(&json) {
let _ = std::fs::write(&meta_path, pretty);
}
}
}
}
Ok(img)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_sentry_from_ref(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
Self::bake_sentry_with_config(image_ref, dest_dir, SentryBakeConfig::default())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_runsc_from_ref(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
Self::bake_runsc_with_config(image_ref, dest_dir, SentryBakeConfig::default())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kboxlike_from_ref(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
Self::bake_kboxlike_with_config(image_ref, dest_dir, SentryBakeConfig::default())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_runsc_with_config(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
mut config: SentryBakeConfig,
) -> Result<Image, Error> {
let runsc_bake_key = sentry_bake_cache_key(image_ref, &config)?;
let warmup = config.warmup.take();
config.warmup = None;
let dest = dest_dir.into();
let _ = Self::bake_sentry_with_config(image_ref, &dest, config)?;
let meta_path = dest.join("metadata.json");
let meta_text = std::fs::read_to_string(&meta_path)
.map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
.map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
if let Some(obj) = meta.as_object_mut() {
obj.insert("backend".to_owned(), serde_json::json!("runsc"));
if let Some(rootfs) = obj.remove("sentry_rootfs") {
obj.insert("runsc_rootfs".to_owned(), rootfs);
}
if let Some(has_workload) = obj.remove("sentry_has_workload") {
obj.insert("runsc_has_workload".to_owned(), has_workload);
}
if let Some(key) = obj.get("sentry_bake_key").cloned() {
obj.insert("runsc_bake_key".to_owned(), key);
}
obj.insert(
"runsc_bake_key".to_owned(),
serde_json::json!(runsc_bake_key),
);
}
let rootfs = meta
.get("runsc_rootfs")
.and_then(|v| v.as_str())
.map(PathBuf::from)
.ok_or_else(|| Error::bake_msg("runsc metadata missing runsc_rootfs".to_owned()))?;
let agent_path = rootfs.join(".supermachine/supermachine-agent");
if let Some(parent) = agent_path.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
if let Some(agent_override) = std::env::var_os("SUPERMACHINE_RUNSC_AGENT_PATH")
.or_else(|| std::env::var_os("SUPERMACHINE_X86_AGENT_PATH"))
{
std::fs::copy(&agent_override, &agent_path).map_err(|e| {
Error::bake_msg(format!(
"stage runsc agent {} -> {}: {e}",
PathBuf::from(agent_override).display(),
agent_path.display()
))
})?;
set_executable(&agent_path).map_err(Error::Io)?;
} else {
supermachine_kernel::extract_supermachine_agent_to(&agent_path).map_err(Error::Io)?;
}
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"runsc_agent_path".to_owned(),
serde_json::json!("/.supermachine/supermachine-agent"),
);
obj.insert(
"runsc_agent_unix_env".to_owned(),
serde_json::json!("SUPERMACHINE_AGENT_UNIX_SOCKET"),
);
}
let meta_out = serde_json::to_string_pretty(&meta)
.map_err(|e| Error::bake_msg(format!("runsc metadata serialize: {e}")))?;
std::fs::write(&meta_path, meta_out).map_err(Error::Io)?;
if let Some(warmup) = warmup {
let image = Image::from_snapshot(&dest)?;
runsc_warmup_checkpoint(&image, &dest, warmup)?;
}
Image::from_snapshot(&dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kboxlike_with_config(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
config: SentryBakeConfig,
) -> Result<Image, Error> {
let kboxlike_bake_key = sentry_bake_cache_key(image_ref, &config)?;
if config.warm_daemon {
return Err(Error::bake_msg(
"kboxlike warm-daemon images are not implemented yet; use runsc or kvm".to_owned(),
));
}
let dest = dest_dir.into();
let _ = Self::bake_sentry_with_config(image_ref, &dest, config)?;
let meta_path = dest.join("metadata.json");
let meta_text = std::fs::read_to_string(&meta_path)
.map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
.map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
if let Some(obj) = meta.as_object_mut() {
obj.insert("backend".to_owned(), serde_json::json!("kboxlike"));
if let Some(rootfs) = obj.remove("sentry_rootfs") {
obj.insert("kboxlike_rootfs".to_owned(), rootfs);
}
if let Some(has_workload) = obj.remove("sentry_has_workload") {
obj.insert("kboxlike_has_workload".to_owned(), has_workload);
}
obj.insert(
"kboxlike_bake_key".to_owned(),
serde_json::json!(kboxlike_bake_key),
);
}
let meta_out = serde_json::to_string_pretty(&meta)
.map_err(|e| Error::bake_msg(format!("kboxlike metadata serialize: {e}")))?;
std::fs::write(&meta_path, meta_out).map_err(Error::Io)?;
let restore_snap = dest.join("restore.snap");
if !restore_snap.is_file() {
std::fs::write(&restore_snap, b"kboxlike-rootfs\n").map_err(Error::Io)?;
}
Image::from_snapshot(&dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_sentry_with_config(
image_ref: &str,
dest_dir: impl Into<PathBuf>,
config: SentryBakeConfig,
) -> Result<Image, Error> {
let dest = dest_dir.into();
let sentry_bake_key = sentry_bake_cache_key(image_ref, &config)?;
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let pull_work = dest.join("oci-pull");
let (layers, image_workload, oci_config) =
crate::bake::pull_oci_layers(image_ref, "amd64", &pull_work)
.map_err(Error::bake_msg)?;
let mut env_pairs: Vec<(String, String)> = Vec::new();
if let Some(cfg) = &oci_config {
for kv in cfg
.get("Env")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
if let Some(eq) = kv.find('=') {
env_pairs.push((kv[..eq].to_string(), kv[eq + 1..].to_string()));
}
}
}
for (k, v) in &config.env {
env_pairs.retain(|(ek, _)| ek != k);
env_pairs.push((k.clone(), v.clone()));
}
let image_workdir = oci_config
.as_ref()
.and_then(|c| c.get("WorkingDir"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let effective_workdir = config
.working_dir
.clone()
.filter(|s| !s.is_empty())
.or(image_workdir);
let image_user = oci_config
.as_ref()
.and_then(|c| c.get("User"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
let effective_user = config.user.clone().filter(|s| !s.is_empty()).or(image_user);
let workload: Option<String> = if let Some(cmd) = &config.cmd {
crate::bake::sentry_workload_script(cmd, &env_pairs, effective_workdir.as_deref())
} else if !config.env.is_empty() || effective_workdir.is_some() {
let mut argv: Vec<String> = Vec::new();
if let Some(cfg) = &oci_config {
for v in cfg
.get("Entrypoint")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
argv.push(v.to_string());
}
for v in cfg
.get("Cmd")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
argv.push(v.to_string());
}
}
if argv.is_empty() {
image_workload.clone()
} else {
crate::bake::sentry_workload_script(&argv, &env_pairs, effective_workdir.as_deref())
}
} else {
image_workload.clone()
};
let rootfs = dest.join("rootfs");
crate::bake::build_sentry_rootfs(&layers, &rootfs, workload.as_deref())
.map_err(Error::bake_msg)?;
let _ = std::fs::remove_dir_all(&pull_work);
for (host, guest) in &config.extra_files {
let rel = guest.trim_start_matches('/');
let target = confined_rootfs_join(&rootfs, rel).ok_or_else(|| {
Error::bake_msg(format!(
"extra-file guest path escapes the rootfs: {guest:?}"
))
})?;
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
std::fs::copy(host, &target).map_err(|e| {
Error::bake_msg(format!(
"stage extra-file {} -> {}: {e}",
host.display(),
target.display()
))
})?;
}
let phys = dir_size_bytes(&rootfs);
let mut metadata = serde_json::json!({
"backend": "sentry",
"sentry_rootfs": rootfs.to_string_lossy(),
"sentry_has_workload": workload.is_some(),
"memory_mib": config.memory_mib.unwrap_or(512),
"vcpus": config.vcpus.unwrap_or(1),
"image": image_ref,
"sentry_bake_key": sentry_bake_key,
"baked_by_version": env!("CARGO_PKG_VERSION"),
"baked_at": chrono_rfc3339_now(),
"snapshot_physical_bytes": phys,
});
if let Some(obj) = metadata.as_object_mut() {
let mut env_obj = serde_json::Map::new();
for (k, v) in &env_pairs {
env_obj.insert(k.clone(), serde_json::json!(v));
}
obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
if let Some(cmd) = &config.cmd {
obj.insert("cmd".to_string(), serde_json::json!(cmd));
obj.remove("entrypoint");
} else if let Some(cfg) = &oci_config {
for (oci_key, meta_key) in [("Cmd", "cmd"), ("Entrypoint", "entrypoint")] {
if let Some(v) = cfg.get(oci_key) {
if !v.is_null() {
obj.insert(meta_key.to_string(), v.clone());
}
}
}
}
if let Some(dir) = &effective_workdir {
obj.insert("working_dir".to_string(), serde_json::json!(dir));
}
if let Some(user) = &effective_user {
obj.insert("user".to_string(), serde_json::json!(user));
}
obj.insert(
"warm_daemon".to_string(),
serde_json::json!(config.warm_daemon),
);
if let Some(tag) = &config.warmup_tag {
obj.insert("warmup_tag".to_string(), serde_json::json!(tag));
}
}
if let Some(obj) = metadata.as_object_mut() {
if !config.mounts.is_empty() {
let arr: Vec<serde_json::Value> = config
.mounts
.iter()
.map(|(host, tag, policy, guest_path)| {
let mut m = serde_json::Map::new();
m.insert(
"host_path".to_string(),
serde_json::json!(host.to_string_lossy()),
);
m.insert("guest_tag".to_string(), serde_json::json!(tag));
m.insert("guest_path".to_string(), serde_json::json!(guest_path));
match policy {
crate::vmm::resources::SymlinkPolicy::Opaque => {}
crate::vmm::resources::SymlinkPolicy::Deny => {
m.insert("symlinks".to_string(), serde_json::json!("deny"));
}
crate::vmm::resources::SymlinkPolicy::Follow => {
m.insert("symlinks".to_string(), serde_json::json!("follow"));
}
}
serde_json::Value::Object(m)
})
.collect();
obj.insert("mounts".to_string(), serde_json::json!(arr));
}
if !config.volumes.is_empty() {
let arr: Vec<serde_json::Value> = config
.volumes
.iter()
.map(|(host, guest, size)| {
serde_json::json!({
"host_file": host.to_string_lossy(),
"guest_path": guest,
"size_bytes": size,
})
})
.collect();
obj.insert("volumes".to_string(), serde_json::json!(arr));
}
}
{
static SENTRY_META_TMP_SEQ: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
let meta_out = serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?;
let seq = SENTRY_META_TMP_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let tmp = dest.join(format!(".metadata.json.tmp.{}.{seq}", std::process::id()));
std::fs::write(&tmp, meta_out).map_err(Error::Io)?;
if let Err(e) = std::fs::rename(&tmp, dest.join("metadata.json")) {
let _ = std::fs::remove_file(&tmp);
return Err(Error::Io(e));
}
}
let img = Image::from_snapshot(&dest)?;
if let Some(warmup) = config.warmup {
let parts = img
.sentry
.as_ref()
.ok_or_else(|| Error::vm_msg("sentry warmup image missing sentry metadata"))?;
let warm_sock = std::env::temp_dir().join(format!(
".supermachine-sentry-warmup-{}-{}.sock",
std::process::id(),
unique_suffix()
));
let sandbox = img
.sentry_sandbox_with(&VmConfig::new())
.map_err(|e| Error::vm_msg(format!("sentry warmup sandbox: {e}")))?
.without_netns()
.with_preserve_stragglers(true);
let pool = sandbox
.pool_with_exec(warm_sock.clone())
.map_err(|e| Error::vm_msg(format!("sentry warmup pool: {e}")))?;
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while !warm_sock.exists() {
if std::time::Instant::now() >= deadline {
pool.stop();
return Err(Error::vm_msg(
"sentry warmup exec socket never appeared (supervisor bind failed)",
));
}
std::thread::sleep(Duration::from_millis(5));
}
if parts.has_workload && parts.rootfs.join(".supermachine/run-workload").is_file() {
let wrapped = vec![
"/.supermachine/sm-checkpoint".to_string(),
"/bin/sh".to_string(),
"/.supermachine/run-workload".to_string(),
];
if pool.warm(wrapped.iter().cloned()).is_ok() {
if let Ok(wpid) = pool.acquire_running() {
let run_dir = parts.rootfs.join("run");
let _ = std::fs::create_dir_all(&run_dir);
let _ = std::fs::write(
run_dir.join("supermachine-workload.pid"),
format!("{wpid}\n"),
);
}
}
}
let pool = Arc::new(pool);
let vm = Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: Some(parts.rootfs.clone()),
vsock_mux_path: warm_sock.clone(),
vsock_exec_path: warm_sock,
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: Some(Arc::clone(&pool)),
runsc: None,
kboxlike: None,
};
let res = warmup(&vm);
let snap_res = if res.is_ok() {
match pool.snapshot(&dest) {
Ok(()) => Ok(()),
Err(e) if e.raw_os_error() == Some(libc::EINVAL) => Ok(()),
Err(e) => Err(Error::vm_msg(format!("sentry warmup C4 snapshot: {e}"))),
}
} else {
Ok(())
};
let _ = vm.stop();
res?;
snap_res?;
return Image::from_snapshot(&dest);
}
Ok(img)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn run_sentry(&self) -> Result<i32, Error> {
let parts = self.sentry.as_ref().ok_or_else(|| {
Error::vm_msg(
"Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
bake with Image::bake_sentry_from_ref."
.to_owned(),
)
})?;
if !parts.has_workload {
return Ok(0);
}
crate::sentry::run(
std::path::Path::new("/bin/sh"),
&["/.supermachine/run-workload".to_string()],
Some(&parts.rootfs),
)
.map_err(|e| Error::vm_msg(format!("sentry run: {e}")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn run_sentry_classified(
&self,
) -> Result<(i32, Option<crate::sentry::SentryError>), Error> {
let parts = self.sentry.as_ref().ok_or_else(|| {
Error::vm_msg(
"Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
bake with Image::bake_sentry_from_ref."
.to_owned(),
)
})?;
if !parts.has_workload {
return Ok((0, None));
}
crate::sentry::run_classified(
std::path::Path::new("/bin/sh"),
&["/.supermachine/run-workload".to_string()],
Some(&parts.rootfs),
)
.map_err(|e| Error::vm_msg(format!("sentry run: {e}")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn sentry_sandbox(&self) -> Result<crate::sentry::Sandbox, Error> {
self.sentry_sandbox_with(&VmConfig::new())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn sentry_sandbox_with(&self, config: &VmConfig) -> Result<crate::sentry::Sandbox, Error> {
let parts = self.sentry.as_ref().ok_or_else(|| {
Error::vm_msg(
"Image is not a sentry image (metadata.json needs \"backend\":\"sentry\"); \
bake with Image::bake_sentry_from_ref."
.to_owned(),
)
})?;
let mut sandbox = crate::sentry::Sandbox::new(parts.rootfs.clone());
if let Some(policy) = &self.egress_policy {
sandbox = sandbox.with_egress(policy.clone());
}
if let Some((uid, gid)) = config.sentry_uid.or(parts.user) {
sandbox = sandbox.with_uid(uid, gid);
}
if !parts.image_env.is_empty() {
sandbox = sandbox.with_env(parts.image_env.clone());
}
for m in &self.mounts {
sandbox = sandbox.with_mount(
std::path::PathBuf::from(&m.guest_path),
std::path::PathBuf::from(&m.host_path),
m.read_only,
);
}
for (host_path, guest_path, _size, _pristine) in &self.volumes {
std::fs::create_dir_all(host_path).map_err(|e| {
Error::vm_msg(format!(
"sentry volume backing dir {}: {e}",
host_path.display()
))
})?;
sandbox = sandbox.with_mount(guest_path.clone(), host_path.clone(), false);
}
if !config.sentry_uncapped {
let mem_mib = config.memory_mib.unwrap_or(self.memory_mib) as u64;
if mem_mib > 0 {
sandbox = sandbox.with_memory_limit(mem_mib * 1024 * 1024);
}
let vcpus = config.vcpus.unwrap_or(self.vcpus).max(1) as u64;
sandbox = sandbox.with_cpu_limit(vcpus * SENTRY_CPU_PERIOD_US, SENTRY_CPU_PERIOD_US);
let pids = config.sentry_pids_max.unwrap_or(SENTRY_DEFAULT_PIDS_MAX);
sandbox = sandbox.with_pids_limit(pids);
}
let exec_timeout = match config.sentry_exec_timeout {
Some(explicit) => explicit, None if config.for_build => Duration::ZERO, None => SENTRY_DEFAULT_EXEC_TIMEOUT, };
sandbox = sandbox.with_exec_timeout(exec_timeout);
for m in &config.virtiofs {
sandbox = sandbox.with_mount(
std::path::PathBuf::from(&m.guest_path),
std::path::PathBuf::from(&m.host_path),
m.read_only,
);
}
for v in &config.volumes {
let host_dir = std::path::PathBuf::from(&v.host_path);
std::fs::create_dir_all(&host_dir).map_err(Error::Io)?;
sandbox = sandbox.with_mount(std::path::PathBuf::from(&v.guest_path), host_dir, false);
}
Ok(sandbox)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn sentry_pool(&self) -> Result<crate::sentry::Pool, Error> {
self.sentry_sandbox()?
.pool()
.map_err(|e| Error::vm_msg(format!("sentry pool: {e}")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kvm_auto(image_ref: &str, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
let dest = dest_dir.into();
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let t_assets = std::time::Instant::now();
let (kernel, initrd) = Self::extract_bundled_kvm_boot_assets(&dest)?;
if crate::trace::enabled("run") {
eprintln!(
"supermachine: kvm boot-assets extract_ms={}",
t_assets.elapsed().as_millis()
);
}
Self::bake_kvm_from_ref(image_ref, &kernel, &initrd, dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn extract_bundled_kvm_boot_assets(dest: &Path) -> Result<(PathBuf, PathBuf), Error> {
let kernel = dest.join("kernel");
match std::env::var_os("SUPERMACHINE_KVM_KERNEL") {
Some(p) if std::path::Path::new(&p).is_file() => {
std::fs::copy(&p, &kernel).map_err(Error::Io)?;
}
_ => supermachine_kernel::extract_kernel_to(&kernel).map_err(Error::Io)?,
}
let assets = dest.join("agent-assets");
std::fs::create_dir_all(&assets).map_err(Error::Io)?;
let agent_bin = assets.join("supermachine-agent");
let busybox_bin = assets.join("busybox");
supermachine_kernel::extract_supermachine_agent_to(&agent_bin).map_err(Error::Io)?;
supermachine_kernel::extract_busybox_to(&busybox_bin).map_err(Error::Io)?;
let initrd = dest.join("agent.cpio");
crate::bake::build_kvm_agent_initramfs(&agent_bin, &busybox_bin, &[], &initrd)
.map_err(Error::bake_msg)?;
let _ = std::fs::remove_dir_all(&assets);
Ok((kernel, initrd))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kvm_from_squashfs(
squashfs: &Path,
kernel: &Path,
agent_initrd: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
let dest = dest_dir.into();
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let rootfs = dest.join("rootfs.squashfs");
if squashfs != rootfs {
std::fs::copy(squashfs, &rootfs).map_err(Error::Io)?;
}
let metadata = serde_json::json!({
"backend": "kvm",
"kvm_kernel": kernel.to_string_lossy(),
"kvm_initrd": agent_initrd.to_string_lossy(),
"kvm_disk": rootfs.to_string_lossy(),
"memory_mib": 512,
"vcpus": 1,
"balloon_target_pages": crate::bake::compute_balloon_target_pages(512),
});
std::fs::write(
dest.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Image::from_snapshot(&dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn bake_kvm_from_squashfs_auto(
squashfs: &Path,
dest_dir: impl Into<PathBuf>,
) -> Result<Image, Error> {
let dest = dest_dir.into();
std::fs::create_dir_all(&dest).map_err(Error::Io)?;
let (kernel, initrd) = Self::extract_bundled_kvm_boot_assets(&dest)?;
Self::bake_kvm_from_squashfs(squashfs, &kernel, &initrd, dest)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn build_kvm_initramfs(
agent_bin: &Path,
busybox_bin: &Path,
ordered_modules: &[PathBuf],
out_cpio: &Path,
) -> Result<(), Error> {
crate::bake::build_kvm_agent_initramfs(agent_bin, busybox_bin, ordered_modules, out_cpio)
.map_err(Error::bake_msg)
}
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
self.acquire_with(&VmConfig::new())
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error> {
let _span = tracing::info_span!(
"supermachine.acquire",
memory_mib = self.memory_mib,
vcpus = self.vcpus,
)
.entered();
let pool_arc = self.ensure_default_pool(config)?;
let worker = pool_arc.acquire()?;
let vm = Vm {
pool: None,
vsock_mux_path: worker.vsock_mux_path.clone(),
vsock_exec_path: worker.vsock_exec_path.clone(),
own_vsock_mux_dir: None,
skip_cleanup: true,
image_meta: Some(Arc::new(ImageMeta {
memory_mib: config.memory_mib.unwrap_or(self.memory_mib),
vcpus: config.vcpus.unwrap_or(self.vcpus),
layers: self.layers.clone(),
delta_squashfs: self.delta_squashfs.clone(),
baker_runtime_sha16: self.baker_runtime_sha16.clone(),
tsi_token: self.tsi_token.clone(),
egress_policy: self.egress_policy.clone(),
pre_exec_sync: self.pre_exec_sync,
})),
};
Ok(PooledVm {
vm: Some(vm),
worker: Some(worker),
pool_arc: Arc::clone(pool_arc),
_image: std::marker::PhantomData,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn acquire_with(&self, config: &VmConfig) -> Result<PooledVm<'_>, Error> {
if self.sentry.is_some() {
if self.kvm_pool.get().is_none() {
let pol = PoolPolicy::default();
if let Ok(sp) = SentryPool::build(
self,
config,
pol.max,
pol.acquire_timeout,
pol.restore_on_release,
) {
let _ = self.kvm_pool.set(Arc::new(Pool {
kvm: None,
sentry: Some(sp),
runsc: None,
kboxlike: None,
}));
}
}
if let Some(pool) = self.kvm_pool.get() {
return pool.acquire();
}
let vm = Vm::start(self, config)?;
return Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
});
}
if self.runsc.is_some() {
if self.kvm_pool.get().is_none() {
let pol = PoolPolicy::default();
if let Ok(rp) = RunscPoolInner::build(
self,
config,
pol.min,
pol.max,
pol.acquire_timeout,
pol.restore_on_release,
) {
let _ = self.kvm_pool.set(Arc::new(Pool {
kvm: None,
sentry: None,
runsc: Some(rp),
kboxlike: None,
}));
}
}
if let Some(pool) = self.kvm_pool.get() {
return pool.acquire();
}
let vm = Vm::start(self, config)?;
return Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
});
}
if self.kboxlike.is_some() {
if self.kvm_pool.get().is_none() {
let pol = PoolPolicy::default();
if let Ok(kp) = KboxlikePoolInner::build(
self,
config,
pol.min,
pol.max,
pol.acquire_timeout,
pol.restore_on_release,
) {
let _ = self.kvm_pool.set(Arc::new(Pool {
kvm: None,
sentry: None,
runsc: None,
kboxlike: Some(kp),
}));
}
}
if let Some(pool) = self.kvm_pool.get() {
return pool.acquire();
}
let vm = Vm::start(self, config)?;
return Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
});
}
if self.kvm_pool.get().is_none() {
if let Ok(warm) = warm_snapshot_for_pool(self, config) {
let pol = PoolPolicy::default();
let inner = Arc::new(KvmPoolInner {
image: warm,
vm_config: config.clone(),
min: 0,
idle: std::sync::Mutex::new(Vec::new()),
admission: Admission::new(pol.max, pol.acquire_timeout),
restore_on_release: pol.restore_on_release,
refilling: std::sync::atomic::AtomicBool::new(false),
shut_down: std::sync::atomic::AtomicBool::new(false),
});
let _ = self.kvm_pool.set(Arc::new(Pool {
kvm: Some(inner),
sentry: None,
runsc: None,
kboxlike: None,
}));
}
}
if let Some(pool) = self.kvm_pool.get() {
return pool.acquire();
}
let vm = Vm::start(self, config)?;
Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
})
}
pub fn pool(&self) -> PoolBuilder<'_> {
PoolBuilder {
image: self,
policy: PoolPolicy::default(),
vm_config: VmConfig::new(),
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn build_pool_arc(
&self,
config: &VmConfig,
policy: PoolPolicy,
) -> Result<Arc<HiddenPool>, Error> {
sweep_volume_temps_once();
#[cfg(target_os = "macos")]
let worker_bin = {
if let Some(p) = std::env::var_os("SUPERMACHINE_WORKER_BIN") {
let p = PathBuf::from(p);
if !p.is_file() {
return Err(Error::assets_msg(format!(
"SUPERMACHINE_WORKER_BIN={} does not exist or is not a file",
p.display()
)));
}
p
} else {
let source =
if let Some(bundled) = std::env::var_os("SUPERMACHINE_WORKER_BIN_BUNDLED") {
let p = PathBuf::from(bundled);
if !p.is_file() {
return Err(Error::assets_msg(format!(
"SUPERMACHINE_WORKER_BIN_BUNDLED={} does not exist or is not a file",
p.display()
)));
}
p
} else {
crate::codesign::locate_worker_bin().ok_or_else(|| {
Error::assets_msg(
"supermachine-worker binary not found (looked for sibling of \
current_exe and target/release/supermachine-worker). Set \
SUPERMACHINE_WORKER_BIN if you have it elsewhere."
.to_owned(),
)
})?
};
crate::assets::ensure_worker_in_user_dir(&source).map_err(Error::assets_msg)?
}
};
#[cfg(not(target_os = "macos"))]
let worker_bin: PathBuf = std::env::var_os("SUPERMACHINE_WORKER_BIN")
.map(PathBuf::from)
.ok_or_else(|| {
Error::assets_msg("SUPERMACHINE_WORKER_BIN must be set on this platform".to_owned())
})?;
#[cfg(target_os = "macos")]
{
crate::codesign::verify_worker_version(&worker_bin).map_err(Error::vm_msg)?;
if let Err(e) = crate::codesign::ensure_worker_signed(&worker_bin) {
return Err(Error::vm_msg(format!(
"supermachine-worker codesign failed: {e}"
)));
}
}
let socks_dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => PathBuf::from(format!(
"/tmp/supermachine-pool-{}-{:x}",
std::process::id(),
unique_suffix(),
)),
};
std::fs::create_dir_all(&socks_dir).map_err(Error::Io)?;
if let Some(home) = std::env::var_os("HOME") {
let cache_dir = PathBuf::from(home)
.join(".cache")
.join("supermachine")
.join("rosetta-aot");
if cache_dir.is_dir() {
let _ = scrub_aotcache_orphans(&cache_dir);
}
}
let memory_mib = config.memory_mib.unwrap_or(self.memory_mib);
let vcpus = config.vcpus.unwrap_or(self.vcpus);
let spawn_timeout = config
.restore_timeout
.unwrap_or_else(|| Duration::from_secs(30));
let name_prefix = "w".to_owned();
let spawn_cfg = Arc::new(SpawnConfig {
observed_footprint_mib: std::sync::atomic::AtomicU64::new(0),
worker_bin,
snapshot_path: self.snapshot_path.clone(),
layers: self.layers.clone(),
delta_squashfs: self.delta_squashfs.clone(),
mounts: self.mounts.clone(),
volumes: self.volumes.clone(),
memory_mib,
vcpus,
socks_dir: socks_dir.clone(),
name_prefix,
spawn_timeout,
baker_runtime_sha16: self.baker_runtime_sha16.clone(),
balloon_target_pages: self.balloon_target_pages,
tsi_token: self.tsi_token.clone(),
egress_policy: self.egress_policy.clone(),
pre_exec_sync: self.pre_exec_sync,
restore_on_release: policy.restore_on_release,
});
let claimed_warm = self.warm_baked_worker.take().and_then(|mut bw| {
match bw.child.try_wait() {
Ok(None) => Some(bw), Ok(Some(_)) => None, Err(_) => None, }
});
let initial = policy.min;
let mut idle: Vec<IdleEntry> = Vec::with_capacity(initial.max(1));
let extra_warm: usize;
if let Some(bw) = claimed_warm {
let warm_use_once = policy.restore_on_release
&& spawn_cfg.volumes.iter().any(|(_, _, _, p)| p.is_some());
let worker = warm_baked_to_worker(
bw,
spawn_cfg.memory_mib,
!spawn_cfg.mounts.is_empty() || !spawn_cfg.volumes.is_empty(),
spawn_cfg
.volumes
.iter()
.map(|(_, gp, _, _)| gp.clone())
.collect(),
spawn_cfg.vcpus,
warm_use_once,
);
if !spawn_cfg.mounts.is_empty() || !spawn_cfg.volumes.is_empty() {
let _ = drop_vfs_caches_via_agent(&worker.vsock_exec_path, spawn_cfg.memory_mib);
}
let _ = sync_time_via_agent(&worker.vsock_exec_path);
idle.push(IdleEntry {
worker,
last_used: Instant::now(),
});
extra_warm = 1;
} else {
extra_warm = 0;
}
let to_spawn = initial.saturating_sub(extra_warm);
if to_spawn == 1 {
idle.push(IdleEntry {
worker: spawn_cfg.spawn_one()?,
last_used: Instant::now(),
});
} else if to_spawn > 1 {
let mut handles = Vec::with_capacity(to_spawn);
for _ in 0..to_spawn {
let cfg = Arc::clone(&spawn_cfg);
handles.push(std::thread::spawn(move || cfg.spawn_one()));
}
for h in handles {
let w = h
.join()
.map_err(|_| Error::vm_msg("pool spawn thread panicked".to_owned()))??;
idle.push(IdleEntry {
worker: w,
last_used: Instant::now(),
});
}
}
let initial = idle.len();
let pool = Arc::new(HiddenPool {
state: Arc::new(Mutex::new(PoolState {
idle,
alive: initial,
waiting: 0,
reaped: 0,
})),
available: Arc::new(Condvar::new()),
dirty: Some(Arc::new(Mutex::new(VecDeque::new()))),
dirty_pending: Some(Arc::new(Condvar::new())),
socks_dir,
shutting_down: Arc::new(AtomicBool::new(false)),
spawn_cfg: Arc::clone(&spawn_cfg),
policy,
});
let wait_handles = pool.wait_handles();
let h_replenish = {
let h = wait_handles.clone();
std::thread::Builder::new()
.name("supermachine-pool-replenish".into())
.spawn(move || replenisher_loop(h))
.map_err(|e| Error::vm_msg(format!("spawn replenisher thread: {e}")))?
};
let mut handles = vec![h_replenish];
if policy.restore_on_release {
let restorer_count = ((policy.max + 1) / 2).clamp(1, 4).min(policy.max.max(1));
for _ in 0..restorer_count {
let h = wait_handles.clone();
let h_restore = std::thread::Builder::new()
.name("supermachine-pool-restore".into())
.spawn(move || restorer_loop(h))
.map_err(|e| Error::vm_msg(format!("spawn restorer thread: {e}")))?;
handles.push(h_restore);
}
}
if pool.policy.idle_timeout != Duration::MAX {
let h = wait_handles.clone();
let h_janitor = std::thread::Builder::new()
.name("supermachine-pool-janitor".into())
.spawn(move || janitor_loop(h))
.map_err(|e| Error::vm_msg(format!("spawn janitor thread: {e}")))?;
handles.push(h_janitor);
}
drop(handles);
Ok(pool)
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn ensure_default_pool(&self, config: &VmConfig) -> Result<&Arc<HiddenPool>, Error> {
if let Some(p) = self.hidden_pool.get() {
return Ok(p);
}
let pool = self.build_pool_arc(config, PoolPolicy::default())?;
let _ = self.hidden_pool.set(pool);
Ok(self
.hidden_pool
.get()
.expect("hidden pool just initialized"))
}
}
#[derive(Clone)]
struct PoolWaitHandles {
state: Arc<Mutex<PoolState>>,
available: Arc<Condvar>,
dirty: Option<Arc<Mutex<VecDeque<Worker>>>>,
dirty_pending: Option<Arc<Condvar>>,
shutting_down: Arc<AtomicBool>,
spawn_cfg: Arc<SpawnConfig>,
policy: PoolPolicy,
}
impl HiddenPool {
fn wait_handles(&self) -> PoolWaitHandles {
PoolWaitHandles {
state: Arc::clone(&self.state),
available: Arc::clone(&self.available),
dirty: self.dirty.as_ref().map(Arc::clone),
dirty_pending: self.dirty_pending.as_ref().map(Arc::clone),
shutting_down: Arc::clone(&self.shutting_down),
spawn_cfg: Arc::clone(&self.spawn_cfg),
policy: self.policy,
}
}
}
fn restorer_loop(h: PoolWaitHandles) {
let (Some(dirty), Some(pending)) = (h.dirty.as_ref(), h.dirty_pending.as_ref()) else {
return;
};
loop {
if h.shutting_down.load(Ordering::SeqCst) {
return;
}
let mut worker = {
let mut q = match dirty.lock() {
Ok(q) => q,
Err(_) => return,
};
loop {
if h.shutting_down.load(Ordering::SeqCst) {
return;
}
if let Some(w) = q.pop_front() {
break w;
}
q = match pending.wait_timeout(q, Duration::from_millis(100)) {
Ok((g, _)) => g,
Err(_) => return,
};
}
};
let snap_path = h.spawn_cfg.snapshot_path.clone();
match worker.send_restore(&snap_path) {
Ok(()) => {
if let Ok(mut s) = h.state.lock() {
s.idle.push(IdleEntry {
worker,
last_used: Instant::now(),
});
h.available.notify_all();
}
}
Err(_) => {
worker.shutdown();
if let Ok(mut s) = h.state.lock() {
s.alive = s.alive.saturating_sub(1);
}
h.available.notify_all();
}
}
}
}
fn replenisher_loop(h: PoolWaitHandles) {
loop {
if h.shutting_down.load(Ordering::SeqCst) {
return;
}
let need_more = {
let s = match h.state.lock() {
Ok(s) => s,
Err(_) => return,
};
s.alive < h.policy.min
};
if !need_more {
let s = match h.state.lock() {
Ok(s) => s,
Err(_) => return,
};
let _ = h.available.wait_timeout(s, Duration::from_millis(100));
continue;
}
if let Ok(mut s) = h.state.lock() {
if s.alive >= h.policy.min {
continue;
}
s.alive += 1;
}
match h.spawn_cfg.spawn_one() {
Ok(w) => {
if let Ok(mut s) = h.state.lock() {
s.idle.push(IdleEntry {
worker: w,
last_used: Instant::now(),
});
h.available.notify_all();
}
}
Err(_) => {
if let Ok(mut s) = h.state.lock() {
s.alive = s.alive.saturating_sub(1);
}
std::thread::sleep(Duration::from_millis(500));
}
}
}
}
fn janitor_loop(h: PoolWaitHandles) {
let timeout = h.policy.idle_timeout;
let min = h.policy.min;
if timeout == Duration::MAX {
return;
}
let tick = (timeout / 4).max(Duration::from_millis(100));
let wait_unit = Duration::from_millis(100).min(tick);
loop {
if h.shutting_down.load(Ordering::SeqCst) {
return;
}
let mut to_evict: Vec<Worker> = Vec::new();
let mut reaped_now = 0u64;
if let Ok(mut s) = h.state.lock() {
let now = Instant::now();
let mut i = 0;
while i < s.idle.len() {
let dead = match s.idle[i].worker.child.try_wait() {
Ok(Some(_)) => true, Ok(None) => false, Err(_) => true, };
if dead {
let entry = s.idle.remove(i);
s.alive = s.alive.saturating_sub(1);
s.reaped += 1;
reaped_now += 1;
to_evict.push(entry.worker);
} else {
i += 1;
}
}
while s.alive > min && !s.idle.is_empty() {
let oldest = &s.idle[0];
if now.duration_since(oldest.last_used) < timeout {
break;
}
let entry = s.idle.remove(0);
s.alive -= 1;
to_evict.push(entry.worker);
}
}
if reaped_now > 0 {
h.available.notify_all();
eprintln!(
"[pool-watchdog] reaped {reaped_now} dead idle worker(s) — pool will respawn \
toward min"
);
}
for mut w in to_evict {
w.shutdown();
}
let mut remaining = tick;
while remaining > Duration::ZERO && !h.shutting_down.load(Ordering::SeqCst) {
let chunk = remaining.min(wait_unit);
if let Ok(s) = h.state.lock() {
let _ = h.available.wait_timeout(s, chunk);
}
remaining = remaining.saturating_sub(chunk);
}
}
}
pub struct PooledVm<'a> {
vm: Option<Vm>,
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
worker: Option<Worker>,
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pool_arc: Arc<HiddenPool>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pool: Option<Arc<KvmPoolInner>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_pool: Option<Arc<SentryPool>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
runsc_pool: Option<Arc<RunscPoolInner>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
kboxlike_pool: Option<Arc<KboxlikePoolInner>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_mem_admission: Option<crate::memory_admission::AdmissionGuard>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_running_pid: Option<i32>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_running_pool: Option<Arc<crate::sentry::Pool>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_stop_running_pool_on_drop: bool,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry_touched: AtomicBool,
_image: std::marker::PhantomData<&'a Image>,
}
impl std::ops::Deref for PooledVm<'_> {
type Target = Vm;
fn deref(&self) -> &Vm {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if self.sentry_pool.is_some() {
self.sentry_touched.store(true, Ordering::Relaxed);
}
self.vm.as_ref().expect("PooledVm used after drop")
}
}
impl std::ops::DerefMut for PooledVm<'_> {
fn deref_mut(&mut self) -> &mut Vm {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if self.sentry_pool.is_some() {
self.sentry_touched.store(true, Ordering::Relaxed);
}
self.vm.as_mut().expect("PooledVm used after drop")
}
}
impl PooledVm<'_> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
if let Some(sp) = &self.sentry_pool {
let dest_dir = dest_dir.into();
let image = write_sentry_snapshot_dir_inner(
&sp.rootfs,
&dest_dir,
"Vm::snapshot_live",
true,
Some(sp.mem_mib as u32),
Some(sp.vcpus),
)?;
if let Some(pool) = &self.sentry_running_pool {
pool.snapshot(&dest_dir)
.map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}")))?;
} else {
sp.capture_live_state_into(&dest_dir)?;
}
return Ok(image);
}
if self.runsc_pool.is_some() {
let dest_dir = dest_dir.into();
let vm = self
.vm
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
let runsc = vm.runsc.as_ref().ok_or_else(|| {
Error::vm_msg("PooledVm::snapshot: runsc pool checkout has no runtime".to_owned())
})?;
return write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot_live", true);
}
self.vm
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?
.snapshot_live(dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot_diff(
&self,
dest_dir: impl Into<PathBuf>,
base_path: &Path,
) -> Result<Image, Error> {
if let Some(sp) = &self.sentry_pool {
let _ = base_path;
let dest_dir = dest_dir.into();
let image = write_sentry_snapshot_dir_inner(
&sp.rootfs,
&dest_dir,
"Vm::snapshot_live",
true,
Some(sp.mem_mib as u32),
Some(sp.vcpus),
)?;
if let Some(pool) = &self.sentry_running_pool {
pool.snapshot(&dest_dir)
.map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}")))?;
} else {
sp.capture_live_state_into(&dest_dir)?;
}
return Ok(image);
}
if self.runsc_pool.is_some() {
let _ = base_path;
let dest_dir = dest_dir.into();
let vm = self
.vm
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
let runsc = vm.runsc.as_ref().ok_or_else(|| {
Error::vm_msg(
"PooledVm::snapshot_diff: runsc pool checkout has no runtime".to_owned(),
)
})?;
return write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot_live", true);
}
self.vm
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?
.snapshot_diff_live(dest_dir, base_path)
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn snapshot(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
self.snapshot_with_opt_base(dest_dir, None)
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn snapshot_diff(
&self,
dest_dir: impl Into<PathBuf>,
base_path: &Path,
) -> Result<Image, Error> {
self.snapshot_with_opt_base(dest_dir, Some(base_path))
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn snapshot_with_opt_base(
&self,
dest_dir: impl Into<PathBuf>,
base_path: Option<&Path>,
) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
let _span = tracing::info_span!(
"supermachine.snapshot",
dest_dir = %dest_dir.display(),
)
.entered();
let worker = self
.worker
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no worker (already dropped?)".to_owned()))?;
let vm = self
.vm
.as_ref()
.ok_or_else(|| Error::vm_msg("PooledVm: no vm (already dropped?)".to_owned()))?;
let meta = vm.image_meta.clone().ok_or_else(|| {
Error::vm_msg(
"PooledVm::snapshot: image metadata missing (acquire from a 0.3.8+ Image)"
.to_owned(),
)
})?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let snap_path = dest_dir.join("restore.snap");
let _ = guest_sync_via_agent(&worker.vsock_exec_path);
let parked = if meta.vcpus > 1 {
worker.send_smpark_park()?
} else {
false
};
let snap_result = match base_path {
Some(base) => worker.send_snapshot_with_base(&snap_path, base, true),
None => worker.send_snapshot(&snap_path),
};
if parked {
let _ = worker.send_smpark_unpark()?;
}
let _stats = snap_result?;
let metadata = serde_json::json!({
"backend": "hvf",
"memory_mib": meta.memory_mib,
"vcpus": meta.vcpus,
"layers": meta
.layers
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>(),
"delta_squashfs": meta
.delta_squashfs
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
"snapshot_base": snap_path.to_string_lossy().to_string(),
"runtime_sha16": meta.baker_runtime_sha16,
"tsi_token": meta.tsi_token,
"egress_policy": meta.egress_policy,
"pre_exec_sync": meta.pre_exec_sync,
"baked_by_version": env!("CARGO_PKG_VERSION"),
"baked_at": chrono_rfc3339_now(),
"source": "PooledVm::snapshot",
});
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Image::from_snapshot(&dest_dir)
}
}
impl Drop for PooledVm<'_> {
fn drop(&mut self) {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
let _ = self.vm.take();
if let Some(worker) = self.worker.take() {
self.pool_arc.release(worker);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(sp) = self.sentry_pool.take() {
let touched = self.sentry_touched.load(Ordering::Relaxed);
let had_running_checkout = self.sentry_running_pid.is_some();
if let (Some(pid), Some(pool)) = (
self.sentry_running_pid.take(),
self.sentry_running_pool.take(),
) {
let _ = pool.release(pid);
let _ = self.vm.take();
if self.sentry_stop_running_pool_on_drop {
pool.stop();
}
} else {
let _ = self.vm.take();
}
let shutting_down = sp.shut_down.load(std::sync::atomic::Ordering::SeqCst);
if !shutting_down && (touched || had_running_checkout) {
if let Err(e) = sp.restore_rootfs_from_baseline() {
eprintln!(
"supermachine: sentry rootfs restore_on_release reset failed ({e}); \
next acquire may observe dirty filesystem state"
);
}
}
let _ = self.sentry_mem_admission.take();
sp.admission.release();
sp.cleanup_if_shutdown_idle();
return;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(pool) = self.runsc_pool.take() {
if let Some(vm) = self.vm.take() {
pool.release_vm(vm);
} else {
pool.release_checkout();
pool.admission.release();
}
return;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(pool) = self.kboxlike_pool.take() {
if let Some(vm) = self.vm.take() {
pool.release_vm(vm);
} else {
pool.admission.release();
}
return;
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(pool) = self.pool.take() {
let Some(vm) = self.vm.take() else {
return;
};
let warm_slot = {
let idle = pool.idle.lock().unwrap_or_else(|e| e.into_inner());
idle.len() < pool.min
};
if !warm_slot {
drop(vm);
pool.admission.release();
return;
}
if pool.restore_on_release {
if let Err(e) = vm.reset_to_snapshot() {
eprintln!(
"supermachine: KVM in-place reset failed ({e}); \
tearing down this VM instead of reusing it"
);
drop(vm);
pool.admission.release();
pool.refill_async();
return;
}
let _ = sync_time_via_agent(&vm.vsock_exec_path);
}
pool.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
pool.admission.notify(); } else {
let _ = self.vm.take();
}
}
}
pub struct PoolBuilder<'a> {
image: &'a Image,
policy: PoolPolicy,
vm_config: VmConfig,
}
impl PoolBuilder<'_> {
pub fn min(mut self, n: usize) -> Self {
self.policy.min = n;
self
}
pub fn max(mut self, n: usize) -> Self {
self.policy.max = n.max(1);
self
}
pub fn idle_timeout(mut self, d: Duration) -> Self {
self.policy.idle_timeout = d;
self
}
pub fn acquire_timeout(mut self, d: Duration) -> Self {
self.policy.acquire_timeout = Some(d);
self
}
pub fn no_acquire_timeout(mut self) -> Self {
self.policy.acquire_timeout = None;
self
}
pub fn restore_on_release(mut self, on: bool) -> Self {
self.policy.restore_on_release = on;
self
}
pub fn for_build(mut self, on: bool) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_for_build(on);
self
}
pub fn with_restore_timeout(mut self, timeout: Duration) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_restore_timeout(timeout);
self
}
pub fn with_memory_mib(mut self, mib: u32) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_memory_mib(mib);
self
}
pub fn with_sentry_exec_timeout(mut self, dur: Duration) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_sentry_exec_timeout(dur);
self
}
pub fn with_vsock_mux_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_vsock_mux_dir(dir);
self
}
pub fn with_vcpus(mut self, vcpus: u32) -> Self {
self.vm_config = std::mem::take(&mut self.vm_config).with_vcpus(vcpus);
self
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn build(self) -> Result<Pool, Error> {
let image = self.image;
let mut policy = self.policy;
if policy.min > policy.max {
policy.min = policy.max;
}
let arc = image.build_pool_arc(&self.vm_config, policy)?;
match image.hidden_pool.set(Arc::clone(&arc)) {
Ok(()) => Ok(Pool { inner: arc }),
Err(_) => Err(Error::vm_msg(
"this Image already has a Pool — pool() can only be called \
once per Image handle.\n\n\
To run two independent pools off the same snapshot, load the \
Image twice via Image.fromSnapshot:\n\n \
const a = await Image.fromSnapshot(snapshotPath);\n \
const b = await Image.fromSnapshot(snapshotPath);\n \
const poolA = await a.pool(...);\n \
const poolB = await b.pool(...);\n\n\
If you called image.acquire() before image.pool(), the \
Image has been bound to a default pool whose policy can't \
be changed in place — either reorder the calls or use \
fromSnapshot() to get a fresh handle."
.to_owned(),
)),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn build(self) -> Result<Pool, Error> {
if self.image.kvm_pool.get().is_some() {
return Err(Error::vm_msg(
"this Image already has a default Pool — image.acquire() was \
called before pool().build(), binding a default pool whose \
policy can't be changed in place. Reorder so pool().build() \
runs first, or load a fresh handle via Image::from_snapshot."
.to_owned(),
));
}
if self.image.sentry.is_some() {
let sp = SentryPool::build(
self.image,
&self.vm_config,
self.policy.max,
self.policy.acquire_timeout,
self.policy.restore_on_release,
)?;
return Ok(Pool {
kvm: None,
sentry: Some(sp),
runsc: None,
kboxlike: None,
});
}
if self.image.runsc.is_some() {
let min = self.policy.min.min(self.policy.max.max(1));
let rp = RunscPoolInner::build(
self.image,
&self.vm_config,
min,
self.policy.max,
self.policy.acquire_timeout,
self.policy.restore_on_release,
)?;
return Ok(Pool {
kvm: None,
sentry: None,
runsc: Some(rp),
kboxlike: None,
});
}
if self.image.kboxlike.is_some() {
let min = self.policy.min.min(self.policy.max.max(1));
let kp = KboxlikePoolInner::build(
self.image,
&self.vm_config,
min,
self.policy.max,
self.policy.acquire_timeout,
self.policy.restore_on_release,
)?;
return Ok(Pool {
kvm: None,
sentry: None,
runsc: None,
kboxlike: Some(kp),
});
}
let warm = warm_snapshot_for_pool(self.image, &self.vm_config)?;
let min = self.policy.min.min(self.policy.max.max(1));
let inner = Arc::new(KvmPoolInner {
image: warm,
vm_config: self.vm_config,
min,
idle: std::sync::Mutex::new(Vec::new()),
admission: Admission::new(self.policy.max, self.policy.acquire_timeout),
restore_on_release: self.policy.restore_on_release,
refilling: std::sync::atomic::AtomicBool::new(false),
shut_down: std::sync::atomic::AtomicBool::new(false),
});
inner.prewarm_to_min();
Ok(Pool {
kvm: Some(inner),
sentry: None,
runsc: None,
kboxlike: None,
})
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_kvm_volumes(
specs: &[crate::vmm::resources::VolumeSpec],
) -> Result<Vec<crate::kvm::run::VolumeAttach>, Error> {
let mut out = Vec::with_capacity(specs.len());
for spec in specs {
let path = std::path::Path::new(&spec.host_path);
let fresh = !path.exists();
if fresh {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
crate::ext4::make_empty_ext4(path, spec.size_bytes)
.map_err(|e| Error::vm_msg(format!("format volume {}: {e}", spec.host_path)))?;
}
out.push(crate::kvm::run::VolumeAttach {
path: spec.host_path.clone(),
size: spec.size_bytes,
mount: spec.guest_path.clone(),
});
}
Ok(out)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn confined_rootfs_join(rootfs: &Path, rel: &str) -> Option<PathBuf> {
let p = Path::new(rel);
let escapes = p.is_absolute()
|| p.components().any(|c| {
matches!(
c,
std::path::Component::ParentDir | std::path::Component::RootDir
)
});
if escapes || rel.is_empty() {
return None;
}
Some(rootfs.join(p))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn persist_kvm_builder_volumes(
snap_dir: &Path,
volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
let meta_path = snap_dir.join("metadata.json");
let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
let mut meta: serde_json::Value = serde_json::from_str(&text)
.map_err(|e| Error::vm_msg(format!("parse {}: {e}", meta_path.display())))?;
let arr: Vec<serde_json::Value> = volumes
.iter()
.map(|(host, guest, size)| {
serde_json::json!({
"host_file": host.to_string_lossy(),
"guest_path": guest,
"size_bytes": size,
})
})
.collect();
if let Some(obj) = meta.as_object_mut() {
obj.insert("volumes".into(), serde_json::json!(arr));
}
std::fs::write(
&meta_path,
serde_json::to_string_pretty(&meta)
.map_err(|e| Error::vm_msg(format!("serialize metadata: {e}")))?,
)
.map_err(Error::Io)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kvm_bake_key_matches(snap_dir: &Path, expected: &str) -> bool {
std::fs::read_to_string(snap_dir.join("metadata.json"))
.ok()
.and_then(|text| serde_json::from_str::<serde_json::Value>(&text).ok())
.and_then(|meta| {
meta.get("kvm_bake_key")
.and_then(|v| v.as_str())
.map(str::to_owned)
})
.as_deref()
== Some(expected)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn builder_mount_specs(
mounts: &[(
PathBuf,
String,
crate::vmm::resources::SymlinkPolicy,
String,
)],
) -> Vec<crate::vmm::resources::MountSpec> {
mounts
.iter()
.map(|(host, tag, policy, guest_path)| {
crate::vmm::resources::MountSpec::new(
host.to_string_lossy().to_string(),
tag.clone(),
guest_path.clone(),
)
.with_symlinks(policy.clone())
})
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn effective_builder_workload(
image_workload: Option<String>,
oci_config: Option<&serde_json::Value>,
cmd: Option<&Vec<String>>,
envs: &[(String, String)],
) -> (
Option<String>,
Vec<(String, String)>,
Option<String>,
Option<String>,
) {
let mut env_pairs: Vec<(String, String)> = Vec::new();
if let Some(cfg) = oci_config {
for kv in cfg
.get("Env")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
if let Some(eq) = kv.find('=') {
env_pairs.push((kv[..eq].to_string(), kv[eq + 1..].to_string()));
}
}
}
for (k, v) in envs {
env_pairs.retain(|(ek, _)| ek != k);
env_pairs.push((k.clone(), v.clone()));
}
let effective_workdir = oci_config
.and_then(|c| c.get("WorkingDir"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_owned);
let effective_user = oci_config
.and_then(|c| c.get("User"))
.and_then(|v| v.as_str())
.filter(|s| !s.is_empty())
.map(str::to_owned);
let workload = if let Some(cmd) = cmd {
crate::bake::sentry_workload_script(cmd, &env_pairs, effective_workdir.as_deref())
} else if !envs.is_empty() || effective_workdir.is_some() {
let mut argv: Vec<String> = Vec::new();
if let Some(cfg) = oci_config {
for v in cfg
.get("Entrypoint")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
argv.push(v.to_string());
}
for v in cfg
.get("Cmd")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|v| v.as_str())
{
argv.push(v.to_string());
}
}
if argv.is_empty() {
image_workload
} else {
crate::bake::sentry_workload_script(&argv, &env_pairs, effective_workdir.as_deref())
}
} else {
image_workload
};
(workload, env_pairs, effective_workdir, effective_user)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn mount_specs_metadata(mounts: &[crate::vmm::resources::MountSpec]) -> Vec<serde_json::Value> {
mounts
.iter()
.map(|m| {
let mut obj = serde_json::Map::new();
obj.insert("host_path".to_string(), serde_json::json!(m.host_path));
obj.insert("guest_tag".to_string(), serde_json::json!(m.guest_tag));
obj.insert("guest_path".to_string(), serde_json::json!(m.guest_path));
match m.symlinks {
crate::vmm::resources::SymlinkPolicy::Opaque => {}
crate::vmm::resources::SymlinkPolicy::Deny => {
obj.insert("symlinks".to_string(), serde_json::json!("deny"));
}
crate::vmm::resources::SymlinkPolicy::Follow => {
obj.insert("symlinks".to_string(), serde_json::json!("follow"));
}
}
serde_json::Value::Object(obj)
})
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn volume_specs_metadata(volumes: &[(PathBuf, String, u64)]) -> Vec<serde_json::Value> {
volumes
.iter()
.map(|(host, guest, size)| {
serde_json::json!({
"host_file": host.to_string_lossy(),
"guest_path": guest,
"size_bytes": size,
})
})
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn insert_kvm_builder_run_config(
obj: &mut serde_json::Map<String, serde_json::Value>,
image_ref: &str,
kvm_bake_key: &str,
env_pairs: &[(String, String)],
cmd: Option<&Vec<String>>,
oci_config: Option<&serde_json::Value>,
effective_workdir: Option<&str>,
effective_user: Option<&str>,
warm_daemon: bool,
warmup_tag: Option<&str>,
mounts: &[crate::vmm::resources::MountSpec],
volumes: &[(PathBuf, String, u64)],
) {
obj.insert("image".to_string(), serde_json::json!(image_ref));
obj.insert("kvm_bake_key".to_string(), serde_json::json!(kvm_bake_key));
obj.insert(
"baked_by_version".to_string(),
serde_json::json!(env!("CARGO_PKG_VERSION")),
);
let mut env_obj = serde_json::Map::new();
for (k, v) in env_pairs {
env_obj.insert(k.clone(), serde_json::json!(v));
}
obj.insert("image_env".to_string(), serde_json::Value::Object(env_obj));
if let Some(cmd) = cmd {
obj.insert("cmd".to_string(), serde_json::json!(cmd));
obj.remove("entrypoint");
} else if let Some(cfg) = oci_config {
for (oci_key, meta_key) in [("Cmd", "cmd"), ("Entrypoint", "entrypoint")] {
if let Some(v) = cfg.get(oci_key) {
if !v.is_null() {
obj.insert(meta_key.to_string(), v.clone());
}
}
}
}
if let Some(dir) = effective_workdir {
obj.insert("working_dir".to_string(), serde_json::json!(dir));
}
if let Some(user) = effective_user {
obj.insert("user".to_string(), serde_json::json!(user));
}
obj.insert("warm_daemon".to_string(), serde_json::json!(warm_daemon));
if let Some(tag) = warmup_tag {
obj.insert("warmup_tag".to_string(), serde_json::json!(tag));
}
if !mounts.is_empty() {
obj.insert(
"mounts".to_string(),
serde_json::Value::Array(mount_specs_metadata(mounts)),
);
}
if !volumes.is_empty() {
obj.insert(
"volumes".to_string(),
serde_json::Value::Array(volume_specs_metadata(volumes)),
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[allow(clippy::too_many_arguments)]
fn write_kvm_cold_builder_metadata(
dest_dir: &Path,
image_ref: &str,
kvm_bake_key: &str,
kernel: &Path,
initrd: &Path,
rootfs: &Path,
memory_mib: u32,
vcpus: u32,
physical_bytes: u64,
env_pairs: &[(String, String)],
cmd: Option<&Vec<String>>,
oci_config: Option<&serde_json::Value>,
effective_workdir: Option<&str>,
effective_user: Option<&str>,
warm_daemon: bool,
warmup_tag: Option<&str>,
mounts: &[crate::vmm::resources::MountSpec],
volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
let mut metadata = serde_json::json!({
"backend": "kvm",
"kvm_kernel": kernel.to_string_lossy(),
"kvm_initrd": initrd.to_string_lossy(),
"kvm_disk": rootfs.to_string_lossy(),
"memory_mib": memory_mib,
"vcpus": vcpus,
"balloon_target_pages": crate::bake::compute_balloon_target_pages(memory_mib),
"baked_at": chrono_rfc3339_now(),
"snapshot_physical_bytes": physical_bytes,
});
if let Some(obj) = metadata.as_object_mut() {
insert_kvm_builder_run_config(
obj,
image_ref,
kvm_bake_key,
env_pairs,
cmd,
oci_config,
effective_workdir,
effective_user,
warm_daemon,
warmup_tag,
mounts,
volumes,
);
}
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::bake_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[allow(clippy::too_many_arguments)]
fn persist_kvm_builder_run_config(
snap_dir: &Path,
image_ref: &str,
kvm_bake_key: &str,
env_pairs: &[(String, String)],
cmd: Option<&Vec<String>>,
oci_config: Option<&serde_json::Value>,
effective_workdir: Option<&str>,
effective_user: Option<&str>,
warm_daemon: bool,
warmup_tag: Option<&str>,
mounts: &[crate::vmm::resources::MountSpec],
volumes: &[(PathBuf, String, u64)],
) -> Result<(), Error> {
let meta_path = snap_dir.join("metadata.json");
let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
let mut meta: serde_json::Value = serde_json::from_str(&text)
.map_err(|e| Error::vm_msg(format!("parse {}: {e}", meta_path.display())))?;
let obj = meta
.as_object_mut()
.ok_or_else(|| Error::vm_msg("KVM metadata is not an object".to_owned()))?;
insert_kvm_builder_run_config(
obj,
image_ref,
kvm_bake_key,
env_pairs,
cmd,
oci_config,
effective_workdir,
effective_user,
warm_daemon,
warmup_tag,
mounts,
volumes,
);
std::fs::write(
&meta_path,
serde_json::to_string_pretty(&meta)
.map_err(|e| Error::vm_msg(format!("serialize metadata: {e}")))?,
)
.map_err(Error::Io)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_kvm_snapshot_metadata(
dest_dir: &Path,
snap_rel: &str,
base: Option<&Path>,
mem_size: usize,
num_cpus: u8,
) -> Result<(), Error> {
let mut meta = serde_json::json!({
"backend": "kvm",
"kvm_snapshot": snap_rel,
"memory_mib": (mem_size >> 20) as u32,
"vcpus": num_cpus as u32,
"baked_at": chrono_rfc3339_now(),
"source": "Vm::snapshot_live",
});
if let Some(b) = base {
meta["kvm_snapshot_base"] = serde_json::json!(b.to_string_lossy());
meta["source"] = serde_json::json!("Vm::snapshot_diff_live");
}
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&meta)
.map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kvm_effective_vcpus(requested: u32, context: &str) -> u32 {
let requested = requested.max(1);
if requested > 1 {
eprintln!(
"supermachine(KVM): {context} requested {requested} vCPUs, but Linux/x86_64 KVM \
SMP is not enabled in this backend yet; using 1 vCPU"
);
1
} else {
requested
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_snapshot_for_pool(image: &Image, config: &VmConfig) -> Result<Image, Error> {
if image
.kvm
.as_ref()
.and_then(|k| k.snapshot.as_ref())
.is_some()
{
return Ok(image.clone());
}
let trace = crate::trace::enabled("pool");
let mem = config.memory_mib.unwrap_or(image.memory_mib);
let vcpus = kvm_effective_vcpus(
config.vcpus.unwrap_or(image.vcpus),
"warm_snapshot_for_pool",
);
let snapshots_dir = default_snapshots_dir();
let family = warm_family_key(image);
let worker_fp = warm_worker_fingerprint();
let warm_name = format!("{family}__kvmwarm__{mem}m{vcpus}v__{worker_fp}");
let warm_dir = snapshots_dir.join(&warm_name);
if let Ok(img) = Image::from_snapshot(&warm_dir) {
if trace {
eprintln!("[warm-pool] cache hit: {}", warm_dir.display());
}
return Ok(img);
}
let _lock = WarmDirLock::acquire(&snapshots_dir, &warm_name);
if let Ok(img) = Image::from_snapshot(&warm_dir) {
if trace {
eprintln!("[warm-pool] cache hit (post-lock): {}", warm_dir.display());
}
return Ok(img);
}
if trace {
eprintln!(
"[warm-pool] cold warm: boot + warmup -> {}",
warm_dir.display()
);
}
let vm = Vm::start(image, config)?;
let sync_deadline = std::time::Instant::now() + std::time::Duration::from_secs(180);
loop {
if guest_sync_via_agent(&vm.vsock_exec_path) {
break;
}
if std::time::Instant::now() >= sync_deadline {
let _ = vm.stop();
return Err(Error::vm_msg(
"KVM warm-pool capture refused: guest agent did not answer sync before deadline"
.to_owned(),
));
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
let partial = snapshots_dir.join(format!("{warm_name}.partial.{}", std::process::id()));
let _ = std::fs::remove_dir_all(&partial);
let _ = vm.snapshot(&partial)?;
finalize_warm_snapshot_metadata(&partial, &family, mem, &worker_fp)?;
dedup_warm_against_golden(&snapshots_dir, &partial, mem, vcpus, &worker_fp, trace);
match std::fs::rename(&partial, &warm_dir) {
Ok(()) => {}
Err(_) if warm_dir.is_dir() => {
let _ = std::fs::remove_dir_all(&partial);
}
Err(e) => {
let _ = std::fs::remove_dir_all(&partial);
return Err(Error::Io(e));
}
}
prune_stale_warm_artifacts(&snapshots_dir, &worker_fp);
Image::from_snapshot(&warm_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prune_stale_warm_artifacts(snapshots_dir: &Path, current_fp: &str) {
let grace = std::time::Duration::from_secs(60);
let now = std::time::SystemTime::now();
let old_enough = |p: &Path| -> bool {
std::fs::metadata(p)
.and_then(|m| m.modified())
.map(|mt| now.duration_since(mt).map(|d| d >= grace).unwrap_or(false))
.unwrap_or(false)
};
if let Ok(rd) = std::fs::read_dir(snapshots_dir) {
for entry in rd.flatten() {
let p = entry.path();
let Some(name) = p.file_name().and_then(|s| s.to_str()) else {
continue;
};
if name.contains("__kvmwarm__")
&& name.rsplit("__").next() != Some(current_fp)
&& p.is_dir()
&& old_enough(&p)
{
let _ = std::fs::remove_dir_all(&p);
}
}
}
let golden_dir = snapshots_dir.join(".kvmgolden");
if let Ok(rd) = std::fs::read_dir(&golden_dir) {
for entry in rd.flatten() {
let p = entry.path();
if p.extension().and_then(|s| s.to_str()) != Some("snap") {
continue;
}
let stale = p
.file_stem()
.and_then(|s| s.to_str())
.and_then(|s| s.rsplit("__").next())
.map(|fp| fp != current_fp)
.unwrap_or(false);
if stale && old_enough(&p) {
let _ = std::fs::remove_file(&p);
}
}
}
}
#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod warm_gc_tests {
use super::*;
fn set_mtime_secs_ago(p: &Path, secs_ago: i64) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let tv = libc::timeval {
tv_sec: now - secs_ago,
tv_usec: 0,
};
let times = [tv, tv];
let c = std::ffi::CString::new(p.to_str().unwrap()).unwrap();
unsafe {
libc::utimes(c.as_ptr(), times.as_ptr());
}
}
#[test]
fn prune_removes_stale_fp_keeps_current_and_fresh() {
let root = std::env::temp_dir().join(format!("sm-gc-test-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let golden_dir = root.join(".kvmgolden");
std::fs::create_dir_all(&golden_dir).unwrap();
let cur = "aaaa11112222";
let old = "bbbb33334444";
let warm_cur = root.join(format!("nginx__kvmwarm__512m1v__{cur}"));
let warm_old = root.join(format!("nginx__kvmwarm__512m1v__{old}"));
let warm_old_fresh = root.join(format!("alpine__kvmwarm__512m1v__{old}"));
for d in [&warm_cur, &warm_old, &warm_old_fresh] {
std::fs::create_dir_all(d).unwrap();
std::fs::write(d.join("vm.snap"), b"x").unwrap();
}
let gold_cur = golden_dir.join(format!("512m1v__{cur}.snap"));
let gold_old = golden_dir.join(format!("512m1v__{old}.snap"));
std::fs::write(&gold_cur, b"x").unwrap();
std::fs::write(&gold_old, b"x").unwrap();
for p in [&warm_cur, &warm_old, &gold_cur, &gold_old] {
set_mtime_secs_ago(p, 120);
}
set_mtime_secs_ago(&warm_old_fresh, 1);
prune_stale_warm_artifacts(&root, cur);
assert!(warm_cur.is_dir(), "current-fp warm dir must be kept");
assert!(
!warm_old.exists(),
"stale-fp warm dir (aged) must be pruned"
);
assert!(
warm_old_fresh.is_dir(),
"stale-fp warm dir within grace must be kept"
);
assert!(gold_cur.exists(), "current-fp golden must be kept");
assert!(!gold_old.exists(), "stale-fp golden (aged) must be pruned");
let _ = std::fs::remove_dir_all(&root);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_family_key(image: &Image) -> String {
let dir = image.snapshot_path.parent().unwrap_or(&image.snapshot_path);
let raw = dir.file_name().and_then(|s| s.to_str()).unwrap_or("");
let sanitized: String = raw
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect();
if sanitized.is_empty() || sanitized == "." || sanitized == ".." {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
image.snapshot_path.hash(&mut h);
format!("img-{:016x}", h.finish())
} else {
sanitized
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn warm_worker_fingerprint() -> String {
std::env::var_os("SUPERMACHINE_WORKER_BIN")
.map(PathBuf::from)
.and_then(|p| current_worker_sha16(&p))
.map(|sha| sha.chars().take(12).collect::<String>())
.unwrap_or_else(|| "noworker".to_owned())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn finalize_warm_snapshot_metadata(
partial: &Path,
family: &str,
mem: u32,
worker_fp: &str,
) -> Result<(), Error> {
let meta_path = partial.join("metadata.json");
let text = std::fs::read_to_string(&meta_path).map_err(Error::Io)?;
let mut meta: serde_json::Value =
serde_json::from_str(&text).map_err(|e| Error::vm_msg(format!("warm metadata: {e}")))?;
let obj = meta
.as_object_mut()
.ok_or_else(|| Error::vm_msg("warm metadata is not an object".to_owned()))?;
obj.insert("kvm_snapshot".into(), serde_json::json!("vm.snap"));
obj.insert("image".into(), serde_json::json!(family));
obj.insert("memory_mib".into(), serde_json::json!(mem));
obj.insert(
"baked_by_version".into(),
serde_json::json!(env!("CARGO_PKG_VERSION")),
);
obj.insert("warm_worker_fp".into(), serde_json::json!(worker_fp));
obj.insert("source".into(), serde_json::json!("warm_snapshot_for_pool"));
std::fs::write(
&meta_path,
serde_json::to_string_pretty(&meta)
.map_err(|e| Error::vm_msg(format!("warm metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn dedup_warm_against_golden(
snapshots_dir: &Path,
partial: &Path,
mem: u32,
vcpus: u32,
worker_fp: &str,
trace: bool,
) {
if std::env::var("SUPERMACHINE_AUTO_DEDUP").as_deref() == Ok("0") {
return;
}
let warm_snap = partial.join("vm.snap");
let golden_dir = snapshots_dir.join(".kvmgolden");
if std::fs::create_dir_all(&golden_dir).is_err() {
return;
}
let golden = golden_dir.join(format!("{mem}m{vcpus}v__{worker_fp}.snap"));
let _lock = WarmDirLock::acquire(&golden_dir, &format!("{mem}m{vcpus}v__{worker_fp}"));
if !golden.exists() {
if std::fs::hard_link(&warm_snap, &golden).is_err() {
let tmp = golden.with_extension("snap.tmp");
if std::fs::copy(&warm_snap, &tmp).is_err() || std::fs::rename(&tmp, &golden).is_err() {
let _ = std::fs::remove_file(&tmp);
return;
}
}
}
match crate::kvm::run::rewrite_full_as_diff(&warm_snap, &golden) {
Ok(diff_size) => {
let meta_path = partial.join("metadata.json");
if let Ok(text) = std::fs::read_to_string(&meta_path) {
if let Ok(mut meta) = serde_json::from_str::<serde_json::Value>(&text) {
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"kvm_snapshot_base".into(),
serde_json::json!(golden.to_string_lossy()),
);
if let Ok(s) = serde_json::to_string_pretty(&meta) {
let _ = std::fs::write(&meta_path, s);
}
}
}
}
if trace {
eprintln!(
"[warm-pool] deduped vs golden {}: diff {:.1} MiB",
golden.display(),
diff_size as f64 / 1048576.0
);
}
}
Err(e) => {
if trace {
eprintln!("[warm-pool] dedup skipped ({e}); warm snapshot kept full");
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct WarmDirLock {
_file: Option<std::fs::File>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl WarmDirLock {
fn acquire(dir: &Path, name: &str) -> Self {
use std::os::fd::AsRawFd;
if std::fs::create_dir_all(dir).is_err() {
return Self { _file: None };
}
let lock_path = dir.join(format!(".{name}.warmlock"));
let Ok(file) = std::fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock_path)
else {
return Self { _file: None };
};
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(120);
loop {
let r = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if r == 0 {
return Self { _file: Some(file) };
}
if std::time::Instant::now() > deadline {
return Self { _file: None };
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
}
#[derive(Clone)]
pub struct Pool {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
inner: Arc<HiddenPool>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
kvm: Option<Arc<KvmPoolInner>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
sentry: Option<Arc<SentryPool>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
runsc: Option<Arc<RunscPoolInner>>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
kboxlike: Option<Arc<KboxlikePoolInner>>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct KvmPoolInner {
image: Image,
vm_config: VmConfig,
min: usize,
idle: Mutex<Vec<Vm>>,
admission: Admission,
restore_on_release: bool,
refilling: std::sync::atomic::AtomicBool,
shut_down: std::sync::atomic::AtomicBool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
enum SentryBackend {
ExecServer {
server: crate::sentry::ExecServer,
exec_path: PathBuf,
},
WarmDaemon {
pool: Arc<crate::sentry::Pool>,
#[allow(dead_code)]
daemon_argv: Vec<String>,
},
LiveTreeC4 {
image: Image,
config: VmConfig,
snapshot_dir: PathBuf,
},
Unified {
pool: Arc<crate::sentry::Pool>,
exec_path: PathBuf,
captures_live_state: bool,
},
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct SentryPool {
backend: SentryBackend,
rootfs: PathBuf,
own_dir: Option<PathBuf>,
build_root: Option<PathBuf>,
baseline_rootfs: Option<PathBuf>,
baseline_root: Option<PathBuf>,
restore_lock: Mutex<()>,
restore_on_release: bool,
admission: Admission,
_mem_admission: crate::memory_admission::AdmissionGuard,
mem_mib: u64,
vcpus: u32,
shut_down: std::sync::atomic::AtomicBool,
warm_daemon: bool,
cleanup_done: std::sync::atomic::AtomicBool,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Drop for SentryPool {
fn drop(&mut self) {
self.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
self.cleanup_now();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl SentryPool {
fn build(
image: &Image,
config: &VmConfig,
max: usize,
acquire_timeout: Option<Duration>,
policy_restore_on_release: bool,
) -> Result<Arc<SentryPool>, Error> {
let parts = image
.sentry
.as_ref()
.ok_or_else(|| Error::vm_msg("SentryPool::build on a non-sentry image"))?;
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
let mut own_dir = None;
if !dir.is_dir() {
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
own_dir = Some(dir.clone());
}
let exec_path = dir.join(format!(
"supermachine-sentry-pool-{}-{}.sock",
std::process::id(),
unique_suffix(),
));
let mut sandbox = image
.sentry_sandbox_with(config)
.map_err(|e| Error::vm_msg(format!("sentry sandbox: {e}")))?
.without_netns();
let mut start_workload = parts.has_workload;
if config.for_build {
start_workload = false;
}
let private_kind = if config.for_build { "build" } else { "runtime" };
let priv_dir = dir.join(format!(
"supermachine-sentry-{private_kind}-{}-{}",
std::process::id(),
unique_suffix(),
));
let private_rootfs = priv_dir.join("rootfs");
copy_dir_recursive(&parts.rootfs, &private_rootfs).map_err(|e| {
Error::vm_msg(format!("sentry {private_kind} private rootfs clone: {e}"))
})?;
if let Some(base_meta) = parts.rootfs.parent().map(|p| p.join("metadata.json")) {
if base_meta.is_file() {
let _ = std::fs::copy(&base_meta, priv_dir.join("metadata.json"));
}
}
sandbox = sandbox.with_root(private_rootfs.clone());
let rootfs = private_rootfs;
let build_root = Some(priv_dir);
let restore_on_release = !config.for_build && policy_restore_on_release;
let mut baseline_rootfs = None;
let mut baseline_root = None;
if restore_on_release {
let base_dir = dir.join(format!(
"supermachine-sentry-baseline-{}-{}",
std::process::id(),
unique_suffix(),
));
let base_rootfs = base_dir.join("rootfs");
copy_dir_recursive(&rootfs, &base_rootfs)
.map_err(|e| Error::vm_msg(format!("sentry rootfs baseline clone: {e}")))?;
baseline_rootfs = Some(base_rootfs);
baseline_root = Some(base_dir);
}
let mem_mib = sentry_cell_mem_mib(image, config);
let vcpus = config.vcpus.unwrap_or(image.vcpus).max(1);
let mem_admission = crate::memory_admission::charge(0);
let c4_snapshot_dir = (!config.for_build)
.then(|| image.snapshot_path.parent())
.flatten()
.filter(|dir| dir.join("restore.snap").is_file() && dir.join("mem.blob").is_file())
.map(Path::to_path_buf);
let c4_entry_kind = if let Some(dir) = &c4_snapshot_dir {
let mut f = std::fs::File::open(dir.join("restore.snap")).map_err(Error::Io)?;
Some(
crate::sentry::state_snap::StateSnapshot::read_from(&mut f)
.map_err(Error::Io)?
.entry_kind,
)
} else {
None
};
let unified = std::env::var("SENTRY_UNIFIED")
.map(|v| v != "0")
.unwrap_or(true);
let c4_live_tree = matches!(
c4_entry_kind,
Some(crate::sentry::state_snap::SnapshotEntryKind::LiveTree)
);
let backend = if c4_live_tree {
let snapshot_dir = c4_snapshot_dir
.clone()
.ok_or_else(|| Error::vm_msg("sentry live-tree C4 missing snapshot dir"))?;
SentryBackend::LiveTreeC4 {
image: image.clone(),
config: config.clone(),
snapshot_dir,
}
} else if unified && !parts.warm_daemon {
let unified_sock = dir.join(format!(
"supermachine-sentry-unified-{}-{}.sock",
std::process::id(),
unique_suffix(),
));
let pool = sandbox
.pool_with_exec(unified_sock.clone())
.map_err(|e| Error::vm_msg(format!("sentry unified pool: {e}")))?;
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while !unified_sock.exists() {
if std::time::Instant::now() >= deadline {
return Err(Error::vm_msg(
"sentry unified exec socket never appeared (supervisor bind failed)",
));
}
std::thread::sleep(Duration::from_millis(5));
}
let mut captures_live_state = false;
if start_workload && rootfs.join(".supermachine/run-workload").is_file() {
let wrapped = vec![
"/.supermachine/sm-checkpoint".to_string(),
"/bin/sh".to_string(),
"/.supermachine/run-workload".to_string(),
];
if pool.warm(wrapped.iter().cloned()).is_ok() {
if let Ok(wpid) = pool.acquire_running() {
captures_live_state = true;
let run_dir = rootfs.join("run");
let _ = std::fs::create_dir_all(&run_dir);
let _ = std::fs::write(
run_dir.join("supermachine-workload.pid"),
format!("{wpid}\n"),
);
}
}
}
SentryBackend::Unified {
pool: Arc::new(pool),
exec_path: unified_sock,
captures_live_state,
}
} else if parts.warm_daemon && !parts.cmd.is_empty() {
{
let pool = sandbox
.pool()
.map_err(|e| Error::vm_msg(format!("sentry warm-daemon pool: {e}")))?;
if let Some(snapshot_dir) = &c4_snapshot_dir {
pool.warm_restore(snapshot_dir).map_err(|e| {
Error::vm_msg(format!(
"sentry warm_restore({}): {e}",
snapshot_dir.display()
))
})?;
} else {
pool.warm(parts.cmd.iter().cloned())
.map_err(|e| Error::vm_msg(format!("sentry warm({:?}): {e}", parts.cmd)))?;
}
SentryBackend::WarmDaemon {
pool: Arc::new(pool),
daemon_argv: parts.cmd.clone(),
}
}
} else {
let server = sandbox
.serve_exec(&exec_path, start_workload)
.map_err(|e| Error::vm_msg(format!("sentry serve_exec: {e}")))?;
SentryBackend::ExecServer { server, exec_path }
};
Ok(Arc::new(SentryPool {
backend,
rootfs,
own_dir,
build_root,
baseline_rootfs,
baseline_root,
restore_lock: Mutex::new(()),
restore_on_release,
admission: Admission::new(max, acquire_timeout),
_mem_admission: mem_admission,
mem_mib,
vcpus,
shut_down: std::sync::atomic::AtomicBool::new(false),
warm_daemon: parts.warm_daemon,
cleanup_done: std::sync::atomic::AtomicBool::new(false),
}))
}
fn stop_backend(&self) {
match &self.backend {
SentryBackend::ExecServer { server, .. } => server.stop(),
SentryBackend::WarmDaemon { pool, .. } => pool.stop(),
SentryBackend::LiveTreeC4 { .. } => {}
SentryBackend::Unified { pool, .. } => pool.stop(),
}
}
fn cleanup_now(&self) {
if self
.cleanup_done
.compare_exchange(
false,
true,
std::sync::atomic::Ordering::SeqCst,
std::sync::atomic::Ordering::SeqCst,
)
.is_err()
{
return;
}
self.stop_backend();
if let Some(br) = &self.build_root {
let _ = std::fs::remove_dir_all(br);
}
if let Some(br) = &self.baseline_root {
let _ = std::fs::remove_dir_all(br);
}
if let Some(d) = &self.own_dir {
let _ = std::fs::remove_dir_all(d);
}
}
fn cleanup_if_shutdown_idle(&self) {
if !self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
if self.admission.alive() != 0 {
return;
}
self.cleanup_now();
}
fn restore_rootfs_from_baseline(&self) -> std::io::Result<()> {
if !self.restore_on_release {
return Ok(());
}
let Some(baseline) = &self.baseline_rootfs else {
return Ok(());
};
let _guard = self.restore_lock.lock().unwrap_or_else(|e| e.into_inner());
let parent = self.rootfs.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("sentry rootfs has no parent: {}", self.rootfs.display()),
)
})?;
let suffix = format!("{}-{}", std::process::id(), unique_suffix());
let staging = parent.join(format!(".supermachine-sentry-restore-{suffix}"));
let _ = std::fs::remove_dir_all(&staging);
copy_dir_recursive(baseline, &staging)?;
replace_dir_contents_from_staging(&staging, &self.rootfs)
}
fn capture_live_state_into(&self, dest_dir: &Path) -> Result<(), Error> {
match &self.backend {
SentryBackend::WarmDaemon { pool, .. } => pool
.snapshot(dest_dir)
.map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}"))),
SentryBackend::Unified {
pool,
captures_live_state: true,
..
} => pool
.snapshot(dest_dir)
.map_err(|e| Error::vm_msg(format!("sentry live snapshot capture: {e}"))),
SentryBackend::Unified {
captures_live_state: false,
..
} => Ok(()),
SentryBackend::ExecServer { .. } | SentryBackend::LiveTreeC4 { .. } => Ok(()),
}
}
fn acquire(self: &Arc<Self>) -> Result<PooledVm<'static>, Error> {
use std::sync::atomic::Ordering::SeqCst;
let deadline = self
.admission
.acquire_timeout
.map(|d| std::time::Instant::now() + d);
loop {
if self.shut_down.load(SeqCst) {
return Err(Error::vm_msg("sentry pool is shutting down"));
}
if self.admission.try_reserve() {
break;
}
if !self.admission.wait_for_change(deadline)? {
return Err(Error::pool_exhausted(
"sentry pool acquire timed out (all max slots in use)",
));
}
}
crate::memory_admission::await_pressure_relief();
let sentry_mem_admission =
crate::memory_admission::admit_with(|| sentry_admission_charge_mib(self.mem_mib, 1));
let mut exec_path = match &self.backend {
SentryBackend::ExecServer { exec_path, .. } => exec_path.clone(),
SentryBackend::Unified { exec_path, .. } => exec_path.clone(),
SentryBackend::WarmDaemon { .. } => PathBuf::new(),
SentryBackend::LiveTreeC4 { .. } => PathBuf::new(),
};
let (running_pid, warm_pool, running_pool, stop_running_pool_on_drop) = match &self.backend
{
SentryBackend::WarmDaemon { pool, .. } => {
match pool.acquire_running() {
Ok(pid) => (
Some(pid),
Some(Arc::clone(pool)),
Some(Arc::clone(pool)),
false,
),
Err(e) => {
self.admission.release();
return Err(Error::vm_msg(format!(
"sentry warm-daemon acquire_running: {e}"
)));
}
}
}
SentryBackend::LiveTreeC4 {
image,
config,
snapshot_dir,
} => {
let sandbox = match image.sentry_sandbox_with(config) {
Ok(s) => s.with_root(snapshot_dir.join("rootfs")).without_netns(),
Err(e) => {
self.admission.release();
return Err(e);
}
};
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
let live_exec_path = dir.join(format!(
"supermachine-sentry-live-c4-{}-{}.sock",
std::process::id(),
unique_suffix(),
));
let pool = match sandbox.pool_with_exec(live_exec_path.clone()) {
Ok(pool) => Arc::new(pool),
Err(e) => {
self.admission.release();
return Err(Error::vm_msg(format!("sentry live-tree pool: {e}")));
}
};
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while !live_exec_path.exists() {
if std::time::Instant::now() >= deadline {
pool.stop();
self.admission.release();
return Err(Error::vm_msg(
"sentry live-tree exec socket never appeared (supervisor bind failed)",
));
}
std::thread::sleep(Duration::from_millis(5));
}
if let Err(e) = pool.warm_restore(snapshot_dir) {
pool.stop();
self.admission.release();
return Err(Error::vm_msg(format!(
"sentry live-tree warm_restore({}): {e}",
snapshot_dir.display()
)));
}
let checkout = match pool.acquire_running() {
Ok(pid) => (Some(pid), None, Some(pool), true),
Err(e) => {
pool.stop();
self.admission.release();
return Err(Error::vm_msg(format!(
"sentry live-tree acquire_running: {e}"
)));
}
};
exec_path = live_exec_path;
checkout
}
SentryBackend::ExecServer { .. } => (None, None, None, false),
SentryBackend::Unified { .. } => (None, None, None, false),
};
let vm = Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: exec_path.clone(),
vsock_exec_path: exec_path,
own_vsock_mux_dir: None,
skip_cleanup: true,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: warm_pool,
sentry_unified_pool: None,
runsc: None,
kboxlike: None,
};
Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: Some(Arc::clone(self)),
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: Some(sentry_mem_admission),
sentry_running_pid: running_pid,
sentry_running_pool: running_pool,
sentry_stop_running_pool_on_drop: stop_running_pool_on_drop,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
})
}
fn sentry_stats(&self) -> Option<crate::sentry::SentryPoolStats> {
match &self.backend {
SentryBackend::WarmDaemon { pool, .. } => Some(pool.stats()),
SentryBackend::ExecServer { .. } => None,
SentryBackend::LiveTreeC4 { .. } => None,
SentryBackend::Unified { .. } => None,
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
struct Admission {
max: usize,
alive: std::sync::atomic::AtomicUsize,
lock: std::sync::Mutex<()>,
available: std::sync::Condvar,
acquire_timeout: Option<Duration>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Admission {
fn new(max: usize, acquire_timeout: Option<Duration>) -> Self {
Self {
max: max.max(1),
alive: std::sync::atomic::AtomicUsize::new(0),
lock: std::sync::Mutex::new(()),
available: std::sync::Condvar::new(),
acquire_timeout,
}
}
fn alive(&self) -> usize {
self.alive.load(std::sync::atomic::Ordering::SeqCst)
}
fn try_reserve(&self) -> bool {
use std::sync::atomic::Ordering::SeqCst;
loop {
let cur = self.alive.load(SeqCst);
if cur >= self.max {
return false;
}
if self
.alive
.compare_exchange(cur, cur + 1, SeqCst, SeqCst)
.is_ok()
{
return true;
}
}
}
fn wait_for_change(&self, deadline: Option<std::time::Instant>) -> Result<bool, Error> {
let g = self.lock.lock().unwrap_or_else(|e| e.into_inner());
let wait = match deadline {
Some(dl) => {
let now = std::time::Instant::now();
if now >= dl {
return Ok(false);
}
(dl - now).min(Duration::from_millis(100))
}
None => Duration::from_millis(100),
};
let _ = self.available.wait_timeout(g, wait);
Ok(true)
}
fn release(&self) {
self.alive.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
self.available.notify_one();
}
fn notify(&self) {
self.available.notify_all();
}
}
#[cfg(all(test, target_os = "linux", target_arch = "x86_64"))]
mod admission_tests {
use super::Admission;
use std::sync::Arc;
use std::time::{Duration, Instant};
#[test]
fn try_reserve_bounds_at_max() {
let a = Admission::new(3, None);
assert!(a.try_reserve());
assert!(a.try_reserve());
assert!(a.try_reserve());
assert_eq!(a.alive(), 3);
assert!(!a.try_reserve());
assert_eq!(a.alive(), 3);
a.release();
assert_eq!(a.alive(), 2);
assert!(a.try_reserve());
assert_eq!(a.alive(), 3);
}
#[test]
fn max_zero_clamps_to_one() {
let a = Admission::new(0, None);
assert_eq!(a.max, 1);
assert!(a.try_reserve());
assert!(!a.try_reserve());
}
#[test]
fn wait_for_change_reports_deadline_passed() {
let a = Admission::new(1, Some(Duration::from_millis(50)));
assert!(a.try_reserve());
let past = Instant::now() - Duration::from_secs(1);
let start = Instant::now();
assert_eq!(a.wait_for_change(Some(past)).unwrap(), false);
assert!(
start.elapsed() < Duration::from_millis(50),
"a passed deadline must not block"
);
assert_eq!(a.alive(), 1);
}
#[test]
fn wait_for_change_wakes_on_notify() {
let a = Arc::new(Admission::new(1, Some(Duration::from_secs(5))));
assert!(a.try_reserve());
let peer = Arc::clone(&a);
let waker = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(20));
peer.notify();
});
let start = Instant::now();
let deadline = Some(Instant::now() + Duration::from_secs(5));
assert_eq!(a.wait_for_change(deadline).unwrap(), true);
let elapsed = start.elapsed();
waker.join().unwrap();
assert!(
elapsed < Duration::from_millis(100),
"notify must wake the waiter promptly, waited {elapsed:?}"
);
}
#[test]
fn wait_for_change_caps_each_wait_without_notify() {
let a = Admission::new(1, Some(Duration::from_secs(10)));
assert!(a.try_reserve());
let start = Instant::now();
let deadline = Some(Instant::now() + Duration::from_secs(10));
assert_eq!(a.wait_for_change(deadline).unwrap(), true);
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(250),
"each wait must cap at ~100ms, waited {elapsed:?}"
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KvmPoolInner {
fn restore_one(&self) -> Result<Vm, Error> {
Vm::start(&self.image, &self.vm_config)
}
fn take_or_restore(&self) -> Result<Vm, Error> {
let deadline = self
.admission
.acquire_timeout
.map(|d| std::time::Instant::now() + d);
loop {
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
return Err(Error::pool_exhausted("pool is shut down"));
}
if let Some(vm) = self.idle.lock().unwrap_or_else(|e| e.into_inner()).pop() {
return Ok(vm);
}
if self.admission.try_reserve() {
return self.restore_one().inspect_err(|_| self.admission.release());
}
if !self.admission.wait_for_change(deadline)? {
return Err(Error::pool_exhausted(format!(
"pool saturated: all {} VM slots in use and none freed within \
acquire_timeout",
self.admission.max
)));
}
}
}
fn prewarm_to_min(&self) {
loop {
let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
if idle_len >= self.min || !self.admission.try_reserve() {
break;
}
match self.restore_one() {
Ok(vm) => {
self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
self.admission.notify();
}
Err(_) => {
self.admission.release();
break;
}
}
}
}
fn refill_async(self: &Arc<Self>) {
use std::sync::atomic::Ordering::SeqCst;
if self.min == 0 {
return;
}
if self
.refilling
.compare_exchange(false, true, SeqCst, SeqCst)
.is_err()
{
return; }
let inner = Arc::clone(self);
std::thread::spawn(move || {
loop {
let idle_len = inner.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
if idle_len >= inner.min || !inner.admission.try_reserve() {
break;
}
match inner.restore_one() {
Ok(vm) => {
inner
.idle
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(vm);
inner.admission.notify();
}
Err(_) => {
inner.admission.release();
break;
}
}
}
inner.refilling.store(false, SeqCst);
});
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl KboxlikePoolInner {
fn build(
image: &Image,
config: &VmConfig,
min: usize,
max: usize,
acquire_timeout: Option<Duration>,
restore_on_release: bool,
) -> Result<Arc<Self>, Error> {
let parts = image
.kboxlike
.as_ref()
.ok_or_else(|| Error::vm_msg("KboxlikePoolInner::build on a non-kboxlike image"))?
.clone();
let max = max.max(1);
let min = min.min(max);
let inner = Arc::new(Self {
parts,
vm_config: config.clone(),
min,
admission: Admission::new(max, acquire_timeout),
idle: Mutex::new(Vec::new()),
restore_on_release,
refilling: std::sync::atomic::AtomicBool::new(false),
shut_down: std::sync::atomic::AtomicBool::new(false),
});
inner.prewarm_to_min();
Ok(inner)
}
fn restore_one(&self) -> Result<Vm, Error> {
Vm::start_kboxlike_parts(&self.parts, &self.vm_config)
}
fn take_or_restore(&self) -> Result<Vm, Error> {
let deadline = self
.admission
.acquire_timeout
.map(|d| std::time::Instant::now() + d);
loop {
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
return Err(Error::pool_exhausted("pool is shut down"));
}
if let Some(vm) = self.idle.lock().unwrap_or_else(|e| e.into_inner()).pop() {
return Ok(vm);
}
if self.admission.try_reserve() {
return self.restore_one().inspect_err(|_| self.admission.release());
}
if !self.admission.wait_for_change(deadline)? {
return Err(Error::pool_exhausted(format!(
"kboxlike pool saturated: all {} VM slots in use and none freed within \
acquire_timeout",
self.admission.max
)));
}
}
}
fn prewarm_to_min(&self) {
loop {
let idle_len = self.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
if idle_len >= self.min || !self.admission.try_reserve() {
break;
}
match self.restore_one() {
Ok(vm) => {
self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
self.admission.notify();
}
Err(e) => {
eprintln!("supermachine(kboxlike): prewarm restore failed: {e}");
self.admission.release();
break;
}
}
}
}
fn refill_async(self: &Arc<Self>) {
use std::sync::atomic::Ordering::SeqCst;
if self.min == 0 {
return;
}
if self
.refilling
.compare_exchange(false, true, SeqCst, SeqCst)
.is_err()
{
return;
}
let inner = Arc::clone(self);
std::thread::spawn(move || {
loop {
let idle_len = inner.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
if idle_len >= inner.min || !inner.admission.try_reserve() {
break;
}
match inner.restore_one() {
Ok(vm) => {
inner
.idle
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(vm);
inner.admission.notify();
}
Err(e) => {
eprintln!("supermachine(kboxlike): refill restore failed: {e}");
inner.admission.release();
break;
}
}
}
inner.refilling.store(false, SeqCst);
});
}
fn release_vm(self: &Arc<Self>, vm: Vm) {
if self.shut_down.load(std::sync::atomic::Ordering::SeqCst) {
drop(vm);
self.admission.release();
return;
}
let warm_slot = {
let idle = self.idle.lock().unwrap_or_else(|e| e.into_inner());
idle.len() < self.min
};
if !warm_slot {
drop(vm);
self.admission.release();
return;
}
if !self.restore_on_release {
self.idle.lock().unwrap_or_else(|e| e.into_inner()).push(vm);
self.admission.notify();
return;
}
drop(vm);
self.admission.release();
self.refill_async();
}
}
impl std::fmt::Debug for Pool {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pool").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy)]
pub struct PoolStats {
pub alive: usize,
pub in_use: usize,
pub idle: usize,
pub waiting: usize,
pub max: usize,
pub min: usize,
pub reaped: u64,
pub observed_footprint_mib: u64,
}
impl Pool {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
let _span = tracing::info_span!(
"supermachine.pool.acquire",
memory_mib = self.inner.spawn_cfg.memory_mib,
vcpus = self.inner.spawn_cfg.vcpus,
)
.entered();
let worker = self.inner.acquire()?;
let vm = Vm {
pool: None,
vsock_mux_path: worker.vsock_mux_path.clone(),
vsock_exec_path: worker.vsock_exec_path.clone(),
own_vsock_mux_dir: None,
skip_cleanup: true,
image_meta: Some(Arc::new(ImageMeta {
memory_mib: self.inner.spawn_cfg.memory_mib,
vcpus: self.inner.spawn_cfg.vcpus,
layers: self.inner.spawn_cfg.layers.clone(),
delta_squashfs: self.inner.spawn_cfg.delta_squashfs.clone(),
baker_runtime_sha16: self.inner.spawn_cfg.baker_runtime_sha16.clone(),
tsi_token: self.inner.spawn_cfg.tsi_token.clone(),
egress_policy: self.inner.spawn_cfg.egress_policy.clone(),
pre_exec_sync: self.inner.spawn_cfg.pre_exec_sync,
})),
};
Ok(PooledVm {
vm: Some(vm),
worker: Some(worker),
pool_arc: Arc::clone(&self.inner),
_image: std::marker::PhantomData,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn acquire(&self) -> Result<PooledVm<'_>, Error> {
if let Some(sp) = &self.sentry {
return sp.acquire();
}
if let Some(rp) = &self.runsc {
let vm = rp.acquire_vm()?;
return Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: Some(Arc::clone(rp)),
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
});
}
if let Some(kp) = &self.kboxlike {
let vm = kp.take_or_restore()?;
return Ok(PooledVm {
vm: Some(vm),
pool: None,
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: Some(Arc::clone(kp)),
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
});
}
let kvm = self.kvm.as_ref().expect("Pool has no backend");
let vm = kvm.take_or_restore()?;
Ok(PooledVm {
vm: Some(vm),
pool: Some(Arc::clone(kvm)),
sentry_pool: None,
runsc_pool: None,
kboxlike_pool: None,
sentry_mem_admission: None,
sentry_running_pid: None,
sentry_running_pool: None,
sentry_stop_running_pool_on_drop: false,
sentry_touched: AtomicBool::new(false),
_image: std::marker::PhantomData,
})
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn stats(&self) -> PoolStats {
let s = self.inner.state.lock().ok();
let alive = s.as_ref().map(|s| s.alive).unwrap_or(0);
let idle = s.as_ref().map(|s| s.idle.len()).unwrap_or(0);
let waiting = s.as_ref().map(|s| s.waiting).unwrap_or(0);
let reaped = s.as_ref().map(|s| s.reaped).unwrap_or(0);
let observed_footprint_mib = self
.inner
.spawn_cfg
.observed_footprint_mib
.load(std::sync::atomic::Ordering::Relaxed);
PoolStats {
alive,
in_use: alive.saturating_sub(idle),
idle,
waiting,
max: self.inner.policy.max,
min: self.inner.policy.min,
reaped,
observed_footprint_mib,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn stats(&self) -> PoolStats {
if let Some(sp) = &self.sentry {
let alive = sp.admission.alive();
return PoolStats {
alive,
in_use: alive,
idle: 0,
waiting: 0,
max: sp.admission.max,
min: 0,
reaped: 0,
observed_footprint_mib: 0,
};
}
if let Some(rp) = &self.runsc {
let alive = rp.admission.alive();
let idle = rp.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
let in_use = rp.checked_out.load(std::sync::atomic::Ordering::SeqCst);
return PoolStats {
alive,
in_use,
idle,
waiting: 0,
max: rp.max,
min: rp.min,
reaped: 0,
observed_footprint_mib: 0,
};
}
if let Some(kp) = &self.kboxlike {
let alive = kp.admission.alive();
let idle = kp.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
return PoolStats {
alive,
in_use: alive.saturating_sub(idle),
idle,
waiting: 0,
max: kp.admission.max,
min: kp.min,
reaped: 0,
observed_footprint_mib: 0,
};
}
let kvm = self.kvm.as_ref().expect("Pool has no backend");
let idle = kvm.idle.lock().unwrap_or_else(|e| e.into_inner()).len();
let alive = kvm.admission.alive();
PoolStats {
alive,
in_use: alive.saturating_sub(idle),
idle,
waiting: 0,
max: kvm.admission.max,
min: kvm.min,
reaped: 0,
observed_footprint_mib: 0,
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn sentry_stats(&self) -> Option<crate::sentry::SentryPoolStats> {
self.sentry.as_ref().and_then(|sp| sp.sentry_stats())
}
pub fn shutdown(&self) {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
self.inner.shutdown_pool();
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
if let Some(sp) = &self.sentry {
sp.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
sp.admission.notify();
sp.cleanup_if_shutdown_idle();
} else if let Some(kvm) = &self.kvm {
kvm.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
let drained: Vec<Vm> =
std::mem::take(&mut *kvm.idle.lock().unwrap_or_else(|e| e.into_inner()));
for vm in drained {
kvm.admission.release();
drop(vm);
}
} else if let Some(runsc) = &self.runsc {
runsc
.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
let drained: Vec<Vm> =
std::mem::take(&mut *runsc.idle.lock().unwrap_or_else(|e| e.into_inner()));
for vm in drained {
drop(vm);
runsc.admission.release();
}
runsc.admission.notify();
} else if let Some(kboxlike) = &self.kboxlike {
kboxlike
.shut_down
.store(true, std::sync::atomic::Ordering::SeqCst);
let drained: Vec<Vm> =
std::mem::take(&mut *kboxlike.idle.lock().unwrap_or_else(|e| e.into_inner()));
for vm in drained {
drop(vm);
kboxlike.admission.release();
}
kboxlike.admission.notify();
}
}
}
}
pub struct OciImageBuilder {
image: String,
name: Option<String>,
pull_policy: PullPolicy,
memory_mib: Option<u32>,
vcpus: Option<u32>,
guest_port: Option<u16>,
cmd: Option<Vec<String>>,
envs: Vec<(String, String)>,
snapshots_dir: Option<PathBuf>,
warmup: Option<Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>>,
warmup_tag: Option<String>,
warm_daemon: bool,
extra_files: Vec<(PathBuf, String)>,
mounts: Vec<(
PathBuf,
String,
crate::vmm::resources::SymlinkPolicy,
String,
)>,
volumes: Vec<(PathBuf, String, u64)>,
extra_kernel_args: Vec<String>,
require_listener: Option<bool>,
pub(crate) platform: Option<String>,
}
impl OciImageBuilder {
pub fn new(image_ref: impl Into<String>) -> Self {
Self {
image: image_ref.into(),
name: None,
pull_policy: PullPolicy::default(),
memory_mib: None,
vcpus: None,
guest_port: None,
cmd: None,
envs: Vec::new(),
snapshots_dir: None,
warmup: None,
warmup_tag: None,
warm_daemon: false,
extra_files: Vec::new(),
mounts: Vec::new(),
volumes: Vec::new(),
extra_kernel_args: Vec::new(),
require_listener: None,
platform: None,
}
}
pub fn with_platform(mut self, platform: impl Into<String>) -> Self {
self.platform = Some(platform.into());
self
}
pub fn with_extra_file(
mut self,
host_path: impl Into<PathBuf>,
guest_path: impl Into<String>,
) -> Self {
self.extra_files.push((host_path.into(), guest_path.into()));
self
}
pub fn with_oci_archive(mut self, archive_path: impl Into<PathBuf>) -> Self {
let path: PathBuf = archive_path.into();
let original =
std::mem::replace(&mut self.image, format!("oci-archive:{}", path.display()));
if self.name.is_none() {
self.name = Some(crate::bake::snapshot_name_for_image(&original));
}
self
}
pub fn with_oci_layout(mut self, layout_dir: impl Into<PathBuf>) -> Self {
let path: PathBuf = layout_dir.into();
let original = std::mem::replace(&mut self.image, format!("oci-layout:{}", path.display()));
if self.name.is_none() {
self.name = Some(crate::bake::snapshot_name_for_image(&original));
}
self
}
pub fn with_mount(
mut self,
host_path: impl Into<PathBuf>,
guest_tag: impl Into<String>,
guest_path: impl Into<String>,
) -> Self {
self.mounts.push((
host_path.into(),
guest_tag.into(),
crate::vmm::resources::SymlinkPolicy::default(),
guest_path.into(),
));
self
}
pub fn with_mount_symlinks(
mut self,
host_path: impl Into<PathBuf>,
guest_tag: impl Into<String>,
guest_path: impl Into<String>,
symlinks: crate::vmm::resources::SymlinkPolicy,
) -> Self {
self.mounts.push((
host_path.into(),
guest_tag.into(),
symlinks,
guest_path.into(),
));
self
}
pub fn with_volume(mut self, spec: crate::vmm::resources::VolumeSpec) -> Self {
self.volumes.push((
PathBuf::from(spec.host_path),
spec.guest_path,
spec.size_bytes,
));
self
}
pub fn with_extra_kernel_arg(mut self, arg: impl Into<String>) -> Self {
self.extra_kernel_args.push(arg.into());
self
}
pub fn with_vcpus(mut self, vcpus: u32) -> Self {
self.vcpus = Some(vcpus);
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_pull_policy(mut self, policy: PullPolicy) -> Self {
self.pull_policy = policy;
self
}
pub fn with_memory_mib(mut self, mib: u32) -> Self {
self.memory_mib = Some(mib);
self
}
pub fn with_guest_port(mut self, port: u16) -> Self {
self.guest_port = Some(port);
self
}
pub fn with_cmd<I, S>(mut self, cmd: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.cmd = Some(cmd.into_iter().map(Into::into).collect());
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.envs.push((key.into(), value.into()));
self
}
pub fn with_snapshots_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.snapshots_dir = Some(dir.into());
self
}
pub fn with_warmup<F>(mut self, warmup: F) -> Self
where
F: FnOnce(&Vm) -> Result<(), Error> + Send + 'static,
{
self.warmup = Some(Box::new(warmup));
self
}
pub fn with_warmup_tag(mut self, tag: impl Into<String>) -> Self {
self.warmup_tag = Some(tag.into());
self
}
pub fn with_warm_daemon(mut self) -> Self {
self.warm_daemon = true;
self
}
pub fn with_listener_required(mut self) -> Self {
self.require_listener = Some(true);
self
}
pub fn build(self) -> Result<Image, Error> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
self.build_kvm()
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
self.build_native()
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn build_kvm(self) -> Result<Image, Error> {
let snapshots_dir = self
.snapshots_dir
.clone()
.unwrap_or_else(default_snapshots_dir);
let derived_name = self
.name
.clone()
.unwrap_or_else(|| crate::bake::snapshot_name_for_image(&self.image));
let snap_dir = snapshots_dir.join(&derived_name);
let backend = linux_default_backend();
let use_runsc = backend == "runsc";
let use_sentry = backend == "sentry";
let use_kboxlike = backend == "kboxlike";
if use_sentry || use_runsc || use_kboxlike {
let backend_name = if use_runsc {
"runsc"
} else if use_kboxlike {
"kboxlike"
} else {
"sentry"
};
let has_warmup = self.warmup.is_some();
let warmup_tag = self.warmup_tag.as_deref().unwrap_or("default");
let no_virt_snap_dir = if has_warmup {
snapshots_dir.join(format!("{}__warm__{}", derived_name, warmup_tag))
} else {
snap_dir.clone()
};
let requested_mounts: Vec<crate::vmm::resources::MountSpec> = self
.mounts
.iter()
.map(|(host, tag, policy, guest_path)| {
crate::vmm::resources::MountSpec::new(
host.to_string_lossy().to_string(),
tag.clone(),
guest_path.clone(),
)
.with_symlinks(policy.clone())
})
.collect();
let cfg = SentryBakeConfig {
memory_mib: self.memory_mib,
vcpus: self.vcpus,
cmd: self.cmd,
env: self.envs,
working_dir: None,
user: None,
extra_files: self.extra_files,
mounts: self.mounts,
volumes: self.volumes,
warmup: self.warmup,
warmup_tag: self.warmup_tag,
warm_daemon: self.warm_daemon,
};
let no_virt_bake_key = sentry_bake_cache_key(&self.image, &cfg)?;
if self.pull_policy == PullPolicy::Missing {
if let Ok(img) = Image::from_snapshot(&no_virt_snap_dir) {
let backend_matches = if use_runsc {
img.runsc.is_some()
} else if use_kboxlike {
img.kboxlike.is_some()
} else {
img.sentry.is_some()
};
if backend_matches {
let meta_matches = std::fs::read(no_virt_snap_dir.join("metadata.json"))
.ok()
.and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
.map(|m| {
let key_matches = m
.get(if use_runsc {
"runsc_bake_key"
} else if use_kboxlike {
"kboxlike_bake_key"
} else {
"sentry_bake_key"
})
.or_else(|| m.get("sentry_bake_key"))
.and_then(|v| v.as_str())
== Some(no_virt_bake_key.as_str());
let memory_matches = match cfg.memory_mib {
Some(expected) => {
m.get("memory_mib").and_then(|v| v.as_u64())
== Some(expected as u64)
}
None => true,
};
let vcpus_matches = match cfg.vcpus {
Some(expected) => {
m.get("vcpus").and_then(|v| v.as_u64())
== Some(expected as u64)
}
None => true,
};
key_matches && memory_matches && vcpus_matches
})
== Some(true);
if !meta_matches {
invalidate_stale_snapshot_tree(&no_virt_snap_dir);
} else if has_warmup && img.mounts != requested_mounts {
eprintln!(
"supermachine: {backend_name} warm snapshot at {} was baked with mounts \
{:?} but this build requests {:?}. Auto-rebaking with the current mounts.",
no_virt_snap_dir.display(),
img.mounts,
requested_mounts,
);
invalidate_stale_snapshot_tree(&no_virt_snap_dir);
} else {
return Ok(img);
}
}
}
}
if use_runsc {
return Image::bake_runsc_with_config(&self.image, &no_virt_snap_dir, cfg);
}
if use_kboxlike {
return Image::bake_kboxlike_with_config(&self.image, &no_virt_snap_dir, cfg);
}
return Image::bake_sentry_with_config(&self.image, &no_virt_snap_dir, cfg);
}
self.build_kvm_in_process(snapshots_dir, derived_name, snap_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn build_kvm_in_process(
self,
snapshots_dir: PathBuf,
derived_name: String,
snap_dir: PathBuf,
) -> Result<Image, Error> {
if self.platform.as_deref().is_some_and(|p| p != "linux/amd64") {
return Err(Error::bake_msg(format!(
"KVM backend on linux/x86_64 only supports platform=linux/amd64, got {}",
self.platform.as_deref().unwrap_or_default()
)));
}
let has_warmup = self.warmup.is_some();
let tag = self.warmup_tag.as_deref().unwrap_or("default");
let output_dir = if has_warmup {
snapshots_dir.join(format!("{}__warm__{}", derived_name, tag))
} else {
snap_dir.clone()
};
let requested_vcpus = self.vcpus.unwrap_or(1);
let effective_vcpus = kvm_effective_vcpus(requested_vcpus, "Image.build");
let key_config = SentryBakeConfig {
memory_mib: self.memory_mib,
vcpus: Some(effective_vcpus),
cmd: self.cmd.clone(),
env: self.envs.clone(),
working_dir: None,
user: None,
extra_files: self.extra_files.clone(),
mounts: self.mounts.clone(),
volumes: self.volumes.clone(),
warmup: if has_warmup {
Some(Box::new(|_| Ok(())))
} else {
None
},
warm_daemon: self.warm_daemon,
warmup_tag: self.warmup_tag.clone(),
};
let kvm_bake_key = sentry_bake_cache_key(&self.image, &key_config)?;
match self.pull_policy {
PullPolicy::Never => {
let image = Image::from_snapshot(&output_dir)?;
if kvm_bake_key_matches(&output_dir, &kvm_bake_key) {
return Ok(image);
}
return Err(Error::cache_invalid(format!(
"cached KVM snapshot at {} was baked with different builder inputs \
(PullPolicy::Never won't auto-rebake)",
output_dir.display()
)));
}
PullPolicy::Missing => {
if let Ok(image) = Image::from_snapshot(&output_dir) {
if kvm_bake_key_matches(&output_dir, &kvm_bake_key) {
return Ok(image);
}
invalidate_stale_snapshot_tree(&snap_dir);
if output_dir != snap_dir {
let _ = std::fs::remove_dir_all(&output_dir);
}
}
}
PullPolicy::Always => {
invalidate_stale_snapshot_tree(&snap_dir);
if output_dir != snap_dir {
let _ = std::fs::remove_dir_all(&output_dir);
}
}
}
let cold_dir = snap_dir.clone();
std::fs::create_dir_all(&cold_dir).map_err(Error::Io)?;
let (kernel, initrd) = Image::extract_bundled_kvm_boot_assets(&cold_dir)?;
let pull_work = cold_dir.join("oci-pull");
let (layers, image_workload, oci_config) =
crate::bake::pull_oci_layers(&self.image, "amd64", &pull_work)
.map_err(Error::bake_msg)?;
let (workload, env_pairs, effective_workdir, effective_user) = effective_builder_workload(
image_workload,
oci_config.as_ref(),
self.cmd.as_ref(),
&self.envs,
);
let rootfs = cold_dir.join("rootfs.squashfs");
let stage = cold_dir.join("rootfs-stage");
let _ = std::fs::remove_dir_all(&stage);
crate::bake::build_sentry_rootfs(&layers, &stage, workload.as_deref())
.map_err(Error::bake_msg)?;
for (host, guest) in &self.extra_files {
let rel = guest.trim_start_matches('/');
let target = confined_rootfs_join(&stage, rel).ok_or_else(|| {
Error::bake_msg(format!(
"extra-file guest path escapes the rootfs: {guest:?}"
))
})?;
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
std::fs::copy(host, &target).map_err(|e| {
Error::bake_msg(format!(
"stage extra-file {} -> {}: {e}",
host.display(),
target.display()
))
})?;
}
crate::bake::write_kvm_rootfs_squashfs(&stage, &rootfs).map_err(Error::bake_msg)?;
let _ = std::fs::remove_dir_all(&stage);
let _ = std::fs::remove_dir_all(&pull_work);
let memory_mib = self.memory_mib.unwrap_or(512);
let vcpus = effective_vcpus;
let phys = std::fs::metadata(&rootfs).map(|m| m.len()).unwrap_or(0)
+ std::fs::metadata(&kernel).map(|m| m.len()).unwrap_or(0)
+ std::fs::metadata(&initrd).map(|m| m.len()).unwrap_or(0);
let mounts = builder_mount_specs(&self.mounts);
write_kvm_cold_builder_metadata(
&cold_dir,
&self.image,
&kvm_bake_key,
&kernel,
&initrd,
&rootfs,
memory_mib,
vcpus,
phys,
&env_pairs,
self.cmd.as_ref(),
oci_config.as_ref(),
effective_workdir.as_deref(),
effective_user.as_deref(),
self.warm_daemon,
self.warmup_tag.as_deref(),
&mounts,
&self.volumes,
)?;
let cold_image = Image::from_snapshot(&cold_dir)?;
let Some(warmup) = self.warmup else {
return Ok(cold_image);
};
let vm = Vm::start(&cold_image, &VmConfig::new())?;
let result = warmup(&vm);
if let Err(e) = result {
let _ = vm.stop();
return Err(e);
}
let warm_image = vm.snapshot_live(&output_dir);
let _ = vm.stop();
let warm_image = warm_image?;
persist_kvm_builder_run_config(
&output_dir,
&self.image,
&kvm_bake_key,
&env_pairs,
self.cmd.as_ref(),
oci_config.as_ref(),
effective_workdir.as_deref(),
effective_user.as_deref(),
self.warm_daemon,
self.warmup_tag.as_deref(),
&mounts,
&self.volumes,
)?;
drop(warm_image);
Image::from_snapshot(&output_dir)
}
fn build_native(self) -> Result<Image, Error> {
let snapshots_dir = self.snapshots_dir.unwrap_or_else(default_snapshots_dir);
let derived_name = self
.name
.clone()
.unwrap_or_else(|| crate::bake::snapshot_name_for_image(&self.image));
let snap_dir = snapshots_dir.join(&derived_name);
let cache_loadable = Image::from_snapshot(&snap_dir).is_ok();
if cache_loadable {
if let Some(baked) = snap_dir_baked_under_other_version(&snap_dir) {
let current = env!("CARGO_PKG_VERSION");
eprintln!(
"supermachine: snapshot at {} was baked under v{baked}; \
current binaries are v{current}. Auto-rebaking \
(this is a one-time cost on the next build after upgrade).",
snap_dir.display()
);
invalidate_stale_snapshot_tree(&snap_dir);
}
}
let cache_loadable = cache_loadable && snap_dir.is_dir();
match self.pull_policy {
PullPolicy::Never => {
if cache_loadable {
return Image::from_snapshot(&snap_dir);
}
let restore_snap = snap_dir.join("restore.snap");
if restore_snap.is_file() {
return Err(Error::cache_invalid(format!(
"snapshot present at {} but not loadable on this binary; \
rebake required (PullPolicy::Never won't auto-rebake)",
snap_dir.display()
)));
}
return Err(Error::cache_miss(format!(
"no cached snapshot for {} at {} (PullPolicy::Never)",
self.image,
snap_dir.display()
)));
}
_ => {}
}
let mut extra_args: Vec<String> = Vec::new();
for (k, v) in &self.envs {
extra_args.push("--env".to_owned());
extra_args.push(format!("{k}={v}"));
}
for (host, guest) in &self.extra_files {
extra_args.push("--extra-file".to_owned());
extra_args.push(format!("{}:{}", host.display(), guest));
}
use crate::vmm::resources::SymlinkPolicy;
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
let effective_mounts: Vec<(PathBuf, String, SymlinkPolicy, String)> = {
let mut effective_mounts = self.mounts.clone();
let platform_amd64 = self.platform.as_deref() == Some("linux/amd64");
if platform_amd64
&& !effective_mounts
.iter()
.any(|(_, tag, _, _)| tag == "rosetta")
{
const ROSETTA_HOST: &str = "/Library/Apple/usr/libexec/oah/RosettaLinux";
let rosetta_path = PathBuf::from(ROSETTA_HOST);
if !rosetta_path.join("rosetta").is_file()
|| !rosetta_path.join("rosettad").is_file()
{
return Err(Error::bake_msg(format!(
"platform=linux/amd64 requires Apple Rosetta runtime at \
{ROSETTA_HOST}; install with `softwareupdate --install-rosetta`"
)));
}
effective_mounts.push((
rosetta_path,
"rosetta".to_owned(),
SymlinkPolicy::Opaque,
"/run/rosetta".to_owned(),
));
}
if platform_amd64
&& !effective_mounts
.iter()
.any(|(_, tag, _, _)| tag == "rosettad-cache")
{
if let Some(home) = std::env::var_os("HOME") {
let cache_dir = PathBuf::from(home)
.join(".cache")
.join("supermachine")
.join("rosetta-aot");
if let Err(e) = std::fs::create_dir_all(&cache_dir) {
eprintln!(
"supermachine: could not create Rosetta AOT cache dir \
{}: {} — amd64 binaries will still run via JIT but \
translations won't persist across VM restarts",
cache_dir.display(),
e
);
} else {
effective_mounts.push((
cache_dir,
"rosettad-cache".to_owned(),
SymlinkPolicy::Opaque,
"/var/cache/rosettad".to_owned(),
));
}
}
}
effective_mounts
};
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
let effective_mounts: Vec<(PathBuf, String, SymlinkPolicy, String)> = self.mounts.clone();
for (host, tag, symlinks, guest_path) in &effective_mounts {
extra_args.push("--mount".to_owned());
let s = match symlinks {
SymlinkPolicy::Opaque => {
format!("{}:{}:{}", host.display(), tag, guest_path)
}
SymlinkPolicy::Deny => {
format!("{}:{}:{}:deny", host.display(), tag, guest_path)
}
SymlinkPolicy::Follow => {
format!("{}:{}:{}:follow", host.display(), tag, guest_path)
}
};
extra_args.push(s);
}
for (host, guest, size_bytes) in &self.volumes {
extra_args.push("--volume".to_owned());
extra_args.push(format!("{}:{}:{}", host.display(), guest, size_bytes));
}
for token in &self.extra_kernel_args {
extra_args.push("--cmdline-extra".to_owned());
extra_args.push(token.clone());
}
if self.require_listener.unwrap_or(false) {
extra_args.push("--supermachine-listener-required".to_owned());
}
let cmd_override = match &self.cmd {
Some(argv) => Some(
serde_json::to_string(argv)
.map_err(|e| Error::bake_msg(format!("encode cmd: {e}")))?,
),
None => None,
};
let root = repo_root_for_bake()?;
let request = crate::bake::BakeRequest {
image: self.image.clone(),
name: self.name.clone(),
runtime: "supermachine".to_owned(),
guest_port: self.guest_port.unwrap_or(80),
memory_mib: self.memory_mib.unwrap_or(256),
vcpus: self.vcpus.unwrap_or(1),
pull_policy: self.pull_policy.as_bake_str().to_owned(),
snapshots_dir: snapshots_dir.clone(),
cmd_override,
extra_args,
platform: self.platform.clone().unwrap_or_else(default_guest_platform),
};
if self.warmup.is_none() {
let trace = crate::trace::enabled("bake");
let bake_t0 = std::time::Instant::now();
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
let use_pre_exec = false;
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
let use_pre_exec = !self.require_listener.unwrap_or(false);
let pipelined = crate::bake::PipelinedWarmup {
warm_dir: snapshots_dir.join(format!("{}__warm__unused", derived_name)),
warm_tag: "unused".to_owned(),
keep_alive: cfg!(all(target_os = "macos", target_arch = "aarch64")),
skip_warm_snapshot: true,
use_pre_exec_trigger: use_pre_exec,
callback: Box::new(|_ctx| Ok(())),
};
match crate::bake::run_push_pipelined(&request, bake_t0, &root, pipelined) {
Ok(warm_handoff) => {
if trace {
eprintln!(
"[bake-trace] always-pipelined (skip-warm) total: {:?} (bg save \
may still be in flight)",
bake_t0.elapsed()
);
}
let img = Image::from_snapshot_pending(&snap_dir)?;
if let Some(bw) = warm_handoff {
*img.warm_baked_worker.inner.lock().unwrap() = Some(bw);
}
return Ok(img);
}
Err(msg) => {
return Err(map_bake_error(&request.image, msg));
}
}
}
let warmup = self.warmup.unwrap();
let tag = self.warmup_tag.as_deref().unwrap_or("default");
let warm_dir = snapshots_dir.join(format!("{}__warm__{}", derived_name, tag));
if let Ok(image) = Image::from_snapshot(&warm_dir) {
let requested_mounts = crate::bake::mounts_meta_from_extra_args(&request.extra_args);
let recorded_mounts = std::fs::read_to_string(warm_dir.join("metadata.json"))
.ok()
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
.and_then(|m| m.get("mounts").cloned())
.and_then(|v| v.as_array().cloned())
.unwrap_or_default();
if recorded_mounts != requested_mounts {
eprintln!(
"supermachine: warm snapshot at {} was baked with mounts {} but \
this build requests {}. Auto-rebaking with the current mounts.",
warm_dir.display(),
serde_json::Value::Array(recorded_mounts),
serde_json::Value::Array(requested_mounts),
);
invalidate_stale_snapshot_tree(&warm_dir);
} else if let Some(baked) = snap_dir_baked_under_other_version(&warm_dir) {
let current = env!("CARGO_PKG_VERSION");
eprintln!(
"supermachine: warm snapshot at {} was baked under v{baked}; \
current binaries are v{current}. Auto-rebaking.",
warm_dir.display()
);
invalidate_stale_snapshot_tree(&warm_dir);
invalidate_stale_snapshot_tree(&snap_dir);
} else {
return Ok(image);
}
}
let trace = crate::trace::enabled("bake");
let bake_t0 = std::time::Instant::now();
let warmup_err: std::sync::Arc<std::sync::Mutex<Option<Error>>> =
std::sync::Arc::new(std::sync::Mutex::new(None));
let warmup_err_inner = warmup_err.clone();
let warmup_t0_capture: std::sync::Arc<std::sync::Mutex<Option<std::time::Instant>>> =
std::sync::Arc::new(std::sync::Mutex::new(None));
let warmup_t0_inner = warmup_t0_capture.clone();
let pipelined = crate::bake::PipelinedWarmup {
warm_dir: warm_dir.clone(),
warm_tag: tag.to_owned(),
keep_alive: cfg!(all(target_os = "macos", target_arch = "aarch64")),
skip_warm_snapshot: false,
use_pre_exec_trigger: {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
false
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
!self.require_listener.unwrap_or(false)
}
},
callback: Box::new(move |ctx| {
if let Ok(mut g) = warmup_t0_inner.lock() {
*g = Some(std::time::Instant::now());
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
let synth_vm = Vm {
pool: None,
vsock_mux_path: ctx.vsock_mux_path.clone(),
vsock_exec_path: ctx.vsock_exec_path.clone(),
own_vsock_mux_dir: None,
skip_cleanup: true,
image_meta: None,
};
let result = warmup(&synth_vm);
drop(synth_vm);
match result {
Ok(()) => Ok(()),
Err(e) => {
let msg = e.to_string();
if let Ok(mut g) = warmup_err_inner.lock() {
*g = Some(e);
}
Err(msg)
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
let synth_vm = Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: ctx.vsock_mux_path.clone(),
vsock_exec_path: ctx.vsock_exec_path.clone(),
own_vsock_mux_dir: None,
skip_cleanup: true,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: None,
kboxlike: None,
};
let result = warmup(&synth_vm);
drop(synth_vm);
match result {
Ok(()) => Ok(()),
Err(e) => {
let msg = e.to_string();
if let Ok(mut g) = warmup_err_inner.lock() {
*g = Some(e);
}
Err(msg)
}
}
}
#[cfg(not(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
)))]
{
let _ = (&ctx, &warmup, &warmup_err_inner);
Ok(())
}
}),
};
match crate::bake::run_push_pipelined(&request, bake_t0, &root, pipelined) {
Ok(warm_handoff) => {
if trace {
eprintln!("[bake-trace] pipelined bake total: {:?}", bake_t0.elapsed());
if let Some(t0) = warmup_t0_capture.lock().ok().and_then(|g| *g) {
eprintln!("[bake-trace] (warmup ran at +{:?})", t0 - bake_t0);
}
}
let img = Image::from_snapshot(&warm_dir)?;
if let Some(bw) = warm_handoff {
*img.warm_baked_worker.inner.lock().unwrap() = Some(bw);
}
Ok(img)
}
Err(msg) => {
if let Some(e) = warmup_err.lock().ok().and_then(|mut g| g.take()) {
return Err(e);
}
Err(map_bake_error(&request.image, msg))
}
}
}
}
fn default_guest_platform() -> String {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
"linux/amd64".to_owned()
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
{
"linux/arm64".to_owned()
}
}
fn snapshot_baked_under_other_version(meta: &serde_json::Value) -> Option<String> {
let current = env!("CARGO_PKG_VERSION");
let baked = meta
.get("baked_by_version")
.and_then(|v| v.as_str())
.map(|s| s.to_owned())
.or_else(|| {
meta.get("kernel")
.and_then(|v| v.as_str())
.and_then(parse_version_segment)
})?;
if baked == current {
None
} else {
Some(baked)
}
}
fn snap_dir_baked_under_other_version(snap_dir: &Path) -> Option<String> {
let meta_text = std::fs::read_to_string(snap_dir.join("metadata.json")).ok()?;
let meta: serde_json::Value = serde_json::from_str(&meta_text).ok()?;
snapshot_baked_under_other_version(&meta)
}
fn snap_dir_backend(snap_dir: &Path) -> Option<String> {
let meta_text = std::fs::read_to_string(snap_dir.join("metadata.json")).ok()?;
let meta: serde_json::Value = serde_json::from_str(&meta_text).ok()?;
meta.get("backend")
.and_then(|v| v.as_str())
.map(|s| s.to_owned())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn linux_default_backend() -> &'static str {
match std::env::var("SUPERMACHINE_BACKEND").as_deref() {
Ok("kvm") => "kvm",
Ok("sentry") => "sentry",
Ok("kboxlike") => "kboxlike",
Ok("runsc") | Ok("auto") | Err(_) => "runsc",
Ok(other) => {
eprintln!(
"supermachine: warning: unsupported SUPERMACHINE_BACKEND={other:?}; \
defaulting to runsc"
);
"runsc"
}
}
}
fn invalidate_stale_snapshot_tree(snap_dir: &Path) {
if let Err(e) = std::fs::remove_dir_all(snap_dir) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!(
"supermachine: warning: failed to remove stale snapshot {}: {e}",
snap_dir.display()
);
}
}
let Some(parent) = snap_dir.parent() else {
return;
};
let Some(base_name) = snap_dir.file_name().and_then(|s| s.to_str()) else {
return;
};
let prefix = format!("{base_name}__warm__");
let Ok(read) = std::fs::read_dir(parent) else {
return;
};
for entry in read.flatten() {
let path = entry.path();
if path
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|n| n.starts_with(&prefix))
{
if let Err(e) = std::fs::remove_dir_all(&path) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!(
"supermachine: warning: failed to remove stale warm snapshot {}: {e}",
path.display()
);
}
}
}
}
}
fn parse_version_segment(kernel_path: &str) -> Option<String> {
let p = Path::new(kernel_path);
for comp in p.components() {
let Some(s) = comp.as_os_str().to_str() else {
continue;
};
let Some(rest) = s.strip_prefix('v') else {
continue;
};
if rest.matches('.').count() < 2 {
continue;
}
if !rest
.split('.')
.all(|seg| seg.chars().all(|c| c.is_ascii_digit()))
{
continue;
}
return Some(rest.to_owned());
}
None
}
fn parse_symlink_policy(s: &str) -> Option<crate::vmm::resources::SymlinkPolicy> {
use crate::vmm::resources::SymlinkPolicy;
match s {
"deny" => Some(SymlinkPolicy::Deny),
"opaque" => Some(SymlinkPolicy::Opaque),
"follow" => Some(SymlinkPolicy::Follow),
_ => None,
}
}
fn scrub_aotcache_orphans(cache_dir: &Path) -> usize {
use std::os::unix::ffi::OsStrExt;
let entries = match std::fs::read_dir(cache_dir) {
Ok(it) => it,
Err(_) => return 0,
};
let mut evicted_zero = 0usize;
let mut evicted_partial = 0usize;
for ent in entries.flatten() {
let name = ent.file_name();
let bytes = name.as_bytes();
let is_aotcache_final = bytes.ends_with(b".aotcache") && !bytes.starts_with(b".");
let is_aotcache_partial = bytes.starts_with(b".") && bytes.ends_with(b".aotcache.partial");
if !is_aotcache_final && !is_aotcache_partial {
continue;
}
let path = ent.path();
let Ok(md) = std::fs::metadata(&path) else {
continue;
};
if !md.is_file() {
continue;
}
if is_aotcache_partial {
if std::fs::remove_file(&path).is_ok() {
evicted_partial += 1;
}
} else if md.len() == 0 {
if std::fs::remove_file(&path).is_ok() {
evicted_zero += 1;
}
}
}
let evicted = evicted_zero + evicted_partial;
if evicted > 0 {
eprintln!(
"supermachine: scrubbed {} AOT cache orphan(s) from {} \
({} zero-byte aotcache, {} mid-flight partial) \u{2014} \
would have trapped rosetta with the segment_count \
assertion or SIGBUS on mmap past EOF",
evicted,
cache_dir.display(),
evicted_zero,
evicted_partial,
);
}
evicted
}
fn default_snapshots_dir() -> PathBuf {
if let Some(d) = std::env::var_os("SUPERMACHINE_SNAPSHOTS") {
return PathBuf::from(d);
}
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."));
home.join(".local/supermachine-snapshots")
}
fn repo_root_for_bake() -> Result<PathBuf, Error> {
if let Some(root) = std::env::var_os("SUPERMACHINE_ROOT") {
return Ok(PathBuf::from(root));
}
let exe = std::env::current_exe().map_err(|e| Error::bake_msg(format!("current_exe: {e}")))?;
for ancestor in exe.ancestors() {
if ancestor.join("tools/supermachine-push").is_file() {
return Ok(ancestor.to_path_buf());
}
if ancestor.join("share/supermachine/kernel").is_file() {
return Ok(ancestor.to_path_buf());
}
}
std::env::current_dir().map_err(|e| Error::bake_msg(format!("current_dir: {e}")))
}
fn map_bake_error(image: &str, msg: String) -> Error {
if let Some(rest) = msg.strip_prefix("KERNEL_PANIC|") {
let mut parts = rest.splitn(2, '|');
let first_line = parts.next().unwrap_or("").to_owned();
let stack: Vec<String> = parts
.next()
.map(|s| s.split('\x1F').map(ToOwned::to_owned).collect())
.unwrap_or_default();
return Error::kernel_panic(first_line, stack);
}
let lc = msg.to_ascii_lowercase();
let has_status = |code: u16| -> bool {
let needle = format!("http {code}");
lc.contains(&needle)
};
if has_status(404)
|| lc.contains("manifest unknown")
|| lc.contains("name unknown")
|| lc.contains("not found")
&& (lc.contains("registry") || lc.contains("manifest") || lc.contains("image"))
{
return Error::image_not_found(image, msg);
}
if has_status(401)
|| has_status(403)
|| lc.contains("unauthorized")
|| lc.contains("forbidden")
|| lc.contains("auth challenge")
{
return Error::registry_auth(image, msg);
}
if lc.contains("could not resolve")
|| lc.contains("dns")
|| lc.contains("connection refused")
|| lc.contains("connection reset")
|| lc.contains("network is unreachable")
|| lc.contains("ssl_connect")
|| lc.contains("tls handshake")
|| lc.contains("curl: (6)") || lc.contains("curl: (7)") || lc.contains("curl: (28)") || lc.contains("curl: (35)") || lc.contains("curl: (56)")
{
return Error::registry_unreachable(msg);
}
if lc.contains("registry") || lc.contains("manifest") || lc.contains("docker pull") {
Error::network_msg(msg)
} else {
Error::bake_msg(msg)
}
}
#[derive(Debug, Clone, Default)]
pub struct VmConfig {
memory_mib: Option<u32>,
vcpus: Option<u32>,
assets: Option<AssetPaths>,
vsock_mux_dir: Option<PathBuf>,
restore_timeout: Option<Duration>,
volumes: Vec<crate::vmm::resources::VolumeSpec>,
virtiofs: Vec<crate::vmm::resources::MountSpec>,
enable_balloon: bool,
sentry_pids_max: Option<u64>,
sentry_uid: Option<(u32, u32)>,
sentry_uncapped: bool,
for_build: bool,
sentry_exec_timeout: Option<Duration>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_DEFAULT_PIDS_MAX: u64 = 4096;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_CPU_PERIOD_US: u64 = 100_000;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
const SENTRY_DEFAULT_EXEC_TIMEOUT: Duration = Duration::from_secs(600);
impl VmConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_memory_mib(mut self, mib: u32) -> Self {
self.memory_mib = Some(mib);
self
}
pub fn with_vcpus(mut self, vcpus: u32) -> Self {
self.vcpus = Some(vcpus);
self
}
pub fn with_balloon(mut self, enable: bool) -> Self {
self.enable_balloon = enable;
self
}
pub fn with_volume(mut self, spec: crate::vmm::resources::VolumeSpec) -> Self {
self.volumes.push(spec);
self
}
pub fn with_virtiofs(mut self, spec: crate::vmm::resources::MountSpec) -> Self {
self.virtiofs.push(spec);
self
}
pub fn with_sentry_pids_max(mut self, n: u64) -> Self {
self.sentry_pids_max = Some(n);
self
}
pub fn with_sentry_uid(mut self, uid: u32, gid: u32) -> Self {
self.sentry_uid = Some((uid, gid));
self
}
pub fn with_sentry_uncapped(mut self, uncapped: bool) -> Self {
self.sentry_uncapped = uncapped;
self
}
pub fn with_sentry_exec_timeout(mut self, dur: Duration) -> Self {
self.sentry_exec_timeout = Some(dur);
self
}
pub fn with_for_build(mut self, on: bool) -> Self {
self.for_build = on;
self
}
pub fn with_assets(mut self, assets: AssetPaths) -> Self {
self.assets = Some(assets);
self
}
pub fn with_vsock_mux_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.vsock_mux_dir = Some(dir.into());
self
}
pub fn with_restore_timeout(mut self, timeout: Duration) -> Self {
self.restore_timeout = Some(timeout);
self
}
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub struct Vm {
pool: Option<WarmPool>,
vsock_mux_path: PathBuf,
vsock_exec_path: PathBuf,
own_vsock_mux_dir: Option<PathBuf>,
skip_cleanup: bool,
image_meta: Option<Arc<ImageMeta>>,
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub struct Vm {
running: Option<crate::kvm::run::RunningVm>,
sentry_exec: Option<crate::sentry::ExecServer>,
sentry_src_rootfs: Option<PathBuf>,
vsock_mux_path: PathBuf,
vsock_exec_path: PathBuf,
own_vsock_mux_dir: Option<PathBuf>,
skip_cleanup: bool,
time_sync_stop: Arc<AtomicBool>,
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
_sentry_mem_admission: Option<crate::memory_admission::AdmissionGuard>,
sentry_warm_pool: Option<Arc<crate::sentry::Pool>>,
sentry_unified_pool: Option<Arc<crate::sentry::Pool>>,
runsc: Option<RunscRuntime>,
kboxlike: Option<KboxlikeRuntime>,
}
#[cfg(not(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
)))]
pub struct Vm {
_never: std::convert::Infallible,
}
#[derive(Clone, Debug)]
pub(crate) struct ImageMeta {
pub memory_mib: u32,
pub vcpus: u32,
pub layers: Vec<PathBuf>,
pub delta_squashfs: Option<PathBuf>,
pub baker_runtime_sha16: Option<String>,
pub tsi_token: Option<String>,
pub egress_policy: Option<String>,
pub pre_exec_sync: bool,
}
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
impl Vm {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn start(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
#[cfg(target_os = "macos")]
if let Err(msg) = crate::codesign::check_self_has_hvf_entitlement() {
return Err(Error::vm_msg(msg));
}
let assets = match &config.assets {
Some(a) => a.clone(),
None => AssetPaths::discover(),
};
let kernel: PathBuf = if let Some(k) = image.bundled_kernel.as_ref() {
k.clone()
} else if let Some(k) = assets.kernel.as_ref() {
k.clone()
} else {
return Err(Error::assets_msg(
"no kernel found: snapshot dir has no bundled kernel and AssetPaths::discover() came up empty; set VmConfig::with_assets() or $SUPERMACHINE_ASSETS_DIR".to_owned(),
));
};
let kernel = kernel.as_path();
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
let mut own_dir = None;
if !dir.is_dir() {
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
own_dir = Some(dir.clone());
}
let vsock_mux_path = dir.join(format!(
"supermachine-vm-{}-{}.sock",
std::process::id(),
unique_suffix(),
));
let vsock_exec_path = {
let mut p = vsock_mux_path.clone();
let mut name = p.file_name().unwrap().to_owned();
name.push("-exec");
p.set_file_name(name);
p
};
let memory_mib = config.memory_mib.unwrap_or(image.memory_mib);
let vcpus = config.vcpus.unwrap_or(image.vcpus);
let mut resources = VmResources::new()
.with_kernel_path(kernel.to_string_lossy().to_string())
.with_memory_mib(memory_mib as usize)
.with_vcpus(vcpus)
.with_cow_restore(true)
.with_restore(image.snapshot_path.to_string_lossy().to_string())
.with_vsock_mux(vsock_mux_path.to_string_lossy().to_string())
.with_vsock_exec(vsock_exec_path.to_string_lossy().to_string());
for layer in &image.layers {
resources = resources.with_block_device(layer.to_string_lossy().to_string());
}
if let Some(delta) = &image.delta_squashfs {
resources = resources.with_block_device(delta.to_string_lossy().to_string());
}
let options = RunOptions::default();
let pool = WarmPool::start(resources, options).map_err(Error::from)?;
let timeout = config
.restore_timeout
.unwrap_or_else(|| Duration::from_secs(10));
let _ = pool
.restore_timeout(image.snapshot_path.to_string_lossy().to_string(), timeout)
.map_err(Error::from)?;
Ok(Vm {
pool: Some(pool),
vsock_mux_path,
vsock_exec_path,
own_vsock_mux_dir: own_dir,
skip_cleanup: false,
image_meta: Some(Arc::new(ImageMeta {
memory_mib,
vcpus,
layers: image.layers.clone(),
delta_squashfs: image.delta_squashfs.clone(),
baker_runtime_sha16: image.baker_runtime_sha16.clone(),
tsi_token: image.tsi_token.clone(),
egress_policy: image.egress_policy.clone(),
pre_exec_sync: image.pre_exec_sync,
})),
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn start(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
use crate::kvm::run::{LinuxVm, LinuxVmConfig};
if image.sentry.is_some() {
return Vm::start_sentry(image, config);
}
if image.runsc.is_some() {
return Vm::start_runsc(image, config);
}
if image.kboxlike.is_some() {
return Vm::start_kboxlike(image, config);
}
let parts = image.kvm.as_ref().ok_or_else(|| {
Error::vm_msg(
"Image has no KVM artifacts: metadata.json needs \"backend\":\"kvm\" \
with kvm_kernel + kvm_initrd (produced by the KVM bake path)."
.to_owned(),
)
})?;
let vm = if let Some(snap) = &parts.snapshot {
if !config.volumes.is_empty() {
eprintln!(
"supermachine(KVM): {} volume(s) requested via VmConfig but this image \
restores from a snapshot — the snapshot's OWN recorded volume attachments \
are re-attached (SMSNAP04), so the config volumes are ignored. To attach \
different volumes, cold-boot the image (e.g. bake_kvm_auto) with them.",
config.volumes.len()
);
}
if !config.virtiofs.is_empty() {
eprintln!(
"supermachine(KVM): {} virtio-fs mount(s) requested via VmConfig but this image \
restores from a snapshot — the snapshot's OWN recorded virtio-fs mounts are \
re-attached (SMSNAP05, DAX slots eagerly re-bound), so the config mounts are \
ignored. To attach different mounts, cold-boot the image with them.",
config.virtiofs.len()
);
}
LinuxVm::restore_from_file(snap)
.map_err(|e| Error::vm_msg(format!("restore snapshot {}: {e}", snap.display())))?
} else {
let kernel_path = parts
.kernel
.as_ref()
.expect("kvm cold-boot kernel (validated in from_snapshot)");
let initrd_path = parts
.initrd
.as_ref()
.expect("kvm cold-boot initrd (validated in from_snapshot)");
let kernel = std::fs::read(kernel_path).map_err(|e| {
Error::vm_msg(format!("read kernel {}: {e}", kernel_path.display()))
})?;
let initrd = std::fs::read(initrd_path).map_err(|e| {
Error::vm_msg(format!("read initrd {}: {e}", initrd_path.display()))
})?;
let disk_str: Option<String> = parts
.disk
.as_ref()
.map(|p| p.to_string_lossy().into_owned());
let disk_size = parts
.disk
.as_ref()
.and_then(|p| std::fs::metadata(p).ok())
.map(|m| m.len())
.unwrap_or(0);
let mem_mib = config.memory_mib.unwrap_or(image.memory_mib).max(1);
let num_cpus = kvm_effective_vcpus(config.vcpus.unwrap_or(image.vcpus), "Vm::start")
.clamp(1, 255) as u8;
let host_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let cmdline = format!(
"console=ttyS0 panic=-1 reboot=t tsi_hijack supermachine.host_time={host_time}"
);
let tsi_token = crate::cli::TsiToken::generate()
.map(|t| t.bytes)
.map_err(|e| {
eprintln!("supermachine(KVM): tsi token generation failed ({e}); booting without vsock control-channel auth");
})
.ok();
let mut vol_specs: Vec<crate::vmm::resources::VolumeSpec> = config.volumes.clone();
for (host_file, guest_path, size_bytes, _pristine) in &image.volumes {
if !vol_specs.iter().any(|v| v.guest_path == *guest_path) {
vol_specs.push(
crate::vmm::resources::VolumeSpec::new(
host_file.to_string_lossy().to_string(),
guest_path.clone(),
)
.with_size_bytes(*size_bytes),
);
}
}
let volumes = prepare_kvm_volumes(&vol_specs)?;
let mut mount_specs: Vec<crate::vmm::resources::MountSpec> = config.virtiofs.clone();
for m in &image.mounts {
if !mount_specs.iter().any(|existing| {
existing.guest_tag == m.guest_tag || existing.guest_path == m.guest_path
}) {
mount_specs.push(m.clone());
}
}
let virtiofs: Vec<crate::kvm::run::VirtioFsAttach> = mount_specs
.iter()
.map(|m| crate::kvm::run::VirtioFsAttach {
host_path: m.host_path.clone(),
tag: m.guest_tag.clone(),
mount: m.guest_path.clone(),
})
.collect();
let lcfg = LinuxVmConfig {
mem_size: (mem_mib as usize) * 1024 * 1024,
num_cpus,
kernel: &kernel,
initrd: Some(&initrd),
disk_path: disk_str.as_deref(),
disk_size,
cmdline: &cmdline,
enable_vsock: true,
volumes: &volumes,
virtiofs: &virtiofs,
tsi_token,
enable_balloon: config.enable_balloon,
};
LinuxVm::new(&lcfg).map_err(|e| Error::vm_msg(format!("LinuxVm::new: {e}")))?
};
let exec_path = vm
.start_exec_bridge(1028)
.map_err(|e| Error::vm_msg(format!("start_exec_bridge: {e}")))?;
let mux_path = vm
.start_tsi_mux()
.unwrap_or_else(|_| exec_path.with_extension("mux-unwired"));
let running = vm.start_running();
let sync_path = exec_path.clone();
let time_sync_stop = Arc::new(AtomicBool::new(false));
let stop_c = time_sync_stop.clone();
std::thread::spawn(move || {
let deadline = std::time::Instant::now() + Duration::from_secs(30);
loop {
if stop_c.load(Ordering::SeqCst) {
break;
}
if sync_time_via_agent(&sync_path) {
break;
}
if std::time::Instant::now() >= deadline {
break;
}
std::thread::sleep(Duration::from_millis(200));
}
});
Ok(Vm {
running: Some(running),
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: mux_path,
vsock_exec_path: exec_path,
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop,
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: None,
kboxlike: None,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn start_sentry(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
let parts = image
.sentry
.as_ref()
.expect("start_sentry called on a non-sentry image");
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
let mut own_dir = None;
if !dir.is_dir() {
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
own_dir = Some(dir.clone());
}
let exec_path = dir.join(format!(
"supermachine-sentry-{}-{}.sock",
std::process::id(),
unique_suffix(),
));
let sandbox = image
.sentry_sandbox_with(config)
.map_err(|e| Error::vm_msg(format!("sentry sandbox: {e}")))?
.without_netns();
crate::memory_admission::await_pressure_relief();
let mem_mib = sentry_cell_mem_mib(image, config);
let mem_admission =
crate::memory_admission::admit_with(|| sentry_admission_charge_mib(mem_mib, 1));
let (sentry_exec, sentry_unified_pool) = if !parts.warm_daemon {
let pool = sandbox
.pool_with_exec(exec_path.clone())
.map_err(|e| Error::vm_msg(format!("sentry unified pool: {e}")))?;
let deadline = std::time::Instant::now() + Duration::from_secs(15);
while !exec_path.exists() {
if std::time::Instant::now() >= deadline {
return Err(Error::vm_msg(
"sentry unified exec socket never appeared (supervisor bind failed)",
));
}
std::thread::sleep(Duration::from_millis(5));
}
if parts.has_workload && parts.rootfs.join(".supermachine/run-workload").is_file() {
let wrapped = vec![
"/.supermachine/sm-checkpoint".to_string(),
"/bin/sh".to_string(),
"/.supermachine/run-workload".to_string(),
];
if pool.warm(wrapped.iter().cloned()).is_ok() {
if let Ok(wpid) = pool.acquire_running() {
let run_dir = parts.rootfs.join("run");
let _ = std::fs::create_dir_all(&run_dir);
let _ = std::fs::write(
run_dir.join("supermachine-workload.pid"),
format!("{wpid}\n"),
);
}
}
}
(None, Some(Arc::new(pool)))
} else {
let server = sandbox
.serve_exec(&exec_path, parts.has_workload)
.map_err(|e| Error::vm_msg(format!("sentry serve_exec: {e}")))?;
(Some(server), None)
};
Ok(Vm {
running: None,
sentry_exec,
sentry_src_rootfs: Some(parts.rootfs.clone()),
vsock_mux_path: exec_path.clone(),
vsock_exec_path: exec_path,
own_vsock_mux_dir: own_dir,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: Some(mem_admission),
sentry_warm_pool: None,
sentry_unified_pool,
runsc: None,
kboxlike: None,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn start_runsc(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
let parts = image
.runsc
.as_ref()
.expect("start_runsc called on a non-runsc image");
let runsc_bin = std::env::var_os("SUPERMACHINE_RUNSC_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("runsc"));
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
let runtime_dir = dir.join(format!(
"supermachine-runsc-{}-{}",
std::process::id(),
unique_suffix()
));
let bundle_dir = runtime_dir.join("bundle");
let private_rootfs = bundle_dir.join("rootfs");
let runsc_root = runtime_dir.join("runsc-root");
std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
let network = runsc_network_for_config(config).to_owned();
let build_netns = if network == crate::exec::RUNSC_NETWORK_SANDBOX {
Some(RunscBuildNetwork::create()?)
} else {
None
};
let netns_path = build_netns.as_ref().map(|net| net.path());
prepare_runsc_bundle(
&runsc_bin,
parts,
&bundle_dir,
&private_rootfs,
&network,
netns_path.as_deref(),
)?;
let container_id = format!("sm-runsc-{}-{}", std::process::id(), unique_suffix());
let pid_file = runtime_dir.join("container.pid");
if let Some(checkpoint) = &parts.checkpoint {
if let Err(e) = runsc_restore_detached(
&runsc_bin,
&runsc_root,
&network,
&bundle_dir,
checkpoint,
&pid_file,
&container_id,
) {
let _ = std::fs::remove_dir_all(&runtime_dir);
return Err(e);
}
} else {
let mut run = Command::new(&runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut run, &runsc_root, &network);
let run_out = run
.arg("run")
.arg("-detach")
.arg("-bundle")
.arg(&bundle_dir)
.arg("-pid-file")
.arg(&pid_file)
.arg(&container_id)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| Error::vm_msg(format!("runsc run {container_id}: {e}")))?;
if !run_out.success() {
let _ = std::fs::remove_dir_all(&runtime_dir);
return Err(Error::vm_msg(format!(
"runsc run failed with status {:?}",
run_out.code()
)));
}
}
Ok(Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: Some(RunscRuntime {
runsc_bin,
runsc_root,
network,
build_netns,
container_id,
bundle_dir,
rootfs: private_rootfs,
parts: parts.clone(),
file_agent_exec: false,
}),
kboxlike: None,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn start_kboxlike(image: &Image, config: &VmConfig) -> Result<Vm, Error> {
let parts = image
.kboxlike
.as_ref()
.expect("start_kboxlike called on a non-kboxlike image");
Self::start_kboxlike_parts(parts, config)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_mounts_for_start(
parts: &KboxlikeImageParts,
config: &VmConfig,
) -> Result<Vec<(PathBuf, String)>, Error> {
let mut mounts = Vec::new();
let mut covered_guest_paths = std::collections::BTreeSet::new();
for m in &config.virtiofs {
covered_guest_paths.insert(m.guest_path.clone());
mounts.push((PathBuf::from(&m.host_path), m.guest_path.clone()));
}
for m in &parts.mounts {
if covered_guest_paths.insert(m.guest_path.clone()) {
mounts.push((PathBuf::from(&m.host_path), m.guest_path.clone()));
}
}
for v in &config.volumes {
let host_dir = PathBuf::from(&v.host_path);
std::fs::create_dir_all(&host_dir).map_err(Error::Io)?;
covered_guest_paths.insert(v.guest_path.clone());
mounts.push((host_dir, v.guest_path.clone()));
}
for (host_dir, guest_path, _size, _pristine) in &parts.volumes {
if !covered_guest_paths.insert(guest_path.clone()) {
continue;
}
std::fs::create_dir_all(host_dir).map_err(Error::Io)?;
mounts.push((host_dir.clone(), guest_path.clone()));
}
Ok(mounts)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn start_kboxlike_parts(parts: &KboxlikeImageParts, config: &VmConfig) -> Result<Vm, Error> {
if parts.warm_daemon {
return Err(Error::vm_msg(
"kboxlike warm-daemon images are not implemented yet".to_owned(),
));
}
let workload = parts.rootfs.join(".supermachine/run-workload");
let argv = if parts.has_workload && workload.is_file() {
vec![
"/bin/sh".to_owned(),
"/.supermachine/run-workload".to_owned(),
]
} else if !parts.cmd.is_empty() {
parts.cmd.clone()
} else {
return Err(Error::vm_msg(
"kboxlike image has no staged workload or recorded cmd".to_owned(),
));
};
let model_exe = kboxlike_host_exe_path(parts, argv.first().map(String::as_str));
let dir = match &config.vsock_mux_dir {
Some(d) => d.clone(),
None => std::env::temp_dir(),
};
std::fs::create_dir_all(&dir).map_err(Error::Io)?;
let runtime_dir = dir.join(format!(
"supermachine-kboxlike-{}-{}",
std::process::id(),
unique_suffix()
));
let shadow_root = runtime_dir.join("shadow");
std::fs::create_dir_all(&shadow_root).map_err(Error::Io)?;
let settle_ms = std::env::var("SUPERMACHINE_KBOXLIKE_CAPTURE_SETTLE_MS")
.ok()
.and_then(|raw| raw.parse::<u64>().ok())
.unwrap_or(750);
let min_tracees = std::env::var("SUPERMACHINE_KBOXLIKE_MIN_TRACEES")
.ok()
.and_then(|raw| raw.parse::<usize>().ok())
.unwrap_or(1);
let max_stops = std::env::var("SUPERMACHINE_KBOXLIKE_MAX_STOPS")
.ok()
.and_then(|raw| raw.parse::<usize>().ok())
.unwrap_or(200000);
let runtime_mounts = Self::kboxlike_mounts_for_start(parts, config)?;
let captured =
crate::kboxlike::capture_live_ptrace_rootfs_exec_snapshot_after_sigstop_request(
&crate::kboxlike::KboxlikeRootfsExecConfig {
rootfs: parts.rootfs.clone(),
argv,
env: parts.image_env.clone(),
cwd: "/".to_owned(),
user: parts.user,
mounts: runtime_mounts.clone(),
},
min_tracees,
max_stops,
std::time::Duration::from_millis(settle_ms),
)
.map_err(|e| {
let _ = std::fs::remove_dir_all(&runtime_dir);
Error::vm_msg(format!("kboxlike capture workload: {e:?}"))
})?;
let mut source_root_host_pid = Some(captured.root_host_pid);
let mut replacement_factory =
if let Some(path) = std::env::var_os("SUPERMACHINE_KBOXLIKE_REPLACEMENT_PATH") {
crate::kboxlike::LinuxStoppedTraceeReplacementFactory::with_static_replacement_path(
PathBuf::from(path),
)
} else if Path::new("/bin/busybox").is_file() {
crate::kboxlike::LinuxStoppedTraceeReplacementFactory::with_static_replacement_path(
"/bin/busybox",
)
} else {
crate::kboxlike::LinuxStoppedTraceeReplacementFactory::default()
};
let mut resumer = crate::kboxlike::PtraceDetachRestoredTraceeResumer;
let trace_label =
std::env::var_os("SUPERMACHINE_KBOXLIKE_TRACE_VM_START").map(|_| "KBOXLIVE_VM_START");
let restore = crate::kboxlike::restore_runtime_snapshot_with_replacements(
&captured.snapshot,
&model_exe,
crate::kboxlike::KboxlikeRuntimeRestoreOptions {
shadow_root: shadow_root.clone(),
trace_label,
..Default::default()
},
&mut replacement_factory,
&mut resumer,
)
.map_err(|e| {
if let Some(pid) = source_root_host_pid.take() {
crate::kboxlike::kill_and_reap_live_process_group(pid);
}
replacement_factory.kill_created_tracees();
let _ = std::fs::remove_dir_all(&runtime_dir);
Error::vm_msg(format!("kboxlike restore workload: {e:?}"))
})?;
if let Some(pid) = source_root_host_pid.take() {
crate::kboxlike::kill_and_reap_live_process_group(pid);
}
let restored_host_pids = restore
.restore
.replacement_host_pids
.values()
.copied()
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
Ok(Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: runtime_dir.join("kboxlike-unwired.sock"),
vsock_exec_path: runtime_dir.join("kboxlike-exec-unwired.sock"),
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: None,
kboxlike: Some(KboxlikeRuntime {
runtime_dir,
source_root_host_pid,
restored_host_pids,
rootfs: parts.rootfs.clone(),
parts: parts.clone(),
mounts: runtime_mounts,
}),
})
}
pub fn vsock_path(&self) -> &Path {
&self.vsock_mux_path
}
pub fn exec_path(&self) -> &Path {
&self.vsock_exec_path
}
#[doc(hidden)]
pub fn exec_binding_token(&self) -> String {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
let target = crate::exec::RunscExecTarget {
runsc_bin: runsc.runsc_bin.clone(),
runsc_root: runsc.runsc_root.clone(),
network: Some(runsc.network.clone()),
container_id: runsc.container_id.clone(),
image_env: runsc.parts.image_env.clone(),
agent_path: Some(runsc.parts.agent_path.clone()),
file_agent_rootfs: runsc.file_agent_exec.then(|| runsc.rootfs.clone()),
};
if let Ok(bytes) = serde_json::to_vec(&target) {
return format!(
"{}{}",
crate::exec::RUNSC_EXEC_TOKEN_PREFIX,
b64_encode(&bytes)
);
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
let target = crate::exec::KboxlikeExecTarget {
rootfs: kboxlike.rootfs.clone(),
image_env: kboxlike.parts.image_env.clone(),
user: kboxlike.parts.user,
mounts: kboxlike.mounts.clone(),
};
if let Ok(bytes) = serde_json::to_vec(&target) {
return format!(
"{}{}",
crate::exec::KBOXLIKE_EXEC_TOKEN_PREFIX,
b64_encode(&bytes)
);
}
}
self.vsock_exec_path.to_string_lossy().to_string()
}
pub fn exec<I, S>(&self, argv: I) -> std::io::Result<crate::exec::ExecChild>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let argv: Vec<String> = argv.into_iter().map(|s| s.into()).collect();
let _span = tracing::info_span!(
"supermachine.exec",
argv0 = argv.first().map(|s| s.as_str()).unwrap_or(""),
argc = argv.len(),
)
.entered();
self.exec_builder().argv(argv).spawn()
}
pub fn exec_builder(&self) -> crate::exec::ExecBuilder {
let b = crate::exec::ExecBuilder::new(self.vsock_exec_path.clone());
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return b.with_runsc_exec(crate::exec::RunscExecTarget {
runsc_bin: runsc.runsc_bin.clone(),
runsc_root: runsc.runsc_root.clone(),
network: Some(runsc.network.clone()),
container_id: runsc.container_id.clone(),
image_env: runsc.parts.image_env.clone(),
agent_path: Some(runsc.parts.agent_path.clone()),
file_agent_rootfs: runsc.file_agent_exec.then(|| runsc.rootfs.clone()),
});
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return b.with_kboxlike_exec(crate::exec::KboxlikeExecTarget {
rootfs: kboxlike.rootfs.clone(),
image_env: kboxlike.parts.image_env.clone(),
user: kboxlike.parts.user,
mounts: kboxlike.mounts.clone(),
});
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(pool) = &self.sentry_warm_pool {
return b.with_warm_pool(Arc::clone(pool));
}
b
}
pub fn write_file(&self, path: &str, bytes: &[u8]) -> std::io::Result<()> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return runsc_write_file(runsc, path, bytes);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return kboxlike_write_file(kboxlike, path, bytes);
}
let body = serde_json::json!({
"action": "write_file",
"path": path,
"data_b64": b64_encode(bytes),
});
crate::exec::send_control(&self.vsock_exec_path, &body)
}
pub fn read_file(&self, path: &str) -> std::io::Result<Vec<u8>> {
self.read_file_with_max_bytes(path, 32 * 1024 * 1024)
}
pub fn read_file_with_max_bytes(&self, path: &str, max_bytes: u64) -> std::io::Result<Vec<u8>> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return runsc_read_file(runsc, path, max_bytes);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return kboxlike_read_file(kboxlike, path, max_bytes);
}
let body = serde_json::json!({
"action": "read_file",
"path": path,
"max_bytes": max_bytes,
});
let ack = crate::exec::send_control_with_ack(
&self.vsock_exec_path,
&body,
Some(std::time::Duration::from_secs(30)),
)?;
let data_b64 = ack
.get("data_b64")
.and_then(|v| v.as_str())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"read_file: agent ack missing data_b64",
)
})?;
b64_decode(data_b64).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn workload_signal(&self, signum: i32) -> std::io::Result<()> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return runsc_workload_signal(runsc, signum);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return kboxlike_workload_signal(kboxlike, signum);
}
let body = serde_json::json!({
"action": "signal",
"signum": signum,
});
crate::exec::send_control(&self.vsock_exec_path, &body)
}
pub fn connect(&self) -> std::io::Result<UnixStream> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return runsc_connect_port(runsc, 0);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return kboxlike_connect_port(kboxlike, 0);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if self.running.is_none() {
return connect_port_sentry(&self.vsock_exec_path, 0);
}
UnixStream::connect(&self.vsock_mux_path)
}
pub fn connect_port(&self, port: u16) -> std::io::Result<UnixStream> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return runsc_connect_port(runsc, port);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return kboxlike_connect_port(kboxlike, port);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if self.running.is_none() {
return connect_port_sentry(&self.vsock_exec_path, port);
}
let unix = UnixStream::connect(&self.vsock_mux_path)?;
if port != 0 {
let mut hdr = [0u8; 20];
hdr[..16].copy_from_slice(b"SMUX-PORT-V1\0\0\0\0");
hdr[16..].copy_from_slice(&(port as u32).to_be_bytes());
(&unix).write_all(&hdr)?;
}
Ok(unix)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn request_balloon_inflate(&self, pages: u32) -> bool {
self.running
.as_ref()
.map(|r| r.request_balloon_inflate(pages))
.unwrap_or(false)
}
pub fn expose_tcp(&self, host_port: u16, guest_port: u16) -> std::io::Result<TcpForwarder> {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(runsc) = &self.runsc {
return self.expose_tcp_runsc(host_port, guest_port, runsc);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if let Some(kboxlike) = &self.kboxlike {
return self.expose_tcp_kboxlike(host_port, guest_port, kboxlike);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
if self.running.is_none() {
return self.expose_tcp_sentry(host_port, guest_port);
}
let listener = TcpListener::bind(("127.0.0.1", host_port))?;
let bound = listener.local_addr()?;
listener.set_nonblocking(false)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let vsock_path = self.vsock_mux_path.clone();
let handle = std::thread::Builder::new()
.name(format!("supermachine-tcp-{host_port}"))
.spawn(move || {
accept_loop(listener, vsock_path, stop_thread, guest_port);
})
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(TcpForwarder {
stop,
handle: Some(handle),
bound,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn expose_tcp_sentry(&self, host_port: u16, guest_port: u16) -> std::io::Result<TcpForwarder> {
let listener = TcpListener::bind(("127.0.0.1", host_port))?;
let bound = listener.local_addr()?;
listener.set_nonblocking(false)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let exec_path = self.vsock_exec_path.clone();
let handle = std::thread::Builder::new()
.name(format!("supermachine-tcp-{host_port}"))
.spawn(move || sentry_accept_loop(listener, exec_path, guest_port, stop_thread))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(TcpForwarder {
stop,
handle: Some(handle),
bound,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn expose_tcp_runsc(
&self,
host_port: u16,
guest_port: u16,
runsc: &RunscRuntime,
) -> std::io::Result<TcpForwarder> {
let listener = TcpListener::bind(("127.0.0.1", host_port))?;
let bound = listener.local_addr()?;
listener.set_nonblocking(false)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let target = runsc_port_forward_target(runsc);
let handle = std::thread::Builder::new()
.name(format!("supermachine-tcp-{host_port}"))
.spawn(move || runsc_accept_loop(listener, target, guest_port, stop_thread))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(TcpForwarder {
stop,
handle: Some(handle),
bound,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn expose_tcp_kboxlike(
&self,
host_port: u16,
guest_port: u16,
kboxlike: &KboxlikeRuntime,
) -> std::io::Result<TcpForwarder> {
let listener = TcpListener::bind(("127.0.0.1", host_port))?;
let bound = listener.local_addr()?;
listener.set_nonblocking(false)?;
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let target = kboxlike_port_forward_target(kboxlike);
let handle = std::thread::Builder::new()
.name(format!("supermachine-tcp-{host_port}"))
.spawn(move || kboxlike_accept_loop(listener, target, guest_port, stop_thread))
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(TcpForwarder {
stop,
handle: Some(handle),
bound,
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn expose_tls(
&self,
cfg: crate::vmm::tls::TlsConfig,
) -> std::io::Result<std::net::SocketAddr> {
if self.runsc.is_some() {
let runsc = self.runsc.as_ref().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "runsc VM is not running")
})?;
let guest_port = match cfg.vm_port {
Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"expose_tls on the runsc backend requires cfg.vm_port = Some(non-zero u16 guest port)",
))
}
};
let target = runsc_port_forward_target(runsc);
return crate::vmm::tls::start_to_unix_stream(
cfg,
format!("runsc bridge guest 127.0.0.1:{guest_port}"),
move || runsc_connect_port_target(&target, guest_port),
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()));
}
if let Some(kboxlike) = &self.kboxlike {
let guest_port = match cfg.vm_port {
Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"expose_tls on the kboxlike backend requires cfg.vm_port = Some(non-zero u16 guest port)",
))
}
};
let target = kboxlike_port_forward_target(kboxlike);
return crate::vmm::tls::start_to_unix_stream(
cfg,
format!("kboxlike bridge guest 127.0.0.1:{guest_port}"),
move || kboxlike_connect_port_target(&target, guest_port),
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()));
}
if self.running.is_none() {
return self.expose_tls_sentry(cfg);
}
let running = self.running.as_ref().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotConnected, "VM is not running")
})?;
running
.expose_tls(cfg)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn expose_tls_sentry(
&self,
cfg: crate::vmm::tls::TlsConfig,
) -> std::io::Result<std::net::SocketAddr> {
let guest_port = match cfg.vm_port {
Some(p) if p != 0 && p <= u16::MAX as u32 => p as u16,
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"expose_tls on the sentry backend requires cfg.vm_port = Some(non-zero u16 guest port)",
))
}
};
let exec_path = self.vsock_exec_path.clone();
crate::vmm::tls::start_to_unix_stream(
cfg,
format!("sentry bridge guest 127.0.0.1:{guest_port}"),
move || connect_port_sentry(&exec_path, guest_port),
)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn stop(mut self) -> Result<(), Error> {
if let Some(pool) = self.pool.take() {
let _ = pool.shutdown().map_err(Error::from)?;
}
self.cleanup_socket();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn stop(mut self) -> Result<(), Error> {
if let Some(running) = self.running.take() {
let _ = running.stop();
}
if let Some(srv) = self.sentry_exec.take() {
srv.stop();
}
if let Some(pool) = self.sentry_unified_pool.take() {
pool.stop();
}
if let Some(mut runtime) = self.kboxlike.take() {
runtime.cleanup();
}
self.cleanup_socket();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) fn reset_to_snapshot(&self) -> Result<(), Error> {
self.running
.as_ref()
.ok_or_else(|| Error::vm_msg("reset_to_snapshot: VM has no running handle".to_owned()))?
.reset_to_snapshot()
.map_err(|e| Error::vm_msg(format!("reset_to_snapshot: {e}")))
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
pub fn snapshot(mut self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
let meta = self.image_meta.clone().ok_or_else(|| {
Error::vm_msg(
"Vm::snapshot requires an in-process Vm (use image.start, not image.acquire)"
.to_owned(),
)
})?;
let pool = self.pool.as_ref().ok_or_else(|| {
Error::vm_msg("Vm::snapshot: no pool to drive the capture".to_owned())
})?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let snap_path = dest_dir.join("restore.snap");
let _ = guest_sync_via_agent(&self.vsock_exec_path);
let _result = pool
.snapshot_timeout(
snap_path.to_string_lossy().to_string(),
Duration::from_secs(60),
)
.map_err(|e| Error::Vm {
msg: format!("snapshot capture failed: {e:?}"),
source: None,
})?;
let metadata = serde_json::json!({
"memory_mib": meta.memory_mib,
"vcpus": meta.vcpus,
"layers": meta
.layers
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<_>>(),
"delta_squashfs": meta
.delta_squashfs
.as_ref()
.map(|p| p.to_string_lossy().to_string()),
"snapshot_base": snap_path.to_string_lossy().to_string(),
"baked_at": chrono_rfc3339_now(),
"source": "Vm::snapshot",
});
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
if let Some(pool) = self.pool.take() {
let _ = pool.shutdown();
}
self.cleanup_socket();
self.skip_cleanup = true;
Image::from_snapshot(&dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot(mut self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
if let Some(runsc) = self.runsc.as_ref() {
let image = write_runsc_snapshot_dir(runsc, &dest_dir, "Vm::snapshot", false)?;
self.cleanup_socket();
self.skip_cleanup = true;
return Ok(image);
}
if self.kboxlike.is_some() {
return Err(Error::vm_msg(
"Vm::snapshot is not implemented for the kboxlike backend yet".to_owned(),
));
}
if self.running.is_none() {
return self.snapshot_sentry(dest_dir);
}
let running = self.running.take().ok_or_else(|| {
Error::vm_msg("Vm::snapshot: this VM is not running (already stopped?)".to_owned())
})?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let _ = guest_sync_via_agent(&self.vsock_exec_path);
let snap = running
.snapshot(Some(&dest_dir.join("volumes")))
.map_err(|e| Error::vm_msg(format!("capture snapshot: {e}")))?;
let snap_path = dest_dir.join("vm.snap");
snap.save(&snap_path)
.map_err(|e| Error::vm_msg(format!("write snapshot {}: {e}", snap_path.display())))?;
let metadata = serde_json::json!({
"backend": "kvm",
"kvm_snapshot": snap_path.to_string_lossy(),
"memory_mib": (snap.mem_size() >> 20) as u32,
"vcpus": snap.num_cpus() as u32,
"baked_at": chrono_rfc3339_now(),
"source": "Vm::snapshot",
});
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
self.cleanup_socket();
self.skip_cleanup = true;
Image::from_snapshot(&dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn snapshot_sentry(mut self, dest_dir: PathBuf) -> Result<Image, Error> {
let src_rootfs = self.sentry_src_rootfs.clone().ok_or_else(|| {
Error::vm_msg(
"Vm::snapshot on the sentry backend requires an in-process Vm from \
Image::start (pooled VMs share a server and can't snapshot the rootfs)"
.to_owned(),
)
})?;
let image = write_sentry_snapshot_dir_inner(
&src_rootfs,
&dest_dir,
"Vm::snapshot",
false,
None,
None,
)?;
if let Some(srv) = self.sentry_exec.take() {
srv.stop();
}
self.cleanup_socket();
self.skip_cleanup = true;
Ok(image)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot_live(&self, dest_dir: impl Into<PathBuf>) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
let running = self
.running
.as_ref()
.ok_or_else(|| Error::vm_msg("Vm::snapshot_live: VM not running".to_owned()))?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let _ = guest_sync_via_agent(&self.vsock_exec_path);
let snap = running
.snapshot_live(Some(&dest_dir.join("volumes")))
.map_err(|e| Error::vm_msg(format!("capture live snapshot: {e}")))?;
let snap_path = dest_dir.join("restore.snap");
snap.save(&snap_path)
.map_err(|e| Error::vm_msg(format!("write snapshot {}: {e}", snap_path.display())))?;
write_kvm_snapshot_metadata(
&dest_dir,
"restore.snap",
None,
snap.mem_size(),
snap.num_cpus(),
)?;
Image::from_snapshot(&dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot_diff_live(
&self,
dest_dir: impl Into<PathBuf>,
base_snap: &Path,
) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
let running = self
.running
.as_ref()
.ok_or_else(|| Error::vm_msg("Vm::snapshot_diff_live: VM not running".to_owned()))?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let _ = guest_sync_via_agent(&self.vsock_exec_path);
let snap = running
.snapshot_live(Some(&dest_dir.join("volumes")))
.map_err(|e| Error::vm_msg(format!("capture live snapshot: {e}")))?;
let snap_path = dest_dir.join("restore.snap");
snap.save_diff(&snap_path, base_snap).map_err(|e| {
Error::vm_msg(format!("write diff snapshot {}: {e}", snap_path.display()))
})?;
write_kvm_snapshot_metadata(
&dest_dir,
"restore.snap",
Some(base_snap),
snap.mem_size(),
snap.num_cpus(),
)?;
Image::from_snapshot(&dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub fn snapshot_diff(
mut self,
dest_dir: impl Into<PathBuf>,
base: &Image,
) -> Result<Image, Error> {
let dest_dir = dest_dir.into();
let base_snap = base
.kvm
.as_ref()
.and_then(|k| k.snapshot.clone())
.ok_or_else(|| {
Error::vm_msg(
"Vm::snapshot_diff: base Image has no KVM snapshot to diff against".to_owned(),
)
})?;
let running = self.running.take().ok_or_else(|| {
Error::vm_msg("Vm::snapshot_diff: this VM is not running (already stopped?)".to_owned())
})?;
std::fs::create_dir_all(&dest_dir).map_err(Error::Io)?;
let _ = guest_sync_via_agent(&self.vsock_exec_path);
let snap = running
.snapshot(Some(&dest_dir.join("volumes")))
.map_err(|e| Error::vm_msg(format!("capture snapshot: {e}")))?;
let snap_path = dest_dir.join("vm.snap");
snap.save_diff(&snap_path, &base_snap).map_err(|e| {
Error::vm_msg(format!("write diff snapshot {}: {e}", snap_path.display()))
})?;
let metadata = serde_json::json!({
"backend": "kvm",
"kvm_snapshot": snap_path.to_string_lossy(),
"kvm_snapshot_base": base_snap.to_string_lossy(),
"memory_mib": (snap.mem_size() >> 20) as u32,
"vcpus": snap.num_cpus() as u32,
"baked_at": chrono_rfc3339_now(),
"source": "Vm::snapshot_diff",
});
std::fs::write(
dest_dir.join("metadata.json"),
serde_json::to_string_pretty(&metadata)
.map_err(|e| Error::vm_msg(format!("metadata serialize: {e}")))?,
)
.map_err(Error::Io)?;
self.cleanup_socket();
self.skip_cleanup = true;
Image::from_snapshot(&dest_dir)
}
fn cleanup_socket(&self) {
let _ = std::fs::remove_file(&self.vsock_mux_path);
let _ = std::fs::remove_file(&self.vsock_exec_path);
if let Some(dir) = &self.own_vsock_mux_dir {
let _ = std::fs::remove_dir(dir);
}
}
}
const B64_ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub fn b64_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
let mut i = 0;
while i + 3 <= bytes.len() {
let b0 = bytes[i] as u32;
let b1 = bytes[i + 1] as u32;
let b2 = bytes[i + 2] as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(B64_ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push(B64_ALPHA[(n & 0x3f) as usize] as char);
i += 3;
}
let rem = bytes.len() - i;
if rem == 1 {
let b0 = bytes[i] as u32;
let n = b0 << 16;
out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push('=');
out.push('=');
} else if rem == 2 {
let b0 = bytes[i] as u32;
let b1 = bytes[i + 1] as u32;
let n = (b0 << 16) | (b1 << 8);
out.push(B64_ALPHA[((n >> 18) & 0x3f) as usize] as char);
out.push(B64_ALPHA[((n >> 12) & 0x3f) as usize] as char);
out.push(B64_ALPHA[((n >> 6) & 0x3f) as usize] as char);
out.push('=');
}
out
}
pub fn b64_decode(s: &str) -> Result<Vec<u8>, String> {
let mut tbl = [255u8; 256];
for (i, &b) in B64_ALPHA.iter().enumerate() {
tbl[b as usize] = i as u8;
}
let bytes: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
if bytes.len() % 4 != 0 {
return Err(format!(
"base64 length {} is not a multiple of 4",
bytes.len()
));
}
let nchunks = bytes.len() / 4;
let mut out = Vec::with_capacity(nchunks * 3);
for (ci, chunk) in bytes.chunks_exact(4).enumerate() {
let v: [u8; 4] = chunk.try_into().unwrap();
let pad = v.iter().filter(|&&b| b == b'=').count();
if pad > 0 {
let last_chunk = ci == nchunks - 1;
let trailing_only = match pad {
1 => v[3] == b'=',
2 => v[2] == b'=' && v[3] == b'=',
_ => false,
};
if !last_chunk || !trailing_only {
return Err(format!("base64 misplaced or excess padding in chunk {ci}"));
}
}
let mut acc: u32 = 0;
for &b in &v {
let d = if b == b'=' {
0
} else {
let d = tbl[b as usize];
if d == 255 {
return Err(format!("invalid base64 character {:#x}", b));
}
d
};
acc = (acc << 6) | (d as u32);
}
out.push(((acc >> 16) & 0xff) as u8);
if pad < 2 {
out.push(((acc >> 8) & 0xff) as u8);
}
if pad < 1 {
out.push((acc & 0xff) as u8);
}
}
Ok(out)
}
#[cfg(any(
all(target_os = "macos", target_arch = "aarch64"),
all(target_os = "linux", target_arch = "x86_64")
))]
impl Drop for Vm {
fn drop(&mut self) {
if self.skip_cleanup {
return;
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
if let Some(pool) = self.pool.take() {
let _ = pool.shutdown();
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
{
self.time_sync_stop.store(true, Ordering::SeqCst);
if let Some(running) = self.running.take() {
let _ = running.stop();
}
if let Some(srv) = self.sentry_exec.take() {
srv.stop();
}
if let Some(pool) = self.sentry_unified_pool.take() {
pool.stop();
}
if let Some(mut runtime) = self.kboxlike.take() {
runtime.cleanup();
}
}
self.cleanup_socket();
}
}
pub struct TcpForwarder {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
bound: SocketAddr,
}
impl TcpForwarder {
pub fn local_addr(&self) -> SocketAddr {
self.bound
}
pub fn stop(mut self) {
self.shutdown();
}
fn shutdown(&mut self) {
self.stop.store(true, Ordering::SeqCst);
let _ = TcpStream::connect_timeout(&self.bound, Duration::from_millis(200));
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
impl Drop for TcpForwarder {
fn drop(&mut self) {
self.shutdown();
}
}
fn accept_loop(listener: TcpListener, vsock_path: PathBuf, stop: Arc<AtomicBool>, guest_port: u16) {
for incoming in listener.incoming() {
if stop.load(Ordering::SeqCst) {
break;
}
let tcp = match incoming {
Ok(s) => s,
Err(_) => continue,
};
#[cfg(target_os = "linux")]
let _ = tcp.set_nodelay(true);
let vsock = vsock_path.clone();
std::thread::Builder::new()
.name("supermachine-tcp-conn".into())
.spawn(move || {
if let Err(e) = splice_tcp_to_unix(tcp, &vsock, guest_port) {
eprintln!("supermachine: tcp forward: {e}");
}
})
.ok();
}
}
fn splice_tcp_to_unix(tcp: TcpStream, vsock_path: &Path, guest_port: u16) -> std::io::Result<()> {
let mut unix = UnixStream::connect(vsock_path)?;
if guest_port != 0 {
let mut hdr = [0u8; 20];
hdr[..16].copy_from_slice(b"SMUX-PORT-V1\0\0\0\0");
hdr[16..].copy_from_slice(&(guest_port as u32).to_be_bytes());
unix.write_all(&hdr)?;
}
let tcp_w = tcp.try_clone()?;
let unix_w = unix.try_clone()?;
let t1 = std::thread::Builder::new()
.name("supermachine-tcp-c2g".into())
.spawn(move || {
let _ = pump(tcp, unix_w);
})?;
let t2 = std::thread::Builder::new()
.name("supermachine-tcp-g2c".into())
.spawn(move || {
let _ = pump(unix, tcp_w);
})?;
let _ = t1.join();
let _ = t2.join();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sentry_accept_loop(
listener: TcpListener,
exec_path: PathBuf,
guest_port: u16,
stop: Arc<AtomicBool>,
) {
for incoming in listener.incoming() {
if stop.load(Ordering::SeqCst) {
break;
}
let tcp = match incoming {
Ok(s) => s,
Err(_) => continue,
};
let _ = tcp.set_nodelay(true);
let exec_path = exec_path.clone();
std::thread::Builder::new()
.name("supermachine-tcp-conn".into())
.spawn(move || {
if let Err(e) = splice_tcp_to_sentry(tcp, &exec_path, guest_port) {
eprintln!("supermachine: sentry tcp forward: {e}");
}
})
.ok();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_accept_loop(
listener: TcpListener,
target: RunscPortForwardTarget,
guest_port: u16,
stop: Arc<AtomicBool>,
) {
for incoming in listener.incoming() {
if stop.load(Ordering::SeqCst) {
break;
}
let tcp = match incoming {
Ok(s) => s,
Err(_) => continue,
};
let _ = tcp.set_nodelay(true);
let target = target.clone();
std::thread::Builder::new()
.name("supermachine-tcp-conn".into())
.spawn(move || {
if let Err(e) = splice_tcp_to_runsc(tcp, &target, guest_port) {
eprintln!("supermachine: runsc tcp forward: {e}");
}
})
.ok();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_accept_loop(
listener: TcpListener,
target: KboxlikePortForwardTarget,
guest_port: u16,
stop: Arc<AtomicBool>,
) {
for incoming in listener.incoming() {
if stop.load(Ordering::SeqCst) {
break;
}
let tcp = match incoming {
Ok(s) => s,
Err(_) => continue,
};
let _ = tcp.set_nodelay(true);
let target = target.clone();
std::thread::Builder::new()
.name("supermachine-tcp-conn".into())
.spawn(move || {
if let Err(e) = splice_tcp_to_kboxlike(tcp, &target, guest_port) {
eprintln!("supermachine: kboxlike tcp forward: {e}");
}
})
.ok();
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_runsc(
client: TcpStream,
target: &RunscPortForwardTarget,
guest_port: u16,
) -> std::io::Result<()> {
let upstream = runsc_connect_port_target(target, guest_port)?;
let client_w = client.try_clone()?;
let upstream_w = upstream.try_clone()?;
let t1 = std::thread::Builder::new()
.name("supermachine-tcp-c2g".into())
.spawn(move || {
let _ = pump(client, upstream_w);
})?;
let t2 = std::thread::Builder::new()
.name("supermachine-tcp-g2c".into())
.spawn(move || {
let _ = pump(upstream, client_w);
})?;
let _ = t1.join();
let _ = t2.join();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_kboxlike(
client: TcpStream,
target: &KboxlikePortForwardTarget,
guest_port: u16,
) -> std::io::Result<()> {
let upstream = kboxlike_connect_tcp_target(target, guest_port)?;
let client_w = client.try_clone()?;
let upstream_w = upstream.try_clone()?;
let t1 = std::thread::Builder::new()
.name("supermachine-tcp-c2g".into())
.spawn(move || {
let _ = pump(client, upstream_w);
})?;
let t2 = std::thread::Builder::new()
.name("supermachine-tcp-g2c".into())
.spawn(move || {
let _ = pump(upstream, client_w);
})?;
let _ = t1.join();
let _ = t2.join();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn splice_tcp_to_sentry(
client: TcpStream,
exec_path: &Path,
guest_port: u16,
) -> std::io::Result<()> {
let upstream = connect_port_sentry(exec_path, guest_port)?;
let client_w = client.try_clone()?;
let upstream_w = upstream.try_clone()?;
let t1 = std::thread::Builder::new()
.name("supermachine-tcp-c2g".into())
.spawn(move || {
let _ = pump(client, upstream_w);
})?;
let t2 = std::thread::Builder::new()
.name("supermachine-tcp-g2c".into())
.spawn(move || {
let _ = pump(upstream, client_w);
})?;
let _ = t1.join();
let _ = t2.join();
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_port_forward_target(runtime: &RunscRuntime) -> RunscPortForwardTarget {
RunscPortForwardTarget {
runsc_bin: runtime.runsc_bin.clone(),
runsc_root: runtime.runsc_root.clone(),
network: runtime.network.clone(),
container_id: runtime.container_id.clone(),
agent_path: runtime.parts.agent_path.clone(),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_port_forward_target(runtime: &KboxlikeRuntime) -> KboxlikePortForwardTarget {
KboxlikePortForwardTarget {
restored_host_pids: runtime.restored_host_pids.clone(),
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_connect_port(runtime: &RunscRuntime, guest_port: u16) -> std::io::Result<UnixStream> {
let port = if guest_port == 0 {
runsc_first_listening_port(runtime)?
} else {
guest_port
};
let target = runsc_port_forward_target(runtime);
runsc_connect_port_target(&target, port)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_port(
runtime: &KboxlikeRuntime,
guest_port: u16,
) -> std::io::Result<UnixStream> {
let target = kboxlike_port_forward_target(runtime);
kboxlike_connect_port_target(&target, guest_port)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_connect_port_target(
target: &RunscPortForwardTarget,
guest_port: u16,
) -> std::io::Result<UnixStream> {
if guest_port == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"runsc bridge requires a non-zero guest port",
));
}
let (client, bridge) = UnixStream::pair()?;
let mut cmd = Command::new(&target.runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut cmd, &target.runsc_root, &target.network);
cmd.arg("exec")
.arg(&target.container_id)
.arg(&target.agent_path)
.arg("--tcp-stdio")
.arg("127.0.0.1")
.arg(guest_port.to_string())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
std::io::Error::new(
e.kind(),
format!(
"runsc exec {} {} --tcp-stdio 127.0.0.1 {}: {e}",
target.container_id, target.agent_path, guest_port
),
)
})?;
let child_stdin = child.stdin.take().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"runsc tcp bridge stdin was not available",
)
})?;
let child_stdout = child.stdout.take().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"runsc tcp bridge stdout was not available",
)
})?;
let startup_deadline = Instant::now() + Duration::from_millis(200);
while Instant::now() < startup_deadline {
if let Some(status) = child.try_wait()? {
let out = child.wait_with_output()?;
let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
let msg = if detail.is_empty() {
format!("runsc tcp bridge to guest port {guest_port} exited with status {status}")
} else {
format!(
"runsc tcp bridge to guest port {guest_port} exited with status {status}: {detail}"
)
};
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
msg,
));
}
std::thread::sleep(Duration::from_millis(10));
}
let bridge_to_child = bridge.try_clone()?;
let child_to_bridge = bridge.try_clone()?;
let to_child = std::thread::Builder::new()
.name("supermachine-runsc-tcp-c2g".into())
.spawn(move || {
let _ = pump(bridge_to_child, child_stdin);
})?;
let from_child = std::thread::Builder::new()
.name("supermachine-runsc-tcp-g2c".into())
.spawn(move || {
let _ = pump(child_stdout, child_to_bridge);
})?;
std::thread::Builder::new()
.name("supermachine-runsc-tcp-bridge".into())
.spawn(move || {
let _ = to_child.join();
let _ = from_child.join();
match child.wait_with_output() {
Ok(out) if !out.status.success() => {
let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
if detail.is_empty() {
eprintln!(
"supermachine: runsc tcp bridge exited with status {}",
out.status
);
} else {
eprintln!(
"supermachine: runsc tcp bridge exited with status {}: {}",
out.status, detail
);
}
}
Ok(_) => {}
Err(e) => eprintln!("supermachine: runsc tcp bridge wait failed: {e}"),
}
drop(bridge);
})?;
Ok(client)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_port_target(
target: &KboxlikePortForwardTarget,
guest_port: u16,
) -> std::io::Result<UnixStream> {
let upstream = kboxlike_connect_tcp_target(target, guest_port)?;
let (client, bridge) = UnixStream::pair()?;
let bridge_to_upstream = bridge.try_clone()?;
let upstream_to_bridge = upstream.try_clone()?;
let to_upstream = std::thread::Builder::new()
.name("supermachine-kboxlike-tcp-c2g".into())
.spawn(move || {
let _ = pump(bridge_to_upstream, upstream);
})?;
let from_upstream = std::thread::Builder::new()
.name("supermachine-kboxlike-tcp-g2c".into())
.spawn(move || {
let _ = pump(upstream_to_bridge, bridge);
})?;
std::thread::Builder::new()
.name("supermachine-kboxlike-tcp-bridge".into())
.spawn(move || {
let _ = to_upstream.join();
let _ = from_upstream.join();
})?;
Ok(client)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_tcp_target(
target: &KboxlikePortForwardTarget,
guest_port: u16,
) -> std::io::Result<TcpStream> {
let port = if guest_port == 0 {
kboxlike_first_listening_port(target)?
} else {
guest_port
};
kboxlike_connect_loopback(port)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_connect_loopback(port: u16) -> std::io::Result<TcpStream> {
let timeout = Duration::from_secs(5);
let v4 = SocketAddr::from(([127, 0, 0, 1], port));
match TcpStream::connect_timeout(&v4, timeout) {
Ok(stream) => {
let _ = stream.set_nodelay(true);
Ok(stream)
}
Err(v4_err) => {
let v6 = SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], port));
match TcpStream::connect_timeout(&v6, timeout) {
Ok(stream) => {
let _ = stream.set_nodelay(true);
Ok(stream)
}
Err(v6_err) => Err(std::io::Error::new(
v6_err.kind(),
format!(
"kboxlike bridge: connect to loopback port {port} failed: IPv4={v4_err}; IPv6={v6_err}"
),
)),
}
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_first_listening_port(runtime: &RunscRuntime) -> std::io::Result<u16> {
let out = runsc_exec_shell(
runtime,
"cat /proc/net/tcp /proc/net/tcp6 2>/dev/null || true",
None,
)?;
if !out.status.success() {
return Err(runsc_io_error("runsc list listening ports", out));
}
parse_first_listening_port(&out.stdout).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"runsc bridge: no listening TCP port found in the guest",
)
})
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_first_listening_port(target: &KboxlikePortForwardTarget) -> std::io::Result<u16> {
let socket_inodes = kboxlike_owned_socket_inodes(&target.restored_host_pids);
if socket_inodes.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"kboxlike bridge: no socket fds owned by the restored workload",
));
}
for pid in &target.restored_host_pids {
let mut proc_net_tcp = Vec::new();
for name in ["tcp", "tcp6"] {
let path = format!("/proc/{pid}/net/{name}");
if let Ok(bytes) = std::fs::read(path) {
proc_net_tcp.extend_from_slice(&bytes);
proc_net_tcp.push(b'\n');
}
}
if let Some(port) = parse_first_owned_listening_port(&proc_net_tcp, &socket_inodes) {
return Ok(port);
}
}
Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"kboxlike bridge: no listening TCP port owned by the restored workload",
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_owned_socket_inodes(pids: &[i32]) -> std::collections::BTreeSet<u64> {
let mut inodes = std::collections::BTreeSet::new();
for pid in pids {
let fd_dir = format!("/proc/{pid}/fd");
let Ok(entries) = std::fs::read_dir(fd_dir) else {
continue;
};
for entry in entries.flatten() {
let Ok(target) = std::fs::read_link(entry.path()) else {
continue;
};
let Some(text) = target.to_str() else {
continue;
};
let Some(inner) = text
.strip_prefix("socket:[")
.and_then(|rest| rest.strip_suffix(']'))
else {
continue;
};
if let Ok(inode) = inner.parse::<u64>() {
inodes.insert(inode);
}
}
}
inodes
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_first_owned_listening_port(
proc_net_tcp: &[u8],
socket_inodes: &std::collections::BTreeSet<u64>,
) -> Option<u16> {
let text = String::from_utf8_lossy(proc_net_tcp);
for line in text.lines() {
let mut fields = line.split_whitespace();
let _slot = fields.next()?;
let local = fields.next()?;
let _remote = fields.next()?;
let state = fields.next()?;
if state != "0A" {
continue;
}
for _ in 0..5 {
fields.next()?;
}
let inode = fields.next()?.parse::<u64>().ok()?;
if !socket_inodes.contains(&inode) {
continue;
}
let Some((_addr, port_hex)) = local.rsplit_once(':') else {
continue;
};
let Ok(port) = u16::from_str_radix(port_hex, 16) else {
continue;
};
if port != 0 {
return Some(port);
}
}
None
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn parse_first_listening_port(proc_net_tcp: &[u8]) -> Option<u16> {
let text = String::from_utf8_lossy(proc_net_tcp);
for line in text.lines() {
let mut fields = line.split_whitespace();
let _slot = fields.next()?;
let local = fields.next()?;
let _remote = fields.next()?;
let state = fields.next()?;
if state != "0A" {
continue;
}
let Some((_addr, port_hex)) = local.rsplit_once(':') else {
continue;
};
let Ok(port) = u16::from_str_radix(port_hex, 16) else {
continue;
};
if port != 0 {
return Some(port);
}
}
None
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn connect_port_sentry(exec_path: &Path, guest_port: u16) -> std::io::Result<UnixStream> {
let mut stream = UnixStream::connect(exec_path)?;
let mut frame = [0u8; 7];
frame[0] = 0xfd; frame[1..5].copy_from_slice(&2u32.to_be_bytes());
frame[5..7].copy_from_slice(&guest_port.to_be_bytes());
stream.write_all(&frame)?;
let mut ack = [0u8; 1];
stream.read_exact(&mut ack)?;
if ack[0] != 1 {
let dst = if guest_port == 0 {
"first listener".to_string()
} else {
format!("guest port {guest_port}")
};
return Err(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
format!("sentry bridge: no listener on {dst}"),
));
}
Ok(stream)
}
fn pump<R, W>(mut r: R, mut w: W) -> std::io::Result<()>
where
R: Read,
W: Write + Shutdownable,
{
let mut buf = [0u8; 16 * 1024];
loop {
let n = match r.read(&mut buf) {
Ok(0) => break,
Ok(n) => n,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
};
w.write_all(&buf[..n])?
}
let _ = w.shutdown_write();
Ok(())
}
trait Shutdownable {
fn shutdown_write(&mut self) -> std::io::Result<()>;
}
impl Shutdownable for TcpStream {
fn shutdown_write(&mut self) -> std::io::Result<()> {
TcpStream::shutdown(self, std::net::Shutdown::Write)
}
}
impl Shutdownable for UnixStream {
fn shutdown_write(&mut self) -> std::io::Result<()> {
UnixStream::shutdown(self, std::net::Shutdown::Write)
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
impl Shutdownable for ChildStdin {
fn shutdown_write(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn unique_suffix() -> u64 {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
nanos.wrapping_add(COUNTER.fetch_add(1, Ordering::Relaxed))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn dir_size_bytes(dir: &Path) -> u64 {
let mut total = 0u64;
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&d) else {
continue;
};
for entry in rd.flatten() {
let Ok(ft) = entry.file_type() else { continue };
if ft.is_dir() {
stack.push(entry.path());
} else if ft.is_file() {
if let Ok(m) = entry.metadata() {
total += m.len();
}
}
}
}
total
}
fn parse_numeric_user(user: &str) -> Option<(u32, u32)> {
let user = user.trim();
if user.is_empty() || user == "root" {
return None;
}
let mut parts = user.split(':');
let uid: u32 = parts.next()?.trim().parse().ok()?;
let gid: u32 = match parts.next() {
Some(g) => g.trim().parse().ok()?,
None => uid,
};
if uid == 0 {
return None;
}
Some((uid, gid))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_bin_path() -> PathBuf {
std::env::var_os("SUPERMACHINE_RUNSC_BIN")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("runsc"))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_warmup_checkpoint(
image: &Image,
dest: &Path,
warmup: Box<dyn FnOnce(&Vm) -> Result<(), Error> + Send>,
) -> Result<(), Error> {
let parts = image
.runsc
.as_ref()
.ok_or_else(|| Error::vm_msg("runsc warmup image missing runsc metadata"))?;
let runsc_bin = runsc_bin_path();
let runtime_dir = std::env::temp_dir().join(format!(
"supermachine-runsc-warmup-{}-{}",
std::process::id(),
unique_suffix()
));
let bundle_dir = runtime_dir.join("bundle");
let runsc_root = runtime_dir.join("runsc-root");
let pid_file = runtime_dir.join("container.pid");
let checkpoint_dir = dest.join("runsc-checkpoint");
let container_id = format!("sm-runsc-warmup-{}-{}", std::process::id(), unique_suffix());
std::fs::create_dir_all(&runsc_root).map_err(Error::Io)?;
let cleanup_dir = |err: Error, runtime_dir: &Path| -> Error {
let _ = std::fs::remove_dir_all(runtime_dir);
err
};
let network = crate::exec::RUNSC_NETWORK_SANDBOX.to_owned();
let build_netns = RunscBuildNetwork::create().map_err(|e| cleanup_dir(e, &runtime_dir))?;
let netns_path = build_netns.path();
write_runsc_bundle_config(
&runsc_bin,
parts,
&bundle_dir,
&parts.rootfs,
&network,
Some(&netns_path),
)
.map_err(|e| cleanup_dir(e, &runtime_dir))?;
runsc_run_detached(
&runsc_bin,
&runsc_root,
&network,
&bundle_dir,
&pid_file,
&container_id,
)
.map_err(|e| cleanup_dir(e, &runtime_dir))?;
let vm = Vm {
running: None,
sentry_exec: None,
sentry_src_rootfs: None,
vsock_mux_path: runtime_dir.join("runsc-unwired.sock"),
vsock_exec_path: runtime_dir.join("runsc-exec-unwired.sock"),
own_vsock_mux_dir: None,
skip_cleanup: false,
time_sync_stop: Arc::new(AtomicBool::new(false)),
_sentry_mem_admission: None,
sentry_warm_pool: None,
sentry_unified_pool: None,
runsc: Some(RunscRuntime {
runsc_bin: runsc_bin.clone(),
runsc_root: runsc_root.clone(),
network: network.clone(),
build_netns: Some(build_netns),
container_id: container_id.clone(),
bundle_dir: bundle_dir.clone(),
rootfs: parts.rootfs.clone(),
parts: parts.clone(),
file_agent_exec: true,
}),
kboxlike: None,
};
let warmup_res = warmup(&vm);
if warmup_res.is_ok() {
let _ = std::fs::remove_dir_all(&checkpoint_dir);
std::fs::create_dir_all(&checkpoint_dir).map_err(Error::Io)?;
runsc_checkpoint(
&runsc_bin,
&runsc_root,
&network,
&checkpoint_dir,
&container_id,
)?;
}
drop(vm);
warmup_res?;
let meta_path = dest.join("metadata.json");
let meta_text = std::fs::read_to_string(&meta_path)
.map_err(|e| Error::bake_msg(format!("read {}: {e}", meta_path.display())))?;
let mut meta: serde_json::Value = serde_json::from_str(&meta_text)
.map_err(|e| Error::bake_msg(format!("parse {}: {e}", meta_path.display())))?;
if let Some(obj) = meta.as_object_mut() {
obj.insert(
"runsc_checkpoint".to_owned(),
serde_json::json!(checkpoint_dir.to_string_lossy()),
);
}
let meta_out = serde_json::to_string_pretty(&meta)
.map_err(|e| Error::bake_msg(format!("runsc metadata serialize: {e}")))?;
std::fs::write(&meta_path, meta_out).map_err(Error::Io)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_runsc_bundle(
runsc_bin: &Path,
parts: &RunscImageParts,
bundle_dir: &Path,
rootfs: &Path,
network: &str,
netns_path: Option<&Path>,
) -> Result<(), Error> {
std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
copy_dir_recursive(&parts.rootfs, rootfs)
.map_err(|e| Error::vm_msg(format!("runsc rootfs clone: {e}")))?;
write_runsc_bundle_config(runsc_bin, parts, bundle_dir, rootfs, network, netns_path)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn prepare_runsc_restore_rootfs(
base_rootfs: &Path,
runtime_dir: &Path,
rootfs: &Path,
) -> Result<(), Error> {
let upper = runtime_dir.join("overlay-upper");
let work = runtime_dir.join("overlay-work");
std::fs::create_dir_all(rootfs).map_err(Error::Io)?;
std::fs::create_dir_all(&upper).map_err(Error::Io)?;
std::fs::create_dir_all(&work).map_err(Error::Io)?;
let opts = format!(
"lowerdir={},upperdir={},workdir={}",
base_rootfs.display(),
upper.display(),
work.display()
);
match Command::new("mount")
.args(["-t", "overlay", "overlay", "-o", &opts])
.arg(rootfs)
.status()
{
Ok(status) if status.success() => Ok(()),
Ok(status) => {
eprintln!(
"supermachine(runsc): overlay rootfs mount failed with status {:?}; \
falling back to full rootfs copy",
status.code()
);
let _ = std::fs::remove_dir_all(rootfs);
let _ = std::fs::remove_dir_all(&upper);
let _ = std::fs::remove_dir_all(&work);
copy_dir_recursive(base_rootfs, rootfs)
.map_err(|e| Error::vm_msg(format!("runsc restore rootfs clone: {e}")))
}
Err(e) => {
eprintln!(
"supermachine(runsc): overlay rootfs mount failed ({e}); \
falling back to full rootfs copy"
);
let _ = std::fs::remove_dir_all(rootfs);
let _ = std::fs::remove_dir_all(&upper);
let _ = std::fs::remove_dir_all(&work);
copy_dir_recursive(base_rootfs, rootfs)
.map_err(|e| Error::vm_msg(format!("runsc restore rootfs clone: {e}")))
}
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_runsc_bundle_config(
runsc_bin: &Path,
parts: &RunscImageParts,
bundle_dir: &Path,
rootfs: &Path,
network: &str,
netns_path: Option<&Path>,
) -> Result<(), Error> {
std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
ensure_runsc_mountpoints(parts, rootfs)?;
ensure_runsc_resolver(rootfs, network)?;
ensure_runsc_file_agent(rootfs)?;
let spec = Command::new(runsc_bin)
.arg("spec")
.current_dir(bundle_dir)
.output()
.map_err(|e| Error::vm_msg(format!("runsc spec: {e}")))?;
if !spec.status.success() {
return Err(Error::vm_msg(format!(
"runsc spec failed with status {:?}: {}",
spec.status.code(),
String::from_utf8_lossy(&spec.stderr)
)));
}
let config_path = bundle_dir.join("config.json");
let config_text = std::fs::read_to_string(&config_path)
.map_err(|e| Error::vm_msg(format!("read runsc config.json: {e}")))?;
let mut spec_json: serde_json::Value = serde_json::from_str(&config_text)
.map_err(|e| Error::vm_msg(format!("parse runsc config.json: {e}")))?;
patch_runsc_spec(&mut spec_json, parts, rootfs, network, netns_path)?;
let spec_out = serde_json::to_string_pretty(&spec_json)
.map_err(|e| Error::vm_msg(format!("serialize runsc config.json: {e}")))?;
std::fs::write(&config_path, spec_out)
.map_err(|e| Error::vm_msg(format!("write runsc config.json: {e}")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_file_agent(rootfs: &Path) -> Result<(), Error> {
let dir = rootfs.join(".supermachine");
std::fs::create_dir_all(dir.join("runsc-exec/requests")).map_err(|e| {
Error::vm_msg(format!(
"create runsc file-agent dir {}: {e}",
dir.display()
))
})?;
let script_path = dir.join("runsc-file-agent.sh");
let script = r#"#!/bin/sh
set +e
DIR=/.supermachine/runsc-exec
mkdir -p "$DIR/requests" /run
if [ -f /.supermachine/run-workload ]; then
/bin/sh /.supermachine/run-workload &
echo $! >/run/supermachine-workload.pid
fi
while :; do
for req in "$DIR"/requests/*; do
[ -d "$req" ] || continue
[ -f "$req/ready" ] || continue
[ ! -f "$req/done" ] || continue
/bin/sh "$req/script" >"$req/stdout" 2>"$req/stderr"
rc=$?
echo "$rc" >"$req/status"
echo 1 >"$req/done"
done
sleep 0.05
done
"#;
std::fs::write(&script_path, script).map_err(|e| {
Error::vm_msg(format!(
"write runsc file-agent {}: {e}",
script_path.display()
))
})?;
let mut perms = std::fs::metadata(&script_path)
.map_err(Error::Io)?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script_path, perms).map_err(Error::Io)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_mountpoints(parts: &RunscImageParts, rootfs: &Path) -> Result<(), Error> {
for mount in &parts.mounts {
let rel = mount.guest_path.trim_start_matches('/');
let target = confined_rootfs_join(rootfs, rel).ok_or_else(|| {
Error::vm_msg(format!(
"runsc mount guest path escapes the rootfs: {:?}",
mount.guest_path
))
})?;
std::fs::create_dir_all(&target).map_err(|e| {
Error::vm_msg(format!("create runsc mountpoint {}: {e}", target.display()))
})?;
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn ensure_runsc_resolver(rootfs: &Path, network: &str) -> Result<(), Error> {
if network != crate::exec::RUNSC_NETWORK_SANDBOX {
return Ok(());
}
let Some(text) = host_resolv_conf_for_runsc() else {
return Ok(());
};
let target = confined_rootfs_join(rootfs, "etc/resolv.conf")
.ok_or_else(|| Error::vm_msg("runsc resolver path escaped rootfs".to_owned()))?;
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent).map_err(Error::Io)?;
}
std::fs::write(&target, text)
.map_err(|e| Error::vm_msg(format!("write runsc resolver {}: {e}", target.display())))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn host_resolv_conf_for_runsc() -> Option<String> {
for path in ["/run/systemd/resolve/resolv.conf", "/etc/resolv.conf"] {
let Ok(text) = std::fs::read_to_string(path) else {
continue;
};
if let Some(text) = sanitize_runsc_resolver(&text) {
return Some(text);
}
}
None
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sanitize_runsc_resolver(text: &str) -> Option<String> {
let nameserver = text.lines().find_map(|line| {
let mut fields = line.split_whitespace();
if !matches!(fields.next(), Some("nameserver")) {
return None;
}
let addr = fields.next()?;
if addr.starts_with("127.") || addr == "::1" || addr.eq_ignore_ascii_case("localhost") {
return None;
}
if addr.contains(':') {
return None;
}
Some(addr.to_owned())
})?;
Some(format!(
"# generated by supermachine for runsc build networking\nnameserver {nameserver}\noptions single-request no-aaaa\n"
))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn copy_runsc_bundle_config(
base_bundle_dir: &Path,
bundle_dir: &Path,
parts: &RunscImageParts,
rootfs: &Path,
network: &str,
netns_path: Option<&Path>,
) -> Result<(), Error> {
std::fs::create_dir_all(bundle_dir).map_err(Error::Io)?;
let config_path = base_bundle_dir.join("config.json");
let config_text = std::fs::read_to_string(&config_path)
.map_err(|e| Error::vm_msg(format!("read base runsc config.json: {e}")))?;
let mut spec_json: serde_json::Value = serde_json::from_str(&config_text)
.map_err(|e| Error::vm_msg(format!("parse base runsc config.json: {e}")))?;
ensure_runsc_mountpoints(parts, rootfs)?;
ensure_runsc_resolver(rootfs, network)?;
patch_runsc_spec(&mut spec_json, parts, rootfs, network, netns_path)?;
let spec_out = serde_json::to_string_pretty(&spec_json)
.map_err(|e| Error::vm_msg(format!("serialize restore runsc config.json: {e}")))?;
std::fs::write(bundle_dir.join("config.json"), spec_out)
.map_err(|e| Error::vm_msg(format!("write restore runsc config.json: {e}")))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_run_detached(
runsc_bin: &Path,
runsc_root: &Path,
network: &str,
bundle_dir: &Path,
pid_file: &Path,
container_id: &str,
) -> Result<(), Error> {
let mut run = Command::new(runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut run, runsc_root, network);
let status = run
.arg("run")
.arg("-detach")
.arg("-bundle")
.arg(bundle_dir)
.arg("-pid-file")
.arg(pid_file)
.arg(container_id)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| Error::vm_msg(format!("runsc run {container_id}: {e}")))?;
if status.success() {
Ok(())
} else {
Err(Error::vm_msg(format!(
"runsc run failed with status {:?}",
status.code()
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_checkpoint(
runsc_bin: &Path,
runsc_root: &Path,
network: &str,
checkpoint_dir: &Path,
container_id: &str,
) -> Result<(), Error> {
runsc_checkpoint_with_options(
runsc_bin,
runsc_root,
network,
checkpoint_dir,
container_id,
false,
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_checkpoint_with_options(
runsc_bin: &Path,
runsc_root: &Path,
network: &str,
checkpoint_dir: &Path,
container_id: &str,
leave_running: bool,
) -> Result<(), Error> {
let mut cmd = Command::new(runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut cmd, runsc_root, network);
cmd.arg("checkpoint").arg("-image-path").arg(checkpoint_dir);
if leave_running {
cmd.arg("-leave-running");
}
let out = cmd
.arg(container_id)
.output()
.map_err(|e| Error::vm_msg(format!("runsc checkpoint {container_id}: {e}")))?;
if out.status.success() {
Ok(())
} else {
Err(Error::vm_msg(format!(
"runsc checkpoint{} failed with status {:?}: {}",
if leave_running { " -leave-running" } else { "" },
out.status.code(),
String::from_utf8_lossy(&out.stderr)
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_restore_detached(
runsc_bin: &Path,
runsc_root: &Path,
network: &str,
bundle_dir: &Path,
checkpoint_dir: &Path,
pid_file: &Path,
container_id: &str,
) -> Result<(), Error> {
let mut cmd = Command::new(runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut cmd, runsc_root, network);
let status = cmd
.arg("restore")
.arg("-detach")
.arg("-bundle")
.arg(bundle_dir)
.arg("-image-path")
.arg(checkpoint_dir)
.arg("-pid-file")
.arg(pid_file)
.arg(container_id)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map_err(|e| Error::vm_msg(format!("runsc restore {container_id}: {e}")))?;
if status.success() {
Ok(())
} else {
Err(Error::vm_msg(format!(
"runsc restore failed with status {:?}",
status.code()
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_delete_force(
runsc_bin: &Path,
runsc_root: &Path,
network: &str,
container_id: &str,
) -> Result<(), Error> {
let mut kill = Command::new(runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut kill, runsc_root, network);
let _ = kill.arg("kill").arg(container_id).arg("KILL").status();
let mut delete = Command::new(runsc_bin);
crate::exec::add_runsc_global_flags_with_network(&mut delete, runsc_root, network);
let status = delete
.arg("delete")
.arg("-force")
.arg(container_id)
.status()
.map_err(|e| Error::vm_msg(format!("runsc delete {container_id}: {e}")))?;
if status.success() {
Ok(())
} else {
Err(Error::vm_msg(format!(
"runsc delete failed with status {:?}",
status.code()
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_exec_shell(
runtime: &RunscRuntime,
script: &str,
stdin_bytes: Option<&[u8]>,
) -> std::io::Result<std::process::Output> {
let mut cmd = Command::new(&runtime.runsc_bin);
crate::exec::add_runsc_global_flags_with_network(
&mut cmd,
&runtime.runsc_root,
&runtime.network,
);
cmd.arg("exec")
.arg(&runtime.container_id)
.arg("/bin/sh")
.arg("-c")
.arg(script)
.stdin(if stdin_bytes.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().map_err(|e| {
std::io::Error::new(
e.kind(),
format!("runsc exec {} for control RPC: {e}", runtime.container_id),
)
})?;
if let Some(bytes) = stdin_bytes {
let mut stdin = child.stdin.take().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"runsc exec stdin was not available",
)
})?;
stdin.write_all(bytes)?;
}
child.wait_with_output()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_io_error(action: &str, out: std::process::Output) -> std::io::Error {
let detail = String::from_utf8_lossy(&out.stderr).trim().to_owned();
std::io::Error::new(
std::io::ErrorKind::Other,
if detail.is_empty() {
format!("{action} failed with status {:?}", out.status.code())
} else {
format!(
"{action} failed with status {:?}: {detail}",
out.status.code()
)
},
)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_write_file(runtime: &RunscRuntime, path: &str, bytes: &[u8]) -> std::io::Result<()> {
let q = sh_quote_runsc(path);
let script = format!(
r#"set -e
p={q}
d=$(dirname -- "$p")
b=$(basename -- "$p")
tmp="$d/.${{b}}.supermachine.$$"
rm -f "$tmp"
cat > "$tmp"
chmod 0644 "$tmp" 2>/dev/null || true
mv -f "$tmp" "$p"
"#
);
let out = runsc_exec_shell(runtime, &script, Some(bytes))?;
if out.status.success() {
Ok(())
} else {
Err(runsc_io_error("runsc write_file", out))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_read_file(runtime: &RunscRuntime, path: &str, max_bytes: u64) -> std::io::Result<Vec<u8>> {
let cap = max_bytes.saturating_add(1);
let q = sh_quote_runsc(path);
let script = format!(
r#"set -e
p={q}
if [ ! -f "$p" ]; then
echo "read_file: not a regular file: $p" >&2
exit 1
fi
size=$(wc -c < "$p" | tr -d ' ')
if [ "$size" -gt "{max_bytes}" ]; then
echo "read_file: file exceeds max_bytes ($size > {max_bytes})" >&2
exit 1
fi
head -c {cap} -- "$p"
"#
);
let out = runsc_exec_shell(runtime, &script, None)?;
if !out.status.success() {
return Err(runsc_io_error("runsc read_file", out));
}
if out.stdout.len() as u64 > max_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"read_file: file exceeds max_bytes ({} > {max_bytes})",
out.stdout.len()
),
));
}
Ok(out.stdout)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_rootfs_join_io(rootfs: &Path, guest_path: &str) -> std::io::Result<PathBuf> {
let rel = guest_path.strip_prefix('/').unwrap_or(guest_path);
let p = Path::new(rel);
if rel.is_empty()
|| p.components().any(|component| {
matches!(
component,
std::path::Component::ParentDir | std::path::Component::RootDir
)
})
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("guest path escapes rootfs: {guest_path}"),
));
}
Ok(rootfs.join(p))
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_write_file(runtime: &KboxlikeRuntime, path: &str, bytes: &[u8]) -> std::io::Result<()> {
let target = kboxlike_rootfs_join_io(&runtime.rootfs, path)?;
let parent = target.parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("write_file path has no parent: {path}"),
)
})?;
std::fs::create_dir_all(parent)?;
let name = target
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("write");
let tmp = parent.join(format!(".supermachine-write-{}-{name}", std::process::id()));
std::fs::write(&tmp, bytes)?;
std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o644))?;
std::fs::rename(&tmp, target)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_read_file(
runtime: &KboxlikeRuntime,
path: &str,
max_bytes: u64,
) -> std::io::Result<Vec<u8>> {
let target = kboxlike_rootfs_join_io(&runtime.rootfs, path)?;
let meta = std::fs::metadata(&target)?;
if !meta.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("read_file: not a regular file: {path}"),
));
}
if meta.len() > max_bytes {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"read_file: file exceeds max_bytes ({} > {max_bytes})",
meta.len()
),
));
}
std::fs::read(target)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_workload_signal(runtime: &RunscRuntime, signum: i32) -> std::io::Result<()> {
let script = format!(
r#"set -e
pid=$(cat /run/supermachine-workload.pid)
kill -s {signum} "$pid"
"#
);
let out = runsc_exec_shell(runtime, &script, None)?;
if out.status.success() {
Ok(())
} else {
Err(runsc_io_error("runsc workload_signal", out))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_workload_signal(runtime: &KboxlikeRuntime, signum: i32) -> std::io::Result<()> {
let Some(pid) = runtime.restored_host_pids.first().copied() else {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"kboxlike workload has no restored host pid",
));
};
let ret = unsafe { libc::kill(pid, signum) };
if ret == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn sh_quote_runsc(s: &str) -> String {
let mut out = String::from("'");
for ch in s.chars() {
if ch == '\'' {
out.push_str("'\\''");
} else {
out.push(ch);
}
}
out.push('\'');
out
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn patch_runsc_spec(
spec: &mut serde_json::Value,
parts: &RunscImageParts,
rootfs: &Path,
network: &str,
netns_path: Option<&Path>,
) -> Result<(), Error> {
let root = spec
.get_mut("root")
.and_then(|v| v.as_object_mut())
.ok_or_else(|| Error::vm_msg("runsc config.json missing root object".to_owned()))?;
root.insert(
"path".to_owned(),
serde_json::json!(rootfs.to_string_lossy().to_string()),
);
root.insert("readonly".to_owned(), serde_json::json!(false));
let process = spec
.get_mut("process")
.and_then(|v| v.as_object_mut())
.ok_or_else(|| Error::vm_msg("runsc config.json missing process object".to_owned()))?;
let mut env: Vec<String> = parts
.image_env
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
if !parts.image_env.iter().any(|(k, _)| k == "PATH") {
env.push("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_owned());
}
process.insert("env".to_owned(), serde_json::json!(env));
process.insert("cwd".to_owned(), serde_json::json!("/"));
process.insert(
"args".to_owned(),
serde_json::json!(["/bin/sh", "/.supermachine/runsc-file-agent.sh"]),
);
if let Some((uid, gid)) = parts.user {
process.insert(
"user".to_owned(),
serde_json::json!({
"uid": uid,
"gid": gid,
}),
);
}
let caps = serde_json::json!([
"CAP_AUDIT_WRITE",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_FOWNER",
"CAP_KILL",
"CAP_NET_BIND_SERVICE",
"CAP_SETGID",
"CAP_SETUID"
]);
process.insert(
"capabilities".to_owned(),
serde_json::json!({
"bounding": caps,
"effective": caps,
"inheritable": caps,
"permitted": caps,
}),
);
process.insert("terminal".to_owned(), serde_json::json!(false));
if network == crate::exec::RUNSC_NETWORK_SANDBOX {
patch_runsc_network_namespace(spec, netns_path)?;
}
if !parts.mounts.is_empty() {
let mounts = spec
.get_mut("mounts")
.and_then(|v| v.as_array_mut())
.ok_or_else(|| Error::vm_msg("runsc config.json missing mounts array".to_owned()))?;
for mount in &parts.mounts {
let opts = if mount.read_only {
vec!["rbind", "ro"]
} else {
vec!["rbind", "rw"]
};
mounts.push(serde_json::json!({
"destination": mount.guest_path,
"type": "bind",
"source": mount.host_path,
"options": opts,
}));
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn patch_runsc_network_namespace(
spec: &mut serde_json::Value,
netns_path: Option<&Path>,
) -> Result<(), Error> {
let Some(namespaces) = spec
.get_mut("linux")
.and_then(|v| v.get_mut("namespaces"))
.and_then(|v| v.as_array_mut())
else {
return Ok(());
};
let Some(netns_path) = netns_path else {
return Ok(());
};
for ns in namespaces {
if ns
.get("type")
.and_then(|v| v.as_str())
.map(|ty| ty == "network")
.unwrap_or(false)
{
ns["path"] = serde_json::json!(netns_path.to_string_lossy().to_string());
return Ok(());
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn copy_dir_recursive(src: &Path, dst: &Path) -> std::io::Result<()> {
use std::collections::HashMap;
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
fn set_owner(path: &Path, uid: u32, gid: u32, follow: bool) -> std::io::Result<()> {
let c = CString::new(path.as_os_str().as_bytes()).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL")
})?;
let r = unsafe {
if follow {
libc::chown(c.as_ptr(), uid as libc::uid_t, gid as libc::gid_t)
} else {
libc::lchown(c.as_ptr(), uid as libc::uid_t, gid as libc::gid_t)
}
};
if r == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
std::fs::create_dir_all(dst)?;
let root_md = std::fs::symlink_metadata(src)?;
let _ = std::fs::set_permissions(dst, root_md.permissions());
set_owner(dst, root_md.uid(), root_md.gid(), true)?;
let mut hardlinks: HashMap<(u64, u64), PathBuf> = HashMap::new();
let mut stack = vec![(src.to_path_buf(), dst.to_path_buf())];
while let Some((s, d)) = stack.pop() {
for entry in std::fs::read_dir(&s)? {
let entry = entry?;
let sp = entry.path();
let dp = d.join(entry.file_name());
let md = std::fs::symlink_metadata(&sp)?;
let ft = md.file_type();
if ft.is_dir() {
std::fs::create_dir_all(&dp)?;
let _ = std::fs::set_permissions(&dp, md.permissions());
set_owner(&dp, md.uid(), md.gid(), true)?;
stack.push((sp, dp));
} else if ft.is_symlink() {
let target = std::fs::read_link(&sp)?;
let _ = std::fs::remove_file(&dp);
std::os::unix::fs::symlink(&target, &dp)?;
set_owner(&dp, md.uid(), md.gid(), false)?;
} else if ft.is_file() {
if md.nlink() > 1 {
let key = (md.dev(), md.ino());
if let Some(first) = hardlinks.get(&key) {
let _ = std::fs::remove_file(&dp);
std::fs::hard_link(first, &dp)?;
set_owner(&dp, md.uid(), md.gid(), true)?;
continue;
}
hardlinks.insert(key, dp.clone());
}
if crate::bake::clonefile_one(&sp, &dp).is_err() {
let _ = std::fs::remove_file(&dp);
std::fs::copy(&sp, &dp)?;
}
set_owner(&dp, md.uid(), md.gid(), true)?;
}
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn commit_staged_snapshot_dir(staging: &Path, dest_dir: &Path) -> Result<(), Error> {
use std::os::unix::ffi::OsStrExt;
let staging_c = std::ffi::CString::new(staging.as_os_str().as_bytes())
.map_err(|_| Error::vm_msg("snapshot: staging path has NUL".to_owned()))?;
let dest_c = std::ffi::CString::new(dest_dir.as_os_str().as_bytes())
.map_err(|_| Error::vm_msg("snapshot: dest path has NUL".to_owned()))?;
const RENAME_EXCHANGE: libc::c_uint = 1 << 1;
let exch = unsafe {
libc::renameat2(
libc::AT_FDCWD,
staging_c.as_ptr(),
libc::AT_FDCWD,
dest_c.as_ptr(),
RENAME_EXCHANGE,
)
};
if exch == 0 {
let _ = std::fs::remove_dir_all(staging);
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOENT) || !dest_dir.exists() {
std::fs::rename(staging, dest_dir).map_err(|e| {
Error::vm_msg(format!(
"snapshot: atomic commit rename {} -> {}: {e}",
staging.display(),
dest_dir.display()
))
})?;
Ok(())
} else {
Err(Error::vm_msg(format!(
"snapshot: gap-free directory exchange {} <-> {} failed: {err}",
staging.display(),
dest_dir.display()
)))
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn runsc_mounts_metadata(mounts: &[crate::vmm::resources::MountSpec]) -> Vec<serde_json::Value> {
mounts
.iter()
.map(|m| {
let mut obj = serde_json::Map::new();
obj.insert("host_path".to_owned(), serde_json::json!(&m.host_path));
obj.insert("guest_tag".to_owned(), serde_json::json!(&m.guest_tag));
obj.insert("guest_path".to_owned(), serde_json::json!(&m.guest_path));
if m.read_only {
obj.insert("read_only".to_owned(), serde_json::json!(true));
}
match m.symlinks {
crate::vmm::resources::SymlinkPolicy::Opaque => {}
crate::vmm::resources::SymlinkPolicy::Deny => {
obj.insert("symlinks".to_owned(), serde_json::json!("deny"));
}
crate::vmm::resources::SymlinkPolicy::Follow => {
obj.insert("symlinks".to_owned(), serde_json::json!("follow"));
}
}
serde_json::Value::Object(obj)
})
.collect()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_runsc_snapshot_dir(
runtime: &RunscRuntime,
dest_dir: &Path,
source: &str,
leave_running: bool,
) -> Result<Image, Error> {
if dest_dir == runtime.rootfs || dest_dir.starts_with(&runtime.rootfs) {
return Err(Error::vm_msg(
"snapshot: dest_dir must be outside the source rootfs".to_owned(),
));
}
let parent = dest_dir
.parent()
.ok_or_else(|| Error::vm_msg("snapshot: dest_dir must have a parent dir".to_owned()))?;
std::fs::create_dir_all(parent).map_err(Error::Io)?;
let staging = parent.join(format!(
".{}.tmp-{}-{}",
dest_dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "snap".to_owned()),
std::process::id(),
unique_suffix(),
));
let _ = std::fs::remove_dir_all(&staging);
let cleanup = |e: Error, staging: &Path| -> Error {
let _ = std::fs::remove_dir_all(staging);
e
};
let staging_rootfs = staging.join("rootfs");
let staging_checkpoint = staging.join("runsc-checkpoint");
let checkpointed = runtime.network != crate::exec::RUNSC_NETWORK_SANDBOX;
if checkpointed {
std::fs::create_dir_all(&staging_checkpoint)
.map_err(|e| cleanup(Error::Io(e), &staging))?;
runsc_checkpoint_with_options(
&runtime.runsc_bin,
&runtime.runsc_root,
&runtime.network,
&staging_checkpoint,
&runtime.container_id,
leave_running,
)
.map_err(|e| cleanup(e, &staging))?;
}
copy_dir_recursive(&runtime.rootfs, &staging_rootfs).map_err(|e| {
cleanup(
Error::vm_msg(format!("snapshot: copy rootfs: {e}")),
&staging,
)
})?;
let mut env_obj = serde_json::Map::new();
for (k, v) in &runtime.parts.image_env {
env_obj.insert(k.clone(), serde_json::json!(v));
}
let user = runtime.parts.user.map(|(uid, gid)| format!("{uid}:{gid}"));
let final_rootfs = dest_dir.join("rootfs");
let final_checkpoint = dest_dir.join("runsc-checkpoint");
let mut metadata = serde_json::json!({
"backend": "runsc",
"runsc_rootfs": final_rootfs.to_string_lossy(),
"runsc_has_workload": runtime.parts.has_workload,
"runsc_agent_path": &runtime.parts.agent_path,
"runsc_agent_unix_env": &runtime.parts.agent_unix_env,
"memory_mib": runtime.parts.memory_mib,
"vcpus": runtime.parts.vcpus,
"image_env": serde_json::Value::Object(env_obj),
"cmd": &runtime.parts.cmd,
"user": user,
"warm_daemon": runtime.parts.warm_daemon,
"mounts": runsc_mounts_metadata(&runtime.parts.mounts),
"baked_by_version": env!("CARGO_PKG_VERSION"),
"baked_at": chrono_rfc3339_now(),
"source": source,
"snapshot_physical_bytes": dir_size_bytes(&staging_rootfs) + if checkpointed { dir_size_bytes(&staging_checkpoint) } else { 0 },
});
if checkpointed {
metadata["runsc_checkpoint"] = serde_json::json!(final_checkpoint.to_string_lossy());
}
let meta_text = serde_json::to_string_pretty(&metadata)
.map_err(|e| cleanup(Error::vm_msg(format!("metadata serialize: {e}")), &staging))?;
std::fs::write(staging.join("metadata.json"), meta_text)
.map_err(|e| cleanup(Error::Io(e), &staging))?;
std::fs::write(staging.join("restore.snap"), b"runsc-checkpoint\n")
.map_err(|e| cleanup(Error::Io(e), &staging))?;
commit_staged_snapshot_dir(&staging, dest_dir).map_err(|e| cleanup(e, &staging))?;
Image::from_snapshot(dest_dir)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn remove_dir_contents(dir: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dir)?;
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let md = std::fs::symlink_metadata(&path)?;
if md.file_type().is_dir() {
std::fs::remove_dir_all(&path)?;
} else {
std::fs::remove_file(&path)?;
}
}
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn replace_dir_contents_from_staging(staging: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
remove_dir_contents(dst)?;
for entry in std::fs::read_dir(staging)? {
let entry = entry?;
std::fs::rename(entry.path(), dst.join(entry.file_name()))?;
}
std::fs::remove_dir_all(staging)?;
Ok(())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_sentry_snapshot_dir(
src_rootfs: &Path,
dest_dir: &Path,
source: &str,
) -> Result<Image, Error> {
write_sentry_snapshot_dir_inner(src_rootfs, dest_dir, source, false, None, None)
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn write_sentry_snapshot_dir_inner(
src_rootfs: &Path,
dest_dir: &Path,
source: &str,
allow_warm_daemon: bool,
memory_mib: Option<u32>,
vcpus: Option<u32>,
) -> Result<Image, Error> {
if dest_dir == src_rootfs || dest_dir.starts_with(src_rootfs) {
return Err(Error::vm_msg(
"snapshot: dest_dir must be outside the source rootfs".to_owned(),
));
}
if !allow_warm_daemon
&& src_rootfs
.parent()
.map(|p| p.join("metadata.json"))
.and_then(|p| std::fs::read(p).ok())
.and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
.and_then(|m| m.get("warm_daemon").and_then(|v| v.as_bool()))
== Some(true)
{
return Err(Error::vm_msg(
"snapshot: source image is a warm-daemon image; cold rootfs capture would \
drop the live warm cell (unsupported)"
.to_owned(),
));
}
let parent = dest_dir
.parent()
.ok_or_else(|| Error::vm_msg("snapshot: dest_dir must have a parent dir".to_owned()))?;
std::fs::create_dir_all(parent).map_err(Error::Io)?;
let staging = parent.join(format!(
".{}.tmp-{}-{}",
dest_dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "snap".to_owned()),
std::process::id(),
unique_suffix(),
));
let _ = std::fs::remove_dir_all(&staging);
let cleanup = |e: Error, staging: &Path| -> Error {
let _ = std::fs::remove_dir_all(staging);
e
};
let staging_rootfs = staging.join("rootfs");
std::fs::create_dir_all(&staging).map_err(|e| cleanup(Error::Io(e), &staging))?;
copy_dir_recursive(src_rootfs, &staging_rootfs).map_err(|e| {
cleanup(
Error::vm_msg(format!("snapshot: copy rootfs: {e}")),
&staging,
)
})?;
let final_rootfs = dest_dir.join("rootfs");
let mut meta = src_rootfs
.parent()
.map(|p| p.join("metadata.json"))
.and_then(|p| std::fs::read(p).ok())
.and_then(|b| serde_json::from_slice::<serde_json::Value>(&b).ok())
.unwrap_or_else(|| serde_json::json!({ "backend": "sentry" }));
if let Some(obj) = meta.as_object_mut() {
obj.insert("backend".to_owned(), serde_json::json!("sentry"));
obj.insert(
"sentry_rootfs".to_owned(),
serde_json::json!(final_rootfs.to_string_lossy()),
);
let inherited_memory_mib = obj
.get("memory_mib")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
let effective_memory_mib = memory_mib.or(inherited_memory_mib).unwrap_or(512);
let inherited_vcpus = obj.get("vcpus").and_then(|v| v.as_u64()).map(|v| v as u32);
let effective_vcpus = vcpus.or(inherited_vcpus).unwrap_or(1).max(1);
obj.insert(
"memory_mib".to_owned(),
serde_json::json!(effective_memory_mib),
);
obj.insert("vcpus".to_owned(), serde_json::json!(effective_vcpus));
obj.insert(
"baked_by_version".to_owned(),
serde_json::json!(env!("CARGO_PKG_VERSION")),
);
obj.insert(
"baked_at".to_owned(),
serde_json::json!(chrono_rfc3339_now()),
);
obj.insert("source".to_owned(), serde_json::json!(source));
obj.insert(
"snapshot_physical_bytes".to_owned(),
serde_json::json!(dir_size_bytes(&staging_rootfs)),
);
}
let meta_text = serde_json::to_string_pretty(&meta)
.map_err(|e| cleanup(Error::vm_msg(format!("metadata serialize: {e}")), &staging))?;
std::fs::write(staging.join("metadata.json"), meta_text)
.map_err(|e| cleanup(Error::Io(e), &staging))?;
{
use crate::sentry::state_snap::{
CpuFeatures, FdTableSnapshot, MemImage, ProcTreeSnapshot, SnapshotEntryKind,
StateSnapshot, CURRENT_VERSION,
};
let state = StateSnapshot {
version: CURRENT_VERSION,
cpu_feature_baseline: CpuFeatures::current(),
entry_kind: SnapshotEntryKind::WarmZygote,
mem: MemImage::default(),
proctree: ProcTreeSnapshot::default(),
netstack: crate::sentry::netstack::LoopSnapshot::default(),
fdtable: FdTableSnapshot::default(),
cwds: Vec::new(),
cmdlines: Vec::new(),
exes: Vec::new(),
};
let mut snap_buf = Vec::new();
state.write_to(&mut snap_buf).map_err(|e| {
cleanup(
Error::vm_msg(format!("snapshot: state container serialize: {e}")),
&staging,
)
})?;
std::fs::write(staging.join("restore.snap"), &snap_buf)
.map_err(|e| cleanup(Error::Io(e), &staging))?;
}
{
use std::os::unix::ffi::OsStrExt;
let staging_c = std::ffi::CString::new(staging.as_os_str().as_bytes()).map_err(|_| {
cleanup(
Error::vm_msg("snapshot: staging path has NUL".to_owned()),
&staging,
)
})?;
let dest_c = std::ffi::CString::new(dest_dir.as_os_str().as_bytes()).map_err(|_| {
cleanup(
Error::vm_msg("snapshot: dest path has NUL".to_owned()),
&staging,
)
})?;
const RENAME_EXCHANGE: libc::c_uint = 1 << 1; let exch = unsafe {
libc::renameat2(
libc::AT_FDCWD,
staging_c.as_ptr(),
libc::AT_FDCWD,
dest_c.as_ptr(),
RENAME_EXCHANGE,
)
};
if exch == 0 {
let _ = std::fs::remove_dir_all(&staging);
} else {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOENT) || !dest_dir.exists() {
std::fs::rename(&staging, dest_dir).map_err(|e| {
cleanup(
Error::vm_msg(format!(
"snapshot: atomic commit rename {} -> {}: {e}",
staging.display(),
dest_dir.display()
)),
&staging,
)
})?;
} else {
return Err(cleanup(
Error::vm_msg(format!(
"snapshot: gap-free directory exchange {} <-> {} failed: {err}",
staging.display(),
dest_dir.display()
)),
&staging,
));
}
}
}
Image::from_snapshot(dest_dir)
}
fn chrono_rfc3339_now() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let days = secs.div_euclid(86_400);
let sod = secs.rem_euclid(86_400);
let hh = sod / 3600;
let mm = (sod % 3600) / 60;
let ss = sod % 60;
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!("{y:04}-{m:02}-{d:02}T{hh:02}:{mm:02}:{ss:02}Z")
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn set_executable(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(path, perms)
}
#[cfg(test)]
mod version_skew_tests {
use super::Image;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use super::{Error, SentryBakeConfig, VmConfig};
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
use std::io::{Read as _, Write as _};
fn snap_dir(meta: serde_json::Value) -> std::path::PathBuf {
static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"sm-verskew-{}-{}",
std::process::id(),
SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("metadata.json"),
serde_json::to_string_pretty(&meta).unwrap(),
)
.unwrap();
dir
}
#[test]
fn from_snapshot_refuses_version_skewed_snapshot() {
let dir = snap_dir(serde_json::json!({
"backend": "kvm",
"kvm_snapshot": "vm.snap",
"memory_mib": 256,
"vcpus": 1,
"baked_by_version": "0.0.1",
}));
let err = Image::from_snapshot(&dir).expect_err("must refuse version skew");
let msg = format!("{err:?}");
assert!(
msg.contains("0.0.1") && msg.contains("Re-bake"),
"error must name the baked version and tell the user to rebake: {msg}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn from_snapshot_refuses_version_skewed_sentry_snapshot() {
let dir = snap_dir(serde_json::json!({
"backend": "sentry",
"sentry_rootfs": "rootfs",
"memory_mib": 256,
"baked_by_version": "0.0.1",
}));
let err = Image::from_snapshot(&dir).expect_err("must refuse sentry version skew");
let msg = format!("{err:?}");
assert!(
msg.contains("0.0.1") && msg.contains("Re-bake"),
"sentry skew error must name the baked version and tell the user to rebake: {msg}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn from_snapshot_allows_current_version_and_indeterminate() {
for meta in [
serde_json::json!({
"backend": "kvm",
"kvm_snapshot": "vm.snap",
"baked_by_version": env!("CARGO_PKG_VERSION"),
}),
serde_json::json!({
"backend": "kvm",
"kvm_snapshot": "vm.snap",
}),
] {
let dir = snap_dir(meta);
match Image::from_snapshot(&dir) {
Ok(_) => {}
Err(e) => {
let msg = format!("{e:?}");
assert!(
!msg.contains("Re-bake"),
"version gate must not fire here: {msg}"
);
}
}
let _ = std::fs::remove_dir_all(&dir);
}
}
#[test]
fn from_snapshot_loads_runsc_rootfs_metadata() {
let dir = snap_dir(serde_json::json!({
"backend": "runsc",
"runsc_rootfs": "rootfs",
"runsc_has_workload": true,
"memory_mib": 384,
"vcpus": 2,
"image_env": {"PATH": "/usr/bin"},
"cmd": ["/bin/sh", "-lc", "echo hi"],
"runsc_agent_path": "/.supermachine/supermachine-agent",
"runsc_agent_unix_env": "SUPERMACHINE_AGENT_UNIX_SOCKET",
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
std::fs::create_dir_all(dir.join("rootfs")).unwrap();
let image = Image::from_snapshot(&dir).expect("runsc snapshot loads");
assert!(image.is_runsc());
assert!(!image.is_sentry());
assert_eq!(image.memory_mib, 384);
assert_eq!(image.vcpus, 2);
let parts = image.runsc.as_ref().expect("runsc parts");
assert!(parts.has_workload);
assert_eq!(parts.cmd, ["/bin/sh", "-lc", "echo hi"]);
assert_eq!(parts.agent_path, "/.supermachine/supermachine-agent");
assert_eq!(parts.agent_unix_env, "SUPERMACHINE_AGENT_UNIX_SOCKET");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn from_snapshot_loads_kboxlike_rootfs_metadata() {
let volume_host = std::env::temp_dir().join(format!(
"sm-kboxlike-metadata-volume-{}",
std::process::id()
));
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "rootfs",
"kboxlike_has_workload": true,
"memory_mib": 512,
"vcpus": 3,
"user": "1000:1001",
"image_env": {"PATH": "/usr/local/bin"},
"cmd": ["/bin/sh", "-lc", "echo kboxlike"],
"volumes": [{"host_file": volume_host.to_string_lossy(), "guest_path": "/var/lib/data", "size_bytes": 12345}],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
std::fs::create_dir_all(dir.join("rootfs")).unwrap();
let image = Image::from_snapshot(&dir).expect("kboxlike snapshot loads");
assert!(image.is_kboxlike());
assert!(!image.is_runsc());
assert!(!image.is_sentry());
assert_eq!(image.memory_mib, 512);
assert_eq!(image.vcpus, 3);
let parts = image.kboxlike.as_ref().expect("kboxlike parts");
assert!(parts.has_workload);
assert_eq!(parts.user, Some((1000, 1001)));
assert_eq!(
parts.image_env,
[("PATH".to_string(), "/usr/local/bin".to_string())]
);
assert_eq!(parts.cmd, ["/bin/sh", "-lc", "echo kboxlike"]);
assert_eq!(parts.volumes.len(), 1);
assert_eq!(parts.volumes[0].0, volume_host);
assert_eq!(parts.volumes[0].1, "/var/lib/data");
assert_eq!(parts.volumes[0].2, 12345);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
fn kboxlike_vm_start_restores_host_root_sleep_smoke() {
let workload_uid_path = format!(
"/tmp/sm-kboxlike-workload-user-{}-{}",
std::process::id(),
1
);
let host_mount = std::env::temp_dir().join(format!(
"sm-kboxlike-host-mount-{}-{}",
std::process::id(),
1
));
std::fs::create_dir_all(&host_mount).unwrap();
std::fs::write(host_mount.join("input.txt"), b"kboxlike-mounted\n").unwrap();
let guest_mount = format!("/tmp/sm-kboxlike-guest-mount-{}-{}", std::process::id(), 1);
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "/",
"kboxlike_has_workload": false,
"memory_mib": 64,
"vcpus": 1,
"user": "65534:65534",
"image_env": {"PATH": "/usr/bin:/bin"},
"cmd": ["/bin/sh", "-lc", format!("id -u > {workload_uid_path}; id -g >> {workload_uid_path}; sleep 30")],
"mounts": [{"host_path": host_mount.to_string_lossy(), "guest_tag": "mnt", "guest_path": guest_mount}],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
let image = Image::from_snapshot(&dir).expect("kboxlike host-root snapshot loads");
let vm = image
.start(&super::VmConfig::new())
.expect("kboxlike vm starts");
let mut workload_user = Vec::new();
for _ in 0..20 {
if let Ok(bytes) = vm.read_file(&workload_uid_path) {
workload_user = bytes;
if String::from_utf8_lossy(&workload_user).contains("65534\n65534") {
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert_eq!(
String::from_utf8_lossy(&workload_user),
"65534\n65534\n",
"kboxlike workload did not run as metadata user"
);
let path = format!("/tmp/sm-kboxlike-file-{}-{}", std::process::id(), 1);
vm.write_file(&path, b"kboxlike-file\n")
.expect("kboxlike write_file");
assert_eq!(
vm.read_file(&path).expect("kboxlike read_file"),
b"kboxlike-file\n"
);
let staged = format!("/tmp/sm-kboxlike-stage-{}-{}", std::process::id(), 1);
let out = vm
.exec_builder()
.argv([
"/bin/sh",
"-lc",
"id -u; id -g; pwd; cat \"$SM_FILE\"; cat \"$SM_STAGE\"; cat \"$SM_MOUNT/input.txt\"",
])
.cwd("/tmp")
.env("SM_FILE", &path)
.env("SM_STAGE", &staged)
.env("SM_MOUNT", &guest_mount)
.stage_file(&staged, b"staged\n")
.output()
.expect("kboxlike exec output");
assert!(out.success(), "kboxlike exec failed: {out:?}");
assert_eq!(
out.stdout,
b"65534\n65534\n/tmp\nkboxlike-file\nstaged\nkboxlike-mounted\n"
);
let mut child = vm
.exec_builder()
.argv([
"/bin/sh",
"-lc",
"cat; printf 'kboxlike-stream-err\\n' >&2; exit 7",
])
.spawn()
.expect("kboxlike streaming exec spawn");
let mut stdin = child.stdin().expect("kboxlike streaming stdin");
stdin
.write_all(b"kboxlike-stream-out\n")
.expect("kboxlike streaming stdin write");
stdin.close().expect("kboxlike streaming stdin close");
let mut stream_stdout = String::new();
child
.stdout()
.expect("kboxlike streaming stdout")
.read_to_string(&mut stream_stdout)
.expect("kboxlike streaming stdout read");
let mut stream_stderr = String::new();
child
.stderr()
.expect("kboxlike streaming stderr")
.read_to_string(&mut stream_stderr)
.expect("kboxlike streaming stderr read");
let status = child.wait().expect("kboxlike streaming wait");
assert_eq!(status.code(), Some(7));
assert_eq!(stream_stdout, "kboxlike-stream-out\n");
assert_eq!(stream_stderr, "kboxlike-stream-err\n");
vm.workload_signal(libc::SIGTERM)
.expect("kboxlike workload_signal");
vm.stop().expect("kboxlike vm stops");
let pool = image
.pool()
.min(1)
.max(1)
.restore_on_release(false)
.build()
.expect("kboxlike pool builds");
let pooled_path = format!("/tmp/sm-kboxlike-pooled-{}-{}", std::process::id(), 1);
{
let pooled = pool.acquire().expect("kboxlike pool acquire");
let stats = pool.stats();
assert_eq!(stats.alive, 1);
assert_eq!(stats.in_use, 1);
assert_eq!(stats.max, 1);
let out = pooled
.exec_builder()
.argv(["/bin/sh", "-lc", "id -u; id -g; echo pooled > \"$P\""])
.env("P", &pooled_path)
.output()
.expect("kboxlike pooled exec output");
assert!(out.success(), "kboxlike pooled exec failed: {out:?}");
assert_eq!(out.stdout, b"65534\n65534\n");
}
let stats = pool.stats();
assert_eq!(stats.alive, 1);
assert_eq!(stats.idle, 1);
assert_eq!(
std::fs::read_to_string(&pooled_path).unwrap_or_default(),
"pooled\n"
);
pool.shutdown();
let _ = std::fs::remove_file(path);
let _ = std::fs::remove_file(staged);
let _ = std::fs::remove_file(pooled_path);
let _ = std::fs::remove_dir_all(host_mount);
let _ = std::fs::remove_dir_all(guest_mount);
let _ = std::fs::remove_file(workload_uid_path);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
fn kboxlike_vm_start_honors_runtime_virtiofs_mount() {
let host_mount = std::env::temp_dir().join(format!(
"sm-kboxlike-runtime-mount-{}-{}",
std::process::id(),
1
));
std::fs::create_dir_all(&host_mount).unwrap();
std::fs::write(host_mount.join("input.txt"), b"runtime-mounted\n").unwrap();
let guest_mount = format!(
"/tmp/sm-kboxlike-runtime-guest-mount-{}-{}",
std::process::id(),
1
);
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "/",
"kboxlike_has_workload": false,
"memory_mib": 64,
"vcpus": 1,
"image_env": {"PATH": "/usr/bin:/bin"},
"cmd": ["/bin/sh", "-lc", "sleep 30"],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
let image = Image::from_snapshot(&dir).expect("kboxlike runtime-mount snapshot loads");
let cfg = VmConfig::new().with_virtiofs(crate::vmm::resources::MountSpec::new(
host_mount.to_string_lossy().to_string(),
"runtime",
guest_mount.clone(),
));
let vm = image
.start(&cfg)
.expect("kboxlike vm starts with runtime mount");
let out = vm
.exec_builder()
.argv(["/bin/sh", "-lc", "cat \"$SM_MOUNT/input.txt\""])
.env("SM_MOUNT", &guest_mount)
.timeout(std::time::Duration::from_secs(10))
.output()
.expect("kboxlike exec reads runtime mount");
assert!(out.success(), "kboxlike runtime mount exec failed: {out:?}");
assert_eq!(out.stdout, b"runtime-mounted\n");
vm.stop().expect("kboxlike runtime-mount vm stops");
let _ = std::fs::remove_dir_all(host_mount);
let _ = std::fs::remove_dir_all(guest_mount);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions and live ptrace restore"]
fn kboxlike_vm_start_honors_runtime_volume_mount() {
let host_volume = std::env::temp_dir().join(format!(
"sm-kboxlike-runtime-volume-{}-{}",
std::process::id(),
1
));
let guest_volume = format!(
"/tmp/sm-kboxlike-runtime-guest-volume-{}-{}",
std::process::id(),
1
);
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "/",
"kboxlike_has_workload": false,
"memory_mib": 64,
"vcpus": 1,
"image_env": {"PATH": "/usr/bin:/bin"},
"cmd": ["/bin/sh", "-lc", "sleep 30"],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
let image = Image::from_snapshot(&dir).expect("kboxlike runtime-volume snapshot loads");
let cfg = VmConfig::new().with_volume(crate::vmm::resources::VolumeSpec::new(
host_volume.to_string_lossy().to_string(),
guest_volume.clone(),
));
let vm = image
.start(&cfg)
.expect("kboxlike vm starts with runtime volume");
let out = vm
.exec_builder()
.argv([
"/bin/sh",
"-lc",
"echo runtime-volume > \"$SM_VOLUME/marker\" && cat \"$SM_VOLUME/marker\"",
])
.env("SM_VOLUME", &guest_volume)
.timeout(std::time::Duration::from_secs(10))
.output()
.expect("kboxlike exec writes runtime volume");
assert!(
out.success(),
"kboxlike runtime volume exec failed: {out:?}"
);
assert_eq!(out.stdout, b"runtime-volume\n");
assert_eq!(
std::fs::read_to_string(host_volume.join("marker")).unwrap_or_default(),
"runtime-volume\n"
);
vm.stop().expect("kboxlike runtime-volume vm stops");
let _ = std::fs::remove_dir_all(host_volume);
let _ = std::fs::remove_dir_all(guest_volume);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions, OCI pull, and live ptrace restore"]
fn kboxlike_bake_warmup_mount_exec_smoke() {
let host_mount = std::env::temp_dir().join(format!(
"sm-kboxlike-bake-host-mount-{}-{}",
std::process::id(),
1
));
std::fs::create_dir_all(&host_mount).unwrap();
std::fs::write(host_mount.join("input.txt"), b"mounted-from-host\n").unwrap();
let dir = std::env::temp_dir().join(format!(
"sm-kboxlike-bake-snap-{}-{}",
std::process::id(),
1
));
let _ = std::fs::remove_dir_all(&dir);
let mut cfg = SentryBakeConfig {
memory_mib: Some(128),
vcpus: Some(1),
cmd: Some(vec![
"/bin/sh".to_owned(),
"-lc".to_owned(),
"sleep 30".to_owned(),
]),
warmup_tag: Some("kboxlike-bake-warmup-mount-smoke-v1".to_owned()),
..Default::default()
};
cfg.mounts.push((
host_mount.clone(),
"workspace".to_owned(),
crate::vmm::resources::SymlinkPolicy::Opaque,
"/workspace".to_owned(),
));
cfg.warmup = Some(Box::new(|vm| {
let out = vm
.exec_builder()
.argv([
"/bin/sh",
"-lc",
"cat /workspace/input.txt > /warmup-mounted.txt && echo warmed > /warmup-created.txt",
])
.timeout(std::time::Duration::from_secs(30))
.output()?;
if !out.success() {
return Err(Error::vm_msg(format!("warmup failed: {out:?}")));
}
Ok(())
}));
let image = Image::bake_kboxlike_with_config("alpine:3.20", &dir, cfg)
.expect("kboxlike bake with warmup and mount");
assert!(image.is_kboxlike());
let vm = image
.start(&VmConfig::new())
.expect("kboxlike baked vm starts");
let out = vm
.exec_builder()
.argv([
"/bin/sh",
"-lc",
"cat /warmup-mounted.txt; cat /warmup-created.txt; cat /workspace/input.txt",
])
.timeout(std::time::Duration::from_secs(30))
.output()
.expect("kboxlike baked exec");
assert!(out.success(), "kboxlike baked exec failed: {out:?}");
assert_eq!(
out.stdout,
b"mounted-from-host\nwarmed\nmounted-from-host\n"
);
vm.stop().expect("kboxlike baked vm stops");
let _ = std::fs::remove_dir_all(host_mount);
let _ = std::fs::remove_dir_all(&dir);
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions, live ptrace restore, and python3"]
fn kboxlike_vm_start_exposes_restored_tcp_listener_smoke() {
let python = if std::path::Path::new("/usr/bin/python3").is_file() {
"/usr/bin/python3"
} else {
"/bin/python3"
};
assert!(
std::path::Path::new(python).is_file(),
"python3 binary not found at /usr/bin/python3 or /bin/python3"
);
let port = kboxlike_free_host_port();
let server_code = format!(
r#"import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", {port}))
s.listen(16)
body = b"kboxlike-http\n"
resp = b"HTTP/1.0 200 OK\r\nContent-Length: " + str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body
while True:
conn, _addr = s.accept()
try:
conn.recv(4096)
conn.sendall(resp)
finally:
conn.close()
"#
);
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "/",
"kboxlike_has_workload": false,
"memory_mib": 128,
"vcpus": 1,
"image_env": {"PATH": "/usr/bin:/bin"},
"cmd": [
python,
"-u",
"-c",
server_code
],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
let cert = dir.join("cert.pem");
let key = dir.join("key.pem");
let openssl_ok = std::process::Command::new("openssl")
.args([
"req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1",
])
.arg("-keyout")
.arg(&key)
.arg("-out")
.arg(&cert)
.args(["-subj", "/CN=localhost"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
let curl_ok = std::process::Command::new("curl")
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
let result = (|| {
let image = Image::from_snapshot(&dir).expect("kboxlike HTTP snapshot loads");
let vm = image
.start(&super::VmConfig::new())
.expect("kboxlike HTTP vm starts");
let direct_addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
let direct = kboxlike_http_get_tcp_until(direct_addr, "/payload.txt")
.expect("direct TCP reaches restored HTTP listener");
eprintln!("KBOXLIVE_VM_START_HTTP_DIRECT ok=true");
assert!(
direct.contains("kboxlike-http"),
"unexpected direct TCP response: {direct:?}"
);
let explicit = kboxlike_http_get_unix_until(|| vm.connect_port(port), "/payload.txt")
.expect("connect_port reaches restored HTTP listener");
eprintln!("KBOXLIVE_VM_START_HTTP_CONNECT_PORT ok=true");
assert!(
explicit.contains("kboxlike-http"),
"unexpected connect_port response: {explicit:?}"
);
let first = kboxlike_http_get_unix_until(|| vm.connect(), "/payload.txt")
.expect("connect reaches first restored HTTP listener");
eprintln!("KBOXLIVE_VM_START_HTTP_CONNECT ok=true");
assert!(
first.contains("kboxlike-http"),
"unexpected connect response: {first:?}"
);
let fwd = vm.expose_tcp(0, port).expect("kboxlike expose_tcp starts");
let exposed = kboxlike_http_get_tcp_until(fwd.local_addr(), "/payload.txt")
.expect("expose_tcp reaches restored HTTP listener");
eprintln!("KBOXLIVE_VM_START_HTTP_EXPOSE_TCP ok=true");
assert!(
exposed.contains("kboxlike-http"),
"unexpected expose_tcp response: {exposed:?}"
);
drop(fwd);
if openssl_ok && curl_ok {
let addr = vm
.expose_tls(crate::vmm::tls::TlsConfig {
listen_addr: "127.0.0.1:0".into(),
vm_port: Some(port as u32),
cert_path: cert.to_string_lossy().into_owned(),
key_path: key.to_string_lossy().into_owned(),
})
.expect("kboxlike expose_tls starts");
let mut tls_body = String::new();
for _ in 0..20 {
tls_body = std::process::Command::new("curl")
.args(["-sk", "--max-time", "8"])
.arg(format!("https://{addr}/payload.txt"))
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
.unwrap_or_default();
if tls_body.contains("kboxlike-http") {
break;
}
std::thread::sleep(std::time::Duration::from_millis(300));
}
eprintln!("KBOXLIVE_VM_START_HTTP_EXPOSE_TLS ok=true");
assert!(
tls_body.contains("kboxlike-http"),
"unexpected expose_tls response: {tls_body:?}"
);
} else {
eprintln!(
"KBOXLIVE_VM_START_HTTP_EXPOSE_TLS skipped openssl_ok={openssl_ok} curl_ok={curl_ok}"
);
}
vm.stop().expect("kboxlike HTTP vm stops");
})();
let _ = std::fs::remove_dir_all(&dir);
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[test]
#[ignore = "requires Linux root/chroot permissions, live ptrace restore, and Chromium"]
fn kboxlike_vm_start_restores_host_root_chromium_devtools_smoke() {
let chrome = std::env::var("SUPERMACHINE_KBOXLIVE_CHROME").unwrap_or_else(|_| {
"/root/.cache/ms-playwright/chromium-1217/chrome-linux64/chrome".to_owned()
});
assert!(
std::path::Path::new(&chrome).is_file(),
"Chromium binary not found at {chrome}; set SUPERMACHINE_KBOXLIVE_CHROME"
);
let profile_dir = std::env::temp_dir().join(format!(
"kboxlike-vm-chromium-profile-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&profile_dir).unwrap();
let user_data_dir = format!("--user-data-dir={}", profile_dir.display());
let image_env = std::env::vars()
.map(|(key, value)| (key, serde_json::json!(value)))
.collect::<serde_json::Map<_, _>>();
let dir = snap_dir(serde_json::json!({
"backend": "kboxlike",
"kboxlike_rootfs": "/",
"kboxlike_has_workload": false,
"memory_mib": 512,
"vcpus": 2,
"image_env": image_env,
"cmd": [
chrome,
"--headless=new",
"--disable-gpu",
"--disable-dev-shm-usage",
"--no-sandbox",
"--remote-debugging-port=0",
user_data_dir,
"about:blank"
],
"warm_daemon": false,
"baked_by_version": env!("CARGO_PKG_VERSION"),
}));
let result = (|| {
let image = Image::from_snapshot(&dir).expect("kboxlike Chromium snapshot loads");
let vm = image
.start(&super::VmConfig::new())
.expect("kboxlike Chromium vm starts");
let port = kboxlike_read_devtools_active_port_until(
&profile_dir,
std::time::Duration::from_secs(3),
)
.expect("restored Chromium DevTools active port");
let status =
kboxlike_probe_devtools_version_until(port, std::time::Duration::from_secs(3))
.expect("restored Chromium DevTools /json/version response");
eprintln!("KBOXLIVE_VM_START_CHROMIUM_DEVTOOLS port={port} ok=true {status}");
vm.stop().expect("kboxlike Chromium vm stops");
})();
let _ = std::fs::remove_dir_all(&profile_dir);
let _ = std::fs::remove_dir_all(&dir);
result
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_free_host_port() -> u16 {
std::net::TcpListener::bind(("127.0.0.1", 0))
.unwrap()
.local_addr()
.unwrap()
.port()
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_http_get_unix_until<F>(mut connect: F, path: &str) -> std::io::Result<String>
where
F: FnMut() -> std::io::Result<std::os::unix::net::UnixStream>,
{
use std::io::Write as _;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
match connect() {
Ok(mut stream) => {
stream.set_read_timeout(Some(std::time::Duration::from_secs(2)))?;
stream.write_all(
format!("GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").as_bytes(),
)?;
let _ = stream.shutdown(std::net::Shutdown::Write);
let response = kboxlike_read_http_response_chunks(&mut stream)?;
if response.contains("200 OK") && response.contains("kboxlike-http") {
return Ok(response);
}
if std::time::Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("HTTP response was not 200: {response:?}"),
));
}
}
Err(err) if std::time::Instant::now() >= deadline => return Err(err),
Err(_) => {}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_http_get_tcp_until(
addr: std::net::SocketAddr,
path: &str,
) -> std::io::Result<String> {
use std::io::Write as _;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
match std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200))
{
Ok(mut stream) => {
stream.set_read_timeout(Some(std::time::Duration::from_secs(2)))?;
stream.write_all(
format!("GET {path} HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n").as_bytes(),
)?;
let _ = stream.shutdown(std::net::Shutdown::Write);
let response = kboxlike_read_http_response_chunks(&mut stream)?;
if response.contains("200 OK") && response.contains("kboxlike-http") {
return Ok(response);
}
if std::time::Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("HTTP response was not 200: {response:?}"),
));
}
}
Err(err) if std::time::Instant::now() >= deadline => return Err(err),
Err(_) => {}
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_read_http_response_chunks<R: std::io::Read>(
reader: &mut R,
) -> std::io::Result<String> {
let mut response = Vec::new();
let mut chunk = [0u8; 1024];
loop {
match reader.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
response.extend_from_slice(&chunk[..n]);
if let Ok(text) = std::str::from_utf8(&response) {
if text.contains("200 OK") && text.contains("kboxlike-http") {
return Ok(text.to_owned());
}
}
}
Err(err)
if matches!(
err.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
break;
}
Err(err) => return Err(err),
}
}
Ok(String::from_utf8_lossy(&response).into_owned())
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_read_devtools_active_port_until(
profile_dir: &std::path::Path,
timeout: std::time::Duration,
) -> std::io::Result<u16> {
let path = profile_dir.join("DevToolsActivePort");
let deadline = std::time::Instant::now() + timeout;
loop {
match std::fs::read_to_string(&path) {
Ok(text) => {
if let Some(first) = text.lines().next() {
if let Ok(port) = first.trim().parse::<u16>() {
return Ok(port);
}
}
}
Err(err)
if err.kind() != std::io::ErrorKind::NotFound
&& std::time::Instant::now() >= deadline =>
{
return Err(err);
}
Err(_) => {}
}
if std::time::Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("{} did not appear", path.display()),
));
}
std::thread::sleep(std::time::Duration::from_millis(25));
}
}
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
fn kboxlike_probe_devtools_version_until(
port: u16,
timeout: std::time::Duration,
) -> std::io::Result<String> {
use std::io::{Read as _, Write as _};
let deadline = std::time::Instant::now() + timeout;
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
loop {
match std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200))
{
Ok(mut stream) => {
stream.set_read_timeout(Some(std::time::Duration::from_millis(500)))?;
stream.write_all(b"GET /json/version HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n")?;
let mut response = Vec::new();
let mut chunk = [0u8; 1024];
loop {
match stream.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
response.extend_from_slice(&chunk[..n]);
if let Ok(text) = std::str::from_utf8(&response) {
if let Some(status) = text.lines().next() {
if status.contains("200") {
return Ok(status.to_owned());
}
}
}
}
Err(err)
if matches!(
err.kind(),
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
) =>
{
break;
}
Err(err) => return Err(err),
}
}
if let Ok(text) = std::str::from_utf8(&response) {
if let Some(status) = text.lines().next() {
if status.contains("200") {
return Ok(status.to_owned());
}
}
}
}
Err(err) if std::time::Instant::now() >= deadline => return Err(err),
Err(_) => {}
}
if std::time::Instant::now() >= deadline {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("DevTools on 127.0.0.1:{port} did not answer"),
));
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
}
#[cfg(test)]
mod oci_image_builder_mount_tests {
use super::OciImageBuilder;
use crate::vmm::resources::SymlinkPolicy;
use std::path::PathBuf;
#[test]
fn with_mount_stores_all_four_fields() {
let b = OciImageBuilder::new("alpine").with_mount("/host/x", "tag", "/workspace");
assert_eq!(b.mounts.len(), 1);
let (h, t, p, gp) = &b.mounts[0];
assert_eq!(h, &PathBuf::from("/host/x"));
assert_eq!(t, "tag");
assert_eq!(p, &SymlinkPolicy::Opaque);
assert_eq!(gp, "/workspace");
}
#[test]
fn with_mount_symlinks_combines_policy_and_guest_path() {
let b = OciImageBuilder::new("alpine").with_mount_symlinks(
"/host/x",
"ws",
"/workspace",
SymlinkPolicy::Follow,
);
let (h, t, p, gp) = &b.mounts[0];
assert_eq!(h, &PathBuf::from("/host/x"));
assert_eq!(t, "ws");
assert_eq!(p, &SymlinkPolicy::Follow);
assert_eq!(gp, "/workspace");
}
#[test]
fn with_mount_symlinks_deny_policy_round_trips() {
let b = OciImageBuilder::new("alpine").with_mount_symlinks(
"/h",
"t",
"/g",
SymlinkPolicy::Deny,
);
assert_eq!(b.mounts[0].2, SymlinkPolicy::Deny);
}
#[test]
fn multiple_mounts_compose_independently() {
let b = OciImageBuilder::new("alpine")
.with_mount("/host/src", "workspace", "/workspace")
.with_mount("/host/cache", "ro-cache", "/cache")
.with_mount_symlinks("/host/strict", "strict", "/strict", SymlinkPolicy::Deny);
assert_eq!(b.mounts.len(), 3);
assert_eq!(b.mounts[0].3, "/workspace");
assert_eq!(b.mounts[1].3, "/cache");
assert_eq!(b.mounts[2].3, "/strict");
assert_eq!(b.mounts[2].2, SymlinkPolicy::Deny);
}
#[test]
fn mounts_preserve_insertion_order() {
let b = OciImageBuilder::new("alpine")
.with_mount("/h1", "first", "/a/b")
.with_mount("/h2", "second", "/a")
.with_mount("/h3", "third", "/a/c");
let tags: Vec<&str> = b.mounts.iter().map(|m| m.1.as_str()).collect();
assert_eq!(tags, vec!["first", "second", "third"]);
}
}
#[cfg(test)]
mod map_bake_error_tests {
use super::{map_bake_error, Error};
#[test]
fn decodes_kernel_panic_sentinel() {
let msg = "KERNEL_PANIC|Kernel panic - not syncing: oops|frame_a\x1Fframe_b\x1Fframe_c"
.to_string();
let err = map_bake_error("nginx:alpine", msg);
match err {
Error::KernelPanic { first_line, stack } => {
assert!(first_line.contains("Kernel panic"));
assert_eq!(stack, vec!["frame_a", "frame_b", "frame_c"]);
}
other => panic!("expected KernelPanic, got {other:?}"),
}
}
#[test]
fn kernel_panic_with_empty_stack() {
let msg = "KERNEL_PANIC|Internal error: Oops|".to_owned();
match map_bake_error("img", msg) {
Error::KernelPanic { first_line, stack } => {
assert_eq!(first_line, "Internal error: Oops");
assert!(stack.len() <= 1);
}
other => panic!("expected KernelPanic, got {other:?}"),
}
}
#[test]
fn non_panic_messages_unchanged() {
let err = map_bake_error(
"img",
"supermachine snapshot timeout; see bake.log".to_owned(),
);
match err {
Error::Bake { msg, .. } => assert!(msg.contains("timeout")),
other => panic!("expected Bake, got {other:?}"),
}
}
}
#[cfg(test)]
mod base64_tests {
use super::{b64_decode, b64_encode};
use proptest::prelude::*;
#[test]
fn known_vectors_round_trip() {
let cases: &[(&[u8], &str)] = &[
(b"", ""),
(b"f", "Zg=="),
(b"fo", "Zm8="),
(b"foo", "Zm9v"),
(b"foob", "Zm9vYg=="),
(b"fooba", "Zm9vYmE="),
(b"foobar", "Zm9vYmFy"),
];
for (raw, enc) in cases {
assert_eq!(b64_encode(raw), *enc, "encode {raw:?}");
assert_eq!(b64_decode(enc).unwrap(), *raw, "decode {enc:?}");
}
}
#[test]
fn decode_rejects_malformed_padding() {
for bad in ["====", "A===", "=AAA", "AA==AAAA", "Zm==Zm9v", "=Zm9"] {
assert!(
b64_decode(bad).is_err(),
"{bad:?} must be rejected as malformed padding"
);
}
}
#[test]
fn decode_rejects_bad_length_and_chars() {
assert!(b64_decode("ABC").is_err(), "length not a multiple of 4");
assert!(b64_decode("Zm9").is_err());
assert!(b64_decode("Zm$v").is_err(), "invalid character");
assert!(b64_decode("Zm9*").is_err());
}
#[test]
fn decode_tolerates_whitespace() {
assert_eq!(b64_decode("Zm9v YmFy").unwrap(), b"foobar");
assert_eq!(b64_decode("Zm9v\nYmFy\n").unwrap(), b"foobar");
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1024))]
#[test]
fn round_trips_for_all_bytes(data in proptest::collection::vec(any::<u8>(), 0..512)) {
let enc = b64_encode(&data);
prop_assert_eq!(enc.len() % 4, 0);
prop_assert_eq!(b64_decode(&enc).unwrap(), data);
}
}
}