# DF-3589 — VERDICT

**Status: not_reproduced on this guest (hardware-gated); mechanism proven
to the trap level with the guest's own toolchain. Confidence in the bug:
certain. Impact class: unprivileged local kernel panic (DoS).**

## Why it did not reproduce *on this guest*

`psmsmoother()` is only called from `proc_synaptics()`
(sys/dev/misc/psm/psm.c:3639) and `proc_elantech()` (psm.c:4804). Those
parsers run only when the probe identified the device as
MOUSE_MODEL_SYNAPTICS or MOUSE_MODEL_ELANTECH, and — decisive for the
*sysctl* attack vector — the `hw.psm.synaptics.*` sysctl tree that makes
the values unprivileged-writable is created **only inside a successful
`enable_synaptics()` probe** (psm.c:6741-6744, `synaptics_sysctl_create_tree`)
or `elantech_init_synaptics()` (psm.c:7209-7211).

The QEMU guest's i8042 aux device answers the IntelliMouse-Explorer probe
(dmesg: `psm0: model IntelliMouse Explorer, device ID 4`), so:

```
$ sysctl hw.psm.synaptics.multiplicator=65536
sysctl: unknown oid 'hw.psm.synaptics.multiplicator'
$ sysctl -N hw.psm.synaptics
sysctl: unknown oid 'hw.psm.synaptics'
```

No node to write, no smoother to run — `not_reproduced` on this guest is
an honest hardware-precondition result, not a refutation. QEMU does not
emulate a Synaptics touchpad, so no guest configuration short of patching
the kernel would instantiate the tree.

## Why the mechanism is certain anyway

1. **Unprivileged write is real.** Every node is registered
   `CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_ANYBODY` (e.g. psm.c:6050-6056
   `multiplicator`, 6101-6107 `div_min`, 6111-6117 `div_max`).
   `sysctl_root()` (sys/kern/kern_sysctl.c:1446) skips the
   `SYSCAP_NOSYSCTL_WR` privilege check exactly when `CTLFLAG_ANYBODY`
   is set.

2. **Validation is lower-bound-only.** `synaptics_sysctl()` case block
   psm.c:5767-5780: MULTIPLICATOR, WEIGHT_*, DIV_MIN, DIV_MAX,
   DIV_MAX_NA, DIV_LEN, VSCROLL_DIV_* → `if (arg < 1) return (EINVAL);`
   Any value up to INT_MAX (2147483647) is accepted and stored
   (psm.c:5813).

3. **The divisor is a 32-bit product of two such values.** psm.c:4307-4309
   clamps `div_x` into `[div_min, div_max]` (both attacker-chosen), and
   psm.c:4337/4338 divides / takes remainder by `div_x * multiplicator`.
   With `div_min = div_max = 65536` the clamp yields `div_x = 65536`
   regardless of the `div_len * div_max_x / len` seed; `65536 * 65536`
   is `2^32` → wraps to 0 in `int` → `idiv` by zero → #DE → kernel panic.

4. **The trap is reachable on the first movement packet.**
   `window_min` is likewise ANYBODY-writable in [1,10] (psm.c:5761-5766);
   set to 1, `psmsmoother()` reaches the division as soon as
   `smoother->queue_len >= 1` (psm.c:4262 passes on the first queued
   packet). Finger pressure only needs to clear `min_pressure`
   (default 32; also ANYBODY-writable in [0,255], psm.c:5732-5736).

5. **Machine-level proof on the guest toolchain.** `divzero_expr.c`
   contains the exact expression with the exact attacker values,
   compiled by the guest's clang and run as the unprivileged user:

   ```
   div_x=65536 multiplicator=65536
   int32 product div_x*multiplicator = 0 (wrapped; 2^32 == 0)
   Floating point exception (core dumped)   exit=136
   ```

   exit 136 = 128+8 = SIGFPE, the userland face of the same `idiv` #DE
   that panics the kernel. The compiler cannot elide the division (the
   divisor is a runtime product), so the kernel codegen is the same
   imul/idiv sequence.

## Exploit chain

`ssh user@laptop` → `sysctl hw.psm.synaptics.multiplicator=65536` +
`div_min=65536` + `div_max=65536` + `window_min=1` (all permitted to
unprivileged users by CTLFLAG_ANYBODY) → wait for/touch the touchpad →
kernel divide-by-zero panic. Values persist across the panic reboot-free
relogin (sysctl is runtime), so the panic repeats on every touchpad use
until an admin resets the nodes: a persistent, unprivileged,
touchpad-DoS-to-panic. No memory corruption; privesc is not in scope for
this class (fixed-point arithmetic trap).

## Fix validation

Not possible on this guest (node tree absent), so `fix_status` is
`not_testable` rather than `fixed`. fix.diff bounds the five divisor-
feeding nodes at 100000 (well above any sane touchpad tuning, far below
overflow: 100000^2 = 10^10 needs 64 bits, hence the companion 64-bit
widening of the product in psmsmoother for defense in depth).

## Kernel references

- sys/dev/misc/psm/psm.c:4336-4344 — `/ (div_x * multiplicator)` and
  `% (div_x * multiplicator)` (the trap)
- sys/dev/misc/psm/psm.c:4307-4309 — div_x clamp into attacker range
- sys/dev/misc/psm/psm.c:5767-5780 — `arg < 1`-only validation for
  MULTIPLICATOR/DIV_MIN/DIV_MAX/DIV_MAX_NA/DIV_LEN
- sys/dev/misc/psm/psm.c:6048-6056, 6099-6117 — ANYBODY registration of
  multiplicator/div_min/div_max (elantech flavor: 7209-7211)
- sys/kern/kern_sysctl.c:1446 — ANYBODY bypasses write privilege check

## 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.
