Remote kernel panic via RFCOMM PN response with MTU=0: missing lower bound poisons dlc->rd_mtu, divide-by-zero in rfcomm_dlc_start
Summary
The PN-response path validates the peer MTU only against upper bounds: mtu=0 (or 1..22) is accepted and stored into rd_mtu, and with CFC enabled by the same response the next DLC output executes credits = rd_rxsize / rd_mtu with divisor 0 - #DE trap 18 in kernel mode -> panic, deterministic and unauthenticated. Verified on the guest with the VERBATIM production files against a stub L2CAP layer: 'Fatal trap 18 ... Stopped at rfcomm_dlc_start+0x3b: divq' 2/2 fresh resets; one-line lower-bound fix validated (rejected, clean disconnect, guest up). NetBSD trunk carries the same-position fix confirming upstream recognition. Bluetooth-capable kernels only (netbt optional, not GENERIC) + an RFCOMM connection to the hostile peer. Both DLC directions poisonable. Availability only - no memory-corruption route.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5733 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 9.9 KB | view raw | |
| l2cap_stub.c | β | 3.3 KB | view raw | |
| Makefile | β | 508 B | β download | |
| build.sh | β | 209 B | view raw | |
| run.sh | β | 167 B | view raw | |
| setup.log | β | 393 B | view raw | |
| build.log | β | 9.9 KB | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 1.4 KB | view raw | |
| run.fix.log | β | 921 B | view raw | |
| panic.txt | β | 576 B | view raw | |
| env.txt | β | 414 B | view raw | |
| fix.diff | β | 582 B | view raw | |
| code_hash.txt | β | 230 B | view raw | |
| VERDICT.md | β | 5.3 KB | β raw |
DF-5733 VERDICT β REPRODUCED (kernel panic, remote-peer-triggered DoS)
Bottom line
Reproduced, deterministic, root-caused, fix validated. An unauthenticated
remote RFCOMM (Bluetooth) peer can set a DLC's negotiated MTU to zero via a
PN response frame, because the response path at sys/netbt/rfcomm_session.c:1374
checks only pn.mtu > RFCOMM_MTU_MAX || pn.mtu > dlc->rd_mtu β no lower
bound. The zero lands in dlc->rd_mtu (line 1385). The next time the DLC
starts output β app write (rfcomm_send β rfcomm_dlc_start) or app read
(rfcomm_rcvd β rfcomm_dlc_start) β sys/netbt/rfcomm_dlc.c:341 executes
dlc->rd_rxsize / dlc->rd_mtu with divisor 0 β x86-64 #DE (trap 18) in
kernel mode β trap_fatal() (sys/platform/pc64/x86_64/trap.c:787,1101) β
panic, all CPUs stopped, system halted in DDB.
Why the injected path is the production path
- The KLD compiles
rfcomm_session.c/rfcomm_dlc.c/rfcomm_upper.ccopied unmodified from/usr/src/sys/netbtβ md5 identical to the audit tree and to the guest's/usr/src(setup.log,env.txt;cbd20192a12ca5178e759d009d92fcb1a). Same compiler (cc 8.3 [DragonFly]), same KLD CFLAGS (-O2 -Werror -mcmodel=kernel). l2cap_stub.cimplements only the 10 L2CAP upper-API functions the RFCOMM code calls; the harness drives the session through the samestruct btprotocallback vector (connected,input) thatsys/netbt/l2cap_upper.cuses β no state-machine code is duplicated or modified.- Injected frames carry valid TS 07.10 FCS (same reversed-CRC-8 table as the
production file) and pass
rfcomm_session_input()'s FCS gate (rfcomm_session.c:664) β nothing is bypassed.
Trigger chain (as observed in run.log / run.2.log)
- Local app connects DLCI 6 (channel 3); session initiator sends SABM(0).
- Peer sends UA(0) β session OPEN β DLC sends PN command (mtu 127).
- Peer sends PN response, mtu=0, flow_control=0xe0 β
rfcomm_session_recv_mcc_pnresponse path:0 > 32767? no;0 > 127? no β check passes βdlc->rd_mtu = 0; CFC enabled on session (rfcomm_session.c:1392-1394). - UA(6) β DLC OPEN (MSC exchanged).
- App sends 1 byte β
rfcomm_dlc_startβcredits = 4096 / 0βFatal trap 18: integer divide fault while in kernel mode,Stopped at rfcomm_dlc_start+0x3b: divq %rcx,%eax.
Impact ceiling β why panic/DoS, not privesc
The primitive is a denominator zero, not a controlled memory write: the
CPU raises #DE at the divq; no memory is corrupted, no operand beyond
zero/nonzero is attacker-controlled, and the trap is fatal before any
result is used. There is no slab-groom/forge path from this bug. Impact is
availability: unauthenticated remote kernel panic (system halt) on any
kernel built with device bluetooth (netbt is optional bluetooth,
present in LINT64, not in GENERIC). Precondition: a local RFCOMM
connection to the hostile peer (normal usage β e.g. any serial-port
service); the panic then fires on first I/O. Both outgoing and incoming DLCs
are poisonable (incoming: PN command creates the DLC with clamped mtu
β₯ 23 at rfcomm_session.c:1334-1337, but a subsequent PN response for
that same DLC still hits the unbounded path at 1374-1385).
Reproduction & stress
- Baseline run 1 (fresh guest): panic (
run.log,panic.txt). - Baseline run 2 after
vm.sh reset with-src(fully clean guest, rebuild): identical panic (run.2.log) β deterministic across resets, cpuid/IP differ only in the ASLR-ish module load address. - No hardware needed: synthetic injection is the endorsed evidence class for this rig (no Bluetooth hardware).
Fix validation (same verbatim code, one-hunk patch)
fix.diff adds the missing lower bound (mirrors NetBSD trunk,
rfcomm_session.c v1.30 "reject invalid or unacceptable MTU"):
- if (pn.mtu > RFCOMM_MTU_MAX || pn.mtu > dlc->rd_mtu) {
+ if (pn.mtu < RFCOMM_MTU_MIN ||
+ pn.mtu > RFCOMM_MTU_MAX || pn.mtu > dlc->rd_mtu) {
Applied with patch -p3 to the module's verbatim copy of
rfcomm_session.c, module rebuilt (build-fix.log flow), guest reset, same
harness re-run with byte-identical injected frames:
after PN response: dlc->rd_mtu = 127(rejected β the "(poisoned!)" suffix is static harness text; the value is what matters)dlc state = 7(RFCOMM_DLC_WAIT_DISCONNECT), DISC(6) sent, then UA(6) closes the DLC, session sends DISC(0) (run.fix.log)- app send β returns without any trap; sysctl returns 0; guest stays up
(
uptime/GUEST_ALIVEin the ssh transcript).
Full-kernel rebuild validation was not possible/applicable: the shipped
X86_64_GENERIC does not compile netbt (optional bluetooth, absent from
GENERIC); the KLD path compiles the identical production .c files with
identical flags, which is the faithful validation for this configuration.
Guest hygiene
Guest was reset to the with-src snapshot after the fix run and is up and
clean (guest_dirty: 0).
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
fixedApplied fix.diff (one hunk, mirrors NetBSD) to the verbatim copy of rfcomm_session.c, rebuilt the KLD, reset the guest, re-ran byte-identical injection: PN response rejected (rd_mtu stays 127), DLC moved to WAIT_DISCONNECT and DISC sent, app write returned without any trap, guest stayed up. Baseline (unpatched) panicked identically before and after guest reset.
run.fix.log (rd_mtu=127, dlc state=7, [upper] disconnected(0), NO 'Fatal trap'), ssh transcript 'SYSCTL_RC=0 ... GUEST_ALIVE', build-fix flow in VERDICT.md
Confirmed kernel references
Detail
Exploit chain
remote peer (L2CAP neighbor) + local RFCOMM connection -> PN response mtu=0 poisons dlc->rd_mtu -> first rfcomm_dlc_start() executes rd_rxsize/rd_mtu with divisor 0 -> Fatal trap 18 (integer divide fault) in rfcomm_dlc_start+0x3b -> trap_fatal -> panic/system halt (remote DoS; no memory-corruption primitive, uid0 not applicable)
Evidence (decisive lines)
run.log + run.2.log: 'after PN response: dlc->rd_mtu = 0' then 'Fatal trap 18: integer divide fault while in kernel mode' / 'Stopped at rfcomm_dlc_start+0x3b: divq %rcx,%eax'; panic.txt (trap frame); run.fix.log: rd_mtu stays 127, dlc state 7 (WAIT_DISCONNECT), DISC sent, guest alive; setup.log: md5s proving verbatim production sources
PoC changes
Authored harness from scratch (no seed): verbatim-source KLD module + L2CAP stub + FCS-correct synthetic frame injector; fixed PN payload size to the true sizeof(struct rfcomm_mcc_pn)=8 and added device_if.h/bus_if.h generated headers for the module build
Verified recommended fix
Reject PN-response MTUs below RFCOMM_MTU_MIN in rfcomm_session_recv_mcc_pn: 'if (pn.mtu < RFCOMM_MTU_MIN || pn.mtu > RFCOMM_MTU_MAX || pn.mtu > dlc->rd_mtu)'
Verdict
Remote unauthenticated Bluetooth peer sends an RFCOMM PN response with MTU=0; the response path at sys/netbt/rfcomm_session.c:1374-1385 only upper-bounds the MTU, stores 0 in dlc->rd_mtu, and the first DLC output (app read or write) divides by zero at sys/netbt/rfcomm_dlc.c:341 -> kernel-mode #DE -> panic, system halted in DDB. Reproduced deterministically twice on fresh guests via a KLD harness compiling the verbatim production netbt state machine with synthetic frame injection at the same btproto->input vector L2CAP uses. Impact ceiling is availability (denominator zero, no controlled memory write, no escalation path). One-line lower-bound fix (mirrors NetBSD trunk) validated: same inputs produce a clean DISC and no panic.
No comments yet.