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

sco_pcb newconn-matching walk UAF: hci_sco_newconn() (netisr) holds a listening pcb across the sonewconn callback while sco_detach() (syscall) unlinks and kfrees it β€” deterministic kernel panic, indirect-call primitive through freed memory

Summary

The walker holds the pcb pointer across the newconn callback (socket allocation) while close(2) of the listening socket does LIST_REMOVE+kfree with no lock and no reference: the walker then reads sp_flags/sp_next and calls through sp_proto on freed, immediately reusable memory. Deterministic first-shot panic in production hci_sco_newconn ('Fatal trap 12 VA 0x18, Stopped at hci_sco_newconn+0x99'); latent primitive is an indirect call through freed M_BLUETOOTH chunks (objcache LIFO reuse). Distinct list and sink from DF-5753. refcount+spinlock+snapshot-walk fix validated through 3 iterations over 519M walks + 12.3M stress rounds, no hit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5761 Β· 14 files
FileTypeDescriptionSize
harness.c β€” 11.3 KB view raw
hci_stub.c β€” 2.4 KB view raw
Makefile β€” 604 B ↓ download
build.sh β€” 85 B view raw
run.sh β€” 172 B view raw
setup.log β€” 871 B view raw
build.log β€” 13.1 KB view raw
run.log β€” 1.1 KB view raw
panic.txt β€” 658 B view raw
fix_run.log β€” 646 B view raw
fix.diff β€” 3.4 KB view raw
env.txt β€” 188 B view raw
VERDICT.md β€” 4.9 KB ↓ raw
verdict.json β€” 4.9 KB view raw
VERDICT.md
↓ download raw

DF-5761 VERDICT

Status: reproduced (rig, production code verbatim; deterministic panic) Impact: panic (use-after-free read/walk + indirect-call primitive in netisr context; realistic ceiling today: local/remote-adjacent kernel panic DoS β€” see primitive characterization below).

