2026-09-03 - (/loop) The guest says which modules to start, and we do not start them

tests   1991  ->  1991   (four experiments, no code)

Four ticks on one null global. Three mechanisms dead: constructors (D491), module_start (D492), a failed .data copy (D493). This tick found the fourth by asking cheaper questions first.

The cheap discriminator, which was decisive

Rather than dump registers at the two module calls, snapshot the module's whole .bss - 0x26e0dc bytes - and ask what the guest changes in it across a run:

0x480001f0d2ac  0x0 -> 0x7070612f00000000
0x480001f0d2b4  0x0 -> 0x00616964654d2f30
(318490 word(s) unchanged)

Two words out of 318,492, and decoded they spell /app0/Media. Two and a half megabytes of the module's own state, untouched.

That reframes everything: not an initialisation that went wrong, a module that was never initialised. And it cost one environment variable, because the cheap half of the pair already existed (D223) - the same lesson as D494, one tick later.

The obvious alternative, killed with three samples

Perhaps initialisation starts and bails on something unimplemented. The run has exactly two: scePthreadSetprio and scePthreadSetaffinity. Forced to answer success:

44/46 distinct, 2077/2080 calls, fault at +0x13dca44

Identical, inside the known ±3. Nothing in this run is refusing the module.

Then the guest said it plainly

sceKernelLoadStartModule("/app0/Media/Modules/PS5Util.prx", 0, 0, 0, 0, 0)  -> 0x40
sceKernelLoadStartModule("/app0/Media/Modules/Il2CppUserAs…", 0x34, <ptr>, 0, …)

Twice, by full path, for the two modules D482 goes hunting for. And orbistoun's answer:

if path.starts_with("/app0/") {
    let handle = NEXT_MODULE_HANDLE.fetch_add(1, Ordering::Relaxed);
    write_int(args[5], 0);
    return handle;
}

A handle, a success code, nothing else. Written when nothing could load a module at all, where answering a handle beat refusing a path that exists. Since D482 and D489 the module is loaded by the time this call arrives - and has still never been started (D495).

Which corrects how I have been thinking about D482

D482 searches the title's tree because the vendor import tables carry bare names and no paths. That is true and the search is still what lets binding happen before the guest runs.

But the paths were never unknown. The guest hands them over in full at runtime, and the search reconstructs by convention what this call states outright. Four ticks were spent asking what starts a module while the guest was calling a function with Start in its name, twice, in every run.

What is still open

What "start" actually does. DT_INIT_ARRAY is empty and no module_start is exported, so it is neither obvious answer. But the question is now about one call with a known name and known arguments rather than about a global that happens to be zero, which is the first time it has been well-posed.

The second call's 0x34 and pointer are recorded and deliberately not interpreted - the parameter names are not established here, and naming them would be an invented arity.

State

cargo test --workspace green - 117 suites, 1991 tests, 0 failures. fmt clean, identity scan clean. No code changed this tick.

Nothing committed. The day holds worklogs 292-345 and D466-D495.

Next: what the platform does on LoadStartModule for a module the loader has already placed. The sibling repos are worth grepping first - obSCEne builds sce_module stubs and prosperous invokes elfldr's load/launch, so one of them may already know.