Claude Agent

3b-rust-compiler-analyzer

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.

LLM Mart · 0 points · 5 views 0 listing impressions 0 install-command copies

What vetted this — trust report

Download trailofbits-skills-plugins_zeroize-audit_agents_3b-rust-compiler-analyzer.md-321ccfe.zip · 5 KB
trailofbits/skills 7234 616 forks CC-BY-SA-4.0 Updated 10h ago
Part of trailofbits/skills — 100 skills

Install

skills CLI npx skills add https://github.com/trailofbits/skills/tree/main/plugins/zeroize-audit/agents/3b-rust-compiler-analyzer.md
Git git clone https://github.com/trailofbits/skills.git

The skills CLI installs just this skill, for any of its supported agents. Git is the plain clone.

Files (skills)
  • 3b-rust-compiler-analyzer.md 16 KB
    ---
    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
    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/`
    
    Section C of `{baseDir}/references/rust-zeroization-patterns.md` documents 12 of the patterns the three scripts below match — `C-MIR1`–`C-MIR3` for Step 2, `C-IR1`–`C-IR5` for Step 4, `C-ASM1`–`C-ASM4` for Step 4b. Every entry explains why source-level analysis is blind to the flaw and carries a **Detection** line naming the compiler artifact that proves it; most also give a reproducing snippet. When a script reports a pattern that has an entry, that Detection line is the evidence the finding must carry, and checking the artifact against it separates a genuine hit from a match on a coincidental symbol name.
    
    Section C is a subset, not an index. `check_mir_patterns.py` and `check_llvm_patterns.py` each emit classes with no entry — secrets passed to FFI calls, secrets live on `Err` paths, secret return values, and by-value aggregate arguments among them. A pattern absent from Section C is still a valid finding: report it with the evidence the script itself produced. Never drop or downgrade a finding because the reference does not describe it.
    
    Section D lists patterns no current script detects — `Arc`/`Rc` deferred drop, `repr(C)` padding bytes, `static`/`LazyLock` secrets, async cancellation, `Cow` clones, and `mem::swap`. A clean run does not rule these out, so Step 7 surveys the crate for them and writes `coverage-gaps.json`. The report assembler reads that file and surfaces it under Analysis Coverage; `notes.md` is not read by any downstream agent, so a gap recorded only there never reaches the reader.
    
    ### 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.
    
    ```bash
    {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`)
    
    ```bash
    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:
    
    ```json
    {
      "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.
    
    ```bash
    # O0 baseline (always):
    {baseDir}/tools/emit_rust_ir.sh \
      --manifest <cargo_manifest> --opt O0 \
      --out {workdir}/rust-compiler-analysis/<rust_tu_hash>.O0.ll
    
    # For each level in opt_levels (e.g. O2):
    {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:
    
    ```bash
    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:
    
    ```json
    {
      "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):
    
    ```bash
    {baseDir}/tools/emit_rust_asm.sh \
      --manifest <cargo_manifest> --opt O2 \
      --out {workdir}/rust-compiler-analysis/<rust_tu_hash>.O2.s
    ```
    
    Analyze:
    
    ```bash
    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`:
    ```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 — Section D Coverage Survey
    
    No script detects the Section D patterns, so survey the crate source directly and write
    `coverage-gaps.json`. Grep `<rust_crate_root>/src` for each marker, then keep only the hits
    that touch a type named in `sensitive-objects.json` or matching the config's sensitive
    patterns:
    
    | Pattern | Grep for |
    |---|---|
    | D1 `Arc`/`Rc` deferred drop | `Arc<`, `Rc<` |
    | D2 `repr(C)` padding | `#[repr(C)]`, `#[repr(C, packed)]` |
    | D3 `static`/`LazyLock` secrets | `static `, `LazyLock`, `OnceLock`, `lazy_static!` |
    | D4 async cancellation | `async fn`, `.await`, `select!` |
    | D5 `Cow` clones | `Cow<` |
    | D6 `mem::swap` | `mem::swap`, `mem::replace` |
    
    Write an array of `{"pattern": "<D id>", "where": "<file:line or symbol>", "why": "<what makes it unaudited>"}`, or `[]` when the survey found nothing. Always write the file — an absent file is indistinguishable from a survey that ran and found nothing, and the report says "no coverage gaps" either way.
    
    ### Step 7 — 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 |
    | `coverage-gaps.json` | Array of Section D patterns the crate uses that no script audits (empty array if none) |
    | `notes.md` | Steps executed, tool outputs, errors, relative paths to evidence files |
    
    ## Finding JSON Shape
    
    Every finding object includes:
    
    ```json
    {
      "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": ["O0", "O2"],
        "o0_volatile_stores": 32,
        "o2_volatile_stores": 0,
        "diff_summary": "All volatile stores eliminated at O2 — classic DSE pattern"
      },
      "related_objects": ["SO-5001"],
      "related_findings": ["F-RUST-SRC-0002"],
      "evidence_files": [
        "rust-compiler-analysis/<rust_tu_hash>.O0.ll",
        "rust-compiler-analysis/<rust_tu_hash>.O2.ll"
      ]
    }
    ```
    
    ## 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 `coverage-gaps.json`** — `[]` when the Step 6 survey found nothing. An absent file reads as full coverage in the report.
    - **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) |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related