Skip to content

[Homebrew/Security] Run sealed Formula checks against the authenticated source tree - #1082

Merged
brandonpayton merged 13 commits into
mainfrom
homebrew/formula-xtask-authority-qk044
Jul 24, 2026
Merged

[Homebrew/Security] Run sealed Formula checks against the authenticated source tree#1082
brandonpayton merged 13 commits into
mainfrom
homebrew/formula-xtask-authority-qk044

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 24, 2026

Copy link
Copy Markdown
Member

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 0555 file, and exposes
only 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 test failed
because the sealed xtask still used the checkout path embedded when it was
compiled. That original checkout was inaccessible by design, so the checker
reported:

global package build input "flake.nix" not found at <original-checkout>/flake.nix

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

  • The resolver passes its already-authenticated source alias directly to the
    sealed checker as
    build-deps program-index-context-check --source-repo-root <alias>.
  • xtask accepts that option only for this freshness check and rejects roots
    that are relative, noncanonical, inaccessible, or incomplete.
  • A scoped, non-sendable guard installs the selected root before registry or
    digest access and restores the default on every return.
  • Global toolchain and fork-tool digest caches are keyed by the selected root.
    Same-root memoization is safe at this boundary because the publisher gives
    the one-shot checker a read-only alias.
  • The resolver supplies the source identity in command arguments, not through
    caller-controlled ambient repository-root state.

The privileged regression uses the real release xtask. It builds the checker
at 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, package
publication, 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.ts legitimately changes the identities of
the eight packages that declare the resolver as a build input:

  • lamp
  • mariadb-test
  • mariadb-vfs
  • nginx-php-vfs
  • nginx-vfs
  • node-vfs
  • redis-vfs
  • wordpress

All and only those identities changed. Independent recomputation matched every
regenerated value; control packages remained unchanged.

Validation completed at 2a8b317c94b5e1ab7042d40bcac7c7563b57420b

  • Full xtask tests: 529/529 unit tests and 1/1 integration test passed.
  • Host resolver tests: 68/68 passed, including the exact checker arguments and
    registry handoff.
  • The complete local Homebrew publisher suite passed, including the real
    Homebrew lifecycle, OCI publication, materialization, sealing, VFS release,
    main-shell closure, and final trust checks.
  • Bundle regeneration and freshness checks passed.
  • Launcher, publisher, Bash, Ruby, and diff checks passed.
  • Independent security and correctness review: GO.

Remaining gate

The exact-head Ubuntu privileged-systemd publisher test remains required. That
is the environment that can prove the real BindReadOnlyPaths and
InaccessiblePaths topology 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.

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.
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-1082-staging

ABI v42. 23 built, 53 failed, 76 total.

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.
@brandonpayton brandonpayton changed the title [Homebrew/Security] Isolate Formula builds from publisher authority [Homebrew/Security] Run sealed Formula checks against the authenticated source tree Jul 24, 2026
Base automatically changed from emdash/homebrew-abi42-generation-qk044 to main July 24, 2026 21:16
@brandonpayton
brandonpayton merged commit 2a8b317 into main Jul 24, 2026
32 of 91 checks passed
@brandonpayton
brandonpayton deleted the homebrew/formula-xtask-authority-qk044 branch July 24, 2026 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant