bvisor/contract/qualification.rs
1//! The QUALIFICATION axis (proof-spine §0/§1) — ORTHOGONAL to the RuntimeGuarantee
2//! axis ([`crate::contract::capability::Enforcement`]). Never collapse them.
3//!
4//! - **RuntimeGuarantee** ([`Enforcement`]): what a backend CLAIMS it can guarantee
5//! for a particular plan + machine profile.
6//! - **QualificationStatus** (this module): what the REPOSITORY has INDEPENDENTLY
7//! qualified through the complete path `spec → admission → lowering → execution →
8//! independent observation`, INCLUDING the fail-closed / teardown branches.
9//!
10//! THE COUPLING LAW: a production profile may advertise [`Enforcement::Enforced`]
11//! for a requirement key ONLY when the committed qualification ledger holds
12//! [`QualificationStatus::Proven`] for that key — with a profile floor the machine
13//! satisfies and a matching mechanism digest. The coupling is enforced as a CI
14//! GATE: the backend's ceiling claim is explicit; the ledger is DERIVED from proof
15//! receipts; the gate asserts they agree. A backend can never self-stamp `Proven`.
16//!
17//! This is the structural fix for the over-claim class: the family `support_matrix`
18//! may CLAIM `Enforced` as aspiration, but production advertises it only when the
19//! claim is `Proven` — so an unproven advertised cell is an `Incomplete`
20//! qualification, not a lie.
21
22use crate::contract::capability::Enforcement;
23use crate::contract::support::RequirementKind;
24use serde::{Deserialize, Serialize};
25
26/// What the repository has INDEPENDENTLY qualified about a capability claim.
27/// Distinct from [`Enforcement`] (the runtime claim a backend makes).
28///
29/// Serialized to/from the committed qualification ledger (kebab-case) so the
30/// integrity coupling gate — which cannot depend on this crate — reads it as data.
31#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33#[non_exhaustive]
34pub enum QualificationStatus {
35 /// The complete contract path PLUS its fail-closed / teardown branches were
36 /// observed by an INDEPENDENT oracle (host-side kernel-state preferred over
37 /// workload self-report; dual-channel). The ONLY status that permits a
38 /// production `Enforced` claim, for the matching key + profile-floor + digest.
39 Proven,
40 /// Explicitly unsupported on this platform/profile; planning REFUSES before
41 /// execution. The fail-closed default for any unproven capability. NOT a
42 /// waiver — a platform limitation is `FailClosed`, never `Waived`.
43 FailClosed,
44 /// Some layers exist (a mechanism, a partial path) but the claimed guarantee
45 /// is NOT proven end-to-end. May not be advertised `Enforced` in production.
46 Incomplete,
47 /// Mechanically impossible to witness, OR deliberately excluded — carried as an
48 /// owner-signed, EXPIRING waiver (owner + reason + expiry). NEVER describes a
49 /// platform capability limitation.
50 Waived,
51 /// Test-only support that exists to PROVE detection (a red fixture's planted
52 /// lie). Never a production claim.
53 FaultInjected,
54}
55
56impl QualificationStatus {
57 /// The coupling-law predicate: whether this qualification permits a production
58 /// [`Enforcement::Enforced`] runtime claim. ONLY [`Self::Proven`] does.
59 #[must_use]
60 pub fn permits_enforced(self) -> bool {
61 matches!(self, Self::Proven)
62 }
63}
64
65/// The coupling-law check at a single (key, profile, claim) point: a runtime
66/// [`Enforcement`] claim is admissible against a qualification only when either the
67/// claim is not `Enforced`, or the qualification is `Proven`. (The full gate also
68/// matches the profile floor and mechanism digest — those live in the ledger + the
69/// integrity gate; this is the pointwise core, kept here so it is unit-testable and
70/// shared by the runtime if it ever consults a qualification.)
71#[must_use]
72pub fn enforced_claim_is_qualified(claim: Enforcement, qualification: QualificationStatus) -> bool {
73 match claim {
74 Enforcement::Enforced => qualification.permits_enforced(),
75 // A backend may always claim a weaker guarantee than it has proven.
76 Enforcement::Mediated | Enforcement::Unsupported => true,
77 }
78}
79
80/// A `blake3` digest of a backend's MECHANISM string for a requirement key (§1's
81/// `H_M`). The coupling law binds a production `Enforced` claim to a `Proven`
82/// ledger row ONLY when the row's digest matches the digest of the mechanism the
83/// backend would actually use — so a backend cannot satisfy the gate by swapping
84/// in a different (unproven) mechanism under the same key.
85///
86/// Derived with [`batpak::event::hash::compute_hash`] (the same blake3 the
87/// launcher attestation uses), over the UTF-8 bytes of the mechanism string a
88/// backend authors in its `mechanism(req, enforcement)` (e.g.
89/// `"linux:landlock:Enforced"`).
90#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
91pub struct MechanismDigest(pub [u8; 32]);
92
93impl MechanismDigest {
94 /// Derive the digest from a backend's mechanism string (the exact bytes a
95 /// backend's `mechanism(req, enforcement)` returns, e.g.
96 /// `"linux:landlock:Enforced"`).
97 #[must_use]
98 pub fn of_mechanism(mechanism: &str) -> Self {
99 Self(batpak::event::hash::compute_hash(mechanism.as_bytes()))
100 }
101
102 /// The lowercase hex spelling of the 32-byte digest (for the committed ledger
103 /// + diagnostics). Deterministic, so a ledger row's digest is one stable line.
104 #[must_use]
105 pub fn to_hex(self) -> String {
106 self.0.iter().map(|b| format!("{b:02x}")).collect()
107 }
108}
109
110/// The MINIMUM machine facts a qualification covers (§3 PROFILE-CLASS): the floor
111/// predicate a production profile must DOMINATE (`p_prod ⊒ floor`) for a `Proven`
112/// receipt earned at the floor to transfer to it.
113///
114/// A qualification is earned on ONE probed CI runner, but production runs
115/// elsewhere; an exact-profile digest would never transfer. So the floor states
116/// the LEAST the mechanism needs, and the load-bearing argument is that proving AT
117/// the floor generalizes UPWARD — a stronger machine still satisfies every minimum.
118/// `satisfied_by` is exactly that domination check.
119#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
120pub struct ProfileFloor {
121 /// The minimum landlock ABI the mechanism requires (`None` = landlock is not
122 /// part of this floor — e.g. a cgroup-only Kill cell does not need landlock).
123 pub landlock_abi_min: Option<u8>,
124 /// The mechanism requires atomic `cgroup.kill` run-tree teardown.
125 pub requires_cgroup_kill: bool,
126 /// The mechanism requires the `pids.peak` usage witness (cgroup v2 ≥ 6.1).
127 pub requires_pids_peak: bool,
128 /// The mechanism requires UNPRIVILEGED user + network namespace creation (the
129 /// `NetworkDenyAll` empty-netns floor, S9 / D3): an unprivileged process may create a
130 /// new netns ONLY when it is also root in a new userns (the S8 rendezvous). A kernel
131 /// that forbids unprivileged userns cannot realize the empty netns, so the cell is
132 /// FAIL_CLOSED there — never a silent pass.
133 pub requires_unprivileged_userns: bool,
134 /// The mechanism requires installing a seccomp BPF FILTER (the
135 /// `ChildSpawn::DenyNewTasks` floor, S10): a kernel without `CONFIG_SECCOMP_FILTER`
136 /// cannot install the task-creation denylist, so the cell is FAIL_CLOSED there — never
137 /// a silent unfiltered run.
138 pub requires_seccomp_filter: bool,
139}
140
141impl ProfileFloor {
142 /// The empty floor: no minimum machine facts (structural mechanisms that hold
143 /// on every machine of the platform — e.g. process spawn / pipe capture).
144 #[must_use]
145 pub const fn structural() -> Self {
146 Self {
147 landlock_abi_min: None,
148 requires_cgroup_kill: false,
149 requires_pids_peak: false,
150 requires_unprivileged_userns: false,
151 requires_seccomp_filter: false,
152 }
153 }
154
155 /// The empty-netns `NetworkDenyAll` floor (S9 / D3): requires unprivileged
156 /// user + network namespace creation, no landlock / cgroup minimum. Structural
157 /// otherwise (the empty netns holds on any kernel that permits unprivileged userns).
158 #[must_use]
159 pub const fn unprivileged_userns_netns() -> Self {
160 Self {
161 landlock_abi_min: None,
162 requires_cgroup_kill: false,
163 requires_pids_peak: false,
164 requires_unprivileged_userns: true,
165 requires_seccomp_filter: false,
166 }
167 }
168
169 /// The `ChildSpawn::DenyNewTasks` floor (S10): requires seccomp FILTER support. No
170 /// landlock / cgroup / userns minimum — the task-creation denylist is structural above
171 /// that floor (it holds on any kernel with `CONFIG_SECCOMP_FILTER`), so the proof
172 /// transfers upward. Below the floor the cell drops from the ceiling (fail-closed).
173 #[must_use]
174 pub const fn seccomp_filter() -> Self {
175 Self {
176 landlock_abi_min: None,
177 requires_cgroup_kill: false,
178 requires_pids_peak: false,
179 requires_unprivileged_userns: false,
180 requires_seccomp_filter: true,
181 }
182 }
183
184 /// The `ChildSpawn::AllowDescendantsWithinBoundary` floor (S10): requires the cgroup
185 /// `cgroup.kill` boundary (the S1 Kill mechanism the descendant inherits), no seccomp /
186 /// landlock / userns minimum — the descendant is killable + counted + namespace-trapped
187 /// by the cgroup. Below the floor (no cgroup) the cell drops from the ceiling.
188 #[must_use]
189 pub const fn cgroup_descendant_boundary() -> Self {
190 Self {
191 landlock_abi_min: None,
192 requires_cgroup_kill: true,
193 requires_pids_peak: false,
194 requires_unprivileged_userns: false,
195 requires_seccomp_filter: false,
196 }
197 }
198
199 /// THE §3 DOMINATION CHECK: whether the concrete machine `facts` satisfy every
200 /// minimum this floor requires (`facts ⊒ self`). A production profile may
201 /// advertise the qualification's key ONLY when this holds — so a receipt earned
202 /// at the floor transfers UPWARD to any stronger machine but NEVER downward to a
203 /// weaker one.
204 #[must_use]
205 pub fn satisfied_by(&self, facts: &ProfileFacts) -> bool {
206 if let Some(min) = self.landlock_abi_min {
207 if facts.landlock_abi < i64::from(min) {
208 return false;
209 }
210 }
211 if self.requires_cgroup_kill && !facts.has_cgroup_kill {
212 return false;
213 }
214 if self.requires_pids_peak && !facts.has_pids_peak {
215 return false;
216 }
217 if self.requires_unprivileged_userns && !facts.has_unprivileged_userns {
218 return false;
219 }
220 if self.requires_seccomp_filter && !facts.has_seccomp_filter {
221 return false;
222 }
223 true
224 }
225}
226
227/// The concrete, TYPED machine facts a [`ProfileFloor`] is checked against — the
228/// probe truths a backend derives once at construction (the live landlock ABI, and
229/// whether the kernel exposes atomic `cgroup.kill` / the `pids.peak` witness).
230///
231/// Distinct from [`crate::contract::support::BackendProfile`] (the per-kind
232/// enforcement CEILING): the ceiling is the OUTPUT of these facts, while the floor
233/// predicate is stated over the facts THEMSELVES (the ABI integer, the cgroup
234/// presence) — a ceiling has no ABI integer to compare a floor against.
235#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
236pub struct ProfileFacts {
237 /// The live landlock ABI integer probed from the kernel (`0` = unavailable).
238 pub landlock_abi: i64,
239 /// Whether the probed cgroup base exposes atomic `cgroup.kill`.
240 pub has_cgroup_kill: bool,
241 /// Whether the probed cgroup base exposes the `pids.peak` usage witness.
242 pub has_pids_peak: bool,
243 /// Whether the host permits UNPRIVILEGED user + network namespace creation (the
244 /// `NetworkDenyAll` empty-netns floor, S9 / D3). Probed once at construction
245 /// (`unprivileged_userns_available`); `false` ⇒ the empty-netns cell fails closed.
246 pub has_unprivileged_userns: bool,
247 /// Whether the host supports installing a seccomp BPF FILTER (the
248 /// `ChildSpawn::DenyNewTasks` floor, S10). Probed once at construction
249 /// (`seccomp_filter_available`); `false` ⇒ the task-creation-deny cell fails closed.
250 pub has_seccomp_filter: bool,
251}
252
253/// One committed row of the qualification LEDGER (§1 `Qualification(p,k)` joined to
254/// its profile-class + mechanism digest + proof receipt). The ledger is the
255/// REPOSITORY's independent record; a backend never authors it.
256///
257/// A row may carry [`QualificationStatus::Proven`] ONLY if every entry in
258/// `proof_receipts` cites a REAL, currently-passing oracle test (`path::fn`) — and
259/// together they prove the COMPLETE §4 contract path INCLUDING the fail-closed /
260/// teardown branch (a cell's guarantee-holds and its setup-failure ⇒ no-effect are
261/// usually distinct oracles). The coupling test enforces the §1 law: every
262/// production-`Enforced` ceiling cell must have a `Proven` row here whose
263/// [`ProfileFloor`] the running profile satisfies AND whose [`MechanismDigest`]
264/// matches the backend's mechanism for that key; the proof-receipt RESOLVER
265/// (`coupling_proof.rs`) additionally fails CI if any cited receipt is a ghost.
266#[derive(Clone, Debug, PartialEq, Eq)]
267pub struct QualificationRow {
268 /// The backend family id this qualification was earned for (e.g. `"linux"`).
269 pub backend: &'static str,
270 /// The requirement key this row qualifies.
271 pub key: RequirementKind,
272 /// The minimum machine facts the proof generalizes from (§3).
273 pub profile_floor: ProfileFloor,
274 /// The exact backend MECHANISM STRING this row proves (e.g.
275 /// `"linux:landlock:Enforced"`). Stored as the source string — not a
276 /// pre-baked digest — because blake3 is not `const`; the coupling test
277 /// re-derives [`MechanismDigest::of_mechanism`] from this AND from the
278 /// backend's live `mechanism(req, enforcement)` and asserts they MATCH, so a
279 /// backend cannot satisfy the gate under a different (unproven) mechanism.
280 pub mechanism: &'static str,
281 /// What the repository independently qualified for this cell.
282 pub status: QualificationStatus,
283 /// The proof receipts: the `path::fn`s of the oracle tests that TOGETHER prove
284 /// the cell's complete §4 path (guarantee-holds + fail-closed branch). Empty for
285 /// non-`Proven` rows. Every entry is machine-resolved to a real `#[test]` by the
286 /// resolver gate — a `Proven` row may NOT cite a ghost.
287 pub proof_receipts: &'static [&'static str],
288}
289
290impl QualificationRow {
291 /// The blake3 [`MechanismDigest`] (§1 `H_M`) of this row's committed mechanism
292 /// string — the digest the coupling gate matches the backend's live mechanism
293 /// against.
294 #[must_use]
295 pub fn mechanism_digest(&self) -> MechanismDigest {
296 MechanismDigest::of_mechanism(self.mechanism)
297 }
298}
299
300/// Derive the [`MechanismDigest`] for a Linux mechanism string built the way
301/// `backend_impl::mechanism` builds it: `"{id}:{primitive}:{enforcement:?}"`. The
302/// ledger is committed `const`, but a digest is a runtime blake3, so the ledger
303/// stores the SOURCE mechanism string and the coupling test re-derives the digest;
304/// this helper keeps the two spellings (ledger vs. backend) in one place.
305#[must_use]
306pub fn linux_mechanism(primitive: &str, enforcement: Enforcement) -> String {
307 format!("linux:{primitive}:{enforcement:?}")
308}
309
310/// Derive the [`MechanismDigest`] source string for a Wasm mechanism.
311#[must_use]
312pub fn wasm_mechanism(primitive: &str, enforcement: Enforcement) -> String {
313 format!("wasm:{primitive}:{enforcement:?}")
314}
315
316/// The COMMITTED Linux qualification ledger (S1): the CURRENTLY-PROVEN cells, each
317/// citing its real passing oracle(s), plus the explicitly-stated non-proven cells.
318///
319/// PROVEN rows cite a real, currently-passing oracle that proves the complete
320/// contract path (verified to exist in `crates/bvisor/tests/`):
321/// - `Filesystem` — landlock denial proven on-disk by the independent G-grid oracle
322/// (above the ABI floor; below it the cell is fail-closed, never advertised).
323/// - `Kill` — atomic `cgroup.kill` teardown DRAINS the tree to empty (observed via
324/// the bounded `wait_until_empty` poll), paired with the pids cap proof.
325/// - `LaunchWorkload` / `CaptureStreams` — the launcher spawns + the host captures
326/// the workload's own stdout/stderr cleanly (no launcher contamination).
327/// - `process_count` budget — the cgroup `pids.max` cap GENUINELY denies forks past
328/// the cap (kernel `pids.events max` counter), witnessed from `pids.peak`.
329/// (process_count is a budget dimension, not a `RequirementKind`; it rides the
330/// `Kill` cell's cgroup floor, so it is documented here, not a separate row.)
331///
332/// `Environment` (explicit envp) — the admitted `Environment::Exact` policy is lowered
333/// to the launcher's explicit env (literals plus parent-resolved secret leases); the
334/// child env EQUALS the admitted table EXACTLY, witnessed by the host reading the
335/// child's `proc` environ AND the workload's own self-report (dual channel, no ambient
336/// leak), with the fail-closed branches (an unresolvable lease means the target never
337/// runs; an invalid policy means admission refuses) proven by the same oracle.
338///
339/// `InheritedFdsNone` (fd-scrub) — the admitted `FdPolicy::None` drives the launcher's
340/// child-side fd-scrub; the child's open fds (read HOST-SIDE from `/proc/<pid>/fd`)
341/// contain ONLY the declared allowlist, a parent-opened non-CLOEXEC sentinel fd is ABSENT
342/// (scrubbed), with the fail-closed branches (an undeclared fd is scrubbed before the
343/// workload; an unrealized fd policy ⇒ the target never runs) proven by the oracle.
344///
345/// `NetworkDenyAll` (empty netns, S9 / D3) — the admitted `NetPolicy::DenyAll` engages a
346/// NEW, EMPTY network namespace (`CLONE_NEWNET`, alongside the S8 userns rendezvous it
347/// requires); the netns has NO external interface, witnessed HOST-SIDE (the host reads the
348/// CHILD's `/proc/<pid>/net/dev` and sees ONLY `lo`) AND by the workload's own self-report
349/// (it cannot reach the network), with the launcher's own control channel still working
350/// through the netns (HostControl carve-out — fd-passed sockets are unaffected). Fail-closed
351/// branches: a kernel without unprivileged userns+netns ⇒ the cell SKIPs LOUD (floor not
352/// met), and an unrealized `AllowList` ⇒ the target never runs. `NetworkAllowList` STAYS
353/// `FailClosed` (no broker in v1).
354///
355/// NON-PROVEN cells are stated explicitly (the coupling test asserts they are NOT
356/// advertised `Enforced` in production): `InheritedFdsOnly` is `Incomplete` (the scrub
357/// realizes only `None`; the selective-keep allowlist has no lowering + no oracle); every
358/// other capability — including `NetworkAllowList` (no broker in v1) and all THREE
359/// `ChildSpawn` child-task keys (proof-spine §2 split + the S6 3-variant freeze) — is
360/// `FailClosed`.
361pub const LINUX_QUALIFICATION_LEDGER: &[QualificationRow] = &[
362 QualificationRow {
363 backend: "linux",
364 key: RequirementKind::Filesystem,
365 // Landlock ABI v1 already enforces path-beneath read/write/execute — the
366 // backend's floor. The proof transfers to any higher ABI.
367 profile_floor: ProfileFloor {
368 landlock_abi_min: Some(1),
369 requires_cgroup_kill: false,
370 requires_pids_peak: false,
371 requires_unprivileged_userns: false,
372 requires_seccomp_filter: false,
373 },
374 mechanism: "linux:landlock:Enforced",
375 status: QualificationStatus::Proven,
376 // Guarantee-holds: independent on-disk G-grid denial oracle. Fail-closed:
377 // below the ABI floor the cell drops from the ceiling (the coupling test
378 // `below_floor_profile_drops_filesystem_from_the_ceiling` proves it).
379 proof_receipts: &[
380 "crates/bvisor/tests/grid_linux_fs.rs::g1_landlock_denies_secret_read_outside_declared_root",
381 ],
382 },
383 QualificationRow {
384 backend: "linux",
385 key: RequirementKind::LaunchWorkload,
386 profile_floor: ProfileFloor::structural(),
387 mechanism: "linux:process_spawn:Enforced",
388 status: QualificationStatus::Proven,
389 // §4 BOTH branches: guarantee-holds (a workload launches + runs to a verdict)
390 // AND fail-closed (a setup problem REFUSES before any child — the target
391 // never runs).
392 proof_receipts: &[
393 "crates/bvisor/tests/launcher_capture_linux.rs::launcher_captures_workload_streams_cleanly_and_deterministically",
394 "crates/bvisor/tests/launcher_skeleton_linux.rs::missing_primitive_refuses_before_any_child",
395 ],
396 },
397 QualificationRow {
398 backend: "linux",
399 key: RequirementKind::CaptureStreams,
400 profile_floor: ProfileFloor::structural(),
401 mechanism: "linux:pipe_capture:Enforced",
402 status: QualificationStatus::Proven,
403 proof_receipts: &[
404 "crates/bvisor/tests/launcher_capture_linux.rs::launcher_captures_workload_streams_cleanly_and_deterministically",
405 ],
406 },
407 QualificationRow {
408 backend: "linux",
409 key: RequirementKind::Kill,
410 // Kill rides cgroup v2 atomic `cgroup.kill`; no landlock minimum.
411 profile_floor: ProfileFloor {
412 landlock_abi_min: None,
413 requires_cgroup_kill: true,
414 requires_pids_peak: false,
415 requires_unprivileged_userns: false,
416 requires_seccomp_filter: false,
417 },
418 mechanism: "linux:cgroup_kill:Enforced",
419 status: QualificationStatus::Proven,
420 // §4: host-state oracle (kernel pids.events) + workload exit (dual channel)
421 // AND teardown observed (cgroup.kill drains the tree to empty).
422 proof_receipts: &[
423 "crates/bvisor/tests/cgroup_enforcement_linux.rs::pids_max_genuinely_denies_forks_past_the_cap_or_explicit_skip",
424 ],
425 },
426 // Non-proven cells, status stated explicitly. These keys are absent from the
427 // production ceiling, so the coupling test never demands a Proven row for them;
428 // they are listed so the ledger states the qualification status explicitly. The
429 // mechanism string for an unimplemented cell mirrors `backend_impl::mechanism`'s
430 // `"none/unimplemented-this-chunk"` primitive (Unsupported in the ceiling).
431 QualificationRow {
432 backend: "linux",
433 key: RequirementKind::Environment,
434 // Explicit-envp lowering is structural (no kernel-version floor): the launcher
435 // serves the admitted env to fexecve on any Linux. The proof transfers to every
436 // machine of the platform.
437 profile_floor: ProfileFloor::structural(),
438 mechanism: "linux:explicit_env:Enforced",
439 status: QualificationStatus::Proven,
440 // §4 BOTH branches, dual-channel:
441 // - guarantee-holds: the child's env EQUALS the admitted table EXACTLY,
442 // witnessed by the HOST reading /proc/<child_pid>/environ (kernel state,
443 // independent) AND the workload's own reported env; a parent sentinel var is
444 // ABSENT in the child (no ambient leak); a SecretLease resolves IN THE CHILD
445 // while the serialized plan+report carry only the REF, never the value;
446 // - fail-closed: an unresolvable lease ⇒ the target NEVER runs (no child
447 // output), and a contract-invalid policy ⇒ admission refuses before execution.
448 proof_receipts: &[
449 "crates/bvisor/tests/env_exact_linux.rs::child_env_equals_the_admitted_table_with_no_ambient_leak",
450 "crates/bvisor/tests/env_exact_linux.rs::an_unresolvable_lease_fails_closed_and_the_target_never_runs",
451 "crates/bvisor/tests/env_exact_linux.rs::a_contract_invalid_policy_is_refused_before_execution",
452 // The full execute()/BoundaryRunner contract-path witness (vs the launcher-
453 // direct /proc oracle above): the durable plan+report carry only the lease ref.
454 "crates/bvisor/tests/env_exact_linux.rs::a_secret_lease_resolves_but_the_durable_plan_and_report_carry_only_the_ref",
455 ],
456 },
457 QualificationRow {
458 backend: "linux",
459 key: RequirementKind::InheritedFdsNone,
460 // The fd-scrub is structural (no kernel-version floor): the launcher reads
461 // /proc/self/fd + raw SYS_close on any Linux. The proof transfers to every
462 // machine of the platform.
463 profile_floor: ProfileFloor::structural(),
464 mechanism: "linux:fd_scrub:Enforced",
465 status: QualificationStatus::Proven,
466 // §4 BOTH branches, dual-channel:
467 // - guarantee-holds: the child's open fds (read HOST-SIDE from
468 // /proc/<child_pid>/fd — kernel state, independent) contain ONLY the declared
469 // allowlist (stdio); a parent-opened non-CLOEXEC SENTINEL fd is ABSENT from the
470 // child (scrubbed), witnessed both host-side AND by the workload's own attempt
471 // to write to it failing (no leak across the boundary);
472 // - fail-closed: an undeclared inherited fd is scrubbed BEFORE the workload (the
473 // launcher mechanism proof), AND a contract-level setup failure ⇒ the target
474 // NEVER runs (the full execute()/BoundaryRunner path refuses an unrealized fd
475 // policy). The execute()-path witness proves the lowering rides the production
476 // contract, not only a run_launcher-direct plan.
477 proof_receipts: &[
478 "crates/bvisor/tests/inherited_fds_none_linux.rs::child_inherits_only_the_declared_fds_no_sentinel_leak",
479 "crates/bvisor/tests/launcher_inherited_fds_linux.rs::undeclared_inherited_fd_is_scrubbed_before_the_workload",
480 "crates/bvisor/tests/inherited_fds_none_linux.rs::an_unrealized_fd_policy_fails_closed_and_the_target_never_runs",
481 // The full execute()/BoundaryRunner contract-path witness (vs the launcher-
482 // direct /proc oracle above): a None-policy spec runs to a clean verdict.
483 "crates/bvisor/tests/inherited_fds_none_linux.rs::a_none_policy_spec_runs_through_the_execute_path",
484 ],
485 },
486 QualificationRow {
487 backend: "linux",
488 key: RequirementKind::InheritedFdsOnly,
489 profile_floor: ProfileFloor::structural(),
490 mechanism: "linux:none/unimplemented-this-chunk:Unsupported",
491 status: QualificationStatus::Incomplete,
492 proof_receipts: &[],
493 },
494 QualificationRow {
495 backend: "linux",
496 key: RequirementKind::NetworkDenyAll,
497 // The empty-netns floor (S9 / D3): requires UNPRIVILEGED user + network namespace
498 // creation (the S8 rendezvous makes the child root-in-userns; CLONE_NEWNET then
499 // births it into an empty netns). No landlock/cgroup minimum — the empty netns is
500 // structural above that floor, so the proof transfers to any kernel that permits
501 // unprivileged userns. Below the floor the cell drops from the ceiling (fail-closed).
502 profile_floor: ProfileFloor::unprivileged_userns_netns(),
503 mechanism: "linux:empty_netns:Enforced",
504 status: QualificationStatus::Proven,
505 // §4 BOTH branches, DUAL channel (host-side kernel-state strongest, per §4):
506 // - guarantee-holds (A) HOST-SIDE: the host reads the CHILD's netns interface list
507 // from /proc/<child_pid>/net/dev and asserts it contains ONLY `lo` — NO external
508 // interface (the independent "zero external interfaces" witness, kernel state the
509 // launcher cannot forge); (B) WORKLOAD self-report: the workload enumerates its
510 // own interfaces + routing table and OBSERVES it has ZERO routes (only `lo`, which
511 // has no address ⇒ no reachable destination). NO-LEAK: no inherited routable socket
512 // survives (the S5 scrub).
513 // HOSTCONTROL: the launcher's own control channel still works through the netns
514 // (the workload runs to a verdict);
515 // - fail-closed: a kernel without unprivileged userns+netns ⇒ the cell SKIPs LOUD
516 // (never a silent pass), and a contract-level setup failure ⇒ the target never runs
517 // (the empty netns engages on the full execute()/BoundaryRunner path).
518 proof_receipts: &[
519 "crates/bvisor/tests/network_deny_all_linux.rs::host_sees_only_loopback_in_the_child_netns_no_external_interface_or_skip",
520 "crates/bvisor/tests/network_deny_all_linux.rs::workload_cannot_reach_the_network_from_the_empty_netns_or_skip",
521 "crates/bvisor/tests/network_deny_all_linux.rs::a_deny_all_spec_runs_through_the_execute_path_or_skip",
522 "crates/bvisor/tests/network_deny_all_linux.rs::network_allow_list_fails_closed_at_admission_the_target_never_runs",
523 ],
524 },
525 QualificationRow {
526 backend: "linux",
527 key: RequirementKind::NetworkAllowList,
528 profile_floor: ProfileFloor::structural(),
529 mechanism: "linux:none/unimplemented-this-chunk:Unsupported",
530 status: QualificationStatus::FailClosed,
531 proof_receipts: &[],
532 },
533 // The three FROZEN child-task semantics (proof-spine S6). S6 froze the taxonomy +
534 // the clone3-pointer/classic-BPF enforcement CONSTRAINT (see `SpawnPolicy`); S10
535 // realizes two of the three and keeps the unenforceable one FailClosed:
536 //
537 // - DenyNewTasks → PROVEN via a seccomp DENYLIST (deny clone/clone3/fork/vfork at the
538 // syscall-NUMBER level — no clone3 arg-deref). Floor = seccomp filter support.
539 // - AllowDescendants → PROVEN via the CGROUP boundary (the descendant inherits the run
540 // cgroup ⇒ killable via cgroup.kill, counted by pids.max, namespace-trapped — NOT
541 // seccomp). Floor = cgroup.kill.
542 // - AllowThreads → STAYS FailClosed: the clone3-pointer/classic-BPF problem (S6) means
543 // seccomp cannot permit-threads-but-deny-processes precisely, and denying clone3
544 // outright breaks glibc threads. The OPEN enforcement problem — NOT faked, absent
545 // from the ceiling (Unsupported), so the coupling gate never demands a Proven row.
546 QualificationRow {
547 backend: "linux",
548 key: RequirementKind::ChildSpawnDenyNewTasks,
549 // The seccomp-filter floor (S10): requires CONFIG_SECCOMP_FILTER. No landlock /
550 // cgroup / userns minimum — the task-creation denylist is structural above that
551 // floor (it holds on any kernel with seccomp filter mode), so the proof transfers
552 // upward. Below the floor the cell drops from the ceiling (fail-closed).
553 profile_floor: ProfileFloor::seccomp_filter(),
554 mechanism: "linux:seccomp_deny_tasks:Enforced",
555 status: QualificationStatus::Proven,
556 // §4 BOTH branches, DUAL channel (host-side kernel-state strongest, per §4):
557 // - guarantee-holds (A) HOST-SIDE: the host reads the CHILD's /proc/<pid>/status
558 // and observes `Seccomp: 2` (filter mode installed — the kernel-state witness the
559 // launcher cannot forge); (B) WORKLOAD self-report: the workload attempts to spawn
560 // a child and OBSERVES the fork/spawn is REFUSED (EPERM). NO-LEAK: the filter is
561 // installed LAST, after landlock, before fexecve;
562 // - fail-closed: a kernel without seccomp filter support ⇒ the cell SKIPs LOUD
563 // (never a silent pass), and a contract-level setup failure ⇒ the target never
564 // runs (the seccomp denylist engages on the full execute()/BoundaryRunner path).
565 proof_receipts: &[
566 "crates/bvisor/tests/child_spawn_linux.rs::deny_new_tasks_fork_is_refused_and_host_sees_seccomp_filter_or_skip",
567 "crates/bvisor/tests/child_spawn_linux.rs::a_deny_new_tasks_spec_runs_through_the_execute_path_or_skip",
568 "crates/bvisor/tests/child_spawn_linux.rs::allow_threads_fails_closed_at_admission_the_target_never_runs",
569 ],
570 },
571 QualificationRow {
572 backend: "linux",
573 key: RequirementKind::ChildSpawnAllowThreads,
574 // STAYS FailClosed: the open clone3-pointer/classic-BPF problem (S6). NOT faked.
575 profile_floor: ProfileFloor::structural(),
576 mechanism: "linux:none/unimplemented-this-chunk:Unsupported",
577 status: QualificationStatus::FailClosed,
578 proof_receipts: &[],
579 },
580 QualificationRow {
581 backend: "linux",
582 key: RequirementKind::ChildSpawnAllowDescendants,
583 // The cgroup-boundary floor (S10): requires cgroup.kill (the descendant inherits the
584 // run cgroup ⇒ killable + counted + namespace-trapped — NOT seccomp). Below the
585 // floor (no cgroup) the cell drops from the ceiling (fail-closed).
586 profile_floor: ProfileFloor::cgroup_descendant_boundary(),
587 mechanism: "linux:cgroup_descendant_boundary:Enforced",
588 status: QualificationStatus::Proven,
589 // §4 BOTH branches:
590 // - guarantee-holds: the workload (placed in the run cgroup via CLONE_INTO_CGROUP — the
591 // SAME placement the S1 Kill cell proves) spawns a DESCENDANT that OUTLIVES it; the
592 // descendant is in the leaf by the kernel fork-membership guarantee (it cannot leave
593 // its cgroup without a privileged cgroup.procs write it lacks), and cgroup.kill reaps
594 // the WHOLE leaf to empty (the S1 drain-to-empty witness — the OBSERVED branch). A
595 // direct per-descendant cgroup.procs membership read is a tracked §4-quality
596 // strengthening; today the descendant claim rests on the fork-inheritance guarantee +
597 // the whole-tree drain, not a direct membership observation.
598 // - fail-closed: no cgroup base ⇒ the cell SKIPs LOUD (absent from the ceiling), and
599 // a contract-level setup failure ⇒ the target never runs.
600 proof_receipts: &[
601 "crates/bvisor/tests/child_spawn_linux.rs::allow_descendants_is_cgroup_confined_and_cgroup_kill_drains_the_tree_or_skip",
602 "crates/bvisor/tests/child_spawn_linux.rs::an_allow_descendants_spec_runs_through_the_execute_path_or_skip",
603 ],
604 },
605];
606
607/// Look up the committed Linux ledger row for a requirement key (the lookup the
608/// coupling gate uses). `None` ⇒ the key is not in the ledger at all (which, for an
609/// `Enforced` production cell, is itself a coupling violation).
610#[must_use]
611pub fn linux_ledger_row(key: RequirementKind) -> Option<&'static QualificationRow> {
612 LINUX_QUALIFICATION_LEDGER.iter().find(|r| r.key == key)
613}
614
615/// The committed Wasm qualification ledger. Every production `Enforced` Wasm
616/// ceiling cell has a `Proven` row backed by the live grid oracle; structurally
617/// unsupported native cells are stated explicitly and stay absent from the ceiling.
618pub const WASM_QUALIFICATION_LEDGER: &[QualificationRow] = &[
619 QualificationRow {
620 backend: "wasm",
621 key: RequirementKind::Filesystem,
622 profile_floor: ProfileFloor::structural(),
623 mechanism: "wasm:wasi_preopen:Enforced",
624 status: QualificationStatus::Proven,
625 proof_receipts: &[
626 "crates/bvisor/tests/grid_wasm.rs::g1_wasi_preopen_denies_secret_read_outside_declared_root",
627 "crates/bvisor/tests/grid_wasm.rs::g3_wasi_preopen_denies_write_outside_quarantine",
628 ],
629 },
630 QualificationRow {
631 backend: "wasm",
632 key: RequirementKind::LaunchWorkload,
633 profile_floor: ProfileFloor::structural(),
634 mechanism: "wasm:wasi_instantiate:Enforced",
635 status: QualificationStatus::Proven,
636 proof_receipts: &[
637 "crates/bvisor/tests/grid_wasm.rs::wasm_profile_admits_declared_roots_spec",
638 "crates/bvisor/tests/grid_wasm.rs::g7_wasi_preopen_commit_allows_in_root_output",
639 ],
640 },
641 QualificationRow {
642 backend: "wasm",
643 key: RequirementKind::CaptureStreams,
644 profile_floor: ProfileFloor::structural(),
645 mechanism: "wasm:wasi_stdio:Enforced",
646 status: QualificationStatus::Proven,
647 proof_receipts: &[
648 "crates/bvisor/tests/grid_wasm.rs::wasi_capture_streams_records_guest_output",
649 ],
650 },
651 QualificationRow {
652 backend: "wasm",
653 key: RequirementKind::Environment,
654 profile_floor: ProfileFloor::structural(),
655 mechanism: "wasm:wasi_env:Enforced",
656 status: QualificationStatus::Proven,
657 proof_receipts: &[
658 "crates/bvisor/tests/grid_wasm.rs::wasi_env_exact_is_delivered_to_guest",
659 ],
660 },
661 QualificationRow {
662 backend: "wasm",
663 key: RequirementKind::TempRoot,
664 profile_floor: ProfileFloor::structural(),
665 mechanism: "wasm:wasi_preopen_tmp:Enforced",
666 status: QualificationStatus::Proven,
667 proof_receipts: &[
668 "crates/bvisor/tests/grid_wasm.rs::wasi_temp_root_is_private_and_writable",
669 ],
670 },
671 QualificationRow {
672 backend: "wasm",
673 key: RequirementKind::CommitArtifact,
674 profile_floor: ProfileFloor::structural(),
675 mechanism: "wasm:preopen_commit:Enforced",
676 status: QualificationStatus::Proven,
677 proof_receipts: &[
678 "crates/bvisor/tests/grid_wasm.rs::g7_wasi_preopen_commit_allows_in_root_output",
679 ],
680 },
681 QualificationRow {
682 backend: "wasm",
683 key: RequirementKind::DiscardArtifact,
684 profile_floor: ProfileFloor::structural(),
685 mechanism: "wasm:preopen_commit:Enforced",
686 status: QualificationStatus::Proven,
687 proof_receipts: &[
688 "crates/bvisor/tests/grid_wasm.rs::wasi_temp_root_is_private_and_writable",
689 ],
690 },
691 QualificationRow {
692 backend: "wasm",
693 key: RequirementKind::ListOutputs,
694 profile_floor: ProfileFloor::structural(),
695 mechanism: "wasm:preopen_readdir:Enforced",
696 status: QualificationStatus::Proven,
697 proof_receipts: &[
698 "crates/bvisor/tests/grid_wasm.rs::g7_wasi_preopen_commit_allows_in_root_output",
699 ],
700 },
701 QualificationRow {
702 backend: "wasm",
703 key: RequirementKind::NetworkDenyAll,
704 profile_floor: ProfileFloor::structural(),
705 mechanism: "wasm:no_socket_cap:Enforced",
706 status: QualificationStatus::Proven,
707 proof_receipts: &[
708 "crates/bvisor/tests/grid_wasm.rs::wasi_socket_capability_is_absent",
709 ],
710 },
711 QualificationRow {
712 backend: "wasm",
713 key: RequirementKind::InheritedFdsNone,
714 profile_floor: ProfileFloor::structural(),
715 mechanism: "wasm:wasi_stdio:Enforced",
716 status: QualificationStatus::Proven,
717 proof_receipts: &[
718 "crates/bvisor/tests/grid_wasm.rs::wasi_no_inherited_fds_contract_runs_without_raw_fd_authority",
719 ],
720 },
721 QualificationRow {
722 backend: "wasm",
723 key: RequirementKind::InheritedFdsOnly,
724 profile_floor: ProfileFloor::structural(),
725 mechanism: "wasm:none/structurally-unsupported:Unsupported",
726 status: QualificationStatus::FailClosed,
727 proof_receipts: &[],
728 },
729 QualificationRow {
730 backend: "wasm",
731 key: RequirementKind::ChildSpawnDenyNewTasks,
732 profile_floor: ProfileFloor::structural(),
733 mechanism: "wasm:none/structurally-unsupported:Unsupported",
734 status: QualificationStatus::FailClosed,
735 proof_receipts: &[],
736 },
737 QualificationRow {
738 backend: "wasm",
739 key: RequirementKind::ChildSpawnAllowThreads,
740 profile_floor: ProfileFloor::structural(),
741 mechanism: "wasm:none/structurally-unsupported:Unsupported",
742 status: QualificationStatus::FailClosed,
743 proof_receipts: &[],
744 },
745 QualificationRow {
746 backend: "wasm",
747 key: RequirementKind::ChildSpawnAllowDescendants,
748 profile_floor: ProfileFloor::structural(),
749 mechanism: "wasm:none/structurally-unsupported:Unsupported",
750 status: QualificationStatus::FailClosed,
751 proof_receipts: &[],
752 },
753 QualificationRow {
754 backend: "wasm",
755 key: RequirementKind::Kill,
756 profile_floor: ProfileFloor::structural(),
757 mechanism: "wasm:none/structurally-unsupported:Unsupported",
758 status: QualificationStatus::FailClosed,
759 proof_receipts: &[],
760 },
761 QualificationRow {
762 backend: "wasm",
763 key: RequirementKind::ExposePath,
764 profile_floor: ProfileFloor::structural(),
765 mechanism: "wasm:none/structurally-unsupported:Unsupported",
766 status: QualificationStatus::FailClosed,
767 proof_receipts: &[],
768 },
769 QualificationRow {
770 backend: "wasm",
771 key: RequirementKind::NetworkAllowList,
772 profile_floor: ProfileFloor::structural(),
773 mechanism: "wasm:none/structurally-unsupported:Unsupported",
774 status: QualificationStatus::FailClosed,
775 proof_receipts: &[],
776 },
777];
778
779/// Look up the committed Wasm ledger row for a requirement key.
780#[must_use]
781pub fn wasm_ledger_row(key: RequirementKind) -> Option<&'static QualificationRow> {
782 WASM_QUALIFICATION_LEDGER.iter().find(|r| r.key == key)
783}
784
785#[cfg(test)]
786#[path = "qualification_tests.rs"]
787mod tests;