A game project i am interested in ( unvanquished ) is going to switch to wasm for its scripting code, which it runs on its own process. I am wondering whether safety checks ( function pointer and bounds check among others ) could be turned off in that situation, to improve performance
There's some discussion in a sibling thread about this too, but in general the answer is "no", wasm safety checks can't be turned off because then that wouldn't be wasm but something else entirely.
That being said if you have test cases which perform significantly worse than native (e.g. 50%+) please feel free to open an issue and we can take a look. For example just the other day we fixed a 4x slowdown for a small loop which turned out to be a low-level register issue.
@Alex Crichton there are sandboxing checks that you have to make in order to guarantee safety, but if you want to give up safety for performance, a lot can be done. There are still some fundamental limitations with the WASM instruction set, but compiling let's say via LLVM (I think WasmEdge is doing that) with no runtime checks and all optimizations enabled can get quite far I think.
Certainly yes wasm has some checks which can be omitted, but at least in my opinion it's not wasm at that point, it's something else entirely.
It is this point that I want some clarity. If it starts with WASM, and it runs in wasmtime, and then it runs in this "something else entirely" and produces the same answer, why is it not WASM anymore?
In other words, why cannot WASM be defined as "a given .wasm file runs in wasmtime and doesn't fail."? And as long as this is satisfied, we are free to create "fast runtime" that gives up safety for performance. You can always run the same wasm file in wasmtime and get all the safety guarantees.
There are many situations where wasm would run in a dedicated process. If the os is of high quality and hardware bugs are absent or mitigated why not disable wasm security checks then?
is what i am saying, unless disabling wasm security checks can change the semantics of the code or something like that
In my mind the difference is the possible inputs. To be the same as wasm you have to perform the exact same as the wasm under all possible inputs, not just one. For example if one input causes wasm to report a call_indirect
signature mismatch but an "unchecked" build went and crashed a different way, that's how they're different.
If call_indirect
never crashes for a wasm build, then an "unchecked" build was applying an optimization that the wasm compiler wasn't smart enough to do but could have done, but that's a different realm of optimization as opposed to simply turning off checks.
Disabling wasm checks can indeed change semantics, for example it could cause crashes to happen at different times which means that any amount of other code could run in the meantime. For example host imports could be called after what would have otherwise been a crash
an overly simple example would be:
if array[huge_out_of_bounds_index] == 0 {
tell_the_host_my_secret_key(&key);
}
wasm guarantees that tell_the_host_my_secret_key
will never run, whereas an "unchecked build" would probably run that
What about if use a safe language like lets say haskell or even safer, would the semantics still change in practice?
Haskell, idris2, etc already manage memory safely and check function pointer usage so wouldnt wasm checks be redundant then?
IMO no, that doesn't change the calculus. Languages have bugs, and albeit for big languages like that they're probably rare they still do happen. Wasm's checks are an extra layer of protection against them.
For example if you're comfortable trusting Haskell why not run Haskell natively?
Portable executables and being able to target the web with the same setup
True! You won't ever be able to turn off the wasm checks on the web, however, and isn't part of the portability story the same behavior on-and-off the web?
Ideally safe languages like haskell, idris2, rust, etc could ask the web assembly runtimes to disable wasm checks. Telling their runtime that its safe code so the semantic wont change.
Even runtimes with this feature could refuse to turn off checks, so the code promises its equivalent whether or not they are present
I think this is in the same spirit as enabling multi threading. Code with race conditions will behave different with threading than without
I have a lot of experience with precisely this thing:
if array[huge_out_of_bounds_index] == 0 {
tell_the_host_my_secret_key(&key);
}
and in Fortran for example (and LFortran), the workflow is that you first compile in Debug mode with bounds checking on. If your code runs for a given input without any runtime errors, then you can turn the bounds checking off and run fast. Then if in production things misbehave due to out of bounds (quite rare actually in my experience for computational physics codes), you recompile and re-run in Debug mode.
Here is a similar example. LPython has the following guarantee / contract: if your Python code compiles in LPython and runs in Debug mode without any runtime errors (such as bounds checking), then we guarantee that exactly the same code on exactly the same input also runs in CPython and produces the same answer (as well as when recompiled with LPython in ReleaseFast mode). Would it mean that LPython is not Python? Well, it's not exactly the same as CPython, but given the contract, it definitely guarantees some subset of Python.
Coming back to WASM. Take our existing fast WASM->x64 (x86_64) backend. We guarantee the following (assuming no bugs in our compiler, which currently is not satisfied): if a given LFortran's code compiles and runs in Debug mode via our WASM->x64 backend and produces the right answer without any runtime errors, then the same a.wasm
should run in wasmtime and produce the same answer (as well as running in the browser!), as well as in our WASM->x64 backend in ReleaseFast mode. For the same inputs. In my mind we are supporting a subset of WASM, in a well defined sense of this paragraph.
I think the point is that your "WASM->x64 backend" isn't a wasm backend, its a "subset of wasm produced by certain compilers" backend. It could not (presumably) compile any arbitrary wasm input into a binary that follows the wasm execution spec. That is fine in an integrated toolchain, but you wouldn't want to use it for potentially-malicious input as many wasm runtimes expect.
@Lann Martin yes, you definitely can't use it for malicious input. You can't use our ReleaseFast backend (even via LLVM) with malicious input. Only Debug and ReleaseSafe. Yes, I think you are right: it's not a WASM backend that can accept malicious wasm file or malicious input. It's a "subset of WASM produced by certain compilers".
So to the other points above, it could be fine to carve out some subset of Wasm to target as an LLVM-like IR, just please don't call it "web assembly" :smile:
@Lann Martin if you can help find some good name for this that would be great. I have a feeling that when you (by "you" I mean the WASM community) say "web assembly" you mean not just the ".wasm" format, but also all the guarantees and ability to accept and run malicious code. Correct?
I would mostly defer to others here on something like that, but something like "Unsafe Wasm" (.uwasm) might be ok :shrug:
"Unsafe WASM" is fine with me. It would be nice to have a term for this, to streamline all discussions related to it.
and just to clarify, I strongly doubt that wasmtime would support this in the foreseeable future
We can continue maintaining our own backend for "Unsafe WASM", I am not asking that wasmtime does it.
One thing that I think is missing from the above discussion: the "bounds checks" are actually built into the semantics of Wasm. Even a subset of Wasm -- say, Wasm modules that would not have trapped in the original execution -- require the following:
The Wasm abstraction for memory is a 32-bit (for now) region and Wasm pointers are offsets into the region. The fastest way we know how to compile this is to have a 4GB+guard area of virtual memory in the host process, and compile all Wasm memory accesses to heap_base + offset
. That heap_base
needs to live in a register, and the extra addition makes addressing modes a little more expensive (e.g. if the original program could do [base + index*4]
on x86, we have to compute tmp = base + index*4
then do [heap_base + tmp]
.)
Note here that the slowdown is related to the semantics, not to the checks. We could make the region much smaller, and put unrelated data within easy reach of heap_base
with no bounds-checks at all; and we still have the slowdown, because it comes from the "hidden heap base" semantics.
The Wasm abstraction for indirect control flow is a br_table
instruction that has a list of target blocks, and a "default" that we branch to if the given index is out-of-bounds. You might say: avoid the bounds-check, and never branch to the default. Except that that is not a never-expected-to-happen-in-safe-executions behavior: it's a builtin feature of the instruction, just like a default:
label in a C switch statement, and is used all the time.
If you implement br_table
in a way that executes ordinary (safe, non-trapping) Wasm programs correctly, you have to implement a compare-and-use-default-if-over-limit behavior. And once you have that, you have the bounds-check.
So what I'm trying to say is: there is no actual way to turn off bounds checks, because the result wouldn't be Wasm. I don't mean that in a philosophical "it wouldn't have the guarantees we like to talk about" way, I mean that in a "it wouldn't run your program correctly because it wouldn't implement the same instruction set" way. Does that make sense?
So what I'm trying to say is: there is no actual way to turn off bounds checks, because the result wouldn't be Wasm. I don't mean that in a philosophical "it wouldn't have the guarantees we like to talk about" way, I mean that in a "it wouldn't run your program correctly because it wouldn't implement the same instruction set" way. Does that make sense?
Yes, the "implicit" bounds checks required by the WASM instructions themselves cannot be turned off (you gave two examples: "hidden heap base", br_table
, and there is probably more), and even "Fast/Unsafe WASM" must implement those. Consequently, these might impose some (slight) limitations on the maximum performance for "ReleaseFast" mode (fastest runtime, no checks), and so WASM might not be the best fit there. On the other hand, for ReleaseSafe (fast runtime and checks) this might be a perfect fit, using wasmtime for example. And for Debug mode (fast compilation, checks, slower runtime), having our custom fast-to-compile WASM backend might be a good fit.
Maybe instead of supporting a subset of wasm ( programs that will never go out of bounds or otherwise have memory errors), extra memory instructions could be added whose checking cost completely goes away when safety checks are not necessary. I imagine such instructions would be slower to implement otherwise but they could still be useful
Last updated: Dec 23 2024 at 12:05 UTC