ReviewOS

zig-utils/zig-js

threadfuzz: property waitAsync notify loses ready waiter

#1028
Closed glennmichael123 opened this 8 days ago ·
8 days ago

Executed reproducer

zig build threadfuzz \
  -Dfuzz-lifecycle=true \
  -Dfuzz-iters=4 \
  -Dfuzz-seed=1027 \
  -j1

This deterministically reports four failures from 224 generated lifecycle programs:

seed 1027: mixed waiter race threw before teardown point: Error: property notify woke wrong count pn0
seed 1028: mixed waiter race threw before teardown point: Error: property notify woke wrong count pn0
seed 1029: mixed waiter race threw before teardown point: Error: property notify woke wrong count pn0
seed 1030: mixed waiter race threw before teardown point: Error: property notify woke wrong count pn0
threadfuzz lifecycle: 224 programs from seed 1027, 4 failures

The failure is at conformance/threadfuzz.zig:5618: after every generated waiter has incremented the shared ready count, Atomics.notify(gate, "pn0", 1) returns a count other than one. The script therefore throws before reaching its intentional teardown throw.

Exact-parent control

The same command was run from archive snapshots with identical Zig, zig-gc, and zig-regex inputs on:

  • exact parent b4f5544d61036e296ebd0046579d0b991b509025;
  • current main d55b4456 (including #1027).

Both produce the same four seeds, message, and 224/4 summary. This is therefore pre-existing and not caused by #1027's scratch-accounting changes.

Control

The deterministic no-GIL correctness profile passes:

threadfuzz verify: 8 programs from seed 1027, 0 failures
threadfuzz verify under unsuppressed TSan: 4 programs from seed 1027, 0 failures

Required resolution

  • Determine whether property-backed Atomics.waitAsync publishes its waiter ticket after returning, or whether readiness/notify has another ordering defect.
  • Preserve typed-array and property-backed waiter semantics; do not weaken the generated assertion or hide the failure with timing sleeps/retries.
  • Add a focused deterministic regression test, then make the lifecycle seeds pass normally and under unsuppressed TSan.

Found while closing #1027; tracked separately because exact parent reproduces identically.

8 days ago

Implemented and pushed to main in 32b0611.

Root cause: finite property-backed Atomics.waitAsync owners slept directly until their deadline while keeping a runtime runnable slot. On this 11-logical-CPU host the automatic budget is 10 slots, while the lifecycle case creates more than 10 workers. The earliest property waiters therefore occupied every slot until their 5-second deadlines, preventing later workers from reaching the readiness barrier; notification then correctly found that pn0 had already expired.

The property path now parks on the realm's existing sleepable progress condition. It publishes GC-safe parked state, releases its runtime scheduler slot through the normal blocking scope, and wakes after a notifier has published promise settlement. Generation sampling plus deadline revalidation closes the notify-before-park window. Deadline scans are scoped to the current interpreter's microtask queue, and the no-waiter event-loop path takes no additional lock.

Evidence:

  • clean baseline: lifecycle seeds 1027-1030 produced 3 failures in 224 programs (wall-ms 51941, 78.97 s command wall);
  • final Debug: 224/224, 0 failures (wall-ms 24929, 53.29 s command wall);
  • final ReleaseSafe: 224/224, 0 failures (wall-ms 16559);
  • final unsuppressed TSan: 224/224, 0 failures and no race report (wall-ms 132149);
  • focused property-waitAsync module filter: 8/8 normal and 8/8 TSan;
  • deterministic one-slot regression: passes in both no-GIL and serialized modes, normal and TSan;
  • current-main full unit suite: 2,568 passed, 1 skipped, 0 failed, 0 leaked across 10 shards.

The test262 corpus is absent in this checkout, so no test262 flip claim is made.

8 days ago

Follow-up landed on main in cce66bb.

The current four-seed failure needed a two-part correction beyond the earlier property-scheduler fix:

  1. The mixed lifecycle generator gave its exact-notification cohorts a 5-second timeout while waiting for every generated worker to publish readiness. Instrumentation showed pn0 was published correctly, then expired about 0.88 ms before notification after the full mixed cohort took longer than five seconds. Notification and timeout behavior are now separate cohorts: exact-notification waiters have no deadline, while the dedicated timeout cohorts retain finite deadlines and assertions.
  2. Removing that harness deadline exposed a real native typed-array lifecycle defect on seeds 1029 and 1030. A spawned Thread final checkpoint treated $262.agent's live_agents == 0 as proof that an infinite native ticket could never be notified, abandoned it, and exited before the peer Thread notified. Spawned-Thread checkpoints now keep infinite native tickets parked on the waiter-table condition until notification or Context termination. Ordinary host checkpoints retain their existing abandonment policy. Context termination release-publishes its stop word and broadcasts allocation-free to parked harvesters.

Deterministic coverage now requires one readiness barrier to publish eight property-backed and eight typed-array waiters, then requires exact 8 notification counts for both domains in serialized and parallel shared-realm modes. Native waiter tests separately pin infinite-ticket notification and termination cleanup.

Verification on the real collector:

  • focused mixed-domain unit test: pass normally and under unsuppressed TSan;
  • native infinite-ticket notification test: pass normally and under TSan;
  • native interruption/cleanup test: pass normally and under TSan;
  • lifecycle seeds 1027–1030: 224 programs, 0 failures in Debug, unsuppressed TSan, and ReleaseSafe;
  • verify profile: 8/8 in Debug and ReleaseSafe, 4/4 under unsuppressed TSan;
  • broader waitAsync unit cluster: 16/17 pass normally and under TSan, with zero leaks and no sanitizer report.

The remaining unit-cluster failure is the pre-existing $262.agent startup case Atomics.waitAsync: not-equal sync, timeout, and cross-agent notify settle. Exact parent 3c139f9f reproduces the same isolated failure, and both parent and current main also park indefinitely in real agents: broadcast rendezvous, blocking wait, notify, report because no agent thread starts. That parent-shared scheduler/environment blocker is unrelated to cce66bb and is not reported as green here.

8 days ago

The exact-parent agent-startup blocker documented in the earlier follow-up is now resolved by #1075, with the final realm-prototype correction in e9400ed6.

That completes the previously partial focused result:

  • complete waitAsync unit cluster: 17/17 normally, 0 failed, 0 leaked;
  • complete waitAsync unit cluster: 17/17 under unsuppressed TSan, 0 failed, 0 leaked;
  • real-agent rendezvous and cross-agent waitAsync notification both pass normally and under unsuppressed TSan;
  • lifecycle seeds 1027-1030 remain recorded at 224 programs, 0 failures in Debug, ReleaseSafe, and unsuppressed TSan;
  • verify profile remains recorded at 8/8 Debug, 8/8 ReleaseSafe, and 4/4 unsuppressed TSan.

The agent-realm defect reproduced on the exact parent of cce66bb7, was tracked separately rather than attributed to #1028, and is now fixed on main. No sleeps, retries, weakened assertions, or source-specific fallback were added.

Sign in to comment on this issue.