Parents: #100, #628 Related: #649
Problem
BudgetAllocator.tryRecover correctly refuses synchronous collection while gc_runtime.allocationRecoveryBlocked() is true, because object/property/element backing allocation can hold trace-sensitive engine locks. It then returns OOM without retaining any recovery request. JavaScript can catch that OOM, release a large live graph, and retry at a safe statement boundary, but no collection is pending and the retry fails against the unchanged cap.
The promoted no-GIL case semantics/oom-one-thread.js proves this exact sequence. The hog catches pressure OOM, clears its ~192 MiB hoard, then its small recovery allocation escapes OOM. Allocation-free runner evidence from #649 shows id 1 threw while ids 2-4 completed normally; budget use remains 201326577 / 201326592 bytes.
Required implementation
- Extend the budget-recovery contract with an allocation-free deferred callback.
- When synchronous recovery is blocked, request Context GC atomically and still fail the current allocation closed.
- Service that request only at an existing safe interpreter boundary.
- Preserve the recursion guard and never invoke the collector under an allocator-internal or trace-sensitive lock.
- Do not raise the heap cap, add retries in JavaScript, or weaken the corpus witness.
Acceptance
- Unit coverage proves blocked allocation calls only the deferred callback, not the recovery callback, and a later safe allocation can recover.
- The unchanged
semantics/oom-one-thread.jspasses in no-GIL Debug and ReleaseSafe with the 192 MiB cap. - Existing trace-sensitive-lock and parallel allocation-recovery tests remain green under TSan.
- Full unit/TSan shards and no-GIL corpus gates have no new regressions.