Skip to content

Latest commit

 

History

104 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

debugger-raconteur

An AI-friendly debugger library that drives rr replay through GDB/MI and turns a recorded crash into one structured, English-narrated analysis.

$ python -m raconteur.agent_tool ./uaf
  1. Continued forward; program received SIGSEGV at main (uaf.c:25)
  2. Examined stack (1 frame); top user frame is main (uaf.c:25)
  3. Listed locals in frame 0: p=0x7c9a3b8ff010 <error: Cannot access memory at address 0x7c9a3b8ff010>
  4. Inserted breakpoint #1 at free if $rdi == 0x7c9a3b8ff010
  5. Reverse-continued; hit breakpoint #1 at __GI___libc_free
  6. Examined stack (3 frames); top user frame is freer (uaf.c:18)
  7. Deleted breakpoint #1
  8. Probed target for known allocators; resolved 4 symbol(s): malloc=[malloc]; calloc=[calloc]; realloc=[realloc]; free=[free].
  9. Captured heap timeline: 1 allocations (1 freed, 0 live).
 10. Conclusion: Use-after-free on 0x7c9a3b8ff010 (reached via `p`). Freed at
     uaf.c:18, used at uaf.c:25. Dangling chunk was the 1st of 1 allocation
     from uaf.c:22 (1.0 MiB); held for 20 ticks before use. No allocations
     reclaimed the freed region during the gap (textbook UAF crash on
     unmapped or sentinel-poisoned memory).

JSON: {"bug": "use-after-free", "summary": "...", "sites": {"freed_at": "uaf.c:18", "used_at": "uaf.c:25"}, "evidence": {...}, "heap_context": {...}}

Why

LLM agents driving raw GDB hallucinate command output — the EnIGMA paper documents this empirically — and burn many turns reassembling crashes from scattered register dumps, backtraces, and locals. raconteur compresses that multi-turn dialog into one tool call whose output is structured (AnalysisReport dataclass) and English-narrated, leaving the model much less surface area to misread.

For agent integration the package exposes record_and_narrate(binary, args) as the headline entry point; for direct library use it exposes tier-1 GDB/MI primitives plus a pluggable investigator registry.

