systemd+dbus: 5-MOAD scan; systemd-0004 CWE-407 seccomp strv O(K*A), dbus-0001 CWE-407 policy optimize O(R^2)
systemd-0004: seccomp_load_syscall_filter_set() in src/shared/seccomp-util.c uses strv_contains(added, name) — O(|added|) linear scan — inside NULSTR_FOREACH over ~537 KNOWN syscalls. On x86_64 (3 arches): ~484,000 string comparisons per service start with SeccompFilter=. Sibling function seccomp_load_syscall_filter_set_raw() already uses hashmap_contains for O(1); this function was left behind. Fix: build Set* from added strv before the NULSTR_FOREACH loop. MEDIUM severity. dbus-0001: bus_client_policy_optimize() in bus/policy.c iterates R rules and for each blanket deny/allow calls remove_rules_by_type_up_to() which scans backward from current position to head — O(R^2) total per new connection creation. At R=100 rules (realistic system bus): ~10,000 comparisons per connect. Fix: single O(R) reverse pass tracking last-seen blanket per rule type. MEDIUM. dbus 5-MOAD summary: MOAD-0001: dbus-0001 DEFECT (policy optimize O(R^2)) MOAD-0002: CLEAN (BusContext is standard daemon context, not a god object) MOAD-0003: CLEAN (single-threaded event loop, no thread-local state) MOAD-0004: CLEAN (_dbus_verbose is no-op in production builds) MOAD-0005: CLEAN (pending_activations hash table coalesces duplicate requests)
This commit is contained in:
parent
a89cc53fed
commit
decacb5dbd
8 changed files with 681 additions and 0 deletions
72
defects/systemd-0004/TICKET.md
Normal file
72
defects/systemd-0004/TICKET.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# systemd-0004: seccomp-util.c strv_contains in KNOWN syscall scan O(K*A)
|
||||
|
||||
**MOAD:** MOAD-0001 (CWE-407)
|
||||
**Severity:** MEDIUM
|
||||
**Speedup:** ~150x at K=537, A=300 (per architecture per service start)
|
||||
**Language:** C
|
||||
**File:** src/shared/seccomp-util.c:1195-1210
|
||||
|
||||
## Description
|
||||
|
||||
`seccomp_load_syscall_filter_set()` builds a `char **added` string vector (strv)
|
||||
containing every syscall name that was already handled by the requested filter
|
||||
set. After processing the filter set it iterates over all ~537 syscalls in the
|
||||
`@known` set and, for each one, calls `strv_contains(added, name)` to check
|
||||
whether that syscall was already covered:
|
||||
|
||||
```c
|
||||
SECCOMP_FOREACH_LOCAL_ARCH(arch) {
|
||||
_cleanup_strv_free_ char **added = NULL;
|
||||
|
||||
r = add_syscall_filter_set(seccomp, set, action, NULL, log_missing, &added);
|
||||
...
|
||||
NULSTR_FOREACH(name, syscall_filter_sets[SYSCALL_FILTER_SET_KNOWN].value) {
|
||||
...
|
||||
if (strv_contains(added, name)) /* O(|added|) linear scan */
|
||||
continue;
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`strv_contains()` performs a linear scan. The `added` strv can hold 200-400
|
||||
entries depending on which filter set is loaded. On x86\_64 systemd iterates 3
|
||||
architectures (x86, x32, x86\_64). Total comparisons:
|
||||
|
||||
```
|
||||
3 arches * 537 KNOWN * ~300 added = ~484,000 string comparisons per service start
|
||||
```
|
||||
|
||||
The sibling function `seccomp_load_syscall_filter_set_raw()` (line 1246)
|
||||
already uses `hashmap_contains(filter, ...)` for O(1) lookup, confirming that
|
||||
O(1) lookup is the intended pattern. This function was never updated to match.
|
||||
|
||||
Every systemd service unit that uses `SystemCallFilter=` triggers this path
|
||||
on service activation. Container environments starting many services
|
||||
simultaneously amplify the impact.
|
||||
|
||||
## Fix
|
||||
|
||||
Before the `NULSTR_FOREACH` loop, build a `Set *added_set` from `added` strv
|
||||
entries. Use `set_contains(added_set, name)` instead of `strv_contains(added,
|
||||
name)`. This mirrors the `hashmap_contains` pattern already used in the raw
|
||||
variant.
|
||||
|
||||
```c
|
||||
/* Build O(1) lookup set from added strv */
|
||||
_cleanup_set_free_ Set *added_set = NULL;
|
||||
STRV_FOREACH(n, added) {
|
||||
r = set_put_strdup(&added_set, *n);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
|
||||
NULSTR_FOREACH(name, syscall_filter_sets[SYSCALL_FILTER_SET_KNOWN].value) {
|
||||
...
|
||||
if (set_contains(added_set, name)) /* O(1) */
|
||||
continue;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Complexity drops from O(K * A) to O(K + A) per architecture.
|
||||
38
defects/systemd-0004/patch/SCAN.md
Normal file
38
defects/systemd-0004/patch/SCAN.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# systemd deeper scan — 2026-04-03
|
||||
|
||||
Source: https://github.com/systemd/systemd (depth=1)
|
||||
Focus: src/shared/seccomp-util.c (missed in 2026-03-31 scan)
|
||||
|
||||
Prior defects:
|
||||
- systemd-0001: strv_extend_strv dedup O(N^2) (src/basic/strv.c) — HIGH
|
||||
- systemd-0002: unit_file_get_list states filter O(U*S) (src/shared/install.c) — MEDIUM
|
||||
- systemd-0003: dbus-cgroup BPF filter strv dedup O(N^2) (src/core/dbus-cgroup.c) — LOW-MEDIUM
|
||||
|
||||
## MOAD-0001 (CWE-407): DEFECT — systemd-0004
|
||||
|
||||
`src/shared/seccomp-util.c` `seccomp_load_syscall_filter_set()`:
|
||||
|
||||
After processing the requested filter set (building `char **added` strv of
|
||||
covered syscall names), the function iterates all ~537 KNOWN syscalls and for
|
||||
each calls `strv_contains(added, name)` which is an O(|added|) linear scan.
|
||||
|
||||
This produces O(K * A) comparisons where:
|
||||
- K = 537 KNOWN syscalls on x86\_64
|
||||
- A = filter set size (200-400 for common sets like @default, @system-service)
|
||||
|
||||
On x86\_64 systemd processes 3 architectures (x86, x32, x86\_64):
|
||||
```
|
||||
3 * 537 * ~300 = ~484,000 string comparisons per service activation with SeccompFilter=
|
||||
```
|
||||
|
||||
The sibling function `seccomp_load_syscall_filter_set_raw()` already uses
|
||||
`hashmap_contains(filter, id)` for O(1) lookup. This function was left behind
|
||||
when the raw variant was optimized.
|
||||
|
||||
**Fix:** build a `Set *added_set` from the `added` strv before the
|
||||
`NULSTR_FOREACH` loop, then use `set_contains(added_set, name)` for O(1).
|
||||
Complexity drops from O(K*A) to O(K+A) per architecture.
|
||||
|
||||
See `systemd-0004-seccomp-strv-to-set.patch` and `TICKET.md`.
|
||||
|
||||
## MOADs 0002-0005: carried from 2026-03-31 scan (all CLEAN)
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
# UNDF: UNDF-PENDING
|
||||
--- a/src/shared/seccomp-util.c
|
||||
+++ b/src/shared/seccomp-util.c
|
||||
@@ -1178,7 +1178,7 @@ int seccomp_load_syscall_filter_set(uint32_t default_action, const SyscallFilter
|
||||
|
||||
SECCOMP_FOREACH_LOCAL_ARCH(arch) {
|
||||
_cleanup_(seccomp_releasep) scmp_filter_ctx seccomp = NULL;
|
||||
- _cleanup_strv_free_ char **added = NULL;
|
||||
+ _cleanup_strv_free_ char **added = NULL;
|
||||
+ _cleanup_set_free_ Set *added_set = NULL;
|
||||
|
||||
log_trace("Operating on architecture: %s", seccomp_arch_to_string(arch));
|
||||
|
||||
@@ -1188,10 +1188,20 @@ int seccomp_load_syscall_filter_set(uint32_t default_action, const SyscallFilter
|
||||
if (r < 0)
|
||||
return log_debug_errno(r, "Failed to add filter set: %m");
|
||||
|
||||
+ /* Build O(1) lookup set from the added strv to avoid O(K*A) quadratic
|
||||
+ * scan in the NULSTR_FOREACH loop below. The sibling function
|
||||
+ * seccomp_load_syscall_filter_set_raw() already uses hashmap_contains
|
||||
+ * for O(1); this brings the named-set path in line with it. */
|
||||
+ if (default_action != default_action_override) {
|
||||
+ char **n;
|
||||
+ STRV_FOREACH(n, added) {
|
||||
+ r = set_put_strdup(&added_set, *n);
|
||||
+ if (r < 0)
|
||||
+ return log_oom();
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
if (default_action != default_action_override)
|
||||
NULSTR_FOREACH(name, syscall_filter_sets[SYSCALL_FILTER_SET_KNOWN].value) {
|
||||
int id;
|
||||
|
||||
id = sym_seccomp_syscall_resolve_name(name);
|
||||
if (id < 0)
|
||||
continue;
|
||||
|
||||
/* Ignore the syscall if it was already handled above */
|
||||
- if (strv_contains(added, name))
|
||||
+ if (set_contains(added_set, name))
|
||||
continue;
|
||||
|
||||
r = sym_seccomp_rule_add_exact(seccomp, default_action, id, 0);
|
||||
117
defects/systemd-0004/test/TestSeccompStrvToSet.java
Normal file
117
defects/systemd-0004/test/TestSeccompStrvToSet.java
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
package defects.systemd_0004;
|
||||
|
||||
import org.junit.Test;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Unit test demonstrating systemd-0004 CWE-407:
|
||||
* seccomp_load_syscall_filter_set() uses strv_contains (O(N) linear scan)
|
||||
* inside NULSTR_FOREACH over ~537 KNOWN syscalls, producing O(K*A) behavior.
|
||||
*
|
||||
* The sibling function already uses hashmap/set for O(1) lookup.
|
||||
* This test proves O(K*A) vs O(K+A) operation count at realistic scales.
|
||||
*/
|
||||
public class TestSeccompStrvToSet {
|
||||
|
||||
/** Simulates strv_contains: linear scan of a list for a name. */
|
||||
static int strv_contains(List<String> added, String name) {
|
||||
for (String s : added) {
|
||||
if (s.equals(name)) return 1; // found
|
||||
}
|
||||
return 0; // not found
|
||||
}
|
||||
|
||||
/** Simulates set_contains: O(1) hash lookup. */
|
||||
static int set_contains(Set<String> added_set, String name) {
|
||||
return added_set.contains(name) ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measures operation count for the O(K*A) defect vs O(K+A) fix.
|
||||
*
|
||||
* @param K number of KNOWN syscalls (e.g. 537 on x86_64)
|
||||
* @param A number of syscalls already added to filter (e.g. 300 for @default)
|
||||
*/
|
||||
static long[] measureOps(int K, int A) {
|
||||
// Build KNOWN syscall list (K entries)
|
||||
List<String> known = new ArrayList<>();
|
||||
for (int i = 0; i < K; i++) {
|
||||
known.add("sys_" + i);
|
||||
}
|
||||
|
||||
// Build added strv: first A syscalls already covered
|
||||
List<String> added_strv = new ArrayList<>();
|
||||
Set<String> added_set = new HashSet<>();
|
||||
for (int i = 0; i < A; i++) {
|
||||
added_strv.add("sys_" + i);
|
||||
added_set.add("sys_" + i);
|
||||
}
|
||||
|
||||
// Defect: O(K*A) - strv_contains inside loop over KNOWN
|
||||
long ops_defect = 0;
|
||||
for (String name : known) {
|
||||
// strv_contains does up to A comparisons
|
||||
for (String s : added_strv) {
|
||||
ops_defect++;
|
||||
if (s.equals(name)) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix: O(K+A) - set_contains inside loop over KNOWN
|
||||
// Build set: A ops
|
||||
long ops_fix = A; // set construction
|
||||
// Loop: K * O(1) lookups
|
||||
for (String name : known) {
|
||||
ops_fix++; // one hash lookup per iteration
|
||||
if (added_set.contains(name)) continue;
|
||||
}
|
||||
|
||||
return new long[]{ops_defect, ops_fix};
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRealisticScale() {
|
||||
// K=537 (x86_64 KNOWN syscalls), A=300 (@default filter set size)
|
||||
int K = 537, A = 300;
|
||||
long[] ops = measureOps(K, A);
|
||||
long defect = ops[0], fix = ops[1];
|
||||
|
||||
System.out.printf("K=%d, A=%d: defect=%,d ops, fix=%,d ops, speedup=%.1fx%n",
|
||||
K, A, defect, fix, (double) defect / fix);
|
||||
|
||||
// The defect should be dramatically more expensive
|
||||
assertTrue("Defect should have > 10x more ops than fix at K=537, A=300",
|
||||
defect > fix * 10);
|
||||
|
||||
// Compute expected speedup
|
||||
double speedup = (double) defect / fix;
|
||||
assertTrue("Speedup should be > 50x at K=537, A=300", speedup > 50.0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSmallScale() {
|
||||
// Even at small scale (K=100, A=50) the defect shows clearly
|
||||
long[] ops = measureOps(100, 50);
|
||||
long defect = ops[0], fix = ops[1];
|
||||
System.out.printf("K=100, A=50: defect=%,d ops, fix=%,d ops, speedup=%.1fx%n",
|
||||
100, 50, defect, fix, (double) defect / fix);
|
||||
assertTrue("Defect always more expensive than fix", defect > fix);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleArches() {
|
||||
// x86_64 runs 3 architectures (x86, x32, x86_64)
|
||||
int arches = 3, K = 537, A = 300;
|
||||
long total_defect = 0, total_fix = 0;
|
||||
for (int i = 0; i < arches; i++) {
|
||||
long[] ops = measureOps(K, A);
|
||||
total_defect += ops[0];
|
||||
total_fix += ops[1];
|
||||
}
|
||||
System.out.printf("3 arches: defect=%,d ops, fix=%,d ops, speedup=%.1fx%n",
|
||||
total_defect, total_fix, (double) total_defect / total_fix);
|
||||
assertTrue("3-arch total: defect > 100x fix", total_defect > total_fix * 100);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue