go-0002: cmd/compile expand1 O(2^D) diamond struct embedding; rustc diamond CLEAN; count 622→623

This commit is contained in:
russell@unturf.com 2026-03-29 18:07:11 -04:00
parent 77242c9120
commit 6220414bb9
2 changed files with 220 additions and 0 deletions

View file

@ -0,0 +1,173 @@
# UNDF: UNDF-2026-000000562
# go-0002: cmd/compile/internal/typecheck expand1 O(2^D) diamond struct embedding
## CWE-407 — Algorithmic Complexity: O(2^D) Exponential Blowup on Diamond-Shaped Struct Embeddings
| Field | Value |
|-------|-------|
| ID | go-0002 |
| Severity | HIGH |
| Ecosystem | go |
| Package | `cmd/compile/internal/typecheck` |
| File | `src/cmd/compile/internal/typecheck/subr.go` |
| Lines | 553587 |
| Complexity | O(2^D) where D = diamond embedding depth |
| Hot path | `CalcMethods``expand1` → recursive embedded struct walk, called during interface satisfaction checks |
## Defect
`expand1` uses a single boolean `Recur` flag on the type struct to prevent infinite
recursion in cyclic types. The flag is set on entry and **cleared on return**. This
guards against cycles but does NOT prevent re-visiting a shared node reached via
multiple paths (diamond-shaped embedding).
```go
// src/cmd/compile/internal/typecheck/subr.go:553-587
func expand1(t *types.Type, top bool) {
if t.Recur() {
return // only guards against stack cycles, NOT diamond revisits
}
t.SetRecur(true)
if !top {
expand0(t) // appends t's methods to the global slist
}
u := t
if u.IsPtr() { u = u.Elem() }
if u.IsStruct() || u.IsInterface() {
// ...
for _, f := range fields {
if f.Embedded == 0 { continue }
if f.Sym == nil { continue }
expand1(f.Type, false) // recurse into each embed
}
}
t.SetRecur(false) // DEFECT: reset allows re-visiting on diamond paths
}
```
### Diamond trace (D = 2)
```
type D struct { method M() }
type B struct { D } // B embeds D
type C struct { D } // C embeds D
type A struct { B; C } // A embeds both B and C → diamond
```
Call tree for `expand1(A, true)`:
```
expand1(A)
expand1(B)
expand1(D) ← visit 1; D.Recur set true then FALSE on return
expand1(C)
expand1(D) ← visit 2: D.Recur is false again → re-entered!
```
For depth D, `expand1` is called `2^(D+1) - 1` times total:
- D=1: 3 calls
- D=10: 2 047 calls
- D=20: 2 097 151 calls
- D=30: 2 147 483 647 calls (hangs compiler)
Note: the method *output* (slist entries) remains correct because `expand0` uses
`f.Sym.SetUniq(true)` to dedup entries. But the **traversal cost** is exponential.
`adddot1` (lines 157209) has the same Recur-flag pattern and is called per symbol
by `dotpath`, compounding the work further.
## Fix
Replace the per-call `Recur` flag with a `map[*types.Type]bool` visited set passed
through the call chain. This converts O(2^D) → O(E) where E = total embedding edges.
```go
// CalcMethods — pass visited set into expand1
func CalcMethods(t *types.Type) {
if t == nil || len(t.AllMethods()) != 0 {
return
}
for _, f := range t.Methods() {
f.Sym.SetUniq(true)
}
slist = slist[:0]
visited := make(map[*types.Type]bool) // NEW: true visited set
expand1(t, true, visited)
// ... rest unchanged
}
// expand1 — accept visited set, skip already-expanded types
func expand1(t *types.Type, top bool, visited map[*types.Type]bool) {
if visited[t] {
return
}
visited[t] = true
if !top {
expand0(t)
}
u := t
if u.IsPtr() { u = u.Elem() }
if u.IsStruct() || u.IsInterface() {
var fields []*types.Field
if u.IsStruct() {
fields = u.Fields()
} else {
fields = u.AllMethods()
}
for _, f := range fields {
if f.Embedded == 0 { continue }
if f.Sym == nil { continue }
expand1(f.Type, false, visited) // pass visited set down
}
}
// No reset — visited[t] stays true permanently
}
```
The `types.Type.Recur()` / `types.Type.SetRecur()` calls in `expand1` can be
removed entirely once the visited map is in place. The flag is still needed by
`adddot1`, which requires a separate fix with a per-call visited set passed
alongside `s`, `t`, `d`.
## Complexity
| Embedding depth D | Calls before (O(2^D)) | Calls after (O(E)) | Speedup |
|-------------------|-----------------------|--------------------|---------|
| 5 | 63 | 11 | 5.7× |
| 10 | 2 047 | 21 | 97.5× |
| 15 | 65 535 | 31 | 2 114× |
| 20 | 2 097 151 | 41 | 51 150× |
| 30 | 2 147 483 647 | 61 | 35 M× |
(After: O(E) = depth × 2 edges per diamond level + 1 leaf = 2D+1 nodes visited)
## Triggering scenario
```go
// A deeply-nested diamond embedding causes exponential compile-time blowup
// when any interface method set is computed (e.g. var _ SomeInterface = A{})
type L0 struct{ M0() }
type L1 struct{ L0; L0 }
type L2 struct{ L1; L1 }
// ...
type L20 struct{ L19; L19 } // triggers > 2M expand1 calls
```
## Evidence
- `expand1` is called from `CalcMethods` (`subr.go:111`) and transitively from
`implements` (`subr.go:680`), `typecheck.go:789`, and `reflectdata/reflect.go:84`.
- `CalcMethods` is invoked during interface satisfaction checks — a hot path in
any Go program with interfaces.
- The `Sym.Uniq()` dedup in `expand0` prevents duplicate slist entries (output is
correct) but does not prevent exponential re-traversal (cost is wrong).
- The `go/types` package (`types2/typeset.go`) correctly uses memoization via
`ityp.tset != nil` and does NOT have this defect; only `cmd/compile/internal/typecheck`
(the compiler-internal frontend) is affected.

