-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Investigate use of stack probes and removal of explicit stack-limit checks #8135
Comments
Some other bits I've thought of after more thinking:
|
Also I'm completely wrong about sigaltstack: we are currently installing a sigaltstack already |
This commit enhances the `host_always_has_some_stack` a bit in light of some thinking around bytecodealliance#8135. Notably this test ensures that the host itself never segfaults even if the wasm exhausts all of its stack. There are a number of ways that we can exit out to the host, though, and only one was tested previously. This commit updates to ensure more cases are covered.
Thanks for filling in a ton of details here! A few thoughts:
|
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI.
I've been nerd sniped.
I'm a bit worried about the complexity of this because libcalls are completely invisible to Wasmtime here. In that sense it's really late in the x64 backend that we realize that a libcall is required and at that point we've lost a lot of context necessary to generate a libcall. I think the best solution here will be to implement something like #8152 but for cranelift-generated libcalls as well. That way we can, after compilation, see what libcalls are required and then go generate a small shim for each libcall. This also handles things like the fp/pc of a wasm module because we can't do that inline but we instead need an actual function frame to record the exit fp/pc (I forget the exact reasons for this but it's why we had to have separate trampolines for builtin functions in wasmtime)
Hopefully handled through #8152 now!
For <1page functions, I implemented this which basically tests whether the faulting address is within one page of the stack pointer. My hope is that this is precise enough to say "yeah it's a stack overflow" and also easy enough that we don't need metadata on every possible instruction that modifies the stack in Cranelift (e.g. I don't want to have to rearrange the stack args/clobbers/etc). The main thing that I'm worried about is a situation like:
where if the first I suppose put another way, I'd like to ideally frame the problem as: All stack overflows caught through the guard page can either be classified as:
|
(sorry hit comment a bit too soon so I added a little bit more context at the end) |
Fortunately we seem to decrement rsp and then store in stages. The comment there cites Valgrind compatibility, which I think specifically means that rsp is decremented first; I don't know if there's a reason we do individual decrements per store on x86, though on aarch64/riscv64 it would allow us to use store-with-immediate-offset insts with small offset fields. In any case, we do seem to already have this "fault is close to actual current SP" property already, which is nice! |
(And likewise the dynamic loop version seems to have the same property) |
Oh nice, so then if we're comfortable saying all faults within 1 page of rsp are stack-overflow related faults, I think all we need to do is turn on stack probes and call it a day then? |
You would know better than me but that sounds plausible, given your work on helper calls in the linked PR! |
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI.
This commit enhances the `host_always_has_some_stack` a bit in light of some thinking around bytecodealliance#8135. Notably this test ensures that the host itself never segfaults even if the wasm exhausts all of its stack. There are a number of ways that we can exit out to the host, though, and only one was tested previously. This commit updates to ensure more cases are covered.
I poked a bit more at this, specifically with respect to cranelift-injected libcalls (e.g.
I got all that wired up and working and AFAIK that covers the cases that at least I could think of where the guard page will now guaranteed be hit in Cranelift-generated code. Stepping back a bit though, I thought a little more about this at a higher level. I ran sightglass's default suite and it showed a 3-4% improvement in execution on spidermonkey.wasm, but no changes elsewhere. I'm not sure if it would show a larger improvement in the situations that you're looking at @cfallin though if they're call-heavy benchmarks. I'm a bit worried about the complexity of this change. Lots about Cranelift/Wasmtime are striking the right balance between simplicity and performance, and I don't think there's any silver bullet for handling stack overflow in wasm. The stack limit check is quite simple but not exactly speedy. Using a guard page feels pretty complex and subtle because we need a guarantee that only Cranelift-generated code hits the guard page, never native code. I've done my best to enumerate all the possible ways that I personally know of we need to handle but I have little confidence that my knowledge is exhaustive, for example I haven't analyzed components yet to see if they need updates too. More-or-less I think we should probably have more compelling performance data motivating this change before pursuing it further. If 3-4% breaks the bank then that seems ok to pursue, but in abstract 3-4% feels pretty small and perhaps not worth the complexity. If there's a 40% improvement in a different benchmark though that's of course more compelling. |
@alexcrichton thanks a bunch for pushing this further! I agree it's best to do this only if it actually matters. In my own work I can at least test your branch if it's available, once I get ICs (lots of tiny function calls) wired up properly, and give more data; it'd have to be worth 1.5x-2x or so in my mind. |
This is the (messy) branch with various bits of work, but for performance testing I'd just comment out this line since the test case probably doesn't rely on catching stack overflow anyway. |
* Enhance test around stack overflow This commit enhances the `host_always_has_some_stack` a bit in light of some thinking around #8135. Notably this test ensures that the host itself never segfaults even if the wasm exhausts all of its stack. There are a number of ways that we can exit out to the host, though, and only one was tested previously. This commit updates to ensure more cases are covered. * Fix test to test correct thing * Update tests/all/stack_overflow.rs Co-authored-by: Nick Fitzgerald <fitzgen@gmail.com> --------- Co-authored-by: Nick Fitzgerald <fitzgen@gmail.com>
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full
* Exit through Cranelift-generated trampolines for builtins This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by #8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full * Fix some build issues * Update winch test expectations * Update Winch to use new builtin shims. This commit refactors the Winch compiler to use the new trampolines for all Wasmtime builtins created in the previous commits. This required a fair bit of refactoring to handle plumbing through a new kind of relocation and function call. Winch's `FuncEnv` now contains a `PrimaryMap` from `UserExternalNameRef` to `UserExternalName`. This is because there's now more than one kind of name than just wasm function relocations, so the raw index space of `UserExternalNameRef` is no longer applicable. This required threading `FuncEnv` to more locations along with some refactorings to ensure that lifetimes work out ok. The `CompiledFunction` no longer stores a trait object of how to map name refs to names and now directly has a `Primarymap`. This also means that Winch's return value from its `TargetIsa` is a `CompiledFunction` as opposed to the previous just-a-`MachBuffer` so it can also package up all the relocation information. This ends up having `winch-codegen` depend on `wasmtime-cranelift-shared` as a new dependency. * Review feedback
…liance#8152) * Exit through Cranelift-generated trampolines for builtins This commit changes how builtin functions in Wasmtime (think `memory.grow`) are implemented. These functions are required to exit through some manner of trampoline to handle runtime requirements for backtracing right now. Currently this is done via inline assembly for each architecture (or external assembly for s390x). This is a bit unfortunate as it's a lot of hand-coding and making sure everything is right, and it's not easy to update as it's multiple platforms to update. The change in this commit is to instead use Cranelift-generated trampolines for this purpose instead. The path for invoking a builtin function now looks like: * Wasm code calls a statically known symbol for each builtin. * The statically known symbol will perform exit trampoline duties (e.g. pc/fp/etc) and then load a function pointer to the host implementation. * The host implementation is invoked and then proceeds as usual. The main new piece for this PR is that all wasm modules and functions are compiled in parallel but an output of this compilation phase is what builtin functions are required. All builtin functions are then unioned together into one set and then anything required is generated just afterwards. That means that only one builtin-trampoline per-module is generated per-builtin. This work is inspired by bytecodealliance#8135 and my own personal desire to have as much about our ABI details flowing through Cranelift as we can. This in theory makes it more flexible to deal with future improvements to our ABI. prtest:full * Fix some build issues * Update winch test expectations * Update Winch to use new builtin shims. This commit refactors the Winch compiler to use the new trampolines for all Wasmtime builtins created in the previous commits. This required a fair bit of refactoring to handle plumbing through a new kind of relocation and function call. Winch's `FuncEnv` now contains a `PrimaryMap` from `UserExternalNameRef` to `UserExternalName`. This is because there's now more than one kind of name than just wasm function relocations, so the raw index space of `UserExternalNameRef` is no longer applicable. This required threading `FuncEnv` to more locations along with some refactorings to ensure that lifetimes work out ok. The `CompiledFunction` no longer stores a trait object of how to map name refs to names and now directly has a `Primarymap`. This also means that Winch's return value from its `TargetIsa` is a `CompiledFunction` as opposed to the previous just-a-`MachBuffer` so it can also package up all the relocation information. This ends up having `winch-codegen` depend on `wasmtime-cranelift-shared` as a new dependency. * Review feedback
Currently, Wasmtime uses explicit stack-limit checks to avoid overflowing the stack, and these checks are generated in the prologue of every Wasm function. This is largely a consequence of the design decision to run on the host's stack (when not doing async): the two subcases are either Wasm hits the guard page itself (and we don't currently have a mechanism to recover from that, with a separate signal stack) or Wasm uses almost all the stack then calls back into the host and the host then overflows (which looks like a host crash). So the advantage of explicit stack-limit checks is that they retain control while giving us a well-defined path to return a trap, but the cost is that they impose overhead on every function call. (And potentially nontrivial overhead: the limit is several chained loads deep in data structures.)
The alternative idea is to switch to reliance on a guard page, with stack probes in functions that have frames larger than a single guard page (just as with native code today), and handle the two cases above by:
(Not coincidentally, I believe this is also the exact scheme that was used by Lucet!)
The text was updated successfully, but these errors were encountered: