# 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` `#include`s 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.)
