5.6 KiB
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 | 553–587 |
| 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).
// 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 157–209) 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.
// 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
// 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
expand1is called fromCalcMethods(subr.go:111) and transitively fromimplements(subr.go:680),typecheck.go:789, andreflectdata/reflect.go:84.CalcMethodsis invoked during interface satisfaction checks — a hot path in any Go program with interfaces.- The
Sym.Uniq()dedup inexpand0prevents duplicate slist entries (output is correct) but does not prevent exponential re-traversal (cost is wrong). - The
go/typespackage (types2/typeset.go) correctly uses memoization viaityp.tset != niland does NOT have this defect; onlycmd/compile/internal/typecheck(the compiler-internal frontend) is affected.