Divide-by-zero and OOB read in umcs7840_calc_baudrate when rate==0
Summary
umcs7840_calc_baudrate(0...) does not reject rate==0. Search loop falls through with i==8 then line 1066 evaluates umcs7840_baudrate_divisors[9]/0 - 1-element OOB read of 9-entry array immediately followed by integer divide-by-zero. On x86 idiv traps #DE kernel panics. ucom framework treats c_ospeed==0 as valid (usb_serial.c:1671) and every sibling serial driver rejects speed 0 in pre_param; umcs7840_pre_param instead calls calc_baudrate and panics. Primary trigger: hw.usb.ucom.cons_baud=0 loader tunable USB console bypasses tty.c ispeed normalization. Unprivileged local ioctl path currently fortuitously blocked by tty.c c_ispeed normalization but fragile defense. [VERIFIED pass2: mechanism proven under -O0 semantics via 100 percent verbatim driver - Fatal trap 18 at umcs7840_calc_baudrate+0x90 with the divisors[9] OOB load at +0x84, three trigger routes incl. unpriv dialer-group first-open of cuaU0 feeding never-initialized sc_it_in; BUT shipping gcc80 -O2 codegen folds the UB - compiled rate-0 path returns EINVAL, no panic on stock binaries; latent-UB downgrade Medium to Low; rate-0 guard fix-validated]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2343 Β· 29 filesDF-2343 evidence pack
umcs7840_calc_baudrate(rate==0) in sys/bus/u4b/serial/umcs.c:1057-1069:
the bucket-search loop for (i = 0; i < len-1 && !(rate > div[i] && rate <=
div[i+1]); ++i) falls through with i == 8 for rate == 0, then
umcs.c:1066 evaluates umcs7840_baudrate_divisors[9] / rate β a
1-element OOB read of the 9-entry .rodata array (umcs.c:1053) followed
by an integer division by zero β Fatal trap 18: integer divide fault
kernel panic.
Verdict (short)
The source defect is REAL and mechanically proven β three production
trigger routes were driven through 100% verbatim umcs.c + 100% stock
ucom/usb_serial.c/tty code on the QEMU guest (kernel-compiled
verbatim-interposition harness), and all three panicked at
umcs7840_calc_baudrate+0x90 (divl):
- direct
ucom_pre_paramdispatch (the exact call usb_serial.c:1680 makes); - unprivileged first open of
/dev/cuaU0.0(usermaxx,dialergroup):ucom_open(usb_serial.c:839-842) passes the never-initialized, all-zerosc_it_intoucom_paramβpre_param(0)β a trigger the original finding UNDERSTATED (it claimed only the console tunable + a "fragile ioctl path"); - USB serial console attach with
hw.usb.ucom.cons_unit=0+hw.usb.ucom.cons_baud=0loader tunables (the finding's primary trigger): panic duringucom_attach(usb_serial.c:523-533).
BUT the panic does not manifest on the stock shipping kernel: the
guest's kernels are built CCVER=gcc80 -O2, and gcc8 folds the
divide-by-zero UB β the compiled loop-exhausted path stores
*divisor=0, *clk=0x80, ret 0 with neither the divisors[9] load
nor the division executed (disassembly in adjacency.txt), so
pre_param rejects rate 0 via its || !divisor check with EINVAL.
Verified live: pre_param(0) β EINVAL, unprivileged first open returns
fd (no panic), on two independent -O2 builds. The panics above were
produced by compiling the identical source bytes with literal -O0
codegen for the verbatim unit (UMCSH_VERBATIM_CODEGEN_O0 in
umcs_harness.c) β i.e. the bug is latent UB: dead under the current
toolchain/codegen, live under literal translation (and for any future
compiler/flags change).
The TIOCSETA* ioctl route with c_ospeed==0 is confirmed BLOCKED
(live): both-zero and mismatched-ispeed variants return EINVAL from
sys/kern/tty.c:1033-1041 + usb_serial.c:1670-1674.
PRIVESC: none. The primitive is a 4-byte .rodata OOB read
(divisors[9], value fixed by the linker β 0 here) whose only use is as
the dividend of a division that traps when rate==0; no write, no
attacker-controlled divisor, no control flow beyond the trap. Impact
ceiling = kernel panic (DoS) if a codegen ever emits the literal
division. No uid=0 path exists.
Fix (fix_2343.diff): reject rate == 0 at the top of
umcs7840_calc_baudrate β root-cause guard covering every caller
(pre_param, set_baudrate). A/B validated: on the -O0 kernel that
panicked on all three routes, the fixed kernel returns -1/EINVAL, the
unprivileged open succeeds, and the console-tunable attach completes;
no regression for good speeds (9600 β divisor 12) or good devices.
Rig
Established verbatim-interposition pattern (cf. DF-2314 pnp rig, DF-2300
fwohci rig): umcs_harness.c #includes the UNMODIFIED guest
sys/bus/u4b/serial/umcs.c (sha256-gated by prep.sh: 33f9ea99β¦bef67),
TU-compiled into a custom kernel (UMCSH = X86_64_GENERIC +
device ucom + device umcs_harness). Interposed externals (the
minimum that cannot run without hardware): usbd_do_request_proc
(simulated vendor control transfers with configurable per-register
values/failures β faithful no-write-on-error semantics), transfer
setup/start/stop/poll stubs, and newbus device_t accessors. Everything
else β ucom_attach/ucom_param/ucom_open, console block, tty.c, ttymalloc,
make_dev /dev/ttyU*+/dev/cuaU* with stock permissions β is the
stock in-kernel code. Control device /dev/umcsh0; tioc0.c drives
the real tty ioctls and the unprivileged first-open trigger.
How to reproduce
- Guest up (
dfbsd-qemu/vm.sh up). Push this directory to guest:/root/umcs. sh prep.shβ verbatim gate + harness install + UMCSH config + build umcshctl/tioc0.sh build.shβ make nativekernel KERNCONF=UMCSH (stock -O2 codegen) + installkernel + nm/objdump adjacency extraction.- reboot;
sh run1_baseline.shβ stock-codegen battery: layout + divisors[9] instrumentation; calc table;pre_param(0)βEINVAL; DF-2344 poison battery; TIOCSETA-with-0 blocked (EINVAL Γ2); unprivileged first open β NO panic (folded UB). - -O0 semantics: edit
UMCSH_VERBATIM_CODEGEN_O0to 1 in /usr/src/sys/bus/u4b/serial/umcs_harness.c, rebuild, reboot:./umcshctl 3 0β Fatal trap 18 (panic 1); reboot, attach +su maxx -c '/home/maxx/tioc0 firstopen /dev/cuaU0.0'β panic 2; addhw.usb.ucom.cons_unit=0/cons_baud=0to /boot/loader.conf, reboot,./umcshctl 4 0x01 0β panic 3 (serial console captures in panic_*_o0_serial.log / panic.txt). - Fix A/B:
cd /usr/src/sys/bus/u4b/serial && patch < fix_2343.diff(+ DF-2344's fix_2344.diff), rebuild, remove tunables, reboot,sh run3_fixed.sh+sh run4_console_fixed.shβ everything green.
Files
umcs_harness.c/umcsh.h kernel harness (includes verbatim umcs.c; /dev/umcsh0) umcshctl.c, tioc0.c userland drivers (scenarios / tty ioctl + first-open) prep.sh, build.sh verbatim gate + kernel build + adjacency extraction run1_baseline.sh stock-codegen battery (run1.log) run2_console.sh console-tunable trigger (baseline: panic 3) run3_fixed.sh fixed-kernel A/B (run3.log) run4_console_fixed.sh fixed-kernel console attach (run4.log) fix_2343.diff the validated rate==0 guard fix_2344.diff DF-2344's fix (kept here, shared rig) build.log FULL stock-O2 kernel build transcript (5.8 MB) build_o0_full.log FULL -O0 build transcript fix_build.log, fix_o2_build.log fixed-kernel build transcripts run1.log, run3.log, run4.log, run5_fixed_o2.log runtime outputs panic.txt the three panic signatures + provenance panic_direct_o0_serial.log serial console, panic 1 (full) panic_firstopen_o0_serial.log serial console, panic 2 (full) panic_console_o0_serial.log serial console, panic 3 (full) adjacency.txt nm + .rodata dump + stock-O2 disassembly adjacency_codegen_addendum.txt -O0 disassembly record (the OOB load + divl) env.txt guest environment VERDICT.md full narrative manifest.json, verdict.json
DF-2343 VERDICT β divide-by-zero + divisors[9] OOB read in umcs7840_calc_baudrate (rate==0)
Status: reproduced (source semantics; see codegen caveat) β no impact on the shipping kernel binary. Impact class: panic (kernel DoS), NOT privesc.
1. Line-accuracy of the claim (all confirmed)
sys/bus/u4b/serial/umcs.c:1053βumcs7840_baudrate_divisors[]has exactly 9 entries{0, 115200, 230400, 403200, 460800, 806400, 921600, 1572864, 3145728};umcs7840_baudrate_divisors_len = NELEM(...) = 9.umcs.c:1061-1062β the pre-check only rejectsrate > divisors[8];rate == 0passes.umcs.c:1064-1065β the loop conditioni < len-1 && !(rate > divisors[i] && rate <= divisors[i+1]): forrate == 0,rate > divisors[i]is false for every i (divisors[0] == 0 and the rest are positive), so the loop runs toi == 8and exits. rate==0 is the unique fall-through input (every rate in [1, 3145728] lands in exactly one bucket because the buckets are contiguous).umcs.c:1066β*divisor = umcs7840_baudrate_divisors[i + 1] / rate;with i==8 βdivisors[9]: 1-element OOB read (4 bytes past the 36-byte array) divided byrate == 0β integer divide fault. Note the finding's phrasing "if the OOB byte is 0 β #DE" is slightly off: the #DE comes from the denominator (rate==0), unconditionally β the OOB dword (linker-dependent; 0x00000000 here, verified at runtime and in the file image) is only the dividend.
2. Mechanical verification
Rig: verbatim TU-interposition harness (established DF-2314/DF-2300
pattern); umcs_harness.c #includes the guest's unmodified umcs.c
(sha256 33f9ea99β¦bef67 gated), compiled into custom kernel UMCSH
(X86_64_GENERIC + device ucom + device umcs_harness). Only
hardware-dependent externals are interposed (usbd_do_request_proc
simulated vendor control transfers; transfer setup/start/stop stubs;
device_t accessors). ucom framework (usb_serial.c), tty.c, ttymalloc,
make_dev are 100% stock in-kernel code and ran for real: attach created
/dev/ttyU0.0-3 + /dev/cuaU0.0-3 with stock permissions.
2a. The panic (literal source semantics, -O0 verbatim unit)
Three routes, one identical fault:
| # | route | evidence |
|---|---|---|
| 1 | direct umcs7840_callback.ucom_pre_param(ucom, t.c_ospeed=0) β the exact call usb_serial.c:1680 makes |
panic.txt / panic_direct_o0_serial.log: umcs7840_calc_baudrate+0x90 β umcs7840_pre_param+0x29 |
| 2 | unprivileged first open of /dev/cuaU0.0 (maxx, dialer): stock ucom_dev_open β ucom_open β usb_serial.c:839-842 passes the never-initialized all-zero sc_it_in β ucom_param β pre_param |
panic_firstopen_o0_serial.log: same fault, current process = 867 (maxx's tioc0), trace adds ucom_param+0xa8 |
| 3 | console attach with hw.usb.ucom.cons_unit=0, hw.usb.ucom.cons_baud=0 (loader tunables β the finding's primary trigger): stock ucom_attach_tty console block usb_serial.c:511-533 sets c_ospeed = ucom_cons_baud = 0 and calls ucom_param |
panic_console_o0_serial.log: panic during umcs7840_attach, after the GPIO/MODE reads and transfer setup, trace ucom_param+0xa8 |
Faulting instruction umcs7840_calc_baudrate+0x90 = 0xffffffff80a856c6 =
divl -0x14(%rbp) (the /rate), preceded at +0x84 by
mov 0xffffffff80d99240(,%rax,4),%eax with rax = i+1 = 9 β the
divisors[9] OOB load (adjacency_codegen_addendum.txt). Runtime
instrumentation (scenario 1) and the linked image (adjacency.txt) both
show divisors[9] == 0x00000000 in this build β the OOB read is into
mapped .rodata (no fault from the load itself; the trap is the division).
2b. The codegen caveat β why the shipping kernel does NOT panic
The guest kernel (and the harness kernel by default) are built
CCVER=gcc80 (gcc 8.3) -O2. At that codegen gcc produces
umcs7840_calc_baudrate.part.0 in which the loop-exhausted (rate==0)
path is folded β division-by-zero is UB, and gcc emits
*divisor=0; *clk=0x80; return 0 with neither the OOB load nor the
division (adjacency.txt disasm, path at +0x33; the only div %edi is in
the bucket-found path where rate > divisors[i] β₯ 0). Verified live on
two independent -O2 builds (run1.log):
- umcshctl 3 0 (pre_param dispatch) β returned 22 (EINVAL, no panic);
- unprivileged first open of cuaU0.0 β open returned fd=3 -- NO PANIC
(post-open termios ospeed=0 β the initial param was rejected);
- pre_param(9600) β accepted; pre_param(3145729) β EINVAL.
So: the claimed DoS is dead on the current stock binary; the defect is
live C-source UB whose manifestation depends on codegen. It was
mechanically resurrected by compiling the identical source bytes with
#pragma GCC optimize("O0") around the verbatim include
(UMCSH_VERBATIM_CODEGEN_O0), and all three panics above were captured
on that build.
2c. Trigger analysis vs the original finding
- Primary claimed trigger (console, cons_baud=0): CONFIRMED as a live
route β but note it needs two loader tunables (
cons_unitto point at the device ANDcons_baud=0; defaults -1/9600 select nothing), i.e. admin misconfiguration; the panic fires at device attach (boot or hotplug), not from user input. - The finding's ioctl claim ("fortuitously blocked by tty.c c_ispeed
normalization"): CONFIRMED, with the mechanism pinned more precisely.
The only
t_paramcall site in the kernel issys/kern/tty.c:1056(TIOCSETA/SETAW/SETAF). tty.c:1033-1036 normalizesc_ispeed0 βtp->t_ospeed(nonzero after a sane setup) but leavesc_ospeed == 0in the termios handed toucom_param, which then rejects atusb_serial.c:1670-1674(c_ispeed && c_ispeed != c_ospeedβ EINVAL). Live results (run1.log, tioc0): both-zero β EINVAL; ispeed=19200/ospeed=0 β EINVAL; CIGNORE variant β OK but tty.c:1052 skips t_param entirely. Root-only init-state nodes (/dev/ttyiU*,SYSCAP_RESTRICTEDROOT, usb_serial.c:1123-1131) are the only writers ofsc_it_in. - Understated by the finding: the first-open route β
sc_it_in/sc_it_outare never initialized by driver or framework (zeroed softc; only writes are the root-gated init-state ioctl at :1131), so the FIRST open of any/dev/ttyU*//dev/cuaU*node feedsc_ospeed == 0straight intopre_parambelow the tty ioctl layer (usb_serial.c:839-842). ttyU is 0600 root:wheel but cuaU is 0660 uucp:dialer** (usb_serial.c:483-484) β a dialer-group unprivileged user triggers it. This route needs no tunables and no console β only a plugged-in MCS7840 adapter (or the umcs7840 driver attached to one). On the shipping -O2 binary this route "only" leaves the port un-parametrized (EINVAL swallowed at :842); on literal codegen it panics (panic 2).
3. Exploitability / privesc assessment (team directive)
No privilege-escalation path. The primitives are: (a) a 4-byte read-only-data OOB read whose value is fixed by the linker (0 here) and is only ever used as a dividend, and (b) a division by zero whose only observable effect is the #DE trap β panic/reboot. No write primitive, no attacker-controlled divisor (rate is the denominator, and it must be 0 to reach the path), no repeatable partial state β the function never returns on the faulting path. Impact ceiling: kernel panic = local DoS, and even that only under non-shipping codegen today. Gates: physical USB device (MCS7840/7820 adapter) + (console route) admin tunables, or (open route) dialer-group membership. Recommended severity: Low (latent UB + codegen-fragile), down from Medium.
4. Fix validation (fix_2343.diff)
rate == 0 guard at the top of umcs7840_calc_baudrate (root cause;
covers pre_param, set_baudrate/cfg_param, cfg_open). A/B on the SAME
-O0-semantics kernel family that panicked (run3.log, run4.log,
run5_fixed_o2.log):
| behavior | baseline (-O0) | fixed (-O0) | fixed (-O2, stock) |
|---|---|---|---|
calc(0) direct |
Fatal trap 18 | rc=-1, outputs untouched (0xDEAD/0xEE) | rc=-1 |
pre_param(0) |
panic / EINVAL | EINVAL 22, alive | EINVAL 22 |
| unpriv first open cuaU0.0 | panic 2 | fd=3, no panic | fd=3, no panic |
| console attach cons_baud=0 | panic 3 | rc=0, 4 ports, no panic | (same guard) |
| calc(9600) | divisor 12 | divisor 12 (no regression) | divisor 12 |
Guest left clean afterwards (vm.sh reset with-src).
5. Anomalies noted (not findings)
- At -O2,
calc(1)returns divisor115200/1 = 115200truncated intouint16_t= 49664 β pre_param accepts rate=1; benign (device just programs a wrong divisor; uint16 truncation is the driver's normal behavior for absurdly low rates). - My harness's direct
calc(rate > 3145728)call bypasses the range guard that gcc inlined into every stock caller, making the bucket walk read forward through .rodata indefinitely β a harness-only artifact (no stock caller reaches the clone without the guard); documented to keep the run logs honest.
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.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableConfirmed kernel references
- sys/bus/u4b/serial/umcs.c:1053
- sys/bus/u4b/serial/umcs.c:1066
- sys/bus/u4b/serial/umcs.c:662
- sys/bus/u4b/serial/usb_serial.c:839
- sys/bus/u4b/serial/usb_serial.c:842
- sys/bus/u4b/serial/usb_serial.c:523
- sys/bus/u4b/serial/usb_serial.c:533
- sys/bus/u4b/serial/usb_serial.c:1670
- sys/bus/u4b/serial/usb_serial.c:1680
- sys/kern/tty.c:1033
- sys/kern/tty.c:1056
Detail
Evidence (decisive lines)
panic.txt (three Fatal trap 18 signatures, RIP umcs7840_calc_baudrate+0x90=divl, traces via umcs7840_pre_param+0x29 and ucom_param+0xa8); panic_*_o0_serial.log (full serial captures); adjacency.txt (stock-O2 disasm showing the UB fold + .rodata dump: divisors[9]=0) and adjacency_codegen_addendum.txt (-O0 disasm: OOB load at +0x84, divl at +0x90); run1.log lines: preparam returned 22 EINVAL + open returned fd=3 NO PANIC + TIOCSETA EINVAL x2 + poison battery; run3.log/run4.log/run5_fixed_o2.log (fixed A/B all green)
PoC changes
rebuilt the seeded concept as a kernel-compiled verbatim-interposition rig (runtime kldload GPFs on this guest): sha256-gated TU include of umcs.c, interposed usbd_do_request_proc with simulated vendor control transfers, transfer stubs, device_t accessors; added -O0 codegen toggle after discovering the stock -O2 build folds the div-by-zero UB; harness ioctl must return 0 (raw -1 is ERESTART on DragonFly and restarts the ioctl forever); fixed ctl errno abort and groom-before-attach ordering (an intervening kprintf clobbered the DF-2344 groom)
Verified recommended fix
reject rate==0 at the top of umcs7840_calc_baudrate (covers pre_param, set_baudrate, cfg_open)
Verdict
Source defect confirmed and mechanically proven: umcs7840_calc_baudrate(rate==0) falls through the bucket search with i==8, loads umcs7840_baudrate_divisors[9] (1-element OOB read of the 9-entry .rodata array, value 0x00000000 in this link, verified at runtime and in the file image) and divides by rate==0 (umcs.c:1066) - Fatal trap 18 integer divide fault. Captured on the verbatim-TU harness kernel (stock ucom/usb_serial.c/tty.c, only control transfers simulated) via three routes: direct pre_param dispatch; UNPRIVILEGED first open of /dev/cuaU0.0 by a dialer-group user (usb_serial.c:839-842 feeds the never-initialized all-zero sc_it_in - a bigger trigger surface than the finding claimed); and the finding primary USB-console route (cons_unit=0 + cons_baud=0 tunables, panic during ucom_attach at usb_serial.c:523-533). CAVEAT that downgrades real-world impact: the shipping kernel is built CCVER=gcc80 -O2 and gcc8 folds the divide-by-zero UB - the compiled rate==0 path executes neither the OOB load nor the division and pre_param returns EINVAL via its !divisor check; verified live (pre_param(0)=EINVAL, unpriv open succeeds, two independent -O2 builds) and by disassembly. The panics required compiling the identical sha256-gated source with -O0 semantics. The claimed TIOCSETA ioctl route is confirmed blocked (tty.c:1033-1041 + usb_serial.c:1670-1674, both variants EINVAL, live). PRIVESC: none - read-only OOB dword used only as a dividend of a trapping division; no write primitive, no controlled divisor; ceiling is codegen-dependent local DoS. Recommend severity Low (latent UB, fragile against codegen change) down from Medium. Fix (rate==0 guard at top of calc_baudrate) validated on the panicking -O0 build: all three routes green (EINVAL / open OK / attach OK), no regression for good speeds.
No comments yet.