wave17 complete: systemd/emacs/vim/qemu/tcl/kafka-0007/spark-0004 + 559/240

This commit is contained in:
russell@unturf.com 2026-03-27 20:15:47 -04:00
parent 4221966e66
commit cce7ec653a
32 changed files with 3007 additions and 5 deletions

View file

@ -0,0 +1,119 @@
# systemd-0001: strv_extend_strv filter_duplicates O(N²) — CWE-407
## Severity
HIGH
## Location
`src/basic/strv.c` — `strv_extend_strv()` and `strv_extend_strv_consume()`
## Root Cause
`strv_contains(t, *s)` is called inside a loop that iterates over every element
of `b`. `strv_contains` expands to `strv_find()`, which is an O(N) linear scan
of the entire target array `t`. As entries are appended to `t`, each successive
membership check scans a longer array. Total cost: O(|b| × |t|) = O(N²).
## Defective Code
```c
// src/basic/strv.c strv_extend_strv()
STRV_FOREACH(s, b) {
if (filter_duplicates && strv_contains(t, *s)) // O(|t|) per iteration
continue;
t[p+i] = strdup(*s);
...
}
// src/basic/strv.c strv_extend_strv_consume()
STRV_FOREACH(s, b) {
if (strv_contains(t, *s)) { // O(|t|) per iteration
free(*s);
continue;
}
t[p+i] = *s;
...
}
```
`strv_contains` is defined in `src/basic/strv.h` as:
```c
#define strv_contains(l, s) (!!strv_find((l), (s)))
```
and `strv_find` is a plain linear scan:
```c
char* strv_find(char * const *l, const char *name) {
STRV_FOREACH(i, l)
if (streq(*i, name))
return *i;
return NULL;
}
```
## Call Chain
- `strv_extend_strv(a, b, /*filter_duplicates=*/true)` — direct callers throughout codebase
- `strv_split_and_extend_full()` → `strv_extend_strv_consume(t, l, filter_duplicates)` → O(N²) when filter=true
- `strv_split_and_extend()` is a common wrapper used in config parsing
## Complexity
- Before: O(N²) — each of N elements checks against growing array of size ~N
- After: O(N) — track seen strings in a `Set*` (systemd's `set.h`) keyed by string hash
## Fix
```c
int strv_extend_strv(char ***a, char * const *b, bool filter_duplicates) {
size_t p, q, i = 0;
assert(a);
q = strv_length(b);
if (q == 0)
return 0;
p = strv_length(*a);
if (p >= SIZE_MAX - q)
return -ENOMEM;
char **t = reallocarray(*a, GREEDY_ALLOC_ROUND_UP(p + q + 1), sizeof(char *));
if (!t)
return -ENOMEM;
t[p] = NULL;
*a = t;
+ _cleanup_set_free_ Set *seen = NULL;
+ if (filter_duplicates) {
+ /* Pre-populate seen with existing entries */
+ STRV_FOREACH(e, t)
+ if (set_put_strdup(&seen, *e) < 0)
+ goto rollback;
+ }
STRV_FOREACH(s, b) {
- if (filter_duplicates && strv_contains(t, *s))
+ if (filter_duplicates && set_contains(seen, *s))
continue;
+ if (filter_duplicates && set_put_strdup(&seen, *s) < 0)
+ goto rollback;
t[p+i] = strdup(*s);
if (!t[p+i])
goto rollback;
i++;
t[p+i] = NULL;
}
...
}
```
The same pattern applies to `strv_extend_strv_consume()`.
## Speedup
At N=1000 strings with filter_duplicates=true:
- Before: ~500,000 string comparisons
- After: ~1,000 hash lookups
- Ratio: ~500x
## References
- CWE-407: Inefficient Algorithmic Complexity
- systemd `src/basic/set.h` — `Set*` uses `Hashmap` internally, O(1) average lookup

View file

@ -0,0 +1,103 @@
# systemd-0002: unit_file_get_list states filter O(U×S) — CWE-407
## Severity
MEDIUM
## Location
`src/shared/install.c` — `unit_file_get_list()`
## Root Cause
Inside `unit_file_get_list()`, the code iterates over all unit files found in
each directory of the unit search path. For each unit file, it calls
`strv_contains(states, unit_file_state_to_string(state))` to check if the
unit's state matches the caller's filter list.
`strv_contains` = `strv_find()` = O(S) linear scan (S = number of states in the
filter). This check is performed once per unit file U, giving O(U × S) total.
In practice, `systemctl list-units --state=STATE1,STATE2,...` can pass S states.
With thousands of units (common on a large system) and S > 1, this degrades
noticeably compared to an O(1) hash lookup.
## Defective Code
```c
// src/shared/install.c unit_file_get_list()
STRV_FOREACH(dirname, lp.search_path) {
...
FOREACH_DIRENT(de, d, return -errno) {
...
UnitFileState state;
r = unit_file_lookup_state(scope, &lp, de->d_name, &state);
if (r < 0)
state = UNIT_FILE_BAD;
if (!strv_isempty(states) &&
!strv_contains(states, unit_file_state_to_string(state))) // O(S) per unit
continue;
...
}
}
```
## Call Chain
- `unit_file_get_list(scope, root_dir, states, patterns, ret)`
- Called by `systemctl list-unit-files` with the `--state=` filter
## Complexity
- Before: O(U × S) — U unit files × S state strings scanned per file
- After: O(U) — one O(1) hash lookup per unit file
## Fix
```c
int unit_file_get_list(
RuntimeScope scope,
const char *root_dir,
char * const *states,
char * const *patterns,
Hashmap **ret) {
_cleanup_(lookup_paths_done) LookupPaths lp = {};
_cleanup_hashmap_free_ Hashmap *h = NULL;
+ _cleanup_set_free_ Set *states_set = NULL;
int r;
...
+ /* Build O(1) lookup set for states filter */
+ if (!strv_isempty(states)) {
+ STRV_FOREACH(s, states) {
+ r = set_put_strdup(&states_set, *s);
+ if (r < 0)
+ return r;
+ }
+ }
STRV_FOREACH(dirname, lp.search_path) {
...
FOREACH_DIRENT(de, d, return -errno) {
...
if (!strv_isempty(states) &&
- !strv_contains(states, unit_file_state_to_string(state)))
+ !set_contains(states_set, unit_file_state_to_string(state)))
continue;
...
}
}
...
}
```
## Speedup
At U=5000 units, S=5 states:
- Before: ~25,000 string comparisons
- After: ~5,000 hash lookups
- Ratio: ~5x (grows linearly with S)
The ratio is modest because S is bounded by the number of valid UnitFileState
values (~10), but the fix is trivially correct and eliminates the linear scan.
## References
- CWE-407: Inefficient Algorithmic Complexity
- `src/basic/set.h` — systemd Set with O(1) lookup

View file

@ -0,0 +1,174 @@
package unit;
import java.util.*;
/**
* CWE-407 unit test: systemd-0001
*
* strv_extend_strv() with filter_duplicates=true calls strv_contains() (O(N)
* linear scan) inside a loop over every element of the input array.
* Total cost: O(N^2).
*
* Fix: replace the growing-array scan with a HashSet for O(1) membership.
*/
public class SystemdStrvExtendDedupTest {
// -----------------------------------------------------------------------
// SLOW path: simulates strv_extend_strv with filter_duplicates
// Uses List.contains() (O(N)) inside the append loop — O(N^2) total.
// -----------------------------------------------------------------------
static long slowExtendDedup(List<String> target, String[] source) {
long ops = 0;
for (String s : source) {
// O(|target|) scan — strv_contains / strv_find equivalent
ops += target.size(); // count every comparison in the scan
if (!target.contains(s)) {
target.add(s);
}
}
return ops;
}
// -----------------------------------------------------------------------
// FAST path: dedup via HashSet — O(N) total.
// -----------------------------------------------------------------------
static long fastExtendDedup(List<String> target, String[] source) {
long ops = 0;
Set<String> seen = new HashSet<>(target);
for (String s : source) {
ops++; // one hash lookup
if (seen.add(s)) {
target.add(s);
}
}
return ops;
}
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
static String[] makeUnique(int n) {
String[] a = new String[n];
for (int i = 0; i < n; i++) a[i] = "unit-" + i + ".service";
return a;
}
static String[] makeDuplicates(int n) {
// All the same string — worst case for dedup scan
String[] a = new String[n];
Arrays.fill(a, "duplicate.service");
return a;
}
// -----------------------------------------------------------------------
// 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++;
}
}
static void testUniqueStrings(int n) {
// Unique strings: slow does N*(N-1)/2 comparisons, fast does N.
List<String> slowTarget = new ArrayList<>();
List<String> fastTarget = new ArrayList<>();
String[] source = makeUnique(n);
long slowOps = slowExtendDedup(slowTarget, source);
long fastOps = fastExtendDedup(fastTarget, source);
check("unique[N=" + n + "]: slow result size == fast result size",
slowTarget.size() == fastTarget.size());
check("unique[N=" + n + "]: result contains all source elements",
slowTarget.containsAll(Arrays.asList(source)));
double ratio = (double) slowOps / fastOps;
System.out.printf(" ops: slow=%d fast=%d ratio=%.1fx%n", slowOps, fastOps, ratio);
check("unique[N=" + n + "]: slowOps/fastOps >= 5x", ratio >= 5.0);
}
static void testAllDuplicates(int n) {
// Growing target: n elements already present, then n more unique elements appended.
// The slow path scans all n existing entries for each new element: O(N^2).
// The fast path does O(1) hash lookup per new element.
List<String> slowTarget = new ArrayList<>();
List<String> fastTarget = new ArrayList<>();
// Pre-fill target with n entries (these already exist, won't be added again)
for (int i = 0; i < n; i++) {
slowTarget.add("existing-" + i + ".service");
fastTarget.add("existing-" + i + ".service");
}
// Source: n new unique strings (none are duplicates of target, so all get added)
String[] source = new String[n];
for (int i = 0; i < n; i++) source[i] = "new-" + i + ".service";
long slowOps = slowExtendDedup(slowTarget, source);
long fastOps = fastExtendDedup(fastTarget, source);
check("growing-target[N=" + n + "]: both add all N new elements",
slowTarget.size() == 2 * n && fastTarget.size() == 2 * n);
check("growing-target[N=" + n + "]: slow and fast agree",
slowTarget.equals(fastTarget));
double ratio = (double) slowOps / fastOps;
System.out.printf(" ops: slow=%d fast=%d ratio=%.1fx%n", slowOps, fastOps, ratio);
check("growing-target[N=" + n + "]: slowOps/fastOps >= 5x", ratio >= 5.0);
}
static void testMixed(int n) {
// Half unique, half duplicate
List<String> slowTarget = new ArrayList<>();
List<String> fastTarget = new ArrayList<>();
String[] source = new String[n];
for (int i = 0; i < n; i++) {
source[i] = (i % 2 == 0) ? "even-" + i + ".service" : "odd.service";
}
long slowOps = slowExtendDedup(slowTarget, source);
long fastOps = fastExtendDedup(fastTarget, source);
check("mixed[N=" + n + "]: slow and fast agree on result",
slowTarget.equals(fastTarget));
double ratio = (double) slowOps / fastOps;
System.out.printf(" ops: slow=%d fast=%d ratio=%.1fx%n", slowOps, fastOps, ratio);
check("mixed[N=" + n + "]: slowOps/fastOps >= 5x", ratio >= 5.0);
}
static void testCorrectnessSmall() {
// Correctness: strv_extend_strv({"a","b"}, {"b","c","d"}, true) -> {"a","b","c","d"}
List<String> slowT = new ArrayList<>(Arrays.asList("a", "b"));
List<String> fastT = new ArrayList<>(Arrays.asList("a", "b"));
String[] src = {"b", "c", "d"};
slowExtendDedup(slowT, src);
fastExtendDedup(fastT, src);
List<String> expected = Arrays.asList("a", "b", "c", "d");
check("correctness: slow result", slowT.equals(expected));
check("correctness: fast result", fastT.equals(expected));
}
// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== systemd-0001: strv_extend_strv dedup O(N^2) → O(N) ===");
testCorrectnessSmall();
testUniqueStrings(500);
testAllDuplicates(500);
testMixed(500);
testUniqueStrings(1000);
System.out.println();
System.out.printf("%d/%d PASS%n", pass, pass + fail);
if (fail > 0) System.exit(1);
}
}

View file

@ -0,0 +1,196 @@
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);
}
}