Skip to main content

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}