I2CRDWR ioctl leaks usrbufs array on copyin failure (kernel memory exhaustion DoS)
Summary
In I2CRDWR handler usrbufs allocated at :352 before copyin of msgs array at :353. If copyin fails code breaks to cleanup at :385 which only frees buf - usrbufs never freed. Each failed call leaks sizeof(void*)*nmsgs bytes. Attacker can repeat indefinitely to exhaust kernel memory. For nmsgs=100000: each call allocates ~1.6MB buf + ~800KB usrbufs copyin fails buf freed but usrbufs (~800KB) leaked. After ~1000 iterations ~800MB leaked.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2309 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| usrbufleak.c | β | 3.9 KB | view raw | |
| fix.diff | β | 521 B | view raw | |
| build.sh | β | 132 B | view raw | |
| run.sh | β | 109 B | view raw | |
| calib.log | β | 730 B | view raw | |
| run.log | β | 936 B | view raw | |
| fix_run.log | β | 910 B | view raw | |
| exhaust.log | β | 1.7 KB | view raw | |
| panic.txt | β | 541 B | view raw | |
| env.txt | β | 809 B | view raw | |
| master2309.log | β | 3.8 KB | view raw | |
| iic.fixbuild.log | β | 876 B | view raw | |
| build.log | β | 0 B | β download | |
| VERDICT.md | β | 4.3 KB | β raw | |
| verdict.json | β | 5.8 KB | view raw |
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 copyining 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
8nmsgs 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 -mtypetempCount +20, MemUse 332K β 7.98M β precisely one live 800 KB object per failed call (buffreed,usrbufsretained). Requests counter +2/call. - 300-call baseline: 230.5 MB consumed vs 228.9 MB predicted β
linear at 8*nmsgs per call, no plateau,
tempCount +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
(
tempLimit = 390M on this guest, seen invmstat -m). With the leaked total pushed past it (~490 calls), the next in-ioctl kmalloc panics:panic: temp: malloc limit exceededwith 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 errorbreak(fix.diff), only iic.ko rebuilt/reloaded. Identical 300-call workload: 2.1 MB consumed (measurement noise;tempCount 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:
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.
Fix verification
fixedfix.diff applied to the guest's /usr/src copy of sys/bus/iicbus/iic.c (on top of the two disclosed setup patches), only iic.ko rebuilt and reloaded. Baseline stock I2CRDWR logic: 300 failing-copyin calls leaked 230.5 MB (temp Count +320, one live 800 KB object per call). Patched: identical workload consumed 2.1 MB of sampling noise with Count flat at 789 and 300/300 calls still returning EFAULT; an additional 200-call run on the patched module also showed ~0 growth. Leak eliminated, error semantics preserved, no double-free (success-path kfree at :372 is skipped by the error break).
fix.diff; iic.fixbuild.log; run.log (baseline 230.5 MB / Count +320); fix_run.log (patched 2.1 MB / Count flat); exhaust.log (extra 200 patched calls ~0); panic.txt (stock exhaustion endpoint).
Confirmed kernel references
Detail
Exploit chain
open /dev/iicN (root on default configs; unprivileged where devfs rules expose the node) -> loop ioctl(I2CRDWR, {msgs = unmapped pointer, nmsgs = 100000}) -> each call leaks 800 KB of M_TEMP permanently -> after ~490 calls (~390 MB) the per-type malloc limit is exhausted and the next in-ioctl kmalloc panics the kernel (or, short of the limit, the system runs with hundreds of MB of kernel heap permanently gone). No info disclosure or code execution; pure resource-exhaustion DoS with a deterministic panic endpoint.
Evidence (decisive lines)
run.log (phase A): 300/300 EFAULT, 230.5 MB consumed vs 228.9 predicted, vmstat -m temp 332K->245M Count 469->789. calib.log: 20 calls -> 16.8 MB, Count +20 (one live object per call). fix_run.log (phase B): same workload on patched iic.ko, 2.1 MB noise, Count flat. exhaust.log: extra 200 patched calls ~0. panic.txt: serial capture 'panic: temp: malloc limit exceeded' _kmalloc<-iicioctl<-dev_dioctl (executed on a stock reload after phase B on guest instance 1). env.txt: uname/kldstat/post-phase allocator state. VERDICT.md: full narrative incl. module-reload attribution and fix double-free analysis.
PoC changes
No seed PoC existed. Authored usrbufleak.c from scratch: failing-copyin I2CRDWR loop with sysctl vm.stats.vm.v_free_count sampling every K calls plus run.sh vmstat -m type-temp snapshots and calibration/attribution/fix-validation phases. Iterations: (1) initial exhaustion probe was accidentally run against the patched module (still loaded from phase B) -- repeated on an explicitly reverted+rebuilt+reloaded stock module, which panicked as expected; (2) that panic killed the guest in DDB before evidence scp, so phases A/B were re-run canonically on a fresh reset with identical results (230.5 MB / Count +320 vs 229.8 MB / Count +320) and the panic kept as a serial-console capture.
Verified recommended fix
Free the usrbufs array on the copyin-failure path: add 'kfree(usrbufs, M_TEMP);' before the 'break' at iic.c:355 (see fix.diff; validated: leak gone, EFAULT semantics unchanged, no double-free).
Verdict
REPRODUCED on the DragonFly 6.5-DEVELOPMENT guest with production modules: iicioctl(I2CRDWR) (sys/bus/iicbus/iic.c:352,353-355,385-386) allocates usrbufs (8nmsgs, M_ZERO, M_TEMP) before copyin()ing the user msgs array and frees only buf on the copyin-failure path -- every failing call permanently leaks exactly one 8nmsgs-byte wired kernel heap object of malloc type temp. Measured with the DF-5539 harness rig (real iicbus.ko/iicbb.ko/iic.ko; setup patches DF-5559/DF-5560 disclosed, I2CRDWR logic stock): 20-call calibration 16.8 MB with vmstat -m temp Count +20 (one live 800 KB object per call at nmsgs=100000); 300-call baseline 230.5 MB consumed vs 228.9 MB predicted, Count +320, perfectly linear; kldunload/kldload of iic.ko does not return the memory (global M_TEMP). Exhaustion endpoint executed on a stock reload: pushing type temp past its per-type Limit (390M) panics the kernel -- 'panic: temp: malloc limit exceeded', _kmalloc() <- iicioctl() <- dev_dioctl() <- devfs_fo_ioctl() <- mapped_ioctl() (panic.txt, serial capture) -- so the DoS is a deterministic panic, not just gradual starvation. fix.diff (one-line kfree(usrbufs, M_TEMP) before the error break) validated by rebuilding only iic.ko: identical 300-call workload consumes 2.1 MB of noise with Count flat and every call still failing EFAULT; an extra 200-call patched run likewise ~0. No double-free (success-path free at :372 is skipped by the break). Reachability: /dev/iicN is 0600 root:wheel (iic.c:128-130), so default-config impact requires root; devfs-exposed embedded configs give unprivileged users a repeatable kernel-memory-exhaustion + guaranteed-panic DoS.
No comments yet.