ReviewOS

zig-utils/zig-js

Private ABI: retain and relocate pending and cached exception cells

#908
Closed chris opened this 9 days ago ·
9 days ago

Parents: #495, #477 Related: #896, #888, #480

Source finding

At 656f4d81, private exception state has several aliases:

  • CContextGroup.pending_exception points to a stable native Boxed exception, containing both .value and .exception_encoded (a separate encoded value handle).
  • privateSetPendingValue / JSC__VM__throwError copy the thrown value into Context.private_pending_exception_root.
  • Binding.relocateRoots rewrites that Context copy, but the exception box and its encoded-value box have no corresponding relocation edge unless independently protected.
  • privateMaterializeTermination caches group.termination_exception; clearing the pending exception drops the Context root while retaining that cache for later reuse.

The canonical private object-box map is explicitly weak. Reconciliation of that map is not a substitute for tracing and rewriting these VM-owned exception aliases.

This is a source-audit finding, not a measured dangling dereference. Arena-only groups do not move or reclaim their managed values; deterministic witnesses must use the supported precise-heap profile and obey its actual host-handle protection contract.

Required work

  • Reproduce pending-exception identity across a real moving nursery collection and compaction, with native-stack scanning disabled.
  • Reproduce cached termination lifetime across pending clear, full collection, and a later termination request.
  • Establish one durable ownership/relocation contract for the VM-owned exception, the encoded value returned by JSC__Exception__asJSValue / JSC__JSValue__toError_, and any explicitly protected exception handles supported by the pinned profile.
  • Preserve first-pending-exception identity, sibling-realm ownership, retirement, clear/take behavior, and exact release at teardown. Do not mutate an already exposed exception to represent an unrelated later failure.
  • Keep allocation-failure admission work in #896 coordinated with this ownership fix. Do not rely on fallible root registration after publishing an exception, conservative stack retention, retaining all cells, disabling movement, or an emergency pool.

Acceptance: deterministic native/engine allocation faults where relevant, actual nursery/major/compaction witnesses, precise private ABI lifecycle fixtures, full units, and suppression-free TSan. Link measured failures and implementation commits separately from this source finding.

Security scope is engine state and lifetime integrity in the pinned embedding profile; no embedding sandbox or permission policy is involved.

9 days ago

Implemented the exception-slot ownership slice in 1fc2ff96, pushed to main.

Measured baseline evidence, separately from the earlier source finding: the two new witnesses against 8d7a3b7a both failed. The pending exception box still held its pre-nursery address, and full collection reclaimed the cached termination object after pending clear. Native-stack scanning was disabled. The implemented tests now assert actual nursery movement counters and actual compaction, not just a successful collection call.

The fix roots the real exception and encoded-value slots, retains the termination cache independently, and traces/relocates both aliases for counted private exception protection. Publication of existing exception handles remains allocation-free. Foreign-VM exception rethrow is refused without publishing a root.

Validation completed:

  • Four focused exception-root tests in Debug, ReleaseSafe, and suppression-free TSan: 4 passed, 0 failed, 0 leaked in each mode.
  • Home private value fixture and both Home/Bun private global-lifecycle fixtures in all three modes. Home reports 392/392 fixture symbols linked and its runtime matrix passed.
  • Four existing realm-root relocation tests.
  • Public C, Home private, and Bun private ABI audits; checked documentation build.

Full sharded Debug units are now running. This issue stays open: final-external-owner teardown still needs a deterministic audit/fix, and broader validation is outstanding. In particular, source review suggests a pending value from a retired sibling can keep the last internal group reference alive after all external owners leave; the new test currently proves balanced final-unprotect retirement, not that separate shutdown case.

Validation also exposed the separately measured VM termination defect in #909. The Home watchdog fixture's success is not evidence of prompt interruption; #909 records why its current assertion is insufficient.

9 days ago

Completed across three implementation commits:

  • 1fc2ff96 established traced and relocated pending/cached exception slots and counted exception protection.
  • fd24068f completed VM-owned native exception/projection storage, semantic source-realm retention, final-external-owner cycle drainage, and protected-handle VM ownership.
  • 0f2b38f5 preserved source-context root-table ownership for ordinary public wrappers while retaining VM-owned native storage and exact exception projection identity.

Measured failures added during the final slice:

  • A sibling-realm public projection produced an exception whose native owner remained the source Context instead of the VM owner.
  • A pending sibling exception retained nonzero tracked allocator bytes after the final context and explicit group owner were released.
  • A protected primary-realm exception did not retain its VM: tracked live bytes were already zero before final unprotect.
  • The first full sharded run exposed a related public-root regression: private global lifecycle creates isolates and consumes exact ownership expected zero retired realms and found one (2,434 passed, 1 skipped, 1 failed, 0 leaked). The follow-up separates VM native storage from source-context protection ownership.

Final validation on 0f2b38f5:

  • Five moving/cache/identity exception tests plus both final-owner allocator witnesses pass in Debug, ReleaseSafe, and suppression-free TSan, with 0 leaks.
  • The protected exception witness performs actual compaction with native-stack scanning disabled, preserves the exact encoded projection, retires the sibling realm, and drains it on final unprotect. The projection is protected before publication to cover pre-existing source-root ownership.
  • The exact global-lifecycle regression passes in Debug, ReleaseSafe, and suppression-free TSan.
  • Home private value shims pass in all three modes: 392/392 fixture symbols linked and the runtime matrix passed. Home reports 447/447 implemented private symbols; Bun reports 437/437.
  • Home and Bun private global-lifecycle fixtures pass in Debug, ReleaseSafe, and suppression-free TSan.
  • Full sharded units: 2,435 passed, 1 skipped, 0 failed, 0 leaked, 584.9 seconds across ten shards.
  • Documentation manifest regenerated intentionally; the checked 50-page docs build passes.

No test262 run was used as evidence because this work changes the pinned private ABI lifetime boundary rather than JavaScript language semantics.

Sign in to comment on this issue.