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

Panic about missing trampolines with subtyping in signatures #8432

Closed
alexcrichton opened this issue Apr 22, 2024 · 5 comments · Fixed by #8579
Closed

Panic about missing trampolines with subtyping in signatures #8432

alexcrichton opened this issue Apr 22, 2024 · 5 comments · Fixed by #8579
Labels
bug Incorrect behavior in the current implementation that needs fixing wasmtime:api Related to the API of the `wasmtime` crate itself

Comments

@alexcrichton
Copy link
Member

This program:

use wasmtime::*;

fn main() {
    let engine = Engine::default();

    let module = Module::new(
        &engine,
        r#"
            (module
                (import "" "a" (func $a (result externref)))

                (func (export "a")
                    (drop (call $a))
                )
            )
        "#,
    )
    .unwrap();

    let mut linker = Linker::new(&engine);
    linker
        .func_wrap("", "a", |mut caller: Caller<'_, ()>| {
            ExternRef::new(&mut caller, 100)
        })
        .unwrap();

    let mut store = Store::new(&engine, ());
    let i = linker.instantiate(&mut store, &module).unwrap();
}

compiled against the main branch currently runs as:

$ cargo run -q
thread 'main' panicked at /home/alex/.cargo/git/checkouts/wasmtime-41807828cb3a7a7e/4b9f53a/crates/wasmtime/src/runtime/func.rs:1306:74:
must have a wasm-to-native trampoline for this signature if the Wasm module is importing a function of this signature
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

cc @fitzgen

@alexcrichton alexcrichton added bug Incorrect behavior in the current implementation that needs fixing wasmtime:api Related to the API of the `wasmtime` crate itself labels Apr 22, 2024
@fitzgen
Copy link
Member

fitzgen commented Apr 22, 2024

As discussed offline, we should look up the trampoline based on the import signature, not the wrapped function's type (which can be a subtype of the import signature).

@shamiao
Copy link

shamiao commented Apr 24, 2024

also happens when externref appeared as a func param.

application note: unsafe fn Linker::func_new_unchecked can be used to bypass this panic.

@fitzgen
Copy link
Member

fitzgen commented May 1, 2024

I started poking at this. Things are a bit hairy because we update FuncData based on whether we have found a trampoline for a host function or not yet, but we could update that to hold onto a trampoline for some supertype of the function which is compatible with that particular use but "incompatible" with some other use that uses the function as its precise type rather than the previous super type. I put "incompatible" in scare quotes because it isn't actually incompatible because the trampolines aren't doing anything with these values other than shuffling them around to different ABI argument/return registers and stack slots. So any trampoline we find would be valid for all future uses, at least as the system currently exists, but this is a pretty subtle invariant to rely upon almost by accident.

