# droidsaw — gdb tooling

Python pretty-printers plus a panic-capture wrapper for droidsaw's shared IR.
Shrinks a panic backtrace from "wall of `BTreeMap<_, _>` internals" to a one-line
summary per IR type (`dex::Cfg(blocks=14, entry=#0, eh-regions=1)` etc.) while
keeping the full field graph one `children()` expansion away.

Works with stock gdb 10+ built with Python 3. Verified on gdb 17.1 / Python 3.14
(Fedora 43).

## Files

| File              | Purpose |
|-------------------|---------|
| `droidsaw.py`     | gdb Python pretty-printers for the droidsaw IR surface |
| `on-panic.sh`     | Wraps any command in gdb; breaks on Rust panic, prints `bt full` + locals, continues |
| `.gdbinit`        | Optional one-liner that sources `droidsaw.py` when gdb starts in this directory |

## Loading the pretty-printers

### Interactive
```
(gdb) source /abs/path/to/tools/gdb/droidsaw.py
(gdb) break droidsaw_dex::cfg::tests::linear_code_single_block
(gdb) run
(gdb) print cfg
$1 = dex::Cfg(blocks=2, entry=#0, eh-regions=0) = { ... }
```

### Auto-load
Drop the included `.gdbinit` next to any binary you debug, or point gdb at it
explicitly:
```
gdb -x /abs/path/to/tools/gdb/.gdbinit ./my-binary
```
For system-wide auto-load (`.gdbinit` that lives next to the binary gets loaded
automatically), add this line to `~/.gdbinit`:
```
add-auto-load-safe-path /home/you/code/droidsaw
```

## Type coverage

Shared IR that has a defined one-line summary:

- `droidsaw_common::graph::dominators::PostDom<N>` → `PostDom::Node(…)` / `PostDom::Exit`
- `droidsaw_common::ssa::Var<R>` → `Var(r{reg}_{ver})`
- `droidsaw_common::ssa::Phi<B,R>` → `Phi(dst=r{reg}_{ver}, preds={N})`
- `droidsaw_common::ssa::Builder<G,R>` → `Builder(defs={N}, next_ver={V}, phi-blocks={P})`
- `droidsaw_common::ssa::SsaError` → variant + `cap` on `IterationLimit`
- `droidsaw_common::Finding` → `Finding(id=…, layer=…, severity=…)`
- `droidsaw_dex::error::DexError` → variant + per-variant key fields (offset/need/have/…)
- `droidsaw_dex::cfg::Cfg` → `dex::Cfg(blocks=…, entry=#…, eh-regions=…)`
- `droidsaw_dex::ssa::SsaBody` → `SsaBody(blocks=…, vars=…, entry=#…, params=…)`
- `droidsaw_hermes::HermesError` → variant + version
- `droidsaw_hermes::decompile::cfg::Cfg` → `hermes::Cfg(blocks=…, entry=#…, order=…, exc-preds=…)`

Types without a useful summary (e.g. scalar wrappers) fall through to gdb's
default visualizer; that behavior is intentional.

## Capturing a Rust panic with `on-panic.sh`

```
tools/gdb/on-panic.sh [--interactive] [--printers PATH] -- CMD [ARGS...]
```

- Sets `RUSTFLAGS="-C force-frame-pointers=yes -C debuginfo=2"` unless already
  set. Ditto `RUST_BACKTRACE=full`.
- Sources `droidsaw.py` so IR values in scope at panic time are readable.
- Breaks on `std::panicking::panic_with_hook`, `std::panicking::rust_panic`,
  and plain `rust_panic` (one will resolve for your rustc; the others stay
  pending and do nothing).
- On hit: prints `bt full`, `info locals`, then `continue`s. The inferior keeps
  running under the normal panic handler, so the process's exit code is
  preserved (`--interactive` drops to the gdb prompt instead of continuing).

### Walkthrough: pre-sweep dex decoder panic

Reproduces `src/decode.rs:280` u32 overflow on `pc + unit_idx` against the
`719b3a5e2b9b` fuzz crasher, run against `droidsaw-dex` at commit `71d5a86`
(one commit before the `dex-hardening-sweep` fix landed). The seven-byte input
triggers `decode_packed_switch` to recurse with `payload_pc = u32::MAX`, and
the unchecked `pc + unit_idx` overflows inside `read_unit`.

```
$ tools/gdb/on-panic.sh -- ./repro fuzz/crashes/fuzz_opcode_decode/719b3a5e2b9b
repro: decode_insns(data=7 bytes, pc=0, insns_size=3)

========== droidsaw: rust panic caught (panic_with_hook) ==========
#0  std::panicking::panic_with_hook ... at library/std/src/panicking.rs:793
#4  core::panicking::panic_fmt ... at library/core/src/panicking.rs:80
#5  core::panicking::panic_const::panic_const_add_overflow () at library/core/src/panicking.rs:175
#6  droidsaw_dex::decode::read_unit
      (data=&[u8] [43, 55, 255, 255, 255, 255, 37],
       insns_off=0, pc=4294967295, unit_idx=1)
      at src/decode.rs:280
#7  droidsaw_dex::decode::decode_packed_switch
      (data=..., insns_off=0, payload_pc=4294967295, switch_pc=0)
      at src/decode.rs:564
#8  droidsaw_dex::decode::decode_insns
      (data=..., insns_off=0, insns_size=3) at src/decode.rs:736
        op = droidsaw_dex::opcodes::Opcode::PackedSwitch
        switch_pc = 0
        payload_pc = 4294967295
        pc = 3
        pending_payloads = <Vec len=1>
        payloads = <BTreeMap length=0>
        instructions = <Vec len=1>
#9  repro::main () at src/main.rs:13
...
========== (end panic report) ==========

thread 'main' panicked at src/decode.rs:280:28:
attempt to add with overflow
```

`pc=4294967295` is `u32::MAX` — the overflow argument. Root cause is the
unchecked `payload_pc = payload_data_off + some_offset` computation upstream in
`decode_packed_switch`. The sweep fix lands in `pc.checked_add(unit_idx)` with
`DexError::InvalidInstruction { … }` on failure.

### Pretty-printer demo against a live `cargo test`

```
$ cargo test --no-run --lib
$ TEST=$(ls target/debug/deps/droidsaw_dex-*)
$ gdb -q $TEST -iex 'set debuginfod enabled off' \
    -ex 'source tools/gdb/droidsaw.py' \
    -ex 'break cfg.rs:518' \
    -ex 'run --exact cfg::tests::linear_code_single_block'

(gdb) print cfg
$1 = dex::Cfg(blocks=2, entry=#0, eh-regions=0) = {
  blocks = alloc::vec::Vec<droidsaw_dex::cfg::BasicBlock, …> { …, len: 2 },
  entry = droidsaw_dex::cfg::BlockIdx ( 0 ),
  exception_regions = alloc::vec::Vec<…> { …, len: 0 },
  addr_to_block = alloc::collections::btree::map::BTreeMap<u32, …> { …, length: 2 }
}
```

The leading `dex::Cfg(blocks=2, entry=#0, eh-regions=0)` is `to_string()`; the
`{ … }` block is the default struct expansion via `children()`. You can
suppress the expansion with `set print elements 0` or walk individual children
(`print cfg.blocks[0]`, etc.).

## Troubleshooting

- **"Function rust_panic not defined. Breakpoint pending."** Expected. Three
  candidate symbol names are attempted; whichever resolves for your rustc
  fires. The rest stay pending.
- **No pretty printer registered for type** — gdb reports the type name it saw.
  If the crate path is present but the printer isn't firing, check that the
  type name in `droidsaw.py`'s `_TABLE` matches the tag gdb is showing.
  Generics are stripped before lookup, so `Var<u16>` and `Var<HermesReg>` share
  a printer.
- **"No symbol 'cfg' in current context."** The optimizer hid it. Build with
  `--profile dev` (overflow-checks on, no inlining) and
  `RUSTFLAGS="-C debuginfo=2"`. libfuzzer release builds often optimize locals
  out entirely.
- **Python traceback in gdb** — printer errors are caught and turned into
  `<droidsaw printer error: …>` stubs; a visible traceback means one leaked
  past the wrapper. Please file it with the gdb version and the type tag.
- **`add-auto-load-safe-path`** — gdb refuses to source `.gdbinit` from
  untrusted paths by default. Either use `-x` to point at it explicitly, or
  whitelist the droidsaw repo in `~/.gdbinit`.
