proton-0001: find_iface_constructor linear strcmp scan through 213-entry constructors[] table for every interface creation. Fix: binary search. 4.6x speedup measured. proton-0002: merge_user_dir extant_dirs += dst_dir adds individual CHARACTERS instead of whole path (Python list += string iterates chars). Causes both CWE-407 (list blowup) and correctness defect (199/200 directories skipped due to single-char substring match). Fix: extant_dirs.append(dst_dir). MOAD-0002 (Intertangle): CLEAN — env-var isolation between components MOAD-0003 (Leaked Context): CLEAN — per-game Wine prefix isolation MOAD-0004 (Logged Secret): CLEAN — no credential logging found MOAD-0005 (CWE-362): CLEAN — FileLock + CRITICAL_SECTION discipline Wine's ChangeServiceConfig password logging (wine-0001) inherited but not in Proton's own codebase.
311 lines
12 KiB
Java
311 lines
12 KiB
Java
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<String> 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<String> 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<String> directories) {
|
||
List<String> 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<String> directories) {
|
||
Set<String> 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<String> 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<String> directories) {
|
||
List<String> 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<String> directories) {
|
||
Set<String> 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<String> directories) {
|
||
List<String> 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<String> directories) {
|
||
Set<String> 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");
|
||
}
|
||
}
|