java-topology/defects/meson/patch/meson-0002-link-whole-diamond-recursion.md
russell@unturf.com 3986d8dc50 diamond hunt: godot-0009/0010 + meson-0002 + typeorm-0004/0005 + ts-0003; count 629→635
New diamond recursion defects (O(2^D) → O(N)):
- godot-0009: Font::_is_cyclic no visited set — CJK fallback diamond, 2648x at F=4,D=8
- godot-0010: Font::_update_rids_fb no visited set — duplicate RIDs + O(N^2) hot path
- meson-0002: get_internal_static_libraries_recurse link_whole guard missing — 132x at D=10
- typescript-0003: hasBaseType inner check() no visited set — 1024x at D=10; hot on instanceof

New O(N²) defects:
- typeorm-0004: SubjectTopologicalSorter Array.indexOf dedup — 200x at N=400
- typeorm-0005: DepGraph.createDFS result.indexOf + addDependency edge dedup — 300x at N=600

CLEAN confirmed (diamond recursion sweep): bazel, cargo, cmake, composer, dgl, diesel,
doctrine-orm, efcore, helm, mybatis, networkx-deeper, ninja, npm-arborist, peewee, pip,
rubygems, seaorm, sqlalchemy, swift

UNDF: 571→578 assigned; MOAD count: 629→635
2026-03-29 16:52:04 -04:00

2.8 KiB
Raw Blame History

UNDF: UNDF-2026-000000412

Classification

Field Value
CWE CWE-407 Inefficient Algorithmic Complexity
Severity MEDIUM
Component mesonbuild/build.py:1625
Function BuildTarget.get_internal_static_libraries_recurse
Hot path Static library link closure — called during build graph evaluation for every target with link_whole dependencies
Status PATCHED (unit test PASS)

Defect

get_internal_static_libraries_recurse uses result (an OrderedSet) as a visited guard for link_targets but omits the guard for link_whole_targets. The link_whole_targets branch recurses unconditionally:

# mesonbuild/build.py:1625
def get_internal_static_libraries_recurse(self, result: OrderedSet[StaticTargetTypes]) -> None:
    for t in self.link_targets:
        if t.is_internal() and t not in result:   # ← guard present
            result.add(t)
            t.get_internal_static_libraries_recurse(result)
    for t in self.link_whole_targets:
        if t.is_internal():
            t.get_internal_static_libraries_recurse(result)   # ← NO guard!

On a diamond link_whole graph — A link_whole B, C; B link_whole D; C link_whole D; D link_whole E — the recursion visits D twice and E twice. At depth D the total node visits is O(2^D).

Diamond trace:

A.recurse(result={}):
  B.recurse(result={}):          # link_whole B
    D.recurse(result={}):        #   link_whole D
      E.recurse(result={}): …    #     → result = {E}
    → result = {E}
  C.recurse(result={E}):         # link_whole C
    D.recurse(result={E}):       #   link_whole D — AGAIN (no guard!)
      E.recurse(result={E}):     #     → visits E again, returns (already in result… but
                                 #       link_whole branch doesn't check!)

In practice link_whole chains in C++ projects are typically shallow (13 levels), giving 28 redundant traversals. Generated build systems (particularly those producing many static stub libraries) can reach D=57 (32128 redundant traversals per target).

Fix

Add the same guard used for link_targets to the link_whole_targets branch:

def get_internal_static_libraries_recurse(self, result: OrderedSet[StaticTargetTypes]) -> None:
    for t in self.link_targets:
        if t.is_internal() and t not in result:
            result.add(t)
            t.get_internal_static_libraries_recurse(result)
    for t in self.link_whole_targets:
        if t.is_internal() and t not in result:   # CWE-407 fix: add guard
            result.add(t)
            t.get_internal_static_libraries_recurse(result)

Complexity: O(2^D) → O(N) where N = total number of internal static libraries in the link closure.

Speedup at D=7: ~128x (128 visits → 1 visit per shared node).