fix: make instantiate async

This commit is contained in:
Jonathan Kelley committed 2026-05-07 16:44:37 -07:00
1 parent f5be0ea510
commit d1ead872b1
1 file changed
+33 -19
+33 -19
View File
@@ -594,24 +594,31 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
.unwrap() .unwrap()
.unchecked_into(); .unchecked_into();
// ── SYNC SECTION — no `.await` past this point ──────────────── // ── HOST-STATE-MUTATING SECTION ───────────────────────────────
// //
// Everything below mutates shared JS state (linear memory, the // Below we grow shared linear memory and the indirect function
// indirect function table, the patch's data segment) and the // table, then async-instantiate the patch into them and commit
// jump-table pointer. We must not yield, because: // the new jump table. There IS one `.await` for the instantiate
// (we can't avoid it: Chrome disallows synchronous
// `new WebAssembly.Instance` on the main thread for modules
// larger than 8MB, and patches routinely cross that), but it's
// safe — the original race we fixed wasn't about yielding here:
// //
// * `memory.grow` detaches the previous `ArrayBuffer`. The old // * `memory.grow` and `funcs.grow` each return their PRIOR
// code captured `memory.buffer()` BEFORE the awaits and then // length atomically. Concurrent `apply_patch` tasks therefore
// read its `byteLength` to compute `__memory_base`; if any // each get a unique, non-overlapping `memory_base` /
// other task ran a `memory.grow` during the awaits (e.g. an // `table_base`, so two patches can't land on the same region.
// allocator call from a concurrent render), the snapshot was // * Host code can't observe the half-instantiated patch: the
// detached, `byteLength` returned 0, and the patch's data // new memory pages are zero, the new table slots are null,
// segment ended up loaded over the host's stack/rodata. // and the jump table isn't committed until the very end of
// * Two concurrent `apply_patch` tasks must not interleave // this block, so nothing redirects through the new slots.
// between memory/table grow and `commit_patch`. Doing all // * The original bug — using `memory.buffer().byteLength()`
// side-effecting work in one synchronous turn guarantees that. // captured before the awaits, which returned 0 if the buffer
// * If the future is cancelled before this point, host state // had been detached by a concurrent grow — is gone because
// is untouched. // we derive `memory_base` from `memory.grow()`'s return
// value instead.
// * Cancellation between grow and `commit_patch` leaks memory
// pages and table slots, but doesn't corrupt anything.
const PAGE_SIZE: u32 = 64 * 1024; const PAGE_SIZE: u32 = 64 * 1024;
let patch_pages = (dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1; let patch_pages = (dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1;
@@ -646,9 +653,16 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
let imports = Object::new(); let imports = Object::new();
Reflect::set(&imports, &"env".into(), &env).unwrap(); Reflect::set(&imports, &"env".into(), &env).unwrap();
// Synchronous instantiation of the precompiled module — writes // Async instantiation of the precompiled module. We use the
// the patch's data segment into memory at `memory_base`. // (Module, imports) form of `WebAssembly.instantiate`, which
let instance = Instance::new(&module, &imports).unwrap(); // resolves directly to an `Instance` (not `{module, instance}`).
// This is the no-size-limit path; the synchronous
// `new WebAssembly.Instance` constructor is capped at 8MB on
// Chrome's main thread.
let instance: Instance = JsFuture::from(WebAssembly::instantiate_module(&module, &imports))
.await
.unwrap()
.unchecked_into();
let inst_exports: Object = instance.exports(); let inst_exports: Object = instance.exports();
// Run the patch's relocation thunks and constructors. Order // Run the patch's relocation thunks and constructors. Order