cellos-host-firecracker 0.5.1

Firecracker microVM backend for CellOS — jailer integration, warm pool with snapshot/restore, KVM nested-virtualisation aware.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
//! FC-13 — `/proc` and `/sys` are mounted by init before workload exec.
//!
//! Acceptance gate (from [Plans/firecracker-release-readiness.md] §FC-13):
//!
//! > FC-13: `/proc` and `/sys` are mounted by init before workload exec.
//! > Acceptance: e2e workload reads `/proc/self/status` and `/sys/devices`
//! > successfully; failure mode (init does not mount) produces a
//! > distinguishable error and is itself covered by a negative test.
//! > (Post-1.0: `mount_proc_and_sys()` at `crates/cellos-init/src/main.rs`
//! > L196–L235 mounts both; the negative test that simulates non-mount
//! > and the positive `/sys/devices` read are CI gaps.)
//!
//! # Workload contract
//!
//! The brief mandates the in-VM workload script:
//!
//! ```text
//! cat /proc/self/mountinfo > /shared/mountinfo.out
//!     && ls /sys/devices > /shared/sys-devices.out
//!     && exit 0; exit 1
//! ```
//!
//! The shell `&&` chain produces a stable contract:
//!
//!   * If both reads succeed, the workload exits 0 and BOTH capture
//!     files exist.
//!   * If either read fails, the chain short-circuits and the workload
//!     exits 1 (the trailing `exit 1` after `;` is reached).
//!
//! On the host side the firecracker-e2e workflow:
//!
//!   1. Stages a `/shared/` mount (the supervisor's shared-volume facility,
//!      mounted into the cell rootfs).
//!   2. Runs the workload via `FirecrackerCellBackend::create()`.
//!   3. Reads back the captured files from `/shared/` after the cell
//!      destroys.
//!   4. Drops the captured files at paths the env vars below name.
//!
//! This file owns the *assertion* contract: given the captured files plus
//! the workload's exit code, decide whether `/proc` and `/sys` were both
//! mounted before exec.
//!
//! # Why not exec the workload from this file?
//!
//! `FirecrackerCellBackend::create()` requires:
//!
//!   * Linux + KVM (a real Firecracker VMM binary on `$PATH`),
//!   * `CAP_NET_ADMIN` for TAP creation,
//!   * a kernel + rootfs pair pre-built into a fixture cache.
//!
//! None of those are satisfiable on the slot's authoring host (Windows)
//! or on the default github-hosted runners. So this file follows the
//! same two-tier pattern as `fc14_capbnd_empty.rs` and
//! `fc34_nftables_drops_undeclared.rs`:
//!
//!   1. Pure-Rust predicates over captured `/proc/self/mountinfo` content
//!      and captured `/sys/devices` listings — runnable on every CI leg
//!      (Windows, macOS, Linux). These codify the FC-13 invariants.
//!   2. A Linux-gated e2e harness, opt-in via
//!      `CELLOS_FIRECRACKER_FC13_E2E=1`, that reads the fixture files
//!      dropped by the firecracker-e2e workflow and asserts the
//!      invariants against live capture.
//!
//! # `/proc/self/mountinfo` line shape (kernel ABI)
//!
//! Per `man 5 proc` (`/proc/[pid]/mountinfo`) each line is:
//!
//! ```text
//! mount-id  parent-id  major:minor  root  mount-point  options  -  fs-type  source  super-options
//! ```
//!
//! The fs-type appears AFTER the `" - "` separator. For FC-13 we look
//! for two specific fs-types on the canonical mount points:
//!
//!   * `proc` mounted at `/proc`,
//!   * `sysfs` mounted at `/sys`.
//!
//! Either fs-type appearing on the wrong mount point, or absent
//! entirely, is a hard failure: it means `cellos-init`'s
//! `mount_proc_and_sys()` did not run, partially failed, or ran in the
//! wrong order relative to workload exec.

// ── /proc/self/mountinfo predicate ─────────────────────────────────────────

/// FC-13 invariant on captured `/proc/self/mountinfo` content: the file must
/// contain at least one line declaring `proc` mounted at `/proc` AND at
/// least one line declaring `sysfs` mounted at `/sys`. Returns `Err(reason)`
/// when either is absent so the caller can blame the right half of
/// `mount_proc_and_sys()`.
///
/// We deliberately accept the kernel's full mountinfo line format (10+
/// space-separated fields with a `" - "` separator before fs-type) without
/// trying to parse every field — only the (mount-point, fs-type) pair
/// matters for FC-13. Robust to mount-id renumbering, optional-field
/// presence/absence, and source-string variation across kernels.
pub fn assert_proc_and_sysfs_mounted(mountinfo: &str) -> Result<(), String> {
    let mut saw_proc = false;
    let mut saw_sysfs = false;

    for line in mountinfo.lines() {
        if line.trim().is_empty() {
            continue;
        }
        // mountinfo lines split into two halves at the literal " - " marker:
        // pre-marker fields end with the per-mount options; post-marker
        // fields start with the fs-type. We need fields[4] (mount-point)
        // from the pre-marker half and the first post-marker field
        // (fs-type) from the post-marker half.
        let Some((pre, post)) = split_mountinfo_line(line) else {
            // Malformed line — ignore rather than fail. The FC-13 contract
            // is "BOTH proc and sysfs lines are present"; a single weird
            // line elsewhere doesn't invalidate that.
            continue;
        };
        let pre_fields: Vec<&str> = pre.split_whitespace().collect();
        let post_fields: Vec<&str> = post.split_whitespace().collect();
        if pre_fields.len() < 5 || post_fields.is_empty() {
            continue;
        }
        let mount_point = pre_fields[4];
        let fs_type = post_fields[0];

        if fs_type == "proc" && mount_point == "/proc" {
            saw_proc = true;
        }
        if fs_type == "sysfs" && mount_point == "/sys" {
            saw_sysfs = true;
        }
    }

    match (saw_proc, saw_sysfs) {
        (true, true) => Ok(()),
        (false, true) => Err(
            "FC-13 violation: /proc/self/mountinfo has NO `proc` line on /proc. \
             cellos-init::mount_proc_and_sys() did not mount /proc before workload \
             exec — or mounted it after fork(). Re-check L241–L261 of \
             crates/cellos-init/src/main.rs."
                .to_string(),
        ),
        (true, false) => Err(
            "FC-13 violation: /proc/self/mountinfo has NO `sysfs` line on /sys. \
             cellos-init::mount_proc_and_sys() did not mount /sys before workload \
             exec. Re-check L263–L280 of crates/cellos-init/src/main.rs."
                .to_string(),
        ),
        (false, false) => Err(
            "FC-13 violation: /proc/self/mountinfo has NEITHER a `proc` line on \
             /proc NOR a `sysfs` line on /sys. cellos-init::mount_proc_and_sys() \
             apparently never ran — the workload was exec'd against a bare rootfs. \
             This is the SEAM-13 regression."
                .to_string(),
        ),
    }
}

/// Split a `/proc/self/mountinfo` line at the literal `" - "` marker. Returns
/// `(pre, post)` where `pre` ends with the per-mount options and `post`
/// starts with the fs-type. Returns `None` on lines that do not contain
/// the marker at all (malformed).
fn split_mountinfo_line(line: &str) -> Option<(&str, &str)> {
    // Use rsplit so we tolerate optional fields (zero or more `tag:value`
    // entries) appearing before the marker — they are themselves separated
    // by spaces but don't contain " - ". rsplit_once on " - " is safe
    // because the marker is always single-space delimited per ABI.
    let (pre, post) = line.split_once(" - ")?;
    Some((pre, post))
}

// ── /sys/devices listing predicate ─────────────────────────────────────────

