D514 - The wall is a module that was loaded, placed, and never started
measured - 2026-09-03 (a deterministic run, the faulting instruction, and the export table)
PPSA02664 has walled at the same place for weeks:
read of 0x8 at the title's own
modules+0x13dca44, after 2077 import calls. D513 made the
run reproducible, which is what made it worth reading the fault properly
rather than around it.
What the instruction actually does
4c 8b 7e 08 mov r15, [rsi+0x8] <- rsi = 0x480001f0c330, r15 becomes 0
49 8b 4f 08 mov rcx, [r15+0x8] <- FAULT: read of 0x8
80 79 19 00 cmp byte [rcx+0x19], 0
So it is not a null return being dereferenced. It is a field
at 0x480001f0c338 - inside the title's own module data -
that holds a pointer and holds zero. Something that should have
written it never ran.
What should have written it
The call stack is
0x4800013d5f00 <- 0x400000f23ac8 <- 0x400000f269c9,
and the two calls immediately before are:
sceKernelLoadStartModule(...) -> 0x40
sceKernelLoadStartModule(...) -> 0x41
orbistoun-cli exports puts the caller inside an export:
Il2CppUserAssemblies.prx exports an entry at
+0x13d5e90, and the frame is +0x13d5f00,
0x70 into it. So that module is mapped at
TITLE_MODULE_BASE, its exported code is running, and the
guest is executing it having asked for it to be
started.
Here is what load_start_module does for a
/app0 path:
let handle = NEXT_MODULE_HANDLE.fetch_add(1, Ordering::Relaxed);
write_int(args[5], 0);
return handle;A handle, and nothing else. The Load
half is done elsewhere and eagerly - place_title_modules
maps every module a title ships before the guest runs, which is why the
code is there to execute at all. The Start half has never
existed.
Which start, and the hypothesis that was wrong
The obvious candidate was module_start, the platform's
named module entry point. Neither module exports it:
Il2CppUserAssemblies.prx 247 exports, no module_start (0xcf833c78728aa305), no module_stop
PS5Util.prx 5 exports, neither
So that is dead. What both modules do have is the ordinary ELF mechanism:
0x0c INIT 0x19 INIT_ARRAY 0x1b INIT_ARRAYSZ
and orbistoun-elf parses init_array,
init_arraysz, preinit_array into
DynamicInfo - where nothing outside the parser has
ever read them. A grep for init_array across every
crate but orbistoun-elf returns nothing.
That is the whole wall. The module's C++ static constructors have never run in any run of this emulator, so a global one of them fills is zero, and the guest reads it three calls later at a site with no visible connection to the load.
Why it took this long to see, which is the part worth generalising
sceKernelLoadStartModule returned a plausible handle and
said nothing. A handle and a silence are indistinguishable from
a module that started - the exact failure principle 3 forbids,
in a function whose own name contains the half it does not
implement.
So the gap is now reported, unconditionally, from the path every run ends on:
orbistoun: 2 module(s) got a handle and were NOT started (nothing runs DT_INIT_ARRAY,
so their constructors have not run): PS5Util.prx (handle 0x40),
Il2CppUserAssemblies.prx (handle 0x41)
This is a gap report, not a diagnostic. Nothing
switches it on and nothing intervenes; it says what the run did not do.
It is beside the intervention summaries in persist because
that is the one path every ending comes through (D513), not because it
is one of them.
It also turned an inference into a measurement. The two paths were
first identified by arithmetic - the snprintf before each
load returned 0x1f and 0x2c, and
/app0/Media/Modules/PS5Util.prx is 31 characters and
/app0/Media/Modules/Il2CppUserAssemblies.prx is 44. Right,
and not evidence. The report names them.
What is deliberately not done here
Running the initialisers. It is the fix, it is a
substantial one, and it needs two things this change does not have: the
placed-module table has to be reachable from the kernel shim, which
today only sees a path string; and calling DT_INIT_ARRAY
means entering guest code from a shim, which is a control-transfer this
emulator does exactly once, at the entry point, through
orbistoun-abi.
Neither is a new concept and both are the obvious next piece of work. Splitting them from the diagnosis keeps this decision about what was measured.
Done the same day, and both halves already existed - see D515. Entering guest code from a shim is
thread::call_guest, in this same crate, built forcall_onceinitialisers. Reaching the module table has an exact precedent one screen away innote_region(D446). The estimate above was written from the shape of the problem rather than from the tree. Running the initialisers took the guest from 2,077 calls and a fault to 20,000,000 calls and its frame loop.
And the test asserts the report, not the behaviour. It cannot assert the constructors did not run, because nothing in that crate loads a module. When something does run them, that test should be replaced by one asserting they ran - not deleted quietly, which is how a description of a gap outlives the gap (D510).