fold/docs/internal/rust-frontend.md
2026-05-27 10:36:57 -04:00

11 KiB

Rust frontend (fold-rust)

A second source-language frontend for fold. Scans Rust source for the subset of issues that meaningfully exist there: raw-pointer dereferences in unsafe blocks, and within-language clone clusters.

The C/C++ scan path is unchanged. This doc covers what's specific to the Rust path: build dependencies, the subprocess + JSON wire contract, the nightly toolchain pin, and the bump procedure.

Scope

What ships today (the MVP):

Capability Where it lives
Raw-pointer dereference detection tools/fold-rust/src/main.rs (HIR DerefHunter)
Within-Rust clone clusters (Type-1/2) tools/fold-rust/src/summary.rs (FunctionSummary hashes) → C++ detectClones()
File-extension auto-detection src/cli/scan_router.cpp::autoDetectRust

What's deliberately out of scope:

  • Equiv (Type-4) and class-dup detectors — they invoke the encoder against clang::FunctionDecl directly. Making the encoder frontend-agnostic is a larger refactor than this frontend covers.
  • Dead-statement on Rust — needs a Rust CFG builder.
  • Cross-language (Rust ↔ C++) clone clustering — Rust uses its own kind-tag taxonomy; the per-Kind discriminators don't overlap clang's so hashes never collide across languages by construction. A future cross-language taxonomy would unblock this.
  • Lifetimes, traits, generics, match-exhaustiveness analysis — those are rust-analyzer / clippy territory; fold's value-add is in the unsafe layer.
  • Promoted findings (Proved, ConcreteWitness) — every Rust finding today is Heuristic confidence. Upgrading requires either threading Z3 path conditions through MIR (a larger lift) or wiring fold fuzz-prep / fold fuzz-run to compile Rust harnesses (a separate plan).

Build dependencies

  • Rust nightly with the rustc-dev and llvm-tools-preview components. Pinned via tools/fold-rust/rust-toolchain.toml. Install once with:
    rustup toolchain install nightly --profile minimal \
        --component rustc-dev,llvm-tools-preview
    
  • No new C++ deps. RustBackend uses the existing nlohmann_json + spdlog already linked into fold-core.

Build the frontend separately from fold's meson build:

cd tools/fold-rust && cargo build --release

The resulting binary lands at tools/fold-rust/target/release/fold-rust. RustBackend discovers it via (in order):

  1. $FOLD_RUST_BIN (preferred for installed/CI deployments)
  2. A hardcoded dev path under the source tree
  3. fold-rust resolved via $PATH

Architecture: subprocess + JSON

fold scan --mode=auto foo.rs          # or --mode=rust for explicit
  │
  └─ scan_router::resolveMode  → "rust"  (extension sniff)
       │
       └─ runRust → runDetectorsOnShard(backend=RustBackend)
             │
             └─ RustBackend::extractFunctions(unit)
                   │
                   └─ posix_spawnp("fold-rust", ["extract", "--unit", unit])
                         │
                         └─ fold-rust:
                             ├─ rustc_driver::run_compiler(...)
                             ├─ HIR walk over crate items
                             ├─ summary::compute_summary(def_id)
                             ├─ DerefHunter::visit_expr(...)
                             └─ NDJSON to stdout, one line per record
                   │
                   └─ Parse NDJSON → FunctionIR + FunctionSummary + Finding
             │
             └─ Polymorphic dispatch:
                  - findings  → ShardResult.findings
                  - summaries → ShardResult.summaries → detectClones()
                  - functions → (unused at MVP)

The subprocess model matches fold's existing worker fanout (src/cli/subprocess_fanout.cpp) so the wire format and process lifecycle stay consistent across languages.

Wire format

fold-rust extract --unit <path> writes NDJSON to stdout. Each line is a self-contained JSON object with two top-level keys: kind and data. Unknown kinds are logged at debug and skipped; future additions are additive.

kind: "info"

{
  "kind": "info",
  "data": {
    "message": "fold-rust RF-3b — ...",
    "version": "0.1.0",
    "unit": "/abs/path/to/unit.rs"
  }
}

Always emitted exactly once, first. Logged at debug. Not surfaced to the user.

kind: "function"

{
  "kind": "function",
  "data": {
    "qualified_name": "crate::module::foo",
    "source_file": "/abs/path/to/unit.rs",
    "start_line": 12,
    "end_line": 25,
    "callees": []
  }
}

One per fn item (free function or impl method). Maps to fold::core::FunctionIR. callees is populated as a future extension; today it's always empty.

kind: "summary"

{
  "kind": "summary",
  "data": {
    "qualified_name": "crate::process_alpha",
    "file": "/abs/path/to/unit.rs",
    "line": 1,
    "column": 1,
    "body_stmt_count": 5,
    "is_method": false,
    "parent_class": "",
    "hash_exact": 5303988442150213244,
    "hash_rename_insensitive": 2371971998466145176,
    "hash_literal_insensitive": 15793788496441557416,
    "body_rename": "{...}",
    "body_literal": "{...}"
  }
}

One per function. The three hashes feed fold::detectClones(). They use the same FNV-1a + mix64 recipe as the clang frontend (src/ir/normalize.cpp), but with Rust-local kind tags — Rust hashes never collide with C++ hashes by construction. The token strings (body_rename, body_literal) support Type-3 (edit-distance) clustering when --clone-type=3.

