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)
PoC verification
Evidence pack
findings/poc/DF-5761 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 fromhci_event_con_req()in netisr/event context when an incoming SCO Connection Request arrives) walks the globalsco_pcblist:LIST_FOREACH(pcb, &sco_pcb, sp_next)and, for each listening pcb, callsnew = (*pcb->sp_proto->newconn)(pcb->sp_upper, &laddr, &raddr)β holding the rawpcbpointer 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 viasco_sdetach) doesLIST_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: thesco_pcbwalk of the newconn matching, not the scodone dispatch, and not covered by DF-5753's authored fix, which locks the scodone dispatch andsco_disconnectteardown 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:
- Four listening pcbs attached via production
sco_attach()+sco_listen()(exactly what bind()+listen() SCO sockets produce). - WALK thread enters
hci_sco_newconn(), snapshot reaches the first listener, calls newconn β the rig callback sleeps 300 ms (sonewconn window). - 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). - 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_nextread on a clobbered chunk. First shot, no retries needed.
Primitive characterization
- UAF read through
sp_next(walk continuation) andsp_flags. - UAF indirect call through
sp_proto->newconnif the freed chunk is reused with attacker-shaped content (sp_protois 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):
- 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). - v2: atomic refcounts β but
newuninitialized 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). - v3 (shipped): atomic +
new = NULLinit β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
fixedfix.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.
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).
No comments yet.