D322 - the futex is called rather than censused, and every wait runs on a thread nobody joins

assumed - 2026-09-07

Two sections were added for the sibling emulator's two open premises about threading: 031-stackattr, for what a thread attribute set says about a running thread's stack, and 032-syncaddr, for the platform's futex. Both promote names out of the census into real declarations, and the second has to call something that blocks. This records the four choices that carry risk, because in each of them the obvious approach is wrong.

1. The futex is called, and a wait cannot be written as a try form

The no-block rule is absolute in this program and has been paid for twice: a probe that hangs loses every check behind it. 016-syncbounds honoured it by measuring sceKernelWaitSema through the non-blocking sceKernelPollSema (D321).

There is no Poll counterpart to a futex wait. Blocking is the function, and the pair that surrounds it is the whole measurement - a wait nothing releases has not been observed working. So the rule cannot be satisfied by choosing a different symbol, and the choice is between not measuring the busiest pair on the platform and measuring it another way.

The other way already exists here. 015-sync/condvar-wakes-a-waiter puts its one unbounded call on a thread nobody joins, advances a volatile counter through known states, and lets the main thread observe with sleeps - "if it never returns, this thread stays here and the run does not". 032-syncaddr uses exactly that arrangement for all three of its waits: the main thread only ever sleeps, reads the counter, and calls the wake, which cannot block.

So the rule is honoured in substance - nothing that can block is on the path the suite depends on - while the letter of "written as the try form or not at all" does not apply to a function with no try form. That is the reviewable call, and it is why this entry is assumed.

2. The comparison width is measured by a case where blocking is an answer

Whether the wait compares 32 bits or 64 is the sharpest open question: the sibling models it as 64-bit from an export layout in which the wait shares an entry point with the Wait64 spelling, and has no way to check.

The discriminating case is a word whose high half differs from the value passed and whose low half matches. A 64-bit comparison returns at once; a 32-bit one blocks. Ordinarily "it blocks" would be an unusable outcome - but on the worker thread it is simply the other reading, and the main thread releases it with the wake it has already proved works. Both answers are recoverable, which is what makes the measurement safe rather than a gamble.

The check reports 64 or 32 and grades neither. It was validated both ways: against a 64-bit host stub it reports 64, and against a deliberately 32-bit one it reports 32 and records the worker's release.

3. No timeout is declared, so none is probed

_umtx_op has a timeout slot and the wrapper may carry one. The sibling refuses a non-zero third register rather than guess at its unit, and every guest call anybody has observed leaves that register zero.

Declaring a third parameter to probe it would be declaring an arity nothing establishes, which principle 2 forbids - and unlike the width, a timeout cannot be measured by a case where both outcomes are safe: the measurement is how long it waits, so getting the unit wrong is either a wait that returns instantly and says nothing or one that outlives the run. It is left out and recorded in docs/backlog/024 for a session that can bring a known unit to it.

4. The stack address is placed against the frame that asked, not against a constant

scePthreadAttrGetstackaddr hands back an address, and whether that is the lowest byte of the stack or its top differs by the whole size of the stack. The sibling answers FreeBSD's convention and records it as an assumption; three retail titles compute a garbage collector's scan bound from it, and got that bound from a placeholder for months (its D575).

The check takes the address of one of its own locals - inside the asking thread's stack by construction - and asks which side of the reported address it falls on. Inside [address, address + size) is a base; inside [address - size, address) is a top; neither is graded a failure, because a region that does not contain the frame asking about it is wrong under both conventions.

No constant is invented and no layout is assumed; the arithmetic is the whole method. sceKernelIsStack reports bounds of its own and is consulted as a second witness where it resolves, but nothing is asserted on it - platform.h is explicit that those two out-pointers are only almost certainly the region's bounds.

What the host build could and could not settle

All seven checks pass against host stubs written for the occasion, and each was then made to fail on purpose, which is the part that makes them evidence:

break what the check did
the wake does nothing wake-releases-a-waiter failed naming the state the worker stopped in, compare-width skipped rather than reporting a width it could not observe, and the run completed
the comparison reads 32 bits compare-width reported 32 and recorded the worker's release
the address is the stack's top address-is-the-base reported a top
the region contains no frame address-is-the-base failed, naming the address

A stub can implement only one convention, so the host proves the mechanism and the arithmetic, never the platform's answer. Which convention each stub picks is stated in its comment rather than left for a reader to infer.

Adding two sections also overran OBS_SCREEN_MAX, and the compile-time assertion D259 put in registry.c caught it. It was raised from 48 to 56 - what two columns hold, by screen.c's own note, rather than arbitrary headroom, so the next session past it has to write the third column that note describes.