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.
5.5 KiB
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.
// 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.
// 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,chumskydefine recursive enum types with many variants. Inhabitedness checking traverses the full type graph. - Large serde schemas —
serde_jsondeserialization of deeply nested structs;schemarsschema derivation on recursive types. Both produce inhabited-predicate graphs proportional to the nesting depth. - Specialization-heavy crates —
rustc-0002fires in the specialization graph traversal. Any crate using# is affected. - Large AST / IR enum types — compiler-in-Rust projects (bindgen, syn, proc-macro2) define large recursive enums. Exact workload for rustc-0001.
rustccompiling 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:
// 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:
- 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
- Validate the patches against
rustc's test suite, particularlytests/uiinference tests and any inhabitedness-specific tests. - Profile
rustccompiling a crate with deeply recursive types (suggest:synorproc-macro2— large, well-known, exercises this path). - Audit for additional
SmallVec::containsorVec::containscalls used as visited sets in type-system traversal — we found 2 sites; there may be others in the vicinity. - Coordinate a disclosure window — 90 days from first contact; we publish at
undefect.com. - Credit in rustc release notes appreciated but not required.
Contact: fox@undefect.com