Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GC] id from different slab #10397

Closed
vouillon opened this issue Mar 14, 2025 · 5 comments · Fixed by #10456
Closed

[GC] id from different slab #10397

vouillon opened this issue Mar 14, 2025 · 5 comments · Fixed by #10456
Labels
bug Incorrect behavior in the current implementation that needs fixing

Comments

@vouillon
Copy link

Test Case

slab-mismatch.zip

Steps to Reproduce

Run the following command:

./target/debug/wasmtime  -W=all-proposals=y slab-mismatch.wasm

Expected Results

It should not crash.

Actual Results

We get a panic:

thread 'main' panicked at /home/jerome/sources/wasmtime/crates/slab/src/lib.rs:387:14:
id from different slab

Versions and Environment

Wasmtime commit: a3381e4

Operating system: Linux

Architecture: x64

@vouillon vouillon added the bug Incorrect behavior in the current implementation that needs fixing label Mar 14, 2025
@fitzgen
Copy link
Member

fitzgen commented Mar 17, 2025

Thanks for filing this bug!

@fitzgen
Copy link
Member

fitzgen commented Mar 18, 2025

Minimized a bit further:

(module
  (type $func (func))
  (type $array (array (mut i32)))
  (type $struct (sub (struct (field $field (ref $func)))))

  (elem func $nop)
  (func $nop)

  (func (export "")
    (local $local_array (ref $array))
    (local $local_struct (ref $struct))
    (local.set $local_struct (struct.new $struct (ref.func $nop)))
    (loop $outer
      (local.set $local_array (array.new $array (i32.const 0) (i32.const 1)))
      (loop $inner
        (array.set $array (ref.cast (ref $array) (local.get $local_array))
                          (i32.const 0)
                          (i32.const 1))
        (br_if $inner (i32.const 0))
      )
      (call_ref $func (struct.get $struct $field (local.get $local_struct)))
      (br $outer)
    )
  )
)

There are a couple interesting things that can't be removed from the test case while preserving the crash:

  • the br_if whose operand is a constant 0
  • the ref.cast into the exact same type that it already is

fitzgen added a commit to fitzgen/wasmtime that referenced this issue Mar 21, 2025
We were previously propagating the needs-inclusion-in-stack-maps bit from a
variable to the values resulting from `builder.use_var(my_var)` and passed to
`builder.def_var(my_var, my_val)`. However, that does not cover every
`ir::Value` generated for a given `ir::Variable`: it is missing block parameters
that the SSA builder inserted and which are *not* then used directly as the
result of `use_var`, but instead passed as arguments another block, and that
block's parameter is then directly or indirectly used as the result of
`builder.use_var(...)`. This chaining can go arbitrarily deep, based on the
program's control flow. By not marking these block parameters as needing
inclusion in stack maps, our stack maps would be incomplete, which meant that
the GC could too-eagerly collect objects, which meant that we would then get
use-after-free bugs.

The solution is for the `SSABuilder` to report all the block parameters that it
inserted, and for which `ir::Variable` it inserted them. We already have a
summary of the mutations that an `SSABuilder` performed, `SideEffects`, and
which must be handled by the `FunctionBuilder` after, e.g., sealing a block, so
we add this block-param-and-var information to it. Now, after the
`FunctionBuilder` has the `SSABuilder` do its thing, it handles fixing up not
only the modified blocks' status, which it was doing before, but also propagates
the needs-inclusion-in-stack-maps metadata from variables to all those
freshly-inserted block parameter values.

After this change, we successfully include *all* of a
needs-inclusion-in-stack-maps variable's values in stack maps, which avoids the
over-eager object reclamation and associated GC heap corruption.

Fixes bytecodealliance#10397
@fitzgen
Copy link
Member

fitzgen commented Mar 21, 2025

I have a fix for this over in #10456

Thanks again for the bug report!

@vouillon
Copy link
Author

Thanks a lot!
I'm not sure the fix is complete, though. I still get similar errors (id from different slab and assertion failed: types.is_subtype(actual_ty, expected_ty)). I'll try to find the time to produce a reduced test case.

@fitzgen
Copy link
Member

fitzgen commented Mar 21, 2025

Yeah, we sandbox the GC heap, so when we have any kind of GC heap corruption bug, it tends to look like those panics rather than overwriting host memory or what have you. Occasionally it might look like a SIGILL at a ud2 instruction because of an assertion check inside of JIT code tripping. But the important thing is that corruption in the GC heap should never result in memory unsafety in the rest of the runtime, just panics or whatever.

Anyways, the point is, it is likely not the same bug if it still triggers after #10456, but just that all kinds of GC heap corruption tend to show the same symptoms.

But as always, reduced test cases are super helpful, thanks for taking the time to make reproducers and all that!

github-merge-queue bot pushed a commit that referenced this issue Mar 24, 2025
We were previously propagating the needs-inclusion-in-stack-maps bit from a
variable to the values resulting from `builder.use_var(my_var)` and passed to
`builder.def_var(my_var, my_val)`. However, that does not cover every
`ir::Value` generated for a given `ir::Variable`: it is missing block parameters
that the SSA builder inserted and which are *not* then used directly as the
result of `use_var`, but instead passed as arguments another block, and that
block's parameter is then directly or indirectly used as the result of
`builder.use_var(...)`. This chaining can go arbitrarily deep, based on the
program's control flow. By not marking these block parameters as needing
inclusion in stack maps, our stack maps would be incomplete, which meant that
the GC could too-eagerly collect objects, which meant that we would then get
use-after-free bugs.

The solution is for the `SSABuilder` to report all the block parameters that it
inserted, and for which `ir::Variable` it inserted them. We already have a
summary of the mutations that an `SSABuilder` performed, `SideEffects`, and
which must be handled by the `FunctionBuilder` after, e.g., sealing a block, so
we add this block-param-and-var information to it. Now, after the
`FunctionBuilder` has the `SSABuilder` do its thing, it handles fixing up not
only the modified blocks' status, which it was doing before, but also propagates
the needs-inclusion-in-stack-maps metadata from variables to all those
freshly-inserted block parameter values.

After this change, we successfully include *all* of a
needs-inclusion-in-stack-maps variable's values in stack maps, which avoids the
over-eager object reclamation and associated GC heap corruption.

Fixes #10397
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Incorrect behavior in the current implementation that needs fixing
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants