use super::*;
#[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>,
) {
match do_boot(
&id,
&spec,
net_alloc.as_ref(),
&vm_dir,
&config,
&cow_manager,
)
.await
{
Ok((process, vm, vsock_uds_path, cow_handle)) => {
let ready_at = Utc::now();
#[allow(
clippy::cast_possible_wrap,
reason = "Firecracker pid fits platform pid_t"
)]
let record = super::reconcile::SandboxStateRecord::new(
&id,
process.pid().map(|p| p as i32),
net_alloc.as_ref(),
cow_handle.as_ref(),
config.firecracker.jailer.is_some(),
None,
);
super::reconcile::write_state_record(&vm_dir, &record);
let mut process = Some(process);
let mut vm = Some(vm);
let mut cow_handle = cow_handle;
let accepted = {
let map = instances.read().unwrap();
match map.get(&id) {
Some(arc) => {
let mut inst = arc.lock().unwrap();
if matches!(inst.state, SandboxState::Stopping | SandboxState::Stopped) {
false
} else {
inst.process = process.take();
inst.vm = vm.take();
inst.vsock_uds_path = Some(vsock_uds_path.clone());
inst.cow_handle = cow_handle.take();
inst.state = SandboxState::Ready;
inst.ready_at = Some(ready_at);
true
}
}
None => false,
}
};
if !accepted {
info!(sandbox_id = %id, "sandbox removed/stopped during boot; tearing down booted resources");
if let Some(process) = process.take() {
tear_down_orphaned_boot(process, cow_handle.take(), &cow_manager).await;
}
return;
}
let _ = events_tx.send(SandboxEvent::new(&id, "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(e) => {
let value = instances.read().unwrap().get(&id).cloned();
if let Some(arc) = value {
let mut inst = arc.lock().unwrap();
inst.state = SandboxState::Failed;
inst.error = Some(e.to_string());
}
if let Some(ref net) = net_alloc {
network.release(net);
}
let _ =
events_tx.send(SandboxEvent::new(&id, "failed").with_attr("error", &e.to_string()));
error!(sandbox_id = %id, error = %e, "sandbox boot failed");
}
}
}
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 cleanup_pending_restore(
cow_manager: &CowManager,
network: &NetworkManager,
cow: Option<CowHandle>,
net_alloc: Option<&NetworkAllocation>,
origin_dir: Option<&Path>,
) {
if let Some(handle) = cow {
cow_manager.teardown(&handle).await;
}
if let Some(net) = net_alloc {
network.release(net);
}
if let Some(dir) = origin_dir
&& let Err(e) = tokio::fs::remove_dir_all(dir).await
&& e.kind() != std::io::ErrorKind::NotFound
{
warn!(dir = %dir.display(), err = %e, "failed to clean up restore origin dir");
}
}
pub(super) async fn kill_and_reap_fc(process: &mut fc_sdk::FirecrackerProcess) {
if let Some(pid) = process.pid()
&& pid > 0
{
let _ = 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,
);
}
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), process.wait()).await;
}
async fn tear_down_orphaned_boot(
mut process: fc_sdk::FirecrackerProcess,
cow_handle: Option<CowHandle>,
cow_manager: &CowManager,
) {
kill_and_reap_fc(&mut process).await;
if let Some(handle) = cow_handle {
cow_manager.teardown(&handle).await;
}
}
#[allow(
clippy::type_complexity,
reason = "boot returns coupled Firecracker resources"
)]
async fn do_boot(
id: &str,
spec: &SandboxSpec,
net_alloc: Option<&NetworkAllocation>,
vm_dir: &Path,
config: &VmmConfig,
cow_manager: &CowManager,
) -> Result<(
fc_sdk::FirecrackerProcess,
Arc<fc_sdk::Vm>,
PathBuf,
Option<CowHandle>,
)> {
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;
if fc_cfg.jailer.is_none() {
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)?;
}
let mut process = 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 (kernel_path, rootfs_path, vsock_fc_path, vsock_host_path, cow_handle) =
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, cow) = match cow_manager.setup(id, &spec.rootfs).await {
Ok(handle) => {
match stage_rootfs_device_for_jailer(&cr, &handle.dm_device, jc.uid, jc.gid)
.await
{
Ok(path) => (path, Some(handle)),
Err(e) => {
debug!(
sandbox_id = %id,
error = %e,
"mknod failed, falling back to rootfs copy"
);
cow_manager.teardown(&handle).await;
let path =
stage_rootfs_copy_for_jailer(&cr, &spec.rootfs, jc.uid, jc.gid)
.await?;
(path, None)
}
}
}
Err(e) => {
debug!(
sandbox_id = %id,
error = %e,
"dm-snapshot unavailable, copying rootfs into chroot"
);
let path =
stage_rootfs_copy_for_jailer(&cr, &spec.rootfs, jc.uid, jc.gid).await?;
(path, None)
}
};
let vsock_host = cr.join("run/firecracker.vsock");
(k, r, "/run/firecracker.vsock".to_string(), vsock_host, cow)
} else {
let (rootfs, cow) = match cow_manager.setup(id, &spec.rootfs).await {
Ok(handle) => match create_rootfs_symlink(vm_dir, &handle.dm_device) {
Ok(link) => (link, Some(handle)),
Err(e) => {
cow_manager.teardown(&handle).await;
return Err(e);
}
},
Err(e) => {
debug!(
sandbox_id = %id,
error = %e,
"dm-snapshot unavailable, using rootfs directly"
);
(spec.rootfs.clone(), None)
}
};
let vsock_path = vm_dir.join("firecracker.vsock");
(
spec.kernel.clone(),
rootfs,
vsock_path.to_str().unwrap().to_owned(),
vsock_path,
cow,
)
};
let vcpu_count = NonZeroU64::new(spec.vcpus.max(1) as u64)
.ok_or_else(|| VmmError::Config("vcpus must be > 0".into()))?;
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_path())
.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) => {
if let Some(ref handle) = cow_handle {
kill_and_reap_fc(&mut process).await;
cow_manager.teardown(handle).await;
if fc_cfg.jailer.is_none() {
let _ = std::fs::remove_file(vm_dir.join("rootfs.link"));
}
}
return Err(VmmError::from(e));
}
};
Ok((process, vm, vsock_host_path, cow_handle))
}