Held send dispatch frame ownership prerequisite
Based on held cb828e2dad56d8f7d30ceae2c542f69023c350a1; retains the
e4510f1a9 foreign-failure propagation fix. No main integration or cycle fix.
The host now assigns a monotonically increasing safe-integer token per link
before sending dispatch. The Worker records it with the interpreter snapshot,
returns it on accepted dispatch/resume replies, and checks it before resume or
abort changes guest state. A missing/stale token or an outer token while a
newer frame is active returns ownershipMismatch; it cannot complete the wrong
SendMessage or pop a middle frame. Duplicate admission tokens are rejected.
The host drive loop propagates the original token through resume and every
cleanup path, including its outer foreign-failure catch and a lost dispatch
reply. Mismatch fails explicitly; it is not converted into LRESULT zero.
Local main dispatch has the same guards. Its _localLink, frame stack and last
accepted token are created only in the GuestThreadHost constructor; there is
no independent recreation/reset path. A new host/link owns a fresh namespace.
Tokens must not be reset or links replaced against a surviving interpreter
stack. Tokens are ownership identities inside one trusted link, not security
capabilities or PID/TID identities.
Validation
node test/test-thread-send-frame-ownership.js passes using the actual
guest-worker.js in a real Node Worker, the actual WorkerLink transport,
and a small stateful Wasm fixture compiled by the existing JS compiler. The
same controls run against the real local host dispatch implementation. No
Worker handler or export is silently stubbed. The fixture implements register
state, callback depth, completion and a real unreachable trap; this checks
JS frame bookkeeping, not x86/WAT callback semantics. Tests include missing,
stale and duplicate tokens; nested restoration; zero result; trap cleanup with
and without restoration; foreign failure with a newer frame active; normal
foreign failure unwind; and accepted dispatch followed by a lost reply. The newer-frame failure test
manually unwinds the inner and then remaining outer frame: automatic deferred
unwind is deliberately not claimed.
A disposable copy of guest-worker.js with only both
if (!ownsThreadSendFrame(msg.frameToken)) guards disabled fails the new test.
A second negative run suppresses only the expected mismatch-reply assertions
so the state comparison is reached: it fails stale command mutated current frame, showing ESP 81 instead of 61 and callback depth 0 instead of 2. The
production files were unchanged and disposable copies removed; the original
test passes again.
node test/test-process-send-broker.js passes unchanged. Production and changed
test JS syntax and git diff --check pass. Full-tree caller inventory found
only test-cross-thread-send.js direct dispatch and
test-process-send-runtime.js direct abort outside the host driver; both now
supply tokens. Those full-VM tests were syntax checked, not executed under
the source-only task restriction. New test has automatic UNIT tier membership.
Remaining holds
This is a rejection guard, not a scheduler: an outer operation that arrives
while a newer frame is active fails and leaves the outer frame pending. There
is no wait-until-top queue, delayed outer-result delivery or retirement unwind.
The existing broker admission restriction remains necessary and unchanged.
Generic completeWait, completeThreadSend, message-wait resume, callExport
and yield-resolver side effects still lack frame/epoch ownership. Destination
thread/link generations, outbound wait state and sender identity also remain
missing. Do not relax independent-chain admission on this patch. The prior
independent-cycle/two-thread reproducer remains intentionally failing. No full
VM, canonical build, browser, Boat, game or GPU execution occurred.