307 lines
11 KiB
Markdown
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.
|
|
```
|