D613 - A wait that did not wait, and a record that answered for somebody else
Status: measured Date: 2026-09-08
What the shared wall pointed at
image+0x39f7c is where both PPSA02664 and PPSA03416 now
stop (D612). The instruction is a structure walk -
mov rsi, [rsi+0x30] then
mov edx, [rsi + rdx*4] - and at the fault rsi
holds 0x542e2e00776f6c66, whose bytes are
flow\0..T. A pointer field holding string data.
The run report's own evidence names what happened immediately before:
just before: libkernel::sceKernelWaitEqueue(0x5e2d0000ee40) -> 0x0
just before: libkernel::sceKernelGetProcessTimeCounter(0xf4) -> 0x2349bfe
And dumping that call's arguments:
arg0 = 0x5e2d0000ee40 the queue
arg1 = 0x610084800f80 the event array - all zeroes
arg2 = 0x1 one event wanted
arg3 = 0x610084800fa4 where the count goes
arg4 = 0x0 no timeout: wait indefinitely
The guest asked to wait indefinitely for one event and was
told, immediately, that it had succeeded. The array it passed
was never written. sceKernelWaitEqueue collected whatever
happened to be pending - nothing - and returned OK
regardless.
That is kevent(2), which blocks until at least one event
is ready or the timeout elapses. It is also the D171 shape exactly: an
out-parameter left as the caller set it, under a success code.
The cost, in one run: 3,853 waits against 44 flips, from two call sites. The guest loops until an event arrives, never blocks, and reads an all-zero event structure every time round.
The fix
GuestEqueue gets a condition variable,
post_event signals it, and wait_events blocks
while the queue is empty - Blocking::Forever for a null
timeout pointer, Blocking::Until for a real one. Nothing
arriving in time answers ETIMEDOUT rather than success, and
the constant is now shared with the event-flag wait instead of being
declared twice for one condition.
take_events is
wait_events(…, Blocking::Never), so the non-blocking
readers are unchanged and there is one implementation rather than
two.
It did not move the wall, and that is worth saying
plainly: 197 distinct imports either way, the same fault at
image+0x39f7c. What it fixed is a wrong answer, which is
its own reason.
And then the trace started lying
With the wait actually waiting, the same evidence line read:
just before: libkernel::sceKernelWaitEqueue(0x5e2d0000ee40) -> 0x74000086ec30
sceKernelWaitEqueue cannot return that. It returns
OK, an errno-encoded vendor code, or the invalid-argument
placeholder - never a mapping address. And the value changed between
runs (0x74000086ec30, 0x74000086e9f0) while
staying inside MAPPING_BASE, which is what a memory-mapping
call answers.
The call record is a ring, and RING_RETURNED[slot] is
the flag that says whether RING_RET[slot] holds a real
answer yet. D571 fixed the writing side: a call that returns
after its slot has been recycled must not store its answer there.
Nothing cleared the flag when a slot was taken. So a
call still running in a recycled slot reported the answer of whichever
call held that slot last.
Invisible while every call returned promptly - the window was a few instructions wide. A call that blocks for the length of a wait holds the slot open, and the defect walks straight into the one record a person reads at a wall.
One store fixes it, ordered Release before the sequence
is published so a reader seeing this call's number can never still see
the last call's flag. The line now reads:
just before: libkernel::sceKernelWaitEqueue(0x5e2d0000ee40) from 0x400000f53413
No arrow, because there is no answer yet. Which is what
recorded_return's documentation has promised since
D459.
The pattern, twice in one iteration
Both of these are a value that was available being reported as a value that was established. The wait had nothing to deliver and said it had delivered; the ring had no answer and produced one. Neither was a wrong calculation - both were a missing "I do not know yet", and in both cases the honest state was already representable and simply not written.