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 theoperatorordialoutgroup, or any user able to open thecuaU*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
umcsdriver 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 matchingUSB_VENDOR_MOSCHIP/USB_PRODUCT_MOSCHIP_MCS7820or_MCS7840must be attached (umcs.c:251-254). No special privileges beyondttyU/cuaUaccess are required; in particular this is not a malicious-USB-device attack β the trigger is the local user'stcsetattr, withc_ospeedsupplied by user space, not by the device. - Reachability:
tcsetattr(fd, TCSANOW, &tio)withcfsetospeed(&tio, B0)β ttyt_paramβucom_param(usb_serial.c:1678) βumcs7840_pre_param(umcs.c:667) βumcs7840_calc_baudrate(0, ...)(umcs.c:1056) βdivisors[9] / 0β#DEtrap β 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(...).
Recommended fix
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
sys/bus/u4b/serial/umcs.c:1056-1070βumcs7840_calc_baudrate(the bug)sys/bus/u4b/serial/umcs.c:667βumcs7840_pre_paramcallersys/bus/u4b/serial/usb_serial.c:1670-1685βucom_paramexplicitly permitsB0sys/kern/tty.c:1030-1084β ttyttioctlacceptsc_ospeed == 0- POSIX-1.2017
cfsetospeed(B0)β standard "hang up" speed
Timeline
- 2026-07-14 Discovered during automated audit.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1048 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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
umcsdriver never attaches (it only probes onUSB_VENDOR_MOSCHIP/USB_PRODUCT_MOSCHIP_MCS7820|_MCS7840). - No
/dev/ttyU*or/dev/cuaU*device node exists, so the trigger PoC'sopen("/dev/cuaU0")returnsENOENT.
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 atumcs.c:1061(confirmed bygrep); harness SIGFPEs onrate==0. - Applied
fix.diffto/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. Builtumcs.komodule too. - Installed
/boot/kernel/kernel(sha25694a607β¦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.koshowsumcs7840_pre_paramopens withlea -1(%rdi),%eax; cmp $0x2fffff,%eax; ja b40β the compiler foldedrate==0 || rate>3145728into the classic(unsigned)(rate-1) > 3145727idiom β and thejatargetb40returnsEINVAL (0x16=22)before thecallq umcs7840_calc_baudrate.part.0. So forrate==0thediv %edisink (ataea) is never reached. - Harness on fixed kernel: buggy-branch still SIGFPEs (it embeds the
original logic), fixed-branch returns
-1cleanly β 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; rawd[9]/0β exit 136 proof). trigger.candrun.shretained unchanged (the canonical live-trigger PoC for a host that does have an MCS7840 adapter attached).- Updated
build.sh/run.shto build & run the harness. - Authored
fix.diff(matches finding proposal).
Fix verification
fixedVALIDATED: 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.
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.
No comments yet.