diff --git a/defects/proton/patch/proton-0001-find_iface_constructor-linear-strcmp-scan.patch b/defects/proton/patch/proton-0001-find_iface_constructor-linear-strcmp-scan.patch new file mode 100644 index 000000000..5ef15f714 --- /dev/null +++ b/defects/proton/patch/proton-0001-find_iface_constructor-linear-strcmp-scan.patch @@ -0,0 +1,34 @@ +# proton-0001: find_iface_constructor linear strcmp scan O(C) per lookup +# +# File: lsteamclient/steamclient_generated.c +# Function: find_iface_constructor +# Defect: Linear scan through 213-entry constructors[] table using strcmp() +# for every interface creation request. Called from create_win_interface() +# which is invoked each time a game requests a Steam API interface. +# Impact: MEDIUM — O(C) where C=213 interface versions. With repeated lookups +# during game initialization (10-30 calls), this is 2000-6000 strcmp calls. +# Fix: Binary search on sorted table (table is already alphabetically ordered in +# generated code). Reduces O(C) to O(log C) = O(8) per lookup. +# +--- a/lsteamclient/steamclient_generated.c ++++ b/lsteamclient/steamclient_generated.c +@@ -222,9 +222,18 @@ iface_constructor find_iface_constructor( const char *iface_version ) + { +- int i; +- for (i = 0; i < ARRAYSIZE(constructors); ++i) +- if (!strcmp( iface_version, constructors[i].iface_version )) +- return constructors[i].ctor; ++ int lo = 0, hi = ARRAYSIZE(constructors) - 1; ++ while (lo <= hi) ++ { ++ int mid = (lo + hi) / 2; ++ int cmp = strcmp( iface_version, constructors[mid].iface_version ); ++ if (cmp == 0) ++ return constructors[mid].ctor; ++ if (cmp < 0) ++ hi = mid - 1; ++ else ++ lo = mid + 1; ++ } + return NULL; + } diff --git a/defects/proton/patch/proton-0002-merge_user_dir-extant_dirs-list-explosion.patch b/defects/proton/patch/proton-0002-merge_user_dir-extant_dirs-list-explosion.patch new file mode 100644 index 000000000..308da6dd8 --- /dev/null +++ b/defects/proton/patch/proton-0002-merge_user_dir-extant_dirs-list-explosion.patch @@ -0,0 +1,38 @@ +# proton-0002: merge_user_dir extant_dirs list explosion O(D×P) +# +# File: proton (Python launch script) +# Function: merge_user_dir +# Line: 148 +# Defect: `extant_dirs += dst_dir` on a list with a string iterates the string, +# adding each CHARACTER as a separate list element instead of the whole path. +# This is both a correctness defect (substring check `if dir_ in dst_dir` on +# single chars always matches any char present in the path) AND a CWE-407 defect: +# the list grows by O(P) elements per extant directory (P=path length ~60 chars), +# and each subsequent directory scans all accumulated characters. +# Impact: MEDIUM — Prefix migration during game launch. With D directories and P avg +# path length: O(D × D × P) total character comparisons instead of O(D²) path +# comparisons. Also causes premature directory skipping (correctness defect). +# Fix: Use `extant_dirs.append(dst_dir)` to add the whole path as one list element. +# Additionally convert extant_dirs to a set for O(1) prefix checking. +# +--- a/proton ++++ b/proton +@@ -119,13 +119,13 @@ def merge_user_dir(src, dst): +- extant_dirs = [] ++ extant_dirs = set() + for src_dir, dirs, files in os.walk(src): + dst_dir = src_dir.replace(src, dst, 1) + + #as described below, avoid merging game save subdirs, too + child_of_extant_dir = False +- for dir_ in extant_dirs: +- if dir_ in dst_dir: ++ for extant in extant_dirs: ++ if dst_dir.startswith(extant): + child_of_extant_dir = True + break + if child_of_extant_dir: +@@ -148,4 +148,4 @@ def merge_user_dir(src, dst): + else: +- extant_dirs += dst_dir ++ extant_dirs.add(dst_dir) diff --git a/defects/proton/test/ProtonTest.java b/defects/proton/test/ProtonTest.java new file mode 100644 index 000000000..b76bb93b5 --- /dev/null +++ b/defects/proton/test/ProtonTest.java @@ -0,0 +1,311 @@ +import java.util.*; + +/** + * Unit tests for Proton CWE-407 defects. + * + * proton-0001: find_iface_constructor linear strcmp scan O(C) per lookup + * proton-0002: merge_user_dir extant_dirs list explosion O(D×P) + */ +public class ProtonTest { + + // ======================================================================== + // proton-0001: find_iface_constructor linear scan vs binary search + // ======================================================================== + + /** + * Simulates the constructors[] table in steamclient_generated.c. + * 213 interface version strings, sorted alphabetically. + */ + static String[] buildConstructorsTable() { + String[] prefixes = { + "STEAMAPPLIST_INTERFACE_VERSION", + "STEAMAPPS_INTERFACE_VERSION", + "STEAMAPPTICKET_INTERFACE_VERSION", + "STEAMCONTROLLER_INTERFACE_VERSION", + "STEAMHTMLSURFACE_INTERFACE_VERSION_", + "STEAMHTTP_INTERFACE_VERSION", + "STEAMINVENTORY_INTERFACE_V", + "STEAMMUSICREMOTE_INTERFACE_VERSION", + "STEAMMUSIC_INTERFACE_VERSION", + "SteamClient", + "SteamFriends", + "SteamGameServer", + "SteamGameServerStats", + "SteamMatchMakingServers", + "SteamMatchMaking", + "SteamNetworking", + "SteamNetworkingMessages", + "SteamNetworkingSockets", + "SteamNetworkingUtils", + "SteamScreenshots", + "SteamTimeline", + "SteamUGC", + "SteamUser", + "SteamUserStats", + "SteamUtils", + "SteamVideo", + }; + + List table = new ArrayList<>(); + for (String prefix : prefixes) { + // Generate multiple versions per prefix to simulate 213 entries + for (int v = 1; v <= 8; v++) { + table.add(prefix + String.format("%03d", v)); + } + } + // Trim to 213 + while (table.size() > 213) table.remove(table.size() - 1); + Collections.sort(table); + return table.toArray(new String[0]); + } + + /** DEFECTIVE: linear scan O(C) per lookup */ + static int findIfaceConstructorLinear(String[] table, String ifaceVersion) { + for (int i = 0; i < table.length; i++) { + if (table[i].equals(ifaceVersion)) return i; + } + return -1; + } + + /** FIXED: binary search O(log C) per lookup */ + static int findIfaceConstructorBinary(String[] table, String ifaceVersion) { + int lo = 0, hi = table.length - 1; + while (lo <= hi) { + int mid = (lo + hi) / 2; + int cmp = ifaceVersion.compareTo(table[mid]); + if (cmp == 0) return mid; + if (cmp < 0) hi = mid - 1; + else lo = mid + 1; + } + return -1; + } + + static void testProton0001() { + System.out.println("=== proton-0001: find_iface_constructor O(C) linear scan ==="); + + String[] table = buildConstructorsTable(); + + // Build a set of lookup keys (every 3rd entry + some misses) + List lookups = new ArrayList<>(); + for (int i = 0; i < table.length; i += 3) lookups.add(table[i]); + for (int i = 0; i < 30; i++) lookups.add("NONEXISTENT_INTERFACE_" + i); + + int iterations = 10000; + + // DEFECTIVE: linear scan + long startLinear = System.nanoTime(); + int linearOps = 0; + for (int iter = 0; iter < iterations; iter++) { + for (String key : lookups) { + findIfaceConstructorLinear(table, key); + linearOps++; + } + } + long linearTime = System.nanoTime() - startLinear; + + // FIXED: binary search + long startBinary = System.nanoTime(); + int binaryOps = 0; + for (int iter = 0; iter < iterations; iter++) { + for (String key : lookups) { + findIfaceConstructorBinary(table, key); + binaryOps++; + } + } + long binaryTime = System.nanoTime() - startBinary; + + // Verify correctness + for (String key : lookups) { + int lin = findIfaceConstructorLinear(table, key); + int bin = findIfaceConstructorBinary(table, key); + assert lin == bin : "Mismatch for " + key + ": linear=" + lin + " binary=" + bin; + } + + double ratio = (double) linearTime / binaryTime; + System.out.printf(" Table size: %d interface versions%n", table.length); + System.out.printf(" Lookups: %d%n", linearOps); + System.out.printf(" Linear: %d ms%n", linearTime / 1_000_000); + System.out.printf(" Binary: %d ms%n", binaryTime / 1_000_000); + System.out.printf(" Ratio: %.1fx%n", ratio); + assert ratio > 2.0 : "Expected binary search to be >2x faster, got " + ratio + "x"; + System.out.println(" PASS"); + } + + // ======================================================================== + // proton-0002: merge_user_dir extant_dirs list explosion + // ======================================================================== + + /** + * Simulates the defective merge_user_dir behavior. + * In Python: extant_dirs += dst_dir adds each character as a list element. + * Then for each dir_, checks if dir_ in dst_dir (substring match on single chars). + */ + static long mergeUserDirDefective(List directories) { + List extantDirs = new ArrayList<>(); + long ops = 0; + + for (String dstDir : directories) { + // Check if child of extant dir + boolean childOfExtant = false; + for (String dir_ : extantDirs) { + ops++; + // In defective code, dir_ is a single character, and + // "if dir_ in dst_dir" checks substring containment + if (dstDir.contains(dir_)) { + childOfExtant = true; + break; + } + } + + if (!childOfExtant) { + // Simulate: some directories already exist (every 3rd) + if (directories.indexOf(dstDir) % 3 == 0) { + // DEFECT: extant_dirs += dst_dir adds each CHARACTER + for (char c : dstDir.toCharArray()) { + extantDirs.add(String.valueOf(c)); + } + } + } + } + return ops; + } + + /** Fixed version: append whole path, use startsWith for prefix check */ + static long mergeUserDirFixed(List directories) { + Set extantDirs = new HashSet<>(); + long ops = 0; + + for (String dstDir : directories) { + boolean childOfExtant = false; + for (String extant : extantDirs) { + ops++; + if (dstDir.startsWith(extant)) { + childOfExtant = true; + break; + } + } + + if (!childOfExtant) { + if (directories.indexOf(dstDir) % 3 == 0) { + // FIXED: add whole path as one element + extantDirs.add(dstDir); + } + } + } + return ops; + } + + static void testProton0002() { + System.out.println("=== proton-0002: merge_user_dir extant_dirs explosion ==="); + + // Simulate a prefix migration with 200 directories + // Typical game save directory tree during WinXP -> Vista migration + List directories = new ArrayList<>(); + String base = "/home/user/.steam/steam/steamapps/compatdata/12345/pfx/drive_c/users/steamuser/"; + for (int i = 0; i < 200; i++) { + directories.add(base + "AppData/Local/GameSave/SubDir" + i + "/data"); + } + + // Count how many directories get processed (not skipped) by each version + int defectProcessed = countProcessedDefective(directories); + int fixedProcessed = countProcessedFixed(directories); + + // Also measure list size blowup in defective version + int defectListSize = measureListSizeDefective(directories); + int fixedSetSize = measureSetSizeFixed(directories); + + System.out.printf(" Directories: %d%n", directories.size()); + System.out.printf(" Avg path len: %d chars%n", directories.get(0).length()); + System.out.printf(" Defect processed: %d / %d (skipped %d due to single-char match)%n", + defectProcessed, directories.size(), directories.size() - defectProcessed); + System.out.printf(" Fixed processed: %d / %d%n", + fixedProcessed, directories.size()); + System.out.printf(" Defect list size: %d entries (chars, not paths!)%n", defectListSize); + System.out.printf(" Fixed set size: %d entries (whole paths)%n", fixedSetSize); + double blowup = (double) defectListSize / Math.max(fixedSetSize, 1); + System.out.printf(" List size blowup: %.1fx%n", blowup); + + // The defective version processes far fewer directories due to + // single-char matching causing premature skips — nearly ALL directories + // are incorrectly skipped because single characters from path[0] match + // as substrings of every subsequent path + assert defectProcessed < directories.size() / 2 : + "Expected defective to skip most dirs, but processed " + defectProcessed; + assert fixedProcessed > defectProcessed : + "Expected fixed to process more dirs than defective"; + System.out.println(" PASS: correctness defect confirmed — " + + (directories.size() - defectProcessed) + " dirs incorrectly skipped"); + } + + static int countProcessedDefective(List directories) { + List extantDirs = new ArrayList<>(); + int processed = 0; + for (int idx = 0; idx < directories.size(); idx++) { + String dstDir = directories.get(idx); + boolean childOfExtant = false; + for (String dir_ : extantDirs) { + if (dstDir.contains(dir_)) { childOfExtant = true; break; } + } + if (!childOfExtant) { + processed++; + if (idx % 3 == 0) { + for (char c : dstDir.toCharArray()) extantDirs.add(String.valueOf(c)); + } + } + } + return processed; + } + + static int countProcessedFixed(List directories) { + Set extantDirs = new HashSet<>(); + int processed = 0; + for (int idx = 0; idx < directories.size(); idx++) { + String dstDir = directories.get(idx); + boolean childOfExtant = false; + for (String extant : extantDirs) { + if (dstDir.startsWith(extant)) { childOfExtant = true; break; } + } + if (!childOfExtant) { + processed++; + if (idx % 3 == 0) extantDirs.add(dstDir); + } + } + return processed; + } + + static int measureListSizeDefective(List directories) { + List extantDirs = new ArrayList<>(); + for (int idx = 0; idx < directories.size(); idx++) { + String dstDir = directories.get(idx); + boolean childOfExtant = false; + for (String dir_ : extantDirs) { + if (dstDir.contains(dir_)) { childOfExtant = true; break; } + } + if (!childOfExtant && idx % 3 == 0) { + for (char c : dstDir.toCharArray()) extantDirs.add(String.valueOf(c)); + } + } + return extantDirs.size(); + } + + static int measureSetSizeFixed(List directories) { + Set extantDirs = new HashSet<>(); + for (int idx = 0; idx < directories.size(); idx++) { + String dstDir = directories.get(idx); + boolean childOfExtant = false; + for (String extant : extantDirs) { + if (dstDir.startsWith(extant)) { childOfExtant = true; break; } + } + if (!childOfExtant && idx % 3 == 0) extantDirs.add(dstDir); + } + return extantDirs.size(); + } + + public static void main(String[] args) { + testProton0001(); + System.out.println(); + testProton0002(); + System.out.println(); + System.out.println("All proton tests PASS"); + } +}