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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5733 Β· 15 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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.c copied 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.c implements only the 10 L2CAP upper-API functions the RFCOMM code calls; the harness drives the session through the same struct btproto callback vector (connected, input) that sys/netbt/l2cap_upper.c uses β€” 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)

  1. Local app connects DLCI 6 (channel 3); session initiator sends SABM(0).
  2. Peer sends UA(0) β†’ session OPEN β†’ DLC sends PN command (mtu 127).
  3. Peer sends PN response, mtu=0, flow_control=0xe0 β†’ rfcomm_session_recv_mcc_pn response path: 0 > 32767? no; 0 > 127? no β†’ check passes β†’ dlc->rd_mtu = 0; CFC enabled on session (rfcomm_session.c:1392-1394).
  4. UA(6) β†’ DLC OPEN (MSC exchanged).
  5. 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_ALIVE in 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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
↓ fix.diffper-fix-DF-5733

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.