I think the best way to move forward is to center that invariant, and not make it an accidental after thought, by

  • keeping track of every function type's "trampoline signature" in ModuleTypes or somewhere around there
  • a "trampoline signature" is where we map all params/results from (ref null? <heapty>) to (ref null <top(heapty)>)
    • (note that this is slightly different from the least-upper-bound of all concrete function types that this function type has a partial ordering with; that would require mapping params to bottom and results to top, which seems like a bit of a footgun if we ever give cranelift more knowledge of uninhabited types. instead, we are relying on implicit unsafe downcasts from the top type to the function's actual type for arguments here, which seems fine given that we control representation and otherwise have type/safety checks on calls)
  • we only generate trampolines for each trampoline signature, not every function signature
    • for modules without any reference types and subtyping, which is 99.9999% of modules, this will be the exact same number of trampolines as today
    • for modules with lots of reference types and subtyping, this will actually dedupe trampolines that are byte-for-byte identical today
  • looking up a trampoline is now a two-step process, where we introduce a step just before our existing type-to-trampoline lookup:
    1. (new) get the VMSharedTypeIndex of this function's type's trampoline signature
    2. (existing) get the trampoline associated with that type index

@alexcrichton
Copy link
Member Author

I like the sound of that yeah and it makes sense to me!

@fitzgen
Copy link
Member

fitzgen commented May 6, 2024

(I've been working on a patch, still have a few kinks to work out)

fitzgen added a commit to fitzgen/wasmtime that referenced this issue May 7, 2024
Previously, we would look up a wasm-to-native trampoline in the Wasm module
based on the host function's type. With Wasm GC and subtyping, this becomes
problematic because a Wasm module can import a function of type `T` but the host
can define a function of type `U` where `U <: T`. And if the Wasm has never
defined type `U` then it wouldn't have a trampoline for it. But our trampolines
don't actually care, they treat all reference values within the same type
hierarchy identically. So the trampoline for `T` would have worked in
practice. But once we find a trampoline for a function, we cache it and reuse it
every time that function is used in the same store again. Even if the function
is imported with its precise type somewhere else. So then we would have a
trampoline of the wrong type. But this happened to be okay in practice because
the trampolines happen not to inspect their arguments or do anything with them
other than forward them between calling convention locations. But relying on
that accidental invariant seems fragile and like a gun aimed at the future's
feet.

This commit makes that invariant non-accidental, centering it and hopefully
making it less fragile by doing so, by making every function type have an
associated "trampoline type". A trampoline type is the original function type
but where all the reference types in its params and results are replaced with
the nullable top versions, e.g. `(ref $my_struct)` is replaced with `(ref null
any)`. Often a function type is its own associated trampoline type, as is the
case for all functions that don't have take or return any references, for
example. Then, all trampoline lookup begins by first getting the trampoline type
of the actual function type, or actual import type, and then only afterwards
finding for the pre-compiled trampoline in the Wasm module.

Fixes bytecodealliance#8432

Co-Authored-By: Jamey Sharp <jsharp@fastly.com>
fitzgen added a commit to fitzgen/wasmtime that referenced this issue May 7, 2024
Previously, we would look up a wasm-to-native trampoline in the Wasm module
based on the host function's type. With Wasm GC and subtyping, this becomes
problematic because a Wasm module can import a function of type `T` but the host
can define a function of type `U` where `U <: T`. And if the Wasm has never
defined type `U` then it wouldn't have a trampoline for it. But our trampolines
don't actually care, they treat all reference values within the same type
hierarchy identically. So the trampoline for `T` would have worked in
practice. But once we find a trampoline for a function, we cache it and reuse it
every time that function is used in the same store again. Even if the function
is imported with its precise type somewhere else. So then we would have a
trampoline of the wrong type. But this happened to be okay in practice because
the trampolines happen not to inspect their arguments or do anything with them
other than forward them between calling convention locations. But relying on
that accidental invariant seems fragile and like a gun aimed at the future's
feet.

This commit makes that invariant non-accidental, centering it and hopefully
making it less fragile by doing so, by making every function type have an
associated "trampoline type". A trampoline type is the original function type
but where all the reference types in its params and results are replaced with
the nullable top versions, e.g. `(ref $my_struct)` is replaced with `(ref null
any)`. Often a function type is its own associated trampoline type, as is the
case for all functions that don't have take or return any references, for
example. Then, all trampoline lookup begins by first getting the trampoline type
of the actual function type, or actual import type, and then only afterwards
finding for the pre-compiled trampoline in the Wasm module.

Fixes bytecodealliance#8432

Co-Authored-By: Jamey Sharp <jsharp@fastly.com>
github-merge-queue bot pushed a commit that referenced this issue May 8, 2024
* wasmtime(gc): Fix wasm-to-native trampoline lookup for subtyping

Previously, we would look up a wasm-to-native trampoline in the Wasm module
based on the host function's type. With Wasm GC and subtyping, this becomes
problematic because a Wasm module can import a function of type `T` but the host
can define a function of type `U` where `U <: T`. And if the Wasm has never
defined type `U` then it wouldn't have a trampoline for it. But our trampolines
don't actually care, they treat all reference values within the same type
hierarchy identically. So the trampoline for `T` would have worked in
practice. But once we find a trampoline for a function, we cache it and reuse it
every time that function is used in the same store again. Even if the function
is imported with its precise type somewhere else. So then we would have a
trampoline of the wrong type. But this happened to be okay in practice because
the trampolines happen not to inspect their arguments or do anything with them
other than forward them between calling convention locations. But relying on
that accidental invariant seems fragile and like a gun aimed at the future's
feet.

This commit makes that invariant non-accidental, centering it and hopefully
making it less fragile by doing so, by making every function type have an
associated "trampoline type". A trampoline type is the original function type
but where all the reference types in its params and results are replaced with
the nullable top versions, e.g. `(ref $my_struct)` is replaced with `(ref null
any)`. Often a function type is its own associated trampoline type, as is the
case for all functions that don't have take or return any references, for
example. Then, all trampoline lookup begins by first getting the trampoline type
of the actual function type, or actual import type, and then only afterwards
finding for the pre-compiled trampoline in the Wasm module.

Fixes #8432

Co-Authored-By: Jamey Sharp <jsharp@fastly.com>

* Fix no-std build

---------

Co-authored-by: Jamey Sharp <jsharp@fastly.com>
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 wasmtime:api Related to the API of the `wasmtime` crate itself
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants