po4yka/rust-skills/rust-debugging

Claude Code and agent skill by po4yka/rust-skills

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Use when debugging a Rust crash, panic, hang, or unreadable native stack trace, on the host or inside an Android or iOS app behind JNI or UniFFI. Triggers on RUST_BACKTRACE, SIGSEGV, SIGABRT, native crash, tombstone, logcat crash output, addr2line, ndk-stack, atos, dSYM, rust-lldb, rust-gdb, lldb-server, _RNv mangled frames, JNI panic, UniFFI panic, and tokio-console for async stalls. Not for slow code or profiling; use `rust-performance`.

Install

npx vskill@latest install po4yka/rust-skills/rust-debugging

SKILL.md

Rust Debugging (Host First, Then Android and iOS)

Reproduce on the host first. A CLI or a test that drives the same code gives you a debugger, a backtrace, sanitizers, and a fast loop. Move to a device only after the host repro fails. On a device, go from logs to the tombstone or crash report, then symbolicate, and attach LLDB last.

Where to start

SymptomStart here
Panic message and a Rust backtrace are visibleHost. Write a test that calls the same function.
Crash only under one input file or payloadHost. Feed the input to a CLI or a unit test.
Crash only on device, no panic messageAndroid or iOS. Pull the tombstone or crash report (sections 3 and 4).
SIGSEGV or SIGBUS with no Rust framesNative memory bug. Symbolicate, then run the host repro under ASan, or the device build under HWASan or MTE (the rust-sanitizers-miri skill).
Kotlin or Swift gets an error, Rust logs nothingFFI boundary. See section 2.
Process hangsDeadlock or async stall. Backtrace every thread, then see section 5.
Snapshot or golden output changed, no panicNot a crash. Read the diff. Re-bless only in a commit that explains why (the rust-test-tools skill).

Panic and signal triage

Signal or messageLikely causeNext step
`called Option::unwrap() on a None value`Unwrap on NoneFind the optional field. Replace with ok_or plus ?.
`called Result::unwrap() on an Err value`Unwrap on an errorPropagate with ? and keep the source error.
index out of bounds: the len is N but the index is MSlice or Vec out of rangeCheck the index math against a length that came from input.
attempt to subtract with overflowInteger underflow with overflow checks onUse checked_sub or saturating_sub. A release build wraps silently, so it is a bug either way.
attempt to multiply with overflowOverflow in size mathUse checked_mul before you allocate.
panic in a function that cannot unwindA panic reached an extern "C" or extern "system" export with no guardGuard the export (section 2).
Signal 6 (SIGABRT)Double panic, abort(), panic = "abort" (the Abort message holds the panic text), or an unguarded export (no Abort message; a panic_cannot_unwind frame after symbolication)Read the tombstone Abort message when it is present. Check the export guards, then reproduce on the host.
Signal 11 (SIGSEGV)Null or dangling pointer, use-after-free in unsafe or in a C dependencySymbolicate, then run the host repro under ASan, or the device build under HWASan or MTE (the rust-sanitizers-miri skill).
Signal 7 (SIGBUS)Misaligned or invalid memory access, often a bad pointer castAudit the unsafe cast (the rust-unsafe skill).
Process killed while it writes to a pipe or socketSIGPIPEIgnore SIGPIPE during init (section 2).
Kotlin InternalException, or a Swift error outside your error enumRust panic caught by the UniFFI guardFix the panic. Do not add a catch-all variant.
Swift fatal error on a non-throwing UniFFI callPanic in a non-throwing exportMake the export panic-free, or return Result.
JNI DETECTED ERROR IN APPLICATION in logcatWrong JNI usage: stale local ref, wrong signature, missing exception checkSee the rust-jni skill.
Frames print as _RNv... or __RNv...The tool predates v0 mangling, the default since 1.97Upgrade the tool (binutils 2.36+, Linux perf 6.16+; the perf in Ubuntu 24.04 and Debian 13 is too old), or pipe through rustfilt. On macOS, use c++filt -_.
A grep for _ZN finds nothingv0 symbols start with _R (__R in Mach-O)Grep _R. #[no_mangle] exports keep their literal names.
Deadlock or hang, no CPU loadLock ordering, or a channel with no senderAttach the debugger and backtrace every thread.

