Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

mode name
drwxr-xr-x benches/
drwxr-xr-x examples/
drwxr-xr-x fuzz/
drwxr-xr-x src/
drwxr-xr-x tests/
-rw-r--r-- Cargo.toml
-rw-r--r-- README.md
README

machoedit

The Apple counterpart of elfedit: rewrite the linking metadata of a Mach-O binary so it can be relocated into a Nix-like store, then re-sign it so arm64 will actually run it.

Why this is not just “patch some bytes”

On elfedit the job is PT_INTERP + DT_RUNPATH + DT_NEEDED. The Mach-O map is:

elfedit (ELF)machoedit (Mach-O)
set_interpreter— (dyld is not relocatable, always system)
set_runpathadd_rpath (LC_RPATH)
add_needed (DT_NEEDED)change_dylib (LC_LOAD_DYLIB install name)
— (n/a on ELF)set_id (LC_ID_DYLIB)
— (n/a on ELF)ad-hoc re-signing (LC_CODE_SIGNATURE)

The last row is the whole point. On Apple Silicon, every Mach-O must carry a valid code signature or the kernel SIGKILLs it at exec. Editing a single byte invalidates the signature, so every edit is followed by a full ad-hoc re-sign: a CS_ADHOC CodeDirectory whose code slots are SHA-256 hashes of each 4 KiB page. Ad-hoc signing needs no certificate and no Apple tooling, so it is implemented here in pure Rust and works cross-platform (you can relocate arm64 macOS binaries from a Linux builder).

Design

Every edit fits inside the header padding the linker leaves between the load commands and the first section (build with -headerpad_max_install_names). Section data never moves, and the code signature keeps its file offset (codeLimit is constant), so the file keeps its length: we rewrite the load-command region in place, zero the freed padding, and recompute the signature. Time/space cost is independent of the binary size, like elfedit.

Performance

Size-independent, like elfedit. A full edit + re-sign (openset_idadd_rpathupdate) is flat from a 16 KiB to a 129 MiB dylib (release, Apple M1, warm pages):

dylib sizeedit + re-sign
16 KiB~90 µs
4 MiB~155 µs
32 MiB~240 µs
129 MiB~230 µs

The mild rise is open’s mmap+parse setup, not the edit or the signing, both of which are constant. Two subtleties make this work:

  • We hash only the pages the edit dirtied (the header pad) and copy every other CodeDirectory slot untouched — never a whole-file re-hash.
  • macOS caches code-signature validation per vnode, so an in-place mmap write leaves it stale and the kernel SIGKILLs whoever maps the “tainted” page (even though codesign --verify passes on disk!). We msync(MS_INVALIDATE) only the dirtied pages to force re-validation, instead of copying the file to a fresh vnode (which would be O(size)). This syscall is the bulk of the fixed ~200 µs cost — the macOS signing tax that ELF simply does not pay.

Reproduce: cargo run --release --example time -- <dylib> (clean per-op numbers) or MACHOEDIT_FIXTURES=<dir> cargo bench (criterion).

Status

Working and validated on real Apple Silicon (macOS 26, arm64): edits are accepted by otool, the re-signed binary passes Apple’s own codesign --verify (“valid on disk / satisfies its Designated Requirement”) and dlopens and runs. See tests/macos_codesign.rs (gated on target_os = "macos"); the pure-computation tests run everywhere.

Real-world coverage

Swept over a representative sample of the ecosystem — ~12 Homebrew bottles (openssl, python, git, gettext, coreutils, ripgrep, …), 252 Mach-O files — with each relocated, re-signed and then re-checked by Apple’s codesign (examples/scan.rs). 249/249 non-bundle Mach-O pass. The sweep forced two fixes the toy fixture had hidden:

  • Special slots. Every real binary is ad-hoc-signed with nSpecialSlots=2 (Info.plist + Requirements); v1 only handled 0. Since we only ever rewrite code slots, preserving the (untouched) special slots is enough — we just stopped rejecting them.
  • Page size is per-binary. Apple Silicon signs in 16 KiB pages, not 4 KiB; the CodeDirectory declares it (pageSize = log2). We now read it from each file instead of assuming. sample_signed.dylib + real_world_signature_verifies_and_survives_edit lock this in.

The 3 remaining rejects are app/framework bundles (“code has no resources but signature indicates they must be present”) — see the scope note below.

Not yet

  • App/framework bundles. This tool signs a Mach-O, not a bundle. Re-sealing a bundle’s resource envelope (CodeResources, Info.plist) is a bundle-level operation (codesign --deep); handle it separately.
  • Fat/universal binaries (lipo -thin arm64 first). Note the cafebabe magic is shared with Java .class files — disambiguate on nfat_arch.
  • Adding an LC_CODE_SIGNATURE to a binary that lacks one (all arm64 binaries already have one).
  • Growing beyond the header pad (would require relocating section data, as install_name_tool does without -headerpad_max_install_names).
  • Certificate-based signing (a Designated Requirement that survives even a version change). Deliberately not done: for a content-addressed store, a content change should produce a new identity, and a cert would be a trust anchor to manage — an impurity. See “Stable identity” below.

Stable identity, without a certificate

Consent (TCC, Gatekeeper, library validation) is keyed on the code identity — the cdhash — not the file path. Ad-hoc signing that changes the cdhash on every build is what creates the AWS EC2 Mac consent-fatigue problem (root is restored each boot, so a grant baked against a churning identity never sticks).

machoedit’s ad-hoc signing is deterministic: code slots are plain SHA-256 of the file’s pages, with no timestamp or nonce. So identical store content yields an identical cdhash across rebuilds — and cdhash() returns exactly the value Apple computes (CandidateCDHashFull; cdhash_short() is the 20-byte form used in cdhash H"…" requirements). Elpe can bake that hash into a PPPC profile shipped in the AMI, and the grant persists as long as the package content does — no certificate, no per-build churn. A version update changes the content and hence the hash, which is correct: it is genuinely a different binary. Verified against Apple’s tooling in tests/macos_codesign.rs.