diff --git a/Makefile b/Makefile index 4ba8000..60d789d 100644 --- a/Makefile +++ b/Makefile @@ -95,9 +95,7 @@ check-compiler: "$$gate_tools/fs-sandbox" \ --dir / \ --ro /usr \ - --ro /etc \ --ro /proc \ - --ro /sys \ --ro "$$gate_tools" \ --ro "$$gate_tools/toolchain" \ --rw /dev/null \ @@ -112,6 +110,8 @@ check-compiler: CARGO_NET_OFFLINE=true \ CARGO_TARGET_DIR="$$cargo_home/target" \ CARGO_TERM_COLOR=never \ + GIT_CONFIG_GLOBAL="/dev/null" \ + GIT_CONFIG_NOSYSTEM="1" \ HOME="$$cargo_home/home" \ LANG=C \ LC_ALL=C \ @@ -167,7 +167,7 @@ check-compiler: /usr/bin/env -i LANG=C LC_ALL=C PATH=/usr/bin:/bin /usr/bin/cc -std=c17 -O2 -D_FORTIFY_SOURCE=3 -fPIE -pie -Wall -Wextra -Werror -pedantic -Wl,-z,relro,-z,now "$$cargo_home/check-src/tools/fs_sandbox.c" -o "$$gate_tools/fs-sandbox"; \ [[ -f "$$gate_tools/fs-sandbox" && ! -L "$$gate_tools/fs-sandbox" && -x "$$gate_tools/fs-sandbox" ]] || { printf '%s\n' 'filesystem sandbox build is invalid' >&2; exit 1; }; \ manifest="$$cargo_home/check-src/compiler/Cargo.toml"; \ - run_cargo /bin/bash -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ "$$(/usr/bin/sed -n "s/^$${capability_set}:[[:space:]]*//p" /proc/self/status)" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e "$$descriptor" || "$${descriptor##*/}" =~ ^[012]$$ ]] || exit 1; done; standard_output="$$(/usr/bin/readlink -- /proc/$$$$/fd/1)"; [[ "$$(/usr/bin/readlink -- /proc/$$$$/fd/0)" == /dev/null && "$$standard_output" =~ ^pipe:\[[0-9]+\]$$ && "$$(/usr/bin/readlink -- /proc/$$$$/fd/2)" == "$$standard_output" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags="$$(/usr/bin/sed -n "s/^flags:[[:space:]]*//p" "/proc/$$$$/fdinfo/$$descriptor")"; expected_access="$$((descriptor == 0 ? 0 : 1))"; [[ "$$((8#$$descriptor_flags & 3))" == "$$expected_access" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z "$$(/usr/bin/sed -n "2p" "/proc/sysvipc/$$ipc_table")" ]] || exit 1; done; ! /usr/bin/head -c 1 -- "$$3/Makefile" >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && "$$(/usr/bin/id -u)" == "$$1" && "$$(/usr/bin/id -g)" == "$$2" && "$$(/usr/bin/hostname)" == orange-gate && "$$(/usr/bin/readlink -- /proc/self/ns/ipc)" != "$$6" && "$$(/usr/bin/readlink -- /proc/self/ns/uts)" != "$$7" && "$$HOME" == "$$4" && "$$PATH" == "$$5/toolchain/bin:/usr/bin:/bin" && "$$(/usr/bin/sed -n "s/^NoNewPrivs:[[:space:]]*//p" /proc/self/status)" == 1 && -z "$$(/usr/bin/sed -n "2p" /proc/net/route)" ]]' gate-isolation "$$gate_uid" "$$gate_gid" "$$repository_root" "$$cargo_home/home" "$$gate_tools" "$$gate_ipc_namespace" "$$gate_uts_namespace"; \ + run_cargo /bin/bash --norc -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ "$$(/usr/bin/sed -n "s/^$${capability_set}:[[:space:]]*//p" /proc/self/status)" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e "$$descriptor" || "$${descriptor##*/}" =~ ^[012]$$ ]] || exit 1; done; standard_output="$$(/usr/bin/readlink -- /proc/$$$$/fd/1)"; [[ "$$(/usr/bin/readlink -- /proc/$$$$/fd/0)" == /dev/null && "$$standard_output" =~ ^pipe:\[[0-9]+\]$$ && "$$(/usr/bin/readlink -- /proc/$$$$/fd/2)" == "$$standard_output" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags="$$(/usr/bin/sed -n "s/^flags:[[:space:]]*//p" "/proc/$$$$/fdinfo/$$descriptor")"; expected_access="$$((descriptor == 0 ? 0 : 1))"; [[ "$$((8#$$descriptor_flags & 3))" == "$$expected_access" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z "$$(/usr/bin/sed -n "2p" "/proc/sysvipc/$$ipc_table")" ]] || exit 1; done; ! /usr/bin/head -c 1 -- "$$3/Makefile" >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /etc/passwd >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /sys/devices/system/cpu/online >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && "$$(/usr/bin/id -u)" == "$$1" && "$$(/usr/bin/id -g)" == "$$2" && "$$(/usr/bin/hostname)" == orange-gate && "$$(/usr/bin/readlink -- /proc/self/ns/ipc)" != "$$6" && "$$(/usr/bin/readlink -- /proc/self/ns/uts)" != "$$7" && "$$HOME" == "$$4" && "$$PATH" == "$$5/toolchain/bin:/usr/bin:/bin" && "$$(/usr/bin/sed -n "s/^NoNewPrivs:[[:space:]]*//p" /proc/self/status)" == 1 && -z "$$(/usr/bin/sed -n "2p" /proc/net/route)" ]]' gate-isolation "$$gate_uid" "$$gate_gid" "$$repository_root" "$$cargo_home/home" "$$gate_tools" "$$gate_ipc_namespace" "$$gate_uts_namespace"; \ run_cargo /usr/bin/env PYTHONHASHSEED=0 /usr/bin/python3 -S -P -B -X utf8 -W error::ResourceWarning "$$cargo_home/check-src/tools/validate_foundation.py"; \ run_cargo /usr/bin/env PYTHONHASHSEED=0 PYTHONPYCACHEPREFIX="$$cargo_home/snapshot-python-cache" /usr/bin/python3 -S -P -B -X utf8 -W error::ResourceWarning -c 'import sys, unittest; sys.path.insert(0, sys.argv.pop(1)); unittest.main(module=None)' "$$cargo_home/check-src" discover -s "$$cargo_home/check-src/tools/tests" -p 'test_*.py'; \ run_cargo /usr/bin/env PYTHONHASHSEED=0 /usr/bin/python3 -S -P -B -X utf8 -W error::ResourceWarning "$$cargo_home/check-src/tools/validate_foundation.py"; \ diff --git a/compiler/README.md b/compiler/README.md index f6d53e4..e2c118b 100644 --- a/compiler/README.md +++ b/compiler/README.md @@ -73,19 +73,23 @@ empty route table. Before the drop, the namespace supervisor bind-mounts the selected toolchain read-only and covers `/home` with a private non-executable `tmpfs`. A protected C launcher built from the captured tree requires Landlock ABI 3 or newer, permits -directory-name traversal but grants file reads and execution only to the system -roots and selected toolchain, and grants writes only to the two gate roots and -four admitted character devices. It also closes every inherited descriptor above -standard error, resets ordinary catchable signal dispositions to default, and -empties the ordinary signal mask before execution. The launcher also fixes hard +directory-name traversal but grants file reads and execution only to `/usr`, the +private `/proc`, the gate tool roots, and the selected toolchain, and grants +writes only to the two gate roots and four admitted character devices. Host +account/configuration files under `/etc` and kernel/device state under `/sys` +remain unreadable, with representative runtime assertions. The launcher also +closes every inherited descriptor above standard error, resets ordinary +catchable signal dispositions to default, and empties the ordinary signal mask +before execution. The launcher also fixes hard ceilings of 4 GiB of virtual address space and 600 CPU seconds per process, 512 MiB per file, 1,024 open files, 256 processes for the real user, and zero core-file bytes, preserving any lower inherited hard ceiling. Copied commands -receive isolated `HOME`, `TMPDIR`, and `PATH` values; a runtime assertion -confirms that the original checkout is unreadable. The namespace supervisor -kills its child if supervision is interrupted. Trusted gate operations alone -retain the descriptors used for later extraction and identity checks. The copied -validator first policy-checks +receive isolated `HOME`, `TMPDIR`, and `PATH` values, disable system Git +configuration, and bind global Git configuration to `/dev/null`; a runtime +assertion confirms that the original checkout is unreadable. The namespace +supervisor kills its child if supervision is interrupted. Trusted gate operations +alone retain the descriptors used for later extraction and identity checks. The +copied validator first policy-checks that exact exported tree before its foundation test modules import. After those tests, it policy-checks the tree again before Cargo, so Python-test drift cannot reach Rust execution. Formatting, linting, documentation, and Rust @@ -111,7 +115,8 @@ trusted outer `/usr/bin/cat` relays that merged stream to the caller's final output sink. The caller can still close or truncate that final sink, and the two copied output channels are intentionally indistinguishable. The system C compiler, launcher source, relay, kernel, mount implementation, and allowlisted -system trees remain trusted boundaries. Resource ceilings are not aggregate +system roots remain trusted boundaries. Private `/proc` still exposes kernel, +CPU, and process metadata. Resource ceilings are not aggregate cgroup budgets: virtual address space and CPU time are limited per process, file size is limited per file, aggregate resident memory is not capped, and the process ceiling includes other processes with the same real user ID. diff --git a/docs/operations/CI_DEPENDENCIES.md b/docs/operations/CI_DEPENDENCIES.md index 58b7edf..66bced7 100644 --- a/docs/operations/CI_DEPENDENCIES.md +++ b/docs/operations/CI_DEPENDENCIES.md @@ -279,7 +279,7 @@ recorded identities and digests. | Result | What is currently fixed | What the result actually establishes | Why it is not reproducible evidence yet | | --- | --- | --- | --- | -| `scripts/ci/check-repository` | Repository bytes, Cargo lock, selected Rust version, allowlisted Python and Cargo environments, fresh bytecode/build cache roots, private mount, PID, `/proc`, network, IPC, and UTS namespaces with asserted distinct identities, a fixed gate hostname, a read-only selected-toolchain bind, hidden `/home`, a protected Landlock allowlist, default ordinary catchable signal dispositions, an empty ordinary signal mask, read-only null standard input, a merged write-only copied-command output pipe with a trusted outer relay, and hard copied-command ceilings of 4 GiB of virtual address space and 600 CPU seconds per process, 512 MiB per file, 1,024 open files, 256 real-user processes, and zero core bytes; the gate uses an unprivileged user namespace where available or a privilege-dropping namespace supervisor otherwise, removes all five capability sets, closes nonstandard descriptors, and runs Cargo locked/offline | Rust formatting, lint, docs, debug and optimized unit/CLI tests, repository invariants, and synthetic/adversarial fixture expectations pass under the executing toolchain and OS without copied-code file contents outside admitted roots, trusted-parent process visibility, inherited host System V IPC objects, the host hostname, inherited blocked `SIGTERM` or ignored `SIGPIPE`, inherited nonstandard descriptors, caller-supplied standard-input bytes, direct access to caller output descriptors, unbounded single-process address-space reservation, unbounded single-process CPU time, unbounded single-file growth, Linux capabilities, privilege-reacquisition permission, or a new external network path | Rust archives, C compiler, Python, shell, Make, kernel, Landlock, namespace and privilege-drop implementations, filesystem, output relay, and host tools are not content-fixed; directory names and unmediated metadata operations remain visible, libc-reserved signal state remains platform-dependent, copied commands can alter their own signal state after execution, the caller controls and can close or truncate the relay's final output sink, resource ceilings are not aggregate cgroup budgets and do not cap aggregate resident memory, and no signed execution manifest or independently replayable environment is emitted | +| `scripts/ci/check-repository` | Repository bytes, Cargo lock, selected Rust version, allowlisted Python and Cargo environments, fresh bytecode/build cache roots, disabled system Git configuration, global Git configuration fixed to `/dev/null`, private mount, PID, `/proc`, network, IPC, and UTS namespaces with asserted distinct identities, a fixed gate hostname, a read-only selected-toolchain bind, hidden `/home`, a protected Landlock allowlist that omits `/etc` and `/sys`, default ordinary catchable signal dispositions, an empty ordinary signal mask, read-only null standard input, a merged write-only copied-command output pipe with a trusted outer relay, and hard copied-command ceilings of 4 GiB of virtual address space and 600 CPU seconds per process, 512 MiB per file, 1,024 open files, 256 real-user processes, and zero core bytes; the gate uses an unprivileged user namespace where available or a privilege-dropping namespace supervisor otherwise, removes all five capability sets, closes nonstandard descriptors, and runs Cargo locked/offline | Rust formatting, lint, docs, debug and optimized unit/CLI tests, repository invariants, and synthetic/adversarial fixture expectations pass under the executing toolchain and OS without copied-code file contents outside admitted roots, host account/configuration file contents, sysfs kernel/device contents, caller-selected system/global Git configuration, trusted-parent process visibility, inherited host System V IPC objects, the host hostname, inherited blocked `SIGTERM` or ignored `SIGPIPE`, inherited nonstandard descriptors, caller-supplied standard-input bytes, direct access to caller output descriptors, unbounded single-process address-space reservation, unbounded single-process CPU time, unbounded single-file growth, Linux capabilities, privilege-reacquisition permission, or a new external network path | Rust archives, C compiler, Python, shell, Make, kernel, Landlock, namespace and privilege-drop implementations, filesystem, output relay, and host tools are not content-fixed; directory names, private-`/proc` kernel/CPU/process metadata, and unmediated metadata operations remain visible, libc-reserved signal state remains platform-dependent, copied commands can alter their own signal state after execution, the caller controls and can close or truncate the relay's final output sink, resource ceilings are not aggregate cgroup budgets and do not cap aggregate resident memory, and no signed execution manifest or independently replayable environment is emitted | | Required hosted CI | Repository revision, direct Action SHAs, actionlint archive digest, and zizmor image digest | The configured repository, Markdown, workflow, and workflow-security methods passed in one hosted run | Runner image, Node, Git, Docker host, Action bundles, and service behavior are not fully archived or fixed | | Dependency Review | Event base/head identities and the selected Action revision | GitHub reported no configured dependency-policy violation for its then-current dependency data | Dependency graph/API state and service implementation are external, time-indexed inputs | | External Links | lychee archive digest, link-check flags, and repository locators | The non-excluded endpoints produced accepted responses during the run | Remote content, DNS, TLS, routing, rate limits, and server policy change independently of repository bytes | diff --git a/docs/security/THREAT_MODEL.md b/docs/security/THREAT_MODEL.md index 96b859e..8ce995d 100644 --- a/docs/security/THREAT_MODEL.md +++ b/docs/security/THREAT_MODEL.md @@ -409,7 +409,7 @@ required control. Reviews must replace conditional ranks with deployment facts. | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | TM-001 | ADV-002/004/008; TB-002/003; AS-001/002/003/011 | Compromise or unilateral authority changes or erases authoritative decisions, settings, or history. | CTL-001, CTL-007, CTL-008; ruleset `18810248` constrains ordinary Git updates, and public history aids detection. | Preserve the active no-bypass ruleset and exact required checks; test safe direct-update rejection; maintain recovery material and periodic owner access review; never claim independent governance. | Medium-high: one administrator and zero required approvals, despite protected Git updates | High | High | Owner or platform compromise can weaken settings or abuse admin recovery; zero approvals do not create independent review. | Project owner; each rules/access change and quarterly; `open-current` | | TM-002 | ADV-001/002; TB-001/004; AS-003/004 | Attacker-controlled workflow, script, or metadata reaches a privileged runner and steals credentials or changes state. | CTL-005/006/019; current PR jobs declare read-only source access, no configured repository or environment secrets, only a job-scoped read-only `GITHUB_TOKEN`, timeouts, and no `pull_request_target`. Ruleset `18810248` requires the exact app-bound Required CI and Dependency Review contexts. PR #11 Required CI `29292600483`, Dependency Review `29292600471`, and CodeQL `29292598799` succeeded; post-merge Required CI `29292740885`, Workflow Online Audit `29292740874`, External Links `29292740884`, Scorecard `29292740941`, and dynamic CodeQL `29292740478` succeeded at latest recorded subject revision `23352bcde976b86890db28ea4d375a31e6354bca`. | Policy-check every workflow diff; keep privileged jobs off PR events; validate metadata before shell use; require CI and security review on workflow paths. | Low now: demonstrated PR jobs have read-only permissions; reassess every permission or event change | High | Medium | Actions/runner isolation, sole-owner review, query coverage, open Scorecard posture alerts, and future permission edits remain trusted or residual risks. Green runs do not prove vulnerability absence. | CI/Release authority, currently Bootstrap Steward; every workflow change; `open-current` | -| TM-003 | ADV-003; TB-005/012/013; AS-004/009/013 | Compromised action, tool, image, Rust toolchain, registry, archive, or dependency falsifies results or executes malicious code. | CTL-004/010/020; current full-SHA settings, pinned tool identities, no third-party Rust crates, exact required PR contexts, green exact-revision hosted evidence, and a local hardened gate that discards the caller environment, restricts tool resolution, validates the captured tree before copied test modules run, revalidates it after them before Rust executes and after Rust checks, and compares NUL-safe non-directory membership plus every tracked file's type, mode, and bytes in all three compiler input roots with a fresh extraction of the identity-checked capture. After initial consistency checks, the authoritative archive and inventory are unlinked behind read-only descriptors that copied Python and Rust processes do not inherit. Every copied command runs with private mount, PID, `/proc`, network, IPC, and UTS namespaces; the latter two remove inherited System V IPC objects and replace the host name with a fixed gate identity. The namespace supervisor bind-mounts the selected toolchain read-only, hides `/home`, drops to the invoking identity with all five capability sets empty and `no_new_privs`, then invokes a protected Landlock launcher that limits file contents to admitted roots, resets ordinary catchable signal dispositions and the ordinary signal mask, closes all nonstandard descriptors, and fixes hard ceilings for core files, per-process address space and CPU time, per-file growth, open files, and real-user processes. Copied standard input is read-only `/dev/null`, while copied standard output and error share one write-only anonymous pipe relayed by trusted outer `/usr/bin/cat`. | Preserve the minimum allowlist and exact admission records; archive dependencies; verify signatures where available; use locked offline checks and separately provisioned owner rebuilds. | Medium: admitted third-party CI and the Rust toolchain execute code | High | High | Environment, namespace, Landlock, signal-state reset, descriptor-confined capture, standard-stream confinement, resource ceilings, and final exact comparison prevent caller-selected wrappers, inherited blocked `SIGTERM` or ignored `SIGPIPE`, copied-code parent-descriptor traversal, inherited host System V IPC access, host-name observation, caller-supplied standard-input bytes, direct access to caller output descriptors, new copied-code network connections, unbounded single-process address-space reservation, unbounded single-process CPU time, unbounded single-file growth, ordinary named-file capture replacement, live-tree drift, added source entries, and unrestored Python-, Rust-test-, or reproducibility-build source drift from silently replacing a passing check. Directory names, unmediated metadata operations, other host identity and IPC mechanisms, libc-reserved signal state, copied-code changes to its own signal state, the caller-controlled final relay sink, aggregate process-tree CPU/storage/resident-memory use, and same-user process accounting remain visible or residual; empty-directory additions are immaterial; the caller can close or truncate the final sink; and an admitted child can restore modified extracted state before comparison. The C compiler, protected launcher, output relay, kernel enforcement, passwordless namespace policy, selected account-owned proxy, `cat`, `hostname`, `mount`, `sudo`, `unshare`, `setpriv`, other system tools, copied validator/tests, archive implementation, publisher, registry, runner, host, and solo-admission judgment remain trusted or residual risks. | Project owner; each admission/update and surveillance run; `open-current` | +| TM-003 | ADV-003; TB-005/012/013; AS-004/009/013 | Compromised action, tool, image, Rust toolchain, registry, archive, or dependency falsifies results or executes malicious code. | CTL-004/010/020; current full-SHA settings, pinned tool identities, no third-party Rust crates, exact required PR contexts, green exact-revision hosted evidence, and a local hardened gate that discards the caller environment, restricts tool resolution, disables system Git configuration, binds global Git configuration to `/dev/null`, validates the captured tree before copied test modules run, revalidates it after them before Rust executes and after Rust checks, and compares NUL-safe non-directory membership plus every tracked file's type, mode, and bytes in all three compiler input roots with a fresh extraction of the identity-checked capture. After initial consistency checks, the authoritative archive and inventory are unlinked behind read-only descriptors that copied Python and Rust processes do not inherit. Every copied command runs with private mount, PID, `/proc`, network, IPC, and UTS namespaces; the latter two remove inherited System V IPC objects and replace the host name with a fixed gate identity. The namespace supervisor bind-mounts the selected toolchain read-only, hides `/home`, drops to the invoking identity with all five capability sets empty and `no_new_privs`, then invokes a protected Landlock launcher that limits file contents to admitted roots, excludes `/etc` host account/configuration files and `/sys` kernel/device state, resets ordinary catchable signal dispositions and the ordinary signal mask, closes all nonstandard descriptors, and fixes hard ceilings for core files, per-process address space and CPU time, per-file growth, open files, and real-user processes. Copied standard input is read-only `/dev/null`, while copied standard output and error share one write-only anonymous pipe relayed by trusted outer `/usr/bin/cat`. | Preserve the minimum allowlist and exact admission records; archive dependencies; verify signatures where available; use locked offline checks and separately provisioned owner rebuilds. | Medium: admitted third-party CI and the Rust toolchain execute code | High | High | Environment, namespace, Landlock, signal-state reset, descriptor-confined capture, standard-stream confinement, resource ceilings, and final exact comparison prevent caller-selected wrappers and system/global Git configuration, inherited blocked `SIGTERM` or ignored `SIGPIPE`, copied-code parent-descriptor traversal, inherited host System V IPC access, host-name observation, `/etc` and `/sys` file-content reads, caller-supplied standard-input bytes, direct access to caller output descriptors, new copied-code network connections, unbounded single-process address-space reservation, unbounded single-process CPU time, unbounded single-file growth, ordinary named-file capture replacement, live-tree drift, added source entries, and unrestored Python-, Rust-test-, or reproducibility-build source drift from silently replacing a passing check. Directory names, private-`/proc` kernel/CPU/process metadata, unmediated metadata operations, other host identity and IPC mechanisms, libc-reserved signal state, copied-code changes to its own signal state, the caller-controlled final relay sink, aggregate process-tree CPU/storage/resident-memory use, and same-user process accounting remain visible or residual; empty-directory additions are immaterial; the caller can close or truncate the final sink; and an admitted child can restore modified extracted state before comparison. The C compiler, protected launcher, output relay, kernel enforcement, passwordless namespace policy, selected account-owned proxy, `cat`, `hostname`, `mount`, `sudo`, `unshare`, `setpriv`, other system tools, copied validator/tests, archive implementation, publisher, registry, runner, host, and solo-admission judgment remain trusted or residual risks. | Project owner; each admission/update and surveillance run; `open-current` | | TM-004 | ADV-001/002/008; TB-001/002/004/005; AS-003/004/005/010 | Secret enters source, artifact, log, cache, or untrusted job and is used before revocation. | CTL-003/005/006/017; secret scanning and push protection enabled, minimal workflow permissions, no product keys, and a fail-closed lifecycle/playbook is documented. | Exercise synthetic leak/revocation paths; enable broader scanning if available; keep release/root keys out of GitHub; rotate immediately and treat history deletion as insufficient. | Medium: humans and tooling can leak unsupported patterns | High | High | Detection is not prevention for every secret; account custody and incident execution are not independently verified. | Security authority, currently Bootstrap Steward; every alert/credential event and quarterly; `open-current` | | TM-005 | ADV-004/005; TB-008/009/010/012; AS-006/008/009/011 | Valid evidence is rebound, omitted, downgraded, or confused across source, target, artifact, or claim context. | CTL-009/012 specify explicit subjects, digests, contexts, assumptions, and fail-closed outcomes. | Ratify canonical schemas; bind complete claim closure to exact bytes and versions; check bundle traversal through separate owner-executable paths; add substitution, omission, downgrade, and cross-target negative tests. | Future | High | High | Schema validity cannot prove truthful binding; human standards intent remains an assumption. | Project owner; every schema/claim change and release; `future-blocking` | | TM-006 | ADV-005/007; TB-008/009; AS-006 | Malformed or adversarial proofs exploit unsoundness, parser differential, resource exhaustion, or hidden axioms. | CTL-009/013 target a small deterministic checker, axiom ledger, resource bounds, implementation diversity, and adversarial corpus. | Prove the checked relation sound; fuzz and mutate accepted objects; test malformed/cyclic/oversized inputs; enforce canonical decoding and budgets; record external audit as unavailable. | Future | High | Critical | A shared solo-authored semantic error can survive multiple implementations; the missing independent audit remains explicit. | Project owner; every checker/format/axiom change and release; `future-stop-ship` | diff --git a/policy/README.md b/policy/README.md index adc2e81..2558a7f 100644 --- a/policy/README.md +++ b/policy/README.md @@ -74,15 +74,20 @@ non-executable `tmpfs`. The protected `tools/fs_sandbox.c` launcher is built from the checked capture with fixed hardening flags. It requires Landlock ABI 3 or newer, grants file reads and execution only to the admitted system/toolchain roots, grants writes only to the two temporary gate roots and admitted character -devices, closes every inherited descriptor above standard error, resets ordinary -catchable signal dispositions to default, and empties the ordinary signal mask. +devices, and does not admit host files under `/etc` or `/sys`. Representative +runtime assertions require `/etc/passwd` and `/sys` CPU state to remain +unreadable. The launcher closes every inherited descriptor above standard error, +resets ordinary catchable signal dispositions to default, and empties the +ordinary signal mask. Before execution it also fixes hard ceilings of 4 GiB of virtual address space and 600 CPU seconds per process, 512 MiB per file, 1,024 open files, 256 processes for the real user, and zero core-file bytes, preserving any lower inherited hard ceiling. Copied commands receive isolated `HOME`, `TMPDIR`, and `PATH` values and -must prove the original checkout unreadable before tests begin. The namespace -supervisor kills its child if supervision is interrupted. Trusted gate operations -retain the descriptors for extraction and SHA-256 identity checks. +disable system Git configuration while binding global Git configuration to +`/dev/null`; they must prove the original checkout unreadable before tests begin. +The namespace supervisor kills its child if supervision is interrupted. Trusted +gate operations retain the descriptors for extraction and SHA-256 identity +checks. The gate verifies those identities before and after its final comparison, extracts a fresh reference, compares NUL-safe sorted non-directory membership across the tested check root and both relocated reproducibility roots, and @@ -100,7 +105,8 @@ channels are intentionally merged. The fixed host C compiler, relay, `mount`, `unshare`, `sudo`, and `setpriv` implementations, protected launcher, passwordless namespace-setup policy, user-namespace availability, kernel namespace/Landlock enforcement, and unrelated same-account processes outside -the private namespace remain trusted or residual boundaries. Resource ceilings +the private namespace remain trusted or residual boundaries. The admitted +private `/proc` still exposes kernel, CPU, and process metadata. Resource ceilings are not aggregate cgroup budgets: virtual address space and CPU time are limited per process, file size is limited per file, aggregate resident memory is not capped, and the process ceiling includes other processes with the same real user diff --git a/policy/gate0-repository-policy.json b/policy/gate0-repository-policy.json index b6aaa9a..d11d0df 100644 --- a/policy/gate0-repository-policy.json +++ b/policy/gate0-repository-policy.json @@ -1,5 +1,5 @@ { - "policy_version": "0.2.91", + "policy_version": "0.2.94", "repository": "chasebryan/orange", "stage": "solo-bootstrap", "status": "enforced", @@ -252,7 +252,7 @@ "CONTRIBUTING.md": "ee6a23e1c2bca6f86f6a40e2511c4de4c253a77ac2b24d3ae3d975416055b86f", "DEPENDENCY_POLICY.md": "ae5e10534b9081c401d943a55fc85fb2aa4a284cc366129f6139eefdb8389438", "GOVERNANCE.md": "8cbf5da50c63908948d181b1525c86e0f8a554eaa71fc98cf2f0ec47f6776103", - "Makefile": "6e2ecb4ab37dea9225e834a6912bb83b937c23edb755fd7cfdba506eabd89451", + "Makefile": "72178308af81fe3261a6991ecf448cee32ac5e1de13d4493b0324c069243deb8", "README.md": "82767e5ee4eebabcdaab249a95171d0feae55664d4868c00ca12f103f9382182", "RELEASE_POLICY.md": "f8a3f0fa3494eb28bdd9fc3e6d18ddc8df2fdf63a4c628a5f6c9d72762586e45", "SECURITY.md": "1a801158996153650a2d94a4dbf5043d0a08ce9b96e4aefa9abdcd66344a0ede", @@ -291,13 +291,13 @@ "docs/DECISIONS.md": "5ba13656b29a404aa7ffc047fe1a02df9a60bf43d440912557889bffb5493047", "docs/LANGUAGE_2026.md": "28bcb8741e67adad12c92fa3e0ad8d4b759cf6625333eea5af6dd5a663c014bd", "docs/SEMANTICS_2026.md": "a465aca6c98344fb1271944ce9cdd5af3575afcf295442dfbbd6e05a95182b33", - "docs/operations/CI_DEPENDENCIES.md": "6636a3fe7852dceba96753f8ed33b93ff859bd37cef08e80a949a29705669326", + "docs/operations/CI_DEPENDENCIES.md": "a02be043a15ad8890307e84bc1e4e26edaff8cfb3a83208c7d76368784928139", "docs/operations/GITHUB_CONTROLS.md": "f86bdf0234e9db17256f4be07e20e65a9913de45e96e1fdd55e2c57d056ae94b", "docs/security/OSPS_BASELINE.md": "38efd43d1e4e15f335c9189c7cf921b58eb9b15ff8305acb75c7a47ff9fd2d72", "docs/security/SECRETS_AND_INCIDENTS.md": "93332edb737f84c7a3f74f256b5fb603537bf6f524388f62013140cb9906f6a6", - "docs/security/THREAT_MODEL.md": "6456abb6e10dee81e58cfcd166cecfd0b9ed3ed6899fd915f2a39f6d08eec9c5", - "policy/README.md": "239983ca837f8fbc3fca86b3fcd2c30e1e038cd3c1a9973b68cf3d76d2ddfcd4", - "policy/makefile-entrypoint-contract-v0.1.json": "64beb23f093f8d055c8862f8633a535adcab1c33d95ee65d569c35bb0732e490", + "docs/security/THREAT_MODEL.md": "640c0e56a8e7c44300059655df1702c4897ecf84d64ee25d1e384119b7d0967c", + "policy/README.md": "d68f85d2acb72939188574f2c602e1444f3760aca74c2cb095c42684a2e594bb", + "policy/makefile-entrypoint-contract-v0.1.json": "2295c5b7c5fd59f0080d600961c580344a8bd2bef751df9ff9c51e3eee5d009e", "schemas/README.md": "39a7b91e15a316c1221cfce5082608eb453f20ea58b5e1c5a0cf32a07a81d774", "schemas/gate0/claim-record-v0.1.schema.json": "a287dde9ddf114da30af61d050aa96406f23e480d62e0f796d66943489579131", "schemas/gate0/evidence-manifest-v0.1.schema.json": "987ba1cddb23aaaf67a1234456fbffde8f80d45678b9671b8df97ad256742efd", @@ -309,8 +309,8 @@ "scripts/ci/install-actionlint": "ac2e08280a32b7d213fb3993f43e5f9d9431dc3f960a41dedea551be2d9433b5", "scripts/ci/install-lychee": "21dd0050173bdd6b461ce7893dc4f77f74dde67b051d76697d6e8ec466b6c41f", "tools/fs_sandbox.c": "80d94ad673b9d3edf628d98e8e7659da93030a56918cc9c2193ede1fdfb76b66", - "tools/tests/test_validate_foundation.py": "428996f350509478ba525b5c63b55fa1a1a52c6d9d65f4cba4d354ad76bfacb0", - "tools/tests/test_validate_foundation_hardening.py": "b49516385f3d7326573256cf6d3a7424dc7bf0ac94ae1f8da89a174ec7aca742" + "tools/tests/test_validate_foundation.py": "7a5d34d64733d76e24dc2f188413e5b24af0fd94ff1a1afeb0e2581656da5e92", + "tools/tests/test_validate_foundation_hardening.py": "ec4d15925bc0c50bf45dca007e20192bc0c00b18c81486a79d0d3c32dbe94353" }, "executable_paths": [ "scripts/ci/check-external-links", diff --git a/policy/makefile-entrypoint-contract-v0.1.json b/policy/makefile-entrypoint-contract-v0.1.json index dce45ad..b8ae11d 100644 --- a/policy/makefile-entrypoint-contract-v0.1.json +++ b/policy/makefile-entrypoint-contract-v0.1.json @@ -33,7 +33,7 @@ "/usr/bin/rm -- \"$$repro_source_paths_after\"; \\\n\tverify_capture_identity", "/usr/bin/env -i LANG=C LC_ALL=C PATH=/usr/bin:/bin /usr/bin/cc -std=c17 -O2 -D_FORTIFY_SOURCE=3 -fPIE -pie -Wall -Wextra -Werror -pedantic -Wl,-z,relro,-z,now \"$$cargo_home/check-src/tools/fs_sandbox.c\" -o \"$$gate_tools/fs-sandbox\"", "manifest=\"$$cargo_home/check-src/compiler/Cargo.toml\"", - "run_cargo /bin/bash -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", + "run_cargo /bin/bash --norc -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /etc/passwd >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /sys/devices/system/cpu/online >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", "PYTHONPYCACHEPREFIX=\"$$cargo_home/snapshot-python-cache\"", "run_cargo cargo fmt --manifest-path \"$$manifest\" --all -- --check", "run_cargo cargo clippy --manifest-path \"$$manifest\" --workspace --lib --bins --locked --offline", @@ -79,10 +79,12 @@ "\"$${namespace_runner[@]}\" /bin/true; \\\n\tfi;", "exec 8<&- 9<&-;", "cd -- /;", - "\"$${namespace_runner[@]}\" \\\n\t\t\t\t\"$$gate_tools/fs-sandbox\" \\\n\t\t\t\t--dir / \\\n\t\t\t\t--ro /usr \\\n\t\t\t\t--ro /etc \\\n\t\t\t\t--ro /proc \\\n\t\t\t\t--ro /sys \\\n\t\t\t\t--ro \"$$gate_tools\" \\\n\t\t\t\t--ro \"$$gate_tools/toolchain\" \\\n\t\t\t\t--rw /dev/null \\\n\t\t\t\t--rw /dev/zero \\\n\t\t\t\t--rw /dev/random \\\n\t\t\t\t--rw /dev/urandom \\\n\t\t\t\t--rw \"$$cargo_home\" \\\n\t\t\t\t--rw \"$$repro_home_b\" \\\n\t\t\t\t-- \\\n\t\t\t\t/usr/bin/env -i", + "\"$${namespace_runner[@]}\" \\\n\t\t\t\t\"$$gate_tools/fs-sandbox\" \\\n\t\t\t\t--dir / \\\n\t\t\t\t--ro /usr \\\n\t\t\t\t--ro /proc \\\n\t\t\t\t--ro \"$$gate_tools\" \\\n\t\t\t\t--ro \"$$gate_tools/toolchain\" \\\n\t\t\t\t--rw /dev/null \\\n\t\t\t\t--rw /dev/zero \\\n\t\t\t\t--rw /dev/random \\\n\t\t\t\t--rw /dev/urandom \\\n\t\t\t\t--rw \"$$cargo_home\" \\\n\t\t\t\t--rw \"$$repro_home_b\" \\\n\t\t\t\t-- \\\n\t\t\t\t/usr/bin/env -i", "CARGO_HOME=\"$$cargo_home\"", "CARGO_NET_OFFLINE=true", "CARGO_TARGET_DIR=\"$$cargo_home/target\"", + "GIT_CONFIG_GLOBAL=\"/dev/null\"", + "GIT_CONFIG_NOSYSTEM=\"1\"", "HOME=\"$$cargo_home/home\"", "PATH=\"$$gate_tools/toolchain/bin:/usr/bin:/bin\"", "RUSTUP_TOOLCHAIN=1.96.1 \\\n\t\t\t\tSOURCE_DATE_EPOCH=0", @@ -103,7 +105,7 @@ "repro_source_paths=/proc/self/fd/8", "/usr/bin/env -i LANG=C LC_ALL=C PATH=/usr/bin:/bin /usr/bin/cc -std=c17 -O2 -D_FORTIFY_SOURCE=3 -fPIE -pie -Wall -Wextra -Werror -pedantic -Wl,-z,relro,-z,now \"$$cargo_home/check-src/tools/fs_sandbox.c\" -o \"$$gate_tools/fs-sandbox\"", "[[ -f \"$$gate_tools/fs-sandbox\" && ! -L \"$$gate_tools/fs-sandbox\" && -x \"$$gate_tools/fs-sandbox\" ]]", - "run_cargo /bin/bash -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", + "run_cargo /bin/bash --norc -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /etc/passwd >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /sys/devices/system/cpu/online >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", "--create --file=\"$$capture_archive_path\"", "--format=gnu --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner --mode='u+rwX,go+rX,go-w,u-s,g-s,o-t'", "/usr/bin/env -i PATH=/usr/bin:/bin GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 /usr/bin/git -C \"$$repository_root\" ls-files --cached -z > \"$$capture_paths_path\"", diff --git a/tools/tests/test_validate_foundation.py b/tools/tests/test_validate_foundation.py index 57f1608..d7911f5 100644 --- a/tools/tests/test_validate_foundation.py +++ b/tools/tests/test_validate_foundation.py @@ -50,6 +50,21 @@ def protected_file_policy() -> dict[str, object]: return {"protected_file_digests": load_json(source)["protected_file_digests"]} +GIT_FIXTURE_ENVIRONMENT = { + "GIT_CONFIG_GLOBAL": os.devnull, + "GIT_CONFIG_NOSYSTEM": "1", + "PATH": "/usr/bin:/bin", +} + + +def git_fixture_environment() -> dict[str, str]: + environment = { + key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") + } + environment.update(GIT_FIXTURE_ENVIRONMENT) + return environment + + def _markdown_table_fields(line: str) -> list[str]: source = line.strip() separators: list[int] = [] @@ -576,9 +591,7 @@ def test_final_snapshot_rejects_late_git_index_drift(self) -> None: ) def test_final_snapshot_rejects_actual_staged_content_drift(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1056,7 +1069,7 @@ def test_git_inventory_rejects_external_gitdir_indirection(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--bare", "--quiet", external_git], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) (root / ".git").write_text( f"gitdir: {external_git}\n", @@ -1079,7 +1092,7 @@ def test_git_inventory_rejects_local_metadata_redirects(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) redirect = root / ".git" / relative_path redirect.parent.mkdir(parents=True, exist_ok=True) @@ -1100,7 +1113,7 @@ def test_git_inventory_accepts_empty_checkout_worktree_config(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) (root / ".git/config.worktree").touch() findings = [] @@ -1116,19 +1129,19 @@ def test_git_inventory_accepts_checkout_worktree_config(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) subprocess.run( [GATE0_GIT_EXECUTABLE, "sparse-checkout", "disable"], cwd=root, check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) subprocess.run( [GATE0_GIT_EXECUTABLE, "config", "--local", "--unset-all", "extensions.worktreeConfig"], cwd=root, check=False, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) findings = [] @@ -1147,7 +1160,7 @@ def test_git_inventory_rejects_linked_empty_worktree_config(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) target = parent / "empty-config" target.touch() @@ -1174,7 +1187,7 @@ def test_git_inventory_rejects_a_symlinked_object_directory(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) objects = root / ".git" / "objects" external = parent / "external-objects" @@ -1195,7 +1208,7 @@ def test_git_inventory_rejects_symlinked_config_and_index(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) external = parent / "external-metadata" external.write_text("external\n", encoding="utf-8") @@ -1221,13 +1234,13 @@ def test_git_inventory_rejects_hardlinked_config_and_index(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) (root / "tracked.txt").write_text("tracked\n", encoding="utf-8") subprocess.run( [GATE0_GIT_EXECUTABLE, "-C", root, "add", "tracked.txt"], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) redirected = root / ".git" / relative_path external = parent / f"external-{relative_path}" @@ -1250,18 +1263,18 @@ def test_git_inventory_rejects_split_indexes(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) (root / "tracked.txt").write_text("tracked\n", encoding="utf-8") subprocess.run( [GATE0_GIT_EXECUTABLE, "-C", root, "add", "tracked.txt"], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) subprocess.run( [GATE0_GIT_EXECUTABLE, "-C", root, "update-index", "--split-index"], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) findings = [] @@ -1277,7 +1290,7 @@ def test_git_inventory_rejects_external_config_includes(self) -> None: subprocess.run( [GATE0_GIT_EXECUTABLE, "init", "--quiet", root], check=True, - env={"PATH": "/usr/bin:/bin"}, + env=GIT_FIXTURE_ENVIRONMENT, ) external = parent / "external.config" external.write_text("[core]\n\tignoreCase = true\n", encoding="utf-8") @@ -1328,9 +1341,7 @@ def test_git_timeout_cleanup_never_uses_an_unbounded_wait(self) -> None: self.assertTrue(all(timeout is not None for timeout in process.wait_timeouts)) def test_global_git_excludes_cannot_hide_repository_files(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: parent = Path(directory) root = parent / "repository" @@ -1367,9 +1378,7 @@ def test_global_git_excludes_cannot_hide_repository_files(self) -> None: self.assertEqual(findings, []) def test_untracked_nested_gitignore_cannot_hide_repository_files(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1402,9 +1411,7 @@ def test_untracked_nested_gitignore_cannot_hide_repository_files(self) -> None: self.assertEqual(findings, []) def test_local_core_worktree_cannot_hide_checkout_files(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: parent = Path(directory) root = parent / "repository" @@ -1441,9 +1448,7 @@ def test_local_core_worktree_cannot_hide_checkout_files(self) -> None: self.assertEqual(findings, []) def test_local_ignore_case_cannot_hide_case_collisions(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1528,9 +1533,7 @@ def test_git_failure_is_fatal_when_repository_metadata_is_present(self) -> None: self.assertEqual({finding.code for finding in findings}, {"resource.inventory_git"}) def test_alternate_index_environment_cannot_redirect_either_git_inventory(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: parent = Path(directory) root = parent / "repository" @@ -1565,9 +1568,7 @@ def test_alternate_index_environment_cannot_redirect_either_git_inventory(self) self.assertEqual(findings, []) def test_stage_inventory_allows_modified_worktree_bytes(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1601,9 +1602,7 @@ def test_stage_inventory_allows_modified_worktree_bytes(self) -> None: self.assertEqual(validator.findings, []) def test_stage_inventory_rejects_a_missing_index_object(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1637,9 +1636,7 @@ def test_stage_inventory_rejects_a_missing_index_object(self) -> None: self.assertEqual({finding.code for finding in findings}, {"resource.inventory_stage"}) def test_stage_inventory_rejects_a_replaced_object_with_the_wrong_type(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1747,9 +1744,7 @@ def test_git_file_inventory_rejects_untracked_admitted_paths(self) -> None: ) def test_git_inventory_rejects_hidden_index_state(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1794,9 +1789,7 @@ def test_git_inventory_rejects_hidden_index_state(self) -> None: self.assertEqual({finding.code for finding in validator.findings}, {"git.index_flags"}) def test_intent_to_add_entries_are_rejected_including_empty_files(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( @@ -1823,9 +1816,7 @@ def test_intent_to_add_entries_are_rejected_including_empty_files(self) -> None: ) def test_modified_staged_empty_file_is_not_intent_to_add(self) -> None: - clean_environment = { - key: value for key, value in os.environ.items() if not key.upper().startswith("GIT_") - } + clean_environment = git_fixture_environment() with tempfile.TemporaryDirectory() as directory: root = Path(directory) subprocess.run( diff --git a/tools/tests/test_validate_foundation_hardening.py b/tools/tests/test_validate_foundation_hardening.py index 834a76b..f23df2d 100644 --- a/tools/tests/test_validate_foundation_hardening.py +++ b/tools/tests/test_validate_foundation_hardening.py @@ -3229,6 +3229,16 @@ def test_make_check_must_be_policy_first_serialized_and_environment_isolated(sel 'HOME="$$HOME"', "make.compiler_environment_contract", ), + ( + 'GIT_CONFIG_GLOBAL="/dev/null"', + "GIT_CONFIG_GLOBAL=$$HOME/.gitconfig", + "make.compiler_environment_contract", + ), + ( + 'GIT_CONFIG_NOSYSTEM="1"', + "GIT_CONFIG_NOSYSTEM=0", + "make.compiler_environment_contract", + ), ( 'PATH="$$gate_tools/toolchain/bin:/usr/bin:/bin"', 'PATH="$$PATH"', @@ -3261,7 +3271,7 @@ def test_make_check_must_be_policy_first_serialized_and_environment_isolated(sel "make.compiler_environment_contract", ), ( - "run_cargo /bin/bash -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", + "run_cargo /bin/bash --norc -p -c 'for capability_set in CapInh CapPrm CapEff CapBnd CapAmb; do [[ \"$$(/usr/bin/sed -n \"s/^$${capability_set}:[[:space:]]*//p\" /proc/self/status)\" == 0000000000000000 ]] || exit 1; done; for descriptor in /proc/self/fd/*; do [[ ! -e \"$$descriptor\" || \"$${descriptor##*/}\" =~ ^[012]$$ ]] || exit 1; done; standard_output=\"$$(/usr/bin/readlink -- /proc/$$$$/fd/1)\"; [[ \"$$(/usr/bin/readlink -- /proc/$$$$/fd/0)\" == /dev/null && \"$$standard_output\" =~ ^pipe:\\[[0-9]+\\]$$ && \"$$(/usr/bin/readlink -- /proc/$$$$/fd/2)\" == \"$$standard_output\" ]] || exit 1; for descriptor in 0 1 2; do descriptor_flags=\"$$(/usr/bin/sed -n \"s/^flags:[[:space:]]*//p\" \"/proc/$$$$/fdinfo/$$descriptor\")\"; expected_access=\"$$((descriptor == 0 ? 0 : 1))\"; [[ \"$$((8#$$descriptor_flags & 3))\" == \"$$expected_access\" ]] || exit 1; done; for ipc_table in msg sem shm; do [[ -z \"$$(/usr/bin/sed -n \"2p\" \"/proc/sysvipc/$$ipc_table\")\" ]] || exit 1; done; ! /usr/bin/head -c 1 -- \"$$3/Makefile\" >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /etc/passwd >/dev/null 2>&1 || exit 1; ! /usr/bin/head -c 1 -- /sys/devices/system/cpu/online >/dev/null 2>&1 || exit 1; [[ $$$$ == 1 && $$PPID == 0 && \"$$(/usr/bin/id -u)\" == \"$$1\" && \"$$(/usr/bin/id -g)\" == \"$$2\" && \"$$(/usr/bin/hostname)\" == orange-gate && \"$$(/usr/bin/readlink -- /proc/self/ns/ipc)\" != \"$$6\" && \"$$(/usr/bin/readlink -- /proc/self/ns/uts)\" != \"$$7\" && \"$$HOME\" == \"$$4\" && \"$$PATH\" == \"$$5/toolchain/bin:/usr/bin:/bin\" && \"$$(/usr/bin/sed -n \"s/^NoNewPrivs:[[:space:]]*//p\" /proc/self/status)\" == 1 && -z \"$$(/usr/bin/sed -n \"2p\" /proc/net/route)\" ]]' gate-isolation \"$$gate_uid\" \"$$gate_gid\" \"$$repository_root\" \"$$cargo_home/home\" \"$$gate_tools\" \"$$gate_ipc_namespace\" \"$$gate_uts_namespace\"", "/usr/bin/true", "make.compiler_environment_contract", ), @@ -3551,8 +3561,6 @@ def test_filesystem_sandbox_denies_unadmitted_files_closes_descriptors_and_caps_ "--ro", "/usr", "--ro", - "/etc", - "--ro", "/proc", "--rw", "/dev/null", @@ -3565,6 +3573,7 @@ def test_filesystem_sandbox_denies_unadmitted_files_closes_descriptors_and_caps_ sandbox_command + [ "/bin/bash", + "--norc", "-p", "-c", ( @@ -3572,6 +3581,8 @@ def test_filesystem_sandbox_denies_unadmitted_files_closes_descriptors_and_caps_ "printf sandboxed > output; " "! cat ../denied/secret >/dev/null 2>&1; " "! cat escape >/dev/null 2>&1; " + "! cat /etc/passwd >/dev/null 2>&1; " + "! cat /sys/devices/system/cpu/online >/dev/null 2>&1; " "! printf escaped > ../denied/output 2>/dev/null; " '[[ ! -e "/proc/self/fd/$1" ]]; ' "/usr/bin/python3 -S -P -B -c 'import mmap, resource; " @@ -3613,6 +3624,7 @@ def poison_signal_state() -> None: sandbox_command + [ "/bin/bash", + "--norc", "-p", "-c", f"kill -{signal_name} $$; exit 99", diff --git a/tools/validate_foundation.py b/tools/validate_foundation.py index 410c159..0f3f69f 100755 --- a/tools/validate_foundation.py +++ b/tools/validate_foundation.py @@ -441,7 +441,7 @@ comment_summary_in_pr: never warn_only: false """ -_PHD = "afa1a883001b0555027fed8ee7cf36d740b1883ac7f3137abb4c216fceacf4e2" +_PHD = "2505d267f3bec16db52c716ccafc364c461733081136a496e5e2ae45d8120c23" _CR = ( "run: /usr/bin/env -u BASH_ENV -u ENV -u GNUMAKEFLAGS -u MAKEFLAGS -u MAKEFILES " "-u MAKEOVERRIDES -u MFLAGS /usr/bin/make --no-builtin-rules --no-builtin-variables check-compiler"