arcbox_vm_driver/driver.rs
1//! The port itself: what a driver does, what a handle to a running VM is.
2//!
3//! The vocabulary here is what every orchestrator needs from every VMM and
4//! nothing a single VMM happens to offer: identify a VM, observe it, stop
5//! it. Everything else — vsock, checkpoints, adopt/detach, balloon, console,
6//! debug — is a capability reached through an `Option<&dyn Cap>` accessor
7//! ([`crate::capability`]), present only when both the driver can do it and
8//! the spec asked for the device.
9
10use std::fmt;
11use std::os::fd::OwnedFd;
12use std::path::{Path, PathBuf};
13use std::time::Duration;
14
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17use tokio::sync::broadcast;
18
19use crate::capability::{
20 Adopt, Balloon, Checkpoint, CheckpointImage, Console, DebugSnapshot, Detach, Prepare, Staging,
21 Vsock, VsockListen,
22};
23use crate::error::Result;
24use crate::spec::{DiskSpec, IsolationSpec, NicSpec, VmId, VmSpec};
25
26/// A VMM adapter: boots specs into handles.
27///
28/// Object-safe on purpose — roots keep an `Arc<dyn VmDriver>` and never name
29/// the adapter again.
30#[async_trait]
31pub trait VmDriver: Send + Sync {
32 /// A short stable name (`"fc"`, `"vz"`, `"hv"`), recorded in
33 /// [`VmRecord::driver`] and [`crate::Error::Driver`].
34 fn name(&self) -> &'static str;
35
36 /// What this driver can do, independent of any one VM.
37 fn capabilities(&self) -> DriverCapabilities;
38
39 /// Boots `spec` with `runtime_dir` as the VM's private scratch space
40 /// (sockets, logs, staged files) and returns a handle to the running VM.
41 ///
42 /// The driver validates the spec ([`VmSpec::validate`]) and fails with
43 /// [`crate::Error::InvalidSpec`] before touching the host. A returned
44 /// handle owns the VM: dropping it kills the VM unless a
45 /// [`Detach`] released it first.
46 async fn boot(&self, spec: VmSpec, runtime_dir: &Path) -> Result<Box<dyn VmHandle>>;
47
48 /// Boots a new VM from a checkpoint this driver wrote.
49 ///
50 /// Fails with [`crate::Error::ForeignCheckpoint`] for a
51 /// [`CheckpointImage::format`] the driver does not recognise as its own.
52 async fn restore(
53 &self,
54 image: &CheckpointImage,
55 spec: RestoreSpec,
56 runtime_dir: &Path,
57 ) -> Result<Box<dyn VmHandle>>;
58
59 /// The adopt capability, for VMs that outlive the process which booted
60 /// them (external-process VMMs). `Some` iff
61 /// [`DriverCapabilities::adopt`].
62 fn adopt(&self) -> Option<&dyn Adopt>;
63
64 /// The prepare capability: spawn the VMM ahead of a boot. `Some` iff
65 /// [`DriverCapabilities::prepare`]; [`boot`](Self::boot) and
66 /// [`restore`](Self::restore) behave exactly like prepare-then-boot.
67 fn prepare(&self) -> Option<&dyn Prepare> {
68 None
69 }
70
71 /// The longest [`VmId`] this driver can run under an isolation, when
72 /// it has a limit of its own.
73 ///
74 /// A driver's layout can bound the id even though the port does not:
75 /// Firecracker's jailer puts its sockets under
76 /// `{chroot base}/{binary}/{id}/root/run/`, and AF_UNIX leaves 107
77 /// bytes for a path, so a long chroot base or a long id makes them
78 /// unaddressable. The failure does not name itself — the VMM comes up
79 /// and binds inside its chroot while every `connect` from outside
80 /// fails, which reads as a boot that timed out. An orchestrator
81 /// minting ids asks here instead of guessing.
82 ///
83 /// `None` means this driver imposes no limit of its own, not that the
84 /// question is unsupported; everything else an id must be is
85 /// [`VmId`]'s own rule. `Some(0)` is a real answer: a layout whose
86 /// fixed part already spends the whole budget leaves room for no id at
87 /// all, and a caller must then refuse every id rather than let the
88 /// arithmetic wrap into a plausible one.
89 fn id_budget(&self, _isolation: &IsolationSpec) -> Option<usize> {
90 None
91 }
92}
93
94/// A running (or just-exited) VM.
95///
96/// The mandatory surface is identify ([`id`](Self::id) /
97/// [`record`](Self::record)), observe ([`state`](Self::state) /
98/// [`events`](Self::events)), and stop ([`shutdown`](Self::shutdown)).
99/// Every other verb is a capability accessor that returns `Some` only when
100/// the driver can and the spec asked. Dropping the handle kills the VM
101/// unless [`Detach`] released it.
102#[async_trait]
103pub trait VmHandle: Send + Sync {
104 /// The VM's identity.
105 fn id(&self) -> &VmId;
106
107 /// The durable record; identical for the handle's whole life.
108 fn record(&self) -> VmRecord;
109
110 /// Where the VM is right now. Observational: never blocks.
111 fn state(&self) -> VmState;
112
113 /// Subscribes to what the VM does on its own. Observational: never
114 /// blocks. [`VmEvent::Exited`] is delivered exactly once; a subscriber
115 /// that arrives after the exit reads it from [`state`](Self::state).
116 fn events(&self) -> broadcast::Receiver<VmEvent>;
117
118 /// Stops the VM.
119 ///
120 /// [`ShutdownMode::Kill`] always succeeds for a live VM.
121 /// [`ShutdownMode::Graceful`] asks the guest and kills it at the
122 /// deadline; the returned [`ExitStatus`] says which happened. Calling
123 /// it on an exited VM returns the recorded status.
124 async fn shutdown(&self, mode: ShutdownMode) -> Result<ExitStatus>;
125
126 /// Dial guest vsock ports. `Some` iff the driver has vsock and the spec
127 /// had a [`crate::VsockSpec`].
128 fn vsock(&self) -> Option<&dyn Vsock> {
129 None
130 }
131
132 /// Checkpoint the VM. `Some` iff [`DriverCapabilities::checkpoint`].
133 fn checkpoint(&self) -> Option<&dyn Checkpoint> {
134 None
135 }
136
137 /// Accept guest-initiated vsock connections. `Some` iff the driver can
138 /// listen and the spec had a [`crate::VsockSpec`].
139 fn vsock_listener(&self) -> Option<&dyn VsockListen> {
140 None
141 }
142
143 /// Give the VM up without stopping it. `Some` iff
144 /// [`DriverCapabilities::adopt`].
145 fn detach(&self) -> Option<&dyn Detach> {
146 None
147 }
148
149 /// The area this VM's files live in — the same one
150 /// [`PreparedVm::staging`](crate::PreparedVm::staging) stages into,
151 /// reached from the other grip on the same VM. `Some` iff
152 /// [`DriverCapabilities::staging`].
153 ///
154 /// The prepared VM is not the only grip that reaches an area: one this
155 /// driver *adopted* has no [`PreparedVm`](crate::PreparedVm) — the VMM
156 /// outlived the process that prepared it — and would otherwise have no
157 /// route to what was staged into it. Taking a disk back out
158 /// ([`Staging::unstage_disk`]) is what an orchestrator whose VM runs on
159 /// a staged disk must do before it stops that VM, and the VM it has to
160 /// do it for may well be an adopted one.
161 fn staging(&self) -> Option<&dyn Staging> {
162 None
163 }
164
165 /// The memory balloon. `Some` iff the driver has one and the spec set
166 /// [`VmSpec::balloon`].
167 fn balloon(&self) -> Option<&dyn Balloon> {
168 None
169 }
170
171 /// The guest console output. `Some` iff the driver exposes it and the
172 /// spec's [`VmSpec::console`] is not `Off`.
173 fn console(&self) -> Option<&dyn Console> {
174 None
175 }
176
177 /// A diagnostic snapshot. `Some` iff [`DriverCapabilities::debug`].
178 fn debug(&self) -> Option<&dyn DebugSnapshot> {
179 None
180 }
181}
182
183/// What a driver can do, independent of any one VM.
184///
185/// `capabilities()` describes the driver; a handle exposes a capability only
186/// when the spec also asked for the device (a VM booted without
187/// [`crate::VsockSpec`] has nothing to dial). The contract test-kit boots a
188/// spec that asks for everything the driver claims and checks that each
189/// flag here agrees with the matching handle accessor.
190#[derive(Debug, Clone, Default, PartialEq, Eq)]
191pub struct DriverCapabilities {
192 /// Handles can dial guest vsock ports ([`Vsock`]).
193 pub vsock: bool,
194 /// Handles can listen for guest-initiated vsock connections
195 /// ([`VsockListen`]).
196 pub vsock_listen: bool,
197 /// Handles can be checkpointed ([`Checkpoint`]) and the driver can
198 /// restore the result.
199 pub checkpoint: bool,
200 /// Checkpoints can be incremental
201 /// ([`CheckpointKind::Diff`](crate::CheckpointKind::Diff)).
202 pub diff_checkpoint: bool,
203 /// VMs outlive this process and can be adopted back ([`Adopt`] /
204 /// [`Detach`]).
205 pub adopt: bool,
206 /// The VMM can be spawned ahead of a boot ([`Prepare`]).
207 pub prepare: bool,
208 /// Prepared VMs and handles expose [`Staging`]: the files a VM boots
209 /// from are brought into an area of its own and named by where they
210 /// landed.
211 pub staging: bool,
212 /// Handles expose a memory balloon.
213 pub balloon: bool,
214 /// Handles expose the guest console output.
215 pub console: bool,
216 /// Handles expose a debug snapshot.
217 pub debug: bool,
218 /// Whether guests may run their own hypervisor.
219 pub nested_virt: NestedVirt,
220}
221
222/// Whether guests under this driver may run their own hypervisor, and if
223/// not, why — in the platform's own words.
224#[derive(Debug, Clone, Default, PartialEq, Eq)]
225pub struct NestedVirt {
226 /// Nested virtualization is available to guests.
227 pub supported: bool,
228 /// Why not, when `supported` is false; empty otherwise.
229 pub reason: String,
230}
231
232impl NestedVirt {
233 /// Nested virtualization is available.
234 pub fn supported() -> Self {
235 Self {
236 supported: true,
237 reason: String::new(),
238 }
239 }
240
241 /// Nested virtualization is unavailable for `reason`.
242 pub fn unsupported(reason: impl Into<String>) -> Self {
243 Self {
244 supported: false,
245 reason: reason.into(),
246 }
247 }
248}
249
250/// Where a VM is in its life.
251///
252/// `Quiesced` is reached only through the [`Checkpoint`] capability's
253/// [`HoldQuiesced`](crate::AfterCheckpoint::HoldQuiesced) option; there is
254/// no pause verb on the handle.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case")]
257pub enum VmState {
258 /// The guest is executing.
259 Running,
260 /// The guest is frozen after a checkpoint that asked to hold it.
261 Quiesced,
262 /// The VM is gone; the status says how.
263 Exited(ExitStatus),
264}
265
266impl fmt::Display for VmState {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 match self {
269 Self::Running => f.write_str("running"),
270 Self::Quiesced => f.write_str("quiesced"),
271 Self::Exited(status) => write!(f, "exited ({status})"),
272 }
273 }
274}
275
276/// Something the VM did on its own, delivered through [`VmHandle::events`].
277#[derive(Debug, Clone, PartialEq, Eq)]
278#[non_exhaustive]
279pub enum VmEvent {
280 /// The VM stopped, whether asked to or not. Delivered exactly once.
281 Exited(ExitStatus),
282 /// The guest asked for a reset (a reboot); the orchestrator decides
283 /// whether to honor it with a stop-and-boot.
284 ResetRequested,
285}
286
287/// How a VM ended.
288///
289/// External-process VMMs report the process's wait status; in-process VMMs
290/// synthesize one (a stop on request is a clean exit, a forced kill is
291/// `signal: Some(SIGKILL)`).
292#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
293pub struct ExitStatus {
294 /// The exit code, when the VM exited normally.
295 pub code: Option<i32>,
296 /// The signal that killed the VM, when it did not.
297 pub signal: Option<i32>,
298}
299
300impl ExitStatus {
301 /// A normal exit with `code`.
302 pub const fn exited(code: i32) -> Self {
303 Self {
304 code: Some(code),
305 signal: None,
306 }
307 }
308
309 /// A death by `signal`.
310 pub const fn signaled(signal: i32) -> Self {
311 Self {
312 code: None,
313 signal: Some(signal),
314 }
315 }
316
317 /// `true` for a normal exit with code 0 and no signal.
318 pub const fn is_clean(&self) -> bool {
319 matches!(self.code, Some(0)) && self.signal.is_none()
320 }
321}
322
323impl fmt::Display for ExitStatus {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 match (self.code, self.signal) {
326 (Some(code), None) => write!(f, "exit code {code}"),
327 (None, Some(signal)) => write!(f, "signal {signal}"),
328 (Some(code), Some(signal)) => write!(f, "exit code {code}, signal {signal}"),
329 (None, None) => f.write_str("unknown status"),
330 }
331 }
332}
333
334impl From<std::process::ExitStatus> for ExitStatus {
335 /// The wait status of an external VMM process, as the port sees it.
336 fn from(status: std::process::ExitStatus) -> Self {
337 use std::os::unix::process::ExitStatusExt as _;
338 Self {
339 code: status.code(),
340 signal: status.signal(),
341 }
342 }
343}
344
345/// How to stop a VM.
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum ShutdownMode {
348 /// Ask the guest to power off and wait up to `timeout`; a guest still
349 /// alive at the deadline is killed, and the returned status says which
350 /// happened.
351 Graceful {
352 /// How long the guest gets before it is killed.
353 timeout: Duration,
354 },
355 /// Stop the VM without asking the guest.
356 Kill,
357}
358
359/// The durable identity of a VM: what survives this process and what
360/// [`Adopt`] consumes to find the VM again.
361///
362/// Stable for a handle's whole life — the same value before, during, and
363/// after the VM runs.
364#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
365pub struct VmRecord {
366 /// The VM's identity.
367 pub id: VmId,
368 /// The driver that booted it ([`VmDriver::name`]).
369 pub driver: String,
370 /// The per-VM directory the driver was given at boot.
371 pub runtime_dir: PathBuf,
372 /// The VMM process, for external-process VMMs; `None` in-process.
373 #[serde(default)]
374 pub process: Option<ProcessRecord>,
375}
376
377/// An external VMM process.
378#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
379pub struct ProcessRecord {
380 /// The VMM's pid.
381 pub pid: u32,
382 /// The VMM's control socket, when it has one.
383 #[serde(default)]
384 pub api_socket: Option<PathBuf>,
385 /// The jail the VMM runs in, when it is confined to one.
386 ///
387 /// `None` means unconfined — and, in a record written before this field
388 /// existed, means the same, because a record that predates it can no
389 /// longer be asked.
390 #[serde(default)]
391 pub jail: Option<JailRecord>,
392}
393
394/// The jail a running VMM is in, as the process that put it there knew it.
395///
396/// Recorded rather than derived, because neither half survives the spawn:
397/// a jailer unshares its mount namespace and pivots into the chroot, so
398/// `/proc/<pid>/root` reads back as `/` from outside and the confinement
399/// cannot be observed at all; and re-deriving the root instead — from an
400/// [`IsolationSpec`]'s `chroot_base` and the adopting process's own config
401/// — names a different directory the moment that config differs from the
402/// one the VM was booted with.
403///
404/// What follows from getting it wrong is not a wrong path but a wrong VM:
405/// an adopter that concludes "unconfined" rebuilds the VM as one, and every
406/// operation that needs the jail — reaching the guest, checkpointing it —
407/// is then refused or aimed outside it (CORE-155).
408#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
409pub struct JailRecord {
410 /// The chroot the VMM is confined to, as the host names it.
411 pub root: PathBuf,
412 /// The uid the VMM runs as.
413 pub uid: u32,
414 /// The gid the VMM runs as.
415 pub gid: u32,
416}
417
418/// What may change when a checkpoint is restored into a new VM.
419///
420/// Everything else — CPUs, memory, disks, devices — is fixed by the image.
421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422pub struct RestoreSpec {
423 /// The restored VM's identity.
424 pub id: VmId,
425 /// The NICs to attach, in the image's bus order (a restored VM usually
426 /// lands on fresh host attachments).
427 #[serde(default)]
428 pub nics: Vec<NicSpec>,
429 /// The disks the image references — the same ids — at their host paths
430 /// for *this* restore: a restored VM's rootfs is a fresh copy-on-write
431 /// device, never the path recorded at checkpoint time.
432 #[serde(default)]
433 pub disks: Vec<DiskSpec>,
434 /// How the restored VMM process is confined.
435 #[serde(default)]
436 pub isolation: IsolationSpec,
437}
438
439/// A connected vsock stream to the guest.
440///
441/// The consumer builds its transport from `fd` — the port hands over a raw
442/// stream socket, not an I/O object — and picks the transport style from
443/// `mode`. The descriptor's blocking flag is unspecified; the consumer sets
444/// what its reactor needs.
445#[derive(Debug)]
446pub struct VsockConn {
447 /// The connected stream socket, owned by the consumer from here on.
448 pub fd: OwnedFd,
449 /// Which transport style the driver requires on this socket.
450 pub mode: IoMode,
451}
452
453/// The one transport fact a driver has to tell its consumer.
454///
455/// The Hypervisor.framework socketpair stalls the kqueue reactor under rapid
456/// connect/teardown, so its driver hands back `Blocking`; every other
457/// adapter hands back `Async`. This is where the old backend match on the
458/// transport side stops.
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460pub enum IoMode {
461 /// Register the socket with an async reactor.
462 Async,
463 /// Drive the socket with blocking I/O on a dedicated thread.
464 Blocking,
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn exit_status_is_clean_only_for_code_zero_without_signal() {
473 assert!(ExitStatus::exited(0).is_clean());
474 assert!(!ExitStatus::exited(1).is_clean());
475 assert!(!ExitStatus::signaled(9).is_clean());
476 assert!(
477 !ExitStatus {
478 code: Some(0),
479 signal: Some(15)
480 }
481 .is_clean()
482 );
483 assert!(
484 !ExitStatus {
485 code: None,
486 signal: None
487 }
488 .is_clean()
489 );
490 }
491
492 #[test]
493 fn exit_status_display_names_what_ended_the_vm() {
494 assert_eq!(ExitStatus::exited(3).to_string(), "exit code 3");
495 assert_eq!(ExitStatus::signaled(9).to_string(), "signal 9");
496 assert_eq!(
497 ExitStatus {
498 code: None,
499 signal: None
500 }
501 .to_string(),
502 "unknown status"
503 );
504 assert_eq!(
505 VmState::Exited(ExitStatus::exited(0)).to_string(),
506 "exited (exit code 0)"
507 );
508 }
509
510 #[test]
511 fn process_exit_status_maps_code_and_signal() {
512 use std::os::unix::process::ExitStatusExt as _;
513 let exited = std::process::ExitStatus::from_raw(3 << 8);
514 assert_eq!(ExitStatus::from(exited), ExitStatus::exited(3));
515 let killed = std::process::ExitStatus::from_raw(9);
516 assert_eq!(ExitStatus::from(killed), ExitStatus::signaled(9));
517 }
518
519 #[test]
520 fn vm_record_round_trips_and_defaults_the_process() {
521 let record = VmRecord {
522 id: VmId::new("vm-1").unwrap(),
523 driver: "fc".into(),
524 runtime_dir: "/run/arcbox/vm-1".into(),
525 process: Some(ProcessRecord {
526 pid: 4242,
527 api_socket: Some("/run/arcbox/vm-1/api.sock".into()),
528 jail: Some(JailRecord {
529 root: "/srv/jailer/firecracker/vm-1/root".into(),
530 uid: 123,
531 gid: 456,
532 }),
533 }),
534 };
535 let json = serde_json::to_string(&record).unwrap();
536 assert_eq!(serde_json::from_str::<VmRecord>(&json).unwrap(), record);
537
538 let in_process: VmRecord = serde_json::from_value(serde_json::json!({
539 "id": "vm-2",
540 "driver": "hv",
541 "runtime_dir": "/run/arcbox/vm-2",
542 }))
543 .unwrap();
544 assert_eq!(in_process.process, None);
545
546 // A record written before the jail was recorded still loads, and
547 // says what it can: a process, confined to nothing it can name.
548 let older: VmRecord = serde_json::from_value(serde_json::json!({
549 "id": "vm-3",
550 "driver": "fc",
551 "runtime_dir": "/run/arcbox/vm-3",
552 "process": { "pid": 7, "api_socket": "/run/arcbox/vm-3/api.sock" },
553 }))
554 .unwrap();
555 assert_eq!(older.process.unwrap().jail, None);
556 }
557}