ReviewOS

zig-utils/zig-js

Runtime: bring property Atomics waiter metadata under heap caps

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

Goal

Make property-mode Atomics.wait / Atomics.waitAsync waiter metadata obey Context.Options.heap_limit_bytes instead of allocating from the process page allocator. This is a resource-control/security hardening follow-up from #30.

Current evidence

src/jsthread.zig currently defines:

  • const prop_alloc = std.heap.page_allocator;
  • sync property wait tickets allocate duplicated keys and PropTicket records from prop_alloc;
  • property waitAsync allocates duplicated keys, PropAsyncTicket records, settlement scratch, and waiter-table backing storage from prop_alloc;
  • gc.zig traces live g.prop_async tickets under Gil.prop_mutex, marking the target object and promise.

Because Context.createWith(.{ .heap_limit_bytes = n }) wraps the context allocator (context_gpa) rather than std.heap.page_allocator, property waiter metadata can grow outside the configured cap. This is separate from the current trace-sensitive-lock work: the current allocator bypass means budget recovery is not involved at all.

Implementation constraints

  • Prefer a context-owned allocator for property waiter metadata, likely via Gil, so the storage lifetime remains tied to the realm and Context.destroy can free abandoned tickets.
  • Preserve the current cross-thread settlement lifetime rules: async tickets may be settled by notifying or polling peers, and teardown may abandon tickets by owner.
  • Be careful with allocation order/rooting. Moving allocations under context_gpa may allow heap-cap recovery at sites that currently cannot recover; any live JS objects created before a recoverable allocation must be rooted or allocated after the native metadata.
  • Keep Gil.prop_mutex lock-aware: waiter-table growth is traced under the same mutex and should either fail closed while held or prove a safe recovery point.

Acceptance criteria

  • Property waiter/ticket/table allocations no longer bypass heap_limit_bytes for normal threaded contexts.
  • Focused positive/negative tests cover capped contexts and preserve existing property Atomics.wait / waitAsync semantics.
  • Trace-sensitive/fail-closed policy is documented for property waiter table growth if recovery can be triggered there.
  • Existing no-GIL, .gil = true, and teardown/settlement tests remain green.

Related: #30, #24, #1.

9 days ago

Implemented the first #37 slice on main:

  • d27eed6f runtime: budget property waiters

What changed:

  • Added Gil.prop_alloc, defaulting to std.heap.page_allocator for standalone unit-test Gil values.
  • Threaded Context creation now installs context_gpa into Gil.prop_alloc, so heap_limit_bytes covers property-mode Atomics waiter metadata.
  • Routed property Atomics.wait / waitAsync key copies, sync tickets, async tickets, waiter-table backing storage, timeout/notify settlement scratch, and teardown cleanup through g.prop_alloc.
  • Kept property waiter table growth trace-sensitive while Gil.prop_mutex is held, because gc.zig traces prop_async by taking that same mutex.
  • Moved pending-path waitAsync native metadata allocation ahead of pending promise/result object creation, reducing recovery exposure for freshly-created JS objects.
  • Added focused witnesses for Gil.prop_alloc wiring and trace-sensitive property waiter queue growth.

Local validation:

  • zig build test -Dtest-filter="property wait" --summary all
  • zig build test -Dtest-filter="trace-sensitive" --summary all
  • zig build test -Dtest-filter="Atomics.waitAsync" --summary all
  • zig build test -Dtest-filter="property Atomics" --summary all
  • bun run docs:build
  • git diff --check

CI for the pushed head is running here: https://github.com/zig-utils/zig-js/actions/runs/29217197628

9 days ago

Pushed a small #37 cleanup follow-up to main:

  • 592b1e20 runtime: clean unqueued waitAsync tickets

What changed:

  • The property Atomics.waitAsync pending path now uses a normal scoped cleanup for the native ticket until it is actually published into Gil.prop_async.
  • This covers the lock-revalidation not-equal success branch as well as errors: if another thread changes the property between the initial check and the locked re-check, the already-allocated key/ticket pair is now freed before returning { async: false, value: "not-equal" }.

Local validation:

  • zig build test -Dtest-filter="Atomics.waitAsync" --summary all
  • zig build test -Dtest-filter="property wait" --summary all
  • git diff --check

CI for the pushed head is running here: https://github.com/zig-utils/zig-js/actions/runs/29217318474

9 days ago

Pushed a focused #37 acceptance-test slice to main:

  • b32e804c test: cover budgeted property waiters

What changed:

  • Added property waiter metadata counts against heap budget, which creates a heap-capped threaded Context, allocates a property waitAsync ticket through the real Gil.prop_alloc path, appends it to Gil.prop_async, and asserts heapBudgetStats().used_bytes increases.
  • The ticket is left in the normal async waiter table so Context.destroy() exercises the real owner/abandon cleanup path.

Local validation:

  • zig build test -Dtest-filter="property waiter metadata counts against heap budget" --summary all
  • zig build test -Dtest-filter="property wait" --summary all
  • git diff --check

CI for the pushed head is running here: https://github.com/zig-utils/zig-js/actions/runs/29217419255

9 days ago

Acceptance-gate update: the implementation and focused tests remain complete on b32e804c, but its combined main run hit an unrelated threadfuzz-midgc worker-close oracle race. That harness race is fixed in 31313a8; the fresh combined gate covering the full #37 stack is https://github.com/zig-utils/zig-js/actions/runs/29218852375.

I am keeping #37 open until that gate is green rather than closing it against the failed predecessor run.

9 days ago

CI routing update: the 31313a8 gate is now superseded by the adjacent #30 root-registry recovery hardening push. The current combined main gate containing the full #37 implementation and acceptance witness is https://github.com/zig-utils/zig-js/actions/runs/29219121325.

9 days ago

CI routing update: the prior combined run was superseded by the #12 shared-identity hardening push. The current main gate containing the complete #37 implementation and acceptance witness is https://github.com/zig-utils/zig-js/actions/runs/29219431086.

9 days ago

CI routing update: the identity-hardening gate was superseded by the structured-clone SAB token security push. The current combined main gate containing the complete #37 implementation and acceptance witness is https://github.com/zig-utils/zig-js/actions/runs/29219914395.

9 days ago

Accepted and complete. Commits d27eed6, 592b1e2, and b32e804 bring property Atomics waiter metadata under the Context heap budget, clean unqueued waitAsync tickets, and add the focused budget/cleanup witness. The combined production gate is green: 31 jobs succeeded, 1 nightly-only job was skipped, and 0 failed: https://github.com/zig-utils/zig-js/actions/runs/29219914395

Sign in to comment on this issue.