{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreidjaggymxbqgc77f72grx2nsytzvhkbiz6ntk7ealpi63v5ah7cje",
"uri": "at://did:plc:ivbknywyskln22er3nkssdhl/app.bsky.feed.post/3mm5ruhqvo562"
},
"path": "/t/wasm32-rust-should-not-spill-v128-function-params-into-shadow-stack/24344#post_1",
"publishedAt": "2026-05-18T19:27:42.000Z",
"site": "https://internals.rust-lang.org",
"tags": [
"WebAssembly spec",
"core::arch::wasm32::v128",
"Compiler Explorer"
],
"textContent": "Rust has support for v128 type from WebAssembly spec. It is represented in Rust by core::arch::wasm32::v128 type.\n\nWeb Assembly treats it as primitive type and you can pass and return it by value, e.g. `(func $add7_to_v128 (;0;) (type 0) (param v128) (result v128)`. This allows backend to generate efficient machine code later that uses, for example, `xmm` registers for that.\n\n### C & C++\n\nC does exactly that:\n\n\n #include <wasm_simd128.h>\n\n bool exact_eq_128(v128_t a, v128_t b)\n __attribute__((export_name(\"exact_eq_128\")))\n {\n v128_t and_res = wasm_v128_andnot(a, b);\n return !wasm_v128_any_true(and_res);\n }\n\n\n v128_t add7_to_v128(v128_t a)\n __attribute__((export_name(\"add7_to_v128\")))\n {\n v128_t b = wasm_u8x16_splat(7);\n return wasm_i8x16_add(a, b);\n }\n\n\nCompile and print:\n\n\n clang -std=c23 -msimd128 -O3 --target=wasm32-unknown-unknown \\\n -nostdlib '-Wl,--no-entry' -o c_v128.wasm .\\c_v128.c\n wasm-tools print .\\c_v128.wasm\n\n\n\n (func $exact_eq_128 (;0;) (type 0) (param v128 v128) (result i32)\n local.get 0 ;; Load value from parameter to stack\n local.get 1 ;; Load value from parameter to stack\n v128.andnot\n v128.any_true\n i32.eqz\n )\n (func $add7_to_v128 (;1;) (type 1) (param v128) (result v128)\n local.get 0\n v128.const i32x4 0x07070707 0x07070707 0x07070707 0x07070707\n i8x16.add\n ;; Implicitly return value from top of the stack\n )\n\n\n### Rust\n\nUnfortunately, Rust spills both arguments and result values to the shadow stack:\n\n\n use core::arch::wasm32::*;\n\n #[unsafe(export_name = \"exact_eq_128\")]\n pub fn exact_eq_128(a: v128, b: v128)->bool {\n let and_res = v128_andnot(a, b);\n !v128_any_true(and_res)\n }\n\n #[unsafe(export_name = \"add7_to_v128\")]\n pub fn add7_to_v128(a: v128)->v128 {\n let b = u8x16_splat(7);\n let c = u8x16_add(a, b);\n c\n }\n\n\nCommand:\n\n\n rustc --version\n rustc 1.95.0 (59807616e 2026-04-14)\n rustc --edition=2024 -Ctarget-feature=+simd128 -Copt-level=3 \\\n --crate-type=cdylib -Clto=thin --target=wasm32-unknown-unknown \\\n .\\eq_v128.rs -o eq_v128_rs.wasm\n wasm-tools strip .\\eq_v128_rs.wasm > eq_v128_rs_s.wasm\n wasm-tools print .\\eq_v128_rs_s.wasm\n\n\nOutput:\n\n\n (func $add7_to_v128 (;0;) (type 0) (param i32 i32)\n local.get 0 ;; Get pointer to place to return on shadow stack\n local.get 1 ;; Get pointer to first actual parameter in shadow stack\n v128.load ;; Load parameter from shadow stack\n v128.const i32x4 0x07070707 0x07070707 0x07070707 0x07070707\n i8x16.add\n v128.store ;; Store value we return to caller provided space\n )\n (func $exact_eq_128 (;1;) (type 1) (param i32 i32) (result i32)\n local.get 0\n v128.load ;; Load first param from shadow stack\n local.get 1\n v128.load ;; Load second param from shadow stack\n v128.andnot\n v128.any_true\n i32.eqz\n )\n\n\nPassing values through shadow stack is bad because backend cannot really optimize accesses to shadow stack because from the perspective of it, it is a shadow memory that can be edited anytime (there is no aliasing information). It is just linear memory without any context.\n\nWasmtime cannot optimize such code: Compiler Explorer\n\nSo, this should be changed.\n\n### Workaround\n\nUntil this behaviour changes, it can be circumvented by marking all functions that accept or receive v128 as `extern`:\n\n\n #[unsafe(export_name = \"exact_eq_128\")]\n pub extern fn exact_eq_128(a: v128, b: v128)->bool {\n let and_res = v128_andnot(a, b);\n !v128_any_true(and_res)\n }\n\n #[unsafe(export_name = \"add7_to_v128\")]\n pub extern fn add7_to_v128(a: v128)->v128 {\n let b = u8x16_splat(7);\n let c = u8x16_add(a, b);\n c\n }\n\n\n\n (func $add7_to_v128 (;0;) (type 0) (param v128) (result v128)\n local.get 0\n v128.const i32x4 0x07070707 0x07070707 0x07070707 0x07070707\n i8x16.add\n )\n (func $exact_eq_128 (;1;) (type 1) (param v128 v128) (result i32)\n local.get 0\n local.get 1\n v128.andnot\n v128.any_true\n i32.eqz\n )\n",
"title": "Wasm32: Rust should not spill v128 function params into shadow stack"
}