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

307 lines
11 KiB
Markdown

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