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] [workspace.lints.clippy]
all = { level = "deny", priority = -1 } all = { level = "deny", priority = -1 }
pedantic = { level = "deny", priority = -1 } pedantic = { level = "deny", priority = -1 }
disallowed_methods = "deny"
disallowed_types = "deny"
[profile.release] [profile.release]
codegen-units = 1 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. 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. 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"] 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]] [[command]]
id = "python" id = "python"
program = "python3" program = "python3"
@ -83,6 +134,15 @@ policy = "skip"
levels = ["V2", "V3", "V4", "V5"] levels = ["V2", "V3", "V4", "V5"]
diagnostic = "Install the qualified slirp4netns 1.3.1 and libslirp 4.9.1 stack." 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]] [[feature]]
id = "linux-x86_64" id = "linux-x86_64"
probe = "linux-x86_64" probe = "linux-x86_64"
@ -93,10 +153,18 @@ diagnostic = "Physical and composition suites currently support Linux x86_64."
[[feature]] [[feature]]
id = "kernel" id = "kernel"
probe = "kernel-at-least" probe = "kernel-at-least"
minimum = "6.8" minimum = "6.9"
policy = "skip" policy = "skip"
levels = ["V2", "V3", "V4", "V5"] 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]] [[feature]]
id = "user-namespaces" id = "user-namespaces"

View file