kind: "finding"

{
  "kind": "finding",
  "data": {
    "finding_kind": "null-deref",
    "confidence": "Heuristic",
    "function": "read_raw",
    "source_file": "/abs/path/to/unit.rs",
    "line": 2,
    "column": 5,
    "hazard": "rust-raw-pointer-read",
    "message": "raw pointer dereferenced (*const i32); verify provenance"
  }
}

One per detected hazard. finding_kind maps to fold::FindingKind via parseFindingKind() in src/frontend/rust_backend.cpp; the allowlist:

  • null-deref, int-overflow, int-truncation, sign-conversion, div-by-zero, oob-access, taint-track

Unknown kinds default to NullDeref and are logged. confidence parses via parseConfidence(): ConcreteWitness | Proved | Heuristic | Syntactic.

hazard is optional and refines the stable public rule without adding new SARIF IDs. Current values are rust-raw-pointer-read, rust-raw-pointer-write, rust-raw-pointer-null-read, and rust-raw-pointer-null-write. RustBackend copies this value into FindingEvidence::correlation_kind, which SARIF exposes as properties.correlationKind.

rustc-private pin

tools/fold-rust/rust-toolchain.toml pins a specific nightly. The plugin-style extern crate rustc_* interface is never stable; bumping the pin is an explicit action, not a side-effect of cargo update.

When to bump

  • A rustc internal API used by summary.rs or main.rs removed or renamed — cargo build fails with an E0599 or E0432.
  • The minimum supported Rust version of one of the crates we depend on (clap, serde) advances past our pinned nightly.
  • Schedule: revisit every ~3-6 months even when nothing's broken, so the pin doesn't drift too far behind upstream.

How to bump

  1. Pick a target nightly (e.g. the current nightly). Confirm it ships rustc-dev for your target arch with rustup component list --toolchain nightly | grep rustc-dev.
  2. Update tools/fold-rust/rust-toolchain.toml:
    [toolchain]
    channel = "nightly-YYYY-MM-DD"
    components = ["rustfmt", "rustc-dev", "llvm-tools-preview"]
    
  3. cd tools/fold-rust && cargo build --release. Triage build errors:
    • Renamed methods → search rustc's library//compiler/ git history for the rename commit.
    • Removed enum variants → check rustc release notes; usually there's a replacement pattern.
    • path_leaf_string in summary.rs and the ExprKind match in expr_kind_tag are the most likely break points.
  4. Run cargo build --release until green.
  5. Run the full integration: fold scan tests/integration/rust_unsafe_null.rs and a clone-pair fixture. Confirm finding counts unchanged.
  6. Run meson test -C build/release (the C++ wire-format tests are stable across rustc bumps; only fold-rust changes).
  7. Commit rust-toolchain.toml + any source patches together with a commit message that names the upstream rustc commit / PR responsible for the API change.

Why nightly (and not stable)

The rustc internals (rustc_driver, rustc_hir, rustc_middle, etc.) require #![feature(rustc_private)], which only nightly accepts. Stable Rust does not expose a programmable compiler interface — proc macros aren't expressive enough for HIR-level analysis, and syn is a parser, not a type-checker.

Tools in the same boat: clippy, miri, rust-analyzer (for some crates), kani, prusti.

Known limits

  • Per-file unit only. RustBackend::discoverUnits accepts a directory with a Cargo.toml and labels it "crate", but fold-rust extract currently treats each invocation as one standalone .rs file. Multi-file crates would need a Cargo workspace driver — defer to a later phase.
  • Generic functions walked once. HIR analysis runs at the definition site, before monomorphization. Type-dependent behavior (e.g. is_raw_ptr() on a T that resolves to *const u8) won't fire. Move to MIR + monomorphization context to fix; out of scope at MVP.
  • HIR walker doesn't see macro-expanded code clearly. Spans point at the call site rather than the inside-the-macro expression. A raw-pointer deref produced by macro expansion will be flagged at the macro invocation line.
  • No incremental support. Each invocation re-parses the input unit. fold's caching layer (--incremental) is clang-specific.

Verification

End-to-end smoke from the project root:

# Raw-pointer deref detection
cat > /tmp/unsafe_deref.rs <<'EOF'
pub unsafe fn read_raw(p: *const i32) -> i32 { *p }
pub fn safe_read(r: &i32) -> i32 { *r }
EOF
./build/release/src/cli/fold scan /tmp/unsafe_deref.rs --format json --output -
# Expect: 1 finding (kind=NullDeref, confidence=Heuristic) on `read_raw`;
# `safe_read` correctly skipped.

# Clone detection (5+ stmts per function clears the min_body_stmts filter)
cat > /tmp/clone_pair.rs <<'EOF'
pub fn process_alpha(items: &[i32]) -> i32 {
    let mut total = 0; let scale = 2; let offset = 1;
    for v in items { total += v * scale + offset; }
    total
}
pub fn process_beta(things: &[i32]) -> i32 {
    let mut sum = 0; let factor = 2; let bias = 1;
    for x in things { sum += x * factor + bias; }
    sum
}
EOF
./build/release/src/cli/fold scan /tmp/clone_pair.rs --format json --output -
# Expect: 1 finding (kind=Clone, rename-insensitive) clustering both fns.