From 5ed646064f00e2ca00b528c62fee7e57d0439746 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 11 Jun 2026 12:49:26 -0500 Subject: [PATCH 1/3] fix hotpatching wasm with new v0 symbol mangling in beta linux builds --- packages/cli/src/build/patch.rs | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/packages/cli/src/build/patch.rs b/packages/cli/src/build/patch.rs index c002b88edc..d6effec834 100644 --- a/packages/cli/src/build/patch.rs +++ b/packages/cli/src/build/patch.rs @@ -1519,13 +1519,23 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result> { /// Uses the heuristics from the wasm-bindgen source code itself: /// /// +/// +/// Symbols arrive in both mangling schemes (legacy `_ZN..$LT$..$GT$..` and v0 `_R..`, the default +/// since rustc 1.97), so each describe pattern needs a matcher per scheme. The v0 patterns match +/// the trailing `` identifiers rather than the `wasm_bindgen` crate path because v0 +/// backrefs (`NtB5_` etc.) routinely compress the path away. If any of these slip through, the +/// describe functions get pinned into the ifunc table, wasm-bindgen's GC can't delete them, and +/// the final module ships an unsatisfiable `__wbindgen_placeholder__.__wbindgen_describe` import. fn name_is_bindgen_symbol(name: &str) -> bool { name.contains("__wbindgen_describe") || name.contains("__wbindgen_externref") || name.contains("wasm_bindgen8describe6inform") || name.contains("wasm_bindgen..describe..WasmDescribe") + || name.contains("12WasmDescribe8describe") || (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe")) + || (name.contains("11WasmClosure") && name.contains("describe")) || (name.contains("wasm_bindgen7closure16Closure") && name.contains("describe")) + || (name.contains("7closure7Closure") && name.contains("describe")) || (name.contains("wasm_bindgen7convert8closures") && name.contains("describe_invoke")) } @@ -1539,10 +1549,35 @@ fn bindgen_symbol_catch() { let symbol = "_ZN12wasm_bindgen7closure16Closure$LT$T$GT$4wrap8describe17h1234567890abcdefE"; assert!(name_is_bindgen_symbol(symbol)); + // v0 mangling (default since rustc 1.97): `::describe` impl with the + // full wasm_bindgen path spelled out + let symbol = "_RNvXNvNtNtCs9jB4f2OZCsR_7web_sys8features36gen_TransformStreamDefaultController1__NtB4_32TransformStreamDefaultControllerNtNtCs9tRDgkfeYnK_12wasm_bindgen8describe12WasmDescribe8describe"; + assert!(name_is_bindgen_symbol(symbol)); + + // v0 describe impl where a backref (NtB5_) compresses the wasm_bindgen::describe path away + let symbol = "_RNvXNvNtNtCs9jB4f2OZCsR_7web_sys8features8gen_Node4NodeENtB5_12WasmDescribe8describeCs3X5Dvr2wWzv_21dioxus_interpreter_js"; + assert!(name_is_bindgen_symbol(symbol)); + + // v0 closure describe_invoke (WasmClosure trait path is backref-compressed to `NtNtBc_`) + let symbol = "_RINvXs1_NvNtNtCs9tRDgkfeYnK_12wasm_bindgen7convert8closuress8_1__DINtNtNtCs9WN6KVdqFxk_4core3ops8function5FnMutTNtBc_7JsValueB1M_mNtCsezy3jvZZ1sp_6js_sys5ArrayEEp6OutputB1M_EL_NtNtBc_7closure11WasmClosure15describe_invokeKb1_EB26_"; + assert!(name_is_bindgen_symbol(symbol)); + + // v0 name of the __wbindgen_describe import shim + let symbol = "_RNvCs9tRDgkfeYnK_12wasm_bindgen19___wbindgen_describe"; + assert!(name_is_bindgen_symbol(symbol)); + // does_not_match_saved_runtime_exports assert!(!name_is_bindgen_symbol("__wbindgen_malloc")); assert!(!name_is_bindgen_symbol("__wbindgen_realloc")); assert!(!name_is_bindgen_symbol("__wbindgen_free")); + + // does_not_match_ordinary_user_symbols_in_either_mangling + assert!(!name_is_bindgen_symbol( + "_ZN5alloc7raw_vec19RawVec$LT$T$C$A$GT$8grow_one17h1234567890abcdefE" + )); + assert!(!name_is_bindgen_symbol( + "_RNvXs5_NtCs9tRDgkfeYnK_12wasm_bindgen5__rt5LazyINtB5_4LazyNtNtCsezy3jvZZ1sp_6js_sys6ObjectE5force" + )); } /// Manually parse the data section from a wasm module From 66ce1722ccca47f5542af1a125864035d9d37d65 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 11 Jun 2026 12:59:27 -0500 Subject: [PATCH 2/3] match describe vector as well --- packages/cli/src/build/patch.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/cli/src/build/patch.rs b/packages/cli/src/build/patch.rs index d6effec834..bdec3b4014 100644 --- a/packages/cli/src/build/patch.rs +++ b/packages/cli/src/build/patch.rs @@ -1532,6 +1532,7 @@ fn name_is_bindgen_symbol(name: &str) -> bool { || name.contains("wasm_bindgen8describe6inform") || name.contains("wasm_bindgen..describe..WasmDescribe") || name.contains("12WasmDescribe8describe") + || name.contains("18WasmDescribeVector15describe_vector") || (name.contains("wasm_bindgen..closure..WasmClosure") && name.contains("describe")) || (name.contains("11WasmClosure") && name.contains("describe")) || (name.contains("wasm_bindgen7closure16Closure") && name.contains("describe")) @@ -1558,6 +1559,15 @@ fn bindgen_symbol_catch() { let symbol = "_RNvXNvNtNtCs9jB4f2OZCsR_7web_sys8features8gen_Node4NodeENtB5_12WasmDescribe8describeCs3X5Dvr2wWzv_21dioxus_interpreter_js"; assert!(name_is_bindgen_symbol(symbol)); + // v0 `::describe_vector` impl (legacy mangling matches these via + // the `wasm_bindgen..describe..WasmDescribe` prefix substring, v0 needs its own pattern) + let symbol = "_RNvXs4_NtNtCs9tRDgkfeYnK_12wasm_bindgen7convert6slicesNtNtCscHiZRFGp0KF_5alloc6string6StringNtNtB9_8describe18WasmDescribeVector15describe_vector"; + assert!(name_is_bindgen_symbol(symbol)); + + // v0 describe_vector impl in a downstream crate with the trait path backref-compressed + let symbol = "_RNvXsf_NtCs4ofacjxbDm2_10dioxus_web8documentNtB5_7JSOwnerNtNtCs9tRDgkfeYnK_12wasm_bindgen8describe18WasmDescribeVector15describe_vector"; + assert!(name_is_bindgen_symbol(symbol)); + // v0 closure describe_invoke (WasmClosure trait path is backref-compressed to `NtNtBc_`) let symbol = "_RINvXs1_NvNtNtCs9tRDgkfeYnK_12wasm_bindgen7convert8closuress8_1__DINtNtNtCs9WN6KVdqFxk_4core3ops8function5FnMutTNtBc_7JsValueB1M_mNtCsezy3jvZZ1sp_6js_sys5ArrayEEp6OutputB1M_EL_NtNtBc_7closure11WasmClosure15describe_invokeKb1_EB26_"; assert!(name_is_bindgen_symbol(symbol)); From c8245e25f82e436740fbd565549591e3930f2502 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 11 Jun 2026 13:29:48 -0500 Subject: [PATCH 3/3] more symbols --- packages/cli/src/build/patch.rs | 36 ++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/build/patch.rs b/packages/cli/src/build/patch.rs index bdec3b4014..27a74821e7 100644 --- a/packages/cli/src/build/patch.rs +++ b/packages/cli/src/build/patch.rs @@ -707,7 +707,7 @@ pub fn create_wasm_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Resu continue; }; - if name.contains("wasm_bindgen4__rt8wbg_cast") && !name.contains("breaks_if_inline") { + if name_is_wbg_cast_symbol(name) { let name = name.to_string(); let old_idx = name_to_ifunc_old .get(&name) @@ -1526,6 +1526,21 @@ pub fn prepare_wasm_base_module(bytes: &[u8]) -> Result> { /// backrefs (`NtB5_` etc.) routinely compress the path away. If any of these slip through, the /// describe functions get pinned into the ifunc table, wasm-bindgen's GC can't delete them, and /// the final module ships an unsatisfiable `__wbindgen_placeholder__.__wbindgen_describe` import. +/// Check if the name is a `wasm_bindgen::__rt::wbg_cast` instantiation (excluding its inner +/// `breaks_if_inlined` helper). These functions need their bodies rewritten to call the base +/// module's JS-bound versions via the ifunc table — if one slips through, the patch keeps its +/// local copy, which calls `breaks_if_inlined` → `describe::inform` → the stubbed +/// `__wbindgen_describe` import and traps with "null function" the first time the patched code +/// casts a value (e.g. creating an event-listener closure). +/// +/// In legacy mangling the path appears as `wasm_bindgen4__rt8wbg_cast`; in v0 (default since +/// rustc 1.97) identifiers starting with `_` get a `_` separator after their length, so `__rt` +/// encodes as `4___rt`. +fn name_is_wbg_cast_symbol(name: &str) -> bool { + (name.contains("wasm_bindgen4__rt8wbg_cast") || name.contains("wasm_bindgen4___rt8wbg_cast")) + && !name.contains("breaks_if_inline") +} + fn name_is_bindgen_symbol(name: &str) -> bool { name.contains("__wbindgen_describe") || name.contains("__wbindgen_externref") @@ -1590,6 +1605,25 @@ fn bindgen_symbol_catch() { )); } +#[test] +fn wbg_cast_symbol_catch() { + // legacy mangling: wbg_cast instantiation matches, its breaks_if_inlined helper does not + assert!(name_is_wbg_cast_symbol( + "_ZN12wasm_bindgen4__rt8wbg_cast17h1234567890abcdefE" + )); + assert!(!name_is_wbg_cast_symbol( + "_ZN12wasm_bindgen4__rt8wbg_cast17breaks_if_inlined17h1234567890abcdefE" + )); + + // v0 mangling: `__rt` encodes as `4___rt` (length 4, `_` separator, then `__rt`) + assert!(name_is_wbg_cast_symbol( + "_RINvNtCsa7akE1TfegA_12wasm_bindgen4___rt8wbg_castINtB4_7closure12OwnedClosureDINtNtNtCs9WN6KVdqFxk_4core3ops8function5FnMutTNtNtNtCs5qlPUvWaqlJ_7web_sys8features14gen_MouseEvent10MouseEventEEp6OutputuEL_Kb1_ENtBO_9JsClosureECsjFep1nV9Dzo_32dioxus_playwright_web_patch_test" + )); + assert!(!name_is_wbg_cast_symbol( + "_RINvNvNtCsa7akE1TfegA_12wasm_bindgen4___rt8wbg_cast17breaks_if_inlinedINtNtB6_7closure12OwnedClosureDINtNtNtCs9WN6KVdqFxk_4core3ops8function5FnMutTNtNtNtCs5qlPUvWaqlJ_7web_sys8features14gen_MouseEvent10MouseEventEEp6OutputuEL_Kb1_ENtB19_9JsClosureECsjFep1nV9Dzo_32dioxus_playwright_web_patch_test" + )); +} + /// Manually parse the data section from a wasm module /// /// We need to do this for data symbols because walrus doesn't provide the right range and offset