diff --git a/packages/subsecond/subsecond/src/lib.rs b/packages/subsecond/subsecond/src/lib.rs index 0497f2570..f6a4e374c 100644 --- a/packages/subsecond/subsecond/src/lib.rs +++ b/packages/subsecond/subsecond/src/lib.rs @@ -551,7 +551,7 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { wasm_bindgen_futures::spawn_local(async move { use js_sys::{ ArrayBuffer, Object, Reflect, - WebAssembly::{self, Memory, Table}, + WebAssembly::{self, Instance, Memory, Module, Table}, }; use wasm_bindgen::prelude::*; use wasm_bindgen::JsValue; @@ -561,20 +561,19 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { let funcs: Table = wasm_bindgen::function_table().unchecked_into(); let memory: Memory = wasm_bindgen::memory().unchecked_into(); let exports: Object = wasm_bindgen::exports().unchecked_into(); - let buffer: ArrayBuffer = memory.buffer().unchecked_into(); let path = table.lib.to_str().unwrap(); if !path.ends_with(".wasm") { return; } - // Start the fetch of the module - let response = web_sys::window().unwrap_throw().fetch_with_str(&path); - - // Wait for the fetch to complete - we need the wasm module size in bytes to reserve in the memory - let response: web_sys::Response = JsFuture::from(response).await.unwrap().unchecked_into(); - - // If the status is not success, we bail + // Fetch + decode the patch wasm. Both awaits are pure I/O — they + // touch no shared state, so the future is safe to drop here. + let response: web_sys::Response = + JsFuture::from(web_sys::window().unwrap_throw().fetch_with_str(&path)) + .await + .unwrap() + .unchecked_into(); if !response.ok() { panic!( "Failed to patch wasm module at {} - response failed with: {}", @@ -582,100 +581,94 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> { response.status_text() ); } - let dl_bytes: ArrayBuffer = JsFuture::from(response.array_buffer().unwrap()) .await .unwrap() .unchecked_into(); - // Expand the memory and table size to accommodate the new data and functions + // Pre-compile. This is the slow part — V8 hands it to a worker — + // and yields the longest. The result is a Module with no side + // effects on host JS state, so cancelling here is also safe. + let module: Module = JsFuture::from(WebAssembly::compile(dl_bytes.unchecked_ref())) + .await + .unwrap() + .unchecked_into(); + + // ── SYNC SECTION — no `.await` past this point ──────────────── // - // Normally we wouldn't be able to trust that we are allocating *enough* memory - // for BSS segments, but ld emits them in the binary when using import-memory. + // Everything below mutates shared JS state (linear memory, the + // indirect function table, the patch's data segment) and the + // jump-table pointer. We must not yield, because: // - // Make sure we align the memory base to the page size + // * `memory.grow` detaches the previous `ArrayBuffer`. The old + // code captured `memory.buffer()` BEFORE the awaits and then + // read its `byteLength` to compute `__memory_base`; if any + // other task ran a `memory.grow` during the awaits (e.g. an + // allocator call from a concurrent render), the snapshot was + // detached, `byteLength` returned 0, and the patch's data + // segment ended up loaded over the host's stack/rodata. + // * Two concurrent `apply_patch` tasks must not interleave + // between memory/table grow and `commit_patch`. Doing all + // side-effecting work in one synchronous turn guarantees that. + // * If the future is cancelled before this point, host state + // is untouched. const PAGE_SIZE: u32 = 64 * 1024; - let page_count = (buffer.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32; - let memory_base = (page_count + 1) * PAGE_SIZE; + let patch_pages = (dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1; - // We need to grow the memory to accommodate the new module - memory.grow((dl_bytes.byte_length() as f64 / PAGE_SIZE as f64).ceil() as u32 + 1); + // Use grow's return value (the prior page count) to derive + // memory_base. Atomic w.r.t. concurrent grows, unlike reading + // memory.buffer().byteLength(). + let prev_pages = memory.grow(patch_pages); + let memory_base = (prev_pages + 1) * PAGE_SIZE; - // We grow the ifunc table to accommodate the new functions - // In theory we could just put all the ifuncs in the jump map and use that for our count, - // but there's no guarantee from the jump table that it references "itself" - // We might need a sentinel value for each ifunc in the jump map to indicate that it is + // grow returns the prior table length, which is __table_base. let table_base = funcs.grow(table.ifunc_count as u32).unwrap(); - // Adjust the jump table to be relative to the new base address + // Rebase the jump table entries onto the patch's table slot range. for v in table.map.values_mut() { *v += table_base as u64; } - // Build up the import object. We copy everything over from the current exports, but then - // need to add in the memory and table base offsets for the relocations to work. - // - // let imports = { - // env: { - // memory: base.memory, - // __tls_base: base.__tls_base, - // __stack_pointer: base.__stack_pointer, - // __indirect_function_table: base.__indirect_function_table, - // __memory_base: memory_base, - // __table_base: table_base, - // ..base_exports - // }, - // }; + // Build the env import object: copy every host export through and + // add __memory_base / __table_base globals so the patch's PIC + // code resolves correctly. let env = Object::new(); - - // Move memory, __tls_base, __stack_pointer, __indirect_function_table, and all exports over for key in Object::keys(&exports) { Reflect::set(&env, &key, &Reflect::get(&exports, &key).unwrap()).unwrap(); } - - // Set the memory and table in the imports - // Following this pattern: Global.new({ value: "i32", mutable: false }, value) for (name, value) in [("__table_base", table_base), ("__memory_base", memory_base)] { let descriptor = Object::new(); Reflect::set(&descriptor, &"value".into(), &"i32".into()).unwrap(); Reflect::set(&descriptor, &"mutable".into(), &false.into()).unwrap(); - let value = WebAssembly::Global::new(&descriptor, &value.into()).unwrap(); - Reflect::set(&env, &name.into(), &value.into()).unwrap(); + let global = WebAssembly::Global::new(&descriptor, &value.into()).unwrap(); + Reflect::set(&env, &name.into(), &global.into()).unwrap(); } - - // Set the memory and table in the imports let imports = Object::new(); Reflect::set(&imports, &"env".into(), &env).unwrap(); - // Download the module, returning { module, instance } - // we unwrap here instead of using `?` since this whole thing is async - let result_object = JsFuture::from(WebAssembly::instantiate_module( - dl_bytes.unchecked_ref(), - &imports, - )) - .await - .unwrap(); + // Synchronous instantiation of the precompiled module — writes + // the patch's data segment into memory at `memory_base`. + let instance = Instance::new(&module, &imports).unwrap(); + let inst_exports: Object = instance.exports(); - // We need to run the data relocations and then fire off the constructors - let res: Object = result_object.unchecked_into(); - let instance: Object = Reflect::get(&res, &"instance".into()) - .unwrap() - .unchecked_into(); - let exports: Object = Reflect::get(&instance, &"exports".into()) - .unwrap() - .unchecked_into(); - _ = Reflect::get(&exports, &"__wasm_apply_data_relocs".into()) - .unwrap() - .unchecked_into::() - .call0(&JsValue::undefined()); - _ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into()) - .unwrap() - .unchecked_into::() - .call0(&JsValue::undefined()); - _ = Reflect::get(&exports, &"__wasm_call_ctors".into()) - .unwrap() - .unchecked_into::() - .call0(&JsValue::undefined()); + // Run the patch's relocation thunks and constructors. Order + // matters: data relocs first (write memory_base- and table_base- + // relative pointers into the patch's data segment), then global + // relocs (adjust GOT.func.internal globals by __table_base — + // wasm-ld synthesizes those as element-segment-relative offsets), + // then ctors. `dyn_into` instead of `unchecked_into` so missing + // exports just no-op rather than throwing. + for func_name in [ + "__wasm_apply_data_relocs", + "__wasm_apply_global_relocs", + "__wasm_call_ctors", + ] { + if let Ok(val) = Reflect::get(&inst_exports, &func_name.into()) { + if let Ok(func) = val.dyn_into::() { + _ = func.call0(&JsValue::undefined()); + } + } + } unsafe { commit_patch(table) }; });