D579 - The mapping arena had no name and its bytes were unreadable
Status: measured Date: 2026-09-07
The blind spot, and what it was hiding
PPSA03416's wall is the platform's asynchronous file path: the guest
builds a command buffer describing the reads it wants and hands it to
sceKernelAprSubmitCommandBufferAndGetResult, which nothing
here serves (worklog 426). The buffer is the whole question - what it
holds is what an implementation has to understand - and the report said
this about it:
arg0 = 0x7400008a3520 -> no region this run mapped, and address-shaped
Two separate gaps, with one visible symptom.
orbistoun-thunk reads a pointee only from a span
published as readable, which is the safety precondition rather than a
preference: an argument that is not a pointer is usually a small
integer, and dereferencing it would fault inside the emulator.
The spans are published before the guest is entered - the image and the
main stack - and a mapping the guest makes at runtime is not
among them, so no bytes were ever dumped for one.
And report::locate names an address against five region
slots. Slot 3 was called other and nothing had ever
registered it, so even a readable arena address would have
printed as a bare number.
This is D387 again, one region over. Thread stacks had exactly this shape - published before entry, so every argument a threaded guest passed read as a wild pointer - and the fix there was to publish the span when it comes into existence. Guest mappings are the other half: they are where an allocator puts the structures a call is handed.
Published where a mapping is placed, not at each of the three sites
Three paths place guest mappings - direct memory, mmap,
and a reserved virtual range - and each ended in a bare
fill_mapping. Adding a second obligation as three more call
sites is a hazard this project has now paid for twice: a reporter wired
into the fault path and not the clean-exit path worked for a title that
crashed and not for one that stopped (worklog 425).
So mapping_placed is the one function that means a
mapping now exists, and fill_mapping is one of the
things it does. The fourth mapping path has one thing to call rather
than a list to remember.
Readable mappings only.
protection_from_guest answers read: false for
a guest that asks for write-only or execute-only memory, and publishing
that would let a dump read a page the host refuses - turning a
diagnostic into a fault with no relation to the guest.
The arena is named, and it is the arena rather than the mappings
Region::Mappings takes the dead slot, named
guest mappings.
A guest that names its own address is not counted. A
hint is honoured wherever it points (D459), and folding
0x5000_0000_0000 into the extent would stretch the arena
across nineteen terabytes nothing placed anything in, so every stray
pointer between the two would be named as a guest mapping.
arena_end_of refuses anything below
MAPPING_BASE, and the test asserts that refusal rather than
the acceptance beside it.
What is registered is the arena - where guest
mappings go - not the set of mappings. It spans the gaps between them
and the padding unit next_mapping_base leaves, so an
address inside it was not necessarily mapped. That is what the name
says, and a fault report states separately whether the address was
mapped, so the two together do not overstate. It is the concession D489
already made for the title's modules, for the same reason and at the
same granularity.
None rather than a zero-length span for an arena
nothing has used. A zero length is already how the reporter
spells an unused slot, so returning one would register the region and
leave every address in it unnamed anyway, with nothing saying which of
the two had happened. Watched failing: with the guard written
end < MAPPING_BASE the test gets
Some((base, 0)).
Registered late, and that limit is stated rather than discovered
Every other region is known before the guest is entered. This one
does not exist then and grows as the guest maps, so it is read in
collected_dumps - the one place its name is used, and one
that runs after the guest has stopped.
That is enough for an argument dump and not for the fault handler. A run killed from outside names a faulting address in the arena as a bare number. Said here because a region that is sometimes registered is exactly the kind of thing a reader would assume was always.
Here rather than on each of the ways a run can end, for the reason
above: that list is what a reporter keeps being wired into incompletely.
Which this change then found a third instance of -
guest_stopped called two of the three reporters by hand and
so was the site that missed paths_opened when it was added.
It calls what_the_guest_asked_for now, which is the
function that exists to stop exactly this.
What it showed on the first run
arg0 = 0x7400008a3520 -> guest mappings+0x8a3520 =
00 00 01 00 14 00 00 00 01 00 00 00 00 00 10 00
00 00 10 09 00 74 00 00 00 00 00 00 00 00 00 00
A legible structure where there had been a bare number, and it
carries a pointer back into the arena at +0x10. The
reading of those fields is not part of this entry - the change
is that they can be read at all, and inventing an interpretation here is
what principle 3 forbids one level up.
The wall did not move, which is the correct outcome: this observes
and changes nothing the guest can see. 193 imports, the same fault,
files 1 reads, 0 KiB.
What this does not establish
That every heap-shaped pointer is now dumped.
Nineteen values in the same run still report
no region this run mapped - host addresses the guest was
handed, and mappings made by paths that do not go through
mapping_placed if any remain. The count went from all of
them to some of them, and the remainder has not been enumerated.
Nor anything about the arena at fault time, per the limit above.
Nor that the arena's extent is what the guest can
read. It is where mappings were placed;
sceKernelVirtualQuery and region_containing
remain the authorities on whether a particular address is mapped, and
this adds no claim beside them.