@ -1,3 +1,8 @@
schema = 1 schema = 1
levels = ["V2", "V3", "V4", "V5"] 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 importlib.util
import io import io
from pathlib import Path from pathlib import Path
import subprocess
import tempfile import tempfile
import unittest import unittest
from unittest import mock from unittest import mock
@ -19,11 +20,16 @@ class RegistryTests(unittest.TestCase):
self.packages = DEV.load_toml(ROOT / "support/work-packages.toml") self.packages = DEV.load_toml(ROOT / "support/work-packages.toml")
self.refinements = DEV.load_toml(ROOT / "support/model-refinements.toml") self.refinements = DEV.load_toml(ROOT / "support/model-refinements.toml")
self.external = DEV.load_toml(ROOT / "support/external-tools.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): def test_committed_registries_are_complete(self):
packages = DEV.validate_packages(self.packages) packages = DEV.validate_packages(self.packages)
DEV.validate_refinements(self.refinements, {package["id"] for package in packages}) DEV.validate_refinements(self.refinements, {package["id"] for package in packages})
DEV.validate_external_tools(self.external) 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): def test_dependency_cycle_is_rejected(self):
packages = copy.deepcopy(self.packages) packages = copy.deepcopy(self.packages)
@ -70,6 +76,7 @@ class RegistryTests(unittest.TestCase):
invalid = { invalid = {
"linux-x86_64": {"id": 1}, "linux-x86_64": {"id": 1},
"kernel-at-least": {"minimum": 6.8}, "kernel-at-least": {"minimum": 6.8},
"registered-host-support": {"contract": "unknown"},
"user-namespace": {"program": 1}, "user-namespace": {"program": 1},
"subordinate-ids": {"minimum_count": "65536"}, "subordinate-ids": {"minimum_count": "65536"},
"open-character-device": {"access": "read-only"}, "open-character-device": {"access": "read-only"},
@ -90,13 +97,217 @@ class RegistryTests(unittest.TestCase):
DEV.validate_external_tools(external) DEV.validate_external_tools(external)
def test_supported_platform_probe_is_mandatory(self): 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): with self.subTest(capability=capability):
external = copy.deepcopy(self.external) external = copy.deepcopy(self.external)
external["feature"] = [feature for feature in external["feature"] if feature["id"] != capability] external["feature"] = [feature for feature in external["feature"] if feature["id"] != capability]
with self.assertRaisesRegex(DEV.RegistryError, "external manifest missing"): with self.assertRaisesRegex(DEV.RegistryError, "external manifest missing"):
DEV.validate_external_tools(external) 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): def test_missing_skip_capability_skips_scenario(self):
integrations = { integrations = {
"scenarios": [{"name": "fuse", "level": "V2", "command": ["false"], "capabilities": ["fuse-device"]}] "scenarios": [{"name": "fuse", "level": "V2", "command": ["false"], "capabilities": ["fuse-device"]}]

View file

@ -21,6 +21,67 @@ import tomllib
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
SUPPORT = ROOT / "support" SUPPORT = ROOT / "support"
LEVELS = ("V1", "V2", "V3", "V4", "V5") 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): class RegistryError(RuntimeError):
@ -186,9 +247,23 @@ def validate_refinements(data: dict, package_ids: set[str]) -> None:
def validate_external_tools(data: dict) -> None: def validate_external_tools(data: dict) -> None:
validate_fields(data, {"schema", "levels", "command", "feature"}, set(), "external manifest") validate_fields(
require(data.get("schema") == 1, "external-tool manifest schema must be 1") 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") require(data.get("levels") == list(LEVELS), "external-tool levels must be V1 through V5")
validate_pidfs_host_support(data)
commands = data.get("command") commands = data.get("command")
features = data.get("feature") features = data.get("feature")
require(isinstance(commands, list) and commands, "external command manifest is empty") 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") item_id = item.get("id")
require(isinstance(item_id, str) and item_id not in ids, "duplicate or invalid external capability ID") require(isinstance(item_id, str) and item_id not in ids, "duplicate or invalid external capability ID")
ids.add(item_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) validate_levels(item.get("levels"), item_id)
require(isinstance(item.get("diagnostic"), str) and item["diagnostic"], f"{item_id}: diagnostic is empty") require(isinstance(item.get("diagnostic"), str) and item["diagnostic"], f"{item_id}: diagnostic is empty")
required = { required = {
@ -210,8 +286,10 @@ def validate_external_tools(data: dict) -> None:
"runsc", "runsc",
"fuse-overlayfs", "fuse-overlayfs",
"slirp4netns", "slirp4netns",
"strace",
"linux-x86_64", "linux-x86_64",
"kernel", "kernel",
"pidfs-host-support",
"user-namespaces", "user-namespaces",
"subordinate-ids", "subordinate-ids",
"fuse-device", "fuse-device",
@ -239,6 +317,7 @@ def validate_external_tools(data: dict) -> None:
probe_fields = { probe_fields = {
"linux-x86_64": set(), "linux-x86_64": set(),
"kernel-at-least": {"minimum"}, "kernel-at-least": {"minimum"},
"registered-host-support": {"contract"},
"user-namespace": {"program", "sysctl_path"}, "user-namespace": {"program", "sysctl_path"},
"subordinate-ids": {"minimum_count", "uidmap_program", "gidmap_program", "unshare_program"}, "subordinate-ids": {"minimum_count", "uidmap_program", "gidmap_program", "unshare_program"},
"open-character-device": {"path", "access"}, "open-character-device": {"path", "access"},
@ -246,11 +325,15 @@ def validate_external_tools(data: dict) -> None:
for feature in features: for feature in features:
feature_id = feature["id"] feature_id = feature["id"]
probe_name = feature.get("probe") 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) validate_fields(feature, common | probe_fields[probe_name], set(), feature_id)
if probe_name == "kernel-at-least": if probe_name == "kernel-at-least":
minimum = feature["minimum"] 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") 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": elif probe_name == "user-namespace":
require(isinstance(feature["program"], str) and feature["program"], f"{feature_id}: program is empty") 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") 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(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") 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: 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("schema") == 1, "integration manifest schema must be 1")
require(data.get("levels") == list(LEVELS[1:]), "integration levels must be V2 through V5") require(data.get("levels") == list(LEVELS[1:]), "integration levels must be V2 through V5")
scenarios = data.get("scenarios") scenarios = data.get("scenarios")
require(isinstance(scenarios, list), "integration scenarios must be a list") require(isinstance(scenarios, list), "integration scenarios must be a list")
names = set() names = set()
for scenario in scenarios: for scenario in scenarios:
validate_fields(scenario, {"name", "level", "command", "capabilities"}, set(), "integration scenario")
name = scenario.get("name") name = scenario.get("name")
require(isinstance(name, str) and name and name not in names, "duplicate or invalid integration scenario") require(isinstance(name, str) and name and name not in names, "duplicate or invalid integration scenario")
names.add(name) names.add(name)
@ -278,11 +519,97 @@ def validate_integrations(data: dict, capability_ids: set[str]) -> None:
command = scenario.get("command") command = scenario.get("command")
require(isinstance(command, list) and command and all(isinstance(part, str) for part in command), f"{name}: invalid command") require(isinstance(command, list) and command and all(isinstance(part, str) for part in command), f"{name}: invalid command")
capabilities = scenario.get("capabilities", []) 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") 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")) packages = validate_packages(load_toml(SUPPORT / "work-packages.toml"))
package_ids = {package["id"] for package in packages} package_ids = {package["id"] for package in packages}
refinements = load_toml(SUPPORT / "model-refinements.toml") 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]} capability_ids = {item["id"] for kind in ("command", "feature") for item in external[kind]}
integrations = load_toml(SUPPORT / "integration-tests.toml") integrations = load_toml(SUPPORT / "integration-tests.toml")
validate_integrations(integrations, capability_ids) 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: def dependency_view(packages: list[dict]) -> str:
@ -320,10 +649,85 @@ def dependency_view(packages: list[dict]) -> str:
return "\n".join(lines) 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: def generate(check: bool) -> None:
packages, _, _ = validated_data() packages, external, integrations, process_safety = validated_data()
target = ROOT / "docs/implementation-dependencies.md" outputs = {
content = dependency_view(packages) 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: if check:
require(target.is_file(), f"generated file missing: {target.relative_to(ROOT)}") 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)}") require(target.read_text() == content, f"generated file drift: {target.relative_to(ROOT)}")
@ -335,6 +739,11 @@ def version_tuple(value: str) -> tuple[int, ...]:
return tuple(int(part) for part in value.split(".")) 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: def subordinate_id_range(path: Path, principals: set[str], minimum_count: int) -> int | None:
try: try:
rows = path.read_text().splitlines() 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" passed = platform.system() == "Linux" and platform.machine() == "x86_64"
return passed, f"{platform.system()} {platform.machine()}" return passed, f"{platform.system()} {platform.machine()}"
if probe == "kernel-at-least": if probe == "kernel-at-least":
release = platform.release().split("-", 1)[0] release = platform.release()
try: observed = kernel_version_tuple(release)
passed = platform.system() == "Linux" and version_tuple(release) >= version_tuple(feature["minimum"]) passed = platform.system() == "Linux" and observed is not None and observed >= version_tuple(feature["minimum"])
except ValueError:
passed = False
return passed, release return passed, release
if probe == "registered-host-support":
return False, "registered runner pending IP-03"
if probe == "user-namespace": if probe == "user-namespace":
try: try:
value = int(Path(feature["sysctl_path"]).read_text().strip()) 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]]: def probe(level: str, external: dict | None = None) -> tuple[set[str], set[str]]:
if external is None: if external is None:
_, external, _ = validated_data() _, external, _, _ = validated_data()
available = set() available = set()
skipped = set() skipped = set()
failed = False failed = False
@ -538,6 +947,148 @@ def dependency_audit() -> None:
print("PASS dependency source, wildcard, and license audit") 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: def refresh_advisories() -> None:
run(["cargo", "deny", "fetch"]) run(["cargo", "deny", "fetch"])
run(["cargo", "deny", "--frozen", "check", "advisories"]) 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: def verify(level: str) -> None:
_, external, integrations = validated_data() _, external, integrations, process_safety = validated_data()
generate(check=True) generate(check=True)
probe("V1", external) probe("V1", external)
run(["cargo", "fmt", "--all", "--", "--check"]) run(["cargo", "fmt", "--all", "--", "--check"])
@ -579,6 +1130,7 @@ def verify(level: str) -> None:
run(["cargo", "test", "--offline", "--workspace", "--all-features", "--locked", "--doc"]) run(["cargo", "test", "--offline", "--workspace", "--all-features", "--locked", "--doc"])
run([sys.executable, "-m", "unittest", "discover", "-s", "tests/tooling"]) run([sys.executable, "-m", "unittest", "discover", "-s", "tests/tooling"])
dependency_audit() dependency_audit()
process_source_audit(process_safety)
run_models() run_models()
target_index = LEVELS.index(level) target_index = LEVELS.index(level)
for current in LEVELS[1:target_index + 1]: for current in LEVELS[1:target_index + 1]:
@ -599,6 +1151,8 @@ def main() -> int:
subparsers.add_parser("dependencies") subparsers.add_parser("dependencies")
subparsers.add_parser("refresh-advisories") subparsers.add_parser("refresh-advisories")
subparsers.add_parser("models") 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() args = parser.parse_args()
try: try:
if args.command == "generate": if args.command == "generate":
@ -617,6 +1171,11 @@ def main() -> int:
elif args.command == "models": elif args.command == "models":
validated_data() validated_data()
run_models() 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: except (RegistryError, OSError, subprocess.CalledProcessError) as error:
print(f"ERROR: {error}", file=sys.stderr) print(f"ERROR: {error}", file=sys.stderr)
return 1 return 1