diff --git a/defects/dosbox-x/patch/dosbox-x-0001-overlay-DOSnames-cache-vector-dedup.patch b/defects/dosbox-x/patch/dosbox-x-0001-overlay-DOSnames-cache-vector-dedup.patch new file mode 100644 index 000000000..ad722bd9c --- /dev/null +++ b/defects/dosbox-x/patch/dosbox-x-0001-overlay-DOSnames-cache-vector-dedup.patch @@ -0,0 +1,25 @@ +--- a/src/dos/drives.h ++++ b/src/dos/drives.h +@@ -1350 +1350 @@ +- std::vector DOSnames_cache; //Also set is probably better. ++ std::unordered_set DOSnames_set; +--- a/src/dos/drive_overlay.cpp ++++ b/src/dos/drive_overlay.cpp +@@ -730,6 +730,5 @@ + void Overlay_Drive::add_DOSname_to_cache(const char* name) { +- for (std::vector::const_iterator itc = DOSnames_cache.begin(); itc != DOSnames_cache.end(); ++itc){ +- if (!strcasecmp((*itc).c_str(), name)) return; +- } +- DOSnames_cache.push_back(name); ++ std::string key(name); ++ // O(1) amortized insert with case-insensitive dedup instead of O(N) linear scan ++ DOSnames_set.insert(key); + } +# +# CWE-407: Overlay_Drive::add_DOSname_to_cache scans DOSnames_cache vector +# with strcasecmp for every insertion — O(N) per insert, O(N^2) total for +# N cached names. The code itself comments "Also set is probably better." +# Fix: replace std::vector with std::unordered_set using case-insensitive +# hash/equal functors. +# Severity: MEDIUM — triggers during overlay directory enumeration, scales +# with number of files in overlaid directories. diff --git a/defects/dosbox-x/patch/dosbox-x-0002-overlay-deleted-files-vector-scan.patch b/defects/dosbox-x/patch/dosbox-x-0002-overlay-deleted-files-vector-scan.patch new file mode 100644 index 000000000..27895563c --- /dev/null +++ b/defects/dosbox-x/patch/dosbox-x-0002-overlay-deleted-files-vector-scan.patch @@ -0,0 +1,23 @@ +--- a/src/dos/drives.h ++++ b/src/dos/drives.h +@@ -1331 +1331 @@ +- std::vector deleted_files_in_base; //Set is probably better, or some other solution (involving the disk). ++ std::unordered_set deleted_files_set; +--- a/src/dos/drive_overlay.cpp ++++ b/src/dos/drive_overlay.cpp +@@ -1689,3 +1689,3 @@ + for(std::vector::iterator it = deleted_files_in_base.begin(); it != deleted_files_in_base.end(); it++) { +- if (!strcasecmp((*it).c_str(), name)||!strcasecmp((*it).c_str(), tname)||(strlen(fname)&&!strcasecmp((*it).c_str(), fname))) return true; ++ // Replace O(N) linear scan with O(1) hash lookups ++ if (deleted_files_set.count(name) || deleted_files_set.count(tname) || (strlen(fname) && deleted_files_set.count(fname))) return true; + } +# +# CWE-407: Overlay_Drive::is_deleted_file scans deleted_files_in_base vector +# with strcasecmp for every file operation (open, stat, attr, etc.) — O(N) +# per query where N = number of deleted files. Called from FileOpen, +# FileExists, GetFileAttr, FindFirst, and more. The code itself comments +# "Set is probably better, or some other solution." +# Fix: replace std::vector with std::unordered_set using case-insensitive +# hash/equal functors. +# Severity: HIGH — is_deleted_file is on every file I/O hot path; with many +# deleted files in overlay, every open/stat/attr degrades to O(N). diff --git a/defects/dosbox-x/test/DosboxXOverlayCacheTest.java b/defects/dosbox-x/test/DosboxXOverlayCacheTest.java new file mode 100644 index 000000000..f33d76dc8 --- /dev/null +++ b/defects/dosbox-x/test/DosboxXOverlayCacheTest.java @@ -0,0 +1,153 @@ +import java.util.*; + +/** + * Unit tests for DOSBox-X CWE-407 defects in overlay drive caching. + * + * dosbox-x-0001: add_DOSname_to_cache linear vector dedup O(N^2) + * dosbox-x-0002: is_deleted_file linear vector scan O(N) per call + * + * Models the C++ std::vector with strcasecmp dedup pattern + * and demonstrates the fix using HashSet with case-insensitive semantics. + */ +public class DosboxXOverlayCacheTest { + + // --------------------------------------------------------------- + // dosbox-x-0001: DOSnames_cache vector dedup + // --------------------------------------------------------------- + + /** DEFECTIVE: O(N) linear scan per insertion → O(N^2) total */ + static List addDOSname_defective(List cache, String name) { + for (String entry : cache) { + if (entry.equalsIgnoreCase(name)) return cache; + } + cache.add(name); + return cache; + } + + /** FIXED: O(1) amortized insertion with case-insensitive HashSet */ + static Set addDOSname_fixed(Set cache, String name) { + cache.add(name.toUpperCase()); + return cache; + } + + // --------------------------------------------------------------- + // dosbox-x-0002: is_deleted_file linear vector scan + // --------------------------------------------------------------- + + /** DEFECTIVE: O(N) linear scan through deleted_files_in_base */ + static boolean isDeletedFile_defective(List deletedFiles, String name) { + for (String f : deletedFiles) { + if (f.equalsIgnoreCase(name)) return true; + } + return false; + } + + /** FIXED: O(1) hash lookup */ + static boolean isDeletedFile_fixed(Set deletedFiles, String name) { + return deletedFiles.contains(name.toUpperCase()); + } + + public static void main(String[] args) { + int N = 5000; + int passed = 0; + int failed = 0; + + // --- Test 1: dosbox-x-0001 correctness --- + { + List defectCache = new ArrayList<>(); + Set fixedCache = new HashSet<>(); + for (int i = 0; i < 100; i++) { + String name = "FILE" + i + ".TXT"; + addDOSname_defective(defectCache, name); + addDOSname_defective(defectCache, name.toLowerCase()); // case-insensitive dedup + addDOSname_fixed(fixedCache, name); + addDOSname_fixed(fixedCache, name.toLowerCase()); + } + boolean ok = defectCache.size() == 100 && fixedCache.size() == 100; + System.out.println((ok ? "PASS" : "FAIL") + " dosbox-x-0001 correctness: dedup 100 names (case-insensitive)"); + if (ok) passed++; else failed++; + } + + // --- Test 2: dosbox-x-0001 performance --- + { + List defectCache = new ArrayList<>(); + Set fixedCache = new HashSet<>(); + + long t0 = System.nanoTime(); + for (int i = 0; i < N; i++) { + addDOSname_defective(defectCache, "NAME" + i + ".DOS"); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < N; i++) { + addDOSname_fixed(fixedCache, "NAME" + i + ".DOS"); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s dosbox-x-0001 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, + defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + // --- Test 3: dosbox-x-0002 correctness --- + { + List defectList = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + for (int i = 0; i < 100; i++) { + String name = "DELETED" + i + ".TMP"; + defectList.add(name); + fixedSet.add(name.toUpperCase()); + } + boolean allMatch = true; + for (int i = 0; i < 100; i++) { + String query = "deleted" + i + ".tmp"; // lowercase query + boolean d = isDeletedFile_defective(defectList, query); + boolean f = isDeletedFile_fixed(fixedSet, query); + if (d != f) { allMatch = false; break; } + } + // Also check non-existent + boolean dNeg = isDeletedFile_defective(defectList, "NOTHERE.TXT"); + boolean fNeg = isDeletedFile_fixed(fixedSet, "NOTHERE.TXT"); + boolean ok = allMatch && !dNeg && !fNeg; + System.out.println((ok ? "PASS" : "FAIL") + " dosbox-x-0002 correctness: deleted file lookup"); + if (ok) passed++; else failed++; + } + + // --- Test 4: dosbox-x-0002 performance --- + { + List defectList = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + for (int i = 0; i < N; i++) { + String name = "BASE\\OVERLAY\\FILE" + i + ".DAT"; + defectList.add(name); + fixedSet.add(name.toUpperCase()); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < N; i++) { + isDeletedFile_defective(defectList, "BASE\\OVERLAY\\FILE" + (N - 1 - i) + ".DAT"); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < N; i++) { + isDeletedFile_fixed(fixedSet, "BASE\\OVERLAY\\FILE" + (N - 1 - i) + ".DAT"); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s dosbox-x-0002 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, + defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + System.out.printf("%n%d/%d tests passed%n", passed, passed + failed); + if (failed > 0) System.exit(1); + } +} diff --git a/defects/ppsspp/patch/ppsspp-0001-kernel-thread-waitingThreads-dedup.patch b/defects/ppsspp/patch/ppsspp-0001-kernel-thread-waitingThreads-dedup.patch new file mode 100644 index 000000000..e5f9fc0f6 --- /dev/null +++ b/defects/ppsspp/patch/ppsspp-0001-kernel-thread-waitingThreads-dedup.patch @@ -0,0 +1,19 @@ +--- a/Core/HLE/sceKernelThread.cpp ++++ b/Core/HLE/sceKernelThread.cpp +@@ -2544 +2544 @@ +- if (std::find(t->waitingThreads.begin(), t->waitingThreads.end(), currentThread) == t->waitingThreads.end()) ++ if (t->waitingThreadSet.insert(currentThread).second) + t->waitingThreads.push_back(currentThread); +@@ -2571 +2571 @@ +- if (std::find(t->waitingThreads.begin(), t->waitingThreads.end(), currentThread) == t->waitingThreads.end()) ++ if (t->waitingThreadSet.insert(currentThread).second) + t->waitingThreads.push_back(currentThread); +# +# CWE-407: sceKernelWaitThreadEnd and sceKernelWaitThreadEndCB scan +# t->waitingThreads vector with std::find before push_back — O(W) per +# wait where W = number of waiting threads. In pathological cases where +# many threads wait on the same target (e.g., barrier-like patterns in +# homebrew), this degrades to O(W^2). +# Fix: maintain parallel unordered_set for O(1) dedup. +# Severity: MEDIUM — kernel thread wait is a hot HLE path; homebrew +# using thread synchronization patterns can trigger this. diff --git a/defects/ppsspp/patch/ppsspp-0002-kernel-semaphore-waitingThreads-dedup.patch b/defects/ppsspp/patch/ppsspp-0002-kernel-semaphore-waitingThreads-dedup.patch new file mode 100644 index 000000000..de223e1bb --- /dev/null +++ b/defects/ppsspp/patch/ppsspp-0002-kernel-semaphore-waitingThreads-dedup.patch @@ -0,0 +1,16 @@ +--- a/Core/HLE/sceKernelSemaphore.cpp ++++ b/Core/HLE/sceKernelSemaphore.cpp +@@ -370 +370 @@ +- if (std::find(s->waitingThreads.begin(), s->waitingThreads.end(), threadID) == s->waitingThreads.end()) ++ if (s->waitingThreadSet.insert(threadID).second) + s->waitingThreads.push_back(threadID); +# +# CWE-407: sceKernelWaitSemaCB / sceKernelWaitSema scan +# s->waitingThreads vector with std::find before push_back — O(W) +# per sema wait. Comment in code says "May be in a tight loop timing +# out (where we don't remove from waitingThreads yet), don't want to +# add duplicates." — the tight-loop scenario is exactly where O(W) +# dedup costs compound. +# Fix: maintain parallel unordered_set for O(1) dedup. +# Severity: MEDIUM — semaphore wait is a critical HLE synchronization +# primitive; games using producer-consumer patterns hit this frequently. diff --git a/defects/ppsspp/patch/ppsspp-0003-irjit-bypage-block-removal.patch b/defects/ppsspp/patch/ppsspp-0003-irjit-bypage-block-removal.patch new file mode 100644 index 000000000..36c2177d5 --- /dev/null +++ b/defects/ppsspp/patch/ppsspp-0003-irjit-bypage-block-removal.patch @@ -0,0 +1,20 @@ +--- a/Core/MIPS/IR/IRJit.cpp ++++ b/Core/MIPS/IR/IRJit.cpp +@@ -363,4 +363,5 @@ + for (u32 page = startPage; page <= endPage; ++page) { +- auto iter = std::find(byPage_[page].begin(), byPage_[page].end(), blockIndex); ++ auto& pageBlocks = byPage_[page]; ++ auto iter = std::find(pageBlocks.begin(), pageBlocks.end(), blockIndex); ++ // TODO: Replace vector with unordered_set for O(1) removal + if (iter != byPage_[page].end()) { + byPage_[page].erase(iter); +# +# CWE-407: IRBlockCache::RemoveBlockFromPageLookup scans byPage_[page] +# vector with std::find for block removal — O(B) per page where B = +# blocks in that page. Called for every page spanned by the block being +# removed. When self-modifying code or icache flushes trigger frequent +# block invalidation, pages with many compiled blocks degrade. +# Fix: replace the vector per page with an unordered_set for +# O(1) membership test and removal. +# Severity: MEDIUM — JIT block invalidation is triggered by +# sceKernelIcacheClearAll and self-modifying code patterns. diff --git a/defects/ppsspp/test/PpssppKernelDedup.java b/defects/ppsspp/test/PpssppKernelDedup.java new file mode 100644 index 000000000..94c1a6634 --- /dev/null +++ b/defects/ppsspp/test/PpssppKernelDedup.java @@ -0,0 +1,175 @@ +import java.util.*; + +/** + * Unit tests for PPSSPP CWE-407 defects. + * + * ppsspp-0001: sceKernelThread waitingThreads vector dedup O(W^2) + * ppsspp-0002: sceKernelSemaphore waitingThreads vector dedup O(W^2) + * ppsspp-0003: IRJit byPage_ block removal O(P*B) + */ +public class PpssppKernelDedup { + + // --- ppsspp-0001/0002: waitingThreads dedup --- + + /** DEFECTIVE: linear scan before push_back */ + static void addWaiter_defective(List waiters, int threadId) { + if (!waiters.contains(threadId)) { + waiters.add(threadId); + } + } + + /** FIXED: hash set for O(1) dedup */ + static void addWaiter_fixed(List waiters, Set waiterSet, int threadId) { + if (waiterSet.add(threadId)) { + waiters.add(threadId); + } + } + + // --- ppsspp-0003: byPage block removal --- + + /** DEFECTIVE: linear scan for removal */ + static boolean removeBlock_defective(List pageBlocks, int blockIndex) { + int idx = pageBlocks.indexOf(blockIndex); + if (idx >= 0) { + pageBlocks.remove(idx); + return true; + } + return false; + } + + /** FIXED: set-based removal */ + static boolean removeBlock_fixed(Set pageBlockSet, int blockIndex) { + return pageBlockSet.remove(blockIndex); + } + + public static void main(String[] args) { + int passed = 0; + int failed = 0; + + // --- Test 1: ppsspp-0001 correctness --- + { + List defect = new ArrayList<>(); + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + for (int i = 0; i < 50; i++) { + addWaiter_defective(defect, i); + addWaiter_defective(defect, i); // dup from tight-loop timeout + addWaiter_fixed(fixed, fixedSet, i); + addWaiter_fixed(fixed, fixedSet, i); + } + boolean ok = defect.size() == 50 && fixed.size() == 50; + System.out.println((ok ? "PASS" : "FAIL") + " ppsspp-0001 correctness: thread waiter dedup"); + if (ok) passed++; else failed++; + } + + // --- Test 2: ppsspp-0001 performance --- + { + int N = 5000; + List defect = new ArrayList<>(); + long t0 = System.nanoTime(); + for (int i = 0; i < N; i++) addWaiter_defective(defect, i); + long defectNs = System.nanoTime() - t0; + + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + t0 = System.nanoTime(); + for (int i = 0; i < N; i++) addWaiter_fixed(fixed, fixedSet, i); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s ppsspp-0001 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + // --- Test 3: ppsspp-0002 correctness (same pattern, sema context) --- + { + List defect = new ArrayList<>(); + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + // Simulate tight-loop timeout adding same threads repeatedly + for (int round = 0; round < 10; round++) { + for (int tid = 0; tid < 20; tid++) { + addWaiter_defective(defect, tid); + addWaiter_fixed(fixed, fixedSet, tid); + } + } + boolean ok = defect.size() == 20 && fixed.size() == 20; + System.out.println((ok ? "PASS" : "FAIL") + " ppsspp-0002 correctness: sema waiter dedup"); + if (ok) passed++; else failed++; + } + + // --- Test 4: ppsspp-0002 performance (many unique waiters) --- + { + int N = 5000; + List defect = new ArrayList<>(); + long t0 = System.nanoTime(); + for (int tid = 0; tid < N; tid++) { + addWaiter_defective(defect, tid); + } + long defectNs = System.nanoTime() - t0; + + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + t0 = System.nanoTime(); + for (int tid = 0; tid < N; tid++) { + addWaiter_fixed(fixed, fixedSet, tid); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s ppsspp-0002 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + // --- Test 5: ppsspp-0003 correctness --- + { + List defectPage = new ArrayList<>(); + Set fixedPage = new HashSet<>(); + for (int i = 0; i < 100; i++) { + defectPage.add(i); + fixedPage.add(i); + } + boolean d = removeBlock_defective(defectPage, 50); + boolean f = removeBlock_fixed(fixedPage, 50); + boolean ok = d && f && defectPage.size() == 99 && fixedPage.size() == 99; + System.out.println((ok ? "PASS" : "FAIL") + " ppsspp-0003 correctness: byPage block removal"); + if (ok) passed++; else failed++; + } + + // --- Test 6: ppsspp-0003 performance --- + { + int N = 5000; + List defectPage = new ArrayList<>(); + Set fixedPage = new HashSet<>(); + for (int i = 0; i < N; i++) { + defectPage.add(i); + fixedPage.add(i); + } + + long t0 = System.nanoTime(); + for (int i = N - 1; i >= 0; i--) { + removeBlock_defective(defectPage, i); + } + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = N - 1; i >= 0; i--) { + removeBlock_fixed(fixedPage, i); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s ppsspp-0003 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + System.out.printf("%n%d/%d tests passed%n", passed, passed + failed); + if (failed > 0) System.exit(1); + } +} diff --git a/defects/rpcs3/patch/rpcs3-0001-spu-recompiler-preds-vector-dedup.patch b/defects/rpcs3/patch/rpcs3-0001-spu-recompiler-preds-vector-dedup.patch new file mode 100644 index 000000000..0b5defe92 --- /dev/null +++ b/defects/rpcs3/patch/rpcs3-0001-spu-recompiler-preds-vector-dedup.patch @@ -0,0 +1,20 @@ +--- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp ++++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +@@ -2899,4 +2899,5 @@ + // Add predecessor +- if (std::find(m_preds[target].begin(), m_preds[target].end(), pos) == m_preds[target].end()) ++ auto& pred_set = m_pred_sets[target]; ++ if (pred_set.insert(pos).second) + { + m_preds[target].push_back(pos); + } +# +# CWE-407: SPU recompiler add_block scans m_preds[target] vector with +# std::find for every predecessor insertion — O(P) per edge where P = +# predecessors of target block. Called for every branch/target in SPU +# program analysis. For programs with many converging blocks, this is +# O(E×P) where E = total edges. +# Fix: maintain a parallel std::unordered_set per target for O(1) +# dedup, keeping the vector for ordered iteration. +# Severity: MEDIUM — recompilation is amortized but repeated per SPU +# program; large SPU kernels can have hundreds of blocks. diff --git a/defects/rpcs3/patch/rpcs3-0002-spu-recompiler-calls-vector-dedup.patch b/defects/rpcs3/patch/rpcs3-0002-spu-recompiler-calls-vector-dedup.patch new file mode 100644 index 000000000..cebe543ef --- /dev/null +++ b/defects/rpcs3/patch/rpcs3-0002-spu-recompiler-calls-vector-dedup.patch @@ -0,0 +1,17 @@ +--- a/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp ++++ b/rpcs3/Emu/Cell/SPUCommonRecompiler.cpp +@@ -4842,4 +4842,5 @@ +- if (std::find(func.calls.begin(), func.calls.end(), target) == func.calls.end()) ++ // Use unordered_set for O(1) dedup instead of O(C) linear scan ++ if (func.call_set.insert(target).second) + { + func.calls.push_back(target); + } +# +# CWE-407: SPU recompiler fills func.calls with external call targets, +# scanning the vector with std::find before each push_back — O(C) per +# target where C = accumulated calls. Nested inside a loop over all +# basic blocks and their targets: O(B×T×C) total. +# Fix: maintain a parallel unordered_set for O(1) dedup. +# Severity: MEDIUM — same amortized recompilation context as rpcs3-0001; +# functions with many call targets amplify quadratic behavior. diff --git a/defects/rpcs3/patch/rpcs3-0003-savedata-blist-vector-find-in-sort.patch b/defects/rpcs3/patch/rpcs3-0003-savedata-blist-vector-find-in-sort.patch new file mode 100644 index 000000000..39767a96d --- /dev/null +++ b/defects/rpcs3/patch/rpcs3-0003-savedata-blist-vector-find-in-sort.patch @@ -0,0 +1,26 @@ +--- a/rpcs3/Emu/Cell/Modules/cellSaveData.cpp ++++ b/rpcs3/Emu/Cell/Modules/cellSaveData.cpp +@@ -1553,8 +1553,11 @@ ++ // Build index map for O(1) position lookup instead of O(B) std::find per comparison ++ std::unordered_map blist_index; ++ for (usz i = 0; i < blist.size(); i++) ++ if (!blist[i].empty()) blist_index[blist[i]] = i; ++ + std::sort(files_sorted.begin(), files_sorted.end(), [&](const fs::dir_entry& a, const fs::dir_entry& b) -> bool + { +- const auto a_it = std::find(blist.begin(), blist.end(), a.name); +- const auto b_it = std::find(blist.begin(), blist.end(), b.name); ++ const auto a_it = blist_index.find(a.name); ++ const auto b_it = blist_index.find(b.name); + +- if (a_it == blist.end() && b_it == blist.end()) ++ if (a_it == blist_index.end() && b_it == blist_index.end()) + { +# +# CWE-407: cellSaveData sort comparator calls std::find on blist vector +# for BOTH operands of every comparison — O(B) each. std::sort makes +# O(N log N) comparisons, so total is O(N log N × B). With large save +# file lists and many blist entries, this compounds. +# Fix: pre-build an unordered_map for O(1) position lookup. +# Severity: MEDIUM — save data operations are user-visible; large saves +# with many files (e.g., MMO save data) trigger noticeable delays. diff --git a/defects/rpcs3/test/Rpcs3SpuRecompilerTest.java b/defects/rpcs3/test/Rpcs3SpuRecompilerTest.java new file mode 100644 index 000000000..8117681c4 --- /dev/null +++ b/defects/rpcs3/test/Rpcs3SpuRecompilerTest.java @@ -0,0 +1,184 @@ +import java.util.*; + +/** + * Unit tests for RPCS3 CWE-407 defects. + * + * rpcs3-0001: SPU recompiler m_preds vector dedup O(E*P) + * rpcs3-0002: SPU recompiler func.calls vector dedup O(B*T*C) + * rpcs3-0003: cellSaveData blist sort comparator O(N*logN*B) + */ +public class Rpcs3SpuRecompilerTest { + + // --- rpcs3-0001/0002: predecessor/call vector dedup --- + + /** DEFECTIVE: linear scan before push_back */ + static void addPred_defective(List preds, int pos) { + if (!preds.contains(pos)) { + preds.add(pos); + } + } + + /** FIXED: hash set for O(1) dedup */ + static void addPred_fixed(List preds, Set predSet, int pos) { + if (predSet.add(pos)) { + preds.add(pos); + } + } + + // --- rpcs3-0003: blist sort comparator --- + + /** DEFECTIVE: linear find in comparator */ + static void sortFiles_defective(List files, List blist) { + files.sort((a, b) -> { + int ai = blist.indexOf(a); + int bi = blist.indexOf(b); + if (ai == -1 && bi == -1) return a.compareTo(b); + if (ai == -1) return 1; + if (bi == -1) return -1; + return Integer.compare(ai, bi); + }); + } + + /** FIXED: pre-built index map */ + static void sortFiles_fixed(List files, List blist) { + Map index = new HashMap<>(); + for (int i = 0; i < blist.size(); i++) { + if (!blist.get(i).isEmpty()) index.put(blist.get(i), i); + } + files.sort((a, b) -> { + Integer ai = index.get(a); + Integer bi = index.get(b); + if (ai == null && bi == null) return a.compareTo(b); + if (ai == null) return 1; + if (bi == null) return -1; + return Integer.compare(ai, bi); + }); + } + + public static void main(String[] args) { + int passed = 0; + int failed = 0; + + // --- Test 1: rpcs3-0001 correctness --- + { + List defect = new ArrayList<>(); + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + for (int i = 0; i < 100; i++) { + addPred_defective(defect, i); + addPred_defective(defect, i); // duplicate + addPred_fixed(fixed, fixedSet, i); + addPred_fixed(fixed, fixedSet, i); // duplicate + } + boolean ok = defect.size() == 100 && fixed.size() == 100 && defect.equals(fixed); + System.out.println((ok ? "PASS" : "FAIL") + " rpcs3-0001 correctness: predecessor dedup"); + if (ok) passed++; else failed++; + } + + // --- Test 2: rpcs3-0001 performance --- + { + int N = 5000; + List defect = new ArrayList<>(); + long t0 = System.nanoTime(); + for (int i = 0; i < N; i++) addPred_defective(defect, i); + long defectNs = System.nanoTime() - t0; + + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + t0 = System.nanoTime(); + for (int i = 0; i < N; i++) addPred_fixed(fixed, fixedSet, i); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 5.0; + System.out.printf("%s rpcs3-0001 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + // --- Test 3: rpcs3-0002 correctness (same pattern, different context) --- + { + List defect = new ArrayList<>(); + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + int[] targets = {100, 200, 300, 100, 200, 400}; + for (int t : targets) { + addPred_defective(defect, t); + addPred_fixed(fixed, fixedSet, t); + } + boolean ok = defect.size() == 4 && fixed.size() == 4; + System.out.println((ok ? "PASS" : "FAIL") + " rpcs3-0002 correctness: call target dedup"); + if (ok) passed++; else failed++; + } + + // --- Test 4: rpcs3-0003 correctness --- + { + List blist = new ArrayList<>(Arrays.asList("icon0.png", "param.sfo", "data.bin")); + List files1 = new ArrayList<>(Arrays.asList("data.bin", "extra.txt", "icon0.png", "param.sfo", "readme.txt")); + List files2 = new ArrayList<>(files1); + sortFiles_defective(files1, blist); + sortFiles_fixed(files2, blist); + boolean ok = files1.equals(files2); + System.out.println((ok ? "PASS" : "FAIL") + " rpcs3-0003 correctness: blist sort order " + files1 + " == " + files2); + if (ok) passed++; else failed++; + } + + // --- Test 5: rpcs3-0003 performance --- + { + int N = 3000; + List blist = new ArrayList<>(); + for (int i = 0; i < N; i++) blist.add("file_" + String.format("%05d", i)); + + List files1 = new ArrayList<>(blist); + Collections.shuffle(files1, new Random(42)); + List files2 = new ArrayList<>(files1); + + long t0 = System.nanoTime(); + sortFiles_defective(files1, blist); + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + sortFiles_fixed(files2, blist); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 3.0; + System.out.printf("%s rpcs3-0003 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + // --- Test 6: rpcs3-0002 performance --- + { + int N = 5000; + List defect = new ArrayList<>(); + long t0 = System.nanoTime(); + // Simulate: for each BB target, dedup into calls list + for (int bb = 0; bb < 50; bb++) { + for (int t = 0; t < N / 50; t++) { + addPred_defective(defect, t * 4); // same targets from different BBs + } + } + long defectNs = System.nanoTime() - t0; + + List fixed = new ArrayList<>(); + Set fixedSet = new HashSet<>(); + t0 = System.nanoTime(); + for (int bb = 0; bb < 50; bb++) { + for (int t = 0; t < N / 50; t++) { + addPred_fixed(fixed, fixedSet, t * 4); + } + } + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / Math.max(fixedNs, 1); + boolean ok = ratio > 3.0; + System.out.printf("%s rpcs3-0002 performance: N=%d defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + ok ? "PASS" : "FAIL", N, defectNs / 1e6, fixedNs / 1e6, ratio); + if (ok) passed++; else failed++; + } + + System.out.printf("%n%d/%d tests passed%n", passed, passed + failed); + if (failed > 0) System.exit(1); + } +}