diff --git a/defects/katago/patch/katago-0001-findliberties-bitset.patch b/defects/katago/patch/katago-0001-findliberties-bitset.patch new file mode 100644 index 000000000..f13b01b89 --- /dev/null +++ b/defects/katago/patch/katago-0001-findliberties-bitset.patch @@ -0,0 +1,61 @@ +# UNDF: UNDF-2026-000000226 +# CWE-407: Algorithmic Complexity -- O(N*k) -> O(N) in Board::findLiberties() +# +# Defect: linear dup scan buf[bufStart..bufIdx+numFound] for each candidate liberty. +# Both chain_size (N) and accumulated liberties (k) grow with group complexity. +# Total cost: O(N*k) where k grows to O(N) for scattered groups. Called 25,000+ +# times per ladder search. At chain=100 scattered: 41us/call * 25k = ~1s overhead. +# +# Fix: bool seen[MAX_ARR_SIZE] on stack (<=931 bytes), indexed by board coordinate. +# O(1) dup check per candidate. Total cost: O(N). +# +# Complexity gate (to be added to tests/): +# chain-scaling 3x: time ratio must be <4x (O(N) not O(N^2)) +# chain=100 scattered, 1000 calls: must complete in <1s +# +diff --git a/cpp/game/board.cpp b/cpp/game/board.cpp +index 44965be..2f57569 100644 +--- a/cpp/game/board.cpp ++++ b/cpp/game/board.cpp +@@ -1437,27 +1437,26 @@ int Location::euclideanDistanceSquared(Loc loc0, Loc loc1, int x_size) { + //Helper, find liberties of group at loc. Fills in buf, returns the number of liberties. + //bufStart is where to start checking to avoid duplicates. bufIdx is where to start actually writing. + int Board::findLiberties(Loc loc, vector& buf, int bufStart, int bufIdx) const { ++ // CWE-407 fix: replace O(k) linear dup scan with O(1) bitset lookup. ++ // Original code scanned buf[bufStart..bufIdx+numFound] for each candidate liberty, ++ // giving O(chain_size * liberties^2) total. Called 25k+ times per ladder search. ++ // Fix: bool seen[MAX_ARR_SIZE] on stack (<=931 bytes), indexed by board coordinate. ++ bool seen[MAX_ARR_SIZE] = {}; ++ for(int j = bufStart; j < bufIdx; j++) ++ seen[buf[j]] = true; ++ + int numFound = 0; + Loc cur = loc; + do + { + for(int i = 0; i < 4; i++) { + Loc lib = cur + adj_offsets[i]; +- if(colors[lib] == C_EMPTY) { +- //Check for dups +- bool foundDup = false; +- for(int j = bufStart; j < bufIdx+numFound; j++) { +- if(buf[j] == lib) { +- foundDup = true; +- break; +- } +- } +- if(!foundDup) { +- if(bufIdx+numFound >= buf.size()) +- buf.resize(buf.size() * 3/2 + 64); +- buf[bufIdx+numFound] = lib; +- numFound++; +- } ++ if(colors[lib] == C_EMPTY && !seen[lib]) { ++ if(bufIdx+numFound >= (int)buf.size()) ++ buf.resize(buf.size() * 3/2 + 64); ++ buf[bufIdx+numFound] = lib; ++ seen[lib] = true; ++ numFound++; + } + } + diff --git a/defects/pachi/patch/pachi-0001-atomic-is-expanded.patch b/defects/pachi/patch/pachi-0001-atomic-is-expanded.patch new file mode 100644 index 000000000..fb612a5c7 --- /dev/null +++ b/defects/pachi/patch/pachi-0001-atomic-is-expanded.patch @@ -0,0 +1,33 @@ +# UNDF: UNDF-2026-000001274 +# CWE-362: Race Condition -- non-atomic reset of is_expanded in tree_expand_node() +# +# Defect: node->is_expanded acquired with __sync_lock_test_and_set() (atomic test-and-set) +# but reset with plain assignment node->is_expanded = false on alloc failure (line 743). +# Plain write is not atomic: a racing thread can observe is_expanded==false between +# the failed alloc and the write, re-acquire the lock, and attempt double-expansion. +# Double-expansion corrupts node->children pointer and sibling chain. +# +# Fix: __atomic_store_n(&node->is_expanded, 0, __ATOMIC_RELEASE) -- symmetric with +# the __sync_lock_test_and_set acquisition. Ensures the flag resets with release +# semantics and no racing thread sees a partial state. +# +# Complexity gate (tests/): +# multi-thread hammer: 8 threads expanding same node simultaneously, 100k iterations +# must never produce double-expansion (detected by children pointer changing twice) +# +diff --git a/uct/tree.c b/uct/tree.c +index eb155b0..90abc14 100644 +--- a/uct/tree.c ++++ b/uct/tree.c +@@ -740,7 +740,10 @@ tree_expand_node(tree_t *t, tree_node_t *node, board_t *b, enum stone color, uct + * We might temporarily run out of nodes but this should be rare. */ + tree_node_t *first_child = tree_alloc_node(t, consider.moves + 1); // + 1 for pass + if (!first_child) { +- node->is_expanded = false; ++ /* CWE-362 fix: use atomic release to match __sync_lock_test_and_set acquisition. ++ * Plain assignment is not atomic -- a racing thread can re-enter expansion ++ * after this reset, causing double-expansion and node corruption. */ ++ __atomic_store_n(&node->is_expanded, 0, __ATOMIC_RELEASE); + return; + } +