Skip to content

Latest commit

 

History

History
141 lines (116 loc) · 5.64 KB

demo.md

File metadata and controls

141 lines (116 loc) · 5.64 KB

This document goes through running my broken code in production and using wasmgdb to analyze a crash, and hopefully fix it.

My Rust code:

struct MyThing {
    value: usize,
}

fn main() {
    let thing = MyThing { value: 3 };
    process_thing(&thing)
}

fn process_thing(thing: &MyThing) {
    let result = calculate(thing.value);
    println!("result is {}", result);
}

fn calculate(value: usize) -> usize {
    value - 10
}

Compiling to Wasm: cargo build --target wasm32-wasi. Transforming the Wasm binary to support coredump (this step will hopefully be gone soon) using wasm-coredump-rewriter: wasm-coredump-rewriter < ./test-program/target/wasm32-wasi/debug/test-program.wasm > ./test-program/target/wasm32-wasi/debug/test-program-final.wasm.

Finally I can run my program using a Node & WASI environment:

$ node --experimental-wasi-unstable-preview1 runner.mjs

thread 'main' panicked at 'attempt to subtract with overflow', src/main.rs:16:5
stack backtrace:
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Oh no, what a suprise, the program crashed because of an exception in Rust. Annoyingly, we don't have more information (so far).

Note that Node isn't able to show us a stack trace due to the wasm-coredump-rewriter transformation. A coredump has been generated and written to a file called coredump.1669887752424.

From the output we can see that the program crashed at line 16, which is here:

...
fn calculate(value: usize) -> usize {
    /* line 16: */ value - 10
}

Now let's analyze the crash in wasmgdb to see where it went wrong (if you didn't spot it from my broken code already).

$ wasmgdb ./test-program/target/wasm32-wasi/debug/test-program.wasm coredump.*
wasmgdb>

We enter a repl similar to gdb, commands are mostly the same.

Let's examine a backtrace:

wasmgdb> bt
#18     000137 as __rust_start_panic () at library/panic_abort/src/lib.rs
#17     000129 as rust_panic () at library/std/src/panicking.rs
#16     000128 as rust_panic_with_hook () at library/std/src/panicking.rs
#15     000117 as {closure#0} () at library/std/src/panicking.rs
#14     000116 as __rust_end_short_backtrace<std::panicking::begin_panic_handler::{closure_env#0}, !> () at library/std/src/sys_common/backtrace.rs
#13     000123 as begin_panic_handler () at library/std/src/panicking.rs
#12     000194 as panic_fmt () at library/core/src/panicking.rs
#11     000198 as panic () at library/core/src/panicking.rs
#10     000012 as calculate (value=0x03000000) at src/main.rs
#9      000011 as process_thing (thing=0x2cff0f00) at src/main.rs
#8      000010 as main () at src/main.rs
#7      000008 as call_once<fn(), ()> (???=0x01000000, ???=0x00000000) at /rustc/b833ad56f46a0bbe0e8729512812a161e7dae28a/library/core/src/ops/function.rs
#6      000020 as __rust_begin_short_backtrace<fn(), ()> (f=0x01000000) at /rustc/b833ad56f46a0bbe0e8729512812a161e7dae28a/library/std/src/sys_common/backtrace.rs
#5      000016 as {closure#0}<()> () at /rustc/b833ad56f46a0bbe0e8729512812a161e7dae28a/library/std/src/rt.rs
#4      000077 as lang_start_internal () at library/std/src/rt.rs
#3      000015 as lang_start<()> (main=0x01000000, argc=0x00000000, argv=0x00000000, sigpipe=0x00620000) at /rustc/b833ad56f46a0bbe0e8729512812a161e7dae28a/library/std/src/rt.rs
#2      000013 as __original_main () at <directory not found>/<file not found>
#1      000005 as _start () at <directory not found>/<file not found>
#0      000264 as _start.command_export at <no location>

Each line represents a frame on the program's stack, let's take one frame and explain what we see:

#3      000015 as lang_start<()> (main=0x01000000, argc=0x00000000, argv=0x00000000, sigpipe=0x00620000) at /rustc/b833ad56f46a0bbe0e8729512812a161e7dae28a/library/std/src/rt.rs
 ^         ^         ^            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 |   Wasm funcidx    |                                    Func arguments                                                          Source location
 |               Func name
 |
frame number

Up the stack from frame #10 is Rust internals, which we can ignore for our example.

We can select a particular frame to further inspect it, let's see #9:

wasmgdb> f 9
000011 as process_thing (thing=0x2cff0f00) at src/main.rs

Inspect locals (includes Wasm func arguments):

wasmgdb> info locals
thing: *MyThing = 0xfff1c

We can see thing's content:

wasmgdb> p (*thing)
thing (0xfff2c): MyThing = {
        value (0xfff2c):        usize = 0x00000003
}

We can already see the value content in memory but we can also access it, which is useful for nested structs:

wasmgdb> p (*thing)->value
value (0xfff2c): usize = 0x00000003

The address 0xfff2c is the start of the MyThing struct, we can also inspect it using its address:

wasmgdb> p (MyThing) 0xfff2c
0xfff2c (0xfff2c): MyThing = {
        value (0xfff2c):        usize = 0x00000003
}

To confirm what we see we can also inspect frame #10:

wasmgdb> f 10
000012 as calculate (value=0x03000000) at src/main.rs

wasmgdb> info locals
value: usize = 0x00000003

As you can see, all that evidence points to the decimal value 3 being passed to the calculate function, which it subtracted to 10, and caused an interger overflow. We figured it out without using a single console.log!

Feel free to reproduce the crash and play with it yourself: https://github.com/xtuc/demo-node-wasm-coredump.