Ruffle (PPSA68091), VLC (PPSA85300) and XPSemu (PPSA97358) read
app0/imports.txt, whose import slot addresses assume the executable's
lowest ELF load page at 0x400000, and dereference them at once. macOS
output always placed that page at 0x140010000, so all three fault on
their first slot read. Linux and Windows have --linux-fixed-base and
--windows-fixed-base for this; macOS had no equivalent.
--macos-fixed-base <address> builds the load image so that the lowest
ELF load page is at the address, as --windows-fixed-base does. The
Mach-O header takes the 64 KiB below it and __PAGEZERO ends there. The
executable is MH_EXECUTE without MH_PIE, so dyld does not slide it. The
address must be 64 KiB aligned, from 0x20000 through 0x7fffffff0000,
and an image ending past the macOS user address space is refused.
WriteMacOsImage wrote the Windows ImageBase constant into the Mach-O
segment and section addresses, __PAGEZERO, the metadata load bias, the
TLS descriptors and the init pointer, while the relocations it rebases
were computed from the WindowsLoadImage base. Read the base from the
load image everywhere instead. Every caller still builds its image at
ImageBase, so the output is unchanged.
On x86-64 macOS, including under Rosetta, the host owns GS (it holds the
pthread TSD base, which the macOS TLS stubs read), so a guest instruction
that reads or writes through GS cannot be translated, and the TLS scan
refused the whole module. Prospero Win's (PPSA99995) Wine ntdll has six.
Overwrite each in place with ud2 and NOP padding of its own length, as the
Windows writer does in #6102, and refuse one that is also an AMD-only
trampoline site.
The Windows deferred preinit thunk pushed r12, r13 and r14 and then
popped r12, r13 and r14, so the caller got r12 and r14 back swapped.
InitializeDeferredModule keeps the module base, table and count live
across the call to the thunk, so a compiler that keeps any of them in
r12 or r14 sees corrupted loader state after opening a deferred module
that has a preinit array. Pop in reverse push order instead and pin the
new bytes in test_guest_preinit.
Converted guest modules never ran their DT_PREINIT_ARRAY entries: the
reader validated the array and the unused-NID filter kept its targets,
but no writer emitted or ran it. ld.so runs a shared object's preinit
array only for the main executable, so the Linux output runs the
entries from the DT_INIT stub before the module initializer; the macOS
and Windows outputs run them before the initializer in their startup
stubs, and the deferred Windows output exports a thunk that runs them
before the initializer for runtime-loaded modules. A module with a
preinit array and no initializer is still rejected, since the Linux
stub exists only with one.
Homebrew built with the ps5-payload-dev SDK imports by symbol name and
names its system libraries with .sprx, while the libraries in libs/
are .prx files that export NIDs. Quake from the websrv launcher
stopped with 457 unresolved imports such as strlen and malloc.
An import without a module that no searched library exports under its
name is now looked up again under the NID of that name. A dependency
named with .sprx, .suprx or .debug_prx loads the .prx library of the
same stem, and libkernel_web also loads libkernel.prx, as
libSceLibcInternal already loads libc.prx.
A short TLS access followed by a near branch with an operand-size prefix
decided the branch was relative from its flow kind and opcode alone, then
re-encoded it to the fall-through address or wrote its rel32 over the prefix
and opcode. Such a branch now counts as one that cannot move.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016TFMtUC4L4wMS4eeEqaNSR
On Linux the kernel maps each PT_LOAD from the start of its first page.
When a segment begins inside the page that holds the end of the previous
segment's zero-filled memory, that page is mapped again from the file, and
the bss tail reads whatever bytes precede the segment in the file. Nuvio
Max'd 0.5.27 (PPSA99288) keeps a mutex there and crashes at launch.
A segment whose page would read wrong bytes is now appended to the output
with that page prefix rebuilt from the segments below it. Outputs whose
pages already read right are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Domain::GuestImport had no weak flag, so WindowsPePatcher handed every
import of a bundled module to the entry stub as a strong one, and a
weak import that no library exports stopped the title with
"unresolved ELF import". The executable's own weak imports already
bind to null there. libOSMesa.so.8 of the Quake homebrew has ten weak
imports that nothing provides, such as ZSTD_trace_compress_begin and
the SDK's __dlopen.
The guest relocation writer now records the ELF weak binding, and the
stub binds such an import to null when no library exports it.
AnyPS5 exports some libkernel functions, such as __error, from
libc.prx, which libkernel.prx links. On Linux they resolve through that
dependency. On Windows, the import stubs search only the listed
modules, and libc.prx was listed only when libSceLibcInternal.prx was
needed. A PS4 homebrew that needs libkernel.prx but no C library (the
OpenOrbis graphics sample) failed with "unresolved ELF import
9BcDykPmo1I". libc.prx is now also listed when libkernel.prx is needed.
PS5 executables need libSceLibcInternal.prx and convert unchanged.
WindowsLoadImage held one buffer spanning every PT_LOAD and BuildSections
copied gap pages into a section, so an image whose segments sit far apart
allocated the whole gap twice: a 32 KiB input with a 2 GiB gap needed 4 GiB
of memory and 30-40 s for --windows and --macos. Segment pages are now kept
in per-range blocks and an unmapped gap becomes a section with only a
virtual size. Output is byte-identical for the existing test inputs.
A function range that begins inside an instruction makes the code analysis
collect a second decoding from the middle of it. The Windows TLS rewriter
skips a non-FS segment-prefixed decoding there (90dcd6256); the macOS scan
did not, and refused the module. OpenSSL's assembly keeps its
"CRYPTOGAMS by <appro@openssl.org>" string in the code segment, where the
"en" of "openssl" decodes as a GS-prefixed instruction, so 24 catalog
titles could not be converted for macOS: "Unsupported macOS guest TLS
instruction (bytes: 65 6e)". The scan now skips such decodings as the
Windows rewriter does.
With the GS traps in WindowsTlsBuilder::Build, GCC 15.2 inlines this
vector insert differently and reports eight -Wstringop-overflow false
positives at it. resize appends the same four zero bytes and builds without
them.
On Windows the host owns GS (the thread environment block), so a guest
instruction that reads or writes through GS cannot be translated the way FS
thread-pointer accesses are. Such an instruction refused the whole module.
Overwrite it in place with ud2 and NOP padding instead, so the module converts
and the instruction reports itself through the crash reporter if it runs.