Found during the #15 design pass while tracing how addressOf() routes between the published table and the persist route; still present on main (src/Parallel/ArenaTaskDirectory.php, the $key = spl_object_id($task) lines in register() and addressOf()).
The hole
$publishedAddresses is keyed by spl_object_id(). The file's own @local-identity comment states the assumption that makes this correct: "a published task is an ordinary request-heap object that never enters the arena." Nothing enforces that assumption — and the substrate makes its violation maximally confusable: every shared object's handle field is deliberately overwritten with SHARED_HANDLE_SENTINEL (0xFFFFFFFF), so every persisted instance reports the same spl_object_id(). The substrate documents this precisely so that "nobody builds identity on it"; this map does.
The failure, concretely
$x = $runtime->persist(new RenderJob(1)); // shared: spl_object_id($x) === sentinel
$y = $runtime->persist(new RenderJob(2)); // shared: spl_object_id($y) === sentinel
$runtime->publishTask($x); // files X under key 0xFFFFFFFF
$runtime->spawnParallel($y); // addressOf(Y): publishedAddresses[0xFFFFFFFF] hits
// → returns X's published address → Y silently runs X
No error anywhere; the second spawn executes the first task and returns its result. publishTask() of two different shared tasks is the same collision from the other side (register() is "idempotent" across two distinct objects). Persisting a task before spawning it is not exotic — the #15 acceptance test itself spawns pre-persisted instances (they take the persist route today only because nothing was published; one published shared task anywhere in the process arms the trap for every later spawn).
Remedy
register() and addressOf() should detect a shared instance before touching the local-identity map — SharedArena::addressOf() / the substrate's addressOfInstance() answering non-null is the honest predicate (checking the sentinel id also works but builds on the very value the substrate says to never build on). Then either:
- refuse publication of a shared instance with the remedy named — publication exists to make a request-heap object reachable by fork inheritance; a shared instance already has a family-wide address and gains nothing from it; and
- route
addressOf() of a shared instance straight to the arena address — which is already correct and idempotent via persistInstance().
Acceptance criteria
Found during the #15 design pass while tracing how
addressOf()routes between the published table and the persist route; still present on main (src/Parallel/ArenaTaskDirectory.php, the$key = spl_object_id($task)lines inregister()andaddressOf()).The hole
$publishedAddressesis keyed byspl_object_id(). The file's own@local-identitycomment states the assumption that makes this correct: "a published task is an ordinary request-heap object that never enters the arena." Nothing enforces that assumption — and the substrate makes its violation maximally confusable: every shared object's handle field is deliberately overwritten withSHARED_HANDLE_SENTINEL(0xFFFFFFFF), so every persisted instance reports the samespl_object_id(). The substrate documents this precisely so that "nobody builds identity on it"; this map does.The failure, concretely
No error anywhere; the second spawn executes the first task and returns its result.
publishTask()of two different shared tasks is the same collision from the other side (register()is "idempotent" across two distinct objects). Persisting a task before spawning it is not exotic — the #15 acceptance test itself spawns pre-persisted instances (they take the persist route today only because nothing was published; one published shared task anywhere in the process arms the trap for every later spawn).Remedy
register()andaddressOf()should detect a shared instance before touching the local-identity map —SharedArena::addressOf()/ the substrate'saddressOfInstance()answering non-null is the honest predicate (checking the sentinel id also works but builds on the very value the substrate says to never build on). Then either:addressOf()of a shared instance straight to the arena address — which is already correct and idempotent viapersistInstance().Acceptance criteria
publishTask($x)with the remedy named.@local-identitycomment's assumption becomes enforced rather than documented.