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

ng_l2cap untimeout misreads cancelled-armed timers as ETIMEDOUT (DF-3515): every LP_ConnectCfm/DisconInd/signaling response is discarded -> L2CAP permanently broken, stuck-timeout con, deterministic teardown panic

Summary

ng_l2cap_lp_untimeout/command_untimeout treat ng_uncallout()==0 as 'timer already fired' and return ETIMEDOUT. Under the DF-3515 contract violation a SUCCESSFULLY-CANCELLED armed timer also returns 0, so lp_con_cfm discards every positive ConnectCfm (con never reaches CON_OPEN, handle never assigned, LP_TIMO stuck), lp_discon_ind ignores disconnects, and every signaling command-response is treated as late. Node teardown with a zombie con trips panic('timeout pending') in ng_l2cap_free_con. VERIFIED on stock guest as uid 65534: CON_IND + positive CON_CFM -> synchronous 'Operation timed out' reply (the discard directly observable), ECHO over ACL dropped, process exit -> 'panic: ng_l2cap_free_con: timeout pending!', guest down (twice). On real hardware every remote-peer connect/disconnect hits the same discard: the whole Bluetooth stack is non-functional on this kernel and teardown panics. Fix validated on rebuilt kernel #1 (kern_timeout contract patch): CFM ok, echo round-trips, clean exit. ng7-local callout_cancel alternative tested - does NOT work (same early-exit).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3524 Β· 11 files
FileTypeDescriptionSize
l2cap_con.c β€” 12.6 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 90 B view raw
run.log β€” 1.6 KB view raw
build.log β€” 59 B view raw
panic.txt β€” 2.2 KB view raw
fix.diff β€” 1.7 KB view raw
fix_run.log β€” 331 B view raw
fix_build.log β€” 192 B view raw
env.txt β€” 406 B view raw
VERDICT.md β€” 4.5 KB ↓ raw
VERDICT.md
↓ download raw

DF-3524 VERDICT β€” verified end-to-end on the QEMU guest

