| name | analyze-crash |
| description | Native crash log analysis for dd-trace-py |
| argument-hint | <crash-uuid-or-paste-log> |
| disable-model-invocation | false |
| context | fork |
| agent | general-purpose |
| allowed-tools | ["Bash","Read","Grep","Glob","TodoWrite","WebSearch"] |
Native Crash Stack Trace Analysis for dd-trace-py
You are analyzing a native crash captured by dd-trace-py's crashtracker. Perform a comprehensive
investigation to help engineers understand and triage the crash. The crashtracker captures all
process-level native crashes, so the crashing code may or may not be in dd-trace-py itself —
investigate the code and patterns thoroughly before making the attribution call.
Input Processing
The user will provide crash data in one of two ways:
Option A: Crash UUID (preferred)
The user provides a crash UUID from Datadog crash telemetry. Use the fetch script bundled with
this skill to retrieve and process the full crash log:
cd .claude/skills/analyze-crash && dd-auth .venv/bin/python fetch_crash.py <UUID> [--tracer-version X.Y.Z] [--days 14]
This fetches the crash log from Datadog, processes the stack frames (demangling C++ symbols,
resolving addresses against /proc/self/maps), and outputs structured JSON with:
- Metadata: tracer version, runtime version, service, org, platform, architecture, signal
- Processed stack: each frame with demangled symbol, binary file, offset, source file/line
- Binary mappings: summary of loaded shared objects
- Datadog log link: direct URL to the crash event
If the user provides only a UUID with no other context, ask:
What is the crash UUID? Any additional filters (tracer version, org ID, language)?
Setup (one-time): run cd .claude/skills/analyze-crash && uv sync to create the venv and
install dependencies.
Authentication: use dd-auth as a command wrapper — it injects Datadog API credentials
into the environment for the wrapped process.
Option B: Pasted crash log or stack trace
The user pastes a raw crash log or stack trace directly. The format is typically:
0x7f6e81c51fb7 PyUnicode_AsUTF8AndSize
0x7f6e769d7850 pycbc::Connection::open_bucket(_object*)
0x7f6e81ccd60c
0x7f6e81c53239 _PyObject_Call
...
Frames may have: hex address only, hex address + symbol, or symbol + .so name + offset. Parse
all available information. If the paste includes full crash JSON (with error.stack and
files./proc/self/maps), extract the metadata and processed stack from it.
Determine the source revision
The crash may come from a released wheel or a branch other than main. GitHub links must point
at the exact revision that built the crashing binary so that line numbers match.
Ask the user: Before starting analysis, ask:
Do you know the commit SHA or release tag for the dd-trace-py version that crashed?
(e.g., v2.18.0, abc1234). If not, I'll use main.
Resolution order (use the first that succeeds):
- User-provided value — commit SHA or tag supplied in the prompt or in response to the
question above. Use it verbatim as
GIT_REF.
- Crash metadata — if the crash was fetched via UUID and includes a
tracer_version
field (e.g., 2.18.0), try the corresponding git tag (v2.18.0). Verify it exists:
git rev-parse --verify "v2.18.0" 2>/dev/null.
- Version string in the stack trace — if the wheel filename or ddtrace version appears in
the trace (e.g.,
ddtrace-2.18.0), try the corresponding git tag.
- Current checkout — if the local repo is not on
main, use git rev-parse HEAD to get
the current SHA.
- Fallback — use
main.
Store the resolved value as GIT_REF and use it in all GitHub links for the rest of the
analysis. State which ref you are using and why at the top of the report (in Additional Context).
Analysis Workflow
GitHub Link Generation
When referencing files in dd-trace-py, always provide clickable GitHub links using the resolved
GIT_REF (see "Determine the source revision" above).
Format:
[filename:line](https://github.com/DataDog/dd-trace-py/blob/{GIT_REF}/path/to/file#Lline)
Examples (assuming GIT_REF=v2.18.0):
- Single line:
[_threads.cpp:226](https://github.com/DataDog/dd-trace-py/blob/v2.18.0/ddtrace/internal/_threads.cpp#L226)
- Range:
[_threads.cpp:226-252](https://github.com/DataDog/dd-trace-py/blob/v2.18.0/ddtrace/internal/_threads.cpp#L226-L252)
Path construction: base URL is https://github.com/DataDog/dd-trace-py/blob/{GIT_REF}/,
then append the repo-relative path (strip any build-specific prefixes like /home/runner/work/,
/usr/local/lib/, etc.).
Phase 1: Parse & Classify Stack Frames
Extract all stack frames and classify each one using the categories below.
Frame Classification
CPython Runtime — the Python interpreter itself:
.so name: python3.X, libpython3.X.so, libpython3.X.so.1.0
- Symbols:
_PyEval_EvalFrameDefault, PyGILState_Ensure, take_gil, new_threadstate,
PyEval_RestoreThread, PyEval_SaveThread, Py_RunMain, PyObject_Call*, _Py_*,
PyThread_exit_thread, _PyRuntime, _PyThreadState_*
dd-trace-py Native (_threads) — periodic thread / GIL management:
.so name: _threads.cpython-*.so
- Symbols:
PeriodicThread_start, PeriodicThread_stop, PeriodicThread_join,
PeriodicThread__periodic, PeriodicThread__before_fork, PeriodicThread__after_fork,
PeriodicThread__on_shutdown, PeriodicThread_awake, PeriodicThread_dealloc,
GILGuard, AllowThreads, PyRef, execute_native_thread_routine
dd-trace-py Native (_native / Rust) — crashtracker, data pipeline, tracer utilities:
.so name: _native.cpython-*.so (under ddtrace/internal/native/)
- Symbols:
crashtracker_*, TraceExporterPy::send, pyo3::*, rust_begin_unwind,
rust_panic, libdd_crashtracker::*, datadog_profiling_ffi::*, ffe::*, ddsketch::*
dd-trace-py Native (_memalloc) — memory profiler:
.so name: _memalloc.cpython-*.so
- Symbols:
memalloc_*, Datadog::Sample* (when called from memalloc context)
dd-trace-py Native (_ddup / _stack / libdd_wrapper) — profiling upload/sampling:
.so names: _ddup.cpython-*.so, _stack.cpython-*.so, libdd_wrapper.cpython-*.so
- Symbols:
ddup_*, Datadog::Profile*, Datadog::Sample*, Datadog::Uploader*,
Datadog::Sampler*, Datadog::EchionSampler*, Datadog::StackRenderer*, echion_*
dd-trace-py IAST/AppSec Native — security instrumentation:
.so name: _native.cpython-*.so (under ddtrace/appsec/), _stacktrace.cpython-*.so
- Symbols:
taint_*, TaintEngine*, tainted_ops::*
dd-trace-py Cython — Python-level profiling helpers:
.so names: _threading.cpython-*.so, _exception.cpython-*.so, _sampler.cpython-*.so,
_lock.cpython-*.so, _task.cpython-*.so, _encoding.cpython-*.so
- Symbols: Cython-generated (often
__pyx_* prefixes or module function names)
Third-Party Native (NOT dd-trace-py) — known third-party libs:
libev-*.so / libev.so.4 — gevent's embedded libev (NOT dd-trace-py)
_native__lib.cpython-*.so — Pyroscope's native extension (path contains pyroscope/)
Note: Pyroscope crashes appear in dd-trace-py crash telemetry because the crashtracker
captures all process-level crashes. _native__lib (double underscore, in pyroscope/) is
ALWAYS Pyroscope, never dd-trace-py. dd-trace-py's module is _native (single underscore,
in ddtrace/internal/native/).
anyhow::error::object_drop, py_spy::*, goblin::* — Pyroscope/py-spy Rust code
- Customer application code, third-party Python extensions
OS/libc — system libraries:
libc.so.6, libpthread.so, libm.so
- Symbols:
gsignal, abort, pthread_exit, pthread_mutex_lock, __clone3, start_thread,
cfree, free, malloc
Create a classification table:
| # | Type | Symbol / Location | Description |
|---|
| 0 | {type} | {symbol} | {brief description} |
| ... | | | |
Phase 2: Locate dd-trace-py Source Code
For each dd-trace-py frame that can be mapped to source code:
-
Map .so name to source file:
_threads.cpython-*.so → ddtrace/internal/_threads.cpp
_native.cpython-*.so (in ddtrace/internal/native/) → src/native/ (Rust)
_memalloc.cpython-*.so → ddtrace/profiling/collector/_memalloc.cpp
_ddup.cpython-*.so → ddtrace/internal/datadog/profiling/ddup/
_stack.cpython-*.so → ddtrace/internal/datadog/profiling/stack/
libdd_wrapper.cpython-*.so → ddtrace/internal/datadog/profiling/dd_wrapper/
-
Find the function in the source file using Glob/Bash grep.
-
Read code with context: find the function body, read 10-15 lines before the relevant
line, mark the crash point, read 5-10 lines after. Show enough to understand what the code is
doing.
Format:
### Frame N: {symbol} ([{file}:{line}](GitHub link))
```cpp
// {file}:{start}-{end}
{code with relevant line marked: // >>> CRASH POINT <<<}
```
**Analysis**: {what this code does and why it may have failed}
Phase 3: Apply General Crash Heuristics
dd-trace-py maintains two complementary native code safety references:
docs/native-code-review.md — full incident-derived rules with historical PR links (10 sections)
.cursor/rules/native-code.mdc — compact triage checklist with trigger symbols and stop conditions
Read both files now. Use their framework to classify the crash. The categories below
summarize the key stack-trace signals; refer to the docs for full rationale and PR history.
3.1 — GIL Lifecycle During Finalization
Stack signals: PyGILState_Ensure, PyEval_RestoreThread, take_gil, new_threadstate,
PyThread_exit_thread, pthread_exit, abort, gsignal, __forced_unwind,
abi::__cxa_throw, std::terminate — especially combined with thread-entry frames like
execute_native_thread_routine or Rust extern "C" boundaries.
What to check:
- Is there a
py_is_finalizing() / is_finalizing() check immediately before each GIL
acquire/restore call? These checks are inherently TOCTTOU — finalization can begin in the
window between the check and the call.
- Does the crash involve C++ RAII unwinding?
pthread_exit on glibc throws
abi::__forced_unwind; any C++ destructor or catch(...) that does not re-throw it will
cause std::terminate → SIGABRT. On musl (Alpine Linux), pthread_exit uses longjmp
instead — catch(abi::__forced_unwind&) never fires.
- For Rust/pyo3 code:
__forced_unwind cannot propagate through extern "C" boundaries.
Rust code that calls PyEval_RestoreThread (or uses py.detach()) needs its own
finalization check — the C++ caller's try/catch cannot protect it.
CPython version behavior (from docs/native-code-review.md):
| CPython | Behavior when take_gil() detects finalization |
|---|
| 3.12 (all) | PyThread_exit_thread() → pthread_exit() → crash |
| 3.13.0–3.13.7 | Same as 3.12 |
| 3.13.8+ | PyThread_hang_thread() → pause() forever (hang, not crash) |