2026-08-29 - The payload wall, answered by asking the guest

The wall had stood for three sessions: the payloads' runtime rejects a handoff structure nothing here could describe, and the two remaining routes were both expensive. It took three runs and a stub that shifts four registers.

vsnprintf first, because it is the most wanted name there is

Twenty-two of the twenty-five payloads import it - ahead of every other missing name, and ahead of write, sleep and the whole socket set. Every logging helper they have is built out of it.

A va_list on this architecture is not a pointer walking a stack: it is a cursor over the caller's own spilled registers and the stack past them, four fields, psABI section 3.5.7. Which means the v forms are not a convenience wrapper - they can render a format the register forms have to refuse, because six registers is where those stop and a list has no such limit (D364). The test asserts exactly that, both ways, on one format.

One renderer, two argument sources. Two formatters would have drifted and the first divergence would have been a string that differed by which spelling of printf was called.

And then the wall itself

Three blocks, three questions. Markers say which field is used and end the run. Answering stubs say how far it gets and say nothing about what was asked. Neither can say what was passed, so: one emitted stub per field, shifting the guest's arguments along by a register, putting its own slot in the first, tail-calling a reporter.

the guest called handoff slot 0 with (0x1, 0x4000000109f7, 0x600000800ef0)

That second argument is in the payload's own .rodata. At that offset, in its own file, is sceKernelDlsym (D365).

The guest supplied the evidence. Nothing was inferred, nothing was read out of firmware, and the whole thing cost one run.

An import table was never going to answer it

klogsrv carries vsnprintf, snprintf and sprintf as eight-byte objects in .bss. Its runtime resolves them one at a time and stores them there. Those names are in no import table and no relocation - which is why entering at main left them null, and which settles what D359 half-saw: the null is a .bss global, and it is null because the runtime that fills it never ran.

So the thunk table grew a second population - one stub per implemented name, published by name - and sceKernelDlsym answers out of it. The same address the linker would have written, so a function reached by name and the same function reached by an import are one address with one counter and one trace entry. A resolver minting its own answers would have made a call behave differently depending on which route found it (D366).

Three things that had to stay true, and one that was already false

A stub count still means the guest's imports; the label list and the binding list are now one list with a test; and unknown became unknown#index, because two unlabelled stubs read as one function called twice - which is exactly what they did, this session, before the change.

The one already false: a_format_that_cannot_be_honoured_empties_the_destination asserted a specific value in first_fault, which is one slot for the whole process. It passed because nothing else had ever recorded a fault. The first vsnprintf test that did made it fail, and the test was the wrong thing rather than the new one - fourth appearance of the shared state hazard, same fix: ask the thing that computed the answer.

Where it got to

klogsrv runs __crt_start at its declared entry for the first time, resolves sceKernelDlsym and getpid, reaches __kernel_init, and stops on handoff field 2 - which it reads as a pointer. Two walls further than this morning, and the next one is a measurement rather than a mystery: one more run of the reporting block describes it.

Unknown fields are mapped read-write now rather than left unmapped. A field read as a pointer yields zero - which a correct program checks - while the address still names the field, so a runtime that reads six of them does not need six runs.

The correction, corrected back

An earlier session called sceKernelDlsym the item that decides whether payload support is native or a hack, then withdrew it: no payload imports it. The measurement was right and the withdrawal went too far. It is not imported because it arrives through the handoff structure - which is a stronger position than being imported, not a weaker one.

The second field, and three smaller things

With the resolver in place the same technique answered the next field. Two runs, differing only in what the unestablished fields hold:

fields what happened
mapped markers __kernel_init reads field 2, carries on, and hands 0x2001 back as a module handle
zero it reads field 2 and faults dereferencing null

Field 2 is a pointer the runtime reads through (D368). And 0x2001 is the marker's own low half plus one - which is the caution worth writing down: a marker a guest uses as data becomes a value the guest computes with, so it names the field and makes the next few calls fiction. Which fill is in use is ORBISTOUN_HANDOFF_FIELDS now, defaulting to markers, and orbistoun-env records it so two runs are never compared across it.

Time and waiting, because between them sleep, gettimeofday, time, usleep and clock_gettime are wanted by more payloads than the whole socket set - a server's loop is wait, poll, timestamp, log, and it does the waiting first. A sleep that returns immediately does not save a guest any time; it turns a paced loop into a spin. All six are POSIX-documented outright, and the two structures are in the FreeBSD checkout the constants come from, so _clock_id.h joined the harvest rather than the identifiers being typed in.

write, which seventeen payloads import and which was simply the POSIX spelling of a call this project already served.

And a rule that had been implicit. clock_gettime and gettimeofday are C library functions, so they were written in orbistoun-libc - and the audit refused it, because both were already declared in libScePosix, where a title was measured importing them. Where a symbol is declared is a claim about the target; where its code lives is a claim about this repository (D367). They are answerable separately, they were conflated because they usually coincide, and the audit is what noticed.