Before you say "cannot reproduce"

  • [ ] You tried a host repro with RUST_BACKTRACE=1, and under a debugger.
  • [ ] The build under test has debug info, and it is not stripped.
  • [ ] You symbolicated against the exact binary that crashed: same Build ID or UUID, not a rebuild.
  • [ ] Logging starts before the suspect code path runs, and one test log::info! reaches the sink.
  • [ ] You read the tombstone or crash report signal and Abort message, not only the app-level

message.

  • [ ] You ran the host repro under a sanitizer when the signal was 11 or 7.

1. Host debugging

Backtraces
bash
# Short backtrace on panic
RUST_BACKTRACE=1 cargo run --locked -p my-cli -- <args>
# Every frame, with addresses and std internals
RUST_BACKTRACE=full cargo run --locked -p my-cli -- <args>
# One test, with its output shown (--nocapture is deprecated since 1.88)
RUST_BACKTRACE=1 cargo test --locked -p my-crate <test_name> -- --no-capture
# Panic backtraces on, error-value backtraces off
RUST_BACKTRACE=1 RUST_LIB_BACKTRACE=0 cargo run --locked -p my-cli -- <args>
  • RUST_LIB_BACKTRACE wins over RUST_BACKTRACE for std::backtrace::Backtrace::capture, which

anyhow calls when it creates an error. With RUST_BACKTRACE=1 alone, every such error pays for a capture. Set the variables before the process starts; std caches them at the first capture.

  • v0 mangling is the default since 1.97. RUST_BACKTRACE=full frames now carry crate hashes, for

example std[5d97c59e5e5fafcc]::panicking::default_hook. The hash changes with the toolchain and the build configuration, so diff RUST_BACKTRACE=1 output, not full output.

  • Since 1.91 the panic line carries the thread ID: `thread 'main' (6583488) panicked at

src/lib.rs:4:23:. Grep for panicked at, not for thread 'main' panicked`.

Debugger

rust-lldb and rust-gdb are toolchain scripts (rustup runs them through its proxy) that load the Rust formatters, so String, Vec, Option, and HashMap print as Rust values instead of raw fields.

bash
cargo build --locked -p my-cli
rust-lldb target/debug/my-cli -- <args> # macOS, or Linux with LLDB
rust-gdb --args target/debug/my-cli <args> # Linux
text
(lldb) b rust_panic (gdb) break rust_panic
(lldb) b my_crate::module::function (gdb) break my_crate::module::function
(lldb) run (gdb) run
(lldb) frame variable (gdb) info locals
(lldb) thread backtrace all (gdb) thread apply all bt
  • In current toolchains (1.88 and later at least), the panic entry is __rustc::rust_panic.

b rust_panic still resolves in LLDB on 1.98.1. A tool that sets the breakpoint with a legacy-name regex can miss it. In GDB, if rust_panic has no location, break on __rustc::rust_panic or core::panicking::panic_fmt.

  • Confirm the formatters loaded before you trust a value. (lldb) type category list must show

Rust (enabled). A Vec that prints as buf and len fields means no formatters.

  • Rust 1.98 removed lib/rustlib/etc/lldb_commands. Inside an IDE, where the wrapper does not

run, on Rust 1.98 and later, load the formatters with command script import of lldb_lookup.py only. Older toolchains also need the lldb_commands line (see the reference).

Read references/rust-gdb-pretty-printers.md when you load formatters by hand (Xcode, Android Studio, plain gdb), configure CodeLLDB, set a closure, trait-method, or conditional breakpoint, or demangle symbols.

