Unprivileged SO_ACCEPTFILTER duplicate-attach race: two M_WAITOK allocations inside the check-then-store window of do_setopt_accept_filter() let concurrent setsockopt on a shared listener orphan struct so_accf chunks permanently β unbounded kernel heap exhaustion
| Field | Value |
|---|---|
| ID | DF-2976 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L |
| CWE | CWE-362 / CWE-401 |
| File | sys/kern/uipc_socket.c |
| Lines | 1999, 2026-2061 (found in uipc_accf.c pass-2 API sweep) |
| Area | kern/net |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
kern_setsockopt() calls sosetopt() directly on the syscall thread with no per-socket serialization for SOL_SOCKET options. do_setopt_accept_filter() reads so->so_accf once at entry, re-checks it, then performs two sleepable M_WAITOK kmallocs before publishing so->so_accf = af. Two unprivileged threads on a shared listener fd both observe NULL and both attach; the loser's 24-byte M_ACCF chunk is orphaned forever (the clear path, reachable only via sodealloc, frees only the current pointer). Non-atomic so_options RMW additionally races concurrent option mutations. Reproduced as uid 1001 on the guest: 117 wins/3000 iterations (6 threads, 3.4s) and 178+ wins/5000 (8 threads); vmstat -m M_ACCF in-use grew exactly by the win count each run. Any local unprivileged user leaks ~24 bytes of permanent kernel heap per race win at a measured ~24 wins/s/process β slow but unbounded kernel-memory-exhaustion DoS. Fix: atomic cmpset publish of the attach (loser frees its allocation) β standalone diff in pack; not kernel-validated (the DF-2975 fix deliberately does not touch this window; re-verified still leaking on the patched kernel).
Timeline
- 2026-09-02 Discovered during pass-2 audit of uipc_accf.c (GLM 5.3); unpriv leak reproduced twice.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2976 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| race_attach.c | β | 3.0 KB | view raw | |
| build.sh | β | 139 B | view raw | |
| run.sh | β | 383 B | view raw | |
| build.log | β | 363 B | view raw | |
| run.log | β | 718 B | view raw | |
| run.2.log | β | 586 B | view raw | |
| env.txt | β | 299 B | view raw | |
| VERDICT.md | β | 2.8 KB | β raw |
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 -mM_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:
--- 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).
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['run.log: vmstat -m accf 1 -> 118 in-use after 117 wins (exactly +1 chunk/win)', 'run.2.log: 118 -> 299 after 178 reported wins (>=3-winner iterations leak 2)', 'VERDICT.md: window between check (uipc_socket.c:2026) and store (2061) holds two M_WAITOK kmallocs and no lock']
PoC changes
Removed local struct accept_filter_arg/SO_ACCEPTFILTER definitions (both exist in guest
Verified recommended fix
Publish so->so_accf with atomic_cmpset_ptr from NULL so only one racing thread wins; losers free their af and release the filter ref.
Verdict
Unprivileged duplicate-attach race in do_setopt_accept_filter() reproduced on the guest: 117/3000 and 178/5000 iterations had >=2 threads successfully attach SO_ACCEPTFILTER concurrently, each win orphaning one 24-byte M_ACCF struct so_accf forever (vmstat -m in-use grew exactly by the win count). Unbounded kernel heap leak rate ~24 wins/s/process -> slow kmem-exhaustion DoS by any local user.
No comments yet.