java-topology/defects/proton/test/ProtonTest.java
russell@unturf.com 2f03245f7b proton: 2 CWE-407 defects — find_iface_constructor O(C) linear scan + merge_user_dir list explosion
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.
2026-03-30 17:38:41 -04:00

311 lines
12 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}