Debug info

A debugger needs variable-level debug info. Symbolication needs line tables and the exact unstripped binary.

  • If [profile.dev] sets debug = "line-tables-only" or debug = false, the debugger shows no

variables. Override it for one build: CARGO_PROFILE_DEV_DEBUG=true cargo build --locked -p my-cli.

  • A release build has no debug info by default, so a device backtrace has no file or line.

Turn it on for the artifact that you archive:

toml
[profile.release]
debug = "line-tables-only" # file:line for symbolication; use true to inspect variables
strip = false # keep symbols in the artifact that you archive
  • Ship the stripped artifact. Archive the unstripped one and symbolicate against the archived

copy. An address means nothing without the exact binary that produced it.

  • On macOS, Cargo defaults to split-debuginfo = "unpacked": the DWARF stays in the object files

under target/, and the binary alone has no line info. To archive a macOS host binary, build with CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=packed and archive the .dSYM next to it.

  • Verify the archive. On ELF, llvm-readelf -S libmy_ffi.so | grep debug_line prints a section.

On Apple, dwarfdump --uuid prints the same UUID for the binary and its .dSYM.

Logging

dbg!, println!, and the default panic hook write to stdout or stderr. An Android app process sends both to /dev/null, so none of them reach logcat. Use tracing for output that must survive on a device. Enable clippy::dbg_macro (restriction group, allow by default) to catch a leftover dbg!.

For a host repro, install a subscriber that logs span timing. EnvFilter needs the env-filter feature of tracing-subscriber.

rust
use tracing_subscriber::{EnvFilter, fmt::format::FmtSpan};
fn main() {
// Host repro only. On a device, the application bootstrap owns the subscriber.
// The default writer is stdout; stderr keeps the program's stdout clean.
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(EnvFilter::from_default_env())
.with_span_events(FmtSpan::CLOSE)
.init();
}
bash
RUST_LOG=my_crate=debug,my_other_crate=trace cargo run --locked -p my-cli -- <args>
  • Without with_span_events, the fmt subscriber logs no span lifecycle. #[instrument] then

only adds context to events.

  • FmtSpan::CLOSE logs one event per closed span with time.busy and time.idle. High idle time

means the span waited. High busy time means it did the work.

  • Instrument a suspect function with #[instrument(skip_all, fields(...))]. The

rust-observability skill owns the field rules and the device subscriber setup.

2. Panics at the FFI boundary

The rust-panic-safety skill owns the guard and hook policy. The rust-jni skill owns JNI export patterns. This section maps what you see to the cause.

  • Since Rust 1.81, a panic that reaches an extern "C" or extern "system" function aborts the

process. This is defined behavior. On the host, stderr shows the panic, then panic in a function that cannot unwind and thread caused non-unwinding panic. aborting., and the signal is SIGABRT. On Android, stderr goes nowhere and the panic text is lost. The tombstone shows SIGABRT, and the symbolicated backtrace has core::panicking::panic_cannot_unwind above the export.

  • Fix it at the export, not at the caller. On jni 0.22 use

EnvUnowned::with_env(...).resolve::<Policy>(). Otherwise wrap the body in catch_unwind.

UniFFI panic and error propagation

UniFFI generates the boundary guard. You must still know what the foreign side sees:

Rust valueKotlinSwift
Ok(v)the return valuethe return value
Err(E), where E is an exported error enuman exception subclass, one per variantenum E: Error, one case per variant
a panic in a throwing exportInternalException with the panic message, not a subclass of your error typea private UniFFI error, not a case of your error enum
a panic in a non-throwing exportInternalExceptiona fatal Swift error that no catch can handle; the app crashes

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
2
Repository forks
1
7-day trend score
5

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/po4yka/rust-skills/rust-debugging/badge)](https://verified-skill.com/skills/po4yka/rust-skills/rust-debugging)

Something wrong with this skill or its page? Tell us.