β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2976

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2976 Β· 8 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 -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:

--- 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_testable
per-fix-DF-2976

Confirmed 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 under _BSD_VISIBLE); first compile failed on redefinition.

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.