D323 - Data imports get storage, not a stub

decided · 2026-08-27 · finishes what D307 deferred

D307 taught the loader to notice that an import names data. It still handed one a function stub, which is the wrong answer in the way that does not announce itself: the guest loads the slot, dereferences what it found, reads x86 instruction bytes as a pointer and carries on.

DataBlocks reserves one zeroed page per data import, in an address range clear of both the images and the thunk table, and ImportResolver asks it before the thunk table.

Why one page each, and why zero

One each because the guest writes to some of them - _Stdout is an object, not a constant - and two imports sharing storage would alias where nothing downstream could see it.

Zero for the reason process_argument_block is zero: the real contents are not known from any lawful source, so every field reads as zero rather than as something invented. A guest reading a pointer gets null and can check it. A virtual call through a null vtable faults immediately and says where - which is worth far more than executing whatever a stub happens to begin with.

The composition is the safety

An import that names code misses in the data blocks and falls through to its own stub, byte for byte as before. That is the property that matters, because it is what stops this change touching every guest at once, and it is asserted directly rather than assumed.

What is proven, and what is not

Proven, by test: each import gets distinct storage; it reads as zero; it accepts a write; the resolver prefers data; a function is unaffected.

Not proven: that this moves any guest. PPSA02664 did go further - image+0xafc959 to image+0xafcc08, two more distinct imports - but a learned policy entry changed in the same interval, and the run report said so itself:

! explicit stub answers went from 0 to 1, so this verdict measures a settings change

So the FURTHER is unattributable and is not claimed. This is a correctness fix justified by what it stops being wrong, not by a wall it moved - which is the distinction D226 draws, and the honest position when the only two candidate causes cannot be separated.

Isolating it needs a build with the storage off, which would mean making a correctness fix into a setting. That is worth doing only if something later depends on the answer.

A flake, introduced and removed

The first four tests all reserved at the shipped base. They run in parallel in one process, so whichever arrived second got Conflict - a failure about the tests rather than the code, and one that would otherwise have appeared intermittently in CI rather than immediately here. Each test now has a base of its own.