rust-debugging
Skill5 starsChanged 10 days agoBSD-3-ClauseView on GitHub
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`.
- Installs packages
2 other projects use a version of this.
Adopted by
Most recently taken up by
- uswuth/rustoriatheir own version, first seen Oct 25, 2026
- Dankosik/rust-cli-skillstheir own version, first seen Sep 8, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- Dankosik/rust-cli-skills★ 0same name
- uswuth/rustoria★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository46 other files it gives its agents.
- AGENTS.mdAGENTS.md
- cargo-workflowsSkill
- ffi-error-progress-cancelSkill
- memory-modelSkill
- rust-android-buildSkill
- rust-async-internalsSkill
- rust-borrow-semanticsSkill
- rust-callback-boundsSkill
- rust-cliSkill
- rust-code-styleSkill
- rust-compiler-errorsSkill
- rust-copy-on-writeSkill
- rust-crate-architectureSkill
- rust-crate-releaseSkill
- rust-databaseSkill
- rust-disciplineSkill
- rust-embedded-no-stdSkill
- rust-event-loop-stateSkill
- rust-hot-pathSkill
- rust-ios-buildSkill
- rust-iterator-implSkill
- rust-jniSkill
- rust-lintsSkill
- rust-macrosSkill
- rust-native-linkingSkill
- rust-networkingSkill
- rust-observabilitySkill
- rust-panic-safetySkill
- rust-pattern-semanticsSkill
- rust-performanceSkill
- rust-pin-projectionSkill
- rust-portSkill
- rust-reviewSkill
- rust-sanitizers-miriSkill
- rust-securitySkill
- rust-send-syncSkill
- rust-serdeSkill
- rust-swift-ffiSkill
- rust-tddSkill
- rust-test-toolsSkill
- rust-type-erasureSkill
- rust-unsafeSkill
- rust-varianceSkill
- rust-wasmSkill
- uniffi-boundarySkill
- uniffi-packaging-versioningSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
13 rules. About 0 are common, 3 are distinctive, and 10 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
10
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 113
Read references/rust-gdb-pretty-printers.md when you load formatters by hand (Xcode, Android Studio, plain gdb), configure CodeLLDB, set a …
- Very specific: line 122
If [profile.dev] sets debug = "line-tables-only" or debug = false, the debugger shows no variables.
- Very specific: line 138
Verify the archive. On ELF, llvm-readelf -S libmy_ffi.so | grep debug_line prints a section.
- Very specific: line 205
Do not add an Internal variant and expect panics to arrive in it.
- Distinctive: line 220
Treat every panic message as a log line.
- Distinctive: line 261
Attach LLDB: only when the log and the tombstone both fall short.
- Very specific: line 263
Read references/android-debugging.md when you filter logcat, route tracing to logcat, need RUST_BACKTRACE on a device, read a tombstone, …
- Distinctive: line 269
Use the same order as on Android: the crash report, then symbolication, then LLDB in Xcode.
- Very specific: line 276
Read references/ios-debugging.md when you symbolicate with atos, check that the .dSYM holds Rust lines, build for a device or the …
- Very specific: line 282
Start with the FmtSpan::CLOSE span timing from section 1.
- Very specific: line 288
Add console-subscriber to the binary crate, enable features = ["full", "tracing"] on tokio, and call console_subscriber::init() first in …
- Very specific: line 293
Add the cfg to the rustflags in .cargo/config.toml, not to RUSTFLAGS.
- Very specific: line 297
Remove all four changes, the rustflags entry included, before you commit.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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
# 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_BACKTRACEwins overRUST_BACKTRACEforstd::backtrace::Backtrace::capture, whichanyhowcalls when it creates an error. WithRUST_BACKTRACE=1alone, 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 examplestd[5d97c59e5e5fafcc]::panicking::default_hook. The hash changes with the toolchain and the build configuration, so diffRUST_BACKTRACE=1output, notfulloutput. - Since 1.91 the panic line carries the thread ID:
thread 'main' (6583488) panicked at src/lib.rs:4:23:. Grep forpanicked at, not forthread '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.
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(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_panicstill resolves in LLDB on 1.98.1. A tool that sets the breakpoint with a legacy-name regex can miss it. In GDB, ifrust_panichas no location, break on__rustc::rust_panicorcore::panicking::panic_fmt. - Confirm the formatters loaded before you trust a value.
(lldb) type category listmust showRust (enabled). AVecthat prints asbufandlenfields 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 withcommand script importoflldb_lookup.pyonly. Older toolchains also need thelldb_commandsline (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:
[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 undertarget/, and the binary alone has no line info. To archive a macOS host binary, build withCARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO=packedand archive the.dSYMnext to it. - Verify the archive. On ELF,
llvm-readelf -S libmy_ffi.so | grep debug_lineprints a section. On Apple,dwarfdump --uuidprints 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.
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();
}RUST_LOG=my_crate=debug,my_other_crate=trace cargo run --locked -p my-cli -- <args>- 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(...))]. Therust-observabilityskill 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, thenpanic in a function that cannot unwindandthread 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 hascore::panicking::panic_cannot_unwindabove the export. - Fix it at the export, not at the caller. On
jni0.22 useEnvUnowned::with_env(...).resolve::<Policy>(). Otherwise wrap the body incatch_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 |
- An exception that is not one of your declared variants is a Rust panic, not a handled error.
- The panic message rides in the Kotlin exception text, in the Swift error description, and in
the
try!fatal-error message of a non-throwing Swift call. A crash reporter that records one of them records the message. - Do not add an
Internalvariant and expect panics to arrive in it. They do not. Convert the failure into a realErron the Rust side when the caller must handle it. Keep a non-throwing export panic-free. See theuniffi-boundaryandffi-error-progress-cancelskills.
Panic reports on a device
In a shipped build, report a panic only through the bootstrap's redacted hook: a closed site
code plus the line and the column. The rust-panic-safety skill has the one copy of that hook. Never
put the payload, a file path, or a backtrace into a shipped log, because panic text can hold input
data, paths, or secrets. Get the message and the backtrace from a host repro, or symbolicate the
crash offline.
On Android, panic = "abort" copies a &str or String panic payload into the tombstone Abort
message line through android_set_abort_message (in panic_abort as of Rust 1.98.1). The line
helps triage, and every crash report that uploads the tombstone also carries the panic text.
Treat every panic message as a log line.
SIGPIPE
A write to a pipe or a socket whose peer closed can raise SIGPIPE. Its default action kills the
process with no Rust panic and no log. A host repro hides this. The Rust runtime of a binary or a
test sets SIGPIPE to ignored before main, so the same write returns an error there:
use std::io::{ErrorKind, Write};
fn main() {
let (reader, mut writer) = std::io::pipe().unwrap();
drop(reader);
// A Rust `main` starts with SIGPIPE ignored, so this is an error, not a kill.
let err = writer.write_all(b"x").unwrap_err();
assert_eq!(err.kind(), ErrorKind::BrokenPipe);
}A cdylib or staticlib inside a JVM or Swift process gets no such setup. Let the application
bootstrap set the ignore disposition during init, then handle ErrorKind::BrokenPipe like any
other error:
pub fn ignore_sigpipe() {
// SAFETY: `signal` with `SIG_IGN` installs no handler code. Call it from init only.
unsafe { libc::signal(libc::SIGPIPE, libc::SIG_IGN) };
}3. Android debugging
Reach for the device only after the host attempt fails. The order that works:
- Wire logging first. Without a logcat sink, nothing Rust prints reaches logcat. The crash channels still fire, but they give a signal and an address, not a panic record.
- Read the record and the signal.
adb logcat | grep -E "rust_panic|SIGABRT|SIGSEGV", andadb logcat -b crashfor the crash dump. - Symbolicate. A tombstone is addresses until
ndk-stackorllvm-addr2linemaps them against the unstripped.sofrom the same build. - Attach LLDB only when the log and the tombstone both fall short.
Read references/android-debugging.md when you filter logcat,
route tracing to logcat, need RUST_BACKTRACE on a device, read a tombstone, run addr2line or
ndk-stack, or attach LLDB from Android Studio.
4. iOS debugging
Use the same order as on Android: the crash report, then symbolication, then LLDB in Xcode.
- Symbolicate against the
.dSYMof the app build, because the linker moves thestaticlibcode into the app binary. Itsdwarfdump --uuidmust match the image UUID in the crash report. - The Xcode ASan and TSan switches do not instrument a prebuilt Rust
staticlib. Run the sanitizers on a host target (therust-sanitizers-miriskill).
Read references/ios-debugging.md when you symbolicate with atos,
check that the .dSYM holds Rust lines, build for a device or the simulator, or break in Rust
from Xcode.
5. Async debugging
Start with the FmtSpan::CLOSE span timing from section 1.
tokio-console shows live task state. It needs console-subscriber, the tokio tracing
feature, and the tokio_unstable cfg. It opens a local TCP port, so use it in a local debug
build only and never ship it in a mobile or production build.
- Add
console-subscriberto the binary crate, enablefeatures = ["full", "tracing"]ontokio, and callconsole_subscriber::init()first inmain.init()installs the global subscriber with its ownfmtlayer, and it panics when a subscriber is already set. Remove the section 1fmt()...init()call, or keep your subscriber and addconsole_subscriber::spawn()to atracing_subscriber::registry()as a layer. - Add the cfg to the rustflags in
.cargo/config.toml, not toRUSTFLAGS. TheRUSTFLAGSvariable replaces every config-file rustflag. Matching[target.<triple>]rustflags replace[build]rustflags, so add it to the table that the workspace already uses.
Remove all four changes, the rustflags entry included, before you commit. A committed
tokio_unstable flag reaches every CI and release build.
# .cargo/config.toml
[build]
rustflags = ["--cfg", "tokio_unstable"]cargo install --locked tokio-console
cargo run --locked -p my-cli -- <args>
tokio-console # in a second terminal| tokio-console shows | Cause | Next step |
This task has lost its waker, and will never be woken again. | A poll returned Pending and dropped every waker clone | Before poll returns Pending, store cx.waker().clone() where the event source can call wake. Replace the stored waker when will_wake returns false |
This task has never yielded (...), threshold 1 s | Blocking work holds a worker thread | Move it to spawn_blocking. The rust-async-internals skill has the blocking-call table |
This task has woken itself for more than 50% of its total wakeups | A self-wake loop: the task busy-polls | Wait on a real event instead of waking itself |
| An idle task, never polled again, no warning | A channel or a lock that nobody releases | Backtrace every thread and find the holder |
On a device or a production build, where tokio-console cannot ship, read the stable
tokio::runtime::RuntimeMetrics instead: num_alive_tasks, global_queue_depth, and the
per-worker worker_total_busy_duration and worker_park_unpark_count (targets with 64-bit
atomics only). The rust-observability skill
covers how to export them.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

