DF-2309 / verdict.json
{ "finding_id": "DF-2309", "status": "reproduced", "reproduced": true, "impact": "dos", "confidence": "certain", "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 (8*nmsgs, 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 8*nmsgs-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.", "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": "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.", "kernel_refs": [ "sys/bus/iicbus/iic.c:351", "sys/bus/iicbus/iic.c:352", "sys/bus/iicbus/iic.c:353-355", "sys/bus/iicbus/iic.c:372", "sys/bus/iicbus/iic.c:385-386", "sys/bus/iicbus/iic.c:128-130" ], "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.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 3300, "guest_dirty": 0, "build_cmd": "cd /root/df2309 && cc -O2 -Wall -o usrbufleak usrbufleak.c (run.sh builds it; stack rig built by /root/df2308/run.sh)", "run_cmd": "cd /root/df2309 && sh run.sh", "code_hash": "fcb4631eeb8be7d320545d2d818647abfb84ebcffd4d0b2d6cf783e9d4855a80", "notes": "Per-call leak is attacker-chosen via nmsgs (8*nmsgs bytes, bounded by kmalloc/type limits); nmsgs itself is unclamped before the kmalloc multiplications (tracked separately as DF-2312). Panic endpoint observed at the temp type limit of 390M on this 4 GB guest; other memory sizes shift the call count, not the outcome. Guest restored with vm.sh reset with-src after evidence extraction.", "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).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied to the guests /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_evidence": "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)." } |