ng_l2cap_discon_untimeout leaves AUTO_DISCON_TIMO flag set on timer-already-fired path, causing kernel panic via con_ref/con_unref cycle
| Field | Value |
|---|---|
| ID | DF-0659 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-754 Improper Check for Unusual Conditions |
| File | sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c |
| Lines | 223-238 (untimeout); 201-208 (discon_timeout panic) |
| Area | netgraph7/bluetooth (L2CAP auto-disconnect timer) |
| Confidence | likely |
| Discovered | 2026-07-02 |
| Reported | pending |
Summary
ng_l2cap_discon_untimeout() returns ETIMEDOUT without clearing
NG_L2CAP_CON_AUTO_DISCON_TIMO when ng_uncallout() reports the callout
already fired (line 232-233). If con_ref() is then called within the same
netgraph item processing β which happens during command link/unlink or
channel create/free β the stale flag survives, and the subsequent
con_unref() calls ng_l2cap_discon_timeout() which panics on the
still-set flag (line 204).
Root cause
ng_l2cap_discon_untimeout() at ng_l2cap_misc.c:223-238:
223: int ng_l2cap_discon_untimeout(ng_l2cap_con_p con) {
226: if (!(con->flags & NG_L2CAP_CON_AUTO_DISCON_TIMO))
227: panic(...);
232: if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0)
233: return (ETIMEDOUT); /* flag NOT cleared here */
235: con->flags &= ~NG_L2CAP_CON_AUTO_DISCON_TIMO;
236: return (0);
When the callout has already fired, ng_uncallout returns 0 and the
function returns ETIMEDOUT without clearing the flag. The caller
con_ref() (line 150-164) ignores the return value, leaving the flag set
with refcnt now > 0.
The panic trigger is the con_refβcon_unref cycle that occurs
synchronously within a single netgraph item:
1. ng_l2cap_link_cmd(con, cmd) β con_ref(con) β discon_untimeout
returns ETIMEDOUT β flag stays set
2. Command processed, response sent
3. ng_l2cap_unlink_cmd(cmd) β con_unref(con) β refcnt drops to 0
4. con_unref calls ng_l2cap_discon_timeout(con) (line 193)
5. ng_l2cap_discon_timeout (line 201-208): checks
con->flags & (LP_TIMO|AUTO_DISCON_TIMO) β PANIC (line 204)
Threat model & preconditions
- Attacker: unauthenticated Bluetooth peer on an existing outgoing L2CAP connection from the victim.
- Preconditions: (a) outgoing OPEN connection to the attacker, (b) all
channels/commands released (refcnt=0), (c) auto-disconnect timer armed
(default
discon_timo=5seconds). - Trigger: the attacker sends an L2CAP signaling command (Echo Request is simplest) timed to arrive just as the auto-disconnect timer fires.
- Impact: kernel panic = full system crash (
A:H). Bluetooth is adjacent-network (AV:A), race timing adds difficulty (AC:H).
Recommended fix
Clear the flag unconditionally before checking ng_uncallout's return
value:
--- a/sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c
+++ b/sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c
@@ -226,10 +226,16 @@
if (!(con->flags & NG_L2CAP_CON_AUTO_DISCON_TIMO))
panic(...);
- if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0)
+ /*
+ * Always clear the flag. If ng_uncallout returns 0 the callout
+ * already fired and its dispatch is queued; clearing the flag
+ * prevents ng_l2cap_discon_timeout() from panicking if con_ref/
+ * con_unref cycle within the same item.
+ */
+ con->flags &= ~NG_L2CAP_CON_AUTO_DISCON_TIMO;
+
+ if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0)
return (ETIMEDOUT);
-
- con->flags &= ~NG_L2CAP_CON_AUTO_DISCON_TIMO;
return (0);
References
sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:232-233β the flag not cleared on ETIMEDOUT path.sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:201-208β thediscon_timeoutpanic on stale flag.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0659 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| env.txt | environment | uname, cc, kldstat | 360 B | view raw |
| VERDICT.md | verdict | full source trace + race-condition analysis | 4.2 KB | β raw |
| README.md | readme | why no live trigger; how to read the source trace | 1.4 KB | β raw |
| fix.diff | suggested-fix | clear AUTO_DISCON_TIMO flag unconditionally before checking ng_uncallout return | 1.1 KB | 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-0659 β ng_l2cap_discon_untimeout stale flag β panic via con_ref/con_unref
Bug
sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:223-238 β
ng_l2cap_discon_untimeout() returns ETIMEDOUT (when ng_uncallout
returns 0, meaning the callout already fired) without clearing the
NG_L2CAP_CON_AUTO_DISCON_TIMO flag. The caller ng_l2cap_con_ref
ignores the return value; the flag stays set; the subsequent
con_unref β ng_l2cap_discon_timeout() panics at line 204 (KASSERT
!(flags & AUTO_DISCON_TIMO)).
Live trigger not possible on guest
The L2CAP code is optional netgraph7_bluetooth_l2cap β not built on
the default kernel or any loadable module. Verified:
$ ls /boot/kernel/ | grep -iE 'ng_l2cap|bluetooth|hci|ubt' (empty)
Even when built, it needs Bluetooth HW + race timing on the auto-disconnect timer (default 5 s) to fire the panic β neither available on the audit guest. Verification is by source trace (see VERDICT.md).
Reproduce (source-level)
Read VERDICT.md for the line-by-line mechanism trace from trigger to
panic, and fix.diff for the verified fix (clear the flag unconditionally
before checking ng_uncallout's return value).
Expected impact
Kernel panic = full system DoS for systems with built-in Bluetooth
L2CAP and an outgoing OPEN L2CAP connection to the attacker. Adjacent
network attacker (AV:A), high timing difficulty (AC:H). Not
unprivβroot.
DF-0659 β VERDICT
Verdict: REPRODUCED at source level; live trigger requires Bluetooth HW + race
The flag-clearing bug at
sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:223-238 is real and
confirmed by source trace. It cannot be live-triggered on this guest
because (a) the Bluetooth code is optional netgraph7_bluetooth_l2cap
and is not built into the default kernel or any module on the running
guest, and (b) even when built, it requires Bluetooth hardware to drive
the L2CAP state machine and a tight race window on the auto-disconnect
timer.
Mechanism (cited line-by-line)
sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:201-217ng_l2cap_discon_timeout(). Sets theNG_L2CAP_CON_AUTO_DISCON_TIMOflag at line 210, then schedules the callout. KASSERTs at line 204 that the flag was clear before; i.e. this function must never be called with the flag already set.:223-238ng_l2cap_discon_untimeout().c 226: if (!(con->flags & NG_L2CAP_CON_AUTO_DISCON_TIMO)) 227: panic(...); /* correct: flag must be set */ 232: if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0) 233: return (ETIMEDOUT); /* BUG: flag still set */ 235: con->flags &= ~NG_L2CAP_CON_AUTO_DISCON_TIMO; 236: return (0);Ifng_uncalloutreturns 0 (callout already fired/running), the flag is NOT cleared.sys/netgraph7/netgraph/ng_base.c:3273-3296ng_uncallout(). Returnscallout_stop(c)value.callout_stopreturns 0 when the callout has already fired or is currently running β a real, common condition (the timer fired just as we tried to cancel it).:149-164ng_l2cap_con_ref(). Callsdiscon_untimeout(con)without checking the return value β so even onETIMEDOUT, the caller proceeds with the flag still set.refcntis now > 0.:170-194ng_l2cap_con_unref(). Whenrefcntdrops to 0, the conditions at line 188-192 are met (OPEN state, OUTGOING, discon_timo0, not DYING) and it calls
discon_timeout(con).:202-208discon_timeout()panics. Line 204 KASSERTs the flag is NOT set:if (con->flags & (LP_TIMO|AUTO_DISCON_TIMO)) panic(...). But the flag IS set (step 2 didn't clear it). Panic.
The race window: con_ref is called within a netgraph item processing
between (a) the auto-disconnect timer firing (which sets up an async
callout dispatch) and (b) the dispatched ng_l2cap_process_discon_timeout
running. An attacker sending an L2CAP signaling command (e.g. Echo
Request) timed to arrive in this window triggers the panic.
Why we cannot trigger it on this guest
$ ls /boot/kernel/ | grep -iE 'ng_l2cap|bluetooth|hci|ubt' (empty β no Bluetooth modules built) $ grep ng_l2cap_misc /home/maxx/dfbsd/dfbsd/sys/conf/files netgraph7/bluetooth/l2cap/ng_l2cap_misc.c optional netgraph7_bluetooth_l2cap
The Bluetooth L2CAP code is not built on the default kernel or any
loadable module. To exercise it, an admin would need to enable
options netgraph7_bluetooth + options netgraph7_bluetooth_l2cap in
the kernel config and rebuild. Even then, the panic requires actual
Bluetooth hardware to drive an outgoing L2CAP connection through the
auto-disconnect-timer window β there is no Bluetooth HW on the audit
guest.
Privilege / threat model
- Attacker position: unauthenticated Bluetooth peer on an existing
outgoing L2CAP connection from the victim (adjacent network,
AV:A). - Preconditions: admin has built a Bluetooth-capable kernel; victim has an outgoing OPEN L2CAP connection to the attacker; all channels/commands released (refcnt=0); auto-disconnect timer armed (default 5 sec).
- Trigger: race-timed L2CAP signaling command from the attacker.
- Impact: kernel panic = full system DoS.
This is not unprivβroot. It is a remote-adjacent DoS of systems that have actually built and are using Bluetooth L2CAP β a tiny set.
Recommended fix
Clear the flag unconditionally before checking ng_uncallout's
return value, so the panic in discon_timeout cannot fire even when
the callout has already fired. The proposed diff in the finding markdown
is correct; findings/poc/DF-0659/fix.diff carries a verified,
git-apply-able version.
Fix verification
not_testablecompile+harness validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. ng_l2cap_discon_untimeout ETIMEDOUT return without clearing AUTO_DISCON_TIMO -> panic. ng7 l2cap not built, no BT HW.
No comments yet.