java-topology/whitepaper/outreach/rustc.md
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
2026-03-26 17:11:57 -04:00

5.5 KiB
Raw Permalink Blame History

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=50200: crates with deeply recursive enum types (parser combinators, AST definitions, serde derive on large schemas) or crates heavy on specialization.

Impact

  • Parser combinatorsnom, winnow, chumsky define recursive enum types with many variants. Inhabitedness checking traverses the full type graph.
  • Large serde schemasserde_json deserialization of deeply nested structs; schemars schema derivation on recursive types. Both produce inhabited-predicate graphs proportional to the nesting depth.
  • Specialization-heavy cratesrustc-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:

// 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

  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