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::FunctionDecldirectly. 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 isHeuristicconfidence. Upgrading requires either threading Z3 path conditions through MIR (a larger lift) or wiringfold fuzz-prep/fold fuzz-runto compile Rust harnesses (a separate plan).
Build dependencies
- Rust nightly with the
rustc-devandllvm-tools-previewcomponents. Pinned viatools/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+spdlogalready 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):
$FOLD_RUST_BIN(preferred for installed/CI deployments)- A hardcoded dev path under the source tree
fold-rustresolved 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.rsormain.rsremoved or renamed —cargo buildfails with anE0599orE0432. - 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
- Pick a target nightly (e.g. the current
nightly). Confirm it shipsrustc-devfor your target arch withrustup component list --toolchain nightly | grep rustc-dev. - Update
tools/fold-rust/rust-toolchain.toml:[toolchain] channel = "nightly-YYYY-MM-DD" components = ["rustfmt", "rustc-dev", "llvm-tools-preview"] 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_stringinsummary.rsand theExprKindmatch inexpr_kind_tagare the most likely break points.
- Renamed methods → search rustc's
- Run
cargo build --releaseuntil green. - Run the full integration:
fold scan tests/integration/rust_unsafe_null.rsand a clone-pair fixture. Confirm finding counts unchanged. - Run
meson test -C build/release(the C++ wire-format tests are stable across rustc bumps; only fold-rust changes). - 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::discoverUnitsaccepts a directory with aCargo.tomland labels it"crate", butfold-rust extractcurrently treats each invocation as one standalone.rsfile. 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 aTthat 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.