{
  "$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"
}