Repository navigation
fix(fspy): keep the main executable at dyld image index 0 on macOS - #840
Merged
Merged
Conversation
wan9chi
added a commit
that referenced
this pull request
Oct 12, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dyld lists DYLD_INSERT_LIBRARIES images ahead of the main executable, so injecting fspy's preload moves the main executable off image index 0. Node.js single executable applications (postject's `getsectdata(..) + _dyld_get_image_vmaddr_slide(0)`) and `deno compile` outputs (denoland/sui, same pattern) then add the preload's slide to their payload address and fail: pnpm 11's standalone binary aborts with `Assertion failed: (magic) == (kMagic)`, and Deno reports `Did not find magic bytes`. Interpose `_dyld_get_image_header`, `_dyld_get_image_vmaddr_slide` and `_dyld_get_image_name` to present the main executable at index 0 followed by the inserted libraries, the same permutation dyld's own `normalizeImageIndex` applies to old x86_64 apps. Add an fspy test that builds a Node SEA with postject and runs it under tracking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wan9chi
force-pushed
the
claude/youthful-hawking-e9861d
branch
from
October 12, 2026 06:43
d9d71d4 to
c4f0ac9
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
fspy benchmarklinuxmacoswindows |
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.
Motivation
On macOS, running pnpm 11's standalone
pnpmbinary as a task with automatic file-access tracking aborts with exit 134:The same command works with
cache: falseor when run directly.deno compileoutputs fail the same way (Did not find magic bytes). #571 hit this on emdash and recorded it as a known, unfixed bug.Root cause
Any inserted library triggers it, not fspy's interceptors:
DYLD_INSERT_LIBRARIES=empty.dylib pnpm --versionaborts too. Since dyld4 (macOS 12), dyld lists inserted libraries ahead of the main executable (RuntimeState::loadInsertedLibraries, "for correct flat namespace lookups"). Image index 0 is then fspy's preload, not the main executable.Node finds its single executable application (SEA) blob with postject, whose lookup assumes index 0 is the main executable:
Under injection this adds the preload's ASLR slide instead of the executable's, so Node reads the wrong memory. Deno's
libsuicopies the same pattern. Bun's compiled binaries locate their payload another way and are unaffected. Linux (postject reads the firstdl_iterate_phdrentry, which is always the main program) and Windows (FindResourceA(NULL, ...)) are unaffected.Changes
_dyld_get_image_header,_dyld_get_image_vmaddr_slide,_dyld_get_image_name). It presents the main executable at index 0, followed by the inserted libraries in theirDYLD_INSERT_LIBRARIESorder. Everything after the main executable keeps its index, so_dyld_image_countand iteration are unchanged.normalizeImageIndex). Where dyld already applies it, the main executable is at index 0 and the interposers map every index to itself.libcdeprecates its dyld bindings in favor ofmach2, so the three functions and the leadingmach_headerfields are declared in the preload'slibc.rs, as it already does forscandirand friends.node_sea: builds a Node SEA from the currentnodewith postject, then runs it under tracking on every platform and checks that its file read is recorded. It's ignored by default like the other Node tests.postjectis added topackages/tools.Notes for reviewers
ci: full) runs x64 Node under Rosetta, where dyld's own remapping already applies. The new test passes there with or without the fix. The macOS arm64 job is where it guards the regression.🤖 Generated with Claude Code