wasmi
wasmi copied to clipboard
Add Wasm `coredump` builder
Implements https://github.com/wasmi-labs/wasmi/issues/538.
This is the first step to adding support for coredumps. The PR introduces a wasmi::coredump module with serialize function. The function can be used to serialize the given process info as coredump. For example:
let frame = Frame {
func_idx: 6,
code_offset: 123,
};
let thread = Thread {
name: "main",
frames: &[frame],
};
let proc = Process {
name: "/usr/bin/true.exe",
threads: &[thread],
memories: &[Memory { min: 0, max: None }],
data: &[],
};
let mut coredump = Vec::new();
serialize(&mut coredump, &proc);
The provided code is not used anywhere just yet.
Related to #538
I also have a few simple smoke tests but they use third-party std dependencies, so I decided to not bring them into wasmi.
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn test_write_u64() {
use super::write_u64;
for x in &[0u64, 1, 2, 3, 4, 10, 21, 49, 1033, 1231245, u64::MAX] {
let mut act = Vec::new();
write_u64(&mut act, *x);
let mut exp = Vec::new();
leb128::write::unsigned(&mut exp, *x).unwrap();
assert_eq!(act, exp);
}
}
#[test]
fn test_serialize() {
let frame = Frame {
func_idx: 6,
code_offset: 123,
};
let thread = Thread {
name: "main",
frames: &[frame],
};
let proc = Process {
name: "/usr/bin/true.exe",
threads: &[thread],
memories: &[Memory { min: 0, max: None }],
data: &[],
};
let mut act = Vec::new();
serialize(&mut act, &proc);
use wasm_coredump_builder::*;
let mut coredump = CoredumpBuilder::new().executable_name("/usr/bin/true.exe");
let mut thread = ThreadBuilder::new().thread_name("main");
let frame = FrameBuilder::new().codeoffset(123).funcidx(6).build();
thread.add_frame(frame);
coredump.add_thread(thread.build());
let exp = coredump.serialize().unwrap();
assert_eq!(act, exp);
}
}
Also, clippy naturally fails because the code I've added is unused.
Codecov Report
:x: Patch coverage is 0% with 82 lines in your changes missing coverage. Please review.
:white_check_mark: Project coverage is 71.06%. Comparing base (9403447) to head (df7b6d5).
:warning: Report is 147 commits behind head on main.
| Files with missing lines | Patch % | Lines |
|---|---|---|
| crates/wasmi/src/coredump.rs | 0.00% | 82 Missing :warning: |
Additional details and impacted files
@@ Coverage Diff @@
## main #1461 +/- ##
==========================================
- Coverage 71.40% 71.06% -0.34%
==========================================
Files 162 163 +1
Lines 16420 16502 +82
==========================================
+ Hits 11724 11727 +3
- Misses 4696 4775 +79
:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
I also have a few simple smoke tests but they use third-party std dependencies, so I decided to not bring them into wasmi.
#[cfg(test)] mod tests { use crate::*; #[test] fn test_write_u64() { use super::write_u64; for x in &[0u64, 1, 2, 3, 4, 10, 21, 49, 1033, 1231245, u64::MAX] { let mut act = Vec::new(); write_u64(&mut act, *x); let mut exp = Vec::new(); leb128::write::unsigned(&mut exp, *x).unwrap(); assert_eq!(act, exp); } } #[test] fn test_serialize() { let frame = Frame { func_idx: 6, code_offset: 123, }; let thread = Thread { name: "main", frames: &[frame], }; let proc = Process { name: "/usr/bin/true.exe", threads: &[thread], memories: &[Memory { min: 0, max: None }], data: &[], }; let mut act = Vec::new(); serialize(&mut act, &proc); use wasm_coredump_builder::*; let mut coredump = CoredumpBuilder::new().executable_name("/usr/bin/true.exe"); let mut thread = ThreadBuilder::new().thread_name("main"); let frame = FrameBuilder::new().codeoffset(123).funcidx(6).build(); thread.add_frame(frame); coredump.add_thread(thread.build()); let exp = coredump.serialize().unwrap(); assert_eq!(act, exp); } }
What is the difference between this PR and what wasm_coredump_builder does?
Tests would be nice but indeed 3rd party dependencies should be avoided. Ideally we had a few tests that do not depend on 3rd party dependencies.
@orsinium thanks a lot for your PR!
All in all this PR still needs some work before we can merge. How willing are you to do this? Otherwise I could drive this to finalization.
I also need to know what differs this from wasm_coredump_builder. Seems to me (superficially) that both crates more or less do the same thing.
The main question is how users would interact with this feature via Wasmi's API and what the performance implications are (if any) if enabled.
I suppose this serializes to this encoding: https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md right?
All in all this PR still needs some work before we can merge. How willing are you to do this?
I'd be happy to properly integrate it with wasmi but it's a big project and I don't know where to start. I hoped that Trap would maybe contain the instruction offset but it doesn't. I'm sure the interpreter knows which instruction caused the trap, so if you can put this information into the trap error, the problem is pretty much solved.
I also need to know what differs this from wasm_coredump_builder
I've added no_std support into wasm_coredump_builder but it still depends on leb128 which requires std. I tried to add no_std support there as well but they don't want to depend on embedded_io for std, and doing it that way would require so much work:
https://github.com/gimli-rs/leb128/issues/25
So, when I started to dig into both crates, I learned that they don't really do much. So, here is a simple implementation, without std or any dependencies. If you don't want to maintain it as part of wasmi, I can release it as a separate crate.
The main question is how users would interact with this feature via Wasmi's API and what the performance implications are (if any) if enabled.
My preferred API would be trap.write_coredump(&mut output) where Trap is trap and write_coredump is a wrapper around coredump::serialize. For that to work (for initial PoC), all Trap needs to know is the function and instruction offset that cause the trap. Using this information, we can form one Frame, put it into one Thread, put it into one Process, and serialize.
I suppose this serializes to this encoding: https://github.com/WebAssembly/tool-conventions/blob/main/Coredump.md right?
Yes! It's pretty much a wasm file with some custom sections.
Concerning your implementation questions: Wasmi does not really store a mapping of the actual functions on the call stack for performance reasons. However, with the information that you can find on the call stack it is possible to compute this information and create the backtrace. This will be slow but having a trap + backtrace situation usually is not a case that needs to be optimized for anyways as long as the backtrace eventually is created, right?
Here are the important parts of the Wasmi executor:
Executor: https://github.com/wasmi-labs/wasmi/blob/main/crates/wasmi/src/engine/executor/instrs.rs#L86CallStack: https://github.com/wasmi-labs/wasmi/blob/v0.44.0/crates/wasmi/src/engine/executor/stack/calls.rs#L24CallFrame: https://github.com/wasmi-labs/wasmi/blob/v0.44.0/crates/wasmi/src/engine/executor/stack/calls.rs#L196CodeMap: https://github.com/wasmi-labs/wasmi/blob/main/crates/wasmi/src/engine/code_map.rs#L79CompiledFuncEntity: https://github.com/wasmi-labs/wasmi/blob/main/crates/wasmi/src/engine/code_map.rs#L763
- The
Executor'sstackfield can be used to acquire the information about the current call stack. - The
CallStackhas a stack ofCallFrames. CallFrame::instr_ptris a pointer into the instructions of a function on the call stack. Thisinstr_ptrcan be used to backtrack the information about the concrete function on the stack by querying all theCodeMap's function entities and see for allCompiledFuncEntity(because uncompiled ones cannot be executed) which is the one that contains theinstr_ptrin itsinstrslice. This will require to split theinstrslice into its begin and end pointer and then see if they containinstr_ptrwithbegin >= instr_ptr && instr_ptr < end.CodeMapsimply stores all information about all Wasm functions, compiled and uncompiled. A function is uncompiled when lazy compilation is used and the function has not yet been executed.
- Though this won't provide you with the function's name since Wasmi does not (yet) support the Wasm name section and thus cannot associate functions to their names. This will only help to associate it with the index of the function in the
CodeMap. However, this index is not the same as the index of the function of the associated Wasm file. So probably not too helpful. - Information about the function's signature is missing because it is implied in Wasmi's bytecode and not needed for execution. I have no idea how we could backtrace this information. If it is critical we probably have to add it to the executor.
- Information about the functions
localsandstackcan also be backtraced to some extend. However, there currently is no information which parts of a function's stack are locals and non-locals. Again this information would be required to be added.
As you can see, adding backtrace support to Wasmi's executor is somewhat of a mess since Wasmi's executor is highly optimized for execution and nothing else.
Though this won't provide you with the function's name
We don't need the function name. All we need is the function index and the instruction index within the function. That's it, that's all we put in coredump. The function name, function signature, line of code etc are inferred by a third-party coredump inspector based on dwarf files or other debug info. It would be helpful to have values of locals and the call stack as well but it's totally fine to keep it out of PoC.
Though this won't provide you with the function's name
We don't need the function name. All we need is the function index and the instruction index within the function. That's it, that's all we put in coredump. The function name, function signature, line of code etc are inferred by a third-party coredump inspector based on dwarf files or other debug info. It would be helpful to have values of locals and the call stack as well but it's totally fine to keep it out of PoC.
- I suppose you need the Wasm function index. For that, as described in my last post, we'd have to add this field to the
CodeMapbecause right now this information is simply not available. - For the instruction index within the function you probably also mean the Wasm instruction index. This is very tricky and unlikely to be implemented easily since Wasm instructions are not Wasmi instructions. A single Wasmi instruction may represent multiple Wasm instructions. Some Wasmi instruction do not even have a Wasm counterpart at all. So it will be very hard to provide a proper mapping between the two. I think this information could be added via the Wasmi translator by memorizing all Wasm instruction indices of all Wasm instruction that may trap during translation. And then we simply store this buffer in the
CodeMapfor each function. This would allow us to backtrace this information by decoding the Wasmi instructions back to the trapping Wasm instructions and find the position within that buffer.
Concerning user facing API of how to get the Wasm coredump, I think we should simply do what Wasmtime does and embed WasmCoreDump to Wasmi's Error type when this option is enabled.
https://docs.rs/wasmtime/latest/wasmtime/struct.WasmCoreDump.html