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

Accept-filter registry has no refcount: kldunload under unloadable=1 deregisters a filter still attached to live listeners β€” next connection dispatches a NULL (or freed-module-text) accf_callback, guaranteed fatal trap; reload also strands stale accf_create/accf_destroy

Field Value
ID DF-2975
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H
CWE CWE-672
File sys/kern/uipc_accf.c
Lines 59-64, 74-78, 87-97, 119-133 (dispatch: uipc_socket2.c:253-258)
Area kern/net
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

uipc_accf.c keeps no reference between registry entries and the sockets using them (admitted at :59-64/:128-133). With net.inet.accf.unloadable=1 (root sysctl), MOD_UNLOAD calls accept_filt_del() which only NULLs accf_callback (:95) in the leaked registry entry while a listener's so_accf->so_accept_filter still points at it; kldunload then frees the module text. The next connection inherits SO_ACCEPTFILTER, soisconnected() loads accf_callback==NULL into so_upcall and immediately calls it β€” call to address 0x0 in the netisr protocol thread. Variant B: a connection already inside the filter has module-text (sohashttpget) cached as so_upcall; post-unload data arrival calls freed kld memory. Additionally accept_filt_add()'s entry-reuse path (:74-78) refreshes only accf_callback, so after unload+reload accf_create/accf_destroy dangle into the PREVIOUS module load. Reproduced on the guest: Fatal trap 12, fault va=0x0, ip=0x8:0x0, guest wedged in ddb. Privileged preconditions (root sysctl + kldunload) cap severity at Low β€” the DF-2918/DF-2966 registry-vs-kldunload family for accept filters. Fix validated in-guest (accf_refs + registry spinlock + EBUSY-while-in-use + reuse-path refresh): identical PoC β†’ 'Device busy', clean connects, refs released after listener exit. No uid0 chain β€” privileged preconditions and NULL-target dispatch.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of uipc_accf.c (GLM 5.3); fatal trap reproduced + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2975 Β· 10 files
FileTypeDescriptionSize
srv.c β€” 1.3 KB view raw
cli.c β€” 1.7 KB view raw
build.sh β€” 71 B view raw
run.sh β€” 516 B view raw
build.log β€” 1.7 KB view raw
run.log β€” 1.0 KB view raw
run.patched.log β€” 1.1 KB view raw
panic.txt β€” 2.0 KB view raw
fix.diff β€” 5.5 KB view raw
VERDICT.md β€” 4.6 KB ↓ raw
VERDICT.md
↓ download raw

DF-2975 β€” VERDICT

status: reproduced / impact: panic (privileged setup; registry-lifetime memory-corruption class) / confidence: certain fix_status: fixed (validated on rebuilt guest kernel)

Baseline (stock INVARIANTS kernel #0, Thu Jul 2 06:02:54 UTC 2026)

Sequence (run.log, panic.txt): sysctl net.inet.accf.unloadable=1 β†’ kldload accf_http β†’ unprivileged listener attaches httpready (srv2975) β†’ kldunload accf_http SUCCEEDS while the live listener still references the registry entry β†’ client connects β†’ handshake completes β†’

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x0
instruction pointer   = 0x8:0x0
current process       = Idle        (netisr protocol thread)

Guest wedged in ddb; vm.sh status β‡’ down.

Why (line-by-line)

  1. sys/kern/uipc_accf.c:127-140 β€” MOD_UNLOAD (gated only by the root-writable sysctl net.inet.accf.unloadable) calls accept_filt_del("httpready"); the registry keeps NO reference from attached sockets (the comment at uipc_accf.c:128-133 admits it).
  2. sys/kern/uipc_accf.c:87-97 β€” accept_filt_del() merely sets p->accf_callback = NULL in the intentionally-leaked registry entry. The listener's so_accf->so_accept_filter still points at that entry.
  3. sys/kern/uipc_socket2.c:372-376 β€” sonewconn() copies the listener's options (including SO_ACCEPTFILTER) into the new connection.
  4. sys/kern/uipc_socket2.c:252-258 β€” soisconnected() on handshake completion: so->so_upcall = head->so_accf->so_accept_filter->accf_callback loads NULL, sets SSB_UPCALL, and the very next statement so->so_upcall(so, so->so_upcallarg, 0) calls address 0x0 in the netisr protocol thread β‡’ Fatal trap 12, IP=0x0 (exactly as observed).

Variant B (same hole, not needed for the PoC): a connection already inside the filter has sohashttpget (module TEXT) cached as so_upcall; after kldunload frees the module mapping, the next TCP segment dispatches into freed kld memory β€” an indirect-call primitive into unmapped/reusable KVA.

Bonus defect fixed by the same diff: accept_filt_add()'s entry-reuse path (uipc_accf.c:74-78) refreshed only accf_callback, so after unload+reload of a filter that defines them, accf_create/accf_destroy in the registry entry dangle into the PREVIOUS module load's text (do_setopt_accept_filter calls both: uipc_socket.c:2011-2013, 2043-2051; latent for the in-tree filters which set them NULL, live for any kld that defines create/destroy).

Severity rationale

Preconditions are privileged (root sysctl write + kldunload), so this is Low severity / memcorrupt bucket β€” the DF-2918 (vfsconf vs kldunload) and DF-2966 (domain registry) family for the accept-filter registry. The primitive itself is a kernel indirect call through a deregistered callback (NULL here; freed module text in variant B).

Fix validation (kernel #1, Fri Sep 4 12:39:28 UTC 2026, fix.diff applied)

make nativekernel KERNCONF=X86_64_GENERIC && make installkernel, reboot, exact same PoC (run.patched.log):

step baseline #0 patched #1
kldunload accf_http with listener attached succeeds Device busy (EBUSY)
./cli 19001 (connect through filter) Fatal trap 12, IP=0x0, guest down connects, cli_rc=0, guest up
pkill srv2975 then kldunload n/a (dead) succeeds (ref released)
vmstat -m accf after unload+reload cycle β€” back to 1 chunk (56 B: entry now has accf_refs), no leak
full GET / HTTP/1.0\r\n\r\n through filter β€” accepted+drained normally

Bad behavior GONE; filter semantics preserved; refcount lifecycle verified both directions. (Regression note: DF-2976's attach race still leaks on this kernel by design β€” separate finding, separate fix.)

Exploit chain

none to uid0 β€” privileged preconditions and a NULL-target dispatch; chain terminates at panic. The freed-text variant would be the uid0-relevant primitive on a system where an attacker can influence post-unload kld placement, but that requires the same root preconditions.

fix.diff summary

  • struct accept_filter gains accf_refs (sys/sys/socketvar.h).
  • Registry ops serialized by accept_filt_spin; accept_filt_get() now returns a HELD reference; new accept_filt_release().
  • accept_filt_del() refuses with EBUSY while refs > 0, and NULLs all three callbacks; accept_filt_add() reuse path refreshes all three callbacks; registry allocation is M_ZERO'd.
  • do_setopt_accept_filter() releases the ref on the clear path (sodealloc) and on the accf_create failure path, and rejects entries with NULL accf_callback (ENOENT).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Exact PoC rerun on the rebuilt kernel: kldunload correctly refused with EBUSY while the filtered listener lives, the triggering connect() completes without any trap, the guest survives, the refcount releases when the listener exits (unload then succeeds), and a normal full HTTP request through the filter still works. The panic is gone with filter semantics preserved.

['run.patched.log', 'build.log (patched objects uipc_accf.o/uipc_socket.o at 12:35 in kernel of 12:53)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 4 12:39:28 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: sysctl unloadable=1 + kldload accf_http; unpriv listener attaches httpready; root kldunload accf_http (deregisters callback from under the live listener); any client connect -> netisr thread calls so_upcall==NULL -> fatal trap. Variant B: in-filter connection caches module-text upcall, post-unload data arrival calls freed kld text.

Evidence (decisive lines)

['panic.txt: Fatal trap 12, fault va=0x0, ip=0x8:0x0, current process Idle (netisr)', 'run.log: kldunload succeeded with live filtered listener, then connect -> trap', 'run.patched.log: kldunload -> Device busy (EBUSY); cli connects; guest up; unload OK after listener exit; vmstat accf back to 1 chunk', 'fix.diff: refcount + spinlock + full-callback refresh']

PoC changes

none vs seed design (srv/cli written fresh for this pack); removed local struct accept_filter_arg redefinition (guest exports it).

Verified recommended fix

Refcount accept_filter entries (accf_refs) under a registry spinlock; accept_filt_get takes a ref, accept_filt_release drops it from do_setopt_accept_filter's clear path; accept_filt_del returns EBUSY while refs>0; reuse path refreshes accf_create/accf_destroy; registry alloc M_ZERO.

Verdict

Accept-filter registry keeps no refcount between registered filters and attached sockets: with net.inet.accf.unloadable=1, kldunload of accf_http succeeds while a listener still holds the filter; the next connection loads accf_callback==NULL into so_upcall and calls it (uipc_socket2.c:254-258) -> Fatal trap 12, instruction pointer 0x0, guest wedged (panic.txt). Privileged preconditions (root sysctl + kldunload) -> Low/memcorrupt, DF-2918 family. fix.diff adds accf_refs + registry spinlock + EBUSY-on-in-use-del + reuse-path refresh of accf_create/accf_destroy; rebuilt kernel (#1) refuses the unload (Device busy), the same PoC connects cleanly, guest stays up, and the module unloads normally once the listener exits.