ReviewOS

zig-utils/zig-js

Native property caches: preserve inherited host get callbacks

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

Parent: #495; related: #471, #498. Found while auditing native frame/property guards after #1086.

Executed public-C reproduction

A JSClassRef getProperty callback increments a private counter for value. Initially it returns NULL (unhandled), so the host object's ordinary prototype supplies value = 7. The same function reads that host 64 times. The private state then makes the callback return 99, followed by one more unhandled read. Neither object shape nor prototype changes.

The identical C fixture was compiled against system JavaScriptCore and a freshly built zig-js library at 102da458:

system JSC:
warm=448 callbacks=64
handled=99 callbacks=65
unhandled=7 callbacks=66

zig-js:
warm=448 callbacks=7
handled=7 callbacks=7
unhandled=7 callbacks=7

The inherited cache bypasses the receiver's host get hook, even when it previously declined and must run again. Current source records a one-hop inherited data observation after the host callback returned unhandled. Generated code rejects host-owner objects, but the fallback inherited-cache helper consumes the observation without checking that receiver hook.

Required change

  • Publish/consume inherited data observations only when the receiver-to-holder lookup prefix is free of mandatory observable get behavior.
  • A host get callback that declined one read must execute on the next read; do not assume it remains unhandled or use shape equality as a callback-state version.
  • Retain safe ordinary own-data and ordinary inherited-data fast paths.
  • Pin the public-C/JSC transcript and test actual native cache fallback/frame safety, exception/order, GC, and shared-mode behavior. No new ABI or weaker callback contract.

Definition of done

  • The public-C reproduction exactly matches the system-JSC values and callback counts.
  • Canonical observations, native inherited-cache consumption, and native caller-frame proof all preserve host get behavior.
  • Focused Debug, ReleaseSafe, unsuppressed TSan and real-GC coverage passes; safe ordinary inherited caching remains exercised.
  • Public C ABI audits/fixtures and affected corpus path comparisons pass with scoped evidence; docs inventory remains accurate.
9 days ago

Fixed in a9dcaf24. Inherited-data observations are no longer published after a receiver host get, and native inherited-cache consumption independently declines such receivers. A callback's prior unhandled result is not a shape-versioned promise about future lookups. The native frame proof uses the same guarded cache path.

The permanent public C fixture now exactly matches system JavaScriptCore:

inherited host get: 448/99/123/7, 64/65/66/67 callbacks

The saved parent instead reports 448/7/7/7 and 7/7/7/7 callbacks. The added exception phase proves that the cache previously suppressed the callback's throw as well as its value and side effects.

Qualification:

  • New publication/consumption/frame-proof regression and precise-GC/shared-lookup regression, plus both existing ordinary inherited native-cache tests: all four functional cases pass Debug, ReleaseSafe, and unsuppressed TSan without allocator leaks. Full collection is asserted; live host data survives, and its owner finalizes exactly once at teardown. Ordinary inherited native reads still execute without runtime callbacks.
  • Public C fixture passes Debug, ReleaseSafe, and unsuppressed TSan. It is part of test-c-api; the complete C/C++ host suite passes.
  • The standard c-api-jsc-diff gate now includes this fixture and matches all 35 rows. Public C ABI audit remains 117 declarations / 117 complete / zero pending, with no signature, layout, or export changes.
  • All 403 affected upstream property/create/prototype/Reflect-get outcomes are byte-identical before/after, with zero failures and zero forced tree/VM divergences. Flips 0 test262 cases; this is scoped evidence.
  • All 19 repository-tool validators, dependency/quickening/GC inventories, 378-file link audit, and checked 51-page docs manifest pass.

Full local combined-unit linking remains blocked by #933's unchanged memory cap. Hosted integration is queued. No throughput claim is made.

Sign in to comment on this issue.