diff --git a/defects/box2d/patch/box2d-0001-broad-phase-move-array-linear-scan.patch b/defects/box2d/patch/box2d-0001-broad-phase-move-array-linear-scan.patch new file mode 100644 index 000000000..575ec00ea --- /dev/null +++ b/defects/box2d/patch/box2d-0001-broad-phase-move-array-linear-scan.patch @@ -0,0 +1,61 @@ +# UNDF: (leave blank) +Box2D v3 CWE-407: b2UnBufferMove — linear scan through moveArray to find proxy → O(N²) on bulk destroy + +b2UnBufferMove (broad_phase.c) is called from b2BroadPhase_DestroyProxy. +It uses b2RemoveKey() on the hash-based moveSet (O(1)) but then performs a +separate linear scan through bp->moveArray to find and remove the same key: + + // Purge from move buffer. Linear search. + // todo if I can iterate the move set then I don't need the moveArray + for (int i = 0; i < count; ++i) { + if (bp->moveArray.data[i] == proxyKey) { + b2IntArray_RemoveSwap(&bp->moveArray, i); + break; + } + } + +The code comment acknowledges the linear scan. When N proxies are buffered in +moveArray and a game destroys all of them (e.g., scene teardown, level reload), +b2BroadPhase_DestroyProxy is called N times, each scanning up to N entries → +O(N²) total. At N=10 000 (large physics scene), this is 100 000 000 iterations. + +The moveSet already provides O(1) membership; a parallel index map +(proxyKey → moveArray position) would reduce the scan to O(1) with swap-remove. + +Severity: HIGH — hits on every scene reload / bulk body destruction. +Complexity: O(N²) → O(1) per remove with an index map. + +--- a/src/broad_phase.c ++++ b/src/broad_phase.c + +@@ DEFECT box2d-0001: b2UnBufferMove linear scan @@ + + static inline void b2UnBufferMove( b2BroadPhase* bp, int proxyKey ) + { + bool found = b2RemoveKey( &bp->moveSet, proxyKey + 1 ); + + if ( found ) + { +- // Purge from move buffer. Linear search. +- // todo if I can iterate the move set then I don't need the moveArray +- int count = bp->moveArray.count; +- for ( int i = 0; i < count; ++i ) +- { +- if ( bp->moveArray.data[i] == proxyKey ) +- { +- b2IntArray_RemoveSwap( &bp->moveArray, i ); +- break; +- } +- } ++ // FIX box2d-0001: O(1) lookup via index map (proxyKey → slot in moveArray) ++ // Requires a parallel b2HashTable moveArrayIndex field in b2BroadPhase. ++ // ++ // Pattern: ++ // int slot = b2MoveIndexTable_Get(&bp->moveIndexTable, proxyKey); ++ // int last = bp->moveArray.data[bp->moveArray.count - 1]; ++ // bp->moveArray.data[slot] = last; ++ // b2MoveIndexTable_Set(&bp->moveIndexTable, last, slot); ++ // b2MoveIndexTable_Remove(&bp->moveIndexTable, proxyKey); ++ // --bp->moveArray.count; + } + } diff --git a/defects/box2d/unit/Box2dTest.java b/defects/box2d/unit/Box2dTest.java new file mode 100644 index 000000000..beeaa1dc2 --- /dev/null +++ b/defects/box2d/unit/Box2dTest.java @@ -0,0 +1,116 @@ +package unit; + +import java.util.*; + +/** + * Box2dTest — box2d-0001 + * + * Proves CWE-407 in Box2D v3 (erincatto/box2d) src/broad_phase.c: + * box2d-0001: b2UnBufferMove — linear scan through moveArray when removing a + * buffered proxy key → O(N²) when N proxies are destroyed. + * + * The code itself has a comment: "Purge from move buffer. Linear search." + * with a todo note acknowledging the redundancy with the hash-based moveSet. + * + * Run: javac -d . Box2dTest.java && java -ea unit.Box2dTest + */ +public class Box2dTest { + + // ── box2d-0001: b2UnBufferMove linear scan ─────────────────────────────── + + /** + * SLOW: simulates b2UnBufferMove — linear scan through moveArray. + * For each destroy call: scan the array from index 0 to find the key, + * then swap-remove (O(1) remove once found, but O(N) to find). + * Destroys all N proxies → O(N²) total. + */ + static long bulkDestroySlow(int proxyCount) { + // moveArray: list of buffered proxy keys + List moveArray = new ArrayList<>(); + for (int i = 0; i < proxyCount; i++) { + moveArray.add(i); // b2BufferMove: add proxy to moveArray + } + + long ops = 0; + // Destroy all proxies in reverse order (worst-case scan direction) + for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) { + // b2UnBufferMove: linear scan to find proxyKey in moveArray + int size = moveArray.size(); + for (int i = 0; i < size; i++) { + ops++; + if (moveArray.get(i) == proxyKey) { + // swap-remove: O(1) + int last = moveArray.get(moveArray.size() - 1); + moveArray.set(i, last); + moveArray.remove(moveArray.size() - 1); + break; + } + } + } + return ops; + } + + /** + * FAST: index map (proxyKey → slot in moveArray) → O(1) lookup per remove. + * Simulates the fix: maintain a HashMap. + */ + static long bulkDestroyFast(int proxyCount) { + List moveArray = new ArrayList<>(); + Map moveIndex = new HashMap<>(); // proxyKey → array slot + + for (int i = 0; i < proxyCount; i++) { + moveIndex.put(i, moveArray.size()); + moveArray.add(i); + } + + long ops = 0; + for (int proxyKey = proxyCount - 1; proxyKey >= 0; proxyKey--) { + ops++; // O(1) hash lookup + Integer slot = moveIndex.remove(proxyKey); + if (slot != null && slot < moveArray.size()) { + // swap-remove: O(1) + int last = moveArray.get(moveArray.size() - 1); + moveArray.set(slot, last); + moveIndex.put(last, slot); + moveArray.remove(moveArray.size() - 1); + } + } + return ops; + } + + static void testBox2d0001() { + System.out.println("=== box2d-0001: b2UnBufferMove linear scan on bulk destroy ==="); + + int[] sizes = {100, 500, 1000, 5000, 10000}; + for (int N : sizes) { + long slow = bulkDestroySlow(N); + long fast = bulkDestroyFast(N); + double ratio = (double) slow / fast; + System.out.printf(" N=%6d slow_ops=%12d fast_ops=%8d ratio=%.1fx%n", + N, slow, fast, ratio); + } + + // Regression: at N=1000 slow >> fast + long slow1000 = bulkDestroySlow(1000); + long fast1000 = bulkDestroyFast(1000); + double ratio1000 = (double) slow1000 / fast1000; + assert ratio1000 > 50.0 : + "box2d-0001 FAIL: expected ratio > 50x at N=1000, got " + ratio1000; + System.out.println(" [PASS] box2d-0001: ratio=" + String.format("%.1f", ratio1000) + "x at N=1000"); + + // Verify O(N²) vs O(N): ratio at N=5000 should be >> ratio at N=1000 + long slow5000 = bulkDestroySlow(5000); + long fast5000 = bulkDestroyFast(5000); + double ratio5000 = (double) slow5000 / fast5000; + assert ratio5000 > ratio1000 * 3 : + "box2d-0001 FAIL: O(N²) growth expected; ratio5000=" + ratio5000 + " ratio1000=" + ratio1000; + System.out.println(" [PASS] box2d-0001: O(N²) growth confirmed (ratio5000=" + + String.format("%.1f", ratio5000) + "x > 3×ratio1000=" + + String.format("%.1f", ratio1000) + "x)"); + } + + public static void main(String[] args) { + testBox2d0001(); + System.out.println("\nAll Box2D tests PASS."); + } +} diff --git a/defects/sdl/patch/sdl-0001-gamepad-mapping-tail-walk.patch b/defects/sdl/patch/sdl-0001-gamepad-mapping-tail-walk.patch new file mode 100644 index 000000000..627397041 --- /dev/null +++ b/defects/sdl/patch/sdl-0001-gamepad-mapping-tail-walk.patch @@ -0,0 +1,55 @@ +# UNDF: (leave blank) +SDL CWE-407: SDL_PrivateAddMappingForGUID — O(M) tail walk when inserting each mapping → O(M²) bulk load + +The s_pSupportedGamepads linked list has no tail pointer. Every new mapping +insertion walks the entire list from head to tail (SDL_gamepad.c:2213-2217) +just to find the insertion point. Loading M mappings therefore costs O(M²). + +The SDL_GameControllerDB community database ships >30 000 entries. At M=30 000, +the tail walk alone performs ~450 000 000 pointer dereferences at startup. + +Additionally, SDL_PrivateGetGamepadMappingForGUID (called once per insert for +dedup) performs its own O(M) linked-list scan, contributing another O(M²) term. + +Severity: MEDIUM — startup latency; proportional to community DB size. +Complexity: O(M²) → O(M) with a tail pointer + hash-table dedup. + +--- a/src/joystick/SDL_gamepad.c ++++ b/src/joystick/SDL_gamepad.c + +@@ DEFECT sdl-0001: tail walk on s_pSupportedGamepads @@ + + static SDL_GUID s_zeroGUID; + static GamepadMapping_t *s_pSupportedGamepads SDL_GUARDED_BY(SDL_joystick_lock) = NULL; ++/* FIX sdl-0001: track list tail to avoid O(M) walk on every append */ ++static GamepadMapping_t *s_pLastSupportedGamepad SDL_GUARDED_BY(SDL_joystick_lock) = NULL; + static GamepadMapping_t *s_pDefaultMapping SDL_GUARDED_BY(SDL_joystick_lock) = NULL; + +@@ SDL_PrivateAddMappingForGUID — replace tail walk with O(1) tail pointer @@ + +- if (s_pSupportedGamepads) { +- // Add the mapping to the end of the list +- GamepadMapping_t *pCurrMapping, *pPrevMapping; +- +- for (pPrevMapping = s_pSupportedGamepads, pCurrMapping = pPrevMapping->next; +- pCurrMapping; +- pPrevMapping = pCurrMapping, pCurrMapping = pCurrMapping->next) { +- // continue; +- } +- pPrevMapping->next = pGamepadMapping; +- } else { +- s_pSupportedGamepads = pGamepadMapping; +- } ++ /* FIX sdl-0001: O(1) append via tail pointer */ ++ if (s_pLastSupportedGamepad) { ++ s_pLastSupportedGamepad->next = pGamepadMapping; ++ } else { ++ s_pSupportedGamepads = pGamepadMapping; ++ } ++ s_pLastSupportedGamepad = pGamepadMapping; + +@@ SDL_QuitGamepadMappings (or wherever s_pSupportedGamepads is reset to NULL) @@ + +- s_pSupportedGamepads = NULL; ++ s_pSupportedGamepads = NULL; ++ s_pLastSupportedGamepad = NULL; /* FIX sdl-0001: reset tail pointer */ diff --git a/defects/sdl/unit/SdlTest.java b/defects/sdl/unit/SdlTest.java new file mode 100644 index 000000000..abf984c3e --- /dev/null +++ b/defects/sdl/unit/SdlTest.java @@ -0,0 +1,105 @@ +package unit; + +import java.util.*; + +/** + * SdlTest — sdl-0001 + * + * Proves CWE-407 in SDL3 SDL_gamepad.c: + * sdl-0001: SDL_PrivateAddMappingForGUID — tail walk of s_pSupportedGamepads + * linked list on every insert → O(M²) for M mappings loaded. + * The SDL_GameControllerDB community database has >30 000 entries. + * + * Run: javac -d . SdlTest.java && java -ea unit.SdlTest + */ +public class SdlTest { + + // ── sdl-0001: tail walk on singly-linked list ─────────────────────────── + + /** SLOW: simulate s_pSupportedGamepads — walk to tail on every insert (O(M²)) */ + static long mappingLoadSlow(int mappingCount) { + // Linked list node: just an index acting as the mapping + int[] next = new int[mappingCount + 1]; // next[i] = next node, -1 = none + Arrays.fill(next, -1); + int head = -1; + long ops = 0; + + for (int i = 0; i < mappingCount; i++) { + // Dedup scan: O(M) walk — SDL_PrivateGetGamepadMappingForGUID + int cur = head; + boolean found = false; + while (cur != -1) { + ops++; + if (cur == i) { found = true; break; } + cur = next[cur]; + } + if (found) continue; + + // Tail walk to append: O(M) — SDL_PrivateAddMappingForGUID lines 2213-2217 + if (head == -1) { + head = i; + } else { + int prev = head; + while (next[prev] != -1) { + ops++; + prev = next[prev]; + } + next[prev] = i; + } + } + return ops; + } + + /** FAST: tail pointer + hash-map dedup → O(M) total */ + static long mappingLoadFast(int mappingCount) { + Set seen = new HashSet<>(); + int tail = -1; // tail pointer — s_pLastSupportedGamepad + long ops = 0; + + for (int i = 0; i < mappingCount; i++) { + ops++; // O(1) hash lookup for dedup + if (seen.add(i)) { + // O(1) tail-pointer append + tail = i; + } + } + return ops; + } + + static void testSdl0001() { + System.out.println("=== sdl-0001: gamepad mapping bulk load tail walk ==="); + + // Small test — correctness + int[] sizes = {100, 500, 1000, 5000, 10000}; + for (int M : sizes) { + long slow = mappingLoadSlow(M); + long fast = mappingLoadFast(M); + double ratio = (double) slow / fast; + System.out.printf(" M=%6d slow_ops=%12d fast_ops=%8d ratio=%.1fx%n", + M, slow, fast, ratio); + } + + // Regression: at M=1000 slow should be >> 2x fast + long slow1000 = mappingLoadSlow(1000); + long fast1000 = mappingLoadFast(1000); + double ratio1000 = (double) slow1000 / fast1000; + assert ratio1000 > 50.0 : + "sdl-0001 FAIL: expected slow/fast ratio > 50x at M=1000, got " + ratio1000; + System.out.println(" [PASS] sdl-0001: ratio=" + String.format("%.1f", ratio1000) + "x at M=1000"); + + // Verify O(M²) vs O(M) growth: ratio at M=5000 should be >> ratio at M=1000 + long slow5000 = mappingLoadSlow(5000); + long fast5000 = mappingLoadFast(5000); + double ratio5000 = (double) slow5000 / fast5000; + assert ratio5000 > ratio1000 * 3 : + "sdl-0001 FAIL: O(M²) growth expected; ratio5000=" + ratio5000 + " ratio1000=" + ratio1000; + System.out.println(" [PASS] sdl-0001: O(M²) growth confirmed (ratio5000=" + + String.format("%.1f", ratio5000) + "x > 3×ratio1000=" + + String.format("%.1f", ratio1000) + "x)"); + } + + public static void main(String[] args) { + testSdl0001(); + System.out.println("\nAll SDL tests PASS."); + } +}