D380 - A fault in our own code should say where in our own code

decided - 2026-08-29

A fault report names the last import called and calls it inside that function. That is an attribution, not a location: it says which function the guest wanted, not which instruction faulted. For a fault in guest code it is the whole story, and this project never needed more - a fault in the emulator's own code was somebody's bug to find by reading.

It stopped being enough when an implementation grew complicated enough to fault inside itself.

Naming the site, on a toolchain with no symbols

The binary carries none, so an address in orbistoun's own code is opaque, and it is randomised per run so it cannot even be written down. But a GuestFn is a function pointer and a function pointer is an address - this project already has a table of where every implementation starts. Sorted once at startup, a fault address finds the nearest preceding one.

The distance is printed with the name, and that is what makes it honest: a fault inside a library routine the compiler called - a copy, a formatter - names the last implementation before it. A small offset is a strong hint; a large one is visibly not a match.

Its first answer was the second kind, and useful for it:

in orbistoun's own code, nearest implementation is posix_sigdelset+0x391c1

Two hundred kilobytes past the last implementation in address order. So the fault is not in an implementation at all - it is in support code called from one, which is a fact the report could not previously state.

And the renderer now follows a pointer only where the run mapped one

The dumper has always checked before dereferencing an argument. Nothing else did: the C library follows a guest pointer because "a guest that passes a bad pointer faults here precisely as it would have faulted there".

That is right for a pointer the guest computed and wrong for one it never set. A %s whose argument came out of an overflow area holding no arguments is the second kind - arbitrary stack contents - and no amount of guarding individual impossible values catches it, because the value is arbitrary. Guarding null, then all-ones, then a third thing is a losing game.

So %s asks the same question the dumper asks, through the same published ranges, and renders (unmapped) for a pointer outside them. Permissive when nothing is known: a caller outside a run has no ranges to check against, and there the old behaviour is the right one.

This also removes an invention. The previous attempt rendered the literal text (bad pointer) for an all-ones argument, which is text this project made up appearing in a guest's own output; (unmapped) is a statement about a range this run published, which is a fact.

What is still not known, said plainly

The -1 read that started all this is unexplained after eight eliminations - the list address, both areas inside it, the format, the destination, a %s argument, the stale-binary class, and both pointer-following paths. The fault is unchanged.

What is now known about it: it is in support code, not an implementation; it is reached from the vsnprintf path; and pinning it further needs a debugger or a symbolised build, neither of which this toolchain gives. Every change made while chasing it stands on its own merits, and none of them was the cause. Recording that is the point.