D519 - The guest calls into its module before the memory manager exists, and D518 blamed the wrong guard

measured - 2026-09-03 (watchpoints, and the guest's own call graph)

D518 ended by naming a cause. The cause was wrong, and the way it was wrong is worth more than the correction.

The disproof

D518's chain was: the singleton at 0x1A30610 has one writer at 0xf1b3cb; that store is guarded twice; the second guard passes; therefore the first guard fails.

The instruction two before that call loads the allocator object from another global:

0xf1b394  mov rdi, [rip+0xb6be75]     -> 0x1A87210
0xf1b39b  mov esi, eax
0xf1b39d  mov edx, 0xffffffff
0xf1b3a2  call 0xEEE410

So watching 0x1A87210 says whether the call site is reached at all:

ORBISTOUN_WATCHPOINT=0x400001a87210+8:rw
  touched after the access at image+0xf14e51; it now holds 0x740001481b00
  touched after the access at image+0xf154f7; it now holds 0x740001481b00
  touched after the access at image+0xf157a2; it now holds 0x740001481b00

Three sites, and none of them 0xf1b39b. The recorder holds 32 distinct sites and used three, so there was room for a fourth - the negative is about the run, not about the instrument (check 3). The global is set, and the code that would consume it never executes.

The mistake, named plainly

Two guards were read out of a disassembly, one was eliminated by measurement, and the other was declared the cause without checking that either one ran. Elimination is not demonstration. It is the failure principle 3 describes - reporting more than the measurement supports - committed inside a decision whose subject was measurement, which is the part worth remembering.

The cheap check that would have caught it is the same one that caught it here: before asking why a branch went the wrong way, ask whether the branch was reached.

What is actually true

The singleton's only writer sits in a function starting at 0xf138a0, and that function has exactly one caller in the whole binary:

nearest prologue before 0xf1b3cb: 0xf138a0
direct callers of 0xf138a0:       ['0xf269e2']

And 0xf269e2 runs after the call that faults:

0xf269c4  call 0xf23970        <- the fault happens inside here, and it never returns
0xf269c9  test al, al
0xf269cb  je   0xf269db
             ... either branch falls through to ...
0xf269e2  call 0xf138a0        <- the memory manager is initialised HERE

So the singleton is null because nothing has initialised it yet, and that is correct at that moment. The wall is not a missing initialisation. It is that the guest reaches code needing the memory manager before the line that creates it.

How it gets there

Inside 0xf23970, the guest loads Il2CppUserAssemblies.prx and then calls into it through its own import table:

0xf23a06  call sceKernelLoadStartModule   -> handle 0x41
0xf23ac3  call 0x1659E80                  -> `ff 25` PLT stub -> GOT slot 0x19935b0

The eboot imports four symbols from Il2CppUserAssemblies, and the module exports 0x9fdbed4fe0989d70 at +0x13d5e90 - which is the frame the fault reports one level down (+0x13d5f00, 0x70 into it). The module then calls back into the eboot, and that code reads the singleton.

Full chain, all of it measured rather than assumed:

eboot init (0xf269c4)
  -> 0xf23970            loads Il2CppUserAssemblies.prx
    -> PLT 0x1659E80     -> module export +0x13d5e90
      -> back into eboot -> reads 0x1A30610 -> null -> read of 0xa0
...and only after 0xf23970 returns would 0xf269e2 have created it.

What this does not establish, and it is the whole remaining question

Whether hardware takes this path at all. Two readings survive and nothing here separates them:

Both are testable, neither is tested, and picking one now would be the same mistake this decision exists to correct. What is settled is the shape: a call ordering, not a missing write.

The second reading is dead - see D521. The callback's two conditional jumps both land at or before the instruction that flows into the singleton read, so no path through it avoids the dereference. And the eboot cannot be handing the module this address by export - it exports one symbol - it installs it into a slot at 0x1AA52B8, which a watchpoint shows being written once and read once, and that read is the call that faults. The first reading survives: the module invokes the callback before the memory manager exists.

Worth noting alongside: this ordering only became reachable because D515 started the modules. That is not evidence D515 is wrong - the binding through the PLT predates it - but a guest that could not previously reach this code is a guest whose ordering nobody has checked.