use super::persistence::{SandboxRecordStore, SandboxTransition};
use super::types::action;
use super::*;
type BootOutput = (Arc<fc_sdk::Vm>, PathBuf);
struct BootFailure {
error: VmmError,
process: Option<fc_sdk::FirecrackerProcess>,
cow_handle: Option<CowHandle>,
}
#[allow(
clippy::too_many_arguments,
reason = "boot task captures manager state"
)]
pub(super) async fn boot_sandbox(
id: SandboxId,
spec: SandboxSpec,
net_alloc: Option<NetworkAllocation>,
vm_dir: PathBuf,
instances: Arc<RwLock<HashMap<SandboxId, Arc<Mutex<SandboxInstance>>>>>,
network: Arc<NetworkManager>,
config: Arc<VmmConfig>,
events_tx: broadcast::Sender<SandboxEvent>,
cow_manager: Arc<CowManager>,
records: Arc<SandboxRecordStore>,
generation: Uuid,
resource_handoff: tokio::sync::oneshot::Sender<()>,
) {
match do_boot(
&id,
&spec,
net_alloc.as_ref(),
&vm_dir,
&config,
&cow_manager,
&instances,
generation,
resource_handoff,
)
.await
{
Ok((vm, vsock_uds_path)) => {
let ready_at = Utc::now();
let current = instances.read().unwrap().get(&id).cloned();
let is_current_generation = current
.as_ref()
.is_some_and(|arc| arc.lock().unwrap().record_generation == Some(generation));
if !is_current_generation {
info!(sandbox_id = %id, "stale sandbox boot completed");
return;
}
let durable_ready =
records
.transition(&id, generation, SandboxTransition::Ready)
.and_then(|commit| match commit.durability_error {
Some(error) => Err(VmmError::Unavailable(format!(
"sandbox {id} ready state is visible, but durability is unconfirmed: {error}"
))),
None => Ok(()),
});
if let Err(record_error) = durable_ready {
let message = format!("failed to persist ready state: {record_error}");
let value = instances.read().unwrap().get(&id).cloned();
let cleanup_lock = value
.as_ref()
.map(|arc| arc.lock().unwrap().cleanup_lock.clone());
let _cleanup_guard = match cleanup_lock.as_ref() {
Some(lock) => Some(lock.lock().await),
None => None,
};
let mut updated_current = false;
let mut failure_record_error = None;
let mut failure_record_visible = false;
if let Some(ref arc) = value {
let mut inst = arc.lock().unwrap();
if can_mark_boot_failed(&inst, generation) {
(failure_record_visible, failure_record_error) =
persist_boot_failure(&records, &id, generation, &message);
inst.state = SandboxState::Failed;
inst.error = Some(message.clone());
updated_current = true;
}
}
let cleanup_complete = if updated_current {
match super::cleanup::release_runtime_resources(
&id,
value.as_ref().unwrap(),
&network,
&config,
&cow_manager,
)
.await
{
Ok(()) => true,
Err(error) => {
error!(sandbox_id = %id, error = %error, "boot failure cleanup incomplete");
false
}
}
} else {
false
};
if failure_record_visible && cleanup_complete {
if let Err(error) = super::reconcile::clear_state_record(&vm_dir) {
error!(sandbox_id = %id, error = %error, "boot failure journal cleanup is not durable");
}
}
if updated_current {
let _ = events_tx
.send(SandboxEvent::new(&id, action::FAILED).with_attr("error", &message));
}
if let Some(error) = failure_record_error {
error!(sandbox_id = %id, error, "failed to persist sandbox boot failure");
}
error!(sandbox_id = %id, error = %record_error, "sandbox ready state was not durable");
return;
}
let mut vm = Some(vm);
let accepted = {
let map = instances.read().unwrap();
match map.get(&id) {
Some(arc) => {
let mut inst = arc.lock().unwrap();
if inst.record_generation != Some(generation)
|| matches!(inst.state, SandboxState::Stopping | SandboxState::Stopped)
{
false
} else {
inst.vm = vm.take();
inst.vsock_uds_path = Some(vsock_uds_path.clone());
inst.state = SandboxState::Ready;
inst.ready_at = Some(ready_at);
true
}
}
None => false,
}
};
if !accepted {
info!(sandbox_id = %id, "sandbox removed/stopped during boot");
return;
}
let _ = events_tx.send(SandboxEvent::new(&id, action::READY));
info!(sandbox_id = %id, "sandbox booted and ready");
if !spec.cmd.is_empty() {
run_initial_cmd(&id, spec, &vsock_uds_path, &instances, &events_tx).await;
}
}
Err(mut failure) => {
let message = failure.error.to_string();
let value = instances.read().unwrap().get(&id).cloned();
let cleanup_lock = value
.as_ref()
.map(|arc| arc.lock().unwrap().cleanup_lock.clone());
let _cleanup_guard = match cleanup_lock.as_ref() {
Some(lock) => Some(lock.lock().await),
None => None,
};
let mut updated_current = false;
let mut record_error = None;
let mut failure_record_visible = false;
if let Some(ref arc) = value {
let mut inst = arc.lock().unwrap();
if can_mark_boot_failed(&inst, generation) {
(failure_record_visible, record_error) =
persist_boot_failure(&records, &id, generation, &message);
inst.state = SandboxState::Failed;
inst.error = Some(message.clone());
if let Some(process) = failure.process.take() {
inst.process = Some(process);
}
if let Some(cow_handle) = failure.cow_handle.take() {
inst.cow_handle = Some(cow_handle);
}
updated_current = true;
}
}
let cleanup_complete = if updated_current {
match super::cleanup::release_runtime_resources(
&id,
value.as_ref().unwrap(),
&network,
&config,
&cow_manager,
)
.await
{
Ok(()) => true,
Err(error) => {
error!(sandbox_id = %id, error = %error, "boot failure cleanup incomplete");
false
}
}
} else if let Some(process) = failure.process.take() {
match tear_down_orphaned_boot(process, failure.cow_handle.take(), &cow_manager)
.await
{
Ok(()) => true,
Err(error) => {
error!(sandbox_id = %id, error = %error, "stale boot failure cleanup incomplete");
false
}
}
} else if let Some(handle) = failure.cow_handle.take() {
match cow_manager.teardown_checked(&handle).await {
Ok(()) => true,
Err(error) => {
error!(sandbox_id = %id, error = %error, "stale boot CoW cleanup incomplete");
false
}
}
} else {
true
};
if failure_record_visible && cleanup_complete {
if let Err(error) = super::reconcile::clear_state_record(&vm_dir) {
error!(sandbox_id = %id, error = %error, "stale boot journal cleanup is not durable");
}
}
if updated_current {
let _ = events_tx
.send(SandboxEvent::new(&id, action::FAILED).with_attr("error", &message));
}
if let Some(record_error) = record_error {
error!(
sandbox_id = %id,
error = %record_error,
"failed to persist sandbox boot failure"
);
}
error!(sandbox_id = %id, error = %failure.error, "sandbox boot failed");
}
}
}
fn can_mark_boot_failed(inst: &SandboxInstance, generation: Uuid) -> bool {
inst.record_generation == Some(generation)
&& !matches!(inst.state, SandboxState::Stopping | SandboxState::Stopped)
}
fn persist_boot_failure(
records: &SandboxRecordStore,
id: &str,
generation: Uuid,
message: &str,
) -> (bool, Option<String>) {
match records.transition(
id,
generation,
SandboxTransition::Failed(message.to_owned()),
) {
Ok(commit) => match commit.durability_error {
Some(error) => (false, Some(error)),
None => (true, None),
},
Err(error) => (false, Some(error.to_string())),
}
}
async fn run_initial_cmd(
id: &SandboxId,
spec: SandboxSpec,
vsock_uds_path: &Path,
instances: &super::InstanceMap,
events_tx: &broadcast::Sender<SandboxEvent>,
) {
let start = StartCommand {
cmd: spec.cmd,
env: spec.env,
working_dir: spec.working_dir,
user: spec.user,
tty: false,
tty_width: 80,
tty_height: 24,
timeout_seconds: 0,
};
match super::workload::start_run_workload(id, vsock_uds_path, start, instances, events_tx).await
{
Ok(mut rx) => {
info!(sandbox_id = %id, "initial cmd started");
tokio::spawn(async move { while rx.recv().await.is_some() {} });
}
Err(e) => {
warn!(sandbox_id = %id, error = %e, "initial cmd failed to start; sandbox stays ready");
}
}
}
pub(super) fn chroot_root(fc_binary: &str, chroot_base_dir: &str, id: &str) -> PathBuf {
let exec_name = Path::new(fc_binary)
.file_name()
.expect("fc_binary must have a filename")
.to_string_lossy();
PathBuf::from(chroot_base_dir)
.join(exec_name.as_ref())
.join(id)
.join("root")
}
pub(super) async fn stage_kernel_for_jailer(
chroot_root: &Path,
kernel_src: &str,
uid: u32,
gid: u32,
) -> Result<String> {
tokio::fs::create_dir_all(chroot_root)
.await
.map_err(VmmError::Io)?;
let kernel_dst = chroot_root.join("vmlinux");
tokio::fs::copy(kernel_src, &kernel_dst)
.await
.map_err(VmmError::Io)?;
chown(
&kernel_dst,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)
.map_err(|e| VmmError::Process(format!("chown kernel: {e}")))?;
Ok("/vmlinux".to_string())
}
pub(super) async fn stage_rootfs_copy_for_jailer(
chroot_root: &Path,
rootfs_src: &str,
uid: u32,
gid: u32,
) -> Result<String> {
tokio::fs::create_dir_all(chroot_root)
.await
.map_err(VmmError::Io)?;
let rootfs_dst = chroot_root.join("rootfs.ext4");
if let Err(e) = tokio::fs::remove_file(&rootfs_dst).await
&& e.kind() != std::io::ErrorKind::NotFound
{
return Err(VmmError::Io(e));
}
tokio::fs::copy(rootfs_src, &rootfs_dst)
.await
.map_err(VmmError::Io)?;
chown(
&rootfs_dst,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)
.map_err(|e| VmmError::Process(format!("chown rootfs: {e}")))?;
Ok("/rootfs.ext4".to_string())
}
pub(super) async fn stage_rootfs_device_for_jailer(
chroot_root: &Path,
dm_device: &str,
uid: u32,
gid: u32,
) -> Result<String> {
tokio::fs::create_dir_all(chroot_root)
.await
.map_err(VmmError::Io)?;
let (major, minor) = crate::snapshot_cow::device_major_minor(dm_device).await?;
let node_path = chroot_root.join("rootfs.ext4");
if let Err(e) = tokio::fs::remove_file(&node_path).await
&& e.kind() != std::io::ErrorKind::NotFound
{
return Err(VmmError::Io(e));
}
crate::snapshot_cow::mknod_blkdev(&node_path, major, minor).await?;
chown(
&node_path,
Some(Uid::from_raw(uid)),
Some(Gid::from_raw(gid)),
)
.map_err(|e| VmmError::Process(format!("chown rootfs device: {e}")))?;
Ok("/rootfs.ext4".to_string())
}
pub(super) fn create_rootfs_symlink(vm_dir: &Path, dm_device: &str) -> Result<String> {
let link_path = vm_dir.join("rootfs.link");
let _ = std::fs::remove_file(&link_path);
std::os::unix::fs::symlink(dm_device, &link_path).map_err(VmmError::Io)?;
link_path
.to_str()
.map(str::to_owned)
.ok_or_else(|| VmmError::Config(format!("non-UTF-8 path: {}", link_path.display())))
}
pub(super) async fn kill_and_reap_fc_checked(
process: &mut fc_sdk::FirecrackerProcess,
) -> Result<()> {
if let Some(pid) = process.pid()
&& pid > 0
{
match nix::sys::signal::kill(
#[allow(
clippy::cast_possible_wrap,
reason = "Firecracker pid fits platform pid_t"
)]
nix::unistd::Pid::from_raw(pid as i32),
nix::sys::signal::Signal::SIGKILL,
) {
Ok(()) | Err(nix::errno::Errno::ESRCH) => {}
Err(error) => {
return Err(VmmError::Process(format!(
"kill firecracker {pid}: {error}"
)));
}
}
}
match tokio::time::timeout(std::time::Duration::from_secs(5), process.wait()).await {
Ok(Ok(_)) => Ok(()),
Ok(Err(error)) => Err(VmmError::Process(format!("reap firecracker: {error}"))),
Err(_) => Err(VmmError::Process("timed out reaping firecracker".into())),
}
}
async fn tear_down_orphaned_boot(
mut process: fc_sdk::FirecrackerProcess,
cow_handle: Option<CowHandle>,
cow_manager: &CowManager,
) -> Result<()> {
kill_and_reap_fc_checked(&mut process).await?;
if let Some(handle) = cow_handle {
cow_manager.teardown_checked(&handle).await?;
}
Ok(())
}
#[allow(
clippy::too_many_arguments,
reason = "boot owns one exact sandbox generation and its handoff signal"
)]
async fn do_boot(
id: &str,
spec: &SandboxSpec,
net_alloc: Option<&NetworkAllocation>,
vm_dir: &Path,
config: &VmmConfig,
cow_manager: &CowManager,
instances: &super::InstanceMap,
generation: Uuid,
resource_handoff: tokio::sync::oneshot::Sender<()>,
) -> std::result::Result<BootOutput, BootFailure> {
let mut resource_handoff = Some(resource_handoff);
let log_path = vm_dir.join("firecracker.log");
let metrics_path = vm_dir.join("firecracker.metrics");
let socket_path = vm_dir.join("firecracker.sock");
let fc_cfg = &config.firecracker;
let prepare_files = (|| -> Result<()> {
if fc_cfg.jailer.is_some() {
return Ok(());
}
if let Some(parent) = log_path.parent() {
std::fs::create_dir_all(parent).map_err(VmmError::Io)?;
}
std::fs::File::create(&log_path).map_err(VmmError::Io)?;
std::fs::File::create(&metrics_path).map_err(VmmError::Io)?;
Ok(())
})();
if let Err(error) = prepare_files {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error,
process: None,
cow_handle: None,
});
}
let process_result = if let Some(ref jc) = fc_cfg.jailer {
spawn_jailer(jc, fc_cfg, id).await
} else {
spawn_direct(fc_cfg, id, &socket_path, &log_path, &metrics_path).await
};
let process = match process_result {
Ok(process) => process,
Err(error) => {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error,
process: None,
cow_handle: None,
});
}
};
#[allow(
clippy::cast_possible_wrap,
reason = "Firecracker pid fits platform pid_t"
)]
let process_pid = process.pid().map(|pid| pid as i32);
let process_socket = process.socket_path().to_owned();
let spawned_record = super::reconcile::SandboxStateRecord::new(
id,
process_pid,
net_alloc,
None,
fc_cfg.jailer.is_some(),
None,
);
let journal_error = super::reconcile::write_state_record(vm_dir, &spawned_record).err();
let mut process = Some(process);
let state = {
let map = instances.read().unwrap();
map.get(id).and_then(|instance| {
let mut instance = instance.lock().unwrap();
(instance.record_generation == Some(generation)).then(|| {
instance.process = process.take();
instance.state
})
})
};
let Some(state) = state else {
return Err(BootFailure {
error: VmmError::WrongState {
id: id.to_owned(),
expected: "the current sandbox generation".into(),
actual: "replaced or removed".into(),
},
process,
cow_handle: None,
});
};
if matches!(state, SandboxState::Stopping | SandboxState::Stopped) {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error: VmmError::WrongState {
id: id.to_owned(),
expected: "a sandbox still booting".into(),
actual: state.to_string(),
},
process: None,
cow_handle: None,
});
}
if let Some(error) = journal_error {
complete_resource_handoff(&mut resource_handoff);
return Err(BootFailure {
error,
process: None,
cow_handle: None,
});
}
let mut cow_handle = None;
let paths: Result<(String, String, String, PathBuf)> = async {
if let Some(ref jc) = fc_cfg.jailer {
let base = jc.chroot_base_dir.as_deref().unwrap_or("/srv/jailer");
let cr = chroot_root(&fc_cfg.binary, base, id);
let k = stage_kernel_for_jailer(&cr, &spec.kernel, jc.uid, jc.gid).await?;
let r = match cow_manager.setup(id, &spec.rootfs).await {
Ok(handle) => {
cow_handle = Some(handle);
let record = super::reconcile::SandboxStateRecord::new(
id,
process_pid,
net_alloc,
cow_handle.as_ref(),
true,
None,
);
super::reconcile::write_state_record(vm_dir, &record)?;
match stage_rootfs_device_for_jailer(
&cr,
&cow_handle.as_ref().unwrap().dm_device,
jc.uid,
jc.gid,
)
.await
{
Ok(path) => path,
Err(e) => {
debug!(
sandbox_id = %id,
error = %e,
"mknod failed, falling back to rootfs copy"
);
cow_manager
.teardown_checked(cow_handle.as_ref().unwrap())
.await?;
cow_handle = None;
let record = super::reconcile::SandboxStateRecord::new(
id,
process_pid,
net_alloc,
None,
true,
None,
);
super::reconcile::write_state_record(vm_dir, &record)?;
stage_rootfs_copy_for_jailer(&cr, &spec.rootfs, jc.uid, jc.gid).await?
}
}
}
Err(e) => {
if matches!(e, VmmError::Unavailable(_)) {
return Err(e);
}
debug!(
sandbox_id = %id,
error = %e,
"dm-snapshot unavailable, copying rootfs into chroot"
);
stage_rootfs_copy_for_jailer(&cr, &spec.rootfs, jc.uid, jc.gid).await?
}
};
let vsock_host = cr.join("run/firecracker.vsock");
Ok((k, r, "/run/firecracker.vsock".to_string(), vsock_host))
} else {
let rootfs = match cow_manager.setup(id, &spec.rootfs).await {
Ok(handle) => {
cow_handle = Some(handle);
let record = super::reconcile::SandboxStateRecord::new(
id,
process_pid,
net_alloc,
cow_handle.as_ref(),
false,
None,
);
super::reconcile::write_state_record(vm_dir, &record)?;
create_rootfs_symlink(vm_dir, &cow_handle.as_ref().unwrap().dm_device)?
}
Err(e) => {
if matches!(e, VmmError::Unavailable(_)) {
return Err(e);
}
debug!(
sandbox_id = %id,
error = %e,
"dm-snapshot unavailable, using rootfs directly"
);
spec.rootfs.clone()
}
};
let vsock_path = vm_dir.join("firecracker.vsock");
Ok((
spec.kernel.clone(),
rootfs,
vsock_path.to_str().unwrap().to_owned(),
vsock_path,
))
}
}
.await;
let state = {
let map = instances.read().unwrap();
map.get(id).and_then(|instance| {
let mut instance = instance.lock().unwrap();
(instance.record_generation == Some(generation)).then(|| {
if cow_handle.is_some() {
debug_assert!(instance.cow_handle.is_none());
instance.cow_handle = cow_handle.take();
}
instance.state
})
})
};
let Some(state) = state else {
return Err(BootFailure {
error: VmmError::WrongState {
id: id.to_owned(),
expected: "the current sandbox generation".into(),
actual: "replaced or removed during boot setup".into(),
},
process: None,
cow_handle,
});
};
complete_resource_handoff(&mut resource_handoff);
let (kernel_path, rootfs_path, vsock_fc_path, vsock_host_path) =
paths.map_err(|error| BootFailure {
error,
process: None,
cow_handle: None,
})?;
if matches!(state, SandboxState::Stopping | SandboxState::Stopped) {
return Err(BootFailure {
error: VmmError::WrongState {
id: id.to_owned(),
expected: "a sandbox still booting".into(),
actual: state.to_string(),
},
process: None,
cow_handle: None,
});
}
let vcpu_count =
NonZeroU64::new(spec.vcpus.max(1) as u64).expect("max(1) guarantees a non-zero vCPU count");
let boot_args = if let Some(net) = net_alloc {
if spec.boot_args.contains("ip=") {
spec.boot_args.clone()
} else {
let ip_param = KernelIpParam {
client: net.ip_address,
gateway: net.gateway,
netmask: net.netmask(),
};
format!("{} {ip_param}", spec.boot_args)
}
} else {
spec.boot_args.clone()
};
let mut builder = VmBuilder::new(process_socket)
.boot_source(BootSource {
kernel_image_path: kernel_path,
boot_args: Some(boot_args),
initrd_path: None,
})
.machine_config(fc_sdk::types::MachineConfiguration {
vcpu_count,
#[allow(
clippy::cast_possible_wrap,
reason = "memory MiB value fits Firecracker API i64"
)]
mem_size_mib: spec.memory_mib as i64,
smt: false,
track_dirty_pages: true,
cpu_template: None,
huge_pages: None,
})
.drive(Drive {
drive_id: "rootfs".into(),
path_on_host: Some(rootfs_path),
is_root_device: true,
is_read_only: Some(false),
partuuid: None,
cache_type: fc_sdk::types::DriveCacheType::Unsafe,
rate_limiter: None,
io_engine: fc_sdk::types::DriveIoEngine::Sync,
socket: None,
});
if let Some(net) = net_alloc {
builder = builder.network_interface(NetworkInterface {
iface_id: "eth0".into(),
guest_mac: Some(net.mac_address.clone()),
host_dev_name: net.tap_name.clone(),
rx_rate_limiter: None,
tx_rate_limiter: None,
});
}
builder = builder.vsock(Vsock {
guest_cid: 3,
uds_path: vsock_fc_path,
vsock_id: None,
});
let vm = match builder.start().await {
Ok(v) => Arc::new(v),
Err(e) => {
return Err(BootFailure {
error: VmmError::from(e),
process: None,
cow_handle: None,
});
}
};
Ok((vm, vsock_host_path))
}
fn complete_resource_handoff(signal: &mut Option<tokio::sync::oneshot::Sender<()>>) {
if let Some(signal) = signal.take() {
let _ = signal.send(());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn boot_failure_cannot_overwrite_shutdown() {
let generation = Uuid::new_v4();
let mut instance = SandboxInstance::new_with_generation(
"box".into(),
SandboxSpec::default(),
None,
PathBuf::from("/tmp/box"),
generation,
);
assert!(can_mark_boot_failed(&instance, generation));
instance.state = SandboxState::Stopping;
assert!(!can_mark_boot_failed(&instance, generation));
instance.state = SandboxState::Stopped;
assert!(!can_mark_boot_failed(&instance, generation));
assert!(!can_mark_boot_failed(&instance, Uuid::new_v4()));
}
}