# DF-2976 — VERDICT

**status: reproduced / impact: dos (unbounded kernel heap leak by unprivileged user) / confidence: certain**

## What was run

`race_attach.c` (this pack) on the DF 6.5-DEVELOPMENT guest (stock INVARIANTS
kernel #0, Thu Jul 2 06:02:54 UTC 2026), as unprivileged uid 1001 (`maxx`),
with `accf_http.ko` loaded by root. Two runs:

- 3000 iterations × 6 threads → 117 iterations where ≥2 threads returned
  success; `vmstat -m` M_ACCF in-use went 1 → 118 (+117 × 24 B = 2.79 K).
- 5000 iterations × 8 threads → 178 wins; in-use 118 → 299 (+181 chunks).

The +chunks ≈ #wins match is the decisive evidence: each race win orphans
exactly one `kmalloc(sizeof(struct so_accf), M_ACCF, M_WAITOK|M_ZERO)`
(uipc_socket.c:2042) that nothing ever frees — the socket only remembers the
last pointer stored at uipc_socket.c:2061, and the clear path
(do_setopt_accept_filter(so, NULL), uipc_socket.c:2009-2022, reached only via
sodealloc at uipc_socket.c:321) frees only the current one.

## Why it is a bug

The whole window between the `af != NULL` check (uipc_socket.c:2026, value
read at function entry line 1999) and `so->so_accf = af` (2061) contains two
M_WAITOK (sleepable) allocations — kmalloc of `accept_filter_arg` (2031) and
of `struct so_accf` (2042) — and no lock: `kern_setsockopt()` (uipc_syscalls.c
:1213-1233) invokes `sosetopt()` (uipc_socket.c:2128) directly on the syscall
thread; there is no protocol-thread dispatch for SOL_SOCKET options. An
unprivileged user with two threads on a dup'd/listener fd wins the race at a
measurable rate (~24 wins/s with 8 threads here).

## Impact ceiling

Unprivileged, unbounded kernel heap exhaustion (M_ACCF zone): ~800 B/s per
process single-guest measurement; scales with parallelism (processes ×
threads). Eventual kmem exhaustion → allocator failure/panic. Rate is slow,
so this is a Low-severity resource-exhaustion DoS, not a fast kill.

## Exploit chain

none (resource leak; no memory-safety violation — loser pointer is orphaned,
never freed-twice, never read after free).

## Fix direction (validated as part of the DF-2975 pack's kernel: NOT included
in DF-2975/fix.diff, which only fixes the registry lifetime)

Publish the attach with an atomic compare-and-swap so only one racer wins:

```diff
--- a/sys/kern/uipc_socket.c
+++ b/sys/kern/uipc_socket.c
@@	af->so_accept_filter = afp;
-	so->so_accf = af;
+	if (!atomic_cmpset_ptr((volatile uintptr_t *)&so->so_accf,
+			       (uintptr_t)NULL, (uintptr_t)af)) {
+		/* another thread attached concurrently: back off */
+		if (af->so_accept_filter_str != NULL)
+			kfree(af->so_accept_filter_str, M_ACCF);
+		kfree(af, M_ACCF);
+		accept_filt_release(afp);
+		error = EINVAL;
+		goto out;
+	}
 	so->so_options |= SO_ACCEPTFILTER;
```

This keeps the fix local (no lock-order interaction with pool tokens).
