bytecodealliance / bytecodealliance/wasmtime
Idea: put VM data structures in arena with relative pointers for security and strict provenance
- Dominant language
- Rust
- Stars
- 18.6k
- Forks
- 1.8k
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 126
Description
As part of the discussion on #9015 / #9026, we discussed handling of VM data structures -- the vmctx struct, tables, function references, and the like -- that are touched both by runtime code (in Rust) and by generated code compiled from the Wasm. There are issues related to strict pointer provenance because pointers to these data structures are exposed to the generated code, and/or the Pulley interpreter, without strict provenance (either through the Pulley bytecode, or through the machine code we invoke that is entirely outside of the domain of Rust's semantics).
It occurs to me that one way to solve this would be to make all VM data structures use *relative* pointers -- e.g., `u32` offsets -- in an arena (per store? per engine?) whose base pointer is a parameter both to the generated code and to the Pulley interpreter. We then trivially have strict provenance because there is *only one pointer* -- and whatever we need to do to preserve provenance (keep it as a pointer in the Pulley interpreter loop; and "expose" it as we pass it to generated code) is localized and manageable.
If provenance were the only benefit, that may not be so interesting; but there are a few others as well:
- Security: maintaining more discipline around raw pointers, and carefully dereferencing offsets into an arena instead (for which we can reserve guard regions just as we do heaps today), is a layer of mitigation/defense-in-depth against engine bugs. For example, if a miscompile or a bug in the generated CLIF caused a pointer-type confusion in VM data structures today, one could plausibly find a memory-read gadget or control-flow escape more easily. We will still have some raw pointers -- the actual code pointer that we invoke in funcrefs, or the memory base address in an imported or owned memory descriptor -- but fewer of them is less exposure.
This angle is not new: WebKit has the [Gigacage](https://phakeobj.netlify.app/posts/gigacage/), I believe V8 has something similar, and I had suggested we build (and now we are building) our Wasm GC implementation in the same way with relative-offset-pointers; so it's a proven mitigation and the overhead seems to be minimal.
Note also that relative-pointer loads/stores can be implemented with fully safe code in the Pulley interpreter. We almost certainly still need unsafe code still for the Wasm heap dereferences and such (though, then again, maybe there's a way around that by either externalizing a table of alternative raw pointer bases, or putting memories in a large gigacage -- teracage? exacage? -- as well).
- Performance: 32-bit relative-offset pointers are half the size of 64-bit raw machine pointers on 64-bit platforms; this is the basis for the performance gain seen with [compressed oops (object pointers)](https://wiki.openjdk.org/display/HotSpot/CompressedOops) in OpenJDK, and also with wasm32 vs. native 64-bit code in some pointer-heavy benchmarks. It's plausible that a 2x shrink on the size of large function tables (for example) might result in slightly better cache residency and performance. Then again carrying the base pointer and adding it has a slight cost (single-digit percentage by analogy to studies on Wasm heap strategies); so maybe overall neutral.
- There may be some other interesting side-effects: for example, if we fully relativize the core Wasm VM data structures, and externalize the "raw pointers" to a table as noted above, it would mean we could snapshot the entire VM state (at the VM level, not the Wasm level).
So it seems we can get (i) fully strict provenance, (ii) better safety, (iii) other interesting new abstractions like whole-engine snapshotting, if we pay this cost. Something to consider later if any of these needs becomes interesting?
Contributor guide
Assessment
This issue has not been assessed yet.