Stream: git-wasmtime

Topic: wasmtime / issue #14671 `VMContext` layout misaligns `fun...


view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 17:16):

elliekorn opened issue #14671:

Test Case

(module
  (tag $e (param i32))
  (func (export "identity") (param i32) (result i32)
    (local.get 0))
)

Nothing throws and no exception opcode is executed - merely defining a tag is
enough.

Steps to Reproduce

Expected Results

The module instantiates and identity returns 7. Every VMFuncRef in the
VMContext is pointer-aligned regardless of how many tags the module defines.

Actual Results

The process aborts during instantiation:

thread 'main' panicked at library/core/src/ptr/non_null.rs:1078:18:
unsafe precondition(s) violated: ptr::write requires that the pointer argument is aligned and non-null
This indicates a bug in the program. This Undefined Behavior check is optional, and cannot be relied on for safety.
thread caused non-unwinding panic. aborting.

The misalignment itself does not depend on the build mode. With the prebuilt
release std the module appears to work, but the exported function's
VMFuncRef (from Func::to_raw) sits at a misaligned address:
| Module | VMFuncRef address |
|---|---|
| no tags | 0x…300 (0 mod 8) |
| 1 tag | 0x…304 (4 mod 8) |
| 2 tags | 0x…308 (0 mod 8) |
and VMOffsets for one defined tag, one escaped function, a start function and
one runtime data segment places func_refs at 52, startup_func_ref at 84 and
runtime_data_bases at 116 — all 4 mod 8 (48 / 80 / 112 with no tags).

Versions and Environment

Wasmtime version or commit: a473f42e459680d55ade5873e5ac95ac0f81966c
(51.0.0-dev)
Operating system: Linux
Architecture: x86_64

Extra Info

Root cause. VMTagDefinition (in for_each_vm_type!,
crates/environ/src/vmtypes.rs) is a plain #[repr(C)] struct with a single
VMSharedTypeIndex field, so it is 4 bytes with 4-byte alignment.
The VMContext layout's dynamic section (in for_each_vmctx_type!,
crates/environ/src/vmctxtypes.rs) places tags directly before func_refs:

align { 16 }
array {
    #[aggregate]
    globals[num_defined_globals; DefinedGlobalIndex]: VMGlobalDefinition
}
array {
    #[aggregate]
    tags[num_defined_tags; DefinedTagIndex]: VMTagDefinition
}
array {
    #[aggregate]
    func_refs[num_escaped_funcs; FuncRefIndex]: VMFuncRef
}

The layout grammar never derives padding from a field's type - alignment is
always an explicit align { ... } entry - and there is none between tags and
func_refs. With globals starting 16-aligned at 16 bytes per element,
func_refs begins at 16·k + 4·num_defined_tags, which is 4 mod 8 for an odd
tag count on 64-bit targets. Every later entry is a multiple of 8 bytes, so
startup_func_ref and runtime_data_bases are misaligned too
(runtime_data_lengths, being u32, is unaffected). 32-bit targets are
unaffected. VMComponentContext's layout, by contrast, has an align { ptr }
before trampoline_func_refs.
Instance::initialize_and_return_funcref writes the VMFuncRef with
NonNull::write at vmctx + func_refs().at(index); the index is valid and in
range, only the address is misaligned.
Why CI doesn't catch it. The ptr::write precondition is only compiled in
when core itself is built with debug assertions, which the prebuilt std
shipped by rustup is not. Ordinary cargo test therefore never runs it. The
existing check_vmtag_begins_aligned test uses an empty module and only checks
where tags begins.
Impact. On x86-64 and AArch64 the misaligned plain loads and stores are
tolerated by hardware, so release builds behave correctly; the practical effect
is aborts in debug-std builds, plus Rust-level UB. Every C++ guest compiled
with native Wasm exception handling declares exactly one tag
(__cpp_exception), so such guests always land on the odd case.
Suggested fix. One alignment entry in for_each_vmctx_type!:

array {
    #[aggregate]
    tags[num_defined_tags; DefinedTagIndex]: VMTagDefinition
}
align { ptr }
array {
    #[aggregate]
    func_refs[num_escaped_funcs; FuncRefIndex]: VMFuncRef
}

align { ptr } rather than align { 8 } so 32-bit targets get no unneeded
padding. The runtime and Cranelift both take their offsets from this one layout,
so the single entry fixes both and also re-aligns startup_func_ref and
runtime_data_bases.
Suggested regression test, built from VMOffsetsFields so it covers both
pointer widths regardless of host or build mode:

#[test]
fn func_refs_aligned_after_odd_tag_count() {
    for ptr in [4u8, 8u8] {
        let offsets = VMOffsets::from(VMOffsetsFields {
            ptr,
            num_imported_functions: 0,
            num_imported_tables: 0,
            num_imported_memories: 0,
            num_imported_globals: 0,
            num_imported_tags: 0,
            num_defined_tables: 0,
            num_defined_memories: 0,
            num_owned_memories: 0,
            num_defined_globals: 0,
            num_defined_tags: 1,
            num_escaped_funcs: 1,
            num_runtime_data: 1,
            has_startup_func: true,
        });
        let p = u32::from(ptr);
        assert_eq!(offsets.func_refs().begin() % p, 0);
        assert_eq!(offsets.startup_func_ref() % p, 0);
        assert_eq!(offsets.runtime_data_bases().begin() % p, 0);
    }
}

view this post on Zulip Wasmtime GitHub notifications bot (Oct 12 2026 at 17:16):

elliekorn added the bug label to Issue #14671.

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 14:41):

alexcrichton commented on issue #14671:

