Enforce qualified pidfs host and process broker contracts

This commit is contained in:
ms2k 2026-07-14 00:05:23 +09:00
commit f840a48376
9 changed files with 1539 additions and 30 deletions

44
.clippy.toml Normal file
View file

@ -0,0 +1,44 @@
# Generated from support/process-safety.toml; do not edit.
disallowed-types = [
{ path = "std::process::Child", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Command", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "tokio::process::Child", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "tokio::process::Command", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
]
disallowed-methods = [
{ path = "libc::clone", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::execve", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::execveat", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::fork", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::posix_spawn", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::sigaction", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::signal", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::vfork", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::wait", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::wait4", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::waitid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "libc::waitpid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::sys::signal::sigaction", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::sys::wait::wait", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::sys::wait::waitid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::sys::wait::waitpid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::unistd::execve", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::unistd::execveat", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::unistd::fork", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "nix::unistd::vfork", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::kill_process", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::kill_process_group", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::pidfd_open", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::pidfd_send_signal", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::waitid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "rustix::process::waitpid", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Child::kill", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Child::try_wait", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Child::wait", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Child::wait_with_output", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Command::output", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Command::spawn", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
{ path = "std::process::Command::status", reason = "process control is confined to the reviewed Lazaret spawn/reap broker" },
]

View file

@ -26,6 +26,8 @@ unused = { level = "deny", priority = -1 }
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "deny", priority = -1 }
disallowed_methods = "deny"
disallowed_types = "deny"
[profile.release]
codegen-units = 1

View file

@ -2,10 +2,10 @@
The Cargo workspace separates shared production logic from process entry points. `lazaret-core` is the initial library boundary. The `lazaret` Client, `lazaret-host-coordinator`, `lazaret-runtime-supervisor`, `lazaret-network-sidecar`, and `lazaret-escalation-client` executables are thin binary packages; process behavior belongs in libraries so it can be verified without invoking private binary internals.
`support/work-packages.toml` is the dependency authority. `support/model-refinements.toml` assigns every modeled action and configured property to production packages and verification levels. `support/external-tools.toml` fixes the baseline host and tool contracts, including whether an unavailable capability fails or skips a level. `docs/implementation-dependencies.md` is generated from the package registry.
`support/work-packages.toml` is the dependency authority. `support/model-refinements.toml` assigns every modeled action and configured property to production packages and verification levels. `support/external-tools.toml` fixes the baseline host and tool contracts, including exact kernel qualification records and whether an unavailable capability fails or skips a level. `support/process-safety.toml` pins dependency source identities, features, and reviewed process APIs and registers the compiler and exercised-path syscall checks for the exclusive spawn/reap broker boundary. `docs/implementation-dependencies.md`, `docs/host-support.md`, and `.clippy.toml` are generated registry outputs.
Run `tools/verify-v1`, `tools/verify-v2`, `tools/verify-v3`, `tools/verify-v4`, or `tools/verify-v5`. Each higher level includes the lower levels. V1 is host-local and includes format, Clippy, tests, dependency policy, registry drift, and both TLC models. Physical suites register commands in `support/integration-tests.toml`; an empty level is an explicit skip. A registered scenario may skip only when every missing capability has `policy = "skip"` in the external-tool manifest.
Normal verification is offline: Cargo receives `--offline` and cargo-deny receives `--frozen`. `tools/dev refresh-advisories` is the explicit online operation that updates the local RustSec database and then verifies advisories offline. The zero-third-party baseline still runs cargo-deny bans, license, and source checks on every verification.
Normal verification is offline: Cargo receives `--offline` and cargo-deny receives `--frozen`. `tools/dev refresh-advisories` is the explicit online operation that updates the local RustSec database and then verifies advisories offline. Every level audits the all-feature dependency source checksums, versions, enabled features, reviewed process APIs, generated compiler policy, and unauthorized lint exemptions. V2 registers optional `strace` cases for exact exercised binary paths; a missing tracer skips those cases, while an observed extra exec, process creation, wait, or `SIGCHLD` mutation fails. Dynamic tracing does not claim coverage of unexercised paths.
Run `tools/dev generate` after changing the work-package registry. `tools/dev generate --check` fails when the committed dependency view differs.
Run `tools/dev generate` after changing the work-package, external-tool, or process-safety registry. `tools/dev generate --check` fails when either committed generated view differs.

436
docs/host-support.md Normal file
View file

@ -0,0 +1,436 @@
# Host support and process-safety registry
Generated from `support/external-tools.toml` and `support/process-safety.toml`; do not edit this file directly.
## Pidfs host support
- Contract: `pidfs-process-identity-v1`
- Platform floor: `linux-x86_64` kernel `6.9` or newer
- Runtime refusal: `fail-before-generation`
- Unavailable verifier behavior: `skip`
- Smoke claim: `current-boot-smoke-only` cached only by `boot_id`
- Smoke bounds: one attempt per step, `5000` ms per step, `60000` ms total, `5000` ms cleanup
- Positive support: unavailable until IP-03 implements an exact running-build matcher and the smoke runner
The smoke steps are bounded observations and never qualify source or build provenance:
1. `linux-x86_64-kernel-floor`
1. `pidfd-operations`
1. `pidfs-magic`
1. `concurrent-same-process-identity`
1. `last-close-live-reopen-identity`
1. `simultaneous-process-distinct-inode`
1. `stale-handle-cannot-affect-live-peer`
1. `exit-reap-drop-all-later-process-non-reuse`
## Qualified kernel builds
| ID | Kind | Release | Source commit | Build image SHA-256 |
| --- | --- | --- | --- | --- |
| `upstream-linux-6.9.0-x86_64` | upstream | `6.9.0` | `a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6` | `e85edb5da3dc18295f05baeb38ff019a6621c1cac2e6ac81b99e14450c72d7fc` |
## Process-safety audit
Policy `causal-child-acquisition-v1` pins 13 dependency source/version/feature/API-review records, generates compiler-resolved disallowed type/method lints, and registers `process-safety-baseline-exercised-paths` as V2 exercised-path tracing.
The dedicated `lazaret-process-broker` boundary is `unimplemented` and no lint exemption is authorized. Dynamic coverage is `registered-exercised-paths-only`; it does not claim unexercised-path coverage.
## Complete registered data
Every authoritative host-qualification and process-safety field is reproduced below so generated-view drift remains complete.
```json
{
"host_capabilities": [
{
"diagnostic": "Physical and composition suites currently support Linux x86_64.",
"id": "linux-x86_64",
"levels": [
"V2",
"V3",
"V4",
"V5"
],
"policy": "skip",
"probe": "linux-x86_64"
},
{
"diagnostic": "Linux x86-64 kernel 6.9 or newer is required; Linux 6.8 is unsupported for pidfs process identity.",
"id": "kernel",
"levels": [
"V2",
"V3",
"V4",
"V5"
],
"minimum": "6.9",
"policy": "skip",
"probe": "kernel-at-least"
},
{
"contract": "pidfs-process-identity-v1",
"diagnostic": "PidfsHostSupportProbe execution is owned by IP-03; this scaffold cannot claim host support from a version string or manifest record.",
"id": "pidfs-host-support",
"levels": [
"V2",
"V3",
"V4",
"V5"
],
"policy": "skip",
"probe": "registered-host-support"
}
],
"host_probe": [
{
"attempts_per_step": 1,
"cache_key": "boot_id",
"cleanup_timeout_ms": 5000,
"id": "pidfs-host-support-probe-v1",
"owner": "IP-03",
"result_claim": "bounded-current-boot-observations",
"step_timeout_ms": 5000,
"steps": [
"linux-x86_64-kernel-floor",
"pidfd-operations",
"pidfs-magic",
"concurrent-same-process-identity",
"last-close-live-reopen-identity",
"simultaneous-process-distinct-inode",
"stale-handle-cannot-affect-live-peer",
"exit-reap-drop-all-later-process-non-reuse"
],
"total_timeout_ms": 60000
}
],
"kernel_qualification": [
{
"acquisition_result_sha256": "867492f65ffd473733544626c032957143f40d040e1a4e8e1e82bf9e4bca6cc3",
"architecture": "x86_64",
"build_config_sha256": "7db4d48deb650052df60273c7f2c2d228c981db4d6f76b8eb929a9e120d7283a",
"build_image_sha256": "e85edb5da3dc18295f05baeb38ff019a6621c1cac2e6ac81b99e14450c72d7fc",
"id": "upstream-linux-6.9.0-x86_64",
"kind": "upstream",
"pidfs_inode_semantics": "boot-monotonic-u64",
"probe_binary_sha256": "040a84f2b06ae73671c262016c649fe21e3fb21fad89cea3d6d76f8d7436648f",
"release": "6.9.0",
"reuse_result_sha256": "b8491adc9c0fd4509ec94a99d03835842f7f92ca6d5baad8f6589a7f3c49517f",
"source_archive_sha256": "24fa01fb989c7a3e28453f117799168713766e119c5381dac30115f18f268149",
"source_commit": "a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6",
"source_ref": "v6.9",
"source_signer_fingerprint": "647F28654894E3BD457199BE38DBBDC86092693E",
"status": "qualified"
}
],
"pidfs_host_support": {
"bounded_claim": "current-boot-smoke-only",
"diagnostic_kernel": "LZ-HOST-PIDFS-KERNEL: PidfsProcessIdentityV1 requires Linux x86-64 kernel 6.9 or newer; Linux 6.8 is unsupported even with a partial pidfs backport.",
"diagnostic_platform": "LZ-HOST-PIDFS-PLATFORM: PidfsProcessIdentityV1 requires Linux x86-64.",
"diagnostic_qualification": "LZ-HOST-PIDFS-QUALIFICATION: the running kernel build has no exact qualified source/build record.",
"diagnostic_smoke": "LZ-HOST-PIDFS-SMOKE: the bounded per-boot pidfs support smoke failed; no runtime resource or durable generation may be created.",
"id": "pidfs-process-identity-v1",
"minimum_kernel": "6.9",
"platform": "linux-x86_64",
"probe": "pidfs-host-support-probe-v1",
"qualification_ids": [
"upstream-linux-6.9.0-x86_64"
],
"runtime_failure_policy": "fail-before-generation",
"verifier_unavailable_policy": "skip"
},
"process_safety": {
"policy": {
"broker_crate": "lazaret-process-broker",
"broker_state": "unimplemented",
"completion_owner": "IP-03",
"disallowed_methods": [
"libc::clone",
"libc::execve",
"libc::execveat",
"libc::fork",
"libc::posix_spawn",
"libc::sigaction",
"libc::signal",
"libc::vfork",
"libc::wait",
"libc::wait4",
"libc::waitid",
"libc::waitpid",
"nix::sys::signal::sigaction",
"nix::sys::wait::wait",
"nix::sys::wait::waitid",
"nix::sys::wait::waitpid",
"nix::unistd::execve",
"nix::unistd::execveat",
"nix::unistd::fork",
"nix::unistd::vfork",
"rustix::process::kill_process",
"rustix::process::kill_process_group",
"rustix::process::pidfd_open",
"rustix::process::pidfd_send_signal",
"rustix::process::waitid",
"rustix::process::waitpid",
"std::process::Child::kill",
"std::process::Child::try_wait",
"std::process::Child::wait",
"std::process::Child::wait_with_output",
"std::process::Command::output",
"std::process::Command::spawn",
"std::process::Command::status"
],
"disallowed_types": [
"std::process::Child",
"std::process::Command",
"tokio::process::Child",
"tokio::process::Command"
],
"dynamic_coverage": "registered-exercised-paths-only",
"dynamic_scenario": "process-safety-baseline-exercised-paths",
"forbidden_syscall_patterns": [
"^clone3?\\(",
"^execveat\\(",
"^fork\\(",
"^vfork\\(",
"^wait4\\(",
"^waitid\\(",
"^rt_sigaction\\(SIGCHLD,"
],
"id": "causal-child-acquisition-v1",
"lint_allow_paths": [],
"lint_config": ".clippy.toml",
"production_roots": [
"crates"
],
"trace_expression": "process,wait4,waitid,rt_sigaction"
},
"runtime_dependency": [
{
"checksum": "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1",
"features": [],
"name": "cobs",
"reviewed_process_apis": [],
"version": "0.3.0"
},
{
"checksum": "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced",
"features": [
"alloc"
],
"name": "embedded-io",
"reviewed_process_apis": [],
"version": "0.4.0"
},
{
"checksum": "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d",
"features": [
"alloc"
],
"name": "embedded-io",
"reviewed_process_apis": [],
"version": "0.6.1"
},
{
"checksum": "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24",
"features": [
"alloc",
"use-std"
],
"name": "postcard",
"reviewed_process_apis": [],
"version": "1.1.3"
},
{
"checksum": "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934",
"features": [
"default",
"proc-macro"
],
"name": "proc-macro2",
"reviewed_process_apis": [],
"version": "1.0.106"
},
{
"checksum": "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368",
"features": [
"default",
"proc-macro"
],
"name": "quote",
"reviewed_process_apis": [],
"version": "1.0.46"
},
{
"checksum": "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e",
"features": [
"alloc",
"default",
"derive",
"serde_derive",
"std"
],
"name": "serde",
"reviewed_process_apis": [],
"version": "1.0.228"
},
{
"checksum": "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad",
"features": [
"alloc",
"result",
"std"
],
"name": "serde_core",
"reviewed_process_apis": [],
"version": "1.0.228"
},
{
"checksum": "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79",
"features": [
"default"
],
"name": "serde_derive",
"reviewed_process_apis": [],
"version": "1.0.228"
},
{
"checksum": "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422",
"features": [
"clone-impls",
"default",
"derive",
"parsing",
"printing",
"proc-macro"
],
"name": "syn",
"reviewed_process_apis": [],
"version": "2.0.118"
},
{
"checksum": "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4",
"features": [],
"name": "thiserror",
"reviewed_process_apis": [],
"version": "2.0.18"
},
{
"checksum": "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5",
"features": [],
"name": "thiserror-impl",
"reviewed_process_apis": [],
"version": "2.0.18"
},
{
"checksum": "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75",
"features": [],
"name": "unicode-ident",
"reviewed_process_apis": [],
"version": "1.0.24"
}
],
"schema": 2,
"trace_case": [
{
"args": [],
"binary": "lazaret",
"expected_exit_code": 0,
"initial_execve_count": 1
},
{
"args": [],
"binary": "lazaret-escalation-client",
"expected_exit_code": 0,
"initial_execve_count": 1
},
{
"args": [],
"binary": "lazaret-host-coordinator",
"expected_exit_code": 0,
"initial_execve_count": 1
},
{
"args": [],
"binary": "lazaret-network-sidecar",
"expected_exit_code": 0,
"initial_execve_count": 1
},
{
"args": [],
"binary": "lazaret-runtime-supervisor",
"expected_exit_code": 0,
"initial_execve_count": 1
}
]
},
"process_trace_capability": {
"args": [
"--version"
],
"diagnostic": "Install strace 6.x or newer to run the registered final-binary exercised-path syscall cases.",
"id": "strace",
"levels": [
"V2",
"V3",
"V4",
"V5"
],
"policy": "skip",
"program": "strace",
"version_pattern": "^strace -- version [6-9]\\.|^strace -- version [1-9][0-9]\\."
},
"process_trace_scenario": {
"capabilities": [
"linux-x86_64",
"strace"
],
"command": [
"tools/dev",
"process-audit",
"--dynamic"
],
"level": "V2",
"name": "process-safety-baseline-exercised-paths"
},
"workspace_lints": {
"clippy": {
"all": {
"level": "deny",
"priority": -1
},
"disallowed_methods": "deny",
"disallowed_types": "deny",
"pedantic": {
"level": "deny",
"priority": -1
}
},
"rust": {
"unsafe_code": "forbid",
"unused": {
"level": "deny",
"priority": -1
}
}
},
"workspace_member_lints": {
"crates/lazaret-client": {
"workspace": true
},
"crates/lazaret-core": {
"workspace": true
},
"crates/lazaret-escalation-client": {
"workspace": true
},
"crates/lazaret-host-coordinator": {
"workspace": true
},
"crates/lazaret-network-sidecar": {
"workspace": true
},
"crates/lazaret-runtime-supervisor": {
"workspace": true
}
}
}
```

View file

@ -1,6 +1,57 @@
schema = 1
schema = 2
levels = ["V1", "V2", "V3", "V4", "V5"]
[pidfs_host_support]
id = "pidfs-process-identity-v1"
platform = "linux-x86_64"
minimum_kernel = "6.9"
probe = "pidfs-host-support-probe-v1"
qualification_ids = ["upstream-linux-6.9.0-x86_64"]
runtime_failure_policy = "fail-before-generation"
verifier_unavailable_policy = "skip"
bounded_claim = "current-boot-smoke-only"
diagnostic_platform = "LZ-HOST-PIDFS-PLATFORM: PidfsProcessIdentityV1 requires Linux x86-64."
diagnostic_kernel = "LZ-HOST-PIDFS-KERNEL: PidfsProcessIdentityV1 requires Linux x86-64 kernel 6.9 or newer; Linux 6.8 is unsupported even with a partial pidfs backport."
diagnostic_qualification = "LZ-HOST-PIDFS-QUALIFICATION: the running kernel build has no exact qualified source/build record."
diagnostic_smoke = "LZ-HOST-PIDFS-SMOKE: the bounded per-boot pidfs support smoke failed; no runtime resource or durable generation may be created."
[[host_probe]]
id = "pidfs-host-support-probe-v1"
owner = "IP-03"
cache_key = "boot_id"
result_claim = "bounded-current-boot-observations"
attempts_per_step = 1
step_timeout_ms = 5000
total_timeout_ms = 60000
cleanup_timeout_ms = 5000
steps = [
"linux-x86_64-kernel-floor",
"pidfd-operations",
"pidfs-magic",
"concurrent-same-process-identity",
"last-close-live-reopen-identity",
"simultaneous-process-distinct-inode",
"stale-handle-cannot-affect-live-peer",
"exit-reap-drop-all-later-process-non-reuse",
]
[[kernel_qualification]]
id = "upstream-linux-6.9.0-x86_64"
kind = "upstream"
architecture = "x86_64"
release = "6.9.0"
source_ref = "v6.9"
source_commit = "a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6"
source_archive_sha256 = "24fa01fb989c7a3e28453f117799168713766e119c5381dac30115f18f268149"
source_signer_fingerprint = "647F28654894E3BD457199BE38DBBDC86092693E"
build_config_sha256 = "7db4d48deb650052df60273c7f2c2d228c981db4d6f76b8eb929a9e120d7283a"
build_image_sha256 = "e85edb5da3dc18295f05baeb38ff019a6621c1cac2e6ac81b99e14450c72d7fc"
probe_binary_sha256 = "040a84f2b06ae73671c262016c649fe21e3fb21fad89cea3d6d76f8d7436648f"
reuse_result_sha256 = "b8491adc9c0fd4509ec94a99d03835842f7f92ca6d5baad8f6589a7f3c49517f"
acquisition_result_sha256 = "867492f65ffd473733544626c032957143f40d040e1a4e8e1e82bf9e4bca6cc3"
pidfs_inode_semantics = "boot-monotonic-u64"
status = "qualified"
[[command]]
id = "python"
program = "python3"
@ -83,6 +134,15 @@ policy = "skip"
levels = ["V2", "V3", "V4", "V5"]
diagnostic = "Install the qualified slirp4netns 1.3.1 and libslirp 4.9.1 stack."
[[command]]
id = "strace"
program = "strace"
args = ["--version"]
version_pattern = '^strace -- version [6-9]\.|^strace -- version [1-9][0-9]\.'
policy = "skip"
levels = ["V2", "V3", "V4", "V5"]
diagnostic = "Install strace 6.x or newer to run the registered final-binary exercised-path syscall cases."
[[feature]]
id = "linux-x86_64"
probe = "linux-x86_64"
@ -93,10 +153,18 @@ diagnostic = "Physical and composition suites currently support Linux x86_64."
[[feature]]
id = "kernel"
probe = "kernel-at-least"
minimum = "6.8"
minimum = "6.9"
policy = "skip"
levels = ["V2", "V3", "V4", "V5"]
diagnostic = "Linux 6.8 or newer supplies the qualified namespace and pidfd baseline."
diagnostic = "Linux x86-64 kernel 6.9 or newer is required; Linux 6.8 is unsupported for pidfs process identity."
[[feature]]
id = "pidfs-host-support"
probe = "registered-host-support"
contract = "pidfs-process-identity-v1"
policy = "skip"
levels = ["V2", "V3", "V4", "V5"]
diagnostic = "PidfsHostSupportProbe execution is owned by IP-03; this scaffold cannot claim host support from a version string or manifest record."
[[feature]]
id = "user-namespaces"

View file

@ -1,3 +1,8 @@
schema = 1
levels = ["V2", "V3", "V4", "V5"]
scenarios = []
[[scenarios]]
name = "process-safety-baseline-exercised-paths"
level = "V2"
command = ["tools/dev", "process-audit", "--dynamic"]
capabilities = ["linux-x86_64", "strace"]

184
support/process-safety.toml Normal file
View file

@ -0,0 +1,184 @@
schema = 2
[policy]
id = "causal-child-acquisition-v1"
broker_crate = "lazaret-process-broker"
broker_state = "unimplemented"
production_roots = ["crates"]
lint_config = ".clippy.toml"
lint_allow_paths = []
dynamic_scenario = "process-safety-baseline-exercised-paths"
dynamic_coverage = "registered-exercised-paths-only"
completion_owner = "IP-03"
trace_expression = "process,wait4,waitid,rt_sigaction"
forbidden_syscall_patterns = [
'^clone3?\(',
'^execveat\(',
'^fork\(',
'^vfork\(',
'^wait4\(',
'^waitid\(',
'^rt_sigaction\(SIGCHLD,',
]
disallowed_types = [
"std::process::Child",
"std::process::Command",
"tokio::process::Child",
"tokio::process::Command",
]
disallowed_methods = [
"libc::clone",
"libc::execve",
"libc::execveat",
"libc::fork",
"libc::posix_spawn",
"libc::sigaction",
"libc::signal",
"libc::vfork",
"libc::wait",
"libc::wait4",
"libc::waitid",
"libc::waitpid",
"nix::sys::signal::sigaction",
"nix::sys::wait::wait",
"nix::sys::wait::waitid",
"nix::sys::wait::waitpid",
"nix::unistd::execve",
"nix::unistd::execveat",
"nix::unistd::fork",
"nix::unistd::vfork",
"rustix::process::kill_process",
"rustix::process::kill_process_group",
"rustix::process::pidfd_open",
"rustix::process::pidfd_send_signal",
"rustix::process::waitid",
"rustix::process::waitpid",
"std::process::Child::kill",
"std::process::Child::try_wait",
"std::process::Child::wait",
"std::process::Child::wait_with_output",
"std::process::Command::output",
"std::process::Command::spawn",
"std::process::Command::status",
]
[[trace_case]]
binary = "lazaret"
args = []
expected_exit_code = 0
initial_execve_count = 1
[[trace_case]]
binary = "lazaret-escalation-client"
args = []
expected_exit_code = 0
initial_execve_count = 1
[[trace_case]]
binary = "lazaret-host-coordinator"
args = []
expected_exit_code = 0
initial_execve_count = 1
[[trace_case]]
binary = "lazaret-network-sidecar"
args = []
expected_exit_code = 0
initial_execve_count = 1
[[trace_case]]
binary = "lazaret-runtime-supervisor"
args = []
expected_exit_code = 0
initial_execve_count = 1
[[runtime_dependency]]
name = "cobs"
version = "0.3.0"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
features = []
reviewed_process_apis = []
[[runtime_dependency]]
name = "embedded-io"
version = "0.4.0"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
features = ["alloc"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "embedded-io"
version = "0.6.1"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
features = ["alloc"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "postcard"
version = "1.1.3"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
features = ["alloc", "use-std"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "proc-macro2"
version = "1.0.106"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
features = ["default", "proc-macro"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "quote"
version = "1.0.46"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
features = ["default", "proc-macro"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "serde"
version = "1.0.228"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
features = ["alloc", "default", "derive", "serde_derive", "std"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "serde_core"
version = "1.0.228"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
features = ["alloc", "result", "std"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "serde_derive"
version = "1.0.228"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
features = ["default"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "syn"
version = "2.0.118"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
features = ["clone-impls", "default", "derive", "parsing", "printing", "proc-macro"]
reviewed_process_apis = []
[[runtime_dependency]]
name = "thiserror"
version = "2.0.18"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
features = []
reviewed_process_apis = []
[[runtime_dependency]]
name = "thiserror-impl"
version = "2.0.18"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
features = []
reviewed_process_apis = []
[[runtime_dependency]]
name = "unicode-ident"
version = "1.0.24"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
features = []
reviewed_process_apis = []

View file

@ -3,6 +3,7 @@ import contextlib
import importlib.util
import io
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest import mock
@ -19,11 +20,16 @@ class RegistryTests(unittest.TestCase):
self.packages = DEV.load_toml(ROOT / "support/work-packages.toml")
self.refinements = DEV.load_toml(ROOT / "support/model-refinements.toml")
self.external = DEV.load_toml(ROOT / "support/external-tools.toml")
self.integrations = DEV.load_toml(ROOT / "support/integration-tests.toml")
self.process_safety = DEV.load_toml(ROOT / "support/process-safety.toml")
def test_committed_registries_are_complete(self):
packages = DEV.validate_packages(self.packages)
DEV.validate_refinements(self.refinements, {package["id"] for package in packages})
DEV.validate_external_tools(self.external)
capability_ids = {item["id"] for kind in ("command", "feature") for item in self.external[kind]}
DEV.validate_integrations(self.integrations, capability_ids)
DEV.validate_process_safety(self.process_safety, self.integrations)
def test_dependency_cycle_is_rejected(self):
packages = copy.deepcopy(self.packages)
@ -70,6 +76,7 @@ class RegistryTests(unittest.TestCase):
invalid = {
"linux-x86_64": {"id": 1},
"kernel-at-least": {"minimum": 6.8},
"registered-host-support": {"contract": "unknown"},
"user-namespace": {"program": 1},
"subordinate-ids": {"minimum_count": "65536"},
"open-character-device": {"access": "read-only"},
@ -90,13 +97,217 @@ class RegistryTests(unittest.TestCase):
DEV.validate_external_tools(external)
def test_supported_platform_probe_is_mandatory(self):
for capability in ("linux-x86_64", "user-namespaces"):
for capability in ("linux-x86_64", "kernel", "pidfs-host-support", "user-namespaces"):
with self.subTest(capability=capability):
external = copy.deepcopy(self.external)
external["feature"] = [feature for feature in external["feature"] if feature["id"] != capability]
with self.assertRaisesRegex(DEV.RegistryError, "external manifest missing"):
DEV.validate_external_tools(external)
def test_pidfs_contract_fields_are_strict(self):
mutations = (
lambda external: external["pidfs_host_support"].pop("diagnostic_smoke"),
lambda external: external["pidfs_host_support"].update(minimum_kernel=6.9),
lambda external: external["pidfs_host_support"].update(unexpected=True),
lambda external: external["host_probe"][0].update(steps=["kernel-version-only"]),
lambda external: external["host_probe"][0].update(attempts_per_step=True),
lambda external: external["host_probe"][0].update(total_timeout_ms=1000),
lambda external: external["host_probe"][0].update(step_timeout_ms=4000),
lambda external: external["kernel_qualification"][0].pop("source_commit"),
lambda external: external["kernel_qualification"][0].update(build_image_sha256="bad"),
lambda external: external["kernel_qualification"][0].update(build_image_sha256=1),
lambda external: external["kernel_qualification"][0].update(id=[]),
lambda external: external["kernel_qualification"][0].update(unexpected=True),
)
for mutation in mutations:
with self.subTest(mutation=mutation):
external = copy.deepcopy(self.external)
mutation(external)
with self.assertRaises(DEV.RegistryError):
DEV.validate_external_tools(external)
def test_linux_68_is_refused_and_69_meets_only_the_floor(self):
kernel = next(feature for feature in self.external["feature"] if feature["id"] == "kernel")
with mock.patch.object(DEV.platform, "system", return_value="Linux"), mock.patch.object(
DEV.platform, "release", return_value="6.8.99"
):
self.assertEqual(DEV.probe_feature(kernel), (False, "6.8.99"))
with mock.patch.object(DEV.platform, "system", return_value="Linux"), mock.patch.object(
DEV.platform, "release", return_value="6.9.0"
):
self.assertEqual(DEV.probe_feature(kernel), (True, "6.9.0"))
with mock.patch.object(DEV.platform, "system", return_value="Linux"), mock.patch.object(
DEV.platform, "release", return_value="6.12.38+deb13-amd64"
):
self.assertEqual(DEV.probe_feature(kernel), (True, "6.12.38+deb13-amd64"))
pidfs = next(feature for feature in self.external["feature"] if feature["id"] == "pidfs-host-support")
self.assertEqual(DEV.probe_feature(pidfs), (False, "registered runner pending IP-03"))
def test_pidfs_smoke_failure_is_a_hard_runtime_refusal(self):
contract = self.external["pidfs_host_support"]
self.assertTrue(contract["diagnostic_smoke"].startswith("LZ-HOST-PIDFS-SMOKE:"))
self.assertEqual(contract["runtime_failure_policy"], "fail-before-generation")
def test_mandatory_capabilities_cannot_be_remapped(self):
for capability in ("linux-x86_64", "kernel", "pidfs-host-support", "user-namespaces"):
with self.subTest(capability=capability):
external = copy.deepcopy(self.external)
feature = next(item for item in external["feature"] if item["id"] == capability)
feature.update(policy="fail")
with self.assertRaisesRegex(DEV.RegistryError, "mandatory feature contract drift"):
DEV.validate_external_tools(external)
external = copy.deepcopy(self.external)
pidfs = next(item for item in external["feature"] if item["id"] == "pidfs-host-support")
pidfs.pop("contract")
pidfs["probe"] = "linux-x86_64"
with self.assertRaisesRegex(DEV.RegistryError, "mandatory feature contract drift"):
DEV.validate_external_tools(external)
external = copy.deepcopy(self.external)
rustc = next(item for item in external["command"] if item["id"] == "rustc")
rustc["levels"] = ["V1"]
with self.assertRaisesRegex(DEV.RegistryError, "mandatory command contract drift"):
DEV.validate_external_tools(external)
def test_unqualified_vendor_build_is_rejected(self):
external = copy.deepcopy(self.external)
vendor = copy.deepcopy(external["kernel_qualification"][0])
vendor.update(
id="vendor-linux-6.9.0-x86_64",
kind="vendor",
vendor="Example Vendor",
vendor_patchset_sha256="0" * 64,
status="unqualified",
)
external["kernel_qualification"].append(vendor)
external["pidfs_host_support"]["qualification_ids"].append(vendor["id"])
with self.assertRaisesRegex(DEV.RegistryError, "not qualified"):
DEV.validate_external_tools(external)
def test_qualified_vendor_release_string_is_representable(self):
external = copy.deepcopy(self.external)
vendor = copy.deepcopy(external["kernel_qualification"][0])
vendor.update(
id="vendor-linux-6.12-x86_64",
kind="vendor",
release="6.12.38-1-amd64",
vendor="Example Vendor",
vendor_patchset_sha256="0" * 64,
)
external["kernel_qualification"].append(vendor)
external["pidfs_host_support"]["qualification_ids"].append(vendor["id"])
DEV.validate_external_tools(external)
def test_process_safety_registration_is_strict(self):
mutations = (
lambda process: process["policy"].pop("dynamic_scenario"),
lambda process: process["policy"].update(production_roots=["crates/lazaret-core"]),
lambda process: process["policy"].update(lint_allow_paths=["crates/foreign.rs"]),
lambda process: process["policy"].update(trace_expression="read"),
lambda process: process["policy"].update(forbidden_syscall_patterns=["$^"]),
lambda process: process["policy"].update(unexpected=True),
lambda process: process["runtime_dependency"][0].update(features=["z", "a"]),
lambda process: process["runtime_dependency"][0].update(checksum=1),
lambda process: process["trace_case"][0].update(initial_execve_count=True),
)
for mutation in mutations:
with self.subTest(mutation=mutation):
process = copy.deepcopy(self.process_safety)
mutation(process)
with self.assertRaises(DEV.RegistryError):
DEV.validate_process_safety(process, self.integrations)
integrations = copy.deepcopy(self.integrations)
integrations["scenarios"] = []
with self.assertRaisesRegex(DEV.RegistryError, "dynamic syscall scenario"):
DEV.validate_process_safety(self.process_safety, integrations)
def test_process_lint_exemption_is_detected(self):
self.assertTrue(DEV.has_process_lint_exemption("#![allow(clippy::disallowed_types)]\n"))
self.assertTrue(DEV.has_process_lint_exemption("#[expect(clippy::{disallowed_methods})]\n"))
def test_workspace_process_lints_are_mandatory(self):
workspace = DEV.load_toml(ROOT / "Cargo.toml")
DEV.validate_workspace_process_lints(workspace)
for group, lint in (("rust", "unsafe_code"), ("clippy", "disallowed_methods"), ("clippy", "disallowed_types")):
with self.subTest(lint=lint):
changed = copy.deepcopy(workspace)
changed["workspace"]["lints"][group].pop(lint)
with self.assertRaises(DEV.RegistryError):
DEV.validate_workspace_process_lints(changed)
def test_every_workspace_member_inherits_process_lints(self):
workspace = DEV.load_toml(ROOT / "Cargo.toml")["workspace"]
for member in workspace["members"]:
with self.subTest(member=member):
manifest = DEV.load_toml(ROOT / member / "Cargo.toml")
DEV.validate_member_process_lints(manifest, member)
changed = copy.deepcopy(manifest)
changed["lints"] = {"workspace": False}
with self.assertRaises(DEV.RegistryError):
DEV.validate_member_process_lints(changed, member)
def test_compiler_resolves_grouped_process_import_alias(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
(root / "src").mkdir()
(root / "Cargo.toml").write_text(
'[package]\nname = "lint-probe"\nversion = "0.0.0"\nedition = "2024"\n'
"[lints.clippy]\ndisallowed_types = \"deny\"\n"
)
(root / ".clippy.toml").write_text(DEV.clippy_config_view(self.process_safety))
(root / "src/main.rs").write_text(
'use std::{process::Command as C};\nfn main() { let _ = C::new("true"); }\n'
)
result = subprocess.run(
["cargo", "clippy", "--offline", "--", "-D", "warnings"],
cwd=root,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("disallowed type", result.stdout)
def test_process_syscall_trace_refuses_bypass(self):
policy = self.process_safety["policy"]
trace_case = self.process_safety["trace_case"][0]
DEV.validate_process_trace(['execve("/tmp/lazaret", ["lazaret"], 0x0) = 0'], policy, trace_case)
for syscall in ("clone", "execveat", "wait4", "waitid"):
with self.subTest(syscall=syscall), self.assertRaisesRegex(DEV.RegistryError, "forbidden"):
DEV.validate_process_trace(
['execve("/tmp/lazaret", ["lazaret"], 0x0) = 0', f"{syscall}(0) = 1"],
policy,
trace_case,
)
with self.assertRaisesRegex(DEV.RegistryError, "SIGCHLD"):
DEV.validate_process_trace(
['execve("/tmp/lazaret", ["lazaret"], 0x0) = 0', "rt_sigaction(SIGCHLD, {}, NULL, 8) = 0"],
policy,
trace_case,
)
with self.assertRaisesRegex(DEV.RegistryError, "execve surface count"):
DEV.validate_process_trace(
['execve("/tmp/lazaret", [], 0x0) = 0', 'execve("/tmp/other", [], 0x0) = 0'],
policy,
trace_case,
)
def test_generated_host_view_covers_authoritative_fields(self):
baseline = DEV.host_contract_view(self.external, self.integrations, self.process_safety)
external = copy.deepcopy(self.external)
external["kernel_qualification"][0]["probe_binary_sha256"] = "0" * 64
self.assertNotEqual(DEV.host_contract_view(external, self.integrations, self.process_safety), baseline)
process = copy.deepcopy(self.process_safety)
process["runtime_dependency"][0]["features"] = ["changed"]
self.assertNotEqual(DEV.host_contract_view(self.external, self.integrations, process), baseline)
def test_integration_manifest_rejects_top_level_extras(self):
integrations = copy.deepcopy(self.integrations)
integrations["unexpected"] = True
capability_ids = {item["id"] for kind in ("command", "feature") for item in self.external[kind]}
with self.assertRaisesRegex(DEV.RegistryError, "unexpected fields"):
DEV.validate_integrations(integrations, capability_ids)
def test_missing_skip_capability_skips_scenario(self):
integrations = {
"scenarios": [{"name": "fuse", "level": "V2", "command": ["false"], "capabilities": ["fuse-device"]}]

View file

@ -21,6 +21,67 @@ import tomllib
ROOT = Path(__file__).resolve().parent.parent
SUPPORT = ROOT / "support"
LEVELS = ("V1", "V2", "V3", "V4", "V5")
PIDFS_SMOKE_STEPS = (
"linux-x86_64-kernel-floor",
"pidfd-operations",
"pidfs-magic",
"concurrent-same-process-identity",
"last-close-live-reopen-identity",
"simultaneous-process-distinct-inode",
"stale-handle-cannot-affect-live-peer",
"exit-reap-drop-all-later-process-non-reuse",
)
PROCESS_DISALLOWED_TYPES = (
"std::process::Child",
"std::process::Command",
"tokio::process::Child",
"tokio::process::Command",
)
PROCESS_DISALLOWED_METHODS = (
"libc::clone",
"libc::execve",
"libc::execveat",
"libc::fork",
"libc::posix_spawn",
"libc::sigaction",
"libc::signal",
"libc::vfork",
"libc::wait",
"libc::wait4",
"libc::waitid",
"libc::waitpid",
"nix::sys::signal::sigaction",
"nix::sys::wait::wait",
"nix::sys::wait::waitid",
"nix::sys::wait::waitpid",
"nix::unistd::execve",
"nix::unistd::execveat",
"nix::unistd::fork",
"nix::unistd::vfork",
"rustix::process::kill_process",
"rustix::process::kill_process_group",
"rustix::process::pidfd_open",
"rustix::process::pidfd_send_signal",
"rustix::process::waitid",
"rustix::process::waitpid",
"std::process::Child::kill",
"std::process::Child::try_wait",
"std::process::Child::wait",
"std::process::Child::wait_with_output",
"std::process::Command::output",
"std::process::Command::spawn",
"std::process::Command::status",
)
PROCESS_FORBIDDEN_SYSCALL_PATTERNS = (
r"^clone3?\(",
r"^execveat\(",
r"^fork\(",
r"^vfork\(",
r"^wait4\(",
r"^waitid\(",
r"^rt_sigaction\(SIGCHLD,",
)
PROCESS_REVIEW_DEPENDENCIES = frozenset({"libc", "nix", "rustix", "tokio"})
class RegistryError(RuntimeError):
@ -186,9 +247,23 @@ def validate_refinements(data: dict, package_ids: set[str]) -> None:
def validate_external_tools(data: dict) -> None:
validate_fields(data, {"schema", "levels", "command", "feature"}, set(), "external manifest")
require(data.get("schema") == 1, "external-tool manifest schema must be 1")
validate_fields(
data,
{
"schema",
"levels",
"pidfs_host_support",
"host_probe",
"kernel_qualification",
"command",
"feature",
},
set(),
"external manifest",
)
require(data.get("schema") == 2, "external-tool manifest schema must be 2")
require(data.get("levels") == list(LEVELS), "external-tool levels must be V1 through V5")
validate_pidfs_host_support(data)
commands = data.get("command")
features = data.get("feature")
require(isinstance(commands, list) and commands, "external command manifest is empty")
@ -199,7 +274,8 @@ def validate_external_tools(data: dict) -> None:
item_id = item.get("id")
require(isinstance(item_id, str) and item_id not in ids, "duplicate or invalid external capability ID")
ids.add(item_id)
require(item.get("policy") in {"fail", "skip"}, f"{item_id}: policy must be fail or skip")
item_policy = item.get("policy")
require(isinstance(item_policy, str) and item_policy in {"fail", "skip"}, f"{item_id}: policy must be fail or skip")
validate_levels(item.get("levels"), item_id)
require(isinstance(item.get("diagnostic"), str) and item["diagnostic"], f"{item_id}: diagnostic is empty")
required = {
@ -210,8 +286,10 @@ def validate_external_tools(data: dict) -> None:
"runsc",
"fuse-overlayfs",
"slirp4netns",
"strace",
"linux-x86_64",
"kernel",
"pidfs-host-support",
"user-namespaces",
"subordinate-ids",
"fuse-device",
@ -239,6 +317,7 @@ def validate_external_tools(data: dict) -> None:
probe_fields = {
"linux-x86_64": set(),
"kernel-at-least": {"minimum"},
"registered-host-support": {"contract"},
"user-namespace": {"program", "sysctl_path"},
"subordinate-ids": {"minimum_count", "uidmap_program", "gidmap_program", "unshare_program"},
"open-character-device": {"path", "access"},
@ -246,11 +325,15 @@ def validate_external_tools(data: dict) -> None:
for feature in features:
feature_id = feature["id"]
probe_name = feature.get("probe")
require(probe_name in probe_fields, f"{feature_id}: unknown feature probe")
require(isinstance(probe_name, str) and probe_name in probe_fields, f"{feature_id}: unknown feature probe")
validate_fields(feature, common | probe_fields[probe_name], set(), feature_id)
if probe_name == "kernel-at-least":
minimum = feature["minimum"]
require(isinstance(minimum, str) and re.fullmatch(r"\d+(?:\.\d+)+", minimum) is not None, f"{feature_id}: minimum must be a dotted numeric version")
if feature_id == "kernel":
require(minimum == data["pidfs_host_support"]["minimum_kernel"], "kernel: minimum must match pidfs host contract")
elif probe_name == "registered-host-support":
require(feature["contract"] == data["pidfs_host_support"]["id"], f"{feature_id}: unknown host contract")
elif probe_name == "user-namespace":
require(isinstance(feature["program"], str) and feature["program"], f"{feature_id}: program is empty")
require(isinstance(feature["sysctl_path"], str) and Path(feature["sysctl_path"]).is_absolute(), f"{feature_id}: sysctl_path must be absolute")
@ -263,14 +346,172 @@ def validate_external_tools(data: dict) -> None:
require(isinstance(feature["path"], str) and Path(feature["path"]).is_absolute(), f"{feature_id}: path must be absolute")
require(feature["access"] == "read-write", f"{feature_id}: access must be read-write")
command_contracts = {
"rustc": ("rustc", "fail", list(LEVELS)),
"cargo": ("cargo", "fail", list(LEVELS)),
"cargo-deny": ("cargo-deny", "fail", list(LEVELS)),
"tlc": ("tlc", "fail", list(LEVELS)),
"runsc": ("runsc", "skip", ["V3", "V4", "V5"]),
"fuse-overlayfs": ("fuse-overlayfs", "skip", ["V2", "V3", "V4", "V5"]),
"slirp4netns": ("slirp4netns", "skip", ["V2", "V3", "V4", "V5"]),
"strace": ("strace", "skip", ["V2", "V3", "V4", "V5"]),
}
commands_by_id = {command["id"]: command for command in commands}
for command_id, (program, policy, levels) in command_contracts.items():
require(command_id in commands_by_id, f"{command_id}: mandatory command entry is missing")
command = commands_by_id[command_id]
require((command["program"], command["policy"], command["levels"]) == (program, policy, levels), f"{command_id}: mandatory command contract drift")
feature_contracts = {
"linux-x86_64": ("linux-x86_64", "skip", ["V2", "V3", "V4", "V5"]),
"kernel": ("kernel-at-least", "skip", ["V2", "V3", "V4", "V5"]),
"pidfs-host-support": ("registered-host-support", "skip", ["V2", "V3", "V4", "V5"]),
"user-namespaces": ("user-namespace", "skip", ["V2", "V3", "V4", "V5"]),
"subordinate-ids": ("subordinate-ids", "skip", ["V2", "V3", "V4", "V5"]),
"fuse-device": ("open-character-device", "skip", ["V2", "V3", "V4", "V5"]),
}
features_by_id = {feature["id"]: feature for feature in features}
for feature_id, (probe_name, policy, levels) in feature_contracts.items():
require(feature_id in features_by_id, f"{feature_id}: mandatory feature entry is missing")
feature = features_by_id[feature_id]
require((feature["probe"], feature["policy"], feature["levels"]) == (probe_name, policy, levels), f"{feature_id}: mandatory feature contract drift")
def validate_pidfs_host_support(data: dict) -> None:
contract = validate_fields(
data.get("pidfs_host_support"),
{
"id",
"platform",
"minimum_kernel",
"probe",
"qualification_ids",
"runtime_failure_policy",
"verifier_unavailable_policy",
"bounded_claim",
"diagnostic_platform",
"diagnostic_kernel",
"diagnostic_qualification",
"diagnostic_smoke",
},
set(),
"pidfs host support",
)
require(contract["id"] == "pidfs-process-identity-v1", "pidfs host support: invalid contract ID")
require(contract["platform"] == "linux-x86_64", "pidfs host support: platform must be linux-x86_64")
require(contract["minimum_kernel"] == "6.9", "pidfs host support: minimum kernel must be 6.9")
require(contract["runtime_failure_policy"] == "fail-before-generation", "pidfs host support: invalid runtime failure policy")
require(contract["verifier_unavailable_policy"] == "skip", "pidfs host support: invalid verifier policy")
require(contract["bounded_claim"] == "current-boot-smoke-only", "pidfs host support: invalid smoke claim")
for diagnostic in (
"diagnostic_platform",
"diagnostic_kernel",
"diagnostic_qualification",
"diagnostic_smoke",
):
require(isinstance(contract[diagnostic], str) and contract[diagnostic].startswith("LZ-HOST-PIDFS-"), f"pidfs host support: invalid {diagnostic}")
probes = data.get("host_probe")
require(isinstance(probes, list) and len(probes) == 1, "pidfs host support: exactly one host probe is required")
probe = validate_fields(
probes[0],
{
"id",
"owner",
"cache_key",
"result_claim",
"attempts_per_step",
"step_timeout_ms",
"total_timeout_ms",
"cleanup_timeout_ms",
"steps",
},
set(),
"pidfs host probe",
)
require(probe["id"] == contract["probe"], "pidfs host support: probe ID mismatch")
require(probe["owner"] == "IP-03", "pidfs host probe: owner must be IP-03")
require(probe["cache_key"] == "boot_id", "pidfs host probe: cache key must be boot_id")
require(probe["result_claim"] == "bounded-current-boot-observations", "pidfs host probe: result claim must remain bounded")
require(isinstance(probe["attempts_per_step"], int) and not isinstance(probe["attempts_per_step"], bool) and probe["attempts_per_step"] == 1, "pidfs host probe: each step must run exactly once")
expected_timeouts = {"step_timeout_ms": 5000, "total_timeout_ms": 60000, "cleanup_timeout_ms": 5000}
for field, expected in expected_timeouts.items():
value = probe[field]
require(isinstance(value, int) and not isinstance(value, bool) and value == expected, f"pidfs host probe: {field} must be {expected}")
require(probe["steps"] == list(PIDFS_SMOKE_STEPS), "pidfs host probe: smoke steps do not match the selected contract")
qualifications = data.get("kernel_qualification")
require(isinstance(qualifications, list) and qualifications, "kernel qualification registry is empty")
by_id = {}
hashes = {
"source_archive_sha256",
"build_config_sha256",
"build_image_sha256",
"probe_binary_sha256",
"reuse_result_sha256",
"acquisition_result_sha256",
}
common = {
"id",
"kind",
"architecture",
"release",
"source_ref",
"source_commit",
"source_archive_sha256",
"source_signer_fingerprint",
"build_config_sha256",
"build_image_sha256",
"probe_binary_sha256",
"reuse_result_sha256",
"acquisition_result_sha256",
"pidfs_inode_semantics",
"status",
}
for qualification in qualifications:
require(isinstance(qualification, dict), "kernel qualification entry must be a table")
kind = qualification.get("kind")
optional = {"vendor", "vendor_patchset_sha256"} if kind == "vendor" else set()
qualification = validate_fields(qualification, common, optional, f"kernel qualification {qualification.get('id')}")
qualification_id = qualification["id"]
require(isinstance(qualification_id, str) and qualification_id and qualification_id not in by_id, "duplicate or invalid kernel qualification ID")
require(isinstance(kind, str) and kind in {"upstream", "vendor"}, f"{qualification_id}: invalid qualification kind")
require(qualification["architecture"] == "x86_64", f"{qualification_id}: architecture must be x86_64")
release = qualification["release"]
require(isinstance(release, str) and release, f"{qualification_id}: invalid release")
release_version = kernel_version_tuple(release)
require(release_version is not None, f"{qualification_id}: release lacks a numeric kernel prefix")
require(release_version >= version_tuple(contract["minimum_kernel"]), f"{qualification_id}: release is below the pidfs floor")
require(isinstance(qualification["source_ref"], str) and qualification["source_ref"], f"{qualification_id}: source ref is empty")
source_commit = qualification["source_commit"]
signer_fingerprint = qualification["source_signer_fingerprint"]
require(isinstance(source_commit, str) and re.fullmatch(r"[0-9a-f]{40}", source_commit) is not None, f"{qualification_id}: invalid source commit")
require(isinstance(signer_fingerprint, str) and re.fullmatch(r"[0-9A-F]{40}", signer_fingerprint) is not None, f"{qualification_id}: invalid signer fingerprint")
for field in hashes:
value = qualification[field]
require(isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None, f"{qualification_id}: invalid {field}")
require(qualification["pidfs_inode_semantics"] == "boot-monotonic-u64", f"{qualification_id}: invalid pidfs inode semantics")
require(qualification["status"] == "qualified", f"{qualification_id}: build is not qualified")
if kind == "vendor":
require(isinstance(qualification.get("vendor"), str) and qualification["vendor"], f"{qualification_id}: vendor is required")
patchset = qualification.get("vendor_patchset_sha256")
require(isinstance(patchset, str) and re.fullmatch(r"[0-9a-f]{64}", patchset) is not None, f"{qualification_id}: invalid vendor patchset hash")
by_id[qualification_id] = qualification
qualification_ids = contract["qualification_ids"]
require(isinstance(qualification_ids, list) and qualification_ids and all(isinstance(item, str) and item for item in qualification_ids), "pidfs host support: qualification IDs are empty or invalid")
require(len(qualification_ids) == len(set(qualification_ids)), "pidfs host support: duplicate qualification IDs")
require(set(qualification_ids) <= set(by_id), "pidfs host support: unknown qualification ID")
def validate_integrations(data: dict, capability_ids: set[str]) -> None:
validate_fields(data, {"schema", "levels", "scenarios"}, set(), "integration manifest")
require(data.get("schema") == 1, "integration manifest schema must be 1")
require(data.get("levels") == list(LEVELS[1:]), "integration levels must be V2 through V5")
scenarios = data.get("scenarios")
require(isinstance(scenarios, list), "integration scenarios must be a list")
names = set()
for scenario in scenarios:
validate_fields(scenario, {"name", "level", "command", "capabilities"}, set(), "integration scenario")
name = scenario.get("name")
require(isinstance(name, str) and name and name not in names, "duplicate or invalid integration scenario")
names.add(name)
@ -278,11 +519,97 @@ def validate_integrations(data: dict, capability_ids: set[str]) -> None:
command = scenario.get("command")
require(isinstance(command, list) and command and all(isinstance(part, str) for part in command), f"{name}: invalid command")
capabilities = scenario.get("capabilities", [])
require(isinstance(capabilities, list), f"{name}: capabilities must be a list")
require(isinstance(capabilities, list) and all(isinstance(capability, str) for capability in capabilities), f"{name}: capabilities must be strings")
require(all(capability in capability_ids for capability in capabilities), f"{name}: unknown capability")
def validated_data() -> tuple[list[dict], dict, dict]:
def validate_process_safety(data: dict, integrations: dict) -> None:
validate_fields(data, {"schema", "policy", "trace_case", "runtime_dependency"}, set(), "process-safety manifest")
require(data.get("schema") == 2, "process-safety manifest schema must be 2")
policy = validate_fields(
data.get("policy"),
{
"id",
"broker_crate",
"broker_state",
"production_roots",
"lint_config",
"lint_allow_paths",
"dynamic_scenario",
"dynamic_coverage",
"completion_owner",
"trace_expression",
"forbidden_syscall_patterns",
"disallowed_types",
"disallowed_methods",
},
set(),
"process-safety policy",
)
require(policy["id"] == "causal-child-acquisition-v1", "process-safety policy: invalid ID")
require(policy["broker_crate"] == "lazaret-process-broker", "process-safety policy: invalid broker crate")
require(policy["broker_state"] == "unimplemented", "process-safety policy: IP-03 has not implemented the broker")
require(policy["lint_config"] == ".clippy.toml", "process-safety policy: invalid lint config")
require(policy["dynamic_coverage"] == "registered-exercised-paths-only", "process-safety policy: dynamic coverage is overstated")
require(policy["dynamic_scenario"] == "process-safety-baseline-exercised-paths", "process-safety policy: dynamic scenario changed")
require(policy["completion_owner"] == "IP-03", "process-safety policy: completion owner must be IP-03")
for field in ("production_roots", "lint_allow_paths"):
values = policy[field]
require(isinstance(values, list) and all(isinstance(value, str) and value for value in values), f"process-safety policy: invalid {field}")
require(len(values) == len(set(values)), f"process-safety policy: duplicate {field}")
require(policy["production_roots"] == ["crates"], "process-safety policy: production roots must cover the complete crates tree")
require(not policy["lint_allow_paths"], "process-safety policy: no lint exemption is allowed before IP-03 implements the broker crate")
require(policy["trace_expression"] == "process,wait4,waitid,rt_sigaction", "process-safety policy: trace expression must cover process, wait, and signal-disposition syscalls")
require(policy["forbidden_syscall_patterns"] == list(PROCESS_FORBIDDEN_SYSCALL_PATTERNS), "process-safety policy: forbidden syscall coverage drift")
require(policy["disallowed_types"] == list(PROCESS_DISALLOWED_TYPES), "process-safety policy: disallowed type coverage drift")
require(policy["disallowed_methods"] == list(PROCESS_DISALLOWED_METHODS), "process-safety policy: disallowed method coverage drift")
for pattern in policy["forbidden_syscall_patterns"]:
try:
re.compile(pattern)
except re.error as error:
raise RegistryError(f"process-safety policy: invalid pattern {pattern}: {error}") from error
trace_cases = data.get("trace_case")
require(isinstance(trace_cases, list) and trace_cases, "process-safety trace cases are empty")
binaries = set()
for trace_case in trace_cases:
trace_case = validate_fields(trace_case, {"binary", "args", "expected_exit_code", "initial_execve_count"}, set(), "process-safety trace case")
binary = trace_case["binary"]
require(isinstance(binary, str) and binary and binary not in binaries, "duplicate or invalid process-safety trace binary")
require(isinstance(trace_case["args"], list) and all(isinstance(arg, str) for arg in trace_case["args"]), f"{binary}: invalid trace arguments")
require(isinstance(trace_case["expected_exit_code"], int) and not isinstance(trace_case["expected_exit_code"], bool), f"{binary}: invalid expected exit code")
initial_execve_count = trace_case["initial_execve_count"]
require(isinstance(initial_execve_count, int) and not isinstance(initial_execve_count, bool) and initial_execve_count == 1, f"{binary}: exactly one tracer-launched execve is required")
binaries.add(binary)
dependencies = data.get("runtime_dependency")
require(isinstance(dependencies, list) and dependencies, "process-safety dependency registry is empty")
identities = set()
for dependency in dependencies:
dependency = validate_fields(dependency, {"name", "version", "checksum", "features", "reviewed_process_apis"}, set(), "process-safety dependency")
identity = (dependency["name"], dependency["version"])
require(all(isinstance(item, str) and item for item in identity), "process-safety dependency: invalid identity")
require(identity not in identities, f"process-safety dependency: duplicate {identity[0]} {identity[1]}")
checksum = dependency["checksum"]
require(isinstance(checksum, str) and re.fullmatch(r"[0-9a-f]{64}", checksum) is not None, f"{identity[0]} {identity[1]}: invalid source checksum")
features = dependency["features"]
require(isinstance(features, list) and all(isinstance(feature, str) and feature for feature in features), f"{identity[0]} {identity[1]}: invalid features")
require(features == sorted(set(features)), f"{identity[0]} {identity[1]}: features must be unique and sorted")
process_apis = dependency["reviewed_process_apis"]
require(isinstance(process_apis, list) and all(isinstance(api, str) and api for api in process_apis), f"{identity[0]} {identity[1]}: invalid reviewed process APIs")
require(process_apis == sorted(set(process_apis)), f"{identity[0]} {identity[1]}: reviewed process APIs must be unique and sorted")
identities.add(identity)
scenarios = {scenario["name"]: scenario for scenario in integrations["scenarios"]}
scenario_name = policy["dynamic_scenario"]
require(scenario_name in scenarios, "process-safety policy: dynamic syscall scenario is not registered")
scenario = scenarios[scenario_name]
require(scenario["level"] == "V2", "process-safety policy: dynamic syscall scenario must be V2")
require(scenario["command"] == ["tools/dev", "process-audit", "--dynamic"], "process-safety policy: dynamic syscall command mismatch")
require(set(scenario["capabilities"]) == {"linux-x86_64", "strace"}, "process-safety policy: dynamic syscall capabilities mismatch")
def validated_data() -> tuple[list[dict], dict, dict, dict]:
packages = validate_packages(load_toml(SUPPORT / "work-packages.toml"))
package_ids = {package["id"] for package in packages}
refinements = load_toml(SUPPORT / "model-refinements.toml")
@ -292,7 +619,9 @@ def validated_data() -> tuple[list[dict], dict, dict]:
capability_ids = {item["id"] for kind in ("command", "feature") for item in external[kind]}
integrations = load_toml(SUPPORT / "integration-tests.toml")
validate_integrations(integrations, capability_ids)
return packages, external, integrations
process_safety = load_toml(SUPPORT / "process-safety.toml")
validate_process_safety(process_safety, integrations)
return packages, external, integrations, process_safety
def dependency_view(packages: list[dict]) -> str:
@ -320,21 +649,101 @@ def dependency_view(packages: list[dict]) -> str:
return "\n".join(lines)
def host_contract_view(external: dict, integrations: dict, process_safety: dict) -> str:
contract = external["pidfs_host_support"]
probe = external["host_probe"][0]
lines = [
"# Host support and process-safety registry",
"",
"Generated from `support/external-tools.toml` and `support/process-safety.toml`; do not edit this file directly.",
"",
"## Pidfs host support",
"",
f"- Contract: `{contract['id']}`",
f"- Platform floor: `{contract['platform']}` kernel `{contract['minimum_kernel']}` or newer",
f"- Runtime refusal: `{contract['runtime_failure_policy']}`",
f"- Unavailable verifier behavior: `{contract['verifier_unavailable_policy']}`",
f"- Smoke claim: `{contract['bounded_claim']}` cached only by `{probe['cache_key']}`",
f"- Smoke bounds: one attempt per step, `{probe['step_timeout_ms']}` ms per step, `{probe['total_timeout_ms']}` ms total, `{probe['cleanup_timeout_ms']}` ms cleanup",
"- Positive support: unavailable until IP-03 implements an exact running-build matcher and the smoke runner",
"",
"The smoke steps are bounded observations and never qualify source or build provenance:",
"",
]
lines.extend(f"1. `{step}`" for step in probe["steps"])
lines.extend(["", "## Qualified kernel builds", "", "| ID | Kind | Release | Source commit | Build image SHA-256 |", "| --- | --- | --- | --- | --- |"])
by_id = {item["id"]: item for item in external["kernel_qualification"]}
for qualification_id in contract["qualification_ids"]:
item = by_id[qualification_id]
lines.append(f"| `{qualification_id}` | {item['kind']} | `{item['release']}` | `{item['source_commit']}` | `{item['build_image_sha256']}` |")
policy = process_safety["policy"]
lines.extend(
[
"",
"## Process-safety audit",
"",
f"Policy `{policy['id']}` pins {len(process_safety['runtime_dependency'])} dependency source/version/feature/API-review records, generates compiler-resolved disallowed type/method lints, and registers `{policy['dynamic_scenario']}` as V2 exercised-path tracing.",
"",
f"The dedicated `{policy['broker_crate']}` boundary is `{policy['broker_state']}` and no lint exemption is authorized. Dynamic coverage is `{policy['dynamic_coverage']}`; it does not claim unexercised-path coverage.",
"",
"## Complete registered data",
"",
"Every authoritative host-qualification and process-safety field is reproduced below so generated-view drift remains complete.",
"",
"```json",
]
)
host_capability_ids = {"linux-x86_64", "kernel", "pidfs-host-support"}
registered_data = {
"pidfs_host_support": contract,
"host_probe": external["host_probe"],
"kernel_qualification": external["kernel_qualification"],
"host_capabilities": [feature for feature in external["feature"] if feature["id"] in host_capability_ids],
"process_trace_capability": next(command for command in external["command"] if command["id"] == "strace"),
"process_trace_scenario": next(scenario for scenario in integrations["scenarios"] if scenario["name"] == policy["dynamic_scenario"]),
"process_safety": process_safety,
"workspace_lints": load_toml(ROOT / "Cargo.toml")["workspace"]["lints"],
"workspace_member_lints": workspace_member_lints(),
}
lines.extend([json.dumps(registered_data, indent=2, sort_keys=True), "```", ""])
return "\n".join(lines)
def clippy_config_view(process_safety: dict) -> str:
policy = process_safety["policy"]
reason = "process control is confined to the reviewed Lazaret spawn/reap broker"
lines = ["# Generated from support/process-safety.toml; do not edit.", "", "disallowed-types = ["]
lines.extend(f" {{ path = {json.dumps(path)}, reason = {json.dumps(reason)} }}," for path in policy["disallowed_types"])
lines.extend(["]", "", "disallowed-methods = ["])
lines.extend(f" {{ path = {json.dumps(path)}, reason = {json.dumps(reason)} }}," for path in policy["disallowed_methods"])
lines.extend(["]", ""])
return "\n".join(lines)
def generate(check: bool) -> None:
packages, _, _ = validated_data()
target = ROOT / "docs/implementation-dependencies.md"
content = dependency_view(packages)
if check:
require(target.is_file(), f"generated file missing: {target.relative_to(ROOT)}")
require(target.read_text() == content, f"generated file drift: {target.relative_to(ROOT)}")
else:
target.write_text(content)
packages, external, integrations, process_safety = validated_data()
outputs = {
ROOT / "docs/implementation-dependencies.md": dependency_view(packages),
ROOT / "docs/host-support.md": host_contract_view(external, integrations, process_safety),
ROOT / ".clippy.toml": clippy_config_view(process_safety),
}
for target, content in outputs.items():
if check:
require(target.is_file(), f"generated file missing: {target.relative_to(ROOT)}")
require(target.read_text() == content, f"generated file drift: {target.relative_to(ROOT)}")
else:
target.write_text(content)
def version_tuple(value: str) -> tuple[int, ...]:
return tuple(int(part) for part in value.split("."))
def kernel_version_tuple(release: str) -> tuple[int, ...] | None:
match = re.match(r"\d+(?:\.\d+)+", release)
return version_tuple(match.group()) if match is not None else None
def subordinate_id_range(path: Path, principals: set[str], minimum_count: int) -> int | None:
try:
rows = path.read_text().splitlines()
@ -432,12 +841,12 @@ def probe_feature(feature: dict) -> tuple[bool, str]:
passed = platform.system() == "Linux" and platform.machine() == "x86_64"
return passed, f"{platform.system()} {platform.machine()}"
if probe == "kernel-at-least":
release = platform.release().split("-", 1)[0]
try:
passed = platform.system() == "Linux" and version_tuple(release) >= version_tuple(feature["minimum"])
except ValueError:
passed = False
release = platform.release()
observed = kernel_version_tuple(release)
passed = platform.system() == "Linux" and observed is not None and observed >= version_tuple(feature["minimum"])
return passed, release
if probe == "registered-host-support":
return False, "registered runner pending IP-03"
if probe == "user-namespace":
try:
value = int(Path(feature["sysctl_path"]).read_text().strip())
@ -464,7 +873,7 @@ def probe_feature(feature: dict) -> tuple[bool, str]:
def probe(level: str, external: dict | None = None) -> tuple[set[str], set[str]]:
if external is None:
_, external, _ = validated_data()
_, external, _, _ = validated_data()
available = set()
skipped = set()
failed = False
@ -538,6 +947,148 @@ def dependency_audit() -> None:
print("PASS dependency source, wildcard, and license audit")
def has_process_lint_exemption(source: str) -> bool:
warnings_allow = re.compile(r"#\s*!?\s*\[[^]]*(?:allow|expect)\s*\([^]]*warnings")
return "disallowed_methods" in source or "disallowed_types" in source or warnings_allow.search(source) is not None
def validate_workspace_process_lints(workspace: dict) -> None:
lints = workspace.get("workspace", {}).get("lints", {})
require(lints.get("rust", {}).get("unsafe_code") == "forbid", "workspace must forbid unsafe Rust")
clippy = lints.get("clippy", {})
require(clippy.get("disallowed_methods") == "deny", "workspace must deny Clippy disallowed methods")
require(clippy.get("disallowed_types") == "deny", "workspace must deny Clippy disallowed types")
def validate_member_process_lints(manifest: dict, package: str) -> None:
require(manifest.get("lints") == {"workspace": True}, f"{package}: package must inherit workspace lints without override")
def workspace_member_lints() -> dict[str, dict]:
workspace = load_toml(ROOT / "Cargo.toml")["workspace"]
return {member: load_toml(ROOT / member / "Cargo.toml").get("lints", {}) for member in workspace["members"]}
def process_source_audit(process_safety: dict) -> None:
policy = process_safety["policy"]
validate_workspace_process_lints(load_toml(ROOT / "Cargo.toml"))
require((ROOT / policy["lint_config"]).read_text() == clippy_config_view(process_safety), "generated Clippy process policy drift")
lint_allow_paths = set(policy["lint_allow_paths"])
for root in policy["production_roots"]:
root_path = ROOT / root
require(root_path.is_dir(), f"process-safety production root is missing: {root}")
for path in sorted(root_path.rglob("*.rs")):
relative = str(path.relative_to(ROOT))
has_allow = has_process_lint_exemption(path.read_text(errors="replace"))
require(not has_allow or relative in lint_allow_paths, f"process-safety lint exemption outside the broker boundary: {relative}")
result = subprocess.run(
["cargo", "metadata", "--locked", "--offline", "--all-features", "--format-version", "1"],
cwd=ROOT,
text=True,
stdout=subprocess.PIPE,
check=True,
)
metadata = json.loads(result.stdout)
packages = {package["id"]: package for package in metadata["packages"]}
workspace_members = set(metadata["workspace_members"])
for package_id in workspace_members:
package = packages[package_id]
validate_member_process_lints(load_toml(Path(package["manifest_path"])), package["name"])
lock_packages = {
(package["name"], package["version"]): package
for package in load_toml(ROOT / "Cargo.lock")["package"]
if "source" in package
}
actual_dependencies = {}
for node in metadata["resolve"]["nodes"]:
if node["id"] in workspace_members:
continue
package = packages[node["id"]]
identity = (package["name"], package["version"])
require(identity not in actual_dependencies, f"duplicate resolved dependency {identity[0]} {identity[1]}")
require(identity in lock_packages, f"resolved dependency is absent from Cargo.lock: {identity[0]} {identity[1]}")
lock_package = lock_packages[identity]
actual_dependencies[identity] = {
"checksum": lock_package.get("checksum"),
"features": sorted(node["features"]),
}
expected_dependencies = {
(dependency["name"], dependency["version"]): {
"checksum": dependency["checksum"],
"features": dependency["features"],
}
for dependency in process_safety["runtime_dependency"]
}
require(actual_dependencies == expected_dependencies, f"process-safety dependency source/feature drift; expected={expected_dependencies}, actual={actual_dependencies}")
for dependency in process_safety["runtime_dependency"]:
if dependency["name"] in PROCESS_REVIEW_DEPENDENCIES:
require(dependency["reviewed_process_apis"], f"{dependency['name']} {dependency['version']}: process APIs require explicit review")
print("PASS process-safety dependency source/features and compiler lint-boundary audit")
def validate_process_trace(lines: list[str], policy: dict, trace_case: dict) -> None:
binary = trace_case["binary"]
execve_count = sum(line.startswith("execve(") for line in lines)
require(execve_count == trace_case["initial_execve_count"], f"{binary}: unexpected execve surface count {execve_count}")
syscall_patterns = [re.compile(pattern) for pattern in policy["forbidden_syscall_patterns"]]
for line in lines:
matches = [pattern.pattern for pattern in syscall_patterns if pattern.search(line) is not None]
require(not matches, f"{binary}: forbidden process syscall surface {line}: {matches}")
def process_syscall_audit(process_safety: dict) -> None:
policy = process_safety["policy"]
strace = shutil.which("strace")
require(strace is not None, "strace is required for the dynamic process-syscall audit")
run(["cargo", "build", "--offline", "--workspace", "--all-features", "--bins", "--locked"])
metadata_result = subprocess.run(
["cargo", "metadata", "--locked", "--offline", "--all-features", "--format-version", "1"],
cwd=ROOT,
text=True,
stdout=subprocess.PIPE,
check=True,
)
metadata = json.loads(metadata_result.stdout)
workspace_members = set(metadata["workspace_members"])
binaries = {
target["name"]
for package in metadata["packages"]
if package["id"] in workspace_members
for target in package["targets"]
if "bin" in target["kind"]
}
trace_cases = {trace_case["binary"]: trace_case for trace_case in process_safety["trace_case"]}
require(set(trace_cases) == binaries, f"process-safety trace-case coverage drift; expected={sorted(binaries)}, actual={sorted(trace_cases)}")
with tempfile.TemporaryDirectory(prefix="lazaret-process-syscalls-") as directory:
for binary in sorted(binaries):
trace_case = trace_cases[binary]
trace_base = Path(directory) / binary
result = subprocess.run(
[
strace,
"-ff",
"-qq",
"-e",
f"trace={policy['trace_expression']}",
"-o",
str(trace_base),
"--",
str(ROOT / "target" / "debug" / binary),
*trace_case["args"],
],
cwd=ROOT,
check=False,
)
require(result.returncode == trace_case["expected_exit_code"], f"{binary}: exercised path exited {result.returncode}")
traces = sorted(Path(directory).glob(f"{binary}.*"))
require(traces, f"process-safety syscall audit produced no trace for {binary}")
lines = [line for trace in traces for line in trace.read_text(errors="replace").splitlines()]
validate_process_trace(lines, policy, trace_case)
print("PASS registered final-binary exercised-path process-syscall audit")
def refresh_advisories() -> None:
run(["cargo", "deny", "fetch"])
run(["cargo", "deny", "--frozen", "check", "advisories"])
@ -570,7 +1121,7 @@ def run_integrations(level: str, external: dict, integrations: dict) -> None:
def verify(level: str) -> None:
_, external, integrations = validated_data()
_, external, integrations, process_safety = validated_data()
generate(check=True)
probe("V1", external)
run(["cargo", "fmt", "--all", "--", "--check"])
@ -579,6 +1130,7 @@ def verify(level: str) -> None:
run(["cargo", "test", "--offline", "--workspace", "--all-features", "--locked", "--doc"])
run([sys.executable, "-m", "unittest", "discover", "-s", "tests/tooling"])
dependency_audit()
process_source_audit(process_safety)
run_models()
target_index = LEVELS.index(level)
for current in LEVELS[1:target_index + 1]:
@ -599,6 +1151,8 @@ def main() -> int:
subparsers.add_parser("dependencies")
subparsers.add_parser("refresh-advisories")
subparsers.add_parser("models")
process_audit_parser = subparsers.add_parser("process-audit")
process_audit_parser.add_argument("--dynamic", action="store_true")
args = parser.parse_args()
try:
if args.command == "generate":
@ -617,6 +1171,11 @@ def main() -> int:
elif args.command == "models":
validated_data()
run_models()
elif args.command == "process-audit":
_, _, _, process_safety = validated_data()
process_source_audit(process_safety)
if args.dynamic:
process_syscall_audit(process_safety)
except (RegistryError, OSError, subprocess.CalledProcessError) as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1