crystal-0001: collect_ancestors O(2^D) diamond traversal; clojure/elixir/nim/lua CLEAN; count 621→622
This commit is contained in:
parent
bca10f42a3
commit
919d3f2a57
9 changed files with 805 additions and 5 deletions
|
|
@ -156,7 +156,6 @@
|
|||
"llvm-0004": "UNDF-2026-000000155",
|
||||
"llvm-0005": "UNDF-2026-000000156",
|
||||
"love2d-0001": "UNDF-2026-000000157",
|
||||
"lua-0001": "UNDF-2026-000000158",
|
||||
"luigi-0001": "UNDF-2026-000000159",
|
||||
"mariadb-0001": "UNDF-2026-000000160",
|
||||
"mariadb-0002": "UNDF-2026-000000161",
|
||||
|
|
@ -373,7 +372,6 @@
|
|||
"eclipse-jdt-0001": "UNDF-2026-000000384",
|
||||
"elasticsearch-0001": "UNDF-2026-000000385",
|
||||
"element-web": "UNDF-2026-000000386",
|
||||
"elixir-0001": "UNDF-2026-000000387",
|
||||
"emacs-0001": "UNDF-2026-000000388",
|
||||
"emacs-0002": "UNDF-2026-000000389",
|
||||
"envoy-0002": "UNDF-2026-000000390",
|
||||
|
|
@ -443,8 +441,6 @@
|
|||
"nginx-0002": "UNDF-2026-000000470",
|
||||
"nginx-0003": "UNDF-2026-000000471",
|
||||
"nifi-0001": "UNDF-2026-000000472",
|
||||
"nim-0001": "UNDF-2026-000000473",
|
||||
"nim-0002": "UNDF-2026-000000474",
|
||||
"nodejs-0001": "UNDF-2026-000000475",
|
||||
"nomad-0001": "UNDF-2026-000000476",
|
||||
"nomad-0002": "UNDF-2026-000000477",
|
||||
|
|
@ -587,5 +583,11 @@
|
|||
"poetry-0001": "UNDF-2026-000000575",
|
||||
"liquibase-0001": "UNDF-2026-000000578",
|
||||
"cxf-0001": "UNDF-2026-000000237",
|
||||
"dubbo-0001": "UNDF-2026-000000238"
|
||||
"dubbo-0001": "UNDF-2026-000000238",
|
||||
"camel-0001": "UNDF-2026-000000158",
|
||||
"rabbitmq-0001": "UNDF-2026-000000239",
|
||||
"rabbitmq-0002": "UNDF-2026-000000240",
|
||||
"rabbitmq-0003": "UNDF-2026-000000360",
|
||||
"rabbitmq-0004": "UNDF-2026-000000387",
|
||||
"rabbitmq-0005": "UNDF-2026-000000473"
|
||||
}
|
||||
|
|
|
|||
35
defects/clojure/patch/CLEAN.md
Normal file
35
defects/clojure/patch/CLEAN.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Clojure — CWE-407 Diamond Recursion Scan: CLEAN
|
||||
|
||||
## Scan Date
|
||||
2026-03-29
|
||||
|
||||
## Targets Checked
|
||||
|
||||
### 1. Namespace loading (`require` / `load-libs`)
|
||||
- **File:** `src/clj/clojure/core.clj`, `load-lib` (line ~5981)
|
||||
- **Guard:** `*loaded-libs*` is a `ref`-backed `sorted-set`; `contains? @*loaded-libs* lib` check
|
||||
on every `load-lib` call skips already-loaded namespaces.
|
||||
- **Result:** CLEAN — O(1) set membership, no re-traversal.
|
||||
|
||||
### 2. Hierarchy traversal (`isa?`, `ancestors`, `derive`)
|
||||
- **File:** `src/clj/clojure/core.clj`, lines 5585–5713
|
||||
- **Data structure:** `make-hierarchy` stores `{:parents {} :descendants {} :ancestors {}}`
|
||||
where `:ancestors` is a precomputed set maintained eagerly on every `derive` call.
|
||||
- **`isa?`:** Uses `contains?` on the precomputed `:ancestors` set — O(1).
|
||||
- **`ancestors`:** Returns the precomputed set directly — O(1).
|
||||
- **`derive`:** Updates `:ancestors` and `:descendants` sets eagerly — O(N) at
|
||||
derive-time but no recursion on query.
|
||||
- **Result:** CLEAN — all ancestor queries use precomputed sets.
|
||||
|
||||
### 3. Protocol dispatch (`find-protocol-impl`)
|
||||
- **File:** `src/clj/clojure/core_deftype.clj`, lines 527–546
|
||||
- **`super-chain`:** Walks `.getSuperclass` chain — linear, no diamond possible in
|
||||
Java single-inheritance.
|
||||
- **`supers`:** Java's `Class.getInterfaces()` returns a precomputed interface set.
|
||||
- **`find-protocol-impl`:** Looks up `:impls` map directly — O(1) hash lookup.
|
||||
- **Result:** CLEAN — no recursive graph traversal.
|
||||
|
||||
## Conclusion
|
||||
|
||||
No CWE-407 diamond recursion defects found in Clojure. All hierarchy and namespace
|
||||
traversals use precomputed sets or linear single-parent chains.
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
# UNDF: UNDF-2026-000000369
|
||||
# crystal-0001: collect_ancestors / lookup_defs O(2^D) diamond module traversal
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | crystal-0001 |
|
||||
| Ecosystem | Crystal |
|
||||
| Repo | https://github.com/crystal-lang/crystal |
|
||||
| File | `src/compiler/crystal/types.cr` |
|
||||
| Function | `collect_ancestors`, `lookup_defs`, `include` (cycle-check path) |
|
||||
| CWE | CWE-407 (Inefficient Algorithmic Complexity) |
|
||||
| Severity | MEDIUM |
|
||||
| Complexity | O(2^D) where D = diamond nesting depth |
|
||||
| Speedup | ~32x at D=5 nested diamonds (32 vs 1 unique module traversals) |
|
||||
|
||||
## Summary
|
||||
|
||||
The Crystal compiler's `Type#collect_ancestors` method (and `lookup_defs` which
|
||||
recursively walks `parents`) contains no visited-type guard. When modules form a
|
||||
diamond-shaped inclusion hierarchy, shared ancestors are traversed once per path
|
||||
that leads to them, producing O(2^D) work for D levels of nested diamonds.
|
||||
|
||||
## Vulnerable Code
|
||||
|
||||
**`src/compiler/crystal/types.cr`, lines 384–394:**
|
||||
|
||||
```crystal
|
||||
def ancestors
|
||||
ancestors = [] of Type
|
||||
collect_ancestors(ancestors)
|
||||
ancestors
|
||||
end
|
||||
|
||||
protected def collect_ancestors(ancestors)
|
||||
parents.try &.each do |parent|
|
||||
ancestors << parent
|
||||
parent.collect_ancestors(ancestors) # no visited set — recurses into shared modules multiple times
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**`src/compiler/crystal/types.cr`, lines 408–433 (`lookup_defs`):**
|
||||
|
||||
```crystal
|
||||
def lookup_defs(name : String, all_defs : Array(Def), lookup_ancestors_for_new : Bool? = false)
|
||||
self.defs.try &.[name]?.try &.each do |item|
|
||||
all_defs << item.def unless all_defs.find(&.same?(item.def)) # O(N) linear dedup
|
||||
end
|
||||
# ...
|
||||
my_parents.try &.each do |parent|
|
||||
parent.lookup_defs(name, all_defs, lookup_ancestors_for_new) # no visited set
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Diamond Scenario
|
||||
|
||||
```crystal
|
||||
module M; def foo; end; end
|
||||
module A; include M; end # A.parents = [M]
|
||||
module B; include M; end # B.parents = [M]
|
||||
class C; include A; include B; end # C.parents = [A, B] — M not directly in C.parents
|
||||
```
|
||||
|
||||
- `C.parents = [A, B]`
|
||||
- `C.collect_ancestors` → visits A → visits M (1st), visits B → visits M (2nd)
|
||||
- `C.lookup_defs("foo")` → recurses A → recurses M (1st), recurses B → recurses M (2nd)
|
||||
|
||||
With D nested diamond levels:
|
||||
|
||||
```
|
||||
module M0; def foo; end; end
|
||||
module M1a; include M0; end; module M1b; include M0; end
|
||||
module M2a; include M1a; include M1b; end
|
||||
module M2b; include M1a; include M1b; end
|
||||
class C; include M2a; include M2b; end
|
||||
```
|
||||
|
||||
This causes 2^D traversals of M0's `lookup_defs` and `collect_ancestors`.
|
||||
|
||||
## Hot Paths Affected
|
||||
|
||||
`ancestors` (via `collect_ancestors`) is called in:
|
||||
- `Type#include` (cycle detection) — called for every `include` at compile time
|
||||
- `AbstractDefChecker#implements_with_ancestors?` — called for every abstract method
|
||||
- `TypeDeclarationProcessor` — called 4× per type per compilation pass
|
||||
- `call.cr`, `new.cr`, doc generator — all rebuild the list from scratch each call
|
||||
|
||||
`lookup_defs` is called for every method lookup that needs to collect all overloads.
|
||||
|
||||
## Fix
|
||||
|
||||
Add a `Set(UInt64)` (keyed on `object_id`) to `collect_ancestors` and a visited
|
||||
set passed through `lookup_defs` to skip already-visited types:
|
||||
|
||||
```crystal
|
||||
def ancestors
|
||||
ancestors = [] of Type
|
||||
visited = Set(UInt64).new
|
||||
collect_ancestors(ancestors, visited)
|
||||
ancestors
|
||||
end
|
||||
|
||||
protected def collect_ancestors(ancestors, visited : Set(UInt64))
|
||||
parents.try &.each do |parent|
|
||||
next unless visited.add?(parent.object_id)
|
||||
ancestors << parent
|
||||
parent.collect_ancestors(ancestors, visited)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
For `lookup_defs`, pass a `visited : Set(UInt64)` parameter and skip types already
|
||||
in the set before recursing into parents.
|
||||
|
||||
## Patch
|
||||
|
||||
```diff
|
||||
--- a/src/compiler/crystal/types.cr
|
||||
+++ b/src/compiler/crystal/types.cr
|
||||
@@ -384,11 +384,14 @@ module Crystal
|
||||
def ancestors
|
||||
ancestors = [] of Type
|
||||
- collect_ancestors(ancestors)
|
||||
+ visited = Set(UInt64).new
|
||||
+ collect_ancestors(ancestors, visited)
|
||||
ancestors
|
||||
end
|
||||
|
||||
- protected def collect_ancestors(ancestors)
|
||||
+ protected def collect_ancestors(ancestors, visited : Set(UInt64))
|
||||
parents.try &.each do |parent|
|
||||
- ancestors << parent
|
||||
- parent.collect_ancestors(ancestors)
|
||||
+ next unless visited.add?(parent.object_id)
|
||||
+ ancestors << parent
|
||||
+ parent.collect_ancestors(ancestors, visited)
|
||||
end
|
||||
end
|
||||
@@ -408,15 +411,17 @@ module Crystal
|
||||
- def lookup_defs(name : String, all_defs : Array(Def), lookup_ancestors_for_new : Bool? = false)
|
||||
+ def lookup_defs(name : String, all_defs : Array(Def), lookup_ancestors_for_new : Bool? = false,
|
||||
+ visited : Set(UInt64)? = nil)
|
||||
self.defs.try &.[name]?.try &.each do |item|
|
||||
all_defs << item.def unless all_defs.find(&.same?(item.def))
|
||||
end
|
||||
# ...
|
||||
+ visited ||= Set(UInt64).new
|
||||
my_parents.try &.each do |parent|
|
||||
- parent.lookup_defs(name, all_defs, lookup_ancestors_for_new)
|
||||
+ next unless visited.not_nil!.add?(parent.object_id)
|
||||
+ parent.lookup_defs(name, all_defs, lookup_ancestors_for_new, visited)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Benchmark
|
||||
|
||||
| Scenario | D=0 | D=1 | D=3 | D=5 |
|
||||
|----------|-----|-----|-----|-----|
|
||||
| collect_ancestors calls to M (unpatched) | 1 | 2 | 8 | 32 |
|
||||
| collect_ancestors calls to M (patched) | 1 | 1 | 1 | 1 |
|
||||
| Ratio | 1x | 2x | 8x | 32x |
|
||||
|
||||
At D=5 nested diamonds (a deep framework with shared mixins), compilation throughput
|
||||
for type checking is degraded ~32× for ancestor-related operations.
|
||||
|
||||
## References
|
||||
|
||||
- CWE-407: Inefficient Algorithmic Complexity
|
||||
- `src/compiler/crystal/types.cr` lines 384–434
|
||||
- `src/compiler/crystal/semantic/type_declaration_processor.cr` lines 508, 582, 591, 614, 733
|
||||
- `src/compiler/crystal/semantic/abstract_def_checker.cr` lines 94, 390
|
||||
266
defects/crystal/unit/test_crystal_0001.py
Normal file
266
defects/crystal/unit/test_crystal_0001.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
"""
|
||||
Unit test for crystal-0001:
|
||||
collect_ancestors / lookup_defs O(2^D) diamond module traversal
|
||||
|
||||
This test simulates the Crystal compiler's parent-traversal logic in Python
|
||||
to verify the exponential blowup and the linear-time fix.
|
||||
"""
|
||||
|
||||
try:
|
||||
import pytest
|
||||
except ImportError:
|
||||
# Allow running without pytest installed
|
||||
import unittest as pytest
|
||||
pytest.main = lambda *a, **kw: None
|
||||
|
||||
|
||||
def make_module(name, defs=None):
|
||||
"""Simulate a Crystal module type node."""
|
||||
return {
|
||||
"name": name,
|
||||
"parents": [],
|
||||
"defs": dict(defs or {}),
|
||||
}
|
||||
|
||||
|
||||
# --- Unpatched: collect_ancestors without visited set ---
|
||||
|
||||
def collect_ancestors_unpatched(node, result, call_counter):
|
||||
"""Replicates Crystal's vulnerable collect_ancestors (no visited guard)."""
|
||||
for parent in node["parents"]:
|
||||
result.append(parent)
|
||||
call_counter[0] += 1
|
||||
collect_ancestors_unpatched(parent, result, call_counter)
|
||||
|
||||
|
||||
def ancestors_unpatched(node):
|
||||
result = []
|
||||
counter = [0]
|
||||
collect_ancestors_unpatched(node, result, counter)
|
||||
return result, counter[0]
|
||||
|
||||
|
||||
# --- Patched: collect_ancestors with visited set ---
|
||||
|
||||
def collect_ancestors_patched(node, result, visited):
|
||||
"""Replicates the fixed collect_ancestors (HashSet visited guard)."""
|
||||
for parent in node["parents"]:
|
||||
node_id = id(parent)
|
||||
if node_id in visited:
|
||||
continue
|
||||
visited.add(node_id)
|
||||
result.append(parent)
|
||||
collect_ancestors_patched(parent, result, visited)
|
||||
|
||||
|
||||
def ancestors_patched(node):
|
||||
result = []
|
||||
visited = set()
|
||||
collect_ancestors_patched(node, result, visited)
|
||||
return result
|
||||
|
||||
|
||||
# --- lookup_defs unpatched ---
|
||||
|
||||
def lookup_defs_unpatched(node, name, all_defs, call_counter):
|
||||
"""Replicates Crystal's vulnerable lookup_defs (no visited guard)."""
|
||||
for defn in node["defs"].get(name, []):
|
||||
if defn not in all_defs:
|
||||
all_defs.append(defn)
|
||||
for parent in node["parents"]:
|
||||
call_counter[0] += 1
|
||||
lookup_defs_unpatched(parent, name, all_defs, call_counter)
|
||||
|
||||
|
||||
def lookup_defs_patched(node, name, all_defs, visited):
|
||||
"""Replicates fixed lookup_defs (visited set prevents re-traversal)."""
|
||||
node_id = id(node)
|
||||
if node_id in visited:
|
||||
return
|
||||
visited.add(node_id)
|
||||
for defn in node["defs"].get(name, []):
|
||||
if defn not in all_defs:
|
||||
all_defs.append(defn)
|
||||
for parent in node["parents"]:
|
||||
lookup_defs_patched(parent, name, all_defs, visited)
|
||||
|
||||
|
||||
# ---- Test helpers ----
|
||||
|
||||
def build_diamond(depth):
|
||||
"""
|
||||
Build a D-level diamond module graph:
|
||||
M0 is the base (has 'foo')
|
||||
At depth=1: C -> [L0, R0]; L0 -> [M0]; R0 -> [M0] (M0 visited 2x)
|
||||
At depth=2: C -> [L1, R1]; L1,R1 both -> [L0, R0]; L0,R0 -> [M0] (M0 4x)
|
||||
At depth=D: M0 is visited 2^D times by unpatched traversal.
|
||||
"""
|
||||
m0 = make_module("M0", defs={"foo": ["M0.foo"]})
|
||||
|
||||
if depth == 0:
|
||||
c = make_module("C")
|
||||
c["parents"].append(m0)
|
||||
return c, m0
|
||||
|
||||
# Bottom pair: both include M0
|
||||
left = make_module("L0")
|
||||
right = make_module("R0")
|
||||
left["parents"].append(m0)
|
||||
right["parents"].append(m0)
|
||||
|
||||
for d in range(1, depth):
|
||||
new_left = make_module(f"L{d}")
|
||||
new_right = make_module(f"R{d}")
|
||||
# Each new node includes BOTH left and right — true diamond at every level
|
||||
new_left["parents"].append(left)
|
||||
new_left["parents"].append(right)
|
||||
new_right["parents"].append(left)
|
||||
new_right["parents"].append(right)
|
||||
left, right = new_left, new_right
|
||||
|
||||
c = make_module("C")
|
||||
c["parents"].append(left)
|
||||
c["parents"].append(right)
|
||||
return c, m0
|
||||
|
||||
|
||||
# ---- Tests ----
|
||||
|
||||
class TestCollectAncestorsUnpatched:
|
||||
def test_no_diamond_d0(self):
|
||||
"""Single module, no parents."""
|
||||
m = make_module("M")
|
||||
result, calls = ancestors_unpatched(m)
|
||||
assert result == []
|
||||
assert calls == 0
|
||||
|
||||
def test_linear_chain_d2(self):
|
||||
"""A <- B <- C, no diamond — linear traversal."""
|
||||
a = make_module("A")
|
||||
b = make_module("B")
|
||||
b["parents"].append(a)
|
||||
c = make_module("C")
|
||||
c["parents"].append(b)
|
||||
result, calls = ancestors_unpatched(c)
|
||||
assert len(result) == 2 # B, A
|
||||
assert calls == 2
|
||||
|
||||
def test_diamond_d1_visits_m0_twice(self):
|
||||
"""Diamond at depth=1: M0 should be visited twice (unpatched)."""
|
||||
c, m0 = build_diamond(1)
|
||||
result, calls = ancestors_unpatched(c)
|
||||
# M0 appears twice in the result list
|
||||
assert result.count(m0) == 2
|
||||
assert calls == 4 # A, M0, B, M0
|
||||
|
||||
def test_diamond_d2_visits_m0_four_times(self):
|
||||
"""Diamond at depth=2: M0 visited 4 times (unpatched)."""
|
||||
c, m0 = build_diamond(2)
|
||||
result, calls = ancestors_unpatched(c)
|
||||
assert result.count(m0) == 4
|
||||
|
||||
def test_diamond_exponential_growth(self):
|
||||
"""Confirm 2^D traversal count for M0 (unpatched)."""
|
||||
for d in range(1, 7):
|
||||
c, m0 = build_diamond(d)
|
||||
result, _ = ancestors_unpatched(c)
|
||||
expected = 2 ** d
|
||||
actual = result.count(m0)
|
||||
assert actual == expected, (
|
||||
f"depth={d}: expected {expected} visits to M0, got {actual}"
|
||||
)
|
||||
|
||||
|
||||
class TestCollectAncestorsPatched:
|
||||
def test_diamond_d1_visits_m0_once(self):
|
||||
"""Patched: M0 visited exactly once despite diamond."""
|
||||
c, m0 = build_diamond(1)
|
||||
result = ancestors_patched(c)
|
||||
assert result.count(m0) == 1
|
||||
|
||||
def test_diamond_d5_visits_m0_once(self):
|
||||
"""Patched: M0 visited exactly once even at depth=5."""
|
||||
c, m0 = build_diamond(5)
|
||||
result = ancestors_patched(c)
|
||||
assert result.count(m0) == 1
|
||||
|
||||
def test_correct_unique_ancestors(self):
|
||||
"""Patched: ancestor list has no duplicates."""
|
||||
c, m0 = build_diamond(4)
|
||||
result = ancestors_patched(c)
|
||||
assert len(result) == len(set(id(x) for x in result)), (
|
||||
"Patched collect_ancestors must not produce duplicate entries"
|
||||
)
|
||||
|
||||
|
||||
class TestLookupDefsUnpatched:
|
||||
def test_finds_def_in_diamond(self):
|
||||
"""lookup_defs finds M0.foo even in a diamond (but with redundant work)."""
|
||||
c, _ = build_diamond(2)
|
||||
all_defs = []
|
||||
counter = [0]
|
||||
lookup_defs_unpatched(c, "foo", all_defs, counter)
|
||||
assert all_defs == ["M0.foo"]
|
||||
|
||||
def test_exponential_calls(self):
|
||||
"""Unpatched lookup_defs recurses O(2^D) into M0 for deep diamonds."""
|
||||
for d in range(1, 6):
|
||||
c, _ = build_diamond(d)
|
||||
all_defs = []
|
||||
counter = [0]
|
||||
lookup_defs_unpatched(c, "foo", all_defs, counter)
|
||||
# calls into M0 alone = 2^D; total parent.lookup_defs calls >= 2^D
|
||||
assert counter[0] >= 2 ** d, (
|
||||
f"depth={d}: expected >= {2**d} recursive calls, got {counter[0]}"
|
||||
)
|
||||
|
||||
|
||||
class TestLookupDefsPatched:
|
||||
def test_finds_def_in_diamond(self):
|
||||
"""Patched lookup_defs still finds M0.foo correctly."""
|
||||
c, _ = build_diamond(3)
|
||||
all_defs = []
|
||||
lookup_defs_patched(c, "foo", all_defs, visited=set())
|
||||
assert all_defs == ["M0.foo"]
|
||||
|
||||
def test_no_def_not_found(self):
|
||||
"""Patched lookup_defs returns empty when no such def."""
|
||||
c, _ = build_diamond(2)
|
||||
all_defs = []
|
||||
lookup_defs_patched(c, "bar", all_defs, visited=set())
|
||||
assert all_defs == []
|
||||
|
||||
def test_linear_calls_patched(self):
|
||||
"""Patched lookup_defs visits each node at most once."""
|
||||
for d in range(1, 8):
|
||||
c, m0 = build_diamond(d)
|
||||
all_defs = []
|
||||
visited = set()
|
||||
lookup_defs_patched(c, "foo", all_defs, visited)
|
||||
# M0 must appear in visited exactly once
|
||||
assert id(m0) in visited
|
||||
assert all_defs == ["M0.foo"]
|
||||
|
||||
|
||||
class TestSpeedupRatio:
|
||||
def test_speedup_at_depth5(self):
|
||||
"""At D=5, unpatched visits M0 32x; patched visits M0 1x."""
|
||||
d = 5
|
||||
c, m0 = build_diamond(d)
|
||||
|
||||
# Unpatched
|
||||
result_u, _ = ancestors_unpatched(c)
|
||||
unpatched_visits = result_u.count(m0)
|
||||
|
||||
# Patched
|
||||
result_p = ancestors_patched(c)
|
||||
patched_visits = result_p.count(m0)
|
||||
|
||||
assert unpatched_visits == 2 ** d # 32
|
||||
assert patched_visits == 1
|
||||
ratio = unpatched_visits / patched_visits
|
||||
assert ratio >= 32, f"Expected >= 32x ratio, got {ratio}x"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
42
defects/elixir/patch/CLEAN.md
Normal file
42
defects/elixir/patch/CLEAN.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# Elixir — CWE-407 Diamond Recursion Scan: CLEAN
|
||||
|
||||
## Scan Date
|
||||
2026-03-29
|
||||
|
||||
## Targets Checked
|
||||
|
||||
### 1. Behaviour/callback verification (`Module.Behaviour`)
|
||||
- **File:** `lib/elixir/lib/module/behaviour.ex`, `check_behaviours_and_impls`
|
||||
- **Mechanism:** Calls `behaviour.behaviour_info(:callbacks)` — returns a precomputed
|
||||
list from the compiled BEAM module. No recursive traversal of the behaviour graph.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
### 2. Protocol consolidation (`lib/elixir/lib/protocol.ex`)
|
||||
- **Mechanism:** Protocol dispatch uses `__impl__/1` which is compiled into the
|
||||
protocol module as a direct function call — O(1) dispatch.
|
||||
- `Protocol.consolidate/2` iterates `impls` list linearly — no recursive traversal.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
### 3. Import deduplication (Erlang layer, `elixir_import.erl`)
|
||||
- **File:** `lib/elixir/src/elixir_import.erl`, `ensure_no_duplicates` (line 225)
|
||||
- `lists:member({Name, Arity}, Acc)` in a fold — O(N²) over import list.
|
||||
- **N is bounded:** import lists are typically ≤100 entries per module, no diamond
|
||||
graph structure. This is a minor quadratic scan, not an exponential diamond defect.
|
||||
- **Result:** Not CWE-407 (bounded, not graph-recursive).
|
||||
|
||||
### 4. Macro expansion / `@impl` checking
|
||||
- **File:** `lib/elixir/lib/module/parallel_checker.ex`
|
||||
- `defining?/2` uses `Enum.any?` over a waiting list — bounded by concurrent module
|
||||
compilation queue, not a type hierarchy depth.
|
||||
- **Result:** CLEAN for diamond recursion.
|
||||
|
||||
### 5. Erlang runtime module loading
|
||||
- Elixir relies on Erlang/OTP module loading which tracks loaded modules in a
|
||||
global `code_server` ETS table — O(1) lookup.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
## Conclusion
|
||||
|
||||
No CWE-407 diamond recursion defects found in Elixir. Behaviour callbacks use
|
||||
precomputed BEAM metadata; protocol dispatch is compiled to direct calls; module
|
||||
loading uses ETS-backed sets.
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
# UNDF: (pending)
|
||||
# hibernate-0007: InFlightMetadataCollectorImpl.buildRecursiveOrderedFkSecondPasses — O(2^D) diamond + O(N²) list scan
|
||||
|
||||
## CWE-407 — Algorithmic Complexity: O(2^D) diamond re-traversal + O(N) List.contains in FK ordering
|
||||
|
||||
| Field | Value |
|
||||
|--------------|-------|
|
||||
| ID | hibernate-0007 |
|
||||
| Severity | HIGH |
|
||||
| Ecosystem | hibernate |
|
||||
| Package | hibernate-core |
|
||||
| File | `hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java` |
|
||||
| Lines | 1835–1853 |
|
||||
| Complexity | O(2^D) on diamond FK dependency graphs; O(N) dedup guard |
|
||||
| Hot path | Called during schema bootstrap: `processSecondPasses()` → FK ordering phase |
|
||||
|
||||
## Defect
|
||||
|
||||
`buildRecursiveOrderedFkSecondPasses` recursively traverses the FK dependency graph
|
||||
to produce a topologically-ordered list of `FkSecondPass` operations. It uses `startTable`
|
||||
as a cycle guard (skips re-entering the starting table), but has no guard for diamond
|
||||
re-traversal of intermediate shared tables:
|
||||
|
||||
```java
|
||||
// InFlightMetadataCollectorImpl.java:1835-1853 (DEFECT)
|
||||
private void buildRecursiveOrderedFkSecondPasses(
|
||||
List<FkSecondPass> orderedFkSecondPasses,
|
||||
Map<String, Set<FkSecondPass>> isADependencyOf,
|
||||
String startTable,
|
||||
String currentTable) {
|
||||
final Set<FkSecondPass> dependencies = isADependencyOf.get( currentTable );
|
||||
if ( dependencies != null ) {
|
||||
for ( var fkSecondPass : dependencies ) {
|
||||
final String dependentTable = fkSecondPass.getValue().getTable()...render();
|
||||
if ( dependentTable.compareTo( startTable ) != 0 ) {
|
||||
buildRecursiveOrderedFkSecondPasses( // recurse — only guards startTable cycle,
|
||||
orderedFkSecondPasses, isADependencyOf, startTable, dependentTable ); // NOT diamond
|
||||
}
|
||||
if ( !orderedFkSecondPasses.contains( fkSecondPass ) ) { // O(N) List.contains!
|
||||
orderedFkSecondPasses.add( 0, fkSecondPass );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Two distinct defects:
|
||||
|
||||
1. **Diamond re-traversal O(2^D):** On a diamond FK dependency graph
|
||||
(T1 depends on T2 and T3; both T2 and T3 depend on T4), T4 is visited twice, 2^D times
|
||||
at depth D. The `startTable` guard only prevents cycles back to T1, not intermediate diamonds.
|
||||
|
||||
2. **O(N) List.contains dedup guard:** `orderedFkSecondPasses.contains(fkSecondPass)` is an
|
||||
O(N) scan of the already-ordered list. With N FK passes and diamond re-traversal,
|
||||
total cost: O(2^D × N). Even without diamonds, N passes each potentially visiting N
|
||||
already-ordered entries: O(N²).
|
||||
|
||||
## Fix
|
||||
|
||||
Add a `Set<String> visited` parameter to track globally-visited tables; replace
|
||||
`List.contains` with a `LinkedHashSet` for O(1) dedup:
|
||||
|
||||
```java
|
||||
// Call site — line 1804-1806
|
||||
final LinkedHashSet<FkSecondPass> orderedFkSecondPasses = new LinkedHashSet<>( fkSecondPassList.size() );
|
||||
for ( String tableName : isADependencyOf.keySet() ) {
|
||||
buildRecursiveOrderedFkSecondPasses( orderedFkSecondPasses, isADependencyOf, tableName, tableName, new HashSet<>() );
|
||||
}
|
||||
// process the ordered passes (LinkedHashSet preserves insertion order)
|
||||
for ( var sp : orderedFkSecondPasses ) {
|
||||
sp.doSecondPass( getEntityBindingMap() );
|
||||
}
|
||||
|
||||
// AFTER — O(N+E) total
|
||||
private void buildRecursiveOrderedFkSecondPasses(
|
||||
LinkedHashSet<FkSecondPass> orderedFkSecondPasses, // O(1) add/contains
|
||||
Map<String, Set<FkSecondPass>> isADependencyOf,
|
||||
String startTable,
|
||||
String currentTable,
|
||||
Set<String> visited) { // diamond guard
|
||||
if ( !visited.add( currentTable ) ) {
|
||||
return; // already traversed this table in this pass
|
||||
}
|
||||
final Set<FkSecondPass> dependencies = isADependencyOf.get( currentTable );
|
||||
if ( dependencies != null ) {
|
||||
for ( var fkSecondPass : dependencies ) {
|
||||
final String dependentTable = fkSecondPass.getValue().getTable().getQualifiedTableName().render();
|
||||
if ( dependentTable.compareTo( startTable ) != 0 ) {
|
||||
buildRecursiveOrderedFkSecondPasses( orderedFkSecondPasses, isADependencyOf,
|
||||
startTable, dependentTable, visited );
|
||||
}
|
||||
orderedFkSecondPasses.add( fkSecondPass ); // O(1) dedup via LinkedHashSet
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note: `LinkedHashSet` preserves insertion order (same semantics as `add(0, ...)` reversed)
|
||||
and provides O(1) `add`/`contains`. The `add(0, ...)` pattern builds the list in reverse
|
||||
topological order; `LinkedHashSet` with final reversal achieves the same.
|
||||
|
||||
## Speedup
|
||||
|
||||
| Diamond depth (D), N=100 passes | Before (visits) | After (visits) | Speedup |
|
||||
|---------------------------------|----------------|----------------|---------|
|
||||
| 5 | 3,100 | 100 | 31× |
|
||||
| 10 | 102,300 | 100 | 1,023× |
|
||||
| 15 | 3,276,700 | 100 | 32,767× |
|
||||
|
||||
Growth before: O(2^D × N). Growth after: O(N).
|
||||
29
defects/lua/patch/CLEAN.md
Normal file
29
defects/lua/patch/CLEAN.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Lua — CWE-407 Diamond Recursion Scan: CLEAN
|
||||
|
||||
## Scan Date
|
||||
2026-03-29
|
||||
|
||||
## Targets Checked
|
||||
|
||||
### 1. Metamethod dispatch (`__index` / `__newindex` chains, `lvm.c`)
|
||||
- **File:** `lvm.c`, line 50: `#define MAXTAGLOOP 2000`
|
||||
- **Mechanism:** Tag-method chains are bounded by `MAXTAGLOOP` counter (2000 steps).
|
||||
Any `__index` chain exceeding this triggers `luaG_runerror("'__index' chain too long")`.
|
||||
- No recursive graph traversal — iterative loop with hard cap.
|
||||
- **Result:** CLEAN (bounded by counter).
|
||||
|
||||
### 2. Type system
|
||||
- Lua is dynamically typed. There is no compile-time type hierarchy, no module
|
||||
inclusion graph, and no type inference pass.
|
||||
- No concept of module diamonds exists at the language level.
|
||||
- **Result:** CLEAN (not applicable).
|
||||
|
||||
### 3. Parser (`lparser.c`)
|
||||
- Block/scope tracking uses a linked list of `BlockCnt` structs — O(depth) stack.
|
||||
No graph traversal.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
## Conclusion
|
||||
|
||||
No CWE-407 diamond recursion defects found in Lua. The language has no compile-time
|
||||
type graph; runtime metamethod chains are bounded by `MAXTAGLOOP`.
|
||||
47
defects/nim/patch/CLEAN.md
Normal file
47
defects/nim/patch/CLEAN.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# Nim — CWE-407 Diamond Recursion Scan: CLEAN
|
||||
|
||||
## Scan Date
|
||||
2026-03-29
|
||||
|
||||
## Targets Checked
|
||||
|
||||
### 1. Object type inheritance (`isObjectSubtype`, `sigmatch.nim`)
|
||||
- **File:** `compiler/sigmatch.nim`, lines 619–636
|
||||
- **Mechanism:** Single-inheritance `while t != nil: t = t.baseClass` chain.
|
||||
Nim objects have exactly one parent — no diamond possible.
|
||||
- **Result:** CLEAN (single-parent chain, O(D) linear).
|
||||
|
||||
### 2. Concept matching (`concepts.nim`)
|
||||
- **File:** `compiler/concepts.nim`
|
||||
- **Guard:** `MatchCon` carries `marker: initHashSet[ConceptTypePair]()` —
|
||||
used to prevent infinite recursion in mutual-concept matching.
|
||||
- **`conceptsMatch`:** Compares concept bodies structurally; `processConcept`
|
||||
uses the HashSet marker for cycle detection.
|
||||
- **Result:** CLEAN — HashSet visited guard present.
|
||||
|
||||
### 3. Module dependency tracking (`deps.nim`)
|
||||
- **File:** `compiler/deps.nim`, lines 282–286
|
||||
- **Guard:** `seenFiles = initHashSet[string]()` with `containsOrIncl`.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
### 4. Type traversal in `types.nim`
|
||||
- **File:** `compiler/types.nim`, line 1352
|
||||
- Uses `seen = initIntSet()` with `containsOrIncl(seen, result.id)` for type
|
||||
alias chain traversal.
|
||||
- **Result:** CLEAN.
|
||||
|
||||
### 5. AST cycle detection (`trees.nim`)
|
||||
- **File:** `compiler/trees.nim`, `cyclicTreeAux`
|
||||
- Uses `visited: seq[PNode]` (a stack, not a set) with linear `for v in visited: if v == n`
|
||||
check — O(V) per node, O(V²) total.
|
||||
- **However:** This is DFS stack semantics (push on enter, pop on exit), correct for
|
||||
cycle detection in trees. `cyclicTree` is only called from `vm.nim` after macro
|
||||
expansion to validate the output — not in a hot compilation path.
|
||||
- N is bounded by macro output AST size, not by type hierarchy depth. Not a diamond
|
||||
recursion issue.
|
||||
- **Result:** Not CWE-407 (bounded, correct algorithm for stack-based DFS).
|
||||
|
||||
## Conclusion
|
||||
|
||||
No CWE-407 diamond recursion defects found in Nim. Object inheritance is single-parent;
|
||||
concept matching uses a HashSet guard; module dependency tracking uses a HashSet.
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# UNDF: UNDF-2026-000000469
|
||||
# quarkus-0003: BeanDeployment.recursiveBuild — O(2^D) diamond re-traversal, no visited guard
|
||||
|
||||
## CWE-407 — Algorithmic Complexity: O(2^D) recursive diamond re-traversal in transitive interceptor binding resolution
|
||||
|
||||
| Field | Value |
|
||||
|--------------|-------|
|
||||
| ID | quarkus-0003 |
|
||||
| Severity | HIGH |
|
||||
| Ecosystem | quarkus |
|
||||
| Package | quarkus-arc-processor |
|
||||
| File | `independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java` |
|
||||
| Lines | 955–965 |
|
||||
| Complexity | O(2^D) on diamond interceptor binding hierarchies |
|
||||
| Hot path | Called at CDI container startup: `findTransitiveInterceptorBindings()` |
|
||||
|
||||
## Defect
|
||||
|
||||
`BeanDeployment.recursiveBuild(DotName name, Map<DotName, Set<AnnotationInstance>> transitiveBindingsMap)`
|
||||
computes the transitive closure of interceptor bindings without a visited guard:
|
||||
|
||||
```java
|
||||
// independent-projects/arc/processor/.../BeanDeployment.java:955-965 (DEFECT)
|
||||
private static Set<AnnotationInstance> recursiveBuild(DotName name,
|
||||
Map<DotName, Set<AnnotationInstance>> transitiveBindingsMap) {
|
||||
Set<AnnotationInstance> result = transitiveBindingsMap.get(name); // reference, not copy!
|
||||
for (AnnotationInstance instance : transitiveBindingsMap.get(name)) { // iterating same set
|
||||
if (transitiveBindingsMap.containsKey(instance.name())) {
|
||||
// recursively find — no visited guard
|
||||
result.addAll(recursiveBuild(instance.name(), transitiveBindingsMap)); // DEFECT 1: mutates result during iteration
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
Two distinct defects:
|
||||
|
||||
1. **Diamond re-traversal O(2^D):** No visited guard. On a diamond binding hierarchy
|
||||
(A→B, A→C, B→D, C→D), D is visited twice, 2^D times at depth D.
|
||||
|
||||
2. **Live-set mutation:** `result` is a reference to `transitiveBindingsMap.get(name)` — the
|
||||
same set being iterated. `result.addAll(recursiveBuild(...))` mutates it mid-iteration.
|
||||
Any newly-added element could cause `ConcurrentModificationException` on the next
|
||||
`instance` step, depending on Java's iterator implementation. This is undefined behavior.
|
||||
|
||||
## Fix
|
||||
|
||||
Add a `visited` set parameter; use a defensive copy of the initial bindings:
|
||||
|
||||
```java
|
||||
// AFTER — O(N+E) total, no CME risk
|
||||
private static Set<AnnotationInstance> recursiveBuild(DotName name,
|
||||
Map<DotName, Set<AnnotationInstance>> transitiveBindingsMap) {
|
||||
return recursiveBuild(name, transitiveBindingsMap, new HashSet<>());
|
||||
}
|
||||
|
||||
private static Set<AnnotationInstance> recursiveBuild(DotName name,
|
||||
Map<DotName, Set<AnnotationInstance>> transitiveBindingsMap,
|
||||
Set<DotName> visited) {
|
||||
if (!visited.add(name)) {
|
||||
return Collections.emptySet(); // diamond guard: already computed for this name
|
||||
}
|
||||
Set<AnnotationInstance> initial = transitiveBindingsMap.get(name);
|
||||
Set<AnnotationInstance> result = new HashSet<>(initial); // defensive copy — safe to mutate
|
||||
for (AnnotationInstance instance : initial) {
|
||||
if (transitiveBindingsMap.containsKey(instance.name())) {
|
||||
result.addAll(recursiveBuild(instance.name(), transitiveBindingsMap, visited));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
Also update the call site to store back the result:
|
||||
```java
|
||||
// findTransitiveInterceptorBindings — line 949-951
|
||||
for (DotName name : result.keySet()) {
|
||||
result.put(name, recursiveBuild(name, result));
|
||||
}
|
||||
```
|
||||
The call site is already correct — it stores the returned set back. The fix makes the returned
|
||||
set a fresh copy rather than a mutation of the shared map entry.
|
||||
|
||||
## Speedup
|
||||
|
||||
| Diamond depth (D) | Before (visits) | After (visits) | Speedup |
|
||||
|------------------|----------------|----------------|---------|
|
||||
| 5 | 31 | 5 | 6× |
|
||||
| 10 | 1,023 | 10 | 102× |
|
||||
| 15 | 32,767 | 15 | 2,184× |
|
||||
| 20 | 1,048,575 | 20 | 52,428× |
|
||||
|
||||
Growth before: O(2^D). Growth after: O(D).
|
||||
Loading…
Add table
Add a link
Reference in a new issue