# DF-2309 — VERDICT: reproduced (linear, permanent kernel-heap leak → deterministic panic at the malloc-type limit; fix validated)

## Bottom line

`iicioctl(I2CRDWR)` at `sys/bus/iicbus/iic.c` allocates `usrbufs`
(sizeof(void*) * d->nmsgs, zeroed, M_TEMP) at :352 before `copyin`ing the
user `msgs` array at :353; when that copyin fails the `break` at :355
reaches an epilogue that frees only `buf` (:385-386) — `usrbufs` is freed
only on the success path (:372). Each failing call permanently leaks
8*nmsgs bytes of wired kernel heap. Verified live on the DragonFly
6.5-DEVELOPMENT guest with the production i2c stack (DF-5539 harness rig;
setup patches DF-5559/DF-5560 disclosed, I2CRDWR logic under test stock):

* 20-call calibration: 16.8 MB consumed; `vmstat -m` type `temp` Count
  +20, MemUse 332K → 7.98M — precisely one live 800 KB object per failed
  call (`buf` freed, `usrbufs` retained). Requests counter +2/call.
* 300-call baseline: **230.5 MB consumed vs 228.9 MB predicted** —
  linear at 8*nmsgs per call, no plateau, `temp` Count +320.
* Module unload/reload does not return the memory (global M_TEMP leak,
  not pool retention).
* **Exhaustion endpoint**: DragonFly enforces a per-malloc-type limit
  (`temp` Limit = 390M on this guest, seen in `vmstat -m`). With the
  leaked total pushed past it (~490 calls), the next in-ioctl kmalloc
  panics: `panic: temp: malloc limit exceeded` with backtrace
  `_kmalloc() <- iicioctl() <- dev_dioctl() <- devfs_fo_ioctl() <-
  mapped_ioctl()` (panic.txt, serial capture). The DoS is a guaranteed
  kernel panic, reachable purely by repeating a failing ioctl.
* **Fix validated**: one-line `kfree(usrbufs, M_TEMP);` before the error
  `break` (fix.diff), only iic.ko rebuilt/reloaded. Identical 300-call
  workload: 2.1 MB consumed (measurement noise; `temp` Count flat, no new
  live objects), 300/300 calls still EFAULT, and a follow-up 200-call run
  on the patched module also ~0. Leak eliminated, error semantics
  preserved, no double-free (the success path frees at :372 and is not
  reached after the break).

## Threat model / impact ceiling

`/dev/iicN` is 0600 root:wheel (iic.c:128-130): on a default
configuration the trigger requires root (impact of the leak itself is
then limited to self-inflicted starvation/panic). On embedded or
devfs-ruleset configurations that expose the node to unprivileged users,
this is an unauthenticated-per-open repeatable kernel-memory-exhaustion
+ deterministic-panic DoS: ~390 MB / ~490 calls at nmsgs=100000 (the
per-call leak is attacker-chosen up to 8*2^32 bytes in principle via
nmsgs, bounded in practice by kmalloc limits and the type limit).

nmsgs bounds worth noting (adjacent, out of scope here): the kmalloc
multiplications at :351/:352 happen before any sanity clamp of nmsgs —
huge nmsgs mainly produce giant transient `buf` allocations plus the
matching `usrbufs` leak; DF-2312 tracks nmsgs bounding separately.

## Reproduction detail

`usrbufleak.c` issues `ioctl(fd, I2CRDWR, &(struct iic_rdwr_data){
.msgs = (void *)0x10, .nmsgs = 100000 })` in a loop; every call returns
-1/EFAULT (any other result aborts). It samples
`vm.stats.vm.v_free_count` every K calls; run.sh snapshots `vmstat -m`
type `temp` around the phases. Numbers above are from the canonical
re-run (guest instance 2, after `vm.sh reset with-src`); the exhaustion
panic was executed on guest instance 1 (serial capture kept — panics are
authoritatively read from the serial console; the run's calib/phase-A/B
logs from that instance were superseded by the canonical re-run with
identical results: 229.8 MB / Count +320 on instance 1 vs 230.5 MB /
Count +320 on instance 2).

## Fix

`fix.diff` — free `usrbufs` on the copyin-failure path before `break`:

```diff
 		error = copyin(d->msgs, buf, sizeof(*d->msgs) * d->nmsgs);
 		if (error)
+			kfree(usrbufs, M_TEMP);
 			break;
```

No double-free: the success path's `kfree(usrbufs)` at :372 is skipped by
the `break`, and M_WAITOK kmallocs in this case cannot fail between :352
and :353. Validated fixed on the guest (see above).

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