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)
PoC verification
Evidence pack
findings/poc/DF-3524 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- 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.
- ng_l2cap_lp_discon_ind (:330-332): same pattern -- DisconInd ignored, con stays OPEN with AUTO_DISCON_TIMO stuck.
- Every L2CAP signaling response (evnt.c, command_untimeout callers) is treated as "too late" -> commands never complete -> channel setup impossible -> DoS + pending-cmd leak.
- 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
fixedBaseline 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
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.
No comments yet.