Repository navigation
fix(macos): emit loadable Mach-O with true ELF segment mapping - #10
Conversation
The macOS writer previously emitted a monolithic __TEXT at 0x100000000
with nsects=0, RWX max protection and a malformed embedded ad-hoc code
signature blob, which macOS/AMFI rejects (SIGKILL) and codesign(1)
refuses to re-sign ('main executable failed strict validation').
Rewrite the writer to produce a faithful host image of the guest:
- Parse the incoming (relinked) ELF program headers and emit one
LC_SEGMENT_64 per PT_LOAD, mapping guest vaddrs exactly (no rebasing
of absolute guest addresses).
- Merge page-rounded overlapping PT_LOADs into a single segment backed
by a synthetic vaddr-indexed image (ELF bss holes zero-filled).
- Embed a proper section_64 (__text) inside its __TEXT segment command
(cmdsize 72+80=152, section_64 now has all 12 fields incl. reserved3).
- Drop the broken embedded code signature; leave an empty __LINKEDIT so
codesign(1) can add a real ad-hoc signature after linking.
- Clear MH_PIE: guest absolute addresses must not be slid by dyld.
- Patch trampoline bodies into a dedicated RX segment placed after the
guest image; jump displacements computed against true guest vaddrs.
- Pad the file to __LINKEDIT start so no segment points past EOF.
Verified on Apple Silicon (M2, macOS 27.2):
- otool -l clean (no 'Inconsistent size'), codesign ad-hoc succeeds.
- End-to-end proof: PS5-style ELF -> ryty --macos -> image -> custom
loader -> Rosetta executes guest x86_64 code; crash report shows the
fault exactly at a magic address encoded in the guest entry stub.
- Note for PS5 titles loading below ~0x10000000: Rosetta occupies the
low address space in translated processes, so such images must be run
through a loader rather than as the main executable.
TestMacOsPatcher previously fed a 256-byte buffer with ELF magic but no program headers and asserted the legacy layout (14 load commands, monolithic __TEXT at a fixed base). The rewritten writer maps guest PT_LOAD segments at their own vaddrs, so the fixture now builds a minimal well-formed ELF64 with a single R|X PT_LOAD at 0x10000000 and asserts: - 12 load commands (1 guest segment + trampoline + LINKEDIT + fixed), - first command is an executable LC_SEGMENT_64 named __TEXT, - the segment vmaddr equals the guest PT_LOAD vaddr (no rebasing).
|
@BioXeon hey, I appreciate your changes but these 2 workflows are failing if you want your PR to be merged please fix this. |
Apple-Silicon Rosetta reserves the low address space of translated processes, so PS5 titles that load below ~0x10000000 (Stray, Biomutant — the standard PS5 layout, entry at vaddr 0x80) cannot be mapped at their own vaddrs, and page 0 is unmappable outright. When the guest loads below 0x10000000, shift the whole image to 0x10000000 and fix up everything that stores an absolute guest pointer: - apply DT_RELA R_X86_64_RELATIVE (write addend+delta) and R_X86_64_64/GLOB_DAT against in-image symbols (via DT_SYMTAB), - shift PLT/GOT lazy-binding slots (DT_JMPREL, R_X86_64_JUMP_SLOT) whose stored value points at a PLT stub in the old space, - shift DT_INIT/DT_FINI function pointers in the dynamic section. Exec segments need no rewriting: PS5 code is compiled RIP-relative — zero absolute instruction forms were found in the exec segments of either title examined (verified by scanning for movabs with in-image immediates). Verified end-to-end with Biomutant (PPSA06255): the relinked image is signed with codesign(1), mapped by the loader PoC, and the guest runs its own init until the first NID import call — captured by the loader's NID tracer.
|
Added a third commit that removes the main execution blocker described in #9: when the guest loads below 0x10000000 (the standard PS5 layout — both titles examined load at vaddr 0 with entry 0x80), the writer now shifts the image to 0x10000000 and applies its own dynamic relocations (943,577 R_X86_64_RELATIVE + 9,101 R_X86_64_64/GLOB_DAT against in-image symbols + 2,158 PLT/GOT lazy slots from DT_JMPREL + DT_INIT/DT_FINI). Exec segments need no rewriting — PS5 code is RIP-relative; a scan for movabs with in-image immediates found zero absolute instruction forms. End-to-end result with Biomutant (PPSA06255) on Apple Silicon: |
The expression `~0xFFFull` produces `unsigned long long` while `hi` is `std::uint64_t` (aka `unsigned long` on Linux GCC), preventing template argument deduction in std::max. Cast explicitly.
|
Thanks for the heads-up! Fixed — the failure was a GCC 13 template argument deduction issue: |
|
Still fails, I will be resolving it on my end. |
Fixes the macOS writer problems described in #9.
What changed
LC_SEGMENT_64per PT_LOAD, mapping guest vaddrs exactly — guestabsolute addresses keep working, no rebasing.
synthetic vaddr-indexed image (ELF bss holes zero-filled).
section_64 __textinside its__TEXTsegment command(
section_64now has all 12 fields incl.reserved3, cmdsize 72+80).__LINKEDITsocodesign(1)can add a real ad-hoc signature.MH_PIE(dyld must not slide the guest image).image; jump displacements computed against true guest vaddrs.
__LINKEDITstart so no segment points past EOF.TestMacOsPatcherto build a well-formed minimal ELF64 and assertthe new per-segment layout.
Verification (Apple Silicon, M2)
ctestgreen (relinker_testscovers the new layout).otool -lfully clean, noInconsistent size;codesign --force --sign -succeeds (41k hashes).ryty --macos, mapped by asmall loader and executed under Rosetta faults exactly at a magic address
encoded in the guest entry stub (see macOS backend: output Mach-O cannot load (AMFI SIGKILL); Rosetta requires classic layout + loader for low-address PS5 titles #9 for details and the Rosetta
low-address-space constraints that make a loader necessary on
Apple Silicon).
Follow-up idea (not in this PR): ship the loader PoC as a runtime helper so
low-address PS5 titles can be executed on Apple Silicon despite Rosetta
reserving the low address space.