D223 - Three cheap diagnostics, and a hypothesis nobody had raised

decided · 2026-08-24 · directed by the user: "get as much info as possible from as many sources as it can, and reduce the debug loop"

Three more diagnostics, chosen because each answers a question the existing ones could not and none needs platform code: ORBISTOUN_BSS_FILL, ORBISTOUN_WATCH, ORBISTOUN_POKE.

The snapshot beat the watchpoint, and that reordering was the point

The plan had been a hardware watchpoint next. A snapshot diff turned out to answer the live question better and for a fraction of the cost: copy a region before the guest runs, compare afterwards, report which words changed.

A watchpoint says which byte, when, and from which instruction, and costs a debug register, a per-platform API and an exception per access. A snapshot says which bytes ended up different, for one memcpy. For "did anything ever fill this slot in?" the second is the whole answer and the first is a more expensive route to it.

It immediately produced the shape of the object the wall faults on:

0x4000019e9ca0  0 -> 0x00004000019765c8   an image pointer
0x4000019e9ca8  0 -> 0x0000000000004000   16 KiB
0x4000019e9cb0  0 -> (never written)
0x4000019e9cb8  0 -> 0x000001d3dd792bd0   a host heap address, different every run

Correcting an earlier claim of mine: I had said the watchpoint would be Windows-specific. The debug registers are an x86 feature - same silicon and semantics on Linux, only the API to set them differs - and this repository already #[cfg]-splits exactly that shape in platform.rs, fault.rs and report.rs. It is ordinary here, not exceptional, and saying otherwise made the option sound worse than it is.

POKE, and the seventh elimination

The unwritten slot at +0x10 was the obvious candidate for the zero base. ORBISTOUN_POKE writes a value at an absolute guest address after relocation and before entry - the absolute-address counterpart to ORBISTOUN_WRITE, which can only reach what an argument points at.

Poked with two different values: the fault did not move. And the watch confirms the poke survived untouched to the fault, so this is an elimination rather than a value that got overwritten - a distinction worth checking, because the two look identical from the fault address alone.

What is left, and it is not what anyone was looking for

Seven things eliminated at image+0xafc959: the stub return, unwritten stack, unwritten heap, arg0's target, arg5's target, memalign, and now the object's unfilled slot.

Which makes the surviving reading a different one entirely. rcx = 0xfffe0 is 0x100000 - 0x20 and rdx = 0x100000, and every candidate for a base that got lost is gone - so perhaps there is no lost base. Perhaps 0xfffe0 is a legitimate address that the real machine maps and this one does not. A guest writing near the top of a fixed low-memory region would look exactly like this, and the run already says the honest thing: an address in no region this run mapped.

Cheap to test, and the address space is ours: map a page there and see whether the guest proceeds. Recorded as a hypothesis rather than a finding - it has not been tried.

BSS_FILL works and is too blunt for this

Filling 1.25 MB of static data with 0xa5 moved the fault - to a different instruction, much earlier. So the guest does read uninitialised statics, and poisoning all of them breaks too much to isolate one. It stays because the answer it gives is real; it is simply the wrong instrument for a single slot, which is what WATCH and POKE are for.