View file

@ -0,0 +1,47 @@
# rustc diamond-recursion scan — CLEAN
**Scan date:** 2026-03-29
**Pattern:** Recursive DAG traversal without visited set (CWE-407 diamond recursion, O(2^D))
**Files scanned:**
- `compiler/rustc_type_ir/src/elaborate.rs``Elaborator`, `supertrait_def_ids`
- `compiler/rustc_type_ir/src/walk.rs``TypeWalker`
- `compiler/rustc_trait_selection/src/traits/util.rs``expand_trait_aliases`
- `compiler/rustc_trait_selection/src/traits/select/mod.rs``evaluate_predicate_recursively`
- `compiler/rustc_hir_analysis/src/collect/predicates_of.rs``implied_predicates_with_filter`
- `compiler/rustc_hir_analysis/src/hir_ty_lowering/bounds.rs``collect_bounds`
## Findings
### `elaborate.rs` — CLEAN
`Elaborator` uses `visited: HashSet<ty::Binder<I, ty::PredicateKind<I>>>` (line 25).
`supertrait_def_ids` uses `set: HashSet` with `set.insert(data.def_id())` guard (line 328).
Both correctly prevent diamond re-traversal.
### `walk.rs` — CLEAN
`TypeWalker` uses `visited: SsoHashSet<I::GenericArg>` with `self.visited.insert(next)`
guard (line 59). Explicitly documented: "walker only visits each type once."
### `expand_trait_aliases` — CLEAN
Uses BFS `VecDeque` without a visited set on trait aliases, but the re-queuing
is gated by `tcx.is_trait_alias()` only. Normal (non-alias) traits are pushed
directly to `trait_preds` output without re-queuing. Diamond blowup not possible
because `tcx.explicit_super_predicates_of` is a memoized query.
### `select/mod.rs` — CLEAN
`evaluate_predicate_recursively` calls `check_candidate_cache` / `insert_candidate_cache`
— a proper memoization cache keyed on trait predicates. No diamond re-traversal.
### `predicates_of.rs` — CLEAN
All supertrait queries go through `tcx.at(span).explicit_super_predicates_of(bound.def_id())`
which is a memoized `TyCtxt` query. Results are cached per `DefId`. No diamond blowup.
### `hir_ty_lowering/bounds.rs` — CLEAN
`collect_bounds` iterates directly over HIR bounds, no recursion into supertraits.
## Conclusion
No diamond recursion CWE-407 defects in rustc. All supertrait and type traversal
uses either a proper `HashSet`/`SsoHashSet` visited set, or memoized `TyCtxt`
query results that cache per `DefId`. The O(2^D) diamond blowup pattern is not
present in the scanned rustc crates.