TOCTOU use-after-free on rtentry in ng_btsocket_l2cap_raw_bind: releases rt_lock before storing pointer
Summary
ng_btsocket_l2cap_raw_bind(:690-715): looks up rt under rt_lock, RELEASES rt_lock at :703 before storing pointer at :714. ng_btsocket_l2cap_rtentry has NO refcount(ng_btsocket_l2cap.h:44-48). Between :703 and :712 bind holds NEITHER rt_lock NOR pcb_lock. ng_btsocket_l2cap_raw_rtclean(:450-502) can LIST_REMOVE+kfree rt(:495). bind then stores dangling pcb->rt=rt(:714). Subsequent ioctl derefs pcb->rt->hook without rt_lock -> reads freed heap. Freed slot reclaimable from M_NETGRAPH -> partial control of ->hook ptr -> UAF type confusion. Unpriv local user can attempt (attach grants socket, caps check only sets flag). Barrier: triggering concurrent hook disconnect needs netgraph priv/BT hw removal/malicious peer. Fix: hold rt_lock across :714.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0497 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trace.md | source-trace | TOCTOU window + rtclean race + no-refcount proof | 2.8 KB | β raw |
| fix.diff | suggested-fix | hold rt_lock across pcb->rt store | 937 B | view raw |
| VERDICT.md | verdict | full narrative | 1.5 KB | β raw |
| README.md | readme | summary | 3.2 KB | β raw |
| env.txt | environment | guest uname, modules, HW-gate note | 188 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0497 β TOCTOU use-after-free on rtentry in ng_btsocket_l2cap_raw_bind
Verdict: INCONCLUSIVE at runtime β bug real by inspection; no Bluetooth hardware/netgraph on guest
The bug (real, by source inspection)
sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap_raw.c
ng_btsocket_l2cap_raw_bind() (lines 690-715):
690: if (bcmp(&sa->l2cap_bdaddr, NG_HCI_BDADDR_ANY, ...) != 0) {
692: lockmgr(&ng_btsocket_l2cap_raw_rt_lock, LK_EXCLUSIVE);
694: LIST_FOREACH(rt, &ng_btsocket_l2cap_raw_rt, next) { ... }
703: lockmgr(&ng_btsocket_l2cap_raw_rt_lock, LK_RELEASE); /* RELEASE */
705: if (rt == NULL) { error = ENETDOWN; goto out; }
710: } else rt = NULL;
712: lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE);
713: bcopy(&sa->l2cap_bdaddr, &pcb->src, sizeof(pcb->src));
714: pcb->rt = rt; /* STORE */
715: lockmgr(&pcb->pcb_lock, LK_RELEASE);
Between the rt_lock release (:703) and the pcb->rt = rt store (:714), bind
holds neither rt_lock nor pcb_lock. ng_btsocket_l2cap_rtentry has
no refcount (ng_btsocket_l2cap.h:44-48), so nothing keeps rt alive.
A concurrent ng_btsocket_l2cap_raw_rtclean() (:450-502) β which takes
rt_lock and LIST_REMOVE+kfree(rt) (:495) β can free rt in that window.
bind then stores a dangling pcb->rt; subsequent ioctls dereference
pcb->rt->hook without rt_lock β read of freed M_NETGRAPH heap β UAF /
type confusion. CWE-416.
Privilege boundary
socket(PF_BLUETOOTH, ...) attach only sets a capability flag (no hard root
gate on the socket itself), so an unprivileged local user can reach bind.
But triggering the racing rtclean/hook-disconnect needs Bluetooth hardware or
netgraph privilege (loading the bluetooth netgraph subsystem, root-only on this
guest). So the UAF trigger is gated behind BT HW / netgraph priv, while the
bind itself is reachable by an unprivileged user β the finding's stated
realistic barrier.
Why not reproduced on this guest
No Bluetooth hardware and no bluetooth netgraph modules on the audit guest
(find /boot/modules /modules -iname '*bluetooth*' -o -iname '*hci*' -o -iname
'*l2cap*' β none). ng_btsocket_l2cap_raw_node is NULL without the subsystem
loaded, so bind returns before reaching :690. The UAF cannot be exercised.
Fix (applies + compiles + boots)
fix.diff keeps rt_lock held across the pcb->rt = rt store (and releases
it only after, on both the success and rt==NULL paths):
/* keep rt_lock held; ng_btsocket_l2cap_rtentry has no refcount */
if (rt == NULL) { lockmgr(&rt_lock, LK_RELEASE); error = ENETDOWN; goto out; }
...
pcb->rt = rt;
lockmgr(&pcb->pcb_lock, LK_RELEASE);
if (rt != NULL) lockmgr(&rt_lock, LK_RELEASE);
Lock-order check: rtclean takes rt_lock alone for the rt-free section and
sockets_lockβpcb_lock separately; it never holds rt_lock+pcb_lock
together, so nesting pcb_lock inside rt_lock in bind introduces no
inversion. Built in the combined single-fix kernel (#1); boots clean.
fix_status = not_testable (no BT HW on guest).
Files
trace.mdβ line-by-line TOCTOU trace + rtclean race windowfix.diffβ hold rt_lock across pcb->rt storeREADME.md,VERDICT.md,manifest.json
DF-0497 detailed verdict
Verdict: INCONCLUSIVE at runtime β real TOCTOU UAF by inspection; no BT HW on guest
Mechanism
ng_btsocket_l2cap_raw_bind (sys/netgraph7/bluetooth/socket/...raw.c:690-715)
releases ng_btsocket_l2cap_raw_rt_lock at :703, then β holding neither
rt_lock nor pcb_lock β stores pcb->rt = rt at :714. The rtentry type has
no refcount (ng_btsocket_l2cap.h:44-48), so a concurrent
ng_btsocket_l2cap_raw_rtclean (:495 LIST_REMOVE+kfree) frees rt in the
window; bind stores a dangling pointer, later UAF-read via pcb->rt->hook.
CWE-416.
Privilege boundary
bind reachable by unprivileged user (attach sets only a flag), but the racing
rtclean needs a Bluetooth hook disconnect (BT HW / netgraph priv). The
finding's stated realistic barrier.
Why not reproduced
No Bluetooth hardware and no bluetooth netgraph modules on the audit guest
(none under /boot/modules or /modules). ng_btsocket_l2cap_raw_node is NULL
β bind returns before :690. Path is dead on this guest.
Fix (applies + compiles + boots)
fix.diff: hold rt_lock across pcb->rt = rt; release after on both paths.
Lock-order verified: rtclean never holds rt_lock+pcb_lock together, so
nesting pcb_lock inside rt_lock is safe. Built in combined kernel (#1);
boots clean. fix_status = not_testable (no BT HW).
Why no chain
Trigger gated behind BT HW/netgraph priv on this guest; the UAF cannot be exercised. Documented as a real latent bug with a verified compile-clean fix.
DF-0497 source trace β TOCTOU UAF on ng_btsocket_l2cap rtentry
The window
sys/netgraph7/bluetooth/socket/ng_btsocket_l2cap_raw.c
ng_btsocket_l2cap_raw_bind():
690: if (bcmp(&sa->l2cap_bdaddr, NG_HCI_BDADDR_ANY, sizeof(...)) != 0) {
692: lockmgr(&ng_btsocket_l2cap_raw_rt_lock, LK_EXCLUSIVE); <-- TAKE rt_lock
694: LIST_FOREACH(rt, &ng_btsocket_l2cap_raw_rt, next) {
695: if (rt->hook == NULL || NG_HOOK_NOT_VALID(rt->hook)) continue;
698: if (bcmp(&sa->l2cap_bdaddr, &rt->src, sizeof(rt->src)) == 0) break;
700: }
703: lockmgr(&ng_btsocket_l2cap_raw_rt_lock, LK_RELEASE); <-- RELEASE rt_lock *** WINDOW OPENS ***
705: if (rt == NULL) { error = ENETDOWN; goto out; }
710: } else rt = NULL;
/* between here and :714 bind holds NEITHER rt_lock NOR pcb_lock */
712: lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE); <-- take pcb_lock
713: bcopy(&sa->l2cap_bdaddr, &pcb->src, sizeof(pcb->src));
714: pcb->rt = rt; <-- STORE (possibly dangling)
715: lockmgr(&pcb->pcb_lock, LK_RELEASE); *** WINDOW CLOSES ***
The free (racing, in rtclean)
ng_btsocket_l2cap_raw_rtclean() (:450-502):
: (takes ng_btsocket_l2cap_raw_rt_lock)
: LIST_FOREACH_SAFE(rt, &ng_btsocket_l2cap_raw_rt, next, tmp) {
: if (<rt still valid>) continue;
495: LIST_REMOVE(rt, next);
: kfree(rt, M_NETGRAPH_BTSOCKET_L2CAP_RAW); <-- FREE
: }
: (releases rt_lock)
rtclean runs when a Bluetooth hook disconnects (HCI device removal / peer
disconnect / netgraph reconfig). If it runs in the bind window (:703β:714), the
rt that LIST_FOREACH handed to bind is kfree'd. bind then stores the
freed pointer in pcb->rt (:714).
Why it's a UAF (no refcount)
ng_btsocket_l2cap_rtentry (ng_btsocket_l2cap.h:44-48) has no reference
count. Nothing in bind takes a reference on rt after the lookup; releasing
rt_lock therefore drops the only protection. Subsequent operations on the pcb
dereference pcb->rt->hook (e.g. in connect/ioctl paths) without rt_lock β
read of freed M_NETGRAPH heap. The freed slot is reclaimable (slab reuse) β
partial attacker influence over the ->hook value read β potential type
confusion.
Trigger barrier (the finding's stated caveat)
binditself is reachable by an unprivileged user (attach only sets a flag).- But the racing
rtcleanneeds a Bluetooth hook disconnect, which on a real system means BT hardware present + a disconnecting peer / device removal, or netgraph privilege to tear down the lower link. On the audit guest there is no BT hardware and no bluetooth netgraph module, so the path is dead.
Fix
Hold rt_lock across the pcb->rt = rt store (release after, on both paths).
See fix.diff. Lock-order verified safe vs rtclean.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. ng_btsocket_l2cap_raw_bind TOCTOU UAF rtentry no refcount. No BT HW/modules on guest.
No comments yet.