/// FC-13 invariant on captured `/sys/devices` listing: the file must be
/// non-empty AND contain at least one entry. An empty listing means either
/// `/sys` is not mounted (the listing went against an empty directory in
/// the rootfs) or the workload could not read the directory (the `ls`
/// failed and the `&&` chain should have short-circuited — so this is
/// belt-and-braces with the exit-code check).
pub fn assert_sys_devices_listing_nonempty(listing: &str) -> Result<(), String> {
    let entries: Vec<&str> = listing
        .lines()
        .map(|l| l.trim())
        .filter(|l| !l.is_empty())
        .collect();
    if entries.is_empty() {
        return Err(
            "FC-13 violation: captured /sys/devices listing is empty. /sys was \
             either not mounted or mounted as an empty tmpfs. On a real Linux \
             system /sys/devices contains kernel device-class roots (block, \
             system, virtual, etc.) so an empty listing here is unambiguous."
                .to_string(),
        );
    }
    Ok(())
}

// ── Workload-exit-code predicate ───────────────────────────────────────────

/// FC-13 invariant: the workload exited 0. Per the brief's shell contract
/// (`cat … && ls … && exit 0; exit 1`), exit 0 implies BOTH the cat and
/// the ls succeeded. Any non-zero code means at least one of `/proc/self/
/// mountinfo` or `/sys/devices` was unreadable — the SEAM-13 failure mode.
pub fn assert_workload_exit_zero(exit_code: i32) -> Result<(), String> {
    if exit_code != 0 {
        return Err(format!(
            "FC-13 violation: workload exited {exit_code} (expected 0). \
             The shell chain `cat /proc/self/mountinfo && ls /sys/devices && \
             exit 0; exit 1` short-circuits to exit 1 only when one of \
             /proc or /sys is unreadable. cellos-init::mount_proc_and_sys() \
             did not establish the mounts before the workload ran."
        ));
    }
    Ok(())
}

// ── Pure-Rust unit-style coverage of the predicates ─────────────────────────
//
// These tests run on every CI leg (Windows, macOS, Linux) so a regression in
// the predicate code itself is caught even when no firecracker-capable
// runner has dropped fixture data.

#[test]
fn mountinfo_predicate_passes_on_canonical_capture() {
    // Realistic snippet adapted from a live `/proc/self/mountinfo` on a
    // PID-1 cellos-init guest. Field counts match the kernel ABI: 10
    // fields pre-marker (with one optional `shared:N` tag), 3 fields
    // post-marker.
    let mi = "\
21 19 0:20 / /proc rw,relatime shared:7 - proc proc rw
22 19 0:21 / /sys rw,relatime shared:8 - sysfs sysfs rw
23 19 0:22 / /dev rw,relatime - devtmpfs devtmpfs rw
";
    assert_proc_and_sysfs_mounted(mi).expect("canonical capture must pass");
}

#[test]
fn mountinfo_predicate_passes_without_optional_tags() {
    // Same content but with no optional `shared:N` field. The ABI says
    // optional fields can be zero or more — predicate must still parse.
    let mi = "\
21 19 0:20 / /proc rw,relatime - proc proc rw
22 19 0:21 / /sys rw,relatime - sysfs sysfs rw
";
    assert_proc_and_sysfs_mounted(mi).expect("optional-fields-absent must pass");
}