Thanks for the issue here, but this is a source of UB and thus a security issue for Wasmtime. We'll be publishing an advisory later today at https://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-p79w-pv2r-29qp

view this post on Zulip Wasmtime GitHub notifications bot (Oct 14 2026 at 14:41):

alexcrichton closed issue #14671:

Test Case

(module
  (tag $e (param i32))
  (func (export "identity") (param i32) (result i32)
    (local.get 0))
)

Nothing throws and no exception opcode is executed - merely defining a tag is
enough.

Steps to Reproduce

Expected Results

The module instantiates and identity returns 7. Every VMFuncRef in the
VMContext is pointer-aligned regardless of how many tags the module defines.

Actual Results

The process aborts during instantiation:

thread 'main' panicked at library/core/src/ptr/non_null.rs:1078:18:
unsafe precondition(s) violated: ptr::write requires that the pointer argument is aligned and non-null
This indicates a bug in the program. This Undefined Behavior check is optional, and cannot be relied on for safety.
thread caused non-unwinding panic. aborting.

The misalignment itself does not depend on the build mode. With the prebuilt
release std the module appears to work, but the exported function's
VMFuncRef (from Func::to_raw) sits at a misaligned address:
| Module | VMFuncRef address |
|---|---|
| no tags | 0x…300 (0 mod 8) |
| 1 tag | 0x…304 (4 mod 8) |
| 2 tags | 0x…308 (0 mod 8) |
and VMOffsets for one defined tag, one escaped function, a start function and
one runtime data segment places func_refs at 52, startup_func_ref at 84 and
runtime_data_bases at 116 — all 4 mod 8 (48 / 80 / 112 with no tags).

Versions and Environment

Wasmtime version or commit: a473f42e459680d55ade5873e5ac95ac0f81966c
(51.0.0-dev)
Operating system: Linux
Architecture: x86_64

Extra Info

Root cause. VMTagDefinition (in for_each_vm_type!,
crates/environ/src/vmtypes.rs) is a plain #[repr(C)] struct with a single
VMSharedTypeIndex field, so it is 4 bytes with 4-byte alignment.
The VMContext layout's dynamic section (in for_each_vmctx_type!,
crates/environ/src/vmctxtypes.rs) places tags directly before func_refs:

align { 16 }
array {
    #[aggregate]
    globals[num_defined_globals; DefinedGlobalIndex]: VMGlobalDefinition
}
array {
    #[aggregate]
    tags[num_defined_tags; DefinedTagIndex]: VMTagDefinition
}
array {
    #[aggregate]
    func_refs[num_escaped_funcs; FuncRefIndex]: VMFuncRef
}

The layout grammar never derives padding from a field's type - alignment is
always an explicit align { ... } entry - and there is none between tags and
func_refs. With globals starting 16-aligned at 16 bytes per element,
func_refs begins at 16·k + 4·num_defined_tags, which is 4 mod 8 for an odd
tag count on 64-bit targets. Every later entry is a multiple of 8 bytes, so
startup_func_ref and runtime_data_bases are misaligned too
(runtime_data_lengths, being u32, is unaffected). 32-bit targets are
unaffected. VMComponentContext's layout, by contrast, has an align { ptr }
before trampoline_func_refs.
Instance::initialize_and_return_funcref writes the VMFuncRef with
NonNull::write at vmctx + func_refs().at(index); the index is valid and in
range, only the address is misaligned.
Why CI doesn't catch it. The ptr::write precondition is only compiled in
when core itself is built with debug assertions, which the prebuilt std
shipped by rustup is not. Ordinary cargo test therefore never runs it. The
existing check_vmtag_begins_aligned test uses an empty module and only checks
where tags begins.
Impact. On x86-64 and AArch64 the misaligned plain loads and stores are
tolerated by hardware, so release builds behave correctly; the practical effect
is aborts in debug-std builds, plus Rust-level UB. Every C++ guest compiled
with native Wasm exception handling declares exactly one tag
(__cpp_exception), so such guests always land on the odd case.
Suggested fix. One alignment entry in for_each_vmctx_type!:

array {
    #[aggregate]
    tags[num_defined_tags; DefinedTagIndex]: VMTagDefinition
}
align { ptr }
array {
    #[aggregate]
    func_refs[num_escaped_funcs; FuncRefIndex]: VMFuncRef
}

align { ptr } rather than align { 8 } so 32-bit targets get no unneeded
padding. The runtime and Cranelift both take their offsets from this one layout,
so the single entry fixes both and also re-aligns startup_func_ref and
runtime_data_bases.
Suggested regression test, built from VMOffsetsFields so it covers both
pointer widths regardless of host or build mode:

#[test]
fn func_refs_aligned_after_odd_tag_count() {
    for ptr in [4u8, 8u8] {
        let offsets = VMOffsets::from(VMOffsetsFields {
            ptr,
            num_imported_functions: 0,
            num_imported_tables: 0,
            num_imported_memories: 0,
            num_imported_globals: 0,
            num_imported_tags: 0,
            num_defined_tables: 0,
            num_defined_memories: 0,
            num_owned_memories: 0,
            num_defined_globals: 0,
            num_defined_tags: 1,
            num_escaped_funcs: 1,
            num_runtime_data: 1,
            has_startup_func: true,
        });
        let p = u32::from(ptr);
        assert_eq!(offsets.func_refs().begin() % p, 0);
        assert_eq!(offsets.startup_func_ref() % p, 0);
        assert_eq!(offsets.runtime_data_bases().begin() % p, 0);
    }
}

Last updated: Oct 31 2026 at 23:03 UTC