Skip to main content

arcbox_computer_runtime/
sandbox.rs

1//! `ComputerManager` — 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_computer` 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::{ComputerError, Result};
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, ComputerEvent, ComputerId, ComputerInfo, ComputerMountSpec,
73    ComputerNetworkInfo, ComputerNetworkSpec, ComputerSpec, ComputerState, ComputerSummary,
74    IdleAction, LifecycleUpdate, RestoreComputerSpec, 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_computers` 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<ComputerId, ComputerRef>>>;
96
97/// Manages the full lifecycle of multiple sandbox microVMs.
98pub struct ComputerManager {
99    computers: Computers,
100    records: Arc<record::ComputerRecordStore>,
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 ComputerManager {
129    /// Create a new manager from the given configuration, over the
130    /// environment-specific components the composer supplies.
131    ///
132    /// Fails with [`ComputerError::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(ComputerError::Config(format!(
183                    "VM driver `{}` has no {capability} capability; {need}",
184                    driver.name()
185                )));
186            }
187        }
188        let records = Arc::new(record::ComputerRecordStore::new(Path::new(
189            &config.firecracker.data_dir,
190        ))?);
191        drop(records.load_all()?);
192        if network.reconcile().is_none() {
193            return Err(ComputerError::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::<_, ComputerError>(())
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: &ComputerId) -> Result<ComputerRef> {
340        self.check_reconcile()?;
341        self.computers
342            .read()
343            .unwrap()
344            .get(id)
345            .cloned()
346            .ok_or_else(|| ComputerError::NotFound(id.clone()))
347    }
348
349    /// This computer's mailbox.
350    pub(super) fn mailbox(&self, id: &ComputerId) -> 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: &ComputerId) -> 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| {
374                ComputerError::Other(format!("sandbox reconciliation stopped: {error}"))
375            })?
376            .clone()
377            .expect("wait_for returned only after reconciliation completed");
378        result.map_err(|error| {
379            ComputerError::Other(format!(
380                "sandbox durable-state reconciliation failed: {error}"
381            ))
382        })
383    }
384
385    fn check_reconcile(&self) -> Result<()> {
386        let result = self.reconcile_done.borrow().clone();
387        match result {
388            None => Err(ComputerError::Other(
389                "sandbox durable-state reconciliation is still running".into(),
390            )),
391            Some(Ok(())) => Ok(()),
392            Some(Err(error)) => Err(ComputerError::Other(format!(
393                "sandbox durable-state reconciliation failed: {error}"
394            ))),
395        }
396    }
397
398    /// Return sandbox IDs whose inactive network allocation still awaits host
399    /// forwarding cleanup.
400    pub async fn pending_network_cleanups(&self) -> Result<Vec<(String, String)>> {
401        self.await_reconcile().await?;
402        Ok(self
403            .reconcile_network()
404            .pending_cleanups()
405            .await
406            .map_err(ComputerError::from)?
407            .into_iter()
408            .map(|(vm, token)| (vm.as_str().to_owned(), token))
409            .collect())
410    }
411
412    /// Validate one exact pending host-cleanup ticket.
413    ///
414    /// Returns the lease that generation held: the host's forwarding rules
415    /// for it are keyed by its address, and after an agent restart the
416    /// quarantine ledger is the only place that address survives.
417    pub async fn validate_network_cleanup(&self, id: &str, token: &str) -> Result<NetworkLease> {
418        self.await_reconcile().await?;
419        Ok(self
420            .reconcile_network()
421            .validate_cleanup(&VmId::new(id)?, token)
422            .await?)
423    }
424
425    /// Recycle one exact inactive sandbox generation after forwarding cleanup.
426    pub async fn finalize_network_cleanup(&self, id: &str, token: &str) -> Result<()> {
427        self.await_reconcile().await?;
428        Ok(self
429            .reconcile_network()
430            .finalize_cleanup(&VmId::new(id)?, token)
431            .await?)
432    }
433
434    /// Wait until the current agent generation's startup cleanup is complete.
435    pub async fn wait_startup_cleanup_complete(&self) {
436        self.reconcile_network()
437            .wait_startup_cleanup_complete()
438            .await;
439    }
440
441    /// Opaque ticket for the host cleanup pass required after agent startup.
442    pub async fn startup_cleanup_token(&self) -> Result<Option<String>> {
443        self.await_reconcile().await?;
444        Ok(self.reconcile_network().startup_cleanup_token().await)
445    }
446
447    /// Validate the current process-generation startup cleanup ticket.
448    pub async fn validate_startup_cleanup(&self, token: &str) -> Result<()> {
449        self.await_reconcile().await?;
450        Ok(self
451            .reconcile_network()
452            .validate_startup_cleanup(token)
453            .await?)
454    }
455
456    /// Release the startup gate once host listeners and legacy DNAT are gone.
457    pub async fn finalize_startup_cleanup(&self, token: &str) -> Result<()> {
458        self.await_reconcile().await?;
459        Ok(self
460            .reconcile_network()
461            .finalize_startup_cleanup(token)
462            .await?)
463    }
464
465    /// The guest network's cleanup protocol.
466    pub(super) fn reconcile_network(&self) -> &dyn NetworkReconcile {
467        reconcile_capability(&*self.services.network)
468    }
469
470    /// Return the active generation's network identity: the external pool IP
471    /// (what DNS, expose, and the API report), its opaque cleanup token, and
472    /// how expose DNAT must target the sandbox (CORE-81/CORE-83).
473    ///
474    /// No startup-cleanup gate here, unlike every method above. An identity
475    /// needs a live lease; a lease comes only from `reserve`, which the
476    /// guest network refuses while the same startup barrier is closed; and
477    /// both callers in the guest agent await `wait_startup_cleanup_complete`
478    /// before asking. The gate this used to take could therefore only ever
479    /// have fired for a sandbox that has no lease to report anyway, and it
480    /// has no non-blocking form on the port.
481    pub fn computer_network_identity(&self, id: &str) -> Result<ComputerNetworkIdentity> {
482        let snapshot = self.snapshot(&id.to_owned())?;
483        let lease = snapshot
484            .lease
485            .as_ref()
486            .ok_or_else(|| ComputerError::WrongState {
487                id: id.to_owned(),
488                expected: "sandbox with an active network allocation".into(),
489                actual: snapshot.state.to_string(),
490            })?;
491        Ok(ComputerNetworkIdentity {
492            ip: lease.ipv4()?,
493            cleanup_token: lease.cleanup_token.clone(),
494            expose: self.services.network.host_ingress(lease)?,
495        })
496    }
497}
498
499/// What the sandbox stack needs from a [`NetworkLease`] beyond its fields.
500///
501/// The port's lease carries an [`IpAddr`](std::net::IpAddr) because a guest
502/// network need not be IPv4 (the platform's node dataplane is IPv6-only),
503/// while everything downstream of *this* manager — the pool address it
504/// reports, the netmask a legacy guest is reconfigured with, the crash
505/// journal's on-disk shape — is v4. The narrowing happens here, once, and
506/// says so when a lease cannot answer.
507pub(super) trait LeaseExt {
508    /// The lease's address.
509    fn ipv4(&self) -> Result<std::net::Ipv4Addr>;
510    /// The gateway the guest routes through.
511    fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr>;
512}
513
514impl LeaseExt for NetworkLease {
515    fn ipv4(&self) -> Result<std::net::Ipv4Addr> {
516        ipv4(self.ip)
517    }
518
519    fn gateway_ipv4(&self) -> Result<std::net::Ipv4Addr> {
520        ipv4(self.gateway)
521    }
522}
523
524/// One address of a lease or identity, narrowed. See [`LeaseExt`].
525pub(super) fn ipv4(address: std::net::IpAddr) -> Result<std::net::Ipv4Addr> {
526    match address {
527        std::net::IpAddr::V4(v4) => Ok(v4),
528        std::net::IpAddr::V6(v6) => Err(ComputerError::Network(format!(
529            "the guest network offered {v6}; this manager's sandboxes are IPv4-only"
530        ))),
531    }
532}
533
534/// The subnet mask `prefix_len` describes, clamped at /32 so an
535/// out-of-range prefix cannot overflow the shift.
536pub(super) fn netmask(prefix_len: u8) -> std::net::Ipv4Addr {
537    let prefix = prefix_len.min(32);
538    if prefix == 0 {
539        std::net::Ipv4Addr::UNSPECIFIED
540    } else {
541        std::net::Ipv4Addr::from(!0u32 << (32 - prefix))
542    }
543}
544
545/// The connectivity every sandbox gets: egress through the host's address,
546/// which is what the System VM's netfilter provides for the pool.
547pub(super) const fn computer_network_policy() -> NetworkPolicy {
548    NetworkPolicy {
549        mode: NetworkMode::Nat,
550    }
551}
552
553/// The mode a guest's interface is activated in, which is also the mode
554/// [`GuestNetwork::identity`] must be read under: a fresh boot and every
555/// invariant-snapshot restore take the fixed identity, and only checkpoints
556/// taken before it existed carry the pool address on the interface itself.
557pub(super) const fn attach_mode(net_invariant: bool) -> AttachMode {
558    if net_invariant {
559        AttachMode::Invariant
560    } else {
561        AttachMode::LegacySnapshot
562    }
563}
564
565/// A sandbox's network identity as seen by forwarding and DNS consumers.
566#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct ComputerNetworkIdentity {
568    /// External pool IP — the address the rest of the system keeps using.
569    pub ip: std::net::Ipv4Addr,
570    /// Opaque generation token carried through host cleanup finalization.
571    pub cleanup_token: String,
572    /// How expose DNAT must target this sandbox, decided by the datapath
573    /// actually applied to its TAP (CORE-81/CORE-83).
574    ///
575    /// The guest network's own answer, passed through unchanged: rendering
576    /// it as forwarding rules is the composing host's job, and only that
577    /// host knows which network it built.
578    pub expose: HostIngress,
579}
580
581/// The guest network's cleanup protocol, which [`ComputerManager::new`]
582/// requires — the quarantine ledger gates every address the pool hands
583/// out, and the startup sweep gates the pool itself.
584pub(super) fn reconcile_capability(network: &dyn GuestNetwork) -> &dyn NetworkReconcile {
585    network
586        .reconcile()
587        .expect("SandboxManager::new requires the guest network's reconcile capability")
588}
589
590/// The driver's `Prepare` capability, which [`ComputerManager::new`]
591/// requires — the boot, pool, and restore flows all spawn the VMM before
592/// there is a guest to run on it.
593pub(crate) fn prepare_capability(driver: &dyn VmDriver) -> &dyn Prepare {
594    driver
595        .prepare()
596        .expect("SandboxManager::new requires the driver's Prepare capability")
597}
598
599/// A grip's `Staging` capability, which [`ComputerManager::new`] requires —
600/// every flow that puts a guest on a VMM first brings that guest's files
601/// into the area the VMM can reach, and pause takes its disk back out of
602/// it.
603///
604/// Takes the accessor's answer rather than the grip it came from: both
605/// grips on a VM have one and they name the same area, so a computer
606/// booted here asks its [`PreparedVm`] and one this process adopted asks
607/// its [`VmHandle`](arcbox_vm_driver::VmHandle).
608pub(crate) fn staging_capability(staging: Option<&dyn Staging>) -> &dyn Staging {
609    staging.expect("SandboxManager::new requires the driver's Staging capability")
610}
611
612/// The VMM's pid as the crash journal records it: what a restart sweep
613/// kills before tearing the sandbox's other resources down.
614pub(crate) fn journaled_pid(prepared: &dyn PreparedVm) -> Option<i32> {
615    prepared
616        .record()
617        .process
618        .and_then(|process| i32::try_from(process.pid).ok())
619}
620
621/// The VMM as the crash journal records it — its API socket and its jail —
622/// which is what lets the next process adopt this VM instead of killing it.
623///
624/// The driver knows both because it spawned the VMM that way. Nothing else
625/// can work either out afterwards: see
626/// [`ComputerStateRecord::api_socket`](crate::sandbox::reconcile::ComputerStateRecord::api_socket)
627/// and [`ComputerStateRecord::jail`](crate::sandbox::reconcile::ComputerStateRecord::jail).
628pub(crate) fn journaled_vmm(prepared: &dyn PreparedVm) -> Option<JournaledVmm> {
629    prepared.record().process.map(|process| JournaledVmm {
630        api_socket: process.api_socket,
631        jail: process.jail.map(Into::into),
632    })
633}
634
635/// The isolation every sandbox VMM runs under: the jailer's, when one is
636/// configured; none otherwise (direct mode).
637pub(crate) fn isolation_spec(config: &RuntimeConfig) -> Result<IsolationSpec> {
638    config
639        .firecracker
640        .jailer
641        .as_ref()
642        .map_or(Ok(IsolationSpec::None), IsolationSpec::try_from)
643}
644
645/// Where a paused computer's retained disk overlay lives: a function of the
646/// data dir and the id, which is why the flows that keep, rename, look for
647/// and delete it can all say it the same way.
648pub(crate) fn preserved_cow_file(config: &RuntimeConfig, id: &str) -> PathBuf {
649    PathBuf::from(&config.firecracker.data_dir)
650        .join("cow")
651        .join(format!("arcbox-cow-{id}.img"))
652}
653
654/// A checkpoint as the catalog holds it, which is what
655/// [`Staging::stage_checkpoint`] takes and what the driver reads back.
656///
657/// The catalog writes one directory per checkpoint and puts `vmstate` and
658/// `mem` in it, so the image is that directory — read off the meta's own
659/// `vmstate_path` rather than recomputed from the catalog's layout, which
660/// this layer does not own. Its name is what the driver stages the
661/// checkpoint under, and a restore reproduces that name from the same
662/// image, so the two cannot drift. The format is the one the catalog
663/// recorded at capture; legacy entries default to the Firecracker format.
664pub(crate) fn catalogued_checkpoint(meta: &SnapshotMeta) -> Result<CheckpointImage> {
665    let dir = meta.vmstate_path.parent().ok_or_else(|| {
666        ComputerError::Snapshot(format!(
667            "checkpoint {} records a vmstate at {}, which is in no directory",
668            meta.id,
669            meta.vmstate_path.display()
670        ))
671    })?;
672    Ok(CheckpointImage {
673        dir: dir.to_path_buf(),
674        format: CheckpointFormat::new(&meta.format),
675        kind: CheckpointKind::Full,
676    })
677}
678
679/// Validate a caller-supplied sandbox or snapshot id.
680///
681/// Ids become filesystem path components and dm/TAP name fragments, so they are
682/// restricted to `[A-Za-z0-9_-]`. This rejects path traversal (`/`, `\`, `..`),
683/// NUL and whitespace. The narrower rule a VMM imposes on the id it runs under
684/// is *not* checked here — that is [`VmId`]'s, applied by
685/// [`validate_new_computer_id`] where an id becomes a VM identity.
686///
687/// Deliberately NO length cap here: this also runs against persisted
688/// records, and what one legacy over-long id would cost differs by call
689/// site. On the sweep path it costs that record its reconciliation — the
690/// journal is skipped rather than acted on, and every resource it names
691/// is held (`reconcile::sweep_orphans`). On a record *load* it costs
692/// every record theirs: [`ComputerRecordStore::load_all`] validates each
693/// id and propagates, so one rejection aborts the whole startup read,
694/// which is the stronger reason the cap is absent. It also runs against
695/// snapshot / execution ids that never become a VM identity at all. Both
696/// limits the driver imposes are enforced only where a sandbox id enters
697/// the system: [`validate_new_computer_id`].
698///
699/// The gap between this alphabet and [`VmId`]'s is not academic: `_` is
700/// legal here and is not a `VmId`, so every record a pre-#680 process
701/// wrote for an `inst_…` sandbox is one this process cannot name — which
702/// is exactly what the sweep and the quarantine ledger read back.
703pub(super) fn validate_id(kind: &str, id: &str) -> Result<()> {
704    if id.is_empty() {
705        return Err(ComputerError::Config(format!("{kind} must not be empty")));
706    }
707    if !id
708        .bytes()
709        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
710    {
711        return Err(ComputerError::Config(format!(
712            "invalid {kind} {id:?}: only ASCII letters, digits, '-' and '_' are allowed"
713        )));
714    }
715    Ok(())
716}
717
718/// Validate a sandbox id at request ingress (create / restore), where it
719/// becomes the identity the VM runs under.
720///
721/// Both halves belong to the driver, and neither is restated here. The
722/// alphabet and the 64-byte ceiling are [`VmId`]'s: an id a VMM will not
723/// take is refused by construction, and Firecracker — which `panic!`s on
724/// its own `--id` in *both* jailed and direct mode — is what that rule is
725/// cut to. The length budget is [`VmDriver::id_budget`], measured on the
726/// layout the configured driver will actually lay down, so a longer chroot
727/// base or binary name tightens it rather than silently reintroducing the
728/// failure it exists to prevent.
729///
730/// Refusing here is the whole point: past this call the id is validated
731/// again inside the async boot task, where the refusal arrives with
732/// nothing tying it back to the request that caused it — an
733/// `InvalidInstanceId` under the jailer, an abort under bare Firecracker,
734/// and an opaque "timed out waiting for socket" when it is the length
735/// (the driver's readiness probe is a `connect()`, which ENAMETOOLONGs on
736/// every attempt while the VMM is up and bound inside its chroot; caught
737/// by the CORE-107 prewarm e2e, whose 51-char builder id overflowed the
738/// stock budget).
739pub(super) fn validate_new_computer_id(
740    id: &str,
741    driver: &dyn VmDriver,
742    config: &RuntimeConfig,
743) -> Result<()> {
744    VmId::new(id)?;
745    if let Some(budget) = driver.id_budget(&isolation_spec(config)?)
746        && id.len() > budget
747    {
748        return Err(ComputerError::Config(format!(
749            "invalid sandbox id {id:?}: at most {budget} characters under this \
750             driver's layout (its sockets must fit the AF_UNIX path limit)"
751        )));
752    }
753    Ok(())
754}
755
756/// Atomically claim `id` in the registry and stand up its actor.
757///
758/// Create and restore both derive per-computer resources deterministically
759/// from the id (CoW file `arcbox-cow-{id}`, dm device `arcbox-snap-{id}`, TAP
760/// name). A check-then-insert with those resources set up in between is a
761/// TOCTOU: two concurrent restores to the same id both pass the check, then
762/// the second `create_sparse_file` truncates the file the first's loop device
763/// is backing, corrupting the winner's rootfs. Claiming the id up front
764/// (check + insert under one write lock) makes the loser fail fast with
765/// `AlreadyExists` before it touches any shared resource.
766///
767/// The actor is **not** spawned here. Its seed needs the durable generation
768/// and the deadlines the record computes, and both are resolved after the
769/// claim; the mailbox and the read snapshot exist from the claim on, so a
770/// caller that finds the entry finds a usable computer either way. The claim
771/// is released on drop unless [`ActorReservation::spawn`] is called, so every
772/// error path unwinds it.
773pub(crate) fn reserve_actor(
774    computers: &Computers,
775    id: &ComputerId,
776    runtime: ComputerRuntime,
777) -> Result<ActorReservation> {
778    let mut map = computers.write().unwrap();
779    if map.contains_key(id) {
780        return Err(ComputerError::AlreadyExists(id.clone()));
781    }
782    let incarnation = Uuid::new_v4();
783    let (mailbox_tx, commands) = tokio::sync::mpsc::unbounded_channel();
784    let runtime = Arc::new(Mutex::new(runtime));
785    let (snapshot_tx, snapshot) = tokio::sync::watch::channel(ComputerSnapshot::project(
786        &runtime.lock().unwrap(),
787        ComputerState::Starting,
788        Deadlines::default(),
789    ));
790    map.insert(
791        id.clone(),
792        ComputerRef {
793            mailbox: Mailbox::new(mailbox_tx.clone()),
794            snapshot,
795            incarnation,
796        },
797    );
798    drop(map);
799    Ok(ActorReservation {
800        computers: Arc::clone(computers),
801        id: id.clone(),
802        incarnation,
803        runtime,
804        mailbox: Mailbox::new(mailbox_tx),
805        commands: Some(commands),
806        snapshot_tx: Some(snapshot_tx),
807    })
808}
809
810/// A claimed id, its runtime state, and the channels its actor will run on.
811///
812/// Dropping it without [`Self::spawn`] releases the claim; the mailbox's last
813/// sender goes with it, so an actor already running would stop too.
814pub(crate) struct ActorReservation {
815    computers: Computers,
816    id: ComputerId,
817    incarnation: Uuid,
818    runtime: Runtime,
819    mailbox: Mailbox,
820    commands: Option<tokio::sync::mpsc::UnboundedReceiver<Command>>,
821    snapshot_tx: Option<tokio::sync::watch::Sender<ComputerSnapshot>>,
822}
823
824impl ActorReservation {
825    /// The claimed computer's runtime state, for the caller to populate
826    /// before its actor starts.
827    pub(crate) fn runtime(&self) -> &Runtime {
828        &self.runtime
829    }
830
831    /// Start the actor and keep the claim.
832    pub(crate) fn spawn(mut self, seed: ActorSpawn) -> Mailbox {
833        let ActorSpawn {
834            services,
835            timers_enabled,
836            generation,
837            deadlines,
838            launch,
839            seeded,
840        } = seed;
841        let mailbox = self.mailbox.clone();
842        let flows = Arc::new(ComputerFlows::new(
843            self.id.clone(),
844            Arc::clone(&self.runtime),
845            Arc::clone(&services),
846            &mailbox,
847            launch,
848        ));
849        let unregister = {
850            let computers = Arc::clone(&self.computers);
851            let id = self.id.clone();
852            let incarnation = self.incarnation;
853            Arc::new(move || forget_computer(&computers, &id, incarnation))
854        };
855        let vm_dir = self.runtime.lock().unwrap().vm_dir.clone();
856        let actor = ComputerActor::new(
857            ComputerSeed {
858                id: self.id.clone(),
859                runtime: Arc::clone(&self.runtime),
860                unregister,
861                generation,
862                vm_dir,
863                records: Arc::clone(&services.records),
864                events: Arc::clone(&services.events),
865                tasks: flows,
866                deadlines,
867                timers_enabled,
868                seeded,
869            },
870            self.commands.take().expect("spawned once"),
871            self.snapshot_tx.take().expect("spawned once"),
872        );
873        // Constructors are synchronous and may run outside a runtime (unit
874        // tests of unrelated surfaces); such a manager has no actors to run
875        // and never provisions one.
876        if tokio::runtime::Handle::try_current().is_ok() {
877            tokio::spawn(actor.run());
878        }
879        mailbox
880    }
881}
882
883impl Drop for ActorReservation {
884    fn drop(&mut self) {
885        if self.commands.is_some() {
886            forget_computer(&self.computers, &self.id, self.incarnation);
887        }
888    }
889}
890
891/// Everything an actor needs that the reservation does not already hold.
892///
893/// Assembled from the manager's own pieces rather than from `&ComputerManager`
894/// so the startup sweep can seed actors too: it runs in a task spawned from
895/// the constructor, before the manager it belongs to exists.
896pub(crate) struct ActorSpawn {
897    pub(crate) services: Arc<ComputerServices>,
898    pub(crate) timers_enabled: tokio::sync::watch::Receiver<bool>,
899    pub(crate) generation: Option<Uuid>,
900    pub(crate) deadlines: Deadlines,
901    pub(crate) launch: Launch,
902    pub(crate) seeded: Seeded,
903}
904
905/// Drop `id`'s entry when it is still `incarnation`'s.
906///
907/// The identity check replaces the `Arc::ptr_eq` guard the instance map
908/// needed: a computer removed and re-created under the same id (deterministic
909/// caller-supplied ids make this common) installs a fresh entry, and the
910/// departing actor must not evict it.
911fn forget_computer(computers: &Computers, id: &ComputerId, incarnation: Uuid) {
912    let mut map = computers.write().unwrap();
913    if map
914        .get(id)
915        .is_some_and(|current| current.incarnation == incarnation)
916    {
917        map.remove(id);
918    }
919}
920
921/// Whether `id`'s entry is still this exact incarnation's — the inverse of
922/// [`forget_computer`], and the question an unanswerable mailbox raises.
923///
924/// Every ending unregisters: the actor's own loop tail whatever stopped it, the
925/// record it forgets on a removal, and [`ActorReservation::drop`] for a create
926/// that unwound before spawning one. A re-created id installs a fresh
927/// incarnation over the departed one, so this is `false` there too. What is
928/// left is an incarnation *still here* whose mailbox no longer answers: an
929/// actor whose task died without finishing, and the one case where a
930/// computer's resources outlive the thing that was managing them.
931///
932/// Reads nothing the actor owns, so it stays answerable for a computer whose
933/// runtime mutex a panic has poisoned.
934fn still_registered(computers: &Computers, id: &ComputerId, incarnation: Uuid) -> bool {
935    computers
936        .read()
937        .unwrap()
938        .get(id)
939        .is_some_and(|current| current.incarnation == incarnation)
940}
941
942#[cfg(test)]
943mod tests {
944    use arcbox_vm_driver::testkit::FakeDriver;
945
946    use super::*;
947
948    fn placeholder(id: &str) -> ComputerRuntime {
949        ComputerRuntime::new(
950            id.to_owned(),
951            ComputerSpec::default(),
952            None,
953            PathBuf::from("/tmp/x"),
954        )
955    }
956
957    /// The manager refuses, at construction, a driver that cannot spawn the
958    /// VMM ahead of a boot, dial the guest, or take its readiness dial-out:
959    /// every sandbox flow needs all three, so a driver without one would
960    /// fail at the first create instead.
961    #[test]
962    fn construction_requires_the_drivers_prepare_and_vsock_capabilities() {
963        use arcbox_vm_driver::testkit::FakeDriver;
964        use arcbox_vm_driver::{DriverCapabilities, VmDriver as _};
965
966        let dir = tempfile::tempdir().unwrap();
967        let mut config = RuntimeConfig::default();
968        config.firecracker.data_dir = dir.path().to_string_lossy().into_owned();
969        // The vm-proto factory, not the agent fake: only a dial-out
970        // readiness makes the driver's `vsock_listen` a requirement, which
971        // is one of the three refusals below.
972        let environment = |driver: FakeDriver| NodeEnvironment {
973            driver: Arc::new(driver),
974            agent: Arc::new(crate::agent::VmProtoAgentFactory::default()),
975            ..crate::testkit::fake_environment(&config).unwrap()
976        };
977        let all = FakeDriver::new().capabilities();
978
979        for (name, capabilities) in [
980            (
981                "prepare",
982                DriverCapabilities {
983                    prepare: false,
984                    ..all.clone()
985                },
986            ),
987            (
988                "vsock",
989                DriverCapabilities {
990                    vsock: false,
991                    ..all.clone()
992                },
993            ),
994            (
995                "vsock_listen",
996                DriverCapabilities {
997                    vsock_listen: false,
998                    ..all
999                },
1000            ),
1001        ] {
1002            let driver = FakeDriver::builder().capabilities(capabilities).build();
1003            let error = ComputerManager::new(config.clone(), environment(driver))
1004                .err()
1005                .unwrap_or_else(|| panic!("a driver without {name} is refused"));
1006            assert!(matches!(error, ComputerError::Config(_)), "{error}");
1007            assert!(error.to_string().contains(name), "{error}");
1008        }
1009
1010        let manager = ComputerManager::new(config.clone(), environment(FakeDriver::new()))
1011            .expect("a driver with every needed capability is accepted");
1012        assert_eq!(manager.services.driver.name(), "fake");
1013    }
1014
1015    #[test]
1016    fn a_claimed_id_rejects_a_concurrent_duplicate() {
1017        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1018        let first = reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")).unwrap();
1019        // A second claim of the same id must fail while the first is live —
1020        // before either touches the per-id resources they both derive.
1021        assert!(matches!(
1022            reserve_actor(&computers, &"dup".to_owned(), placeholder("dup")),
1023            Err(ComputerError::AlreadyExists(_))
1024        ));
1025        drop(first);
1026        assert!(!computers.read().unwrap().contains_key("dup"));
1027    }
1028
1029    /// A jailed config, which is what makes a driver answer with a budget
1030    /// at all — the layout behind that answer is the driver's business.
1031    fn jailed_config() -> RuntimeConfig {
1032        let mut config = RuntimeConfig::default();
1033        config.firecracker.jailer = Some(crate::config::JailerConfig {
1034            uid: 0,
1035            gid: 0,
1036            chroot_base_dir: Some("/srv/jailer".into()),
1037            netns: None,
1038            new_pid_ns: false,
1039            cgroup_version: None,
1040            parent_cgroup: None,
1041        });
1042        config
1043    }
1044
1045    /// The id shape the platform node actually mints, in the rendering a
1046    /// driver can run: the control plane's `inst_<uuid v7>` with the `_`
1047    /// the VMM refuses turned into a `-`.
1048    const CONTROL_PLANE_ID: &str = "inst-019e409e-7546-7a3e-8b2c-1f2e3d4c5b6a";
1049
1050    #[test]
1051    fn validate_id_accepts_safe_ids_and_rejects_traversal() {
1052        for ok in ["sandbox1", "a-b_c", "0f3e9d16-1234", "A_B-9"] {
1053            assert!(validate_id("id", ok).is_ok(), "{ok} should be valid");
1054        }
1055        // The generic validator stays uncapped and keeps the underscore: it
1056        // also runs against persisted records and against snapshot /
1057        // execution ids that never become a VM identity, where a legacy id
1058        // must not become fatal.
1059        assert!(validate_id("id", &"a".repeat(60)).is_ok());
1060        assert!(validate_id("id", "inst_019e409e-7546").is_ok());
1061        for bad in ["", "..", ".", "a/b", "a\\b", "a b", "a.b", "a\0b", "../etc"] {
1062            assert!(
1063                validate_id("id", bad).is_err(),
1064                "{bad:?} should be rejected"
1065            );
1066        }
1067    }
1068
1069    /// Ingress holds a new id to whatever budget the *configured* driver
1070    /// reports for the isolation it will run under — this layer keeps no
1071    /// copy of a number. Anything past it must fail fast here instead of
1072    /// surfacing as a socket connect timeout.
1073    ///
1074    /// Whether a given layout leaves room for the ids an ArcBox node mints
1075    /// is the adapter's own question, answered against the deployed
1076    /// constants in `arcbox-fc-driver`
1077    /// (`the_deployed_jail_layout_admits_the_ids_a_node_mints`).
1078    #[test]
1079    fn a_new_sandbox_id_is_held_to_the_drivers_own_budget() {
1080        let mut config = jailed_config();
1081        let budget = CONTROL_PLANE_ID.len();
1082        let driver = FakeDriver::builder().jailed_id_budget(budget).build();
1083        assert!(validate_new_computer_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
1084        assert!(validate_new_computer_id(&"a".repeat(budget), &driver, &config).is_ok());
1085        assert!(validate_new_computer_id(&"a".repeat(budget + 1), &driver, &config).is_err());
1086
1087        // A tighter budget refuses what the looser one took, rather than
1088        // silently reintroducing the connect timeout.
1089        let tighter = FakeDriver::builder().jailed_id_budget(budget - 1).build();
1090        assert!(validate_new_computer_id(CONTROL_PLANE_ID, &tighter, &config).is_err());
1091
1092        // A driver that reports no budget for this isolation bounds
1093        // nothing: only `VmId`'s own 64-byte ceiling is left.
1094        config.firecracker.jailer = None;
1095        assert!(validate_new_computer_id(&"a".repeat(budget + 1), &tighter, &config).is_ok());
1096    }
1097
1098    /// CORE-140. Firecracker validates the `--id` it is handed and refuses it
1099    /// by panicking — under the jailer *and* in direct mode, where the same
1100    /// flag is passed straight to the VMM. So the refusal cannot be gated on
1101    /// a jailer being configured: it belongs to the id itself.
1102    #[test]
1103    fn an_underscored_id_is_refused_at_ingress_in_every_spawn_mode() {
1104        let mut config = jailed_config();
1105        let driver = FakeDriver::builder()
1106            .jailed_id_budget(CONTROL_PLANE_ID.len())
1107            .build();
1108        for _ in 0..2 {
1109            let error =
1110                validate_new_computer_id(&CONTROL_PLANE_ID.replacen('-', "_", 1), &driver, &config)
1111                    .expect_err("firecracker would refuse to run under this id");
1112            assert!(
1113                matches!(&error, ComputerError::Config(message) if message.contains('_')),
1114                "the error should name the offending character, got {error}"
1115            );
1116            assert!(validate_new_computer_id(CONTROL_PLANE_ID, &driver, &config).is_ok());
1117            // Direct mode passes the same `--id`, so it is refused there too.
1118            config.firecracker.jailer = None;
1119        }
1120    }
1121
1122    #[test]
1123    fn a_dropped_claim_frees_the_id() {
1124        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1125        {
1126            let _claim = reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).unwrap();
1127            assert!(computers.read().unwrap().contains_key("tmp"));
1128            // never spawned → dropped here
1129        }
1130        assert!(!computers.read().unwrap().contains_key("tmp"));
1131        // The id is free to claim again after an unwound restore.
1132        assert!(reserve_actor(&computers, &"tmp".to_owned(), placeholder("tmp")).is_ok());
1133    }
1134
1135    /// A departing actor unregisters *its own* entry and no other.
1136    ///
1137    /// The `Arc::ptr_eq` guard the instance map needed, as an incarnation:
1138    /// a computer removed and re-created under the same id (deterministic
1139    /// caller-supplied ids make this common) installs a fresh entry, and the
1140    /// one on its way out must not evict it.
1141    #[test]
1142    fn a_departing_actor_does_not_evict_its_replacement() {
1143        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1144        let departing = reserve_actor(&computers, &"same".to_owned(), placeholder("same")).unwrap();
1145        let incarnation = departing.incarnation;
1146        std::mem::forget(departing);
1147
1148        assert!(matches!(
1149            reserve_actor(&computers, &"same".to_owned(), placeholder("same")),
1150            Err(ComputerError::AlreadyExists(_))
1151        ));
1152        forget_computer(&computers, &"same".to_owned(), incarnation);
1153        let replacement = reserve_actor(&computers, &"same".to_owned(), placeholder("same"))
1154            .expect("the id is free once the departing actor let it go");
1155
1156        forget_computer(&computers, &"same".to_owned(), incarnation);
1157        assert!(
1158            computers.read().unwrap().contains_key("same"),
1159            "the stale incarnation must not evict the replacement"
1160        );
1161        drop(replacement);
1162        assert!(!computers.read().unwrap().contains_key("same"));
1163    }
1164
1165    /// Registration is per incarnation, which is what lets a handover pass
1166    /// tell an actor that finished from one that died.
1167    ///
1168    /// `detach_all` reads this after an unanswerable mailbox: `false` means
1169    /// the actor ended and unregistered (or its id was re-created since), so
1170    /// nothing was lost, while `true` means its task went away without
1171    /// finishing and its guest is still out there.
1172    #[test]
1173    fn registration_is_per_incarnation() {
1174        let computers: Computers = Arc::new(RwLock::new(HashMap::new()));
1175        let id = "box".to_owned();
1176        let departing = reserve_actor(&computers, &id, placeholder("box")).unwrap();
1177        let incarnation = departing.incarnation;
1178        assert!(still_registered(&computers, &id, incarnation));
1179
1180        // A different incarnation of a live id is not this one.
1181        assert!(!still_registered(&computers, &id, Uuid::new_v4()));
1182
1183        // An actor still holding its claim while unable to answer: the case
1184        // worth reporting.
1185        std::mem::forget(departing);
1186        assert!(still_registered(&computers, &id, incarnation));
1187
1188        // And once it has let go — its own exit, or a replacement taking the
1189        // id — that incarnation is no longer registered.
1190        forget_computer(&computers, &id, incarnation);
1191        assert!(!still_registered(&computers, &id, incarnation));
1192        let replacement = reserve_actor(&computers, &id, placeholder("box")).unwrap();
1193        assert!(!still_registered(&computers, &id, incarnation));
1194        assert!(still_registered(&computers, &id, replacement.incarnation));
1195    }
1196}