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)
PoC verification
Evidence pack
findings/poc/DF-2975 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
sys/kern/uipc_accf.c:127-140βMOD_UNLOAD(gated only by the root-writable sysctlnet.inet.accf.unloadable) callsaccept_filt_del("httpready"); the registry keeps NO reference from attached sockets (the comment atuipc_accf.c:128-133admits it).sys/kern/uipc_accf.c:87-97βaccept_filt_del()merely setsp->accf_callback = NULLin the intentionally-leaked registry entry. The listener'sso_accf->so_accept_filterstill points at that entry.sys/kern/uipc_socket2.c:372-376βsonewconn()copies the listener's options (includingSO_ACCEPTFILTER) into the new connection.sys/kern/uipc_socket2.c:252-258βsoisconnected()on handshake completion:so->so_upcall = head->so_accf->so_accept_filter->accf_callbackloads NULL, setsSSB_UPCALL, and the very next statementso->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_filtergainsaccf_refs(sys/sys/socketvar.h).- Registry ops serialized by
accept_filt_spin;accept_filt_get()now returns a HELD reference; newaccept_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 NULLaccf_callback(ENOENT).
Fix verification
fixedExact 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)']
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
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.
No comments yet.