systemd+dnsmasq: 5-MOAD scan; dnsmasq-0001 + systemd-0003 MOAD-0001 defects

dnsmasq: option_filter() O(N²) duplicate elimination in src/dhcp-common.c.
Two nested loops over dhcp_opt list called per DHCP reply. Fix: 256-entry
bitmap keyed by 1-byte option code. 23/23 PASS, 25-50x measured ratio.
MOADs 0002/0004/0005 CLEAN; MOAD-0003 N/A (single-threaded).

systemd: dbus-cgroup.c IPIngressFilterPath/IPEgressFilterPath setter calls
strv_contains() inside for(;;) loop — O(N²) on D-Bus property writes. Fix:
HashSet pre-populated before loop. 17/17 PASS, 250x at N=500.
MOADs 0002/0003/0004/0005 CLEAN (single-threaded sd-event, no key logging).
This commit is contained in:
russell@unturf.com 2026-03-31 21:34:28 -04:00
parent 6c6dc2b114
commit db4e0cc498
6 changed files with 680 additions and 0 deletions

View file

@ -0,0 +1,49 @@
# systemd 5-MOAD scan — 2026-03-31
Source: https://github.com/systemd/systemd (depth=1)
Focus: src/core/, src/resolve/, src/cryptsetup/, src/login/
Prior defects in this directory:
- systemd-0001: strv_extend_strv dedup O(N²) (src/basic/strv.c) — HIGH
- systemd-0002: unit_file_get_list states filter O(U×S) (src/shared/install.c) — MEDIUM
## MOAD-0001 (CWE-407): DEFECT — systemd-0003
`src/core/dbus-cgroup.c` BPF filter path setter: `strv_contains(*filters, path)`
called inside a `for(;;)` loop reading D-Bus array elements for
`IPIngressFilterPath=` / `IPEgressFilterPath=` properties. O(N²) total for N
paths. See `systemd-0003-dbus-cgroup-bpf-filter-strv-dedup.patch`.
Same pattern also appears in `src/core/load-fragment.c`
`config_parse_ip_filter_bpf_progs()`. Both sites are LOW-MEDIUM severity
(config/property-write path, not hot packet path).
Other candidates reviewed and found LOW severity or too small in practice:
- `src/core/device.c` LIST_FOREACH + strv_contains(aliases, d->path): called
on udev hotplug events. aliases list is small (udev SYSTEMD_ALIAS values).
- `src/core/transaction.c` nested LIST_FOREACH: not a membership test, just
pairwise conflict checking over job sets.
## MOAD-0002 (Intertangle): CLEAN (architectural note)
The `Manager` struct (`src/core/manager.h`, 672 lines) is a god object holding
all systemd state. This is intentional and well-known. The subsystems (device,
mount, swap, service, etc.) all reference it. No novel coupling to patch.
Marked CLEAN.
## MOAD-0003 (Leaked Context): CLEAN
systemd is a single-threaded sd-event loop. No `thread_local`, `__thread`, or
`pthread_getspecific` found in `src/core/` or `src/resolve/`. CLEAN.
## MOAD-0004 (CWE-312 Logged Secret): CLEAN
Reviewed `src/cryptsetup/cryptsetup.c` for LUKS passphrase, volume key, and
TPM key material in log calls. Found only device names and error messages in
`log_debug`/`log_info`/`log_warning` — no passphrase values, no key bytes,
no token secrets. CLEAN.
## MOAD-0005 (Thundering Herd): CLEAN
systemd is a single-threaded sd-event loop. No concurrent hashmap/set access.
CLEAN.

View file

