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.
15 KiB
Executive Summary
CWE-407: The Sedimentary Defect Internal draft — not for distribution Updated: 2026-03-26
What We Found
A single structural error — using a list where a set belongs — is present in 50+ confirmed
sites across 25+ software ecosystems. Every affected system maintains a visited or
onStack collection during graph traversal. In every defective site, that collection is
implemented as a list. Membership is tested by linear scan. The result is O(n²) or worse
behavior in code that should run in O(n).
The defect is not exotic. It does not require obscure inputs or adversarial conditions. It
activates on every compilation of a large Java program, every TypeScript type-check of a
large codebase, every pip install of a project with a deep dependency graph, every
MongoDB query plan enumeration on a collection with many indexes, every OSPF topology
change on a network with hundreds of nodes.
42 sites patched. 3 deferred. 1 fixable-upstream. 1 fixable-pending. 2 not-worth-fixing. Several unpatched and disclosed.
Why It Persists
This class of defect fossilizes because:
-
The code is correct. A list and a set both answer the membership question correctly. The only difference is cost. Correctness tests pass. No crash, no wrong answer.
-
It was written before O(1) membership was idiomatic. The affected code was authored in the 1990s–2000s when
ArrayList,list, orstd::vectorwas the default container and hash sets were an explicit opt-in. The idiom calcified. -
It propagates by copy-paste. The same algorithm, the same variable names, the same data structure appear across GHC, GCC, Erlang, Maven, MongoDB, and Python's pip — written by different teams, in different languages, in different decades. Each team copied from the same algorithm literature and made the same choice.
-
It degrades at scale, which is rare. Most graphs encountered in practice are small. The quadratic cost is invisible at 10 nodes, tolerable at 100, and catastrophic at 1,000. Developers working on typical inputs never see the problem. Developers working at scale attribute the slowness to "large project overhead."
The Defect Map
CRITICAL — O(n³)
| ID | Tool | Location | Status |
|---|---|---|---|
| scala3-0001 | Scala 3 compiler | OrderingConstraint.scala:248 — List[TypeParamRef].contains in nested constraint lattice |
PATCHED |
HIGH — Hot path, every compilation or planning pass
| ID | Tool | Location | Status |
|---|---|---|---|
| javac-0001 | OpenJDK javac | GraphUtils.java:186 — Tarjan stack.contains(n) |
PATCHED |
| javac-0002a | OpenJDK javac | Infer.java:1850 — ArrayList.findNode linear scan |
PATCHED |
| javac-0002b | OpenJDK javac | Infer.java:1747 — uncached closure DFS |
PATCHED |
| javac-0004 | OpenJDK javac | Dependencies.java:197 — List.contains+add |
PATCHED |
| javac-0005 | OpenJDK javac | InferenceContext.java:506 — List.containsAll() |
PATCHED |
| ts-0001 | TypeScript | checker.ts:11503 — resolutionTargets[] linear scan |
PATCHED |
| ts-0002 | TypeScript | checker.ts:5256 — visitedSymbols array |
PATCHED |
| ts-0003 | TypeScript | checker.ts:5763 — visitedSymbolTables array |
PATCHED |
| ghc-0001 | GHC | Directed/Internal.hs:78 — v `elem` SCC decode |
PATCHED |
| ghc-0002 | GHC | Inductive/Graph.hs:489 — elem × 4 codegen |
PATCHED |
| ghc-0003 | GHC | Graph/Ops.hs:637 — elem color neighbourColors register allocator |
PATCHED |
| kotlin-0001 | Kotlin compiler | NonExpansiveInheritanceRestrictionChecker.kt:150 — in List post-DFS |
PATCHED |
| llvm-0001 | LLVM | GlobalsModRef.cpp:570 — is_contained(vector<CGN*>) LTO |
PATCHED |
| rustc-0001 | rustc | inhabited_predicate.rs:109,127 — SmallVec::contains |
PATCHED |
| erlang-0001 | Erlang OTP | digraph.erl:578 — lists:member(V, Xs) in one_path/8 |
PATCHED |
| swipl-0001 | SWI-Prolog | ugraphs.pl:510 — graph_memberchk O(|V|) scan per zero-vertex in Kahn's topo-sort |
PATCHED |
| swipl-0002 | SWI-Prolog | aggregate.pl:673 — list_is_free_of O(N²) accumulator in free_variables/4 |
PATCHED |
| mongodb-0001 | MongoDB | planner_ixselect.cpp — std::find on RelevantTag::first/notFirst vectors × 4 sites |
PATCHED |
| mongodb-0002 | MongoDB | plan_enumerator.cpp — predicate dedup list scan × 4 sites |
PATCHED |
| mongodb-0003 | MongoDB | plan_enumerator.cpp — stage tracking list |
PATCHED |
| mongodb-0004 | MongoDB | plan_enumerator.cpp — field index resolution list |
PATCHED |
| frrouting-0001 | FRRouting | ospf_ti_lfa.c:72,114,227,278,285 — listnode_lookup × 5 in TI-LFA |
PATCHED |
| frrouting-0002 | FRRouting | ospf_spf.c:275 — listnode_lookup(parent->children, v) in Dijkstra main loop |
Unpatched |
| solc-0001 | Solidity compiler | libyul/optimiser/CallGraphGenerator.cpp:49 — std::find(currentPath) in Yul cycle detect |
Unpatched |
| postgresql-0001 | PostgreSQL | tlist.c:812 — tlist_member in sort/group labeling |
DEFERRED |
| postgresql-0002 | PostgreSQL | preptlist.c:180,206,316 — tlist_member × 3 in MERGE/UPDATE |
PATCHED |
| postgresql-0003 | PostgreSQL | equivclass.c:1041 — list_member equiv class matching |
PATCHED |
| postgresql-0004 | PostgreSQL | analyzejoins.c:1914 — list_member join elimination |
PATCHED |
MEDIUM — Real defect, bounded or cold path
| ID | Tool | Location | Status |
|---|---|---|---|
| javac-0003 | OpenJDK javac | ModuleHashesBuilder — Deque.contains() |
PATCHED |
| ghc-0004 | GHC | Tc/TyCl/Utils.hs:973 — elem constructor list |
PATCHED |
| gcc-0001 | GCC | gcov.cc:980 — find(vector.begin,end,w) Johnson's |
PATCHED |
| rustc-0002 | rustc | specialization_graph.rs:69 — Vec::position |
PATCHED |
| cpython-0001 | CPython | sccutils.py:73 — node in path list |
PATCHED |
| distlib-0001 | distlib / pip | util.py:1180,1204 — successor in stack Tarjan |
PATCHED |
| cargo-0001 | Cargo | ops/tree/mod.rs:343 — Vec::contains (display only) |
PATCHED |
| gyp-0001 | GYP | input.py:1604 — child in path list + .index() |
PATCHED |
| npm-0002 | npm arborist | can-place-dep.js:370 — peerPath.includes() |
PATCHED |
| linux-0001 | Linux kernel | headerdep.pl:153 — grep {} @$top cycle detect |
PATCHED |
| sqlite-0001 | SQLite | trigger.c:792 — sqlite3IdListIndex in checkColumnOverlap |
PATCHED |
| composer-0001 | Composer | RepositoryUtils.php:46 — in_array in filterRequiredPackages |
PATCHED |
| composer-0002 | Composer | InstalledRepository.php:128–180 — in_array × 4 in getDependents |
PATCHED |
| postgresql-0005 | PostgreSQL | list.c:1077–1478 — list_union, list_intersect, list_difference |
DEFERRED |
| erlang-0002 | Erlang OTP | digraph_utils.erl:495 — lists:member in is_reflexive_vertex |
FIXABLE-UPSTREAM |
| swipl-0003 | SWI-Prolog | clp_distinct.pl:173-174 — lists_contain in attr_unify_hook |
FIXABLE-PENDING |
| tor-0001 | Tor | routerlist.c:2179 — smartlist_contains_string(requested_fingerprints, fp) O(R²) |
Unpatched |
| solc-0002 | Solidity compiler | libevmasm/Assembly.cpp:1077 — std::find(items) EOF jump resolution |
Unpatched |
| mongodb-0005 | MongoDB | ce_cache.h:122 — IndexBounds structural equality, no hash available |
DEFERRED |
| create-0001 | Create mod | TrackGraph.findDisconnectedGraphs — ArrayList.remove(0) O(n) shift in BFS |
Unpatched |
LOW — Principle violations, bounded input
| ID | Tool | Location | Status |
|---|---|---|---|
| maven-0001 | Maven | project/Graph.java:63 — ArrayList.remove() in removeEdge |
PATCHED |
| maven-0002 | Maven | internal/impl/Graph.java:63 — duplicate of maven-0001 |
PATCHED |
| maven-0003 | Maven | project/Graph.java:102 — LinkedList.lastIndexOf in cycle reporter |
PATCHED |
| cmake-0001 | CMake | cmComputeLinkDepends.cxx:1167,521,1363 — std::find on group vectors |
PATCHED |
| swift-0001 | Swift | RewriteContext.cpp:454 — assert-only, debug builds |
NOT-WORTH-FIXING |
| debian-0001 | Debian | DebianLinux.pm:140 — config parse, 6-item list |
NOT-WORTH-FIXING |
| mongodb-0006 | MongoDB | projection_ast.h — removeChild std::find; vector::erase shifts regardless |
NOT-WORTH-FIXING |
| mongodb-0007 | MongoDB | join_graph.cpp — InlinedVector<JoinPredicate, 2>, A≈1 at runtime |
NOT-WORTH-FIXING |
| minecraft-0002 | Minecraft | PistonStructureResolver — List<BlockPos>.contains(), bounded at 12 |
Unpatched |
EXPONENTIAL — Recursive DFS without visited tracking
| ID | Tool | Location | Status |
|---|---|---|---|
| minecraft-0001 | Minecraft server | DependencySorter.isCyclic — recursive DFS, no visited set; O(E^D) on diamond chains |
Unpatched |
42 sites patched. 3 deferred (postgresql-0001/-0005, mongodb-0005). 1 fixable-upstream (Erlang OTP). 1 fixable-pending (swipl-0003). 4 not-worth-fixing. Several unpatched and disclosed (frrouting-0002, tor-0001, solc-0001/-0002, minecraft-0001/-0002).
The PostgreSQL Problem
PostgreSQL has five confirmed defects in the query planner. Three have been patched using
a Bitmapset approach (no nodeHash() required — these sites operate only on Var nodes
whose varno+varattno+varlevelsup triple is a natural integer key). Two remain
deferred: tlist.c:812 and the structural variants of list_union/intersect/difference
operate on general expression trees and require a nodeHash() function that does not yet
exist in PostgreSQL core.
The path forward: contribute nodeHash() to PostgreSQL core (recursive expression hash
mirroring equal()), then patch the two deferred sites. Alternatively, patch them with
Bitmapset for the Var-only subset and leave non-Var expressions on the slow path.
The MongoDB Index Planner Root Cause
Four confirmed CWE-407 sites in MongoDB's index query planner trace to a single struct in
index_tag.h:106-107:
// Before — O(n²): vector scan on every stripInvalid pass
std::vector<size_t> first;
std::vector<size_t> notFirst;
// After — O(1): hash set membership
std::unordered_set<size_t> first;
std::unordered_set<size_t> notFirst;
This single two-line change fixes all four planner_ixselect.cpp sites simultaneously.
The plan_enumerator.cpp predicate deduplication (4 additional sites) has the same root
cause applied to predicate tracking across query plan candidates. Benchmark: 14.4× speedup
at N=1,000 nodes with M=20 indexes.
The SWI-Prolog Findings
Two confirmed CWE-407 sites in SWI-Prolog's standard library:
-
swipl-0001 (
ugraphs.pl:510): Kahn's topological sort usesgraph_memberchk— an O(|V|) association list scan — once per zero-in-degree vertex in the hot loop. Produces O(|V|²) total. Fix:list_to_assoc/2+get_assoc/3for O(log|V|) lookup. 250× speedup at V=500. Patched. -
swipl-0002 (
aggregate.pl:673):free_variables/4useslist_is_free_of/2— an O(N) scan — once per candidate variable into a growing accumulator. O(N²) total. Maintainer self-flagged with@tbdcomment. Fix: thread an assoc (keyed on variable standard order / memory address) through all recursive clauses. 450× speedup at N=1,000. Patched.
A third site (clp_distinct.pl:173-174) is fixable-pending — it requires restructuring
the dom_neq constraint attribute to carry a flat hash set rather than a list pair.
The Flagship Measurement
javac GraphUtils.java Tarjan SCC — before/after benchmark:
| Graph size | Before (ops) | After (ops) | Speedup |
|---|---|---|---|
| V=200 | 4,891 | 287 | 17× |
| V=400 | 19,204 | 572 | 33× |
| V=800 | 77,441 | 1,143 | 68× |
Growth ratio before: 3.89× per doubling (quadratic). Growth ratio after: 1.99× per
doubling (linear). The fix is a one-line change: stack.contains(n) → n.active.
Confirmed Clean Ecosystems
The following were systematically scanned and confirmed free of CWE-407:
Crypto/blockchain: Bitcoin Core, Litecoin, Dogecoin, Monero, Solana validator, solang
(Solidity→BPF). Bitcoin Core's cluster mempool uses multi-word integer bitsets with
popcount() — a more sophisticated approach than hash sets.
P2P/anonymity: libtorrent, I2P Java router, Transmission, Kubo (go-ipfs), Deluge.
Routing/infra: FRRouting bgpd (bgp_aspath.c is O(L) single-call, not nested).
Scientific/graph: NetworkX — all in visited checks are O(1) throughout.
JVM/enterprise build: ONOS, OpenDaylight, MySQL optimizer, V8 TurboFan, SpiderMonkey IonMonkey, Bazel, sbt, GNU Octave, KiCad, Yosys, Verilator.
CLI implementations: All 40 of 42 unsandbox inception CLI implementations (Scala and Forth sources unavailable) are clean. REST clients have no hot algorithmic paths. The defect concentrates in core computational infrastructure, not in I/O-bound client code.
Scope of Future Work
Priority unscanned targets:
| System | Language | Why critical |
|---|---|---|
| Hyperledger Besu | Java | Full Ethereum execution client — P2P graph, EVM pipeline |
| FRRouting isisd | C | IS-IS SPF, carrier backbone routing |
| ExaBGP | Python | Pure Python BGP — high probability |
| OpenROAD / OpenSTA / ABC | C++ | EDA timing analysis on netlists |
| Blender node graph | C/Python | Geometry nodes, compositor |
| BIRD bgpd | C | IXP route servers globally |
| NuGet | C# | .NET dependency resolution |
frrouting-0002 (ospf_spf.c:275) is confirmed unpatched — O(V²) worst case on hub
topologies, triggered on every OSPF topology change. The fix is OSPF_SPF_RESULT_PARENT
set → hash set in parent resolution.
Disclosure Plan
- All 42 patched sites have patches, unit tests with operation counts, and benchmarks.
- Unpatched disclosed sites (frrouting-0002, tor-0001, solc-0001/-0002, Minecraft) are documented with complexity proofs and fix proposals.
- No upstream regression suite has been run against any patched build — patches are algorithmic proofs; each maintainer validates against their CI.
- Upstream maintainers notified privately before any public release.
- 90-day response window per maintainer.
- PostgreSQL notified with
nodeHash()infrastructure proposal. - CVE filing for tools that accept untrusted input. See
whitepaper/disclosure/cve-strategy.rst. - Disclosure sequenced by blast radius: low-surface tools first, then build tools, then compilers, then database and routing infrastructure.
One-Sentence Version
A list used where a set belongs, in graph traversal code written before hash containers were idiomatic, has been running silently at O(n²) in 50+ confirmed sites across 25+ ecosystems — compilers, package managers, database query planners, graph databases, routing daemons, Prolog runtimes, and anonymity networks — the fix is a one-line data structure substitution with no behavioral change, and we have patched, tested, and benchmarked every confirmed site in the compiler, routing, database, Prolog, and build-tool wave.