Skip to content

Fix chained fixups in universal Mach-O files - #1

Draft
johanntan wants to merge 1 commit into
masterfrom
fix/universal-chained-fixups
Draft

johanntan wants to merge 1 commit into
masterfrom
fix/universal-chained-fixups

Conversation

@johanntan

@johanntan johanntan commented Sep 10, 2026 •

Copy link
Copy Markdown
Owner

kubo/plthook is archived, so GitHub rejected creating this PR upstream. This draft keeps the tested patch reviewable in the fork for downstream MelonLoader integration; it has not been submitted or accepted upstream.

Downstream integration: LavaGang/MelonLoader#1201.

When a loaded Mach-O image is part of a universal binary and uses LC_DYLD_CHAINED_FIXUPS, plthook reads the fixup chain from the wrong file offset. A thin dylib that hooks successfully can fail with no such function: puts after packaging the identical architecture into a universal dylib. This was encountered while loading a universal Unity player.

This change locates the slice matching the loaded image's CPU type and subtype, then adds its file offset only when reading the chain from disk. Runtime addresses remain slice-relative. It handles fat32/fat64 architecture tables in either byte order and rejects truncated tables, missing architectures, and out-of-bounds slice/fixup reads. There are no public API changes or new pointer formats.

Validation on an Apple Silicon Mac:

  • Reproduced the failure on the current upstream revision with explicitly enabled chained fixups; the thin fixture passes and the universal fixture fails.
  • The fix passes thin, universal, and fat64 fixtures using plthook_open, plthook_open_by_handle, and plthook_open_by_address, on both x86_64 under Rosetta and native arm64.
  • Existing tests pass on both architectures. Header tests cover byte order, exact CPU subtype selection, missing CPU/subtype, truncation, and invalid bounds.

The regression is part of the existing Darwin run_tests target, so the current macOS CI steps execute it without workflow changes. It can also be run directly:

make -C test macos_chained_tests MACOS_TEST_ARCH=x86_64
make -C test macos_chained_tests MACOS_TEST_ARCH=arm64

The fixtures explicitly request chained fixups because older deployment/linker defaults otherwise hide this bug. No game assets are needed.

Find the on-disk slice matching the loaded Mach-O CPU type and subtype before
reading chained fixups. Account for fat32 and fat64 tables in either byte order,
validate slice bounds, and keep runtime addresses relative to the loaded image.

Add hook regressions for thin and universal libraries through all three open
APIs, plus malformed-header and architecture-selection tests. Run the new
regressions in the existing macOS CI job.

Co-Authored-By: Codex <codex@openai.com>
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