Asyncify fixes for sequential awaited host calls, and drop CI - #1
Merged
Merged
Conversation
A pending job that calls an asyncified host function suspends, but the only
pending-job driver was synchronous, so the second sequential await inside one
guest execution left the runtime corrupted. Release builds also omitted
Emscripten's {async: true} on bindings that may suspend.
This fork exists to build packages published manually into a private scope from the consuming repo, and nothing here holds credentials to publish or deploy.
Emscripten's cwrap returns the raw WebAssembly export whenever the return type is not a string, every argument is numeric and no options object is passed, which skips ccall and its check for a call that suspended. That check is what turns a suspension into a Promise, so assertSync — already wrapped around these bindings for exactly this — could never fire, and a pending job that suspended into a host function left the caller reading a pointer the unwound call never wrote.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings in the two unreleased Asyncify fixes this fork exists for, plus one defect found while verifying them, and drops CI.
1. Runtime disposal ordering (upstream justjake#256 — already on
main)No change needed:
4ccfbf5is already here. Worth stating because npmlatestis still 0.32.0, published 2026-02-16, five months before that merge — so the registry has no build containing it. That alone is a reason to publish this fork.2. Sequential awaited host calls (upstream justjake#258 / PR justjake#265)
Applied PR justjake#265, with two corrections:
scripts/generate.tsbut each variant'ssrc/ffi.tsis generated and committed, so merging it alone changes nothing at runtime. Regenerated viapnpm run build:codegen— five release asyncify variants.executePendingJobsAsyncread a detached heap view. It allocated anInt32ArrayoverHEAPU8.buffer, awaited, then readtypedArray[0]. WASM memory growth during the suspension detaches that buffer, so the read yieldsundefined, which was then used as a context pointer. AddedModuleMemory.readPointer(), which builds the view after the await.One correction to the PR's stated cause: on Emscripten 5.0.1 the
{ async: true }change is not what fixes justjake#258 —executePendingJobsAsync()is. Measured both ways against a local build. Kept{ async: true }regardless, since it matches the debug builds and Emscripten's documented contract.3. The synchronous job driver returned a value it never produced
Found while verifying the above.
runtime.executePendingJobs()on an Asyncify runtime corrupted WASM memory (RuntimeError: memory access out of bounds) from the second sequential awaited host call in one guest execution.Cause is in
cwrap's fast path:QTS_ExecutePendingJobreturns"number", takes three numbers, and passed no options, so it got the raw export — skippingccalland its suspension check, which is ordinary control flow in release builds, not an assertion:So a suspension went unnoticed and the caller dereferenced a pointer the unwound call never wrote.
assertSyncis already wrapped around exactly these bindings for this purpose and could never fire, because the raw export returns a number, never a Promise.Passing
{}as options restores the check. Applied to every synchronous sibling of aMaybeAsyncbinding on asyncify builds.memory access out of boundsError: Function unexpectedly returned a Promise4. Removed the CI workflow
This fork is built and published manually from the consuming repo, and holds no credentials to publish or deploy.
Verification
Full suite on a local native build (Emscripten 5.0.1, all 22 variants): 7 files, 610 passed, 49 skipped. The new regression test fails without the fix and passes with it.
asyncify-sync-driver.test.tsis deliberately its own file: an unresumed suspension outlives the context it happened in, and a debug build aborts the module outright, so a shared-module test block sees every later test fail — which reads exactly like the fix caused a regression when it did not.Items 2 and 3 are worth sending upstream; neither the
cwrapfast-path diagnosis nor the detached-view bug is in the justjake#258 thread.