fix: toctou in wasm apply_patch causing UB (#5534)
This commit is contained in:
1 parent
83f492d10d
commit
003ec3270b
1 file changed
+67
-74
@@ -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::<js_sys::Function>()
|
||||
.call0(&JsValue::undefined());
|
||||
_ = Reflect::get(&exports, &"__wasm_apply_global_relocs".into())
|
||||
.unwrap()
|
||||
.unchecked_into::<js_sys::Function>()
|
||||
.call0(&JsValue::undefined());
|
||||
_ = Reflect::get(&exports, &"__wasm_call_ctors".into())
|
||||
.unwrap()
|
||||
.unchecked_into::<js_sys::Function>()
|
||||
.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::<js_sys::Function>() {
|
||||
_ = func.call0(&JsValue::undefined());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe { commit_patch(table) };
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user