# 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`):

```c
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:

```diff
-	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).
