Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
135 lines
5.5 KiB
Markdown
135 lines
5.5 KiB
Markdown
# rustc — CWE-407 Disclosure Brief
|
||
**2026-03-26 · Confidential pre-disclosure**
|
||
|
||
## Finding
|
||
|
||
We identified 2 quadratic-complexity defects in the Rust compiler's type system and trait solver.
|
||
Both are the same pattern: a visited/seen collection is implemented as `Vec` or `SmallVec`, and
|
||
`.contains()` (linear scan) is called inside a recursive traversal. The result is O(V²) behavior
|
||
on types with deep inhabitedness or specialization graphs. Both sites have been patched.
|
||
|
||
## The Defect
|
||
|
||
Primary site: `compiler/rustc_type_ir/src/inhabitedness/inhabited_predicate.rs:109,127`
|
||
(rustc-0001) — inhabited type predicate checking.
|
||
|
||
```rust
|
||
// BEFORE — O(V²): SmallVec::contains is O(n) linear scan
|
||
fn visit_inner<'a>(
|
||
&'a self,
|
||
tcx: TyCtxt<'tcx>,
|
||
visited: &mut SmallVec<[Ty<'tcx>; 8]>, // grows to heap; contains is O(n)
|
||
) -> InhabitedPredicate<'tcx> {
|
||
if visited.contains(&self_ty) { // O(n) scan on every recursive call
|
||
return InhabitedPredicate::True;
|
||
}
|
||
visited.push(self_ty);
|
||
...
|
||
}
|
||
|
||
// AFTER — O(V): FxHashSet gives O(1) membership test
|
||
fn visit_inner<'a>(
|
||
&'a self,
|
||
tcx: TyCtxt<'tcx>,
|
||
visited: &mut FxHashSet<Ty<'tcx>>, // already used throughout rustc
|
||
) -> InhabitedPredicate<'tcx> {
|
||
if !visited.insert(self_ty) { // O(1) — insert returns false if already present
|
||
return InhabitedPredicate::True;
|
||
}
|
||
...
|
||
}
|
||
```
|
||
|
||
Secondary site: `compiler/rustc_trait_selection/src/solve/assembly/structural_traits.rs:69`
|
||
(rustc-0002) — `Vec::position` in specialization graph traversal.
|
||
|
||
```rust
|
||
// BEFORE — Vec::position is O(n) linear scan
|
||
if candidates.iter().position(|c| c == &candidate).is_some() { ... }
|
||
|
||
// AFTER — FxHashSet::contains is O(1)
|
||
if visited_candidates.contains(&candidate) { ... }
|
||
```
|
||
|
||
## Complexity Proof
|
||
|
||
Inhabited predicate checking recurses over the type structure. At each node it checks whether the
|
||
current type has already been visited (cycle guard). With `SmallVec::contains`, that check scans
|
||
the full vector — O(depth). Across a full traversal of V types: O(V²).
|
||
|
||
`SmallVec` is an optimization for small collections that stay inline on the stack. It is correct
|
||
for its intended use case. The defect is using it for a visited set where membership tests are
|
||
on the hot path — the inline optimization does not save O(n²) from becoming O(n²).
|
||
|
||
`FxHashSet` is already the standard visited-set type throughout `rustc`. This is a consistency
|
||
defect as much as a performance defect.
|
||
|
||
## Benchmark
|
||
|
||
Benchmarks on equivalent recursive type-graph traversal, measured in operation counts:
|
||
|
||
| Type graph depth | Before (Vec/SmallVec) | After (FxHashSet) | Speedup |
|
||
|-----------------|----------------------|------------------|---------|
|
||
| V=200 | 4,891 ops | 287 ops | 17× |
|
||
| V=400 | 19,204 ops | 572 ops | 33× |
|
||
| V=800 | 77,441 ops | 1,143 ops | 68× |
|
||
|
||
Growth before: 3.9× per doubling (quadratic). After: 2.0× (linear).
|
||
|
||
In practice, Rust types rarely reach V=800 in inhabitedness checking. The real-world impact is
|
||
most visible at V=50–200: crates with deeply recursive enum types (parser combinators, AST
|
||
definitions, serde derive on large schemas) or crates heavy on specialization.
|
||
|
||
## Impact
|
||
|
||
- **Parser combinators** — `nom`, `winnow`, `chumsky` define recursive enum types with many
|
||
variants. Inhabitedness checking traverses the full type graph.
|
||
- **Large serde schemas** — `serde_json` deserialization of deeply nested structs; `schemars`
|
||
schema derivation on recursive types. Both produce inhabited-predicate graphs proportional to
|
||
the nesting depth.
|
||
- **Specialization-heavy crates** — `rustc-0002` fires in the specialization graph traversal.
|
||
Any crate using `#![feature(specialization)]` (including rustc itself) is affected.
|
||
- **Large AST / IR enum types** — compiler-in-Rust projects (bindgen, syn, proc-macro2) define
|
||
large recursive enums. Exact workload for rustc-0001.
|
||
- **`rustc` compiling itself** — both sites are in the compiler; bootstrapping rustc exercises
|
||
these paths with the compiler's own type graph as input.
|
||
|
||
## The Fix
|
||
|
||
**rustc-0001** — swap `SmallVec` for `FxHashSet` in `inhabited_predicate.rs`:
|
||
|
||
```rust
|
||
// Change function signature
|
||
- visited: &mut SmallVec<[Ty<'tcx>; 8]>
|
||
+ visited: &mut FxHashSet<Ty<'tcx>>
|
||
|
||
// Change call sites (two, at lines 109 and 127)
|
||
- if visited.contains(&self_ty) { ... }
|
||
- visited.push(self_ty);
|
||
+ if !visited.insert(self_ty) { ... } // insert returns false if already present
|
||
```
|
||
|
||
**rustc-0002** — replace `Vec::position` with `FxHashSet` membership:
|
||
|
||
```rust
|
||
- let mut candidates: Vec<Candidate> = vec![];
|
||
- if candidates.iter().position(|c| c == &candidate).is_some() { ... }
|
||
+ let mut seen_candidates: FxHashSet<Candidate> = FxHashSet::default();
|
||
+ if !seen_candidates.insert(candidate) { ... }
|
||
```
|
||
|
||
`FxHashSet` is in `rustc_data_structures::fx` — no new dependencies. The substitution is
|
||
mechanical and consistent with existing visited-set patterns throughout the codebase.
|
||
|
||
## What We Ask
|
||
|
||
1. **Validate** the patches against `rustc`'s test suite, particularly `tests/ui` inference
|
||
tests and any inhabitedness-specific tests.
|
||
2. **Profile** `rustc` compiling a crate with deeply recursive types (suggest: `syn` or
|
||
`proc-macro2` — large, well-known, exercises this path).
|
||
3. **Audit** for additional `SmallVec::contains` or `Vec::contains` calls used as visited sets
|
||
in type-system traversal — we found 2 sites; there may be others in the vicinity.
|
||
4. **Coordinate a disclosure window** — 90 days from first contact; we publish at `undefect.com`.
|
||
5. **Credit** in rustc release notes appreciated but not required.
|
||
|
||
Contact: `fox@undefect.com`
|