Status: REPRODUCED (stock kernel) β€” impact: kernel panic (+ total L2CAP connection-establishment outage + con/cmd leaks). FIX VALIDATED (kernel #1 + stock ng7 modules).

Root cause (certain)

ng_l2cap_lp_untimeout() / ng_l2cap_command_untimeout() (sys/netgraph7/bluetooth/l2cap/ng_l2cap_misc.c:493-504 / command variant) interpret ng_uncallout() == 0 as "the timer already fired" and return ETIMEDOUT:

if (ng_uncallout(&con->con_timo, con->l2cap->node) == 0)
    return (ETIMEDOUT);
con->flags &= ~NG_L2CAP_CON_LP_TIMO;

Under the DF-3515 callout_stop(9) contract violation (kern_timeout.c _callout_cancel_or_stop completes an armed STOP inline, without setting CALLOUT_PREVENTED, then early-exits with return 0 at :888-891), callout_stop() of an ARMED, not-yet-fired timer returns 0 -- so a successfully cancelled timer is indistinguishable from a fired one. callout_cancel() suffers the identical early-exit (verified by experiment: swapping stop->cancel in ng7 ng_uncallout did NOT fix the wedge; only the kern_timeout fix did).

Consequences, all in my audited file ng_l2cap_llpi.c:

  1. ng_l2cap_lp_con_cfm (:180-181): every positive LP_ConnectCfm that arrives while the LP timer is armed (i.e. every real one) is DISCARDED with ETIMEDOUT before :183-186 assigns con_handle/state -- no L2CAP connection can ever reach CON_OPEN. The synchronous control-message reply carries ETIMEDOUT to userspace (observed). NG_L2CAP_CON_LP_TIMO stays set with NO timer running (zombie con); the con descriptor + its channels leak.
  2. ng_l2cap_lp_discon_ind (:330-332): same pattern -- DisconInd ignored, con stays OPEN with AUTO_DISCON_TIMO stuck.
  3. Every L2CAP signaling response (evnt.c, command_untimeout callers) is treated as "too late" -> commands never complete -> channel setup impossible -> DoS + pending-cmd leak.
  4. Node teardown with a zombie con hits the deliberate panic("timeout pending") in ng_l2cap_free_con (ng_l2cap_misc.c:280) -- deterministic kernel panic: observed twice on the stock guest (panic.txt), backtrace ng_l2cap_free_con <- ng_l2cap_cleanup <- ng_l2cap_disconnect <- ng_destroy_hook <- ng_apply_item.

Corroboration: DF-3513's pack independently recorded the same class ("DF-3515: callout_stop() returns 0 for an armed callout, so complete_command() always takes the silent ETIMEDOUT path") wedging the HCI layer.

Reproduction (guest, X86_64_GENERIC #0, INVARIANTS)

Graph: ng7 control socket --c0-- l2cap0:hci (the socket node forges the ng_hci lower layer; ng7 data sockets have no privilege check, the whole attack loop runs as uid 65534).

# ./l2cap_con con LP_CON_IND -> ok HCI_NODE_UP -> ok LP_CON_CFM (pos) -> Operation timed out <-- cfm DISCARDED ECHO_RSP -> NOT RECEIVED (con never OPEN) (process exit) -> panic: ng_l2cap_free_con: timeout pending! state=0, flags=0x28 [guest down]

Kernel side: ng_l2cap_lp_receive: unexpected ACL data packet. Connection does not exist, con_handle=291 -- 0x123 was never assigned because llpi.c:185 never ran.

Fix validation

Applied the DF-3521-validated kern_timeout.c fix (inline-completed stop/cancel reports PREVENTED -> callout_stop(armed) returns 1), full kernel rebuild (make nativekernel, RC=0, kernel #1), STOCK ng7 modules (ng_uncallout untouched, still callout_stop):

# ./l2cap_con con LP_CON_CFM (pos) -> ok (connection accepted -> CON_OPEN) ECHO_RSP -> received 17 bytes -- echo works exit=0 (NO teardown panic)

The bug is reachable by any local user (ng7 socket attach is unprivileged) whenever an ng_l2cap node is wired to an HCI lower layer (real Bluetooth stack or any netgraph graph); on real hardware the same discard hits every remote-peer-driven connect/disconnect.

Impact ceiling: deterministic unprivileged local kernel panic + complete L2CAP protocol outage; no memory-corruption primitive found (the stuck flags only poison state, they do not corrupt memory).

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

Baseline stock: CFM -> ETIMEDOUT, no echo, teardown panic (twice). Fixed kernel: CFM -> ok (CON_OPEN), ECHO_RSP received, exit=0, no panic.

fix_run.log, fix_build.log, panic.txt
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 14 05:19:46 UTC 2026 (kern_timeout.diff only; ng7 modules stock)

Confirmed kernel references

Detail

Exploit chain

unprivileged local user (ng7 socket attach has no privilege check) wires a socket to an l2cap node's hci hook, injects LP_ConnectInd + LP_ConnectCfm + closes the socket -> stuck-LP_TIMO con -> panic at hook teardown. On real hardware the same discard hits every remote-peer connect (radio-range DoS of the whole Bluetooth stack) and the same panic fires on interface/node teardown.

Evidence (decisive lines)

run.log (stock: CFM 'Operation timed out', no echo), panic.txt (ng_l2cap_free_con timeout pending, backtrace via ng_l2cap_disconnect <- ng_destroy_hook <- ng_apply_item), fix_run.log (kernel #1: CFM ok + echo + exit 0)

PoC changes

harness built from scratch for ng7: forged ng_hci lower layer via control+data socket on l2cap:hci; struct ng_hci_lp_con_cfm_ep is {u8 status; u8 link_type; u16 con_handle; bdaddr} (10 bytes, NOT u32 status as guessed first); messages addressed via hook path 'c0'; data via sendto(hook name).

Verified recommended fix

kern_timeout.c: inline-completed stop/cancel must report CALLOUT_PREVENTED (fix.diff = the DF-3521-validated patch).

Verdict

ng_l2cap_lp_untimeout()/ng_l2cap_command_untimeout() read ng_uncallout()==0 as 'timer already fired' and return ETIMEDOUT; under the DF-3515 callout_stop contract violation every SUCCESSFULLY-CANCELLED armed timer also returns 0, so ng_l2cap_lp_con_cfm (llpi.c:180-181) discards every positive LP_ConnectCfm (synchronous reply carries ETIMEDOUT to userspace - observed), the con stays W4_LP_CON_CFM with NG_L2CAP_CON_LP_TIMO stuck and con_handle never assigned (kernel log: ACL data 'Connection does not exist' for handle 0x123), and node teardown with the zombie con hits the deliberate panic('timeout pending') in ng_l2cap_free_con - deterministic kernel panic, observed twice on stock (panic.txt). L2CAP connect/disconnect/signaling are completely broken on this kernel (every response discarded). Fixed by the kern_timeout contract fix alone (kernel #1, STOCK ng7 modules): CFM accepted, con OPEN, L2CAP echo round-trips, clean exit, no panic.