2026-09-03 - (/loop) The trace was naming the wrong functions

tests   1991  ->  1991   (a sizing bug; the fix is in code the suite already covers)

Went looking for why the guest reads a null field inside its own il2cpp init. Found instead that the report had been lying about which functions it called, ever since the stub table was shared four worklogs ago.

The contradiction that gave it away

The report's own "what to do about it" section named the calls just before the fault:

just before: libc::usleep(0x30) -> 0x176fa836720

usleep cannot return a heap pointer. orbistoun's implementation sleeps and answers zero - that is four lines of code with no other path. A label whose return cannot match its contract is a falsifiable claim, and this one was false.

Corrected, it is libc::_Znwm(0x30) -> 0x246edb418e0 - operator new(48), forty-eight bytes in and a heap pointer out. The other three in the window were wrong too, and all four now agree with their own returns:

as reported actually
usleep(0x30) -> 0x176fa836720 _Znwm(0x30) -> 0x246edb418e0
_ZdlPvSt11align_val_t(p) -> 0xb strlen(p) -> 0xb
_ZSt15get_new_handlerv(p) -> p strncpy(p) -> p
memcpy(p) -> p memcpy(p) -> p

The mechanism, which is mine (D490)

Service::labels sizes its vector to the executable's symbol count and appends the by-name stubs after it. Correct for one module - the by-name block sits past every import.

D484 then put four modules in one table: the executable's 585 imports, then Il2CppUserAssemblies at 585, PS5Util at 1102, libc at 1116. The by-name block sits at 585 as well. So every call from a title module was named after whichever by-name stub shared its index, and since D489 the guest spends nearly all its time in those modules.

import_labels_for now builds the vector across every module at its own offset. The labels are computed inside link_title_modules, because that is where the module bytes are - so the symbol database is passed in rather than the 43 MB of module bytes being carried out.

Nearly acted on it as a provenance defect

The plan for this tick was to find which call answered the null. The first two hypotheses were that the symbol database had a bad name for a NID - which would have been recorded as a provenance defect against a name that was never involved.

What stopped it was asking whether the value could have come from the function named. It could not. That check is cheap and it is the same shape as principle 3 one level up: a label is a claim, and a claim that contradicts its own data is the one to pull.

What it does not explain

The wall is where it was: read of 0x8 at the title's own modules+0x13dca44, stable across runs, inside the title's own il2cpp init. operator new succeeded - it answered a real heap pointer - so the null came from somewhere else. With the labels correct that is now a question the trace can be asked; it could not be before.

The ±3 oscillation survives (44/46 distinct, 2077/2080 calls across three runs).

State

cargo test --workspace green - 117 suites, 1991 tests, 0 failures. clippy --tests clean, fmt clean, identity scan clean.

Removing the now-unused bytes parameter from prepare_diagnostics collapsed its call site and brought place_and_relocate back under the line limit, which the label change had pushed over.

Nothing committed. The day holds worklogs 292-340 and D466-D490.

Next: the null read, now that the trace can be read. operator new answered; something between it and the field access at +8 did not.