196 lines
7.7 KiB
Java
196 lines
7.7 KiB
Java
package unit;
|
||
|
||
import java.util.*;
|
||
|
||
/**
|
||
* CWE-407 unit test: systemd-0002
|
||
*
|
||
* unit_file_get_list() calls strv_contains(states, unit_file_state_to_string(state))
|
||
* for every unit file found on disk. strv_contains is O(S) linear scan over the
|
||
* states filter array. Total cost: O(U × S) where U = unit files, S = state filters.
|
||
*
|
||
* Fix: build a HashSet from the states array before the loop — O(1) lookup per unit.
|
||
*/
|
||
public class SystemdUnitFileGetListTest {
|
||
|
||
// Enum representing systemd UnitFileState values
|
||
enum UnitFileState {
|
||
ENABLED, DISABLED, STATIC, MASKED, LINKED, INDIRECT,
|
||
ENABLED_RUNTIME, LINKED_RUNTIME, ALIAS, GENERATED,
|
||
TRANSIENT, BAD
|
||
}
|
||
|
||
static UnitFileState[] ALL_STATES = UnitFileState.values();
|
||
|
||
// -----------------------------------------------------------------------
|
||
// SLOW path: simulates the defective strv_contains check per unit file
|
||
// -----------------------------------------------------------------------
|
||
static long slowFilter(String[] unitFiles, String[] states) {
|
||
long ops = 0;
|
||
List<String> matched = new ArrayList<>();
|
||
for (String unit : unitFiles) {
|
||
// Assign a state (deterministic from unit name hash)
|
||
UnitFileState state = ALL_STATES[Math.abs(unit.hashCode()) % ALL_STATES.length];
|
||
String stateStr = state.name().toLowerCase();
|
||
|
||
// O(S) linear scan — strv_contains equivalent
|
||
boolean found = false;
|
||
for (String s : states) {
|
||
ops++;
|
||
if (stateStr.equals(s)) {
|
||
found = true;
|
||
break;
|
||
}
|
||
}
|
||
if (found) matched.add(unit);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// FAST path: HashSet built once, O(1) lookup per unit
|
||
// -----------------------------------------------------------------------
|
||
static long fastFilter(String[] unitFiles, String[] states) {
|
||
long ops = 0;
|
||
Set<String> stateSet = new HashSet<>(Arrays.asList(states));
|
||
List<String> matched = new ArrayList<>();
|
||
for (String unit : unitFiles) {
|
||
UnitFileState state = ALL_STATES[Math.abs(unit.hashCode()) % ALL_STATES.length];
|
||
String stateStr = state.name().toLowerCase();
|
||
ops++; // single hash lookup
|
||
if (stateSet.contains(stateStr)) matched.add(unit);
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Helpers
|
||
// -----------------------------------------------------------------------
|
||
static String[] makeUnits(int n) {
|
||
String[] a = new String[n];
|
||
for (int i = 0; i < n; i++) a[i] = "unit-" + i + ".service";
|
||
return a;
|
||
}
|
||
|
||
static String[] makeStates(int s) {
|
||
String[] a = new String[s];
|
||
String[] names = {"enabled", "disabled", "static", "masked", "linked"};
|
||
for (int i = 0; i < s; i++) a[i] = names[i % names.length];
|
||
return a;
|
||
}
|
||
|
||
// Count matching results identically for both paths for correctness check
|
||
static Set<String> matchedSet(String[] unitFiles, String[] states) {
|
||
Set<String> stateSet = new HashSet<>(Arrays.asList(states));
|
||
Set<String> matched = new LinkedHashSet<>();
|
||
for (String unit : unitFiles) {
|
||
UnitFileState state = ALL_STATES[Math.abs(unit.hashCode()) % ALL_STATES.length];
|
||
if (stateSet.contains(state.name().toLowerCase())) matched.add(unit);
|
||
}
|
||
return matched;
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Tests
|
||
// -----------------------------------------------------------------------
|
||
static int pass = 0;
|
||
static int fail = 0;
|
||
|
||
static void check(String name, boolean condition) {
|
||
if (condition) {
|
||
System.out.println("PASS: " + name);
|
||
pass++;
|
||
} else {
|
||
System.out.println("FAIL: " + name);
|
||
fail++;
|
||
}
|
||
}
|
||
|
||
// Slow path variant that always scans full list (no early exit) to measure worst case
|
||
static long slowFilterWorstCase(String[] unitFiles, String[] states) {
|
||
long ops = 0;
|
||
for (String unit : unitFiles) {
|
||
// Assign a state that never matches any filter (simulates "bad"/"transient" units)
|
||
String stateStr = "bad"; // last state, never in typical filter lists
|
||
for (String s : states) {
|
||
ops++;
|
||
if (stateStr.equals(s)) break; // never breaks — scans all S
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static long fastFilterWorstCase(String[] unitFiles, String[] states) {
|
||
long ops = 0;
|
||
Set<String> stateSet = new HashSet<>(Arrays.asList(states));
|
||
for (String unit : unitFiles) {
|
||
ops++; // one hash lookup per unit, regardless of match
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static void testCase(int numUnits, int numStates) {
|
||
String[] units = makeUnits(numUnits);
|
||
String[] states = makeStates(numStates);
|
||
|
||
// Worst case: every unit has state NOT in filter, slow path scans all S per unit
|
||
long slowOps = slowFilterWorstCase(units, states);
|
||
long fastOps = fastFilterWorstCase(units, states);
|
||
|
||
double ratio = (double) slowOps / fastOps;
|
||
System.out.printf(" U=%d S=%d: slowOps=%d fastOps=%d ratio=%.1fx%n",
|
||
numUnits, numStates, slowOps, fastOps, ratio);
|
||
check("U=" + numUnits + " S=" + numStates + ": slowOps == U*S",
|
||
slowOps == (long) numUnits * numStates);
|
||
check("U=" + numUnits + " S=" + numStates + ": fastOps == numUnits", fastOps == numUnits);
|
||
check("U=" + numUnits + " S=" + numStates + ": slowOps/fastOps >= 5x", ratio >= 5.0);
|
||
}
|
||
|
||
static void testCorrectness() {
|
||
// Small case: 6 units, filter for "enabled" and "static"
|
||
String[] units = new String[12];
|
||
for (int i = 0; i < 12; i++) units[i] = "svc-" + i + ".service";
|
||
String[] states = {"enabled", "static"};
|
||
|
||
// Run both paths and compare matched counts
|
||
Set<String> expected = matchedSet(units, states);
|
||
|
||
// Slow path result
|
||
Set<String> slowMatched = new LinkedHashSet<>();
|
||
Set<String> stateSet = new HashSet<>(Arrays.asList(states));
|
||
for (String unit : units) {
|
||
UnitFileState state = ALL_STATES[Math.abs(unit.hashCode()) % ALL_STATES.length];
|
||
String stateStr = state.name().toLowerCase();
|
||
for (String s : states) {
|
||
if (stateStr.equals(s)) { slowMatched.add(unit); break; }
|
||
}
|
||
}
|
||
|
||
check("correctness: slow matches expected", slowMatched.equals(expected));
|
||
|
||
// Fast path result
|
||
Set<String> fastMatched = new LinkedHashSet<>();
|
||
for (String unit : units) {
|
||
UnitFileState state = ALL_STATES[Math.abs(unit.hashCode()) % ALL_STATES.length];
|
||
if (stateSet.contains(state.name().toLowerCase())) fastMatched.add(unit);
|
||
}
|
||
check("correctness: fast matches expected", fastMatched.equals(expected));
|
||
check("correctness: slow equals fast", slowMatched.equals(fastMatched));
|
||
}
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Main
|
||
// -----------------------------------------------------------------------
|
||
public static void main(String[] args) {
|
||
System.out.println("=== systemd-0002: unit_file_get_list states filter O(U*S) → O(U) ===");
|
||
|
||
testCorrectness();
|
||
testCase(1000, 5);
|
||
testCase(2000, 5);
|
||
testCase(5000, 10);
|
||
|
||
System.out.println();
|
||
System.out.printf("%d/%d PASS%n", pass, pass + fail);
|
||
if (fail > 0) System.exit(1);
|
||
}
|
||
}
|