ReviewOS

zig-utils/zig-js

Native calls: preserve caller frames across captured-value coercion

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

Parent: #498; related: #468, #491. Discovered while qualifying #1085.

Executed reproduction

Node and forced tree-walker return [114,1]. Required bytecode with native execution enabled throws TypeError: null is not an object (evaluating 'caller.arguments[0]') on both the saved parent production binary and the #1085 cleanup binary:

var capturedLeaf;
var replaceCapture;
function makeLeaf() {
  var captured = 1;
  capturedLeaf = function(delta) { return captured + delta; };
  replaceCapture = function(value) { captured = value; };
}
makeLeaf();
function caller(delta) { var result = capturedLeaf(delta); return result; }
for (var i = 0; i < 64; i++) caller(7);
var calls = 0;
replaceCapture({valueOf:function(){calls++;return caller.arguments[0]+100;}});
var result = caller(7);
JSON.stringify([result,calls]);

The coercion callback must see the caller's active arguments object and execute once. The existing native safety classifier accepts a bounded numeric leaf containing a captured-local load while checking only incoming arguments and direct-data property access. A mutable captured value can become an object whose arithmetic conversion re-enters JavaScript, exposing the skipped caller activation. This classifier explanation is an inference from the executed failure and the current call path, to be verified by focused native-entry witnesses.

Required change

  • Make activation elision conditional on a complete no-reentry proof for the callee's live inputs. Unknown or mutable captured state is not numeric merely because its bytecode participates in arithmetic.
  • Audit the same proof for receiver property values, current arity, and inherited/observable access; execute each claimed failure before expanding scope.
  • Retain exact caller/arguments identity, mapped parameters, coercion and exception order, steps, hooks, GC roots, and no-GIL behavior.
  • Preserve genuinely safe numeric native entries. A guarded miss must occur before any observable effect and use ordinary activation execution.

Definition of done

  • The executed reproduction matches Node under forced tree, required VM with JIT off/on, and explicitly exercised direct native entry.
  • Positive numeric controls retain native hits; reentrant cases retain correct frames without duplicate effects.
  • Focused Debug, ReleaseSafe, unsuppressed TSan, real-GC, and no-GIL coverage passes.
  • Affected saved-parent/current corpus path sets and tree/VM outcomes are compared with nonzero denominators; scoped evidence is posted.
9 days ago

The same failure is now executed for a receiver property value, with no scope inferred from static reading:

var receiver={bias:1,leaf:function(delta){return this.bias+delta;}};
function caller(delta){var result=receiver.leaf(delta);return result;}
for(var i=0;i<64;i++)caller(7);
var calls=0;
receiver.bias={valueOf:function(){calls++;return caller.arguments[0]+100;}};
var result=caller(7);
JSON.stringify([result,calls]);

Node/tree return [114,1]; warmed required bytecode throws the same null-arguments TypeError. Both captured and receiver variants return [114,1] in cold required bytecode (zero warmup calls) on both the saved parent and #1085 production binaries. This narrows the failing boundary to warmed native activation elision; both live-input sources need the same no-reentry proof.

9 days ago

Fixed in 102da458. Numeric bytecode alone no longer proves a captured or receiver value free of reentry. Activation elision now requires copied Number arguments and no such mutable input reads. Unsafe native operations miss before effects and resume with the ordinary caller activation; the positive argument-only numeric entry still records an actual native hit.

Qualification:

  • Both warmed captured and receiver reproductions change from the null-arguments TypeError to Node's [114,1]. Their cold required-VM and forced-tree controls already passed. The regression matrix passes forced tree and required VM with JIT off/on, including mapped-argument updates (8114), exact thrown identity, and real-GC callbacks.
  • Explicit direct-entry tests require an installed optimizer entry, observe native attempts, verify misses before either callback executes, and then verify exactly one callback per ordinary call. Four actual no-GIL workers produce checksum 7040 and exactly 32 captured + 32 receiver callbacks.
  • All seven selected functional tests pass Debug, ReleaseSafe, and unsuppressed TSan, including existing exact-step, hook, termination, and moving-GC checks. No allocator leaks are reported.
  • Broader JIT gate: 131 passed / one platform skip / zero failures or leaks. VM and concurrency smokes: four cases each. Seeded threadfuzz: default 400 and verify 300 programs, zero failures.
  • Exact before/after diagnostics and forced tree/VM reports match across eleven affected subtrees: 2,093 cases, zero default-tier failures on both sides, and the same three existing proper-tail-call differences in the call subtree. The new local reproductions are pinned regressions; this is not a whole-corpus pass-rate update.
  • Quickening inventory, GC relocation audit, 378-file link audit, and checked 51-page docs manifest pass.

The full local combined-unit link remains blocked by #933's existing memory cap. Hosted integration is pending. No current throughput claim is made.

Sign in to comment on this issue.