Parent: #482 Related: #887, #493
Problem
The optional max_js_threads admission check reads every ThreadRecord.done field while holding Gil.api_lock. Completion writes that field while holding the record's different join_mutex.
At a8f1b3c:
These locks do not establish a happens-before edge between a completing worker and a concurrently admitting caller under no-GIL execution. This is an engine-state synchronization defect. The cap is currently a TestingOptions knob; this finding does not claim a production resource-limit bypass or a measured TSan failure. It was identified by source review during #887 validation.
Required implementation
- Read completion through the existing record synchronization, preserving the API-lock transaction for cap check, ID claim, and record insertion.
- Verify the API-lock/record-lock order against GC tracing, join settlement, spawn failure, and teardown. Do not introduce a global execution lock or a suppression.
- Establish the intended live boundary (
doneversusexited) from the existing contract before changing policy. Do not quietly change the cap's meaning while fixing the race. - Preserve ID-space checks and allocation/spawn failure rollback, including the startup/queue reservations introduced by #887 and #886.
Acceptance
- A deterministic bounded witness exercises capped admission concurrently with completion and passes suppression-free TSan.
- The chosen live-boundary semantics, successful admission/refusal, and unique IDs are checked exactly in GIL and no-GIL modes.
- Allocation/spawn failure does not leave an admission or startup reservation behind.
- Applicable unit, lifecycle, and GC checks pass; no new Thread limit, retry of JS callbacks, or race suppression is used.