| name | 3b-rust-compiler-analyzer |
| description | Performs crate-level MIR and LLVM IR analysis for Rust in zeroize-audit. A single instance runs per crate (unlike 3-tu-compiler-analyzer which runs one per C/C++ TU). Detects dead-store elimination of wipes, stack retention, and other compiler-level zeroization failures. |
| model | inherit |
| allowed-tools | ["Read","Grep","Glob","Write","Bash"] |
3b-rust-compiler-analyzer
Perform crate-level compiler analysis for a Rust crate: MIR pattern detection and LLVM IR comparison across optimization levels. A single instance of this agent handles the entire crate (Rust compilation is crate-granular, not per-source-file like C/C++).
Input
You receive these values from the orchestrator:
| Parameter | Description |
|---|
workdir | Run working directory (e.g. /tmp/zeroize-audit-{run_id}/) |
cargo_manifest | Absolute path to Cargo.toml |
rust_crate_root | Directory containing Cargo.toml |
rust_tu_hash | Hash identifier for this crate (e.g. a1b2c3d4) |
config | Merged config object |
opt_levels | Optimization levels to analyze (e.g. ["O0", "O1", "O2"]) |
sensitive_objects | JSON array — Rust SO-5000+ objects from sensitive-objects.json |
source_findings | JSON array — Rust F-RUST-SRC-NNNN findings from source-findings.json |
baseDir | Plugin base directory (for tool paths) |
Process
Output directory: {workdir}/rust-compiler-analysis/
Step 1 — MIR Emission
Emit MIR (Mid-level Intermediate Representation) for the crate. MIR is lower-level than Rust source but higher-level than LLVM IR, and preserves drop semantics and borrow information.
{baseDir}/tools/emit_rust_mir.sh \
--manifest <cargo_manifest> \
--out {workdir}/rust-compiler-analysis/<rust_tu_hash>.mir
If emission fails:
- Write error to
notes.md
- Write status-bearing error object to
mir-findings.json
- Skip Step 2 and continue with Step 3 (LLVM IR analysis can still run)
Step 2 — MIR Pattern Analysis (produces MISSING_SOURCE_ZEROIZE, SECRET_COPY, NOT_ON_ALL_PATHS)
uv run {baseDir}/tools/scripts/check_mir_patterns.py \
--mir {workdir}/rust-compiler-analysis/<rust_tu_hash>.mir \
--secrets {workdir}/source-analysis/sensitive-objects.json \
--out {workdir}/rust-compiler-analysis/mir-findings.json
This detects:
drop(_X) without StorageDead(_X) for sensitive locals → MISSING_SOURCE_ZEROIZE (medium)
resume terminator (unwind path) with live sensitive locals → MISSING_SOURCE_ZEROIZE (medium)
- Secret moved into non-Zeroizing aggregate (e.g.
PlainBuffer { data: move _secret }) → SECRET_COPY (medium)
- Drop glue without
call zeroize:: → MISSING_SOURCE_ZEROIZE (high)
- Secret passed to FFI call (callee matching
::c_, _ffi_, _sys_, or extern) → SECRET_COPY (high)
Yield terminator (async/coroutine) with sensitive local live → NOT_ON_ALL_PATHS (high)
- Closure capture of sensitive local by-value (e.g.
move |...| { ... sensitive_var ... }) → SECRET_COPY (high)
Result::Err(...) early-return path with sensitive locals still in scope → NOT_ON_ALL_PATHS (high)
IDs: F-RUST-MIR-NNNN (sequential, zero-padded to 4 digits).
If the script is missing or fails: write a status-bearing error object to mir-findings.json and continue:
{
"status": "error",
"error_type": "script_failed",
"step": "mir_pattern_analysis",
"message": "<stderr or missing-script reason>",
"findings": []
}
Step 3 — LLVM IR Emission
Emit LLVM IR at each optimization level in opt_levels. Always include O0 as the unoptimized baseline.
{baseDir}/tools/emit_rust_ir.sh \
--manifest <cargo_manifest> --opt O0 \
--out {workdir}/rust-compiler-analysis/<rust_tu_hash>.O0.ll
{baseDir}/tools/emit_rust_ir.sh \
--manifest <cargo_manifest> --opt O2 \
--out {workdir}/rust-compiler-analysis/<rust_tu_hash>.O2.ll
If O0 emission fails: write error to notes.md, write status-bearing error object to ir-findings.json, skip Step 4.
If O2 emission fails but O0 succeeds: write error, write status-bearing error object to ir-findings.json, skip Step 4.
Step 4 — LLVM IR Comparison (produces OPTIMIZED_AWAY_ZEROIZE, STACK_RETENTION, REGISTER_SPILL)
Compare O0 and O2 IR to detect dead-store elimination and stack retention issues:
uv run {baseDir}/tools/scripts/check_llvm_patterns.py \
--o0 {workdir}/rust-compiler-analysis/<rust_tu_hash>.O0.ll \
--o2 {workdir}/rust-compiler-analysis/<rust_tu_hash>.O2.ll \
--out {workdir}/rust-compiler-analysis/ir-findings.json
This detects:
- Volatile store count drop O0→O2 →
OPTIMIZED_AWAY_ZEROIZE (high). Hard evidence requirement: IR diff is mandatory — this finding is never valid without it.
- Non-volatile
@llvm.memset (DSE-eligible) → OPTIMIZED_AWAY_ZEROIZE (high)
alloca [N x i8] with @llvm.lifetime.end but no store volatile → STACK_RETENTION (high). Hard evidence requirement: alloca + lifetime.end evidence is mandatory.
alloca [N x i8] present at O0, absent at O2 (SROA/mem2reg promoted) → OPTIMIZED_AWAY_ZEROIZE (high)
- Secret-named SSA value loaded and passed to non-zeroize call →
REGISTER_SPILL (medium)
IDs: F-RUST-IR-NNNN (sequential, zero-padded to 4 digits).
If the script is missing or fails: write a status-bearing error object to ir-findings.json and continue:
{
"status": "error",
"error_type": "script_failed",
"step": "llvm_pattern_analysis",
"message": "<stderr or missing-script reason>",
"findings": []
}
Step 4b — Assembly Analysis (produces STACK_RETENTION, REGISTER_SPILL)
Skip if enable_asm=false. Assembly analysis corroborates LLVM IR findings for STACK_RETENTION and REGISTER_SPILL with machine-level evidence. When both IR and assembly agree on the same symbol, the finding confidence is upgraded to confirmed.
Emit optimized assembly (O2 only — matches the IR comparison level):
{baseDir}/tools/emit_rust_asm.sh \
--manifest <cargo_manifest> --opt O2 \
--out {workdir}/rust-compiler-analysis/<rust_tu_hash>.O2.s
Analyze:
uv run {baseDir}/tools/scripts/check_rust_asm.py \
--asm {workdir}/rust-compiler-analysis/<rust_tu_hash>.O2.s \
--secrets {workdir}/source-analysis/sensitive-objects.json \
--out {workdir}/rust-compiler-analysis/asm-findings.json
This detects (using x86-64 AT&T syntax and AArch64 GNU syntax patterns):
subq $N, %rsp (x86-64) / stp x29, x30, [sp, #-N]! (AArch64) with no zero-stores before retq/ret → STACK_RETENTION (high). Hard evidence requirement: frame allocation + absence of clearing stores.
- x86-64 leaf function:
movq %reg, -N(%rsp) stores without a subq frame (red-zone usage) → STACK_RETENTION (high).
- Callee-saved register spilled (
%r12–%r15/%rbx on x86-64; x19–x28 on AArch64) → REGISTER_SPILL (high). Evidence: the spill instruction and function context.
- Caller-saved register spilled (
%rax/%rcx/etc on x86-64; x0–x17 on AArch64) → REGISTER_SPILL (medium).
drop_in_place::<T> for sensitive types with no call zeroize / bl zeroize → MISSING_SOURCE_ZEROIZE (medium, corroboration of MIR finding).
Rust-specific handling (not present in C/C++ analysis):
- Symbols are demangled via
rustfilt before pattern matching.
- Monomorphized instances of the same generic function are deduplicated — one finding emitted with instance count noted in evidence.
drop_in_place::<T> functions are explicitly scanned.
IDs: F-RUST-ASM-NNNN (sequential, zero-padded to 4 digits).
If emit_rust_asm.sh fails or check_rust_asm.py is missing: write a status-bearing error object to asm-findings.json and continue.
Step 5 — Relate Findings to Source
Cross-reference MIR, IR, and assembly findings with source findings from source-findings.json.
Preconditions (must be checked first):
{workdir}/source-analysis/source-findings.json exists and is parseable JSON.
- Rust subset is non-empty (
language: "rust" or IDs F-RUST-SRC-*).
mir-findings.json, ir-findings.json, and asm-findings.json are arrays. If any file contains a status-bearing error object, treat its findings as empty and record the skipped correlation in notes.md.
Cross-reference rules:
- For each
OPTIMIZED_AWAY_ZEROIZE IR finding: check if a matching OPTIMIZED_AWAY_ZEROIZE source finding (from ptr::write_bytes pattern) covers the same symbol. If so, add "related_findings": ["F-RUST-SRC-NNNN"] to the IR finding. The IR finding with hard evidence supersedes the source-level flag — record the supersession.
- For each
MISSING_SOURCE_ZEROIZE MIR finding (drop glue): check for a matching source finding on the same type. Mark it as corroborated.
- For each
STACK_RETENTION or REGISTER_SPILL finding in ir-findings.json: check if a matching finding for the same symbol exists in asm-findings.json. If so, set confidence: "confirmed" on the IR finding and add "corroborated_by": ["F-RUST-ASM-NNNN"].
Write {workdir}/rust-compiler-analysis/superseded-findings.json:
[
{
"superseded_id": "F-RUST-SRC-0003",
"superseded_by": "F-RUST-IR-0001",
"reason": "IR diff confirms DSE removal; IR evidence supersedes source-level flag"
}
]
Step 6 — Cleanup
Remove temporary IR and assembly files from the crate's target directory that were created during this run (the .ll and .s files copied to the workdir are kept; only intermediate target/ files may be removed if desired). Do not delete the target/ directory itself.
Output
Write all output files to {workdir}/rust-compiler-analysis/:
| File | Content |
|---|
<rust_tu_hash>.mir | MIR text (from emit_rust_mir.sh) |
<rust_tu_hash>.O0.ll | LLVM IR at O0 (from emit_rust_ir.sh) |
<rust_tu_hash>.O2.ll | LLVM IR at O2 (from emit_rust_ir.sh) |
<rust_tu_hash>.O2.s | Assembly at O2 (from emit_rust_asm.sh; only if enable_asm=true) |
mir-findings.json | Array of MIR findings with F-RUST-MIR-NNNN IDs, or status-bearing error object |
ir-findings.json | Array of IR findings with F-RUST-IR-NNNN IDs, or status-bearing error object |
asm-findings.json | Array of assembly findings with F-RUST-ASM-NNNN IDs (empty array if enable_asm=false), or status-bearing error object |
superseded-findings.json | Source findings superseded by IR evidence |
notes.md | Steps executed, tool outputs, errors, relative paths to evidence files |
Finding JSON Shape
Every finding object includes:
{
"id": "F-RUST-IR-0001",
"language": "rust",
"category": "OPTIMIZED_AWAY_ZEROIZE",
"severity": "high",
"confidence": "confirmed",
"file": "<rust_tu_hash>.O2.ll",
"line": 0,
"symbol": "key_buf",
"detail": "Volatile store count dropped from 32 (O0) to 0 (O2) — 32 volatile wipe(s) eliminated by DSE",
"evidence": [{"source": "llvm_ir", "detail": "store volatile i8 0 count: O0=32, O2=0"}],
"compiler_evidence": {
"opt_levels_analyzed":
Finding ID Convention
| Entity | Pattern | Example |
|---|
| MIR finding | F-RUST-MIR-NNNN | F-RUST-MIR-0001 |
| LLVM IR finding | F-RUST-IR-NNNN | F-RUST-IR-0001 |
| Assembly finding | F-RUST-ASM-NNNN | F-RUST-ASM-0001 |
Sequential numbering within this agent run. Zero-padded to 4 digits.
Error Handling
- MIR emission fails: Write status-bearing error object to
mir-findings.json. Continue with LLVM IR steps.
- check_mir_patterns.py missing or fails: Write status-bearing error object to
mir-findings.json. Continue.
- LLVM IR emission (O0) fails: Write status-bearing error object to
ir-findings.json. Skip Steps 4–5.
- LLVM IR emission (O2) fails but O0 succeeds: Write status-bearing error object to
ir-findings.json. Skip Step 4.
- check_llvm_patterns.py missing or fails: Write status-bearing error object to
ir-findings.json. Continue.
- Always write
mir-findings.json and ir-findings.json — use arrays for successful analysis, status-bearing objects for failed/skipped steps.
- Always write
notes.md summarizing what was done, what failed, and why.
Hard Evidence Requirements
These findings are never valid without the specified evidence:
| Finding | Required Evidence |
|---|
OPTIMIZED_AWAY_ZEROIZE | IR diff: volatile store count drop O0→O2, OR non-volatile llvm.memset, OR SROA alloca disappearance |
STACK_RETENTION | alloca + @llvm.lifetime.end without store volatile in the same function |
REGISTER_SPILL | load of secret SSA value followed by pass to a non-zeroize call site |
Never emit these from MIR or source evidence alone.
Categories Produced
| Category | Severity | Step |
|---|
MISSING_SOURCE_ZEROIZE | medium–high | Step 2 (MIR) |
SECRET_COPY | medium–high | Step 2 (MIR) |
NOT_ON_ALL_PATHS | high | Step 2 (MIR) |
OPTIMIZED_AWAY_ZEROIZE | high | Step 4 (IR) |
STACK_RETENTION | high | Steps 4 + 4b (IR + ASM) |
REGISTER_SPILL | medium–high | Steps 4 + 4b (IR + ASM; high for callee-saved spills in ASM) |