#[test]
fn mountinfo_predicate_fails_when_proc_missing() {
    // The half-mounted failure mode: sysfs is fine but proc never got
    // mounted. The predicate must blame /proc specifically (so the
    // operator can map the error to the right code path in
    // mount_proc_and_sys()).
    let mi = "22 19 0:21 / /sys rw,relatime - sysfs sysfs rw\n";
    let err = assert_proc_and_sysfs_mounted(mi).expect_err("missing /proc must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
    assert!(err.contains("NO `proc` line"), "got: {err}");
}

#[test]
fn mountinfo_predicate_fails_when_sysfs_missing() {
    let mi = "21 19 0:20 / /proc rw,relatime - proc proc rw\n";
    let err = assert_proc_and_sysfs_mounted(mi).expect_err("missing /sys must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
    assert!(err.contains("NO `sysfs` line"), "got: {err}");
}

#[test]
fn mountinfo_predicate_fails_when_both_missing() {
    // The total-failure mode: mount_proc_and_sys() never ran. Predicate
    // must surface SEAM-13 explicitly so the operator routes it to the
    // right post-mortem.
    let mi = "23 19 0:22 / /dev rw,relatime - devtmpfs devtmpfs rw\n";
    let err = assert_proc_and_sysfs_mounted(mi).expect_err("missing both must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
    assert!(err.contains("SEAM-13"), "got: {err}");
}

#[test]
fn mountinfo_predicate_rejects_proc_on_wrong_mount_point() {
    // Defence-in-depth: `proc` filesystem mounted at /tmp/proc must NOT
    // satisfy the assertion. The contract is `proc` AT `/proc`.
    let mi = "\
21 19 0:20 / /tmp/proc rw,relatime - proc proc rw
22 19 0:21 / /sys rw,relatime - sysfs sysfs rw
";
    let err = assert_proc_and_sysfs_mounted(mi).expect_err("proc on wrong mount point must fail");
    assert!(err.contains("NO `proc` line"), "got: {err}");
}

#[test]
fn mountinfo_predicate_rejects_sysfs_on_wrong_mount_point() {
    let mi = "\
21 19 0:20 / /proc rw,relatime - proc proc rw
22 19 0:21 / /tmp/sys rw,relatime - sysfs sysfs rw
";
    let err = assert_proc_and_sysfs_mounted(mi).expect_err("sysfs on wrong mount point must fail");
    assert!(err.contains("NO `sysfs` line"), "got: {err}");
}

#[test]
fn mountinfo_predicate_tolerates_blank_and_malformed_lines() {
    // Blank lines and lines without the " - " marker should not cause
    // the parser to fail outright — the contract is "BOTH lines are
    // present", not "no other lines are present".
    // Explicit "\n" prefix instead of a `\` line-continuation followed by
    // a blank source line — clippy's `multiple lines skipped by escaped
    // newline` lints the latter, and the two are equivalent at runtime.
    let mi = "\nthis-line-has-no-marker-and-must-be-skipped\n\
21 19 0:20 / /proc rw,relatime - proc proc rw\n\n\
22 19 0:21 / /sys rw,relatime - sysfs sysfs rw\n\n";
    assert_proc_and_sysfs_mounted(mi).expect("blank + malformed lines must not break parser");
}

#[test]
fn sys_devices_listing_predicate_accepts_canonical_listing() {
    // Realistic listing from a stock Linux /sys/devices.
    let listing = "block\nsystem\nvirtual\n";
    assert_sys_devices_listing_nonempty(listing).expect("canonical listing must pass");
}

#[test]
fn sys_devices_listing_predicate_accepts_single_entry() {
    // Minimal but non-empty: a single device-class root is sufficient
    // evidence that /sys is mounted and readable.
    assert_sys_devices_listing_nonempty("system\n").expect("single-entry listing must pass");
}

#[test]
fn sys_devices_listing_predicate_rejects_empty() {
    let err = assert_sys_devices_listing_nonempty("").expect_err("empty listing must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
    assert!(err.contains("/sys/devices"), "got: {err}");
}

#[test]
fn sys_devices_listing_predicate_rejects_whitespace_only() {
    // `ls` against an unmounted `/sys` could write nothing or just a
    // newline depending on the shell. Whitespace-only must still fail.
    let err = assert_sys_devices_listing_nonempty("\n  \n\t\n")
        .expect_err("whitespace-only listing must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
}

#[test]
fn workload_exit_predicate_accepts_zero() {
    assert_workload_exit_zero(0).expect("exit 0 must pass");
}

#[test]
fn workload_exit_predicate_rejects_one() {
    // The shell `&&` chain falls through to `exit 1` on read failure.
    let err = assert_workload_exit_zero(1).expect_err("exit 1 must fail");
    assert!(err.contains("FC-13 violation"), "got: {err}");
    assert!(err.contains("workload exited 1"), "got: {err}");
}

#[test]
fn workload_exit_predicate_rejects_signal_terminations() {
    // 128 + signal — cellos-init's exit-code convention for kills.
    let err = assert_workload_exit_zero(137).expect_err("SIGKILL-coded exit must fail");
    assert!(err.contains("workload exited 137"), "got: {err}");
}

// ── Linux-only e2e harness ─────────────────────────────────────────────────
//
// Strict acceptance gate from the brief. Gated by:
//
//   * `#[cfg(target_os = "linux")]` — the surrounding crate is Linux-only
//     (lib.rs uses libc + Linux-specific syscalls). On Windows authoring
//     hosts the crate pre-fails to compile so this file never reaches the
//     linker.
//   * `CELLOS_FIRECRACKER_FC13_E2E=1` — opt-in. Skipped on every CI leg
//     that has not run the firecracker-e2e workflow's FC-13 stage. This
//     mirrors the FC-14 / FC-34 skip-on-no-Firecracker pattern.
//
// Fixture files dropped by the workflow:
//
//   * `CELLOS_FIRECRACKER_FC13_MOUNTINFO_PATH` — captured
//     `/proc/self/mountinfo` from inside the cell (the workload's
//     `cat /proc/self/mountinfo > /shared/mountinfo.out` output).
//   * `CELLOS_FIRECRACKER_FC13_SYS_DEVICES_PATH` — captured
//     `ls /sys/devices` listing (the workload's
//     `ls /sys/devices > /shared/sys-devices.out` output).
//   * `CELLOS_FIRECRACKER_FC13_EXIT_CODE` — the workload's exit code as
//     a decimal string. The supervisor receives this over vsock per
//     FC-19 and the workflow forwards it to the test as an env var.
//
// Decoupling capture from assertion lets the same predicate code regress
// against fixture data without any of the Rust test code needing to spin
// up a VM itself.

#[cfg(target_os = "linux")]
fn fc13_e2e_opted_in() -> bool {
    std::env::var("CELLOS_FIRECRACKER_FC13_E2E")
        .map(|v| v.trim() == "1")
        .unwrap_or(false)
}

#[cfg(target_os = "linux")]
fn read_fixture_file(env_var: &str) -> String {
    let path = std::env::var(env_var).unwrap_or_else(|_| {
        panic!(
            "{env_var} must be set when CELLOS_FIRECRACKER_FC13_E2E=1; the \
             firecracker-e2e workflow's FC-13 stage drops this file"
        )
    });
    if !std::path::Path::new(&path).exists() {
        panic!(
            "FC-13 fixture at {path:?} (env {env_var}) does not exist. \
             The firecracker-e2e workflow must have produced this file \
             before invoking `cargo test -p cellos-host-firecracker --test \
             mountinfo_assertion`. Most likely cause: the in-VM workload \
             did not reach `exit 0` (so the && chain short-circuited and \
             the redirect target was never written) — which is itself the \
             FC-13 negative case. Check the workload's exit code first."
        );
    }
    std::fs::read_to_string(&path)
        .unwrap_or_else(|e| panic!("failed to read FC-13 fixture at {path:?} (env {env_var}): {e}"))
}

#[cfg(target_os = "linux")]
fn read_workload_exit_code() -> i32 {
    let raw = std::env::var("CELLOS_FIRECRACKER_FC13_EXIT_CODE").unwrap_or_else(|_| {
        panic!(
            "CELLOS_FIRECRACKER_FC13_EXIT_CODE must be set when \
             CELLOS_FIRECRACKER_FC13_E2E=1 — the firecracker-e2e \
             workflow forwards the workload's vsock-reported exit code \
             into this env var so the assertion can match the brief's \
             shell-chain contract"
        )
    });
    raw.trim().parse::<i32>().unwrap_or_else(|e| {
        panic!(
            "CELLOS_FIRECRACKER_FC13_EXIT_CODE={raw:?} is not a valid \
             decimal i32: {e}"
        )
    })
}

/// FC-13 e2e (Linux + opt-in): the workload exited 0 AND the captured
/// `/proc/self/mountinfo` contains both `proc` (on `/proc`) and `sysfs`
/// (on `/sys`) mount lines AND the captured `/sys/devices` listing is
/// non-empty.
///
/// Skipped (returns Ok) when `CELLOS_FIRECRACKER_FC13_E2E` is unset so
/// this file is safe to include in `cargo test` runs without
/// firecracker. The skip path mirrors `fc14_capbnd_empty.rs` and
/// `fc34_nftables_drops_undeclared.rs`.
#[cfg(target_os = "linux")]
#[test]
fn fc13_proc_and_sys_mounted_before_workload_exec_e2e() {
    if !fc13_e2e_opted_in() {
        eprintln!(
            "skipping FC-13 e2e mountinfo assertion: \
             CELLOS_FIRECRACKER_FC13_E2E not set. This is expected \
             outside the firecracker-e2e CI workflow."
        );
        return;
    }

    // Step 1: workload exit code must be 0. The shell `&&` chain
    // (`cat … && ls … && exit 0; exit 1`) ensures this is a strict
    // proxy for "both reads succeeded inside the VM". We check this
    // FIRST because if it failed, the next two assertions are
    // potentially meaningless (the redirects may not have fired).
    let exit_code = read_workload_exit_code();
    assert_workload_exit_zero(exit_code).unwrap_or_else(|e| {
        panic!(
            "FC-13 workload exit-code invariant failed: {e}\n\
             This means the in-VM `cat /proc/self/mountinfo && ls \
             /sys/devices && exit 0; exit 1` chain fell through to \
             `exit 1` — at least one of /proc or /sys was unreadable \
             when the workload ran."
        )
    });

    // Step 2: captured /proc/self/mountinfo must contain both `proc`
    // (on /proc) and `sysfs` (on /sys) lines.
    let mountinfo = read_fixture_file("CELLOS_FIRECRACKER_FC13_MOUNTINFO_PATH");
    assert_proc_and_sysfs_mounted(&mountinfo).unwrap_or_else(|e| {
        panic!(
            "FC-13 mountinfo invariant failed: {e}\n\
             ----- captured /proc/self/mountinfo -----\n{mountinfo}\n\
             ----- end -----"
        )
    });

    // Step 3: captured /sys/devices listing must be non-empty. This
    // is the brief's "AND attempts to read /sys/devices; both
    // succeed" half — exit 0 already implies `ls` succeeded, but a
    // non-empty listing further proves /sys was mounted as the
    // expected sysfs (not a stub tmpfs).
    let sys_devices = read_fixture_file("CELLOS_FIRECRACKER_FC13_SYS_DEVICES_PATH");
    assert_sys_devices_listing_nonempty(&sys_devices).unwrap_or_else(|e| {
        panic!(
            "FC-13 /sys/devices listing invariant failed: {e}\n\
             ----- captured /sys/devices listing -----\n{sys_devices}\n\
             ----- end -----"
        )
    });
}

/// FC-13 negative test (Linux + opt-in): when the e2e workflow runs the
/// "no-init-mount" failure-injection variant, the workload's exit code
/// MUST be non-zero. The brief calls this out as a distinguishable error
/// covered by a negative test.
///
/// Activated by `CELLOS_FIRECRACKER_FC13_NEGATIVE=1` (separate from the
/// positive opt-in so the workflow can run them in parallel without the
/// negative case poisoning the positive assertion). Skipped otherwise.
#[cfg(target_os = "linux")]
#[test]
fn fc13_negative_no_mount_workload_fails_distinguishably_e2e() {
    let opt_in = std::env::var("CELLOS_FIRECRACKER_FC13_NEGATIVE")
        .map(|v| v.trim() == "1")
        .unwrap_or(false);
    if !opt_in {
        eprintln!(
            "skipping FC-13 negative-leg e2e: \
             CELLOS_FIRECRACKER_FC13_NEGATIVE not set. This is expected \
             outside the firecracker-e2e CI workflow's failure-injection \
             stage."
        );
        return;
    }

    let exit_code = read_workload_exit_code();
    if exit_code == 0 {
        panic!(
            "FC-13 negative-leg violation: workload exited 0 even though \
             the failure-injection harness disabled mount_proc_and_sys(). \
             This means /proc and /sys appear to be mounted by some other \
             code path — investigate before trusting the positive-leg \
             evidence. exit_code={exit_code}"
        );
    }
    // We deliberately do NOT assert the captured fixture files exist on
    // the negative leg: the `&&` chain short-circuited so the redirect
    // targets were never written. The exit code IS the evidence.
}