Skip to content

ArenaTaskDirectory keys published tasks by spl_object_id(), which is one shared sentinel for every persisted object — wrong-task execution is reachable #35

Description

@lisachenko

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

  • The scenario above executes Y, not X — or refuses loudly at publishTask($x) with the remedy named.
  • Publishing two distinct request-heap tasks still yields two addresses (existing behaviour, pinned).
  • The @local-identity comment's assumption becomes enforced rather than documented.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions