D650 - The signal goes to another thread

Status: measured Date: 2026-09-09

The contract is now complete

REQ-20260909T1445Z-2a90 asked obSCEne to repeat the exception-handler probe with a real thread handle. Sweep 20260909-151910 answered every question D648 had to leave open:

call return
raise(self, 30) with a handler 0x0, and the handler's flag reads 1 on the next line
raise(self, 31) EINVAL, with a valid handle - so 31 really is refused
raise(1, 30) ESRCH
raise(self, 30) with no handler does not return - the process takes the signal and dies
a worker's raise(main, 30) while main is in the handler 0x0, handler re-entered on main

Delivery is synchronous. The flag settles it: the handler runs before the call returns. The handler is passed the signal number in rdi - 0x1e, exactly the value PPSA25872's own handler opens by comparing against - and a context pointer in rsi and rdx.

Three things follow and are implemented: the same-thread delivering call, via the call_guest path execute_once already uses; EINVAL for 31; and a new StopReason::Signalled, because a raise with no handler ends the process and neither Aborted nor Exited could say so without naming a call the guest never made.

And it did not move PPSA25872 at all

The title raises on thread 0x5e2d00000200 while running on 0x5e2d00001dc0. The target is not the caller. A collector suspending a mutator - stop-the-world, which is what the whole shape suggested from the beginning and what the trace never actually said until it was instrumented.

The measurement covers this case: the caller gets 0 and the handler runs on the target thread. Orbistoun cannot do that. Nothing here can interrupt a thread already running guest code - guest code executes natively, there is no interpreter loop to check a flag in, and the target is not inside an orbistoun implementation at the moment the signal is raised.

Why the branch refuses instead of answering zero

Answering 0 would move the wall today. It would also tell the collector that a thread had acknowledged a suspension it never received - and the collector's next act is to read that thread's registers and walk its stack, so it would proceed on state nobody produced. The failure would surface far from here, as a corrupt heap or a hang in a different place, and nothing would point back. That is the exact trade principle 3 exists to refuse, and it is worth naming that the temptation was real: this is the furthest-reaching title in the corpus and one 0 would have produced a visibly longer run.

The concept this needs, named rather than built

Cross-thread delivery is a mechanism, not a stub, so it is recorded here rather than assumed into existence. The shape it would take:

That last line is the honest limit and the reason this is a decision rather than a patch: a mutator spinning in pure guest code would never take the signal, and a collector waiting on it would hang exactly as it does now. It converts "never delivered" into "delivered at the next call", which is a real improvement and not the contract.