java-topology/defects/go/patch/go-0002-expand1-diamond-recur-flag.md

5.6 KiB
Raw Blame History

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 CalcMethodsexpand1 → 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 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.

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

  • 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.