What it does today

  • Replay-only. Drives rr replay over an already-recorded trace. Live attach, core dumps, and ptrace-only backends are out of scope.
  • Bug classes. Four investigators ship in the default registry:
    • use-after-free (dangling pointer + reachable free() of the faulting address; for any heap-aware allocator, including glibc, libstdc++, ASAN, MSAN, tcmalloc, jemalloc, mimalloc).
    • stack-buffer-overflow (stack-protector canary fire; identifies the corrupted function and the most-recent unsafe sink — strcpy, sprintf, memcpy, gets, etc.).
    • double-free (glibc's heap-corruption abort path; reverse-walks to both the second-free site that triggered abort and the prior first-free site).
    • format-string (SIGSEGV inside libc's printf-family processing; identifies the calling source line for a printf(argv[N])-style vulnerability). New bug classes plug in via register_investigator(matcher, fn) — no dispatcher edit required. See examples/custom_investigator/ for a runnable example that adds a fifth (SIGFPE / divide-by-zero) class.
  • Heap-aware UAF analysis. Beyond freed/used source lines the UAF report carries a HeapContext: allocation-site history for the dangling chunk, free-to-use tick gap, and a free-to-use gap analysis that distinguishes textbook UAF crashes (no reclaim, unmapped page) from heap-spray-shape exploits (the freed region was reclaimed by another allocation while the dangling pointer was live).
  • Allocator auto-discovery. Session.suggest_allocators() probes the binary's symbol table and returns specs for every known allocator that resolves, so non-glibc binaries don't need manual configuration.

Requirements

  • Linux x86_64
  • rr 5.x or later, with kernel.perf_event_paranoid <= 1
  • gdb 13+ (anything that speaks the mi3 protocol works)
  • Python 3.10+ and pygdbmi >= 0.11

Install

git clone git@github.com:blazer502/debugger-raconteur.git
cd debugger-raconteur
python -m venv .venv && source .venv/bin/activate
pip install -e .

Five-minute example

Record a crashing binary under rr, then narrate:

# In the binary's source directory:
gcc -g -O0 -o uaf uaf.c
rr record -o rr-trace ./uaf || true   # SEGV exit ignored

Programmatic, low-level:

import raconteur as rcr

session = rcr.load("rr-trace/")          # tier-1 Session
report = rcr.analyze_crash(rcr.narrate("rr-trace/"))
print(report.bug)                         # "use-after-free"
print(report.sites)                       # {"freed_at": "uaf.c:18", "used_at": "uaf.c:25"}
print(report.heap_context.free_to_use_ticks)

High-level (records + analyses + narrates in one call, what an agent uses):

import raconteur as rcr

text = rcr.record_and_narrate("./uaf")
# Returns numbered English transcript followed by a single "JSON: {...}"
# line with the full AnalysisReport dict.

Public API

Importable from the top-level raconteur package:

Symbol Purpose
load(trace_dir) -> Session Open an rr replay; raw tier-1 primitives.
narrate(trace_dir) -> NarratedSession Same, plus per-call narration recording.
analyze_crash(session) -> AnalysisReport Run the investigator dispatcher.
record_and_narrate(binary, args="") -> str High-level entry: record + analyse + narrate.
register_investigator(name, matcher, fn) Add a bug-class investigator at import time.
AnalysisReport Stable result dataclass: bug, summary, sites, evidence, heap_context.
HeapContext UAF-only: dangling_chunk, alloc_site_history, free_to_use_ticks, reallocs_spanning_gap.
AllocEvent, AllocatorSpec Heap timeline + allocator-tracking primitives.
WriteEvent Result of find_last_write: expr, loc, tick, frame, new_value.
GLIBC_ALLOCATORS, CXX_ALLOCATORS, ASAN_ALLOCATORS, MSAN_ALLOCATORS, TCMALLOC_ALLOCATORS, JEMALLOC_ALLOCATORS, MIMALLOC_ALLOCATORS Allocator presets to pass to Session.heap_history.

Session (tier-1 primitives, all symmetric forward/reverse):

cont / rcont   step / rstep   next / rnext   finish / rfinish
run_until(loc)   break_(loc, cond=None)   watch(expr, kind)   delete(bp)
where(depth)   locals(frame)   eval(expr, frame)   recent_calls(n)
find_last_write(expr)
heap_history(allocators=None)   suggest_allocators()   current_tick()

find_last_write is the deterministic "where was this last written?" primitive — sets a hardware write-watchpoint on expr, reverse-continues until it fires, and returns a WriteEvent with the writing instruction's source location, call frame, and post-write value. Equivalent to the MVP-criterion step 2 (watch + rcont → last write) in one call.

NarratedSession mirrors these and additionally records a NarrationEvent per call into .events; multi-step operations (heap_history, suggest_allocators) emit one summary event each so the transcript stays readable.

See DESIGN.md for output schemas and the architectural rationale behind the two-tier tool surface.

Allocator support

The default tracks malloc / calloc / realloc / free (glibc). For other allocators, pass an explicit spec list — or let suggest_allocators figure it out:

session = rcr.load("trace/")

# Auto-detect what's in the binary
specs = session.suggest_allocators()
history = session.heap_history(allocators=specs)

# Or pick a family
history = session.heap_history(allocators=rcr.JEMALLOC_ALLOCATORS)

# Or roll your own
history = session.heap_history(allocators=(
    rcr.AllocatorSpec("partition_alloc_root_malloc", "malloc"),
    rcr.AllocatorSpec("partition_alloc_root_free",   "free"),
))

record_and_narrate and the UAF investigator use auto-discovery internally, so the high-level path works on libc, libstdc++, sanitiser builds, and the three named third-party allocators (tcmalloc, jemalloc, mimalloc) without configuration.

Integration as an MCP server

For MCP-aware clients (Claude Desktop, Cursor, IDE plugins), raconteur ships a stdio JSON-RPC server at raconteur.mcp_server:

python -m raconteur.mcp_server          # speaks MCP over stdin/stdout
# or
make mcp-serve

Thirty tools are advertised — three one-shot crash narrators plus the full tier-1 primitive surface for interactive triage:

Tool family Examples Use
One-shot narrators narrate_run, analyze_trace, suggest_allocators Single-call crash report; the original surface.
Session lifecycle rcr_open, rcr_close Open a stateful debugging session against a trace dir; returns a session_id.
Navigation rcr_cont/rcr_rcont, rcr_step/rcr_rstep, rcr_next/rcr_rnext, rcr_finish/rcr_rfinish, rcr_run_until Forward + reverse execution control.
Breakpoints rcr_break, rcr_watch, rcr_delete Conditional breakpoints + watchpoints.
Inspection rcr_where, rcr_locals, rcr_frame_args, rcr_eval, rcr_recent_calls Stack + locals + arg + expression queries.
Heap / memory rcr_heap_history, rcr_read_memory, rcr_classify_addr, rcr_mappings, rcr_find_last_write, rcr_find_writes_to Allocator-aware heap timeline, raw memory reads, mapping classification, watch-based write tracing, multi-step write-origin chase.
Sanitizer logs parse_sanitizer_output Parse ASAN/UBSAN/MSAN/LSAN stderr into a structured SanitizerCrash — useful when you have only a sanitizer log, not a recorded trace.
Misc rcr_suggest_allocators, rcr_current_tick, rcr_narration Session-scoped utilities.

The server has no external dependencies beyond raconteur itself — JSON-RPC framing is implemented directly so the MCP SDK is not required. A typical Claude Desktop ~/Library/Application Support/Claude/claude_desktop_config.json entry:

{
  "mcpServers": {
    "raconteur": {
      "command": "python",
      "args": ["-m", "raconteur.mcp_server"]
    }
  }
}

How raconteur compares to other AI-CTF / debugger tools

Surveyed five prior systems (full notes in docs/improvement_plan.md). Crash-analysis layer only — fuzzer / patch-gen / orchestration intentionally out of scope.

raconteur EnIGMA / SWE-agent HackSynth NYU CTF / Cybench Buttercup (AIxCC 2nd) Atlantis (AIxCC 1st)
Live debugger rr + gdb/MI gdb (raw passthrough) none gdb wrappers, agent-driven none gdb batch + rr-backtracer Docker
Structured crash report AnalysisReport ❌ raw (gdb) text ❌ free-text prose ❌ raw stdout ⚠️ CrashInfo for Redis dedup only ❌ raw <sanitizer_output> + [RCA] text
Allocator-aware heap context ✅ alloc/free timeline + reclaim detection ❌ regex-only
Mapping classification ✅ heap/stack/.text:libc/unmapped per address ❌ agent does info proc mappings
Exploit-craft fields ✅ per bug class (saved-RIP / canary / fmtstr %N$ / chunk offset / abort-msg + tcache idx) ❌ agent re-derives
Write-origin chase find_writes_to(addr, n) ✅ via separate rr-backtracer Docker
Symbol/source resolution ✅ DWARF-resolved at frame construction + per-bug evidence sites ❌ agent does info line *0x... partial (Ghidra) partial (tree-sitter) partial (asan_debug.py identifier dump)
Sanitizer-log parser parse_sanitizer_output (ASAN/UBSAN/MSAN/LSAN/glibc) partial ✅ vendored Clusterfuzz StackParser partial regex
MCP / agent surface ✅ 32 tools (one-shots + tier-1 stateful) IAT bundle (text wire) none shell + Ghidra subcommands task-queue + LLM context only task-queue + per-stage analyzers
Per-event narration cap ✅ HackSynth-style truncation marker per-history-window only hard char cap n/a n/a n/a

The headline differentiator: across all five surveyed systems, raconteur is the only one with a locked-JSON crash report shape, allocator-aware heap context with reclaim detection, and per-bug-class exploit-craft fields. The capabilities other systems each ship in isolation — Atlantis's write-origin chase, Buttercup's regex-only sanitizer parser, HackSynth's narration cap — are all available in raconteur via the same 32-tool MCP surface.

Integration as a SWE-agent / EnIGMA IAT

A ready-to-drop-in bundle lives at examples/swe-agent/narrate.sh. It defines narrate_run as a SWE-agent IAT and shells out to python -m raconteur.agent_tool. The bundle's @yaml preamble gives the agent a tool docstring that includes the (RECOMMENDED FOR MEMORY-SAFETY BUGS) tag and the Use this FIRST guidance.

Order matters. Add config/commands/narrate.sh at the top of command_files in your SWE-agent CTF config, not appended after debug.sh. With deepseek-coder-v2 (16B MoE) the empirical effect is:

narrate.sh listed hashmap_uaf uaf_reuse cxx_uaf
first 3/3 solved, 2 turns 3/3 solved, 2 turns 3/3 solved, 2 turns
last 2/3 solved, 2–6 turns 3/3 (1/3 wrong sites) 0/3 — never reached the tool

Same model, same fixtures, same prompts — only the catalog position changes. Small open models anchor strongly on the first tool listed; giving them narrate_run as the obvious first choice is the difference between "always solves in 2 turns" and "never reaches the right tool".

bench/agent_harness.py is a minimal local-model loop that drives the agent against the same tool catalog without needing the full SWE-agent Docker harness. Useful for iterating on prompts. See examples/swe-agent/README.md for full installation steps.

Acceptance / golden tests

Nine fixtures live under acceptance/. Each has a Makefile that builds the binary and records it under rr. Goldens under tests/golden/*.json capture the normalised expected output (addresses and ticks masked; source locations / sizes / bug class / heap-context shape locked literal).

Fixture Bug Allocator family
uaf use-after-free (1 MiB malloc) glibc
hashmap_uaf UAF (iterator invalidation after rehash) glibc
uaf_reuse UAF with tcache reclaim glibc
cxx_uaf UAF via new/delete libstdc++
jemalloc_uaf UAF via je_malloc/je_free jemalloc (prefixed)
mimalloc_uaf UAF via mi_malloc/mi_free mimalloc
double_free double-free (tcache detector) glibc
fmtstr format-string (printf(argv[1])) n/a
stackov stack-buffer-overflow (canary fire) n/a
make record-all              # build + rr-record every required fixture
make record-jemalloc         # opt-in: needs a je_-prefixed jemalloc build
make record-mimalloc         # opt-in: needs a mimalloc static lib
make acceptance              # run golden tests
make acceptance-update       # regenerate after an intentional schema change

The jemalloc and mimalloc fixtures are opt-in because they require external builds. See acceptance/jemalloc_uaf/Makefile and acceptance/mimalloc_uaf/Makefile for the one-time build steps.

Exit code 0 = ship; exit code 1 = clean unified diff showing what changed. Add a new bug class by writing a fixture, recording it, and calling python tests/test_acceptance.py --update --only <name> to generate a starting golden.

Limitations

  • Replay-only. No live attach; the trace must already exist. Most cases worth analysing are reproducible enough to record once.
  • rr's environment. Requires Intel/AMD with rdtsc/rdtscp and a perf-event-paranoid sysctl. Doesn't run inside most CI containers without configuration.
  • C/C++. Rust binaries with DWARF should work but aren't fixture- tested. No JIT'd or interpreted languages.
  • One bug per investigation. analyze_crash runs the first matching investigator; chained or multi-bug crashes need separate passes today.
  • Allocator layer-mixing was a known issue through commit a5d3e13 but is now fixed (3227b9d); passing overlapping presets like GLIBC_ALLOCATORS + CXX_ALLOCATORS works correctly.

Further reading

  • DESIGN.md — architecture, tier-1/tier-2 split, output schemas, open risks.
  • docs/landscape.md — 2026 survey of agentic-hacking tools and benchmarks (CyberGym, AIxCC, NYU CTF Bench, EnIGMA, Buttercup, Team Atlanta) and where raconteur fits in that ecosystem.
  • acceptance/ — nine end-to-end fixtures, each with a tiny C/C++ source file and Makefile demonstrating the bug class.
  • bench/agent_harness.py — minimal local-model agent loop, used for the A/B measurements that motivated the heap-aware feature. Captured transcripts in bench/results/.
  • examples/swe-agent/ — drop-in IAT bundle for SWE-agent v0.7.0.
  • examples/cybergym/ — recipe for integrating with CyberGym's EnIGMA reference agent (real-CVE benchmark; 1,507 tasks).
  • examples/custom_investigator/ — out-of-tree extension via register_investigator; ships a SIGFPE bug class as the example.

Status

Pre-1.0. The result schema (AnalysisReport, HeapContext, AllocEvent, AllocatorSpec) is locked by tests/golden/; the tier-1 primitive surface is stable enough to depend on. Investigator coverage is intentionally narrow (2 bug classes) and will grow as fixtures arrive.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages