Skip to main content

arcbox_computer_runtime/
sandbox.rs

1//! `SandboxManager` — orchestrates sandbox microVM lifecycle.
2//!
3//! A sandbox is a short-lived, strongly-isolated microVM decoupled from its
4//! workload: when the initial `cmd` process exits the sandbox transitions back
5//! to `Ready` rather than stopping, and continues accepting `Run` calls until
6//! an explicit `Stop`/`Remove` or TTL expiry.
7//!
8//! `create_sandbox` returns immediately with state `"starting"`.  The VM boots
9//! in a background task which broadcasts a `"ready"` event on success.
10
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex, RwLock};
14use std::time::Duration;
15
16use arcbox_vm_driver::net::{
17    AttachMode, GuestNetwork, HostIngress, NetworkIdentity, NetworkLease, NetworkMode,
18    NetworkPolicy, NetworkReconcile,
19};
20use arcbox_vm_driver::{
21    CheckpointFormat, CheckpointImage, CheckpointKind, DiskSource, IsolationSpec, NicSpec, Prepare,
22    PreparedVm, Staging, VmDriver, VmHandle, VmId,
23};
24use chrono::{DateTime, Utc};
25use tokio::sync::broadcast;
26use tracing::{debug, error, info, warn};
27use uuid::Uuid;
28
29use crate::agent::{ExecInputMsg, ExitStatus, OutputChunk, PortWait, StartCommand};
30use crate::agent::{GuestAgent, Readiness};
31use crate::config::RuntimeConfig;
32use crate::environment::NodeEnvironment;
33use crate::error::{Result, VmmError};
34use crate::lifecycle::actor::{
35    Command, ComputerActor, ComputerSeed, ComputerSnapshot, Deadlines, Mailbox, Seeded,
36};
37use crate::lifecycle::event::{Provision, RestoreOrigin};
38use crate::lifecycle::flows::{BootLaunch, ComputerFlows, ComputerServices, Launch, RestoreLaunch};
39use crate::lifecycle::runtime::{ComputerRuntime, Runtime};
40use crate::sandbox::reconcile::JournaledVmm;
41use crate::snapshot::{SnapshotCatalog, SnapshotMeta};
42use crate::snapshot_cow::{CowHandle, CowManager};
43use crate::template_catalog::TemplateCatalog;
44
45pub(crate) mod boot;
46mod checkpoint;
47pub(crate) mod cleanup;
48pub(crate) mod events;
49mod execution;
50mod files;
51mod lifecycle;
52pub(crate) mod pause;
53pub(crate) mod policy;
54pub(crate) mod pool;
55pub(crate) mod reconcile;
56pub(crate) mod record;
57pub(crate) mod spec;
58mod storage;
59mod templates;
60mod timers;
61pub(crate) mod types;
62pub(crate) mod warm;
63pub(crate) mod workload;
64
65pub use execution::{
66    ExecutionChannel, ExecutionOutput, ExecutionSnapshot, ExecutionSpec, StdinState,
67};
68pub use pause::reason as pause_reason;
69pub(crate) use spec::ROOTFS_DISK_ID;
70pub(crate) use types::NetworkAttachment;
71pub use types::{
72    CheckpointInfo, CheckpointSummary, IdleAction, LifecycleUpdate, RestoreSandboxSpec,
73    SandboxEvent, SandboxId, SandboxInfo, SandboxMountSpec, SandboxNetworkInfo, SandboxNetworkSpec,
74    SandboxSpec, SandboxState, SandboxSummary, TemplateWarmRef,
75};
76
77const EVENT_CHANNEL_CAPACITY: usize = 256;
78type ReconcileResult = std::result::Result<(), Arc<str>>;
79
80/// One computer as the registry holds it: its mailbox, its read snapshot, and
81/// the identity that tells this incarnation from a same-id replacement.
82///
83/// What replaced `Arc<Mutex<ComputerRuntime>>`. Every verb is a send on the
84/// mailbox and every read is a borrow of the snapshot, so neither the map
85/// lock nor a per-computer mutex is ever held across an await — the
86/// discipline `list_sandboxes` used to have to state.
87#[derive(Clone)]
88pub(crate) struct ComputerRef {
89    mailbox: Mailbox,
90    snapshot: tokio::sync::watch::Receiver<ComputerSnapshot>,
91    incarnation: Uuid,
92}
93
94/// The live computers, by id.
95pub(crate) type Computers = Arc<RwLock<HashMap<SandboxId, ComputerRef>>>;
96
97/// Manages the full lifecycle of multiple sandbox microVMs.
98pub struct SandboxManager {
99    computers: Computers,
100    records: Arc<record::SandboxRecordStore>,
101    /// What every computer's flows are built from — the driver, the guest
102    /// network, the agent factory, the record store, the catalogs and the
103    /// pool. One `Arc`, cloned into each actor.
104    services: Arc<ComputerServices>,
105    snapshots: Arc<SnapshotCatalog>,
106    /// Template catalog (CORE-107); see `templates.rs` for the manager surface.
107    templates: Arc<TemplateCatalog>,
108    config: Arc<RuntimeConfig>,
109    events: Arc<events::EventBus>,
110    cow_manager: Arc<CowManager>,
111    /// Pre-warmed restore slots (CORE-78); see `pool.rs`.
112    pool: Arc<pool::SlotPool>,
113    /// Warm template snapshot bookkeeping (CORE-77); see `warm.rs`.
114    warm: Arc<warm::WarmCache>,
115    /// Addressable executions (CORE-55); see `execution.rs`.
116    executions: Arc<execution::ExecutionRegistry>,
117    /// Publishes the startup reconciliation result. Lifecycle and read APIs
118    /// gate on it so callers never observe partial recovered state.
119    reconcile_done: tokio::sync::watch::Receiver<Option<ReconcileResult>>,
120    /// Whether the deadline timers may fire. `false` until
121    /// [`Self::into_shared`], which is the inertness contract a plain
122    /// [`Self::new`] manager has: unit tests of unrelated surfaces rely on it.
123    /// The actors hold receivers, so the flip reaches every one of them —
124    /// including the ones the startup sweep seeded before it was flipped.
125    timers_enabled: tokio::sync::watch::Sender<bool>,
126}
127
128impl SandboxManager {
129    /// Create a new manager from the given configuration, over the
130    /// environment-specific components the composer supplies.
131    ///
132    /// Fails with [`VmmError::Config`] when the environment's driver lacks a
133    /// capability every sandbox needs — `Prepare` (the flows spawn the VMM
134    /// ahead of the guest), `Staging` (every flow brings its computer's
135    /// files into the area the VMM can reach), `Vsock` (the guest agent is
136    /// reached over it), or whatever the agent factory's readiness gate
137    /// needs (`VsockListen` for the guest's dial-out) — or when its guest
138    /// network offers no `NetworkReconcile` (the cleanup-token protocol is
139    /// how a host releases the addresses a previous process held), so an
140    /// environment missing one is refused here instead of at the first boot
141    /// or the first cleanup ticket.
142    pub fn new(config: RuntimeConfig, environment: NodeEnvironment) -> Result<Self> {
143        let NodeEnvironment {
144            driver,
145            network,
146            agent,
147            cow_manager,
148        } = environment;
149        let capabilities = driver.capabilities();
150        for (missing, capability, need) in [
151            (
152                driver.prepare().is_none(),
153                "prepare",
154                "the boot and pool flows spawn the VMM ahead of the guest",
155            ),
156            (
157                !capabilities.staging,
158                "staging",
159                "every flow brings the files its computer boots from into the area the VMM \
160                 can reach, and pause takes the disk back out of it",
161            ),
162            // The last hard-wired transport assumption in this layer: every
163            // agent implementation the crate ships reaches its guest through
164            // the VM handle. A composition root that knows its Computers are
165            // not dialable at all is what will retire it.
166            (
167                !capabilities.vsock,
168                "vsock",
169                "the guest agent is reached over vsock",
170            ),
171            // What the readiness gate needs is the agent port's answer, not
172            // this layer's: only a dial-out has to be listened for before
173            // the guest starts.
174            (
175                matches!(agent.readiness(), Readiness::DialOut { .. })
176                    && !capabilities.vsock_listen,
177                "vsock_listen",
178                "the readiness gate is the guest's vsock dial-out",
179            ),
180        ] {
181            if missing {
182                return Err(VmmError::Config(format!(
183                    "VM driver `{}` has no {capability} capability; {need}",
184                    driver.name()
185                )));
186            }
187        }
188        let records = Arc::new(record::SandboxRecordStore::new(Path::new(
189            &config.firecracker.data_dir,
190        ))?);
191        drop(records.load_all()?);
192        if network.reconcile().is_none() {
193            return Err(VmmError::Config(
194                "guest network has no reconcile capability; the quarantine ledger is how a host \
195                 releases the addresses a previous process held"
196                    .into(),
197            ));
198        }
199        let snapshots = Arc::new(SnapshotCatalog::new(&config.firecracker.data_dir));
200        let templates = Arc::new(TemplateCatalog::new(&config.firecracker.data_dir));
201        let events = Arc::new(events::EventBus::new(EVENT_CHANNEL_CAPACITY));
202        let config = Arc::new(config);
203
204        // Sweep leftovers of a previous agent process (crash / respawn):
205        // orphaned VMM processes, TAPs, dm devices, chroots. Create and
206        // restore wait for this to finish (await_reconcile) so a re-created
207        // same-id sandbox can't have its deterministically-named resources torn
208        // down mid-flight. Only meaningful inside a tokio runtime; sync
209        // constructions (unit tests) have no previous instance to reconcile.
210        let (reconcile_tx, reconcile_done) = tokio::sync::watch::channel(None);
211        let executions = Arc::new(execution::ExecutionRegistry::default());
212        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
213        let pool = Arc::new(pool::SlotPool::default());
214        let (timers_enabled, timers_gate) = tokio::sync::watch::channel(false);
215        let services = Arc::new(ComputerServices {
216            driver,
217            network,
218            agents: agent,
219            config: Arc::clone(&config),
220            cow_manager: Arc::clone(&cow_manager),
221            records: Arc::clone(&records),
222            snapshots: Arc::clone(&snapshots),
223            events: Arc::clone(&events),
224            pool: Arc::clone(&pool),
225        });
226        if tokio::runtime::Handle::try_current().is_ok() {
227            let config = Arc::clone(&config);
228            let cow_manager = Arc::clone(&cow_manager);
229            let snapshots = Arc::clone(&snapshots);
230            let records = Arc::clone(&records);
231            let computers = Arc::clone(&computers);
232            let services = Arc::clone(&services);
233            let network = Arc::clone(&services.network);
234            let driver = Arc::clone(&services.driver);
235            tokio::spawn(async move {
236                let result = async {
237                    let mut swept = reconcile::sweep_orphans(
238                        &config,
239                        driver.as_ref(),
240                        &*network,
241                        &cow_manager,
242                        &snapshots,
243                        &records,
244                    )
245                    .await?;
246                    let mut runtime = swept.take_runtime();
247                    let mut inactive = Vec::new();
248                    let normalized = reconcile::normalize_durable_records(
249                        &records,
250                        Path::new(&config.firecracker.data_dir),
251                        Some(&mut runtime),
252                        &mut inactive,
253                    );
254                    // A reclaimed sandbox is in exactly one of these two by
255                    // now — never claimed, or built into an instance — and
256                    // dropping either would kill its guest while leaving its
257                    // lease and template refcount held. Release both before
258                    // reporting the refusal.
259                    reconcile::release_unclaimed(&mut runtime, &*network, &cow_manager).await;
260                    if let Err(error) = normalized {
261                        reconcile::release_instances(&mut inactive, &*network, &cow_manager).await;
262                        return Err(error);
263                    }
264                    // Publish before anything else can fail. The reclaimed
265                    // sandboxes' only handles live in these computers, so a
266                    // later error — a runtime directory that will not delete,
267                    // say — would otherwise un-reclaim every guest by
268                    // dropping them, over something none of them caused.
269                    reconcile::seed_computers(inactive, &computers, &services, &timers_gate);
270                    reconcile::finalize_sweep(swept).await?;
271                    reconcile_capability(&*network).replay_complete();
272                    Ok::<_, VmmError>(())
273                }
274                .await
275                .map_err(|error| Arc::<str>::from(error.to_string()));
276                let _ = reconcile_tx.send(Some(result));
277            });
278            // Executions die with their sandbox; purge on terminal events so
279            // every teardown path (stop / remove / TTL / boot failure) is
280            // covered without threading the registry through each of them.
281            execution::spawn_teardown_purge(Arc::clone(&executions), events.subscribe());
282        } else {
283            let mut inactive = Vec::new();
284            reconcile::normalize_durable_records(
285                &records,
286                Path::new(&config.firecracker.data_dir),
287                None,
288                &mut inactive,
289            )?;
290            reconcile_capability(&*services.network).replay_complete();
291            reconcile::seed_computers(inactive, &computers, &services, &timers_enabled.subscribe());
292            let _ = reconcile_tx.send(Some(Ok(())));
293        }
294
295        Ok(Self {
296            computers,
297            records,
298            services,
299            snapshots,
300            templates,
301            config,
302            events,
303            cow_manager,
304            pool,
305            warm: Arc::new(warm::WarmCache::default()),
306            executions,
307            reconcile_done,
308            timers_enabled,
309        })
310    }
311
312    /// Wrap the manager in an `Arc` and let its computers' deadline timers
313    /// fire.
314    ///
315    /// Production embedders (the guest agent's `SandboxService`) must use
316    /// this; a plain [`Self::new`] manager never fires idle timers (unit
317    /// tests exercising unrelated surfaces rely on that inertness).
318    ///
319    /// One `watch` flip, which every actor is holding a receiver for — the
320    /// weak self-handle a detached expiry task needed is gone with the
321    /// detached task.
322    #[must_use]
323    pub fn into_shared(self) -> Arc<Self> {
324        // `send_replace`, not `send`: a manager with no computers yet has no
325        // receivers, and `send` would refuse — leaving every actor spawned
326        // afterwards reading a gate that was never opened.
327        self.timers_enabled.send_replace(true);
328        Arc::new(self)
329    }
330
331    /// What every actor this manager spawns is built from.
332    pub(super) fn spawn_context(
333        &self,
334    ) -> (Arc<ComputerServices>, tokio::sync::watch::Receiver<bool>) {
335        (Arc::clone(&self.services), self.timers_enabled.subscribe())
336    }
337
338    /// This computer's registry entry.
339    pub(super) fn computer(&self, id: &SandboxId) -> Result<ComputerRef> {
340        self.check_reconcile()?;
341        self.computers
342            .read()
343            .unwrap()
344            .get(id)
345            .cloned()
346            .ok_or_else(|| VmmError::NotFound(id.clone()))
347    }
348
349    /// This computer's mailbox.
350    pub(super) fn mailbox(&self, id: &SandboxId) -> Result<Mailbox> {
351        Ok(self.computer(id)?.mailbox)
352    }
353
354    /// What a read of this computer sees, without touching its mailbox.
355    pub(super) fn snapshot(&self, id: &SandboxId) -> Result<ComputerSnapshot> {
356        Ok(self.computer(id)?.snapshot.borrow().clone())
357    }
358
359    /// Wait until the startup orphan sweep has completed.
360    ///
361    /// The sweep tears down resources by deterministic per-id names
362    /// (`arcbox-snap-{id}`, the id's TAP, `arcbox-cow-{id}`) and adjusts the
363    /// shared template refcount. A create/restore that runs concurrently with
364    /// it — e.g. a client re-creating the same id right after an agent restart
365    /// — would have its live device destroyed or a live template detached.
366    /// Gating create/restore on the sweep removes that class entirely; it runs
367    /// once, so after completion this returns immediately.
368    pub(super) async fn await_reconcile(&self) -> Result<()> {
369        let mut rx = self.reconcile_done.clone();
370        let result = rx
371            .wait_for(Option::is_some)
372            .await
373            .map_err(|error| VmmError::Other(format!("sandbox reconciliation stopped: {error}")))?
374            .clone()
375            .expect("wait_for returned only after reconciliation completed");
376        result.map_err(|error| {
377            VmmError::Other(format!(
378                "sandbox durable-state reconciliation failed: {error}"
379            ))
380        })
381    }
382
383    fn check_reconcile(&self) -> Result<()> {
384        let result = self.reconcile_done.borrow().clone();
385        match result {
386            None => Err(VmmError::Other(
387                "sandbox durable-state reconciliation is still running".into(),
388            )),
389            Some(Ok(())) => Ok(()),
390            Some(Err(error)) => Err(VmmError::Other(format!(
391                "sandbox durable-state reconciliation failed: {error}"
392            ))),
393        }
394    }
395
396    /// Return sandbox IDs whose inactive network allocation still awaits host
397    /// forwarding cleanup.
398    pub async fn pending_network_cleanups(&self) -> Result<Vec<(String, String)>> {
399        self.await_reconcile().await?;
400        Ok(self
401            .reconcile_network()
402            .pending_cleanups()
403            .await
404            .map_err(VmmError::from)?
405            .into_iter()
406            .map(|(vm, token)| (vm.as_str().to_owned(), token))
407            .collect())
408    }
409
410    /// Validate one exact pending host-cleanup ticket.
411    ///
412    /// Returns the lease that generation held: the host's forwarding rules
413    /// for it are keyed by its address, and after an agent restart the
414    /// quarantine ledger is the only place that address survives.
415    pub async fn validate_network_cleanup(&self, id: &str, token: &str) -> Result<NetworkLease> {
416        self.await_reconcile().await?;
417        Ok(self
418            .reconcile_network()
419            .validate_cleanup(&VmId::new(id)?, token)
420            .await?)
421    }
422
423    /// Recycle one exact inactive sandbox generation after forwarding cleanup.
424    pub async fn finalize_network_cleanup(&self, id: &str, token: &str) -> Result<()> {
425        self.await_reconcile().await?;
426        Ok(self
427            .reconcile_network()
428            .finalize_cleanup(&VmId::new(id)?, token)
429            .await?)
430    }
431
432    /// Wait until the current agent generation's startup cleanup is complete.
433    pub async fn wait_startup_cleanup_complete(&self) {
434        self.reconcile_network()
435            .wait_startup_cleanup_complete()
436            .await;
437    }
438
439    /// Opaque ticket for the host cleanup pass required after agent startup.
440    pub async fn startup_cleanup_token(&self) -> Result<Option<String>> {
441        self.await_reconcile().await?;
442        Ok(self.reconcile_network().startup_cleanup_token().await)
443    }
444
445    /// Validate the current process-generation startup cleanup ticket.
446    pub async fn validate_startup_cleanup(&self, token: &str) -> Result<()> {
447        self.await_reconcile().await?;
448        Ok(self
449            .reconcile_network()
450            .validate_startup_cleanup(token)
451            .await?)
452    }
453
454    /// Release the startup gate once host listeners and legacy DNAT are gone.
455    pub async fn finalize_startup_cleanup(&self, token: &str) -> Result<()> {
456        self.await_reconcile().await?;
457        Ok(self
458            .reconcile_network()
459            .finalize_startup_cleanup(token)
460            .await?)
461    }
462
463    /// The guest network's cleanup protocol.
464    pub(super) fn reconcile_network(&self) -> &dyn NetworkReconcile {
465        reconcile_capability(&*self.services.network)
466    }
467
468    /// Return the active generation's network identity: the external pool IP
469    /// (what DNS, expose, and the API report), its opaque cleanup token, and
470    /// how expose DNAT must target the sandbox (CORE-81/CORE-83).
471    ///
472    /// No startup-cleanup gate here, unlike every method above. An identity
473    /// needs a live lease; a lease comes only from `reserve`, which the
474    /// guest network refuses while the same startup barrier is closed; and
475    /// both callers in the guest agent await `wait_startup_cleanup_complete`
476    /// before asking. The gate this used to take could therefore only ever
477    /// have fired for a sandbox that has no lease to report anyway, and it
478    /// has no non-blocking form on the port.
479    pub fn sandbox_network_identity(&self, id: &str) -> Result<SandboxNetworkIdentity> {
480        let snapshot = self.snapshot(&id.to_owned())?;
481        let lease = snapshot
482            .lease
483            .as_ref()
484            .ok_or_else(|| VmmError::WrongState {
485                id: id.to_owned(),
486                expected: "sandbox with an active network allocation".into(),
487                actual: snapshot.state.to_string(),
488            })?;
489        Ok(SandboxNetworkIdentity {
490            ip: lease.ipv4()?,
491            cleanup_token: lease.cleanup_token.clone(),
492            expose: self.services.network.host_ingress(lease)?,
493        })
494    }
495}
496
497/// What the sandbox stack needs from a [`NetworkLease`] beyond its fields.
498///
499/// The port's lease carries an [`IpAddr`](std::net::IpAddr) because a guest
500/// network need not be IPv4 (the platform's node dataplane is IPv6-only),
501/// while everything downstream of *this* manager — the pool address it
502/// reports, the netmask a legacy guest is reconfigured with, the crash
503/// journal's on-disk shape — is v4. The narrowing happens here, once, and
504/// says so when a lease cannot answer.
505pub(super) trait LeaseExt {
506    /// The lease's address.
507    fn ipv4(&self) -> Result<std::net::Ipv4Addr>;
508    /// The gateway the guest routes through.
509    fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr>;
510}
511
512impl LeaseExt for NetworkLease {
513    fn ipv4(&self) -> Result<std::net::Ipv4Addr> {
514        ipv4(self.ip)
515    }
516
517    fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr> {
518        ipv4(self.gateway)
519    }
520}
521
522/// One address of a lease or identity, narrowed. See [`LeaseExt`].
523pub(super) fn ipv4(address: std::net::IpAddr) -> Result<std::net::Ipv4Addr> {
524    match address {
525        std::net::IpAddr::V4(v4) => Ok(v4),
526        std::net::IpAddr::V6(v6) => Err(VmmError::Network(format!(
527            "the guest network offered {v6}; this manager's sandboxes are IPv4-only"
528        ))),
529    }
530}
531
532/// The subnet mask `prefix_len` describes, clamped at /32 so an
533/// out-of-range prefix cannot overflow the shift.
534pub(super) fn netmask(prefix_len: u8) -> std::net::Ipv4Addr {
535    let prefix = prefix_len.min(32);
536    if prefix == 0 {
537        std::net::Ipv4Addr::UNSPECIFIED
538    } else {
539        std::net::Ipv4Addr::from(!0u32 << (32 - prefix))
540    }
541}
542
543/// The connectivity every sandbox gets: egress through the host's address,
544/// which is what the System VM's netfilter provides for the pool.
545pub(super) const fn sandbox_network_policy() -> NetworkPolicy {
546    NetworkPolicy {
547        mode: NetworkMode::Nat,
548    }
549}
550
551/// The mode a guest's interface is activated in, which is also the mode
552/// [`GuestNetwork::identity`] must be read under: a fresh boot and every
553/// invariant-snapshot restore take the fixed identity, and only checkpoints
554/// taken before it existed carry the pool address on the interface itself.
555pub(super) const fn attach_mode(net_invariant: bool) -> AttachMode {
556    if net_invariant {
557        AttachMode::Invariant
558    } else {
559        AttachMode::LegacySnapshot
560    }
561}
562
563/// A sandbox's network identity as seen by forwarding and DNS consumers.
564#[derive(Debug, Clone, PartialEq, Eq)]
565pub struct SandboxNetworkIdentity {
566    /// External pool IP — the address the rest of the system keeps using.
567    pub ip: std::net::Ipv4Addr,
568    /// Opaque generation token carried through host cleanup finalization.
569    pub cleanup_token: String,
570    /// How expose DNAT must target this sandbox, decided by the datapath
571    /// actually applied to its TAP (CORE-81/CORE-83).
572    ///
573    /// The guest network's own answer, passed through unchanged: rendering
574    /// it as forwarding rules is the composing host's job, and only that
575    /// host knows which network it built.
576    pub expose: HostIngress,
577}
578
579/// The guest network's cleanup protocol, which [`SandboxManager::new`]
580/// requires — the quarantine ledger gates every address the pool hands
581/// out, and the startup sweep gates the pool itself.
582pub(super) fn reconcile_capability(network: &dyn GuestNetwork) -> &dyn NetworkReconcile {
583    network
584        .reconcile()
585        .expect("SandboxManager::new requires the guest network's reconcile capability")
586}
587
588/// The driver's `Prepare` capability, which [`SandboxManager::new`]
589/// requires — the boot, pool, and restore flows all spawn the VMM before
590/// there is a guest to run on it.
591pub(crate) fn prepare_capability(driver: &dyn VmDriver) -> &dyn Prepare {
592    driver
593        .prepare()
594        .expect("SandboxManager::new requires the driver's Prepare capability")
595}
596
597/// A grip's `Staging` capability, which [`SandboxManager::new`] requires —
598/// every flow that puts a guest on a VMM first brings that guest's files
599/// into the area the VMM can reach, and pause takes its disk back out of
600/// it.
601///
602/// Takes the accessor's answer rather than the grip it came from: both
603/// grips on a VM have one and they name the same area, so a computer
604/// booted here asks its [`PreparedVm`] and one this process adopted asks
605/// its [`VmHandle`](arcbox_vm_driver::VmHandle).
606pub(crate) fn staging_capability(staging: Option<&dyn Staging>) -> &dyn Staging {
607    staging.expect("SandboxManager::new requires the driver's Staging capability")
608}
609
610/// The VMM's pid as the crash journal records it: what a restart sweep
611/// kills before tearing the sandbox's other resources down.
612pub(crate) fn journaled_pid(prepared: &dyn PreparedVm) -> Option<i32> {
613    prepared
614        .record()
615        .process
616        .and_then(|process| i32::try_from(process.pid).ok())
617}
618
619/// The VMM as the crash journal records it — its API socket and its jail —
620/// which is what lets the next process adopt this VM instead of killing it.
621///
622/// The driver knows both because it spawned the VMM that way. Nothing else
623/// can work either out afterwards: see
624/// [`SandboxStateRecord::api_socket`](crate::sandbox::reconcile::SandboxStateRecord::api_socket)
625/// and [`SandboxStateRecord::jail`](crate::sandbox::reconcile::SandboxStateRecord::jail).
626pub(crate) fn journaled_vmm(prepared: &dyn PreparedVm) -> Option<JournaledVmm> {
627    prepared.record().process.map(|process| JournaledVmm {
628        api_socket: process.api_socket,
629        jail: process.jail.map(Into::into),
630    })
631}
632
633/// The isolation every sandbox VMM runs under: the jailer's, when one is
634/// configured; none otherwise (direct mode).
635pub(crate) fn isolation_spec(config: &RuntimeConfig) -> Result<IsolationSpec> {
636    config
637        .firecracker
638        .jailer
639        .as_ref()
640        .map_or(Ok(IsolationSpec::None), IsolationSpec::try_from)
641}
642
643/// Where a paused computer's retained disk overlay lives: a function of the
644/// data dir and the id, which is why the flows that keep, rename, look for
645/// and delete it can all say it the same way.
646pub(crate) fn preserved_cow_file(config: &RuntimeConfig, id: &str) -> PathBuf {
647    PathBuf::from(&config.firecracker.data_dir)
648        .join("cow")
649        .join(format!("arcbox-cow-{id}.img"))
650}
651
652/// A checkpoint as the catalog holds it, which is what
653/// [`Staging::stage_checkpoint`] takes and what the driver reads back.
654///
655/// The catalog writes one directory per checkpoint and puts `vmstate` and
656/// `mem` in it, so the image is that directory — read off the meta's own
657/// `vmstate_path` rather than recomputed from the catalog's layout, which
658/// this layer does not own. Its name is what the driver stages the
659/// checkpoint under, and a restore reproduces that name from the same
660/// image, so the two cannot drift. The format is the one the catalog
661/// recorded at capture; legacy entries default to the Firecracker format.
662pub(crate) fn catalogued_checkpoint(meta: &SnapshotMeta) -> Result<CheckpointImage> {
663    let dir = meta.vmstate_path.parent().ok_or_else(|| {
664        VmmError::Snapshot(format!(
665            "checkpoint {} records a vmstate at {}, which is in no directory",
666            meta.id,
667            meta.vmstate_path.display()
668        ))
669    })?;
670    Ok(CheckpointImage {
671        dir: dir.to_path_buf(),
672        format: CheckpointFormat::new(&meta.format),
673        kind: CheckpointKind::Full,
674    })
675}
676
677/// Validate a caller-supplied sandbox or snapshot id.
678///
679/// Ids become filesystem path components and dm/TAP name fragments, so they are
680/// restricted to `[A-Za-z0-9_-]`. This rejects path traversal (`/`, `\`, `..`),
681/// NUL and whitespace. The narrower rule a VMM imposes on the id it runs under
682/// is *not* checked here — that is [`VmId`]'s, applied by
683/// [`validate_new_sandbox_id`] where an id becomes a VM identity.
684///
685/// Deliberately NO length cap here: this also runs against persisted
686/// records, and what one legacy over-long id would cost differs by call
687/// site. On the sweep path it costs that record its reconciliation — the
688/// journal is skipped rather than acted on, and every resource it names
689/// is held (`reconcile::sweep_orphans`). On a record *load* it costs
690/// every record theirs: [`SandboxRecordStore::load_all`] validates each
691/// id and propagates, so one rejection aborts the whole startup read,
692/// which is the stronger reason the cap is absent. It also runs against
693/// snapshot / execution ids that never become a VM identity at all. Both
694/// limits the driver imposes are enforced only where a sandbox id enters
695/// the system: [`validate_new_sandbox_id`].
696///
697/// The gap between this alphabet and [`VmId`]'s is not academic: `_` is
698/// legal here and is not a `VmId`, so every record a pre-#680 process
699/// wrote for an `inst_…` sandbox is one this process cannot name — which
700/// is exactly what the sweep and the quarantine ledger read back.
701pub(super) fn validate_id(kind: &str, id: &str) -> Result<()> {
702    if id.is_empty() {
703        return Err(VmmError::Config(format!("{kind} must not be empty")));
704    }
705    if !id
706        .bytes()
707        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
708    {
709        return Err(VmmError::Config(format!(
710            "invalid {kind} {id:?}: only ASCII letters, digits, '-' and '_' are allowed"
711        )));
712    }
713    Ok(())
714}
715
716/// Validate a sandbox id at request ingress (create / restore), where it
717/// becomes the identity the VM runs under.
718///
719/// Both halves belong to the driver, and neither is restated here. The
720/// alphabet and the 64-byte ceiling are [`VmId`]'s: an id a VMM will not
721/// take is refused by construction, and Firecracker — which `panic!`s on
722/// its own `--id` in *both* jailed and direct mode — is what that rule is
723/// cut to. The length budget is [`VmDriver::id_budget`], measured on the
724/// layout the configured driver will actually lay down, so a longer chroot
725/// base or binary name tightens it rather than silently reintroducing the
726/// failure it exists to prevent.
727///
728/// Refusing here is the whole point: past this call the id is validated
729/// again inside the async boot task, where the refusal arrives with
730/// nothing tying it back to the request that caused it — an
731/// `InvalidInstanceId` under the jailer, an abort under bare Firecracker,
732/// and an opaque "timed out waiting for socket" when it is the length
733/// (the driver's readiness probe is a `connect()`, which ENAMETOOLONGs on
734/// every attempt while the VMM is up and bound inside its chroot; caught
735/// by the CORE-107 prewarm e2e, whose 51-char builder id overflowed the
736/// stock budget).
737pub(super) fn validate_new_sandbox_id(
738    id: &str,
739    driver: &dyn VmDriver,
740    config: &RuntimeConfig,
741) -> Result<()> {
742    VmId::new(id)?;
743    if let Some(budget) = driver.id_budget(&isolation_spec(config)?)
744        && id.len() > budget
745    {
746        return Err(VmmError::Config(format!(
747            "invalid sandbox id {id:?}: at most {budget} characters under this \
748             driver's layout (its sockets must fit the AF_UNIX path limit)"
749        )));
750    }
751    Ok(())
752}
753
754/// Atomically claim `id` in the registry and stand up its actor.
755///
756/// Create and restore both derive per-computer resources deterministically
757/// from the id (CoW file `arcbox-cow-{id}`, dm device `arcbox-snap-{id}`, TAP
758/// name). A check-then-insert with those resources set up in between is a
759/// TOCTOU: two concurrent restores to the same id both pass the check, then
760/// the second `create_sparse_file` truncates the file the first's loop device
761/// is backing, corrupting the winner's rootfs. Claiming the id up front
762/// (check + insert under one write lock) makes the loser fail fast with
763/// `AlreadyExists` before it touches any shared resource.
764///
765/// The actor is **not** spawned here. Its seed needs the durable generation
766/// and the deadlines the record computes, and both are resolved after the
767/// claim; the mailbox and the read snapshot exist from the claim on, so a
768/// caller that finds the entry finds a usable computer either way. The claim
769/// is released on drop unless [`ActorReservation::spawn`] is called, so every
770/// error path unwinds it.
771pub(crate) fn reserve_actor(
772    computers: &Computers,
773    id: &SandboxId,
774    runtime: ComputerRuntime,
775) -> Result<ActorReservation> {
776    let mut map = computers.write().unwrap();
777    if map.contains_key(id) {
778        return Err(VmmError::AlreadyExists(id.clone()));
779    }
780    let incarnation = Uuid::new_v4();
781    let (mailbox_tx, commands) = tokio::sync::mpsc::unbounded_channel();
782    let runtime = Arc::new(Mutex::new(runtime));
783    let (snapshot_tx, snapshot) = tokio::sync::watch::channel(ComputerSnapshot::project(
784        &runtime.lock().unwrap(),
785        SandboxState::Starting,
786        Deadlines::default(),
787    ));
788    map.insert(
789        id.clone(),
790        ComputerRef {
791            mailbox: Mailbox::new(mailbox_tx.clone()),
792            snapshot,
793            incarnation,
794        },
795    );
796    drop(map);
797    Ok(ActorReservation {
798        computers: Arc::clone(computers),
799        id: id.clone(),
800        incarnation,
801        runtime,
802        mailbox: Mailbox::new(mailbox_tx),
803        commands: Some(commands),
804        snapshot_tx: Some(snapshot_tx),
805    })
806}
807
808/// A claimed id, its runtime state, and the channels its actor will run on.
809///
810/// Dropping it without [`Self::spawn`] releases the claim; the mailbox's last
811/// sender goes with it, so an actor already running would stop too.
812pub(crate) struct ActorReservation {
813    computers: Computers,
814    id: SandboxId,
815    incarnation: Uuid,
816    runtime: Runtime,
817    mailbox: Mailbox,
818    commands: Option<tokio::sync::mpsc::UnboundedReceiver<Command>>,
819    snapshot_tx: Option<tokio::sync::watch::Sender<ComputerSnapshot>>,
820}
821
822impl ActorReservation {
823    /// The claimed computer's runtime state, for the caller to populate
824    /// before its actor starts.
825    pub(crate) fn runtime(&self) -> &Runtime {
826        &self.runtime
827    }
828
829    /// Start the actor and keep the claim.
830    pub(crate) fn spawn(mut self, seed: ActorSpawn) -> Mailbox {
831        let ActorSpawn {
832            services,
833            timers_enabled,
834            generation,
835            deadlines,
836            launch,
837            seeded,
838        } = seed;
839        let mailbox = self.mailbox.clone();
840        let flows = Arc::new(ComputerFlows::new(
841            self.id.clone(),
842            Arc::clone(&self.runtime),
843            Arc::clone(&services),
844            &mailbox,
845            launch,
846        ));
847        let unregister = {
848            let computers = Arc::clone(&self.computers);
849            let id = self.id.clone();
850            let incarnation = self.incarnation;
851            Arc::new(move || forget_computer(&computers, &id, incarnation))
852        };
853        let vm_dir = self.runtime.lock().unwrap().vm_dir.clone();
854        let actor = ComputerActor::new(
855            ComputerSeed {
856                id: self.id.clone(),
857                runtime: Arc::clone(&self.runtime),
858                unregister,
859                generation,
860                vm_dir,
861                records: Arc::clone(&services.records),
862                events: Arc::clone(&services.events),
863                tasks: flows,
864                deadlines,
865                timers_enabled,
866                seeded,
867            },
868            self.commands.take().expect("spawned once"),
869            self.snapshot_tx.take().expect("spawned once"),
870        );
871        // Constructors are synchronous and may run outside a runtime (unit
872        // tests of unrelated surfaces); such a manager has no actors to run
873        // and never provisions one.
874        if tokio::runtime::Handle::try_current().is_ok() {
875            tokio::spawn(actor.run());
876        }
877        mailbox
878    }
879}
880
881impl Drop for ActorReservation {
882    fn drop(&mut self) {
883        if self.commands.is_some() {
884            forget_computer(&self.computers, &self.id, self.incarnation);
885        }
886    }
887}
888
889/// Everything an actor needs that the reservation does not already hold.
890///
891/// Assembled from the manager's own pieces rather than from `&SandboxManager`
892/// so the startup sweep can seed actors too: it runs in a task spawned from
893/// the constructor, before the manager it belongs to exists.
894pub(crate) struct ActorSpawn {
895    pub(crate) services: Arc<ComputerServices>,
896    pub(crate) timers_enabled: tokio::sync::watch::Receiver<bool>,
897    pub(crate) generation: Option<Uuid>,
898    pub(crate) deadlines: Deadlines,
899    pub(crate) launch: Launch,
900    pub(crate) seeded: Seeded,
901}
902
903/// Drop `id`'s entry when it is still `incarnation`'s.
904///
905/// The identity check replaces the `Arc::ptr_eq` guard the instance map
906/// needed: a computer removed and re-created under the same id (deterministic
907/// caller-supplied ids make this common) installs a fresh entry, and the
908/// departing actor must not evict it.
909fn forget_computer(computers: &Computers, id: &SandboxId, incarnation: Uuid) {
910    let mut map = computers.write().unwrap();
911    if map
912        .get(id)
913        .is_some_and(|current| current.incarnation == incarnation)
914    {
915        map.remove(id);
916    }
917}
918
919/// Whether `id`'s entry is still this exact incarnation's — the inverse of
920/// [`forget_computer`], and the question an unanswerable mailbox raises.
921///
922/// Every ending unregisters: the actor's own loop tail whatever stopped it, the
923/// record it forgets on a removal, and [`ActorReservation::drop`] for a create
924/// that unwound before spawning one. A re-created id installs a fresh
925/// incarnation over the departed one, so this is `false` there too. What is
926/// left is an incarnation *still here* whose mailbox no longer answers: an
927/// actor whose task died without finishing, and the one case where a
928/// computer's resources outlive the thing that was managing them.
929///
930/// Reads nothing the actor owns, so it stays answerable for a computer whose
931/// runtime mutex a panic has poisoned.
932fn still_registered(computers: &Computers, id: &SandboxId, incarnation: Uuid) -> bool {
933    computers
934        .read()
935        .unwrap()
936        .get(id)
937        .is_some_and(|current| current.incarnation == incarnation)
938}
939
940#[cfg(test)]
941mod tests {
942    use arcbox_vm_driver::testkit::FakeDriver;
943
944    use super::*;
945
946    fn placeholder(id: &str) -> ComputerRuntime {
947        ComputerRuntime::new(
948            id.to_owned(),
949            SandboxSpec::default(),
950            None,
951            PathBuf::from("/tmp/x"),
952        )
953    }
954
955    /// The manager refuses, at construction, a driver that cannot spawn the
956    /// VMM ahead of a boot, dial the guest, or take its readiness dial-out:
957    /// every sandbox flow needs all three, so a driver without one would
958    /// fail at the first create instead.
959    #[test]
960    fn construction_requires_the_drivers_prepare_and_vsock_capabilities() {
961        use arcbox_vm_driver::testkit::FakeDriver;
962        use arcbox_vm_driver::{DriverCapabilities, VmDriver as _};
963
964        let dir = tempfile::tempdir().unwrap();
965        let mut config = RuntimeConfig::default();
966        config.firecracker.data_dir = dir.path().to_string_lossy().into_owned();
967        // The vm-proto factory, not the agent fake: only a dial-out
968        // readiness makes the driver's `vsock_listen` a requirement, which
969        // is one of the three refusals below.
970        let environment = |driver: FakeDriver| NodeEnvironment {
971            driver: Arc::new(driver),
972            agent: Arc::new(crate::agent::VmProtoAgentFactory::default()),
973            ..crate::testkit::fake_environment(&config).unwrap()
974        };
975        let all = FakeDriver::new().capabilities();
976
977        for (name, capabilities) in [
978            (
979                "prepare",
980                DriverCapabilities {
981                    prepare: false,
982                    ..all.clone()
983                },
984            ),
985            (
986                "vsock",
987                DriverCapabilities {
988                    vsock: false,
989                    ..all.clone()
990                },
991            ),
992            (
993                "vsock_listen",
994                DriverCapabilities {
995                    vsock_listen: false,
996                    ..all
997                },
998            ),
999        ] {
1000            let driver = FakeDriver::builder().capabilities(capabilities).build();
1001            let error = SandboxManager::new(config.clone(), environment(driver))
1002                .err()
1003                .unwrap_or_else(|| panic!("a driver without {name} is refused"));
1004            assert!(matches!(error, VmmError::Config(_)), "{error}");
1005            assert!(error.to_string().contains(name), "{error}");
1006        }
1007
1008        let manager = SandboxManager::new(config.clone(), environment(FakeDriver::new()))
1009            .expect("a driver with every needed capability is accepted");
1010        assert_eq!(manager.services.driver.name(), "fake");
1011    }
1012
1013    #[test]
1014    fn a_claimed_id_rejects_a_concurrent_duplicate() {
1015        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1016        let first = reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")).unwrap();
1017        // A second claim of the same id must fail while the first is live —
1018        // before either touches the per-id resources they both derive.
1019        assert!(matches!(
1020            reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")),
1021            Err(VmmError::AlreadyExists(_))
1022        ));
1023        drop(first);
1024        assert!(!computers.read().unwrap().contains_key("dup"));
1025    }
1026
1027    /// A jailed config, which is what makes a driver answer with a budget
1028    /// at all — the layout behind that answer is the driver's business.
1029    fn jailed_config() -> RuntimeConfig {
1030        let mut config = RuntimeConfig::default();
1031        config.firecracker.jailer = Some(crate::config::JailerConfig {
1032            uid: 0,
1033            gid: 0,
1034            chroot_base_dir: Some("/srv/jailer".into()),
1035            netns: None,
1036            new_pid_ns: false,
1037            cgroup_version: None,
1038            parent_cgroup: None,
1039        });
1040        config
1041    }
1042
1043    /// The id shape the platform node actually mints, in the rendering a
1044    /// driver can run: the control plane's `inst_<uuid v7>` with the `_`
1045    /// the VMM refuses turned into a `-`.
1046    const CONTROL_PLANE_ID: &str = "inst-019e409e-7546-7a3e-8b2c-1f2e3d4c5b6a";
1047
1048    #[test]
1049    fn validate_id_accepts_safe_ids_and_rejects_traversal() {
1050        for ok in ["sandbox1", "a-b_c", "0f3e9d16-1234", "A_B-9"] {
1051            assert!(validate_id("id", ok).is_ok(), "{ok} should be valid");
1052        }
1053        // The generic validator stays uncapped and keeps the underscore: it
1054        // also runs against persisted records and against snapshot /
1055        // execution ids that never become a VM identity, where a legacy id
1056        // must not become fatal.
1057        assert!(validate_id("id", &"a".repeat(60)).is_ok());
1058        assert!(validate_id("id", "inst_019e409e-7546").is_ok());
1059        for bad in ["", "..", ".", "a/b", "a\\b", "a b", "a.b", "a\0b", "../etc"] {
1060            assert!(
1061                validate_id("id", bad).is_err(),
1062                "{bad:?} should be rejected"
1063            );
1064        }
1065    }
1066
1067    /// Ingress holds a new id to whatever budget the *configured* driver
1068    /// reports for the isolation it will run under — this layer keeps no
1069    /// copy of a number. Anything past it must fail fast here instead of
1070    /// surfacing as a socket connect timeout.
1071    ///
1072    /// Whether a given layout leaves room for the ids an ArcBox node mints
1073    /// is the adapter's own question, answered against the deployed
1074    /// constants in `arcbox-fc-driver`
1075    /// (`the_deployed_jail_layout_admits_the_ids_a_node_mints`).
1076    #[test]
1077    fn a_new_sandbox_id_is_held_to_the_drivers_own_budget() {
1078        let mut config = jailed_config();
1079        let budget = CONTROL_PLANE_ID.len();
1080        let driver = FakeDriver::builder().jailed_id_budget(budget).build();
1081        assert!(validate_new_sandbox_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
1082        assert!(validate_new_sandbox_id(&"a".repeat(budget), &driver, &config).is_ok());
1083        assert!(validate_new_sandbox_id(&"a".repeat(budget + 1), &driver, &config).is_err());
1084
1085        // A tighter budget refuses what the looser one took, rather than
1086        // silently reintroducing the connect timeout.
1087        let tighter = FakeDriver::builder().jailed_id_budget(budget - 1).build();
1088        assert!(validate_new_sandbox_id(CONTROL_PLANE_ID, &tighter, &config).is_err());
1089
1090        // A driver that reports no budget for this isolation bounds
1091        // nothing: only `VmId`'s own 64-byte ceiling is left.
1092        config.firecracker.jailer = None;
1093        assert!(validate_new_sandbox_id(&"a".repeat(budget + 1), &tighter, &config).is_ok());
1094    }
1095
1096    /// CORE-140. Firecracker validates the `--id` it is handed and refuses it
1097    /// by panicking — under the jailer *and* in direct mode, where the same
1098    /// flag is passed straight to the VMM. So the refusal cannot be gated on
1099    /// a jailer being configured: it belongs to the id itself.
1100    #[test]
1101    fn an_underscored_id_is_refused_at_ingress_in_every_spawn_mode() {
1102        let mut config = jailed_config();
1103        let driver = FakeDriver::builder()
1104            .jailed_id_budget(CONTROL_PLANE_ID.len())
1105            .build();
1106        for _ in 0..2 {
1107            let error =
1108                validate_new_sandbox_id(&CONTROL_PLANE_ID.replacen('-', "_", 1), &driver, &config)
1109                    .expect_err("firecracker would refuse to run under this id");
1110            assert!(
1111                matches!(&error, VmmError::Config(message) if message.contains('_')),
1112                "the error should name the offending character, got {error}"
1113            );
1114            assert!(validate_new_sandbox_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
1115            // Direct mode passes the same `--id`, so it is refused there too.
1116            config.firecracker.jailer = None;
1117        }
1118    }
1119
1120    #[test]
1121    fn a_dropped_claim_frees_the_id() {
1122        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1123        {
1124            let _claim = reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).unwrap();
1125            assert!(computers.read().unwrap().contains_key("tmp"));
1126            // never spawned → dropped here
1127        }
1128        assert!(!computers.read().unwrap().contains_key("tmp"));
1129        // The id is free to claim again after an unwound restore.
1130        assert!(reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).is_ok());
1131    }
1132
1133    /// A departing actor unregisters *its own* entry and no other.
1134    ///
1135    /// The `Arc::ptr_eq` guard the instance map needed, as an incarnation:
1136    /// a computer removed and re-created under the same id (deterministic
1137    /// caller-supplied ids make this common) installs a fresh entry, and the
1138    /// one on its way out must not evict it.
1139    #[test]
1140    fn a_departing_actor_does_not_evict_its_replacement() {
1141        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1142        let departing = reserve_actor(&computers, &"same".to_owned(), placeholder("same")).unwrap();
1143        let incarnation = departing.incarnation;
1144        std::mem::forget(departing);
1145
1146        assert!(matches!(
1147            reserve_actor(&computers, &"same".to_owned(), placeholder("same")),
1148            Err(VmmError::AlreadyExists(_))
1149        ));
1150        forget_computer(&computers, &"same".to_owned(), incarnation);
1151        let replacement = reserve_actor(&computers, &"same".to_owned(), placeholder("same"))
1152            .expect("the id is free once the departing actor let it go");
1153
1154        forget_computer(&computers, &"same".to_owned(), incarnation);
1155        assert!(
1156            computers.read().unwrap().contains_key("same"),
1157            "the stale incarnation must not evict the replacement"
1158        );
1159        drop(replacement);
1160        assert!(!computers.read().unwrap().contains_key("same"));
1161    }
1162
1163    /// Registration is per incarnation, which is what lets a handover pass
1164    /// tell an actor that finished from one that died.
1165    ///
1166    /// `detach_all` reads this after an unanswerable mailbox: `false` means
1167    /// the actor ended and unregistered (or its id was re-created since), so
1168    /// nothing was lost, while `true` means its task went away without
1169    /// finishing and its guest is still out there.
1170    #[test]
1171    fn registration_is_per_incarnation() {
1172        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1173        let id = "box".to_owned();
1174        let departing = reserve_actor(&computers, &id, placeholder("box")).unwrap();
1175        let incarnation = departing.incarnation;
1176        assert!(still_registered(&computers, &id, incarnation));
1177
1178        // A different incarnation of a live id is not this one.
1179        assert!(!still_registered(&computers, &id, Uuid::new_v4()));
1180
1181        // An actor still holding its claim while unable to answer: the case
1182        // worth reporting.
1183        std::mem::forget(departing);
1184        assert!(still_registered(&computers, &id, incarnation));
1185
1186        // And once it has let go — its own exit, or a replacement taking the
1187        // id — that incarnation is no longer registered.
1188        forget_computer(&computers, &id, incarnation);
1189        assert!(!still_registered(&computers, &id, incarnation));
1190        let replacement = reserve_actor(&computers, &id, placeholder("box")).unwrap();
1191        assert!(!still_registered(&computers, &id, incarnation));
1192        assert!(still_registered(&computers, &id, replacement.incarnation));
1193    }
1194}