# 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 ` 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"` ```json { "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"` ```json { "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"` ```json { "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"` ```json { "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`: ```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. ```