Root cause (path:line)

  • sys/netbt/hci_link.c:782-806 β€” hci_sco_newconn() (called from hci_event_con_req() in netisr/event context when an incoming SCO Connection Request arrives) walks the global sco_pcb list: LIST_FOREACH(pcb, &sco_pcb, sp_next) and, for each listening pcb, calls new = (*pcb->sp_proto->newconn)(pcb->sp_upper, &laddr, &raddr) β€” holding the raw pcb pointer across a callback that allocates a socket (sco_newconn() β†’ sonewconn()).
  • sys/netbt/sco_upper.c:245-246 β€” sco_detach() (the syscall context, close(2) of a listening SCO socket via sco_sdetach) does LIST_REMOVE(pcb, sp_next); kfree(pcb, M_BLUETOOTH); with no lock and no reference anywhere in netbt (same design gap as the DF-5753/5749 family, but a different list and a different sink: the sco_pcb walk of the newconn matching, not the scodone dispatch, and not covered by DF-5753's authored fix, which locks the scodone dispatch and sco_disconnect teardown only).

A walker that has already loaded pcb (or is inside the callback) continues through LIST_NEXT(pcb, sp_next) / pcb->sp_flags / (*pcb->sp_proto->newconn) on freed β€” and immediately reusable β€” memory.

Reproduced behaviour (run.log, panic.txt β€” deterministic)

Rig: production sources compiled verbatim (md5s in setup.log); hw.scow_fire=1:

  1. Four listening pcbs attached via production sco_attach() + sco_listen() (exactly what bind()+listen() SCO sockets produce).
  2. WALK thread enters hci_sco_newconn(), snapshot reaches the first listener, calls newconn β€” the rig callback sleeps 300 ms (sonewconn window).
  3. CLOSE thread sco_detach()s all four (LIST_REMOVE + kfree), then re-allocates same-size chunks filled 0xA5 (objcache LIFO reuse β€” models any same-zone M_BLUETOOTH allocation).
  4. The walk resumes and dereferences the freed pcb: Fatal trap 12 ... current process = Idle Stopped at hci_sco_newconn+0x99: movq 0x18(%rax),%rax β€” fault virtual address 0x18 = sp_next read on a clobbered chunk. First shot, no retries needed.

Primitive characterization

  • UAF read through sp_next (walk continuation) and sp_flags.
  • UAF indirect call through sp_proto->newconn if the freed chunk is reused with attacker-shaped content (sp_proto is a kernel pointer in a same-zone reallocation β€” reachable in principle by slab grooming with M_BLUETOOTH-sized objects; not developed to uid0 here because triggering the race requires Bluetooth adjacency/controller events (netbt), which constrains the attacker model to local-with-bt-hardware or a paired peer).
  • The windows recur on every incoming SCO connection request racing socket close; the callback window (socket allocation) is wide.

Fix validation (3 iterations β€” the shipped fix.diff is the final one)

fix.diff: sp_refcnt (atomic ops) + sco_pcblock spinlock; sco_attach inserts under the lock; sco_detach removes under the lock and releases the list reference; hci_sco_newconn snapshots+pins candidates under the lock and runs the callback unpinned-lock.

Iterations (all rig-visible, exactly what fix validation is for):

  1. v1: plain --sp_refcnt (non-atomic) + missing attach lock β†’ deterministic round survived, but the stress phase double-faulted (lost decrements β†’ premature kfree β†’ UAF on two cpus).
  2. v2: atomic refcounts β€” but new uninitialized when the snapshot found no listeners β†’ success path wrote through a garbage pointer (hci_sco_newconn+0x228, the classic empty-walk path my restructure introduced).
  3. v3 (shipped): atomic + new = NULL init β†’ SURVIVED: 519149322 walks, 12274611 stress rounds, no hit, guest up (fix_run.log).

Threat

Unprivileged local user who owns a listening SCO socket racing close(2) against an incoming SCO connection request (needs a controller driver on netbt, as with the whole netbt family), or the remote peer timing connection requests against the local close. Ceiling: kernel panic (reproduced); latent UAF-call primitive.

Rig notes

The baseline run.log/panic.txt were captured with the pre-marker harness; the build in the pack carries two extra progress markers in the CLOSE thread (harmless; used during fix debugging).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src/sys/netbt (sco.h + sco_upper.c + hci_link.c), rig rebuilt from the patched verbatim sources: the deterministic close-under-callback round completes cleanly (CLOSE markers show all four victims detached with the walk's pins holding), then 519,149,322 hci_sco_newconn walks against 12,274,611 concurrent attach/listen/detach stress rounds complete with no fault; guest up.

fix_run.log: '[harness] CLOSE: starting detaches / detached victim 0..3 / CLOSE thread: 4 listening pcbs detached' followed by '[harness] SURVIVED: 519149322 walks, 12274611 stress rounds, no hit' and uptime; build.log shows the patched sources compiled -Werror clean.
↓ fix.diffper-fix-DF-5761

Confirmed kernel references

Detail

Exploit chain

Primitive: UAF read through sp_next/sp_flags continuing the listener walk, and an indirect call (*pcb->sp_proto->newconn)(pcb->sp_upper, ...) through freed memory whenever the chunk is reused with attacker-shaped content (same M_BLUETOOTH zone, objcache LIFO reuse demonstrated in the rig with 0xA5-filled reallocations). Realistic ceiling today: kernel panic DoS - triggering requires a controller driver on netbt (incoming SCO Connection Request racing socket close), so the attacker is a local unprivileged socket owner racing close(2) or a paired peer timing connection requests. uid0 escalation not developed: the trigger surface (Bluetooth adjacency) constrains practical exploitation; the rig proves the primitive exists on production code.

Evidence (decisive lines)

run.log + panic.txt: 'Fatal trap 12: page fault ... fault virtual address = 0x18 ... current process = Idle ... Stopped at hci_sco_newconn+0x99: movq 0x18(%rax),%rax' right after the CLOSE thread detached the listening pcbs under the sleeping newconn callback; fix_run.log: 'SURVIVED: 519149322 walks, 12274611 stress rounds, no hit' with the fixed verbatim sources, guest up (uptime in transcript).

PoC changes

Adapted the DF-5753 rig (fake hci_if device + hci_attach glue + hci_stub) into a two-lwkt-thread harness: WALK thread loops production hci_sco_newconn(unit, &peer) (exactly the hci_event_con_req netisr context); CLOSE thread runs production sco_attach/sco_listen/sco_detach; the rig newconn callback sleeps 300ms once to model sonewconn cost; after each detach an immediate same-size kmalloc is 0xA5-filled and kept (objcache LIFO reuse model); stress phase hammers both sides after the deterministic round. Two CLOSE-thread progress markers were added during fix debugging.

Verified recommended fix

Reference-count sco_pcb (atomic ops) + guard list mutation with a spinlock; hci_sco_newconn snapshots+pins listening candidates under the lock and runs newconn() outside it (see fix.diff).

Verdict

Reproduced deterministically (first shot) with a KLD rig compiling the verbatim production hci_link.c/sco_upper.c (plus hci_unit/hci_event/bt_input/hci_misc/l2cap_lower) against stubbed socket layers: hci_sco_newconn()'s LIST_FOREACH over the global sco_pcb list holds a raw pcb pointer across the ->newconn callback (sonewconn window), while the syscall-context sco_detach() (close(2) of a listening SCO socket) LIST_REMOVEs + kfrees the pcbs with no lock anywhere; the walk then dereferences freed memory -> Fatal trap 12, 'Stopped at hci_sco_newconn+0x99: movq 0x18(%rax),%rax', current process Idle (netisr-context thread). This is a distinct list and sink from the DF-5753 scodone dispatch (DF-5753's authored fix does not cover the con_req-side walk).