@ -0,0 +1,87 @@
# systemd-0003: dbus-cgroup IPIngressFilterPath/IPEgressFilterPath dedup O(N²) — CWE-407
## Severity
LOW-MEDIUM
## Location
`src/core/dbus-cgroup.c` — BPF filter path setter block (~line 578)
## Root Cause
When systemd accepts an `IPIngressFilterPath=` or `IPEgressFilterPath=` array
via D-Bus (e.g., `systemctl set-property SomeUnit.service IPIngressFilterPath=…`),
it reads each path string from the bus message in a `for (;;)` loop and calls
`strv_contains(*filters, path)` before appending to the `*filters` strv.
`strv_contains` expands to `strv_find()`, an O(N) linear scan of the entire
target array. Each successive path added to the array causes a longer scan of
all previously accepted paths. For N paths in the incoming message, total cost
is O(N²) string comparisons.
```c
/* src/core/dbus-cgroup.c */
for (;;) {
const char *path;
r = sd_bus_message_read(message, "s", &path);
...
if (!UNIT_WRITE_FLAGS_NOOP(flags) && !strv_contains(*filters, path)) {
r = strv_extend(filters, path); /* O(N) scan before each append */
...
}
n++;
}
```
In practice, kernel BPF program lists per cgroup are short (typically < 10
entries), so the quadratic cost is low in normal operation. However, a
misbehaving or malicious D-Bus client can send a large array, triggering
pathological O(N²) CPU usage in PID 1 (systemd itself) during property
handling.
## Fix
Build a `Set*` (hashmap-backed, O(1) lookup) from the already-accepted paths
before the loop, then use `set_contains()` instead of `strv_contains()`:
```c
_cleanup_set_free_ Set *seen = NULL;
/* Pre-populate with existing filter paths */
STRV_FOREACH(e, *filters)
if (set_put_strdup(&seen, *e) < 0)
return log_oom();
for (;;) {
const char *path;
r = sd_bus_message_read(message, "s", &path);
...
if (!UNIT_WRITE_FLAGS_NOOP(flags) && !set_contains(seen, path)) {
r = set_put_strdup(&seen, path);
if (r < 0)
return log_oom();
r = strv_extend(filters, path);
if (r < 0)
return log_oom();
}
n++;
}
```
The same `strv_contains` pattern appears in `src/core/load-fragment.c` inside
`config_parse_ip_filter_bpf_progs()` which builds BPF prog path lists from
unit files. That site has the same O(N²) characteristic and should apply the
same fix.
## Speedup
At N=500 duplicate filter paths sent via D-Bus:
- Before: ~125,000 string comparisons
- After: ~500 hash lookups
- Ratio: ~250x
At typical sizes (N < 10) the difference is immaterial. The fix matters for
adversarial or automated D-Bus clients.
## References
- CWE-407: Inefficient Algorithmic Complexity
- `src/core/dbus-cgroup.c` STR_IN_SET block for `IPIngressFilterPath`
- `src/core/load-fragment.c` `config_parse_ip_filter_bpf_progs()`
- `src/basic/strv.h` `strv_contains` macro → `strv_find()` O(N)
- `src/basic/set.h` `Set*` backed by hashmap, O(1) average

View file

