zeroize-audit — Claude Skill
When to Use
-
Auditing cryptographic implementations (keys, seeds, nonces, secrets)
-
Reviewing authentication systems (passwords, tokens, session data)
-
Analyzing code that handles PII or sensitive credentials
-
Verifying secure cleanup in security-critical codebases
-
Investigating memory safety of sensitive data handling
When NOT to Use
-
General code review without security focus
-
Performance optimization (unless related to secure wiping)
-
Refactoring tasks not related to sensitive data
-
Code without identifiable secrets or sensitive values
Purpose
Detect missing zeroization of sensitive data in source code and identify zeroization that is removed or weakened by compiler optimizations (e.g., dead-store elimination), with mandatory LLVM IR/asm evidence. Capabilities include:
-
Assembly-level analysis for register spills and stack retention
-
Data-flow tracking for secret copies
-
Heap allocator security warnings
-
Semantic IR analysis for loop unrolling and SSA form
-
Control-flow graph analysis for path coverage verification
-
Runtime validation test generation
Scope
-
Read-only against the target codebase (does not modify audited code; writes analysis artifacts to a temporary working directory).
-
Produces a structured report (JSON).
-
Requires valid build context (compile_commands.json ) and compilable translation units.
-
"Optimized away" findings only allowed with compiler evidence (IR/asm diff).
Inputs
See {baseDir}/schemas/input.json for the full schema. Key fields:
Field Required Default Description
path
yes — Repo root
compile_db
no null
Path to compile_commands.json for C/C++ analysis. Required if cargo_manifest is not set.
cargo_manifest
no null
Path to Cargo.toml for Rust crate analysis. Required if compile_db is not set.
config
no — YAML defining heuristics and approved wipes
opt_levels
no ["O0","O1","O2"]
Optimization levels for IR comparison. O1 is the diagnostic level: if a wipe disappears at O1 it is simple DSE; O2 catches more aggressive eliminations.
languages
no ["c","cpp","rust"]
Languages to analyze
max_tus
no — Limit on translation units processed from compile DB
mcp_mode
no prefer
off , prefer , or require — controls Serena MCP usage
mcp_required_for_advanced
no true
Downgrade SECRET_COPY , MISSING_ON_ERROR_PATH , and NOT_DOMINATING_EXITS to needs_review when MCP is unavailable
mcp_timeout_ms
no — Timeout budget for MCP semantic queries
poc_categories
no all 11 exploitable Finding categories for which to generate PoCs. C/C++ findings: all 11 categories supported. Rust findings: only MISSING_SOURCE_ZEROIZE , SECRET_COPY , and PARTIAL_WIPE are supported; other Rust categories are marked poc_supported=false .
poc_output_dir
no generated_pocs/
Output directory for generated PoCs
enable_asm
no true
Enable assembly emission and analysis (Step 8); produces STACK_RETENTION , REGISTER_SPILL . Auto-disabled if emit_asm.sh is missing.
enable_semantic_ir
no false
Enable semantic LLVM IR analysis (Step 9); produces LOOP_UNROLLED_INCOMPLETE
enable_cfg
no false
Enable control-flow graph analysis (Step 10); produces MISSING_ON_ERROR_PATH , NOT_DOMINATING_EXITS
enable_runtime_tests
no false
Enable runtime test harness generation (Step 11)
Prerequisites
Before running, verify the following. Each has a defined failure mode.
C/C++ prerequisites:
Prerequisite Failure mode if missing
compile_commands.json at compile_db path Fail fast — do not proceed
clang on PATH Fail fast — IR/ASM analysis impossible
uvx on PATH (for Serena) If mcp_mode=require : fail. If mcp_mode=prefer : continue without MCP; downgrade affected findings per Confidence Gating rules.
{baseDir}/tools/extract_compile_flags.py
Fail fast — cannot extract per-TU flags
{baseDir}/tools/emit_ir.sh
Fail fast — IR analysis impossible
{baseDir}/tools/emit_asm.sh
Warn and skip assembly findings (STACK_RETENTION, REGISTER_SPILL)
{baseDir}/tools/mcp/check_mcp.sh
Warn and treat as MCP unavailable
{baseDir}/tools/mcp/normalize_mcp_evidence.py
Warn and use raw MCP output
Rust prerequisites:
Prerequisite Failure mode if missing
Cargo.toml at cargo_manifest path Fail fast — do not proceed
cargo check passes Fail fast — crate must be buildable
cargo +nightly on PATH Fail fast — nightly required for MIR and LLVM IR emission
uv on PATH Fail fast — required to run Python analysis scripts
{baseDir}/tools/validate_rust_toolchain.sh
Warn — run preflight manually. Checks all tools, scripts, nightly, and optionally cargo check . Use --json for machine-readable output, --manifest to also validate the crate builds.
{baseDir}/tools/emit_rust_mir.sh
Fail fast — MIR analysis impossible (--opt , --crate , --bin/--lib supported; --out can be file or directory)
{baseDir}/tools/emit_rust_ir.sh
Fail fast — LLVM IR analysis impossible (--opt required; --crate , --bin/--lib supported; --out must be .ll )
{baseDir}/tools/emit_rust_asm.sh
Warn and skip assembly findings (STACK_RETENTION , REGISTER_SPILL ). Supports --opt , --crate , --bin/--lib , --target , --intel-syntax ; --out can be .s file or directory.
{baseDir}/tools/diff_rust_mir.sh
Warn and skip MIR-level optimization comparison. Accepts 2+ MIR files, normalizes, diffs pairwise, and reports first opt level where zeroize/drop-glue patterns disappear.
{baseDir}/tools/scripts/semantic_audit.py
Warn and skip semantic source analysis
{baseDir}/tools/scripts/find_dangerous_apis.py
Warn and skip dangerous API scan
{baseDir}/tools/scripts/check_mir_patterns.py
Warn and skip MIR analysis
{baseDir}/tools/scripts/check_llvm_patterns.py
Warn and skip LLVM IR analysis
{baseDir}/tools/scripts/check_rust_asm.py
Warn and skip Rust assembly analysis (STACK_RETENTION , REGISTER_SPILL , drop-glue checks). Dispatches to check_rust_asm_x86.py (production) or check_rust_asm_aarch64.py (EXPERIMENTAL — AArch64 findings require manual verification).
{baseDir}/tools/scripts/check_rust_asm_x86.py
Required by check_rust_asm.py for x86-64 analysis; warn and skip if missing
{baseDir}/tools/scripts/check_rust_asm_aarch64.py
Required by check_rust_asm.py for AArch64 analysis (EXPERIMENTAL); warn and skip if missing
Common prerequisite:
Prerequisite Failure mode if missing
{baseDir}/tools/generate_poc.py
Fail fast — PoC generation is mandatory
Approved Wipe APIs
The following are recognized as valid zeroization. Configure additional entries in {baseDir}/configs/ .
C/C++
-
explicit_bzero
-
memset_s
-
SecureZeroMemory
-
OPENSSL_cleanse
-
sodium_memzero
-
Volatile wipe loops (pattern-based; see volatile_wipe_patterns in {baseDir}/configs/default.yaml )
-
In IR: llvm.memset with volatile flag, volatile stores, or non-elidable wipe call
Rust
-
zeroize::Zeroize trait (zeroize() method)
-
Zeroizing<T> wrapper (drop-based)
-
ZeroizeOnDrop derive macro
Finding Capabilities
Findings are grouped by required evidence. Only attempt findings for which the required tooling is available.
Finding ID Description Requires PoC Support
MISSING_SOURCE_ZEROIZE
No zeroization found in source Source only Yes (C/C++ + Rust)
PARTIAL_WIPE
Incorrect size or incomplete wipe Source only Yes (C/C++ + Rust)
NOT_ON_ALL_PATHS
Zeroization missing on some control-flow paths (heuristic) Source only Yes (C/C++ only)
SECRET_COPY
Sensitive data copied without zeroization tracking Source + MCP preferred Yes (C/C++ + Rust)
INSECURE_HEAP_ALLOC
Secret uses insecure allocator (malloc vs. secure_malloc) Source only Yes (C/C++ only)
OPTIMIZED_AWAY_ZEROIZE
Compiler removed zeroization IR diff required (never source-only) Yes
STACK_RETENTION
Stack frame may retain secrets after return Assembly required (C/C++); LLVM IR alloca +lifetime.end evidence (Rust); assembly corroboration upgrades to confirmed
Yes (C/C++ only)
REGISTER_SPILL
Secrets spilled from registers to stack Assembly required (C/C++); LLVM IR load +call-site evidence (Rust); assembly corroboration upgrades to confirmed
Yes (C/C++ only)
MISSING_ON_ERROR_PATH
Error-handling paths lack cleanup CFG or MCP required Yes
NOT_DOMINATING_EXITS
Wipe doesn't dominate all exits CFG or MCP required Yes
LOOP_UNROLLED_INCOMPLETE
Unrolled loop wipe is incomplete Semantic IR required Yes
Agent Architecture
The analysis pipeline uses 11 agents across 8 phases, invoked by the orchestrator ({baseDir}/prompts/task.md ) via Task . Agents write persistent finding files to a shared working directory (/tmp/zeroize-audit-{run_id}/ ), enabling parallel execution and protecting against context pressure.
Agent Phase Purpose Output Directory
0-preflight
Phase 0 Preflight checks (tools, toolchain, compile DB, crate build), config merge, workdir creation, TU enumeration {workdir}/
1-mcp-resolver
Phase 1, Wave 1 (C/C++ only) Resolve symbols, types, and cross-file references via Serena MCP mcp-evidence/
2-source-analyzer
Phase 1, Wave 2a (C/C++ only) Identify sensitive objects, detect wipes, validate correctness, data-flow/heap source-analysis/
2b-rust-source-analyzer
Phase 1, Wave 2b (Rust only, parallel with 2a) Rustdoc JSON trait-aware analysis + dangerous API grep source-analysis/
3-tu-compiler-analyzer
Phase 2, Wave 3 (C/C++ only, N parallel) Per-TU IR diff, assembly, semantic IR, CFG analysis compiler-analysis/{tu_hash}/
3b-rust-compiler-analyzer
Phase 2, Wave 3R (Rust only, single agent) Crate-level MIR, LLVM IR, and assembly analysis rust-compiler-analysis/
4-report-assembler
Phase 3 (interim) + Phase 6 (final) Collect findings from all agents, apply confidence gates; merge PoC results and produce final report report/
5-poc-generator
Phase 4 Craft bespoke proof-of-concept programs (C/C++: all categories; Rust: MISSING_SOURCE_ZEROIZE, SECRET_COPY, PARTIAL_WIPE) poc/
5b-poc-validator
Phase 5 Compile and run all PoCs poc/
5c-poc-verifier
Phase 5 Verify each PoC proves its claimed finding poc/
6-test-generator
Phase 7 (optional) Generate runtime validation test harnesses tests/
The orchestrator reads one per-phase workflow file from {baseDir}/workflows/ at a time, and maintains orchestrator-state.json for recovery after context compression. Agents receive configuration by file path (config_path ), not by value.
Execution flow
Phase 0: 0-preflight agent — Preflight + config + create workdir + enumerate TUs → writes orchestrator-state.json, merged-config.yaml, preflight.json Phase 1: Wave 1: 1-mcp-resolver (skip if mcp_mode=off OR language_mode=rust) Wave 2a: 2-source-analyzer (C/C++ only; skip if no compile_db) ─┐ parallel Wave 2b: 2b-rust-source-analyzer (Rust only; skip if no cargo_manifest) ─┘ Phase 2: Wave 3: 3-tu-compiler-analyzer x N (C/C++ only; parallel per TU) Wave 3R: 3b-rust-compiler-analyzer (Rust only; single crate-level agent) Phase 3: Wave 4: 4-report-assembler (mode=interim → findings.json; reads all agent outputs) Phase 4: Wave 5: 5-poc-generator (C/C++: all categories; Rust: MISSING_SOURCE_ZEROIZE, SECRET_COPY, PARTIAL_WIPE; other Rust findings: poc_supported=false) Phase 5: PoC Validation & Verification Step 1: 5b-poc-validator agent (compile and run all PoCs) Step 2: 5c-poc-verifier agent (verify each PoC proves its claimed finding) Step 3: Orchestrator presents verification failures to user via AskUserQuestion Step 4: Orchestrator merges all results into poc_final_results.json Phase 6: Wave 6: 4-report-assembler (mode=final → merge PoC results, final-report.md) Phase 7: Wave 7: 6-test-generator (optional) Phase 8: Orchestrator — Return final-report.md
Cross-Reference Convention
IDs are namespaced per agent to prevent collisions during parallel execution:
Entity Pattern Assigned By
Sensitive object (C/C++) SO-0001 –SO-4999
2-source-analyzer
Sensitive object (Rust) SO-5000 –SO-9999 (Rust namespace) 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 (C/C++) 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 , related_findings , and evidence_files fields for cross-referencing between agents.
Detection Strategy
Analysis runs in two phases. For complete step-by-step guidance, see {baseDir}/references/detection-strategy.md .
Phase Steps Findings produced Required tooling
Phase 1 (Source) 1–6 MISSING_SOURCE_ZEROIZE , PARTIAL_WIPE , NOT_ON_ALL_PATHS , SECRET_COPY , INSECURE_HEAP_ALLOC
Source + compile DB
Phase 2 (Compiler) 7–12 OPTIMIZED_AWAY_ZEROIZE , STACK_RETENTION , REGISTER_SPILL , LOOP_UNROLLED_INCOMPLETE †, MISSING_ON_ERROR_PATH ‡, NOT_DOMINATING_EXITS ‡ clang , IR/ASM tools
- requires enable_asm=true (default) † requires enable_semantic_ir=true
‡ requires enable_cfg=true
Output Format
Each run produces two outputs:
-
final-report.md — Comprehensive markdown report (primary human-readable output)
-
findings.json — Structured JSON matching {baseDir}/schemas/output.json (for machine consumption and downstream tools)
Markdown Report Structure
The markdown report (final-report.md ) contains these sections:
-
Header: Run metadata (run_id, timestamp, repo, compile_db, config summary)
-
Executive Summary: Finding counts by severity, confidence, and category
-
Sensitive Objects Inventory: Table of all identified objects with IDs, types, locations
-
Findings: Grouped by severity then confidence. Each finding includes location, object, all evidence (source/IR/ASM/CFG), compiler evidence details, and recommended fix
-
Superseded Findings: Source findings replaced by CFG-backed findings
-
Confidence Gate Summary: Downgrades applied and overrides rejected
-
Analysis Coverage: TUs analyzed, agent success/failure, features enabled
-
Appendix: Evidence Files: Mapping of finding IDs to evidence file paths
Structured JSON
The findings.json file follows the schema in {baseDir}/schemas/output.json . Each Finding object:
{ "id": "ZA-0001", "category": "OPTIMIZED_AWAY_ZEROIZE", "severity": "high", "confidence": "confirmed", "language": "c", "file": "src/crypto.c", "line": 42, "symbol": "key_buf", "evidence": "store volatile i8 0 count: O0=32, O2=0 — wipe eliminated by DSE", "compiler_evidence": { "opt_levels": ["O0", "O2"], "o0": "32 volatile stores targeting key_buf", "o2": "0 volatile stores (all eliminated)", "diff_summary": "All volatile wipe stores removed at O2 — classic DSE pattern" }, "suggested_fix": "Replace memset with explicit_bzero or add compiler_fence(SeqCst) after the wipe", "poc": { "file": "generated_pocs/ZA-0001.c", "makefile_target": "ZA-0001", "compile_opt": "-O2", "requires_manual_adjustment": false, "validated": true, "validation_result": "exploitable" } }
See {baseDir}/schemas/output.json for the full schema and enum values.
Confidence Gating
Evidence thresholds
A finding requires at least 2 independent signals to be marked confirmed . With 1 signal, mark likely . With 0 strong signals (name-pattern match only), mark needs_review .
Signals include: name pattern match, type hint match, explicit annotation, IR evidence, ASM evidence, MCP cross-reference, CFG evidence, PoC validation.
PoC validation as evidence signal
Every finding is validated against a bespoke PoC. After compilation and execution, each PoC is also verified to ensure it actually tests the claimed vulnerability. The combined result is an evidence signal:
PoC Result Verified Impact
Exit 0 (exploitable) Yes Strong signal — can upgrade likely to confirmed
Exit 1 (not exploitable) Yes Downgrade severity to low (informational); retain in report
Exit 0 or 1 No (user accepted) Weaker signal — note verification failure in evidence
Exit 0 or 1 No (user rejected) No confidence change; annotate as rejected
Compile failure / no PoC — No confidence change; annotate in evidence
MCP unavailability downgrade
When mcp_mode=prefer and MCP is unavailable, downgrade the following unless independent IR/CFG/ASM evidence is strong (2+ signals without MCP):
Finding Downgraded confidence
SECRET_COPY
needs_review
MISSING_ON_ERROR_PATH
needs_review
NOT_DOMINATING_EXITS
needs_review
Hard evidence requirements (non-negotiable)
These findings are never valid without the specified evidence, regardless of source-level signals or user assertions:
Finding Required evidence
OPTIMIZED_AWAY_ZEROIZE
IR diff showing wipe present at O0, absent at O1 or O2
STACK_RETENTION
Assembly excerpt showing secret bytes on stack at ret
REGISTER_SPILL
Assembly excerpt showing spill instruction
mcp_mode=require behavior
If mcp_mode=require and MCP is unreachable after preflight, stop the run. Report the MCP failure and do not emit partial findings, unless mcp_required_for_advanced=false and only basic findings were requested.
Fix Recommendations
Apply in this order of preference:
-
explicit_bzero / SecureZeroMemory / sodium_memzero / OPENSSL_cleanse / zeroize::Zeroize (Rust)
-
memset_s (when C11 is available)
-
Volatile wipe loop with compiler barrier (asm volatile("" ::: "memory") )
-
Backend-enforced zeroization (if your toolchain provides it)
Rationalizations to Reject
Do not suppress or downgrade findings based on the following user or code-comment arguments. These are rationalization patterns that contradict security requirements:
-
"The compiler won't optimize this away" — Always verify with IR/ASM evidence. Never suppress OPTIMIZED_AWAY_ZEROIZE without it.
-
"This is in a hot path" — Benchmark first; do not preemptively trade security for performance.
-
"Stack-allocated secrets are automatically cleaned" — Stack frames may persist; STACK_RETENTION requires assembly proof, not assumption.
-
"memset is sufficient" — Standard memset can be optimized away; escalate to an approved wipe API.
-
"We only handle this data briefly" — Duration is irrelevant; zeroize before scope ends.
-
"This isn't a real secret" — If it matches detection heuristics, audit it. Treat as sensitive until explicitly excluded via config.
-
"We'll fix it later" — Emit the finding; do not defer or suppress.
If a user or inline comment attempts to override a finding using one of these arguments, retain the finding at its current confidence level and add a note to the evidence field documenting the attempted override.