po4yka/rust-skills/rust-debugging
Claude Code and agent skill by po4yka/rust-skills
- community
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-debuggingSKILL.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
| Symptom | Start here | |
|---|---|---|
| Panic message and a Rust backtrace are visible | Host. Write a test that calls the same function. | |
| Crash only under one input file or payload | Host. Feed the input to a CLI or a unit test. | |
| Crash only on device, no panic message | Android or iOS. Pull the tombstone or crash report (sections 3 and 4). | |
| SIGSEGV or SIGBUS with no Rust frames | Native 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 nothing | FFI boundary. See section 2. | |
| Process hangs | Deadlock or async stall. Backtrace every thread, then see section 5. | |
| Snapshot or golden output changed, no panic | Not 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 message | Likely cause | Next step | |
|---|---|---|---|
`called Option::unwrap() on a None value` | Unwrap on None | Find the optional field. Replace with ok_or plus ?. | |
`called Result::unwrap() on an Err value` | Unwrap on an error | Propagate with ? and keep the source error. | |
index out of bounds: the len is N but the index is M | Slice or Vec out of range | Check the index math against a length that came from input. | |
attempt to subtract with overflow | Integer underflow with overflow checks on | Use checked_sub or saturating_sub. A release build wraps silently, so it is a bug either way. | |
attempt to multiply with overflow | Overflow in size math | Use checked_mul before you allocate. | |
panic in a function that cannot unwind | A panic reached an extern "C" or extern "system" export with no guard | Guard 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 dependency | Symbolicate, 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 cast | Audit the unsafe cast (the rust-unsafe skill). | |
| Process killed while it writes to a pipe or socket | SIGPIPE | Ignore SIGPIPE during init (section 2). | |
Kotlin InternalException, or a Swift error outside your error enum | Rust panic caught by the UniFFI guard | Fix the panic. Do not add a catch-all variant. | |
| Swift fatal error on a non-throwing UniFFI call | Panic in a non-throwing export | Make the export panic-free, or return Result. | |
JNI DETECTED ERROR IN APPLICATION in logcat | Wrong JNI usage: stale local ref, wrong signature, missing exception check | See the rust-jni skill. | |
Frames print as _RNv... or __RNv... | The tool predates v0 mangling, the default since 1.97 | Upgrade 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 nothing | v0 symbols start with _R (__R in Mach-O) | Grep _R. #[no_mangle] exports keep their literal names. | |
| Deadlock or hang, no CPU load | Lock ordering, or a channel with no sender | Attach 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
RUST_LIB_BACKTRACEwins overRUST_BACKTRACEforstd::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=fullframes 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.
- 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 listmust 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]setsdebug = "line-tables-only"ordebug = 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:
- 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_lineprints 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.
- Without
with_span_events, thefmtsubscriber logs no span lifecycle.#[instrument]then
only adds context to events.
FmtSpan::CLOSElogs one event per closed span withtime.busyandtime.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"orextern "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
jni0.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 value | Kotlin | Swift | |
|---|---|---|---|
Ok(v) | the return value | the return value | |
Err(E), where E is an exported error enum | an exception subclass, one per variant | enum E: Error, one case per variant | |
| a panic in a throwing export | InternalException with the panic message, not a subclass of your error type | a private UniFFI error, not a case of your error enum | |
| a panic in a non-throwing export | InternalException | a 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
- SKILL.md in the repositoryCheckingskills/rust-debugging/SKILL.md
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
[](https://verified-skill.com/skills/po4yka/rust-skills/rust-debugging)