Skip to content

Add Wasm coredump builder #1461

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

orsinium
Copy link
Contributor

@orsinium orsinium commented Apr 8, 2025

Implements #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

@orsinium
Copy link
Contributor Author

orsinium commented Apr 8, 2025

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);
    }
}

@orsinium
Copy link
Contributor Author

orsinium commented Apr 8, 2025

Also, clippy naturally fails because the code I've added is unused.

Copy link

codecov bot commented Apr 8, 2025

Codecov Report

Attention: Patch coverage is 0% with 82 lines in your changes missing coverage. Please review.

Project coverage is 71.06%. Comparing base (9403447) to head (df7b6d5).

Files with missing lines Patch % Lines
crates/wasmi/src/coredump.rs 0.00% 82 Missing ⚠️
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     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Robbepop
Copy link
Member

Robbepop commented Apr 8, 2025

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.

@Robbepop
Copy link
Member

Robbepop commented Apr 8, 2025

@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?

@orsinium
Copy link
Contributor Author

orsinium commented Apr 9, 2025

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:

gimli-rs/leb128#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.

@Robbepop
Copy link
Member

Robbepop commented Apr 9, 2025

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:


  • The Executor's stack field can be used to acquire the information about the current call stack.
  • The CallStack has a stack of CallFrames.
  • CallFrame::instr_ptr is a pointer into the instructions of a function on the call stack. This instr_ptr can be used to backtrack the information about the concrete function on the stack by querying all the CodeMap's function entities and see for all CompiledFuncEntity (because uncompiled ones cannot be executed) which is the one that contains the instr_ptr in its instr slice. This will require to split the instr slice into its begin and end pointer and then see if they contain instr_ptr with begin >= instr_ptr && instr_ptr < end.
  • CodeMap simply 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 locals and stack can 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.

@orsinium
Copy link
Contributor Author

orsinium commented Apr 9, 2025

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.

@Robbepop
Copy link
Member

Robbepop commented Apr 9, 2025

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 CodeMap because 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 CodeMap for 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.

@Robbepop Robbepop changed the title Add coredump builder Add Wasm coredump builder Apr 9, 2025
@Robbepop
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants