From e781dbe9e97c95a1fcf3d76c3bd3c46ebe5fe538 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 17:37:01 -0400 Subject: [PATCH] pcsx2: 1 CWE-407 defect (pcsx2-0003), MOAD 0002-0005 CLEAN --- ...x2-0003-achievements-badge-paths-map.patch | 27 +++++ .../test/AchievementBadgePathsTest.class | Bin 0 -> 4454 bytes .../test/AchievementBadgePathsTest.java | 114 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 defects/pcsx2-0003/patch/pcsx2-0003-achievements-badge-paths-map.patch create mode 100644 defects/pcsx2-0003/test/AchievementBadgePathsTest.class create mode 100644 defects/pcsx2-0003/test/AchievementBadgePathsTest.java diff --git a/defects/pcsx2-0003/patch/pcsx2-0003-achievements-badge-paths-map.patch b/defects/pcsx2-0003/patch/pcsx2-0003-achievements-badge-paths-map.patch new file mode 100644 index 000000000..a2e48565b --- /dev/null +++ b/defects/pcsx2-0003/patch/pcsx2-0003-achievements-badge-paths-map.patch @@ -0,0 +1,27 @@ +# UNDF: UNDF-2026-XXXXXXXXX +--- a/pcsx2/Achievements.cpp ++++ b/pcsx2/Achievements.cpp +@@ -219,7 +219,8 @@ namespace + { +- static std::vector> s_achievement_badge_paths; ++ // Keyed by rc_client_achievement_t pointer — use unordered_map for O(1) lookup ++ // instead of O(A) std::find_if scan per DrawAchievement call. ++ static std::unordered_map s_achievement_badge_paths; + } + +@@ -2774,10 +2775,10 @@ void Achievements::DrawAchievement(const rc_client_achievement_t* cheevo) + std::string* badge_path; +- if (const auto badge_it = std::find_if( +- s_achievement_badge_paths.begin(), s_achievement_badge_paths.end(), [cheevo](const auto& it) { return (it.first == cheevo); }); +- badge_it != s_achievement_badge_paths.end()) ++ if (const auto badge_it = s_achievement_badge_paths.find(cheevo); ++ badge_it != s_achievement_badge_paths.end()) + { + badge_path = &badge_it->second; + } + else + { + std::string new_badge_path = Achievements::GetAchievementBadgePath(cheevo, cheevo->state); +- badge_path = &s_achievement_badge_paths.emplace_back(cheevo, std::move(new_badge_path)).second; ++ badge_path = &s_achievement_badge_paths.emplace(cheevo, std::move(new_badge_path)).first->second; + } diff --git a/defects/pcsx2-0003/test/AchievementBadgePathsTest.class b/defects/pcsx2-0003/test/AchievementBadgePathsTest.class new file mode 100644 index 0000000000000000000000000000000000000000..4c10f21187b875d4d2e8f8ccf2c3bf56bc1e6b33 GIT binary patch literal 4454 zcmcIo>sM6Q75|-i-FaLD5k^K(t`;l;LNqBT14IOk5MC`-1QL_W%muE@+&j*_gP63d zCegHYHAx?7O=;7Ieb{#s)5=3*(zI#QH0{cl{sH~kwfdo1U8`%kV(dP5F3cU7xbmfb zn0p@k?6dc8|Mor`-~M3b27oU7EeIFf0z4wT@G+E}l;`B;gse?B4^5s_VkU#HUDZ^x zlfhlL>sSbW1Ox;{gdj3(u*5T_nrQA<4HEXNrjnLTJ-WKby?S(*E=<3 zu_KFi=0Q4Qrd4gal#s6zu?zJKo0Iaa(yePT*&J2P85$X;teFNwdA@Dgqn34zB6g#R zOi5+PlzjzE`AEj`P~IbA5B8EOIUeU%7C1Ow3Wobcd;%>DUOAOYoMEV5#or43R7Q)L zs;)I3wg;A(`$X(V8$)qSPm-ld?^M^Mp=e}scY%8xR9UaCAhhiizg%#-(rNh&=U@;~ zbaFP|Pi(2{-E~Yr7sJM!J$eLmr~()n6vllQsRQn#_i#FF*$j5hgMeDxGIvdDcr+Zyu)9Hpz8G zPDcGW%`nxIPZ(*EGfGP6`fMg8NnIVaaY@ec&5$Hou#BT&++tTpZPT8qBu%Gy$k%36 zU{&3k&DUx{oWWxP9_Q8WGpj_m;?=8}%CwT^l{gPJdp+vwYC3n8Y7v4sizfs;$yGd+ zheMB^A%gJG^f-f;U5*BE9#0GSyofL0i>uj~4+W7a#etz>+wRPInbwrj__w+Kia8nV}6&w;m@(Kn4e0@(vz7lTr$ZJ42P9tQNy; z72R?oF+yEnlw5ivh~MK60^StyNBoIl$2yn#tw58#^3&6&i~)NyZWBDnMKqI zP2RSJn=>QsgoHut)ifn-RS1P1*tZ#K*I`m9dIH`ddf#1?YHc>u*iP(qQ9)xR75vqw zk)N74YVhgzMVf7;FQc!wei7_tE{N~YFW&~W<$m4`kpeFIJ6aX!`*{5_1co+m#Jh|l zM#Zm@k2W#9j?Hbph|k?DJ->jmhKSG8yoAUys$H~I`_kpx??ycChngllj}D!0^)`gP zv@5OP;imOX_0?p~3hw3Bw7M;e zY1xTKgRS5aDWD#M_BbvJ1SiL(=Nh78TNcsLe+>u57IAREb3Hibb`K2(T0QO-U%79~ zB~&*wmix}{UPAY1*we5Ks*UI6R?j&fyL|f}Vb3o~Pu9Vm2+(*B{qChW+=~dc-BoCz z-mDeP^k_GppggVj9^xcYsHmt5&2+>{aCRrE2kMQH{Ev`wL z6%%YvY{Xmb9FawQYQP3$&<3QoV1w!G# zg>dMCzQ5=S(xr3$SK%6Slhpjkn5Ufd)7MKPW%#VVgwKtP`AB$eB2*Od#j7|sYJpN& zQB;Nt7hiwfMkzLE>%(?kT|5VcE3C?9%JhCT}9gV>1yqS7EyX^3`* ziO>Vks7josI`I@n@eGdR7c{>@P+!NxQ~*xkO&b4&V0#xa{2g)p6AIp=WcV+&+##rJ z6J}Wz608nMc8tE`(Ahjv>?}@M#OX%SZF9{EF~!(|s@FH`e^O*6*VAyNtI; OGDuQ?Cba&F$o~KXE|eGm literal 0 HcmV?d00001 diff --git a/defects/pcsx2-0003/test/AchievementBadgePathsTest.java b/defects/pcsx2-0003/test/AchievementBadgePathsTest.java new file mode 100644 index 000000000..767e0f5fc --- /dev/null +++ b/defects/pcsx2-0003/test/AchievementBadgePathsTest.java @@ -0,0 +1,114 @@ +import java.util.*; + +/** + * Unit test for PCSX2 pcsx2-0003: + * Achievements::DrawAchievement uses std::find_if on a std::vector of + * (pointer, path) pairs to look up our cached badge path per achievement. + * Called once per achievement per ImGui frame while our achievements window + * is open. A game with A achievements gives O(A^2) total scan work per frame. + * + * Modern PS2 titles routinely ship 50-200 RetroAchievements entries. + * + * Defect file: pcsx2/Achievements.cpp line 2777 + * Pattern: std::find_if(s_achievement_badge_paths.begin(), ..., ptr == cheevo) + * Fix: change s_achievement_badge_paths to std::unordered_map<const void*, string> + * and use .find(cheevo) for O(1) lookup. + */ +public class AchievementBadgePathsTest { + + // --- Defective: vector-as-map, O(A) lookup per draw call --- + static String drawAchievementDefective( + List badgePaths, // pair + Map pathStore, + long cheevoId, + int[] pathCounter) { + + for (long[] entry : badgePaths) { + if (entry[0] == cheevoId) { + return pathStore.get(entry[1]); + } + } + // Not found: add new entry + long idx = pathCounter[0]++; + pathStore.put(idx, "badge_" + cheevoId + ".png"); + badgePaths.add(new long[]{cheevoId, idx}); + return pathStore.get(idx); + } + + // --- Fixed: unordered_map, O(1) lookup per draw call --- + static String drawAchievementFixed( + Map badgeMap, + long cheevoId) { + + return badgeMap.computeIfAbsent(cheevoId, + id -> "badge_" + id + ".png"); + } + + // Simulate one frame: draw all achievements + static long frameDefective(int numAchievements) { + List badgePaths = new ArrayList<>(numAchievements); + Map pathStore = new HashMap<>(); + int[] counter = {0}; + + long ops = 0; + for (int i = 0; i < numAchievements; i++) { + drawAchievementDefective(badgePaths, pathStore, (long) i, counter); + ops += badgePaths.size(); // track scan length + } + return ops; + } + + static void frameFixed(int numAchievements) { + Map badgeMap = new HashMap<>(numAchievements); + for (int i = 0; i < numAchievements; i++) { + drawAchievementFixed(badgeMap, (long) i); + } + } + + public static void main(String[] args) { + int N = 200; + + // Correctness: both paths must return same badge path + List vecPaths = new ArrayList<>(); + Map pathStore = new HashMap<>(); + int[] counter = {0}; + Map mapPaths = new HashMap<>(); + + for (int i = 0; i < N; i++) { + String defPath = drawAchievementDefective(vecPaths, pathStore, (long) i, counter); + String fixPath = drawAchievementFixed(mapPaths, (long) i); + assert defPath.equals(fixPath) + : "Path mismatch at i=" + i + ": " + defPath + " != " + fixPath; + } + + // Second pass: lookup must return same path (not duplicate) + for (int i = 0; i < N; i++) { + String defPath = drawAchievementDefective(vecPaths, pathStore, (long) i, counter); + String fixPath = drawAchievementFixed(mapPaths, (long) i); + assert defPath.equals(fixPath) : "Cache miss mismatch at i=" + i; + } + + // Warm up + for (int i = 0; i < 50; i++) { + frameDefective(N); + frameFixed(N); + } + + // Benchmark: simulate 500 frames + int FRAMES = 500; + long t0 = System.nanoTime(); + for (int f = 0; f < FRAMES; f++) frameDefective(N); + long defectNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int f = 0; f < FRAMES; f++) frameFixed(N); + long fixedNs = System.nanoTime() - t0; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("DrawAchievement badge lookup A=%d achievements %d frames defect=%.1fms fixed=%.1fms ratio=%.1fx%n", + N, FRAMES, defectNs / 1e6, fixedNs / 1e6, ratio); + + assert ratio > 2.0 : "Expected >2x speedup, got " + ratio; + System.out.println("PASS"); + } +}