[Homebrew/Security] Run sealed Formula checks against the authenticated source tree - #1082
Merged
Merged
Conversation
Bind the staged package-producing commit to its workflow-only descendant, validate the exact rootfs closure, and feed one byte-bound local index to build and verification jobs. Keep index tooling credential-free, make the cyclic VFS deferral explicit, retain an exact focused Chromium execution proof, and document the postpublication closeout sequence.
Prebuild the exact host xtask under the trusted workflow identity and scope its path only to bottle build and verification steps. Require both Formula runners to independently select that host binary, then expose only its read-only source alias to isolated Formula tests. Recheck the original inode and bytes on every entry so stale or replaced policy code fails closed. Cover the full binary-resolver surface, workflow trust mutations, caller/path/access replacement cases, and Linux systemd bind-mount behavior.
Forward the exact workflow-prepared xtask through each dev-shell command invocation instead of preserving it globally in dev-shell.sh. This keeps Formula execution least-authority while restoring the global package-toolchain input byte-for-byte, so existing package cache keys remain current. Trust mutations and an exact-source projection regression protect both sides of the contract.
Problem: Cargo normally hard-links the Linux release xtask to its hashed deps artifact, while the Formula isolation boundary requires one inode with no alternate alias. Implementation: seal Cargo’s exact bytes into a fresh mode-0555, single-link inode before both Formula build and verification. Keep the launcher’s stronger nlink policy, bind both workflows to one helper, and make trust mutations reject either seal bypass. Validation: the canonical publisher suite, workflow trust self-test, exact-source program projection, helper regression, and an exact linux/amd64 Cargo reproduction passed. The reproduction changed the checker from nlink 2/mode 755 to byte-identical nlink 1/mode 555. Not run: kernel, POSIX conformance, and browser suites are outside this publisher-only change. Live publication remains gated on a newly pinned Asa canary. Residual boundary: a concurrent attacker already running as the trusted workflow UID could race the writable Cargo target; untrusted Formula code has not run when sealing occurs.
The hosted runner keeps the reviewed Kandelo checkout below a workflow-private home directory. Requiring the isolated Formula identity to read the original checker path rejected that secure layout before Formula execution, even though the launcher deliberately hides the checkout and exposes the exact checker through a root-created read-only bind alias. Keep the checker mode, single-link, owner, non-writability, ancestor-replacement, inode-state, and SHA-256 checks. Remove only the redundant original-path read/execute requirement, retain the isolated alias audit, split the remaining failures into precise diagnostics, and model a mode-0700 runner home in the privileged regression. Validated with the complete Homebrew publisher contract suite and a privileged Ubuntu bind-mount proof. The exact hosted-runner path remains for the Asa canary after sealing and tap repinning.
The checker sealer guarantees mode 0555, but the launcher’s historical bitmask also accepted owner-only mode 0700. That made an unsafe-mode regression reach the later alias audit instead of failing at the pre-bind boundary. Require the exact 0555 sealer contract and bind that predicate in the publisher trust checker. This keeps the private-original/read-only-alias design while making every accepted checker mode deterministic. Validated with shell/Ruby syntax, the publisher trust checker, and diff checks. The privileged Linux/systemd launcher regression is the required next gate before tap repinning.
The production-shaped checker case intentionally hides the original checkout below a mode-0700 runner home. That also hid the negative test’s deliberately Formula-owned inner directory, so the replacement test could no longer observe the unsafe ancestor it was meant to reject. Make the source parent traverse-only for that one negative assertion, then restore mode 0700 before the positive read-only-alias path. This preserves both independent contracts without weakening production isolation. Validated with shell syntax and diff checks. The privileged Linux/systemd launcher regression remains the required next gate.
Keep the privileged launcher test faithful to the production checker contract. The test now makes only its private checker copy writable while changing or restoring bytes, and restores the exact 0555 seal before every launcher invocation.
Homebrew rebuilds the ordinary environment before running Formula tests, which drops WASM_POSIX_XTASK_BIN. Carry the launcher-validated checker through the HOMEBREW_* namespace, keep caller-selected aliases excluded, and prove the behavior with the pinned real Homebrew lifecycle.
Contributor
Phase B-1 matrix build status —
|
| Package | Arch | Status | Sha |
|---|---|---|---|
| icu | wasm32 | built | 48a3ee79 |
| libcurl | wasm32 | built | 8273da44 |
| libcxx | wasm32 | built | 53612e1c |
| libcxx | wasm64 | built | 986b4977 |
| libiconv | wasm32 | built | b494e26a |
| libpng | wasm32 | built | e31da77e |
| libxml2 | wasm32 | built | f8e7709d |
| libzip | wasm32 | built | 0e020e8d |
| openssl | wasm32 | built | bdef2279 |
| openssl | wasm64 | built | 11532ba6 |
| sqlite | wasm32 | built | a21f5097 |
| sqlite | wasm64 | built | 1f7a45fb |
| zlib | wasm32 | built | a826c5a1 |
| zlib | wasm64 | built | 09bd3336 |
| bc | wasm32 | built | 87d9afde |
| bzip2 | wasm32 | built | 09122fb0 |
| coreutils | wasm32 | built | 72765db8 |
| cpython | wasm32 | built | 314d4763 |
| curl | wasm32 | built | 63cca65e |
| dash | wasm32 | built | 0b2035e2 |
| diffutils | wasm32 | built | 5b829e0f |
| dinit | wasm32 | built | 28167e43 |
| erlang | wasm32 | failed | — |
| fbdoom | wasm32 | built | ad115c1c |
| file | wasm32 | failed | — |
| findutils | wasm32 | failed | — |
| gawk | wasm32 | failed | — |
| git | wasm32 | failed | — |
| grep | wasm32 | failed | — |
| gzip | wasm32 | failed | — |
| homebrew-bootstrap | wasm32 | failed | — |
| kandelo-sdk | wasm32 | failed | — |
| kernel | wasm32 | failed | — |
| less | wasm32 | failed | — |
| lsof | wasm32 | failed | — |
| m4 | wasm32 | failed | — |
| make | wasm32 | failed | — |
| mariadb | wasm32 | failed | — |
| mariadb | wasm64 | failed | — |
| modeset | wasm32 | failed | — |
| msmtpd | wasm32 | failed | — |
| nano | wasm32 | failed | — |
| ncurses | wasm32 | failed | — |
| netcat | wasm32 | failed | — |
| nginx | wasm32 | failed | — |
| php | wasm32 | failed | — |
| posix-utils-lite | wasm32 | failed | — |
| ruby | wasm32 | failed | — |
| sed | wasm32 | failed | — |
| shell | wasm32 | failed | — |
| spidermonkey | wasm32 | failed | — |
| tar | wasm32 | failed | — |
| tcl | wasm32 | failed | — |
| unzip | wasm32 | failed | — |
| userspace | wasm32 | failed | — |
| vim | wasm32 | failed | — |
| wget | wasm32 | failed | — |
| xz | wasm32 | failed | — |
| zip | wasm32 | failed | — |
| zstd | wasm32 | failed | — |
| bash | wasm32 | failed | — |
| lamp | wasm32 | failed | — |
| mariadb-test | wasm32 | failed | — |
| mariadb-vfs | wasm32 | failed | — |
| mariadb-vfs | wasm64 | failed | — |
| nethack | wasm32 | failed | — |
| nginx-php-vfs | wasm32 | failed | — |
| nginx-vfs | wasm32 | failed | — |
| node | wasm32 | failed | — |
| redis-vfs | wasm32 | failed | — |
| spidermonkey-node | wasm32 | failed | — |
| vim-browser-bundle | wasm32 | failed | — |
| wordpress | wasm32 | failed | — |
| nethack-browser-bundle | wasm32 | failed | — |
| node-vfs | wasm32 | failed | — |
| rootfs | wasm32 | failed | — |
Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.
A read-only bind preserves the workflow user as inode owner, which prevents tap support from authenticating the checker with a self-contained root-owned rule. Stage the validated bytes as one root-owned 0555 inode, bind that exact inode over the authoritative release path, and verify its mount, state, and bytes at command entry, final verification, and cleanup.
This was referenced Jul 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
This PR lets Homebrew build and test Formula source without giving that source
access to publication credentials or the workflow's private Kandelo checkout.
The publisher prepares the package-index checker before Formula code runs,
seals its exact bytes into a root-owned, single-link
0555file, and exposesonly that checker and a read-only source alias to the isolated Formula user.
The H10 Asa canary proved that the seal and Homebrew re-exec bridge worked, then
exposed a relocation bug. Asa compiled and installed, but
brew testfailedbecause the sealed
xtaskstill used the checkout path embedded when it wascompiled. That original checkout was inaccessible by design, so the checker
reported:
The authenticated read-only source tree was available at its isolated alias,
but the relocated checker did not yet know to use it.
How this works
sealed checker as
build-deps program-index-context-check --source-repo-root <alias>.xtaskaccepts that option only for this freshness check and rejects rootsthat are relative, noncanonical, inaccessible, or incomplete.
digest access and restores the default on every return.
Same-root memoization is safe at this boundary because the publisher gives
the one-shot checker a read-only alias.
caller-controlled ambient repository-root state.
The privileged regression uses the real release
xtask. It builds the checkerat the original checkout, copies the exact bytes into the root-owned seal,
mounts the source read-only at a different path, and makes the original
checkout inaccessible. It first proves the old invocation fails for the
expected compile-path reason, then proves the explicit authenticated root
succeeds.
Safe failure of the H10 canary
The H10 run failed closed during
brew test, before bottle upload, packagepublication, index publication, or release publication. It did not publish an
Asa bottle or change a Formula. Its only durable output was the workflow's
normal failure report in the tap, so H11 can safely reuse Asa's reserved rebuild
identity.
Expected package identity updates
Changing
host/src/binary-resolver.tslegitimately changes the identities ofthe eight packages that declare the resolver as a build input:
lampmariadb-testmariadb-vfsnginx-php-vfsnginx-vfsnode-vfsredis-vfswordpressAll and only those identities changed. Independent recomputation matched every
regenerated value; control packages remained unchanged.
Validation completed at
2a8b317c94b5e1ab7042d40bcac7c7563b57420bxtasktests: 529/529 unit tests and 1/1 integration test passed.registry handoff.
Homebrew lifecycle, OCI publication, materialization, sealing, VFS release,
main-shell closure, and final trust checks.
Remaining gate
The exact-head Ubuntu privileged-systemd publisher test remains required. That
is the environment that can prove the real
BindReadOnlyPathsandInaccessiblePathstopology end to end; macOS cannot exercise that isolation.After it passes, the tap will pin this exact commit and run a fresh Asa canary.
This PR remains stacked on #1079's reviewed ABI 42 package generation. It does
not change kernel, libc, or shared ABI implementation code.