zeroize-audit (Claude Skill)
Audits C/C++/Rust code for missing zeroization and compiler-removed wipes. Pipeline: source scan -> MCP/LSP semantic context -> IR diff -> assembly checks.
Findings
MISSING_SOURCE_ZEROIZE,PARTIAL_WIPE,NOT_ON_ALL_PATHSOPTIMIZED_AWAY_ZEROIZE(IR evidence required)REGISTER_SPILL,STACK_RETENTION(assembly evidence for C/C++; LLVM IR evidence for Rust; assembly corroboration available for Rust viacheck_rust_asm.py)SECRET_COPY,INSECURE_HEAP_ALLOCMISSING_ON_ERROR_PATH,NOT_DOMINATING_EXITS,LOOP_UNROLLED_INCOMPLETE
Working Directory
Each run creates a working directory at /tmp/zeroize-audit-{run_id}/ with the following structure. Agents write persistent finding files here; later agents and the orchestrator reconstruct the full picture from these files without relying on conversation history.
/tmp/zeroize-audit-{run_id}/
preflight.json # Orchestrator: env, config, TU list
mcp-evidence/
status.json # MCP resolver status (success/partial/fail)
symbols.json # Resolved symbol definitions + types
references.json # Cross-file reference graph
notes.md # MCP observations + cross-refs
source-analysis/
sensitive-objects.json # C/C++ SO-NNNN + Rust SO-NNNN (shared, appended by each source-analyzer)
source-findings.json # F-SRC-NNNN (C/C++) + F-RUST-SRC-NNNN (Rust, appended)
tu-map.json # C/C++ TU hashes + Rust crate hash
rust-semantic-findings.json # Intermediate: 2b-rust-source-analyzer rustdoc output
rust-dangerous-api-findings.json # Intermediate: 2b-rust-source-analyzer grep output
rust-notes.md # 2b-rust-source-analyzer notes
notes.md # 2-source-analyzer observations
rust-compiler-analysis/
{rust_tu_hash}.mir # MIR text (emit_rust_mir.sh; supports --opt, --bin/--lib)
{rust_tu_hash}.O0.ll # LLVM IR at O0 (emit_rust_ir.sh; supports --bin/--lib)
{rust_tu_hash}.O2.ll # LLVM IR at O2 (emit_rust_ir.sh; supports --bin/--lib)
{rust_tu_hash}.O2.s # Assembly at O2 (emit_rust_asm.sh; only if enable_asm=true)
mir-findings.json # F-RUST-MIR-NNNN IDs
ir-findings.json # F-RUST-IR-NNNN IDs
asm-findings.json # F-RUST-ASM-NNNN IDs (empty array if enable_asm=false)
notes.md
compiler-analysis/
{tu_hash}/
ir-findings.json # F-IR-{tu_hash}-NNNN IDs
asm-findings.json # F-ASM-{tu_hash}-NNNN IDs
cfg-findings.json # F-CFG-{tu_hash}-NNNN IDs
semantic-ir.json # F-SIR-{tu_hash}-NNNN IDs
superseded-findings.json # CFG results that replace heuristic source findings
notes.md
report/
raw-findings.json # All findings pre-gating
id-mapping.json # Namespaced IDs -> final ZA-NNNN IDs
findings.json # Gated findings (structured JSON for downstream tools)
final-report.md # Comprehensive markdown report (primary output)
notes.md
poc/ # PoC files, manifest, validation/verification results, notes.md
poc_manifest.json # Generated by agent 5
poc_validation_results.json # Written by agent 5b (compile/run results)
poc_verification.json # Written by agent 5c (semantic verification)
poc_final_results.json # Written by orchestrator Phase 5 (merged results)
tests/ # Test harnesses, Makefile, notes.md
Cross-Reference Convention
IDs are namespaced per agent to prevent collisions during parallel execution:
| Entity | Pattern | Assigned By |
|---|---|---|
| Sensitive object (C/C++) | SO-NNNN |
2-source-analyzer |
| Sensitive object (Rust) | SO-NNNN (offset 5000+) |
2b-rust-source-analyzer |
| Source finding (C/C++) | F-SRC-NNNN |
2-source-analyzer |
| Source finding (Rust) | F-RUST-SRC-NNNN |
2b-rust-source-analyzer |
| IR finding (C/C++) | F-IR-{tu_hash}-NNNN |
3-tu-compiler-analyzer |
| ASM finding | F-ASM-{tu_hash}-NNNN |
3-tu-compiler-analyzer |
| CFG finding | F-CFG-{tu_hash}-NNNN |
3-tu-compiler-analyzer |
| Semantic IR finding | F-SIR-{tu_hash}-NNNN |
3-tu-compiler-analyzer |
| Rust MIR finding | F-RUST-MIR-NNNN |
3b-rust-compiler-analyzer |
| Rust LLVM IR finding | F-RUST-IR-NNNN |
3b-rust-compiler-analyzer |
| Rust assembly finding | F-RUST-ASM-NNNN |
3b-rust-compiler-analyzer |
| Translation unit | TU-{hash} |
Orchestrator |
| Final finding | ZA-NNNN |
4-report-assembler |
Every finding JSON object includes:
related_objects:["SO-0003"]— which sensitive objects this applies torelated_findings:["F-SRC-0001"]— related findings in other filesevidence_files:["compiler-analysis/a1b2/ir-diff-O0-O2.txt"]— paths relative to workdir
Dual-Mode Report Assembly
Agent 4-report-assembler is invoked twice during a run:
- Interim mode (Phase 3): Collects findings, applies supersessions and confidence gates, produces
findings.jsononly. Nofinal-report.mdat this stage. - Final mode (Phase 6): Reads existing
findings.json, merges PoC validation and verification results frompoc/poc_final_results.json, then produces both an updatedfindings.jsonand the finalfinal-report.md.
Agent Error Protocol
- Always write output files: Every agent must write its status/output JSON files even on failure (use empty arrays
[]or error status objects). - Prefer partial results over nothing: If one sub-step fails (e.g., ASM analysis), write results from completed steps and continue.
- Notes.md is mandatory: Every agent writes a
notes.mdsummarizing what it did, any errors, and relative paths to its output files. - Temp file cleanup: Agents must clean up
/tmp/zeroize-audit/<tu_hash>.*temp files on completion or failure.
Prerequisites
C/C++ analysis:
compile_commands.jsonis mandatory.- Codebase must be buildable with commands from the compile DB.
- Required tools:
clang,uvx(for Serena MCP server),python3.
Rust analysis:
Cargo.tomlpath is mandatory.- Crate must be buildable (
cargo checkpasses). - Required tools:
cargo +nightly,uv.
Quick check:
which clang uvx python3 # C/C++
cargo +nightly --version # Rust
uv --version # Rust Python scripts
Rust Analysis — Few-Shot Examples
Example 1 — Copy derive on sensitive type → SECRET_COPY (critical)
#[derive(Copy, Clone)]
struct HmacKey([u8; 32]);
Finding: SECRET_COPY (critical). #[derive(Copy)] on HmacKey — all assignments are untracked duplicates, no Drop ever runs. Every let k2 = k1 silently copies all 32 key bytes with no automatic cleanup.
Fix: Remove Copy. Add #[derive(ZeroizeOnDrop)] from the zeroize crate.
Example 2 — mem::forget on secret → MISSING_SOURCE_ZEROIZE (critical)
let key = SecretKey::new();
// ... use key ...
std::mem::forget(key); // BAD: prevents Drop / ZeroizeOnDrop
Finding: MISSING_SOURCE_ZEROIZE (critical). mem::forget() prevents Drop and ZeroizeOnDrop from running — secret bytes remain in memory indefinitely.
Fix: Remove mem::forget. Let the value drop normally, or call key.zeroize() before the forget if explicit timing is required.
Example 3 — Non-volatile memset removed at O2 → OPTIMIZED_AWAY_ZEROIZE (high)
At O0 LLVM IR:
store volatile i8 0, ptr %key_buf ; 32 volatile stores present
At O2 LLVM IR:
; stores absent — LLVM DSE removed them (key_buf never read after)
Finding: OPTIMIZED_AWAY_ZEROIZE (high). Volatile store count dropped from 32 (O0) to 0 (O2). Dead-store elimination removed the wipe.
Fix: Use zeroize::Zeroize::zeroize(&mut key_buf) which emits a compiler-fence-backed wipe that survives DSE.