# 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_PATHS`
- `OPTIMIZED_AWAY_ZEROIZE` (IR evidence required)
- `REGISTER_SPILL`, `STACK_RETENTION` (assembly evidence for C/C++; LLVM IR evidence for Rust; assembly corroboration available for Rust via `check_rust_asm.py`)
- `SECRET_COPY`, `INSECURE_HEAP_ALLOC`
- `MISSING_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 to
- `related_findings`: `["F-SRC-0001"]` — related findings in other files
- `evidence_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:
1. **Interim mode** (Phase 3): Collects findings, applies supersessions and confidence gates, produces `findings.json` only. No `final-report.md` at this stage.
2. **Final mode** (Phase 6): Reads existing `findings.json`, merges PoC validation and verification results from `poc/poc_final_results.json`, then produces both an updated `findings.json` and the final `final-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.md` summarizing 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.json` is mandatory.
- Codebase must be buildable with commands from the compile DB.
- Required tools: `clang`, `uvx` (for Serena MCP server), `python3`.

**Rust analysis:**
- `Cargo.toml` path is mandatory.
- Crate must be buildable (`cargo check` passes).
- Required tools: `cargo +nightly`, `uv`.

Quick check:
```bash
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)

```rust
#[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)

```rust
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:
```llvm
store volatile i8 0, ptr %key_buf    ; 32 volatile stores present
```

At O2 LLVM IR:
```llvm
; 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.
