nix: repair darwin Mach-O page hashes the store-path rewrite invalidates - #4344
Conversation
|
Required workers must start within 120 minutes after GitHub marks them ready. Once assigned, each has 120 seconds for setup, then 10800 seconds for validation, then 10 seconds for cleanup. Add the Extended validation: the |
Client-side network during CI
Client-side connections only (proxy env): eval fetches, gh, git. Daemon-side substitutions and fixed-output builders are not visible here. Run: https://github.com/indexable-inc/index/actions/runs/30665471543 |
Repin `nix-src` to megamerge 5ab172cd (53 patches), adding `libstore:
Bit-reproducibly fix darwin Mach-O page hashes after rewriting`.
`RewritingSink` substitutes scratch-path bytes for final-path bytes after
the builder exits. Apple's `ld` has already ad-hoc-signed every arm64
binary at link time, and the CodeDirectory's SHA-256 page hashes cover
exactly the bytes being rewritten, so the registered output carries a
signature that no longer describes its contents. macOS kills such binaries
at first page-in (`cs_invalid_page`). The patch recomputes only the
mismatched page-hash slots in place, preserving the `linker-signed` flag,
the 4 KiB page size, the identifier and every special slot, so the result
is bit-identical to a cold build.
Measured on aarch64-darwin against claude-code (index#4336), patched
daemon vs the one this repin replaces:
unpatched 80 bytes differ .check: invalid signature (code or
signature have been modified)
patched 48 bytes differ .check: valid on disk / satisfies its
Designated Requirement
What this does NOT do: make `nix build --rebuild` pass. All four runs
(patched x2, unpatched x2) exit 1. The residual 48 bytes are `LC_UUID`
(16) plus the page-0 CodeDirectory hash that covers it. The helper
preserves `LC_UUID` deliberately -- that preservation is what makes the
repair bit-reproducible -- and `LC_UUID` is ld64's own content hash,
computed over content that still held the redirected `$out`. Reaching
rc=0 additionally requires `-Wl,-no_uuid` (measured: rc=0 twice with
both, rc=1 with either alone), which is a nixpkgs bintools decision that
nixpkgs#188347 reversed in 23.11 to restore symbolication. Recomputing
`LC_UUID` inside nix was tried and rejected: four reconstructions of
ld64's content-UUID algorithm all failed to reproduce a cold build's
value, and pinning nix to an undocumented linker checksum would be worse
than the Mach-O parsing upstream already objected to. So #4336 stays
open on that half.
Fork-only by construction. Vendored from NixOS/nix#15638 at its
pre-force-push revision 883e4331 (the minimal in-process form: a 376-line
darwin-only helper and a 16-line `#ifdef __APPLE__` call site); the PR
later grew a settings surface, a `nix __fixup-macho` subcommand and
substitution-time verification, and was admin-closed 2026-07-05 with
"baking in support for MACH-O into Nix is a NACK". #15638 credits
NixOS/nix#14999, our own still-open draft. Marked `upstream = "hold"`:
a human argues it or drops it, per this fork's `aiPrsAllowed = false`.
`passthru.tests.machoRewrite` wires the patch's own functional test
(`tests/functional/macho-rewrite.sh`), which asserts the rewritten binary
still executes, verifies under codesign, and keeps `linker-signed`.
Refs #4336
Assisted-by: Claude <noreply@anthropic.com>
The nix-src comment listed the Mach-O page-hash fix among the patches with no lib/fork-packages.nix entry, which the entry this branch adds makes false for that one patch.
617a2a0 to
6475255
Compare
|
Rebased onto main, and the repin half of this PR is gone: main now pins What is left is the part main does not have:
A second commit corrects that now-stale sentence in (Comment written by Claude Code, an AI agent.) |
Repins
nix-srcto megamerge5ab172cd(53 patches), addinglibstore: Bit-reproducibly fix darwin Mach-O page hashes after rewriting.What was broken
RewritingSinksubstitutes scratch-path bytes for final-path bytes after the builder exits. Apple'sldhas already ad-hoc-signed every arm64 binary at link time, and the CodeDirectory's SHA-256 page hashes cover exactly the bytes being rewritten — so the output carries a signature that no longer describes its own contents. macOS kills such binaries at first page-in (cs_invalid_page).The patch recomputes only the mismatched page-hash slots in place, preserving the
linker-signedflag, the 4 KiB page size, the identifier and every special slot, so the result is bit-identical to a cold build.Measured, aarch64-darwin, claude-code
Patched daemon vs the one this repin replaces (
--rebuild --keep-failed, comparing the.checkwrapper against the registered one):.checksignatureinvalid signature (code or signature have been modified)valid on disk/satisfies its Designated RequirementThe locked input builds to the same store path that was tested:
35gmhy2d…-nix-aarch64-apple-darwin-2.34.7+ix.g5ab172cde2d3.h908bb0d8.What this does not do
It does not make
nix build --rebuildpass. All four runs — patched ×2, unpatched ×2 — exit 1. #4336 stays open.The residual 48 bytes are
LC_UUID(16) plus the page-0 CodeDirectory hash covering it. The helper preservesLC_UUIDdeliberately — that preservation is what makes the repair bit-reproducible — andLC_UUIDis ld64's own content hash, computed over content that still held the redirected$out.Reaching rc=0 needs a second, independent change. Measured combinations:
-Wl,-no_uuid-Wl,-no_uuidBoth halves are necessary. The second is a nixpkgs bintools decision that nixpkgs#188347 reversed in 23.11 to restore symbolication, so it is not this PR's to make.
Recomputing
LC_UUIDinside nix was tried and rejected: four reconstructions of ld64's content-UUID algorithm all failed to reproduce a cold build's value. Pinning nix to an undocumented linker checksum would be worse than the Mach-O parsing upstream already objected to.Why it is fork-only
Vendored from NixOS/nix#15638 at its pre-force-push revision
883e4331— the minimal in-process form (376-line darwin-only helper, 16-line#ifdef __APPLE__call site). The PR later grew a settings surface, anix __fixup-machosubcommand and substitution-time verification, and was admin-closed 2026-07-05 with "baking in support for MACH-O into Nix is a NACK" and "We should be doing less rewriting - not more". #15638 credits NixOS/nix#14999, our own still-open draft using the non-reproduciblecodesign -f -s -approach.Marked
upstream = "hold". Nothing was posted upstream: this fork'saiPrsAllowed = false, and the maintainer explicitly asked for less agent-generated volume on that thread.Two known gaps, both from taking the minimal form, recorded in the fork-packages
reason: the Mach-O parse runs in the daemon as root (later #15638 revisions moved it behind a privilege-dropped hook for that reason), and it re-signs any page whose stored hash disagrees with its bytes, so a file already corrupt for an unrelated reason is silently repaired rather than left detectably broken.Test
passthru.tests.machoRewritewires the patch's own functional test (tests/functional/macho-rewrite.sh), which asserts the rewritten binary still executes, carries the substituted path, verifies undercodesign, and keepsflags=0x20002(adhoc,linker-signed). It expects--checkitself to fail, for theLC_UUIDreason above. I have not executed the suite — it is wired, not yet run here.Unverified claim, dropped
The upstream thread cites 437 broken slices across 48 packages in the live channels. I scanned 2610 signed Mach-O objects in the local store (610
bin/*, 2000*.dylib) and found zero modified;fish-4.8.0/bin/fishand everyffmpeg-headlessdylib verify clean. I could not reproduce it, so that number is not carried anywhere in this change.Refs #4336
Note
Add focused functional test for darwin Mach-O page hash repair after store-path rewrite
machoRewritefocused functional test in packages/nix/default.nix covering the Mach-O ad-hoc signature page-hash mismatch that occurs after store-path rewriting on darwin.hold, documenting the scope and provenance of the vendored patch.machoRewritetest is included in the standardpassthru.testsset for non-cross builds.Macroscope summarized 6475255.