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

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=5 seconds).
  • 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).

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

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0659 Β· 6 files
FileTypeDescriptionSize
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
README.md readme why no live trigger; how to read the source trace
↓ download 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.

VERDICT.md verdict full source trace + race-condition analysis
↓ download raw

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)

  1. sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:201-217 ng_l2cap_discon_timeout(). Sets the NG_L2CAP_CON_AUTO_DISCON_TIMO flag 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.
  2. :223-238 ng_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); If ng_uncallout returns 0 (callout already fired/running), the flag is NOT cleared.
  3. sys/netgraph7/netgraph/ng_base.c:3273-3296 ng_uncallout(). Returns callout_stop(c) value. callout_stop returns 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).
  4. :149-164 ng_l2cap_con_ref(). Calls discon_untimeout(con) without checking the return value β€” so even on ETIMEDOUT, the caller proceeds with the flag still set. refcnt is now > 0.
  5. :170-194 ng_l2cap_con_unref(). When refcnt drops to 0, the conditions at line 188-192 are met (OPEN state, OUTGOING, discon_timo

    0, not DYING) and it calls discon_timeout(con).

  6. :202-208 discon_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.

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_testable

compile+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.