Skip to content

Threadfuzz: make microtask lifecycle accounting race-free #875

Description

@chrisbbreuer

Parent: #482
Related: #474, #493, #874

Root cause

The lifecycle fuzzer accumulated Promise-reaction completions for Thread.asyncJoin(), Atomics.waitAsync(...), and Lock.asyncHold(fn) with shared ordinary numeric += operations. Those reactions may run concurrently on different no-GIL settling or task-pump threads after their jobs settle. The counters therefore contained JavaScript program races, not engine synchronization witnesses.

The initially observed failures were both in the async-hold total and each lost exactly one unique marker:

  • seed 17: 923080 / 984084, delta 61004 (thread 2, iteration 4)
  • seed 26: 1804312 / 1845315, delta 41003 (thread 1, iteration 3)

Six immediate seed-17 reruns and a later instrumented 20-seed window did not reproduce the schedule collision. Source ordering nevertheless proves the assertion was invalid: runHoldJob releases the callback-form async hold before resolving the outer Promise, and its reaction performed the racy += afterward. A wider audit found the same invalid pattern in the async-join and wait-async reaction totals.

The no-function asyncHold() release total is different: each fulfillment reaction retains the live hold while updating its score and calls release() only afterward, so that ordinary total remains lock-protected.

Required correction

  • Accumulate async-join, wait-async, and callback-form async-hold sum/count pairs in a shared integer TypedArray using Atomics.add.
  • Keep validating each async-hold callback result against its unique expected marker.
  • Check every exact sum and completion count after run-loop quiescence so missing, duplicate, rejected, or wrong-value delivery remains a hard failure.
  • Preserve the ordinary release total and document why its live no-function hold provides synchronization.
  • Do not serialize engine task execution, move assertions into unrelated locks, weaken expected totals, retry failed seeds, add sleeps, or treat non-reproduction as proof.
  • Preserve the original failure evidence and link the test correction to Promises: bound host rejection queue draining #874, whose lifecycle gate exposed it.

Acceptance

  • Post-settlement shared totals contain no ordinary read-modify-write outside synchronization.
  • Exact atomic sum/count pairs detect missing, duplicate, and rejected async-join, wait-async, and async-hold completions; async-hold also validates every returned marker.
  • Seeds 17–36 pass as one 1,120-program lifecycle window.
  • Seeds 17 and 26 pass under suppression-free TSan.
  • The full 22,400-program lifecycle profile passes with zero functional failures.
  • The change is test-only and makes no runtime throughput or semantic claim.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions