# 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).