@ -0,0 +1,159 @@
package unit;
import java.util.*;
/**
* CWE-407 unit test: systemd-0003
*
* dbus-cgroup.c IPIngressFilterPath/IPEgressFilterPath setter calls
* strv_contains() — O(N) linear scan — inside a for(;;) loop that processes
* each incoming D-Bus path string. Total cost: O(N²) for N paths.
*
* Fix: build a HashSet before the loop and use O(1) membership instead.
*/
public class SystemdDbusCgroupFilterDedupTest {
static int pass = 0;
static int fail = 0;
static void check(String label, boolean cond) {
if (cond) {
System.out.println(" PASS: " + label);
pass++;
} else {
System.out.println(" FAIL: " + label);
fail++;
}
}
// -----------------------------------------------------------------------
// SLOW path: simulates strv_contains(*filters, path) inside the loop
// Each new path triggers an O(|filters|) linear scan — O(N²) total.
// -----------------------------------------------------------------------
static long slowFilterAppend(List<String> filters, String[] incoming) {
long ops = 0;
for (String path : incoming) {
// strv_contains = linear scan of current filters list
ops += filters.size(); // count every comparison
if (!filters.contains(path)) {
filters.add(path);
}
}
return ops;
}
// -----------------------------------------------------------------------
// FAST path: use HashSet for O(1) membership, O(N) total.
// -----------------------------------------------------------------------
static long fastFilterAppend(List<String> filters, String[] incoming) {
long ops = 0;
Set<String> seen = new HashSet<>(filters);
for (String path : incoming) {
ops++; // one hash lookup
if (seen.add(path)) {
filters.add(path);
}
}
return ops;
}
// -----------------------------------------------------------------------
// Test: correctness — both slow and fast produce the same unique list
// -----------------------------------------------------------------------
static void testCorrectness() {
List<String> slowFilters = new ArrayList<>(Arrays.asList("/prog/a.o", "/prog/b.o"));
List<String> fastFilters = new ArrayList<>(Arrays.asList("/prog/a.o", "/prog/b.o"));
String[] incoming = {"/prog/b.o", "/prog/c.o", "/prog/d.o", "/prog/b.o"};
slowFilterAppend(slowFilters, incoming);
fastFilterAppend(fastFilters, incoming);
List<String> expected = Arrays.asList("/prog/a.o", "/prog/b.o", "/prog/c.o", "/prog/d.o");
check("correctness: slow produces correct deduplicated list", slowFilters.equals(expected));
check("correctness: fast produces correct deduplicated list", fastFilters.equals(expected));
check("correctness: slow and fast agree", slowFilters.equals(fastFilters));
}
// -----------------------------------------------------------------------
// Test: all-unique paths (no duplicates)
// -----------------------------------------------------------------------
static void testAllUnique(int n) {
List<String> slowF = new ArrayList<>();
List<String> fastF = new ArrayList<>();
String[] incoming = new String[n];
for (int i = 0; i < n; i++) incoming[i] = "/bpf/prog-" + i + ".o";
long slowOps = slowFilterAppend(slowF, incoming);
long fastOps = fastFilterAppend(fastF, incoming);
check("allUnique[N=" + n + "]: slow appends all", slowF.size() == n);
check("allUnique[N=" + n + "]: fast appends all", fastF.size() == n);
check("allUnique[N=" + n + "]: results agree", slowF.equals(fastF));
double ratio = (double) slowOps / fastOps;
System.out.printf(" allUnique[N=%d] ops: slow=%d fast=%d ratio=%.1fx%n",
n, slowOps, fastOps, ratio);
check("allUnique[N=" + n + "]: ratio >= 5x", ratio >= 5.0);
}
// -----------------------------------------------------------------------
// Test: all-duplicate paths (same path sent N times)
// -----------------------------------------------------------------------
static void testAllDuplicates(int n) {
List<String> slowF = new ArrayList<>();
List<String> fastF = new ArrayList<>();
String[] incoming = new String[n];
Arrays.fill(incoming, "/bpf/same.o");
long slowOps = slowFilterAppend(slowF, incoming);
long fastOps = fastFilterAppend(fastF, incoming);
check("allDup[N=" + n + "]: slow accepts only 1", slowF.size() == 1);
check("allDup[N=" + n + "]: fast accepts only 1", fastF.size() == 1);
System.out.printf(" allDup[N=%d] ops: slow=%d fast=%d%n",
n, slowOps, fastOps);
}
// -----------------------------------------------------------------------
// Test: mixed unique + duplicate paths
// -----------------------------------------------------------------------
static void testMixed(int n) {
List<String> slowF = new ArrayList<>();
List<String> fastF = new ArrayList<>();
String[] incoming = new String[n];
for (int i = 0; i < n; i++) {
// Even entries are unique, odd entries repeat "/bpf/odd.o"
incoming[i] = (i % 2 == 0) ? "/bpf/prog-" + i + ".o" : "/bpf/odd.o";
}
long slowOps = slowFilterAppend(slowF, incoming);
long fastOps = fastFilterAppend(fastF, incoming);
check("mixed[N=" + n + "]: slow and fast agree", slowF.equals(fastF));
double ratio = (double) slowOps / fastOps;
System.out.printf(" mixed[N=%d] ops: slow=%d fast=%d ratio=%.1fx%n",
n, slowOps, fastOps, ratio);
check("mixed[N=" + n + "]: ratio >= 5x", ratio >= 5.0);
}
// -----------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------
public static void main(String[] args) {
System.out.println("=== systemd-0003: dbus-cgroup BPF filter path dedup O(N²) → O(N) ===");
testCorrectness();
testAllDuplicates(100);
testAllUnique(100);
testAllUnique(500);
testMixed(200);
testMixed(500);
System.out.println();
System.out.printf("%d/%d PASS%n", pass, pass + fail);
if (fail > 0) System.exit(1);
}
}