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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2343 Β· 29 files
FileTypeDescriptionSize
umcs_harness.c β€” 18.0 KB view raw
umcsh.h β€” 1.5 KB view raw
umcshctl.c β€” 1.3 KB view raw
tioc0.c β€” 2.9 KB view raw
prep.sh β€” 2.2 KB view raw
build.sh β€” 252 B view raw
run1_baseline.sh β€” 2.0 KB view raw
run2_console.sh β€” 691 B view raw
run3_fixed.sh β€” 1.5 KB view raw
run4_console_fixed.sh β€” 482 B view raw
build.log β€” 5.6 MB ↓ download
build_o0_full.log β€” 5.6 MB ↓ download
fix_build.log β€” 5.6 MB ↓ download
fix_o2_build.log β€” 5.6 MB ↓ download
run1.log β€” 4.6 KB view raw
run3.log β€” 2.6 KB view raw
run4.log β€” 796 B view raw
run5_fixed_o2.log β€” 832 B view raw
panic.txt β€” 5.4 KB view raw
panic_direct_o0_serial.log β€” 2.0 MB ↓ download
panic_firstopen_o0_serial.log β€” 33.9 KB view raw
panic_console_o0_serial.log β€” 29.1 KB view raw
adjacency.txt β€” 772 B view raw
adjacency_codegen_addendum.txt β€” 2.3 KB view raw
fix_2343.diff β€” 285 B view raw
fix_2344.diff β€” 466 B view raw
env.txt β€” 1.1 KB view raw
VERDICT.md β€” 9.3 KB ↓ raw
README.md β€” 6.5 KB ↓ raw

DF-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):

  1. direct ucom_pre_param dispatch (the exact call usb_serial.c:1680 makes);
  2. unprivileged first open of /dev/cuaU0.0 (user maxx, dialer group): ucom_open (usb_serial.c:839-842) passes the never-initialized, all-zero sc_it_in to ucom_param β†’ pre_param(0) β€” a trigger the original finding UNDERSTATED (it claimed only the console tunable + a "fragile ioctl path");
  3. USB serial console attach with hw.usb.ucom.cons_unit=0 + hw.usb.ucom.cons_baud=0 loader tunables (the finding's primary trigger): panic during ucom_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

  1. Guest up (dfbsd-qemu/vm.sh up). Push this directory to guest:/root/umcs.
  2. sh prep.sh β€” verbatim gate + harness install + UMCSH config + build umcshctl/tioc0.
  3. sh build.sh β€” make nativekernel KERNCONF=UMCSH (stock -O2 codegen) + installkernel + nm/objdump adjacency extraction.
  4. 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).
  5. -O0 semantics: edit UMCSH_VERBATIM_CODEGEN_O0 to 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; add hw.usb.ucom.cons_unit=0/cons_baud=0 to /boot/loader.conf, reboot, ./umcshctl 4 0x01 0 β†’ panic 3 (serial console captures in panic_*_o0_serial.log / panic.txt).
  6. 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

VERDICT.md
↓ download raw

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 rejects rate > divisors[8]; rate == 0 passes.
  • umcs.c:1064-1065 β€” the loop condition i < len-1 && !(rate > divisors[i] && rate <= divisors[i+1]): for rate == 0, rate > divisors[i] is false for every i (divisors[0] == 0 and the rest are positive), so the loop runs to i == 8 and 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 by rate == 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_unit to point at the device AND cons_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_param call site in the kernel is sys/kern/tty.c:1056 (TIOCSETA/SETAW/SETAF). tty.c:1033-1036 normalizes c_ispeed 0 β†’ tp->t_ospeed (nonzero after a sane setup) but leaves c_ospeed == 0 in the termios handed to ucom_param, which then rejects at usb_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 of sc_it_in.
  • Understated by the finding: the first-open route β€” sc_it_in / sc_it_out are 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 feeds c_ospeed == 0 straight into pre_param below 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 divisor 115200/1 = 115200 truncated into uint16_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_testable
per-fix-DF-2343

Confirmed kernel references

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.