β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-4939

if_ic: every SIOCSIFFLAGS down→up cycle leaks the ic_obuf/ic_ifbuf pair (unbounded M_DEVBUF kernel heap leak)

Summary

The down branch never frees/NULLs the buffers and the up branch unconditionally re-kmallocs - each cycle orphans 2x(mtu+ICHDRLEN) bytes. REPRODUCED 300/300 on guest via a virtual-I2C-bridge KLD harness + stock ifconfig: devbuf +2 allocs/+3008B per cycle exactly (mtu 65535 = ~128KB/cycle); NULL-guard reuse fix validated byte-flat on rebuilt if_ic.ko. Race-free, distinct from DF-2000/2001.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4939 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 3.9 KB ↓ raw
lpbbfake.c β€” 3.4 KB view raw
Makefile.harness β€” 102 B ↓ download
build.sh β€” 108 B view raw
run.sh β€” 90 B view raw
build.log β€” 1.9 KB view raw
run.log β€” 2.8 KB view raw
env.txt β€” 596 B view raw
fix.diff β€” 780 B view raw
verdict.json β€” 3.2 KB view raw

DF-4939 — if_ic: unbounded kernel heap leak on every SIOCSIFFLAGS down→up cycle

File: sys/dev/netif/ic/if_ic.c (icioctl, lines 179–202) Verdict: REPRODUCED (leak) on the QEMU guest, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, using a virtual-I2C-bridge KLD harness (lpbbfake.c, included). Fix VALIDATED: patched if_ic.ko shows a byte-flat M_DEVBUF budget across the same 200-cycle workload that leaked ~600 KB unfixed.

Prerequisite (no I2C hardware needed)

The ic driver only attaches as a child of iicbus, which needs an I2C master controller. The harness KLD registers a fake lpbb bit-bang bridge on ISA with virtual lines (SDA always-ACK, SCL always-high), producing lpbb0 β†’ iicbb0 β†’ iicbus0 β†’ ic0 on any machine.

Build

# on the guest (root):
mkdir -p /root/harness && cd /root/harness
cp <pack>/lpbbfake.c <pack>/Makefile.harness ./Makefile   # see Makefile.harness
make                       # -> lpbbfake.ko

Run

./run.sh

which performs, in order:

  1. kldload lpbbfake.ko; kldload iicbb; kldload iicbus; kldload if_ic
  2. ifconfig ic0 10.99.99.1 10.99.99.2 netmask 255.255.255.252 up
  3. baseline vmstat -m (M_DEVBUF row)
  4. 200 Γ— (ifconfig ic0 down; ifconfig ic0 up)
  5. vmstat -m again β†’ compare M_DEVBUF InUse count/KB

Expected (unfixed kernel)

devbuf  219   214K     (before)
devbuf  619   814K     (after 200 cycles)   <- +400 allocs, +600K (2 x 1504 B/cycle)

Second round of 100 cycles adds another +200 allocs / +300K (run.2.log) β€” the leak is unbounded and linear in the number of cycles. With ifr_mtu = 65535 (max allowed by if.c:2272), each cycle leaks 2 Γ— 65539 B, so a root shell looping ifconfig ic0 down; ifconfig ic0 up exhausts kernel heap (local DoS / memory-pressure amplifier; SIOCSIFFLAGS requires SYSCAP_RESTRICTEDROOT, if.c:2135).

Fix validation (fix.diff)

Apply fix.diff to a guest copy of /usr/src/sys/dev/netif/ic/if_ic.c, rebuild if_ic.ko, reboot, repeat step 3–5: M_DEVBUF stays 219 / 214K β†’ 219 / 214K (byte-flat). Observed and recorded in run.log (section FIX-CYCLES-DONE).

Root cause

icioctl SIOCSIFFLAGS down-branch (if_ic.c:179–187) releases the iicbus and clears IFF_RUNNING but never frees (nor NULLs) sc->ic_obuf/sc->ic_ifbuf. The up-branch (if_ic.c:188–202) unconditionally kmallocs two fresh buffers and overwrites the pointers, orphaning the previous pair. Repeat forever.

VERDICT.md
↓ download raw

DF-4939 VERDICT β€” REPRODUCED (kernel heap leak, unbounded, privileged trigger)

What was claimed

sys/dev/netif/ic/if_ic.c icioctl() SIOCSIFFLAGS: the down branch (if_ic.c:179-187) never frees sc->ic_obuf / sc->ic_ifbuf, and the up branch (if_ic.c:193-197) unconditionally re-kmallocs both, overwriting the pointers. Every down→up cycle leaks 2 × (if_mtu + ICHDRLEN) bytes of M_DEVBUF. Unbounded: a privileged loop (while :; do ifconfig ic0 down; ifconfig ic0 up; done) exhausts the kernel heap.

How it was reproduced (guest, 2026-09-25)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026, X86_64_GENERIC, clean-source snapshot; sys/dev/netif/ic/if_ic.c sha256 3ff9e187... (identical to host audit tree).

No I2C controller exists in QEMU, so the ic driver (child of iicbus) cannot attach on stock hardware. A test-harness KLD (lpbbfake.c in this pack) registers a driver named "lpbb" on the ISA bus β€” the exact bus name iicbb attaches to (iicbb.c:444 DRIVER_MODULE(iicbb, lpbb, ...)) β€” and implements the iicbb_if bit-bang methods against virtual lines (SDA reads 0 = ACK, SCL reads 1). This yields lpbb0 β†’ iicbb0 β†’ iicbus0 β†’ ic0 with no hardware. All subsequent exercise of the bug is via stock ifconfig(8) only; the harness never touches if_ic internals.

Observed (full transcript in run.log):

kldload lpbbfake.ko, iicbb, iicbus, if_ic   -> ic0 attached
ifconfig ic0 10.99.99.1 10.99.99.2 netmask 255.255.255.252 up
vmstat -m : devbuf   219   214K   (baseline)
200 x (ifconfig ic0 down && ifconfig ic0 up)
vmstat -m : devbuf   619   814K   (+400 allocs, +600 KB)

+400 allocations = exactly 2 per cycle (ic_obuf + ic_ifbuf), +600 KB β‰ˆ 200 Γ— 2 Γ— 1504 B. run.2.log: a further 100 cycles β†’ 819 allocs / 1.09M (+200, +300 KB): linear, unbounded, deterministic (not a race β€” 300/300 cycles leaked).

Impact

Kernel memory exhaustion DoS (VM pressure, allocation failures system-wide) driven by an otherwise-inert config loop. Trigger is privileged (SIOCSIFFLAGS is gated by caps_priv_check SYSCAP_RESTRICTEDROOT, sys/net/if.c:2135) and requires an if_ic instance (I2C-attached system, or the same virtual-bridge technique if an attacker can load modules). With if_mtu at the generic max 65535 (if.c:2272), the leak rate is ~128 KB/cycle. Severity Low (privileged trigger, leak class).

Non-overlap: DF-2000 (SIOCSIFMTU free-vs-use UAF race) and DF-2001 (NULL deref pre-alloc) both concern the same buffers but different defects; this finding is the steady-state lifecycle leak, no race involved.

Fix validation

fix.diff (authored after reproduction): up-branch allocates only when the respective pointer is NULL (buffers retained across down are reused). Applied to the guest's /usr/src copy, if_ic.ko rebuilt (build.log.fix), installed to /boot/kernel, guest rebooted, identical 200-cycle workload:

vmstat -m : devbuf   219   214K   (before)
vmstat -m : devbuf   219   214K   (after 200 cycles)  <- byte-flat

Bad behavior GONE. Guest then reset to clean-source snapshot.

Kernel references

Observations, limitations

  • The one-off shutdown hang noted during teardown (vm.sh down after heavy cycling, console log overwritten by the next boot) was NOT characterized and is NOT counted as part of this finding.
  • Leak is M_DEVBUF-typed; vmstat -m InUse count/KB used as the observable.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched if_ic.ko (fix.diff) rebuilt and installed on the guest; the exact baseline workload (200 down/up cycles) no longer leaks: M_DEVBUF InUse stays 219 allocs / 214K before and after (baseline unfixed: +400 allocs / +600K).

run.log FIX VALIDATION section (FIX-CYCLES-DONE); build.log.fix note in build.log
↓ fix.diffper-fix-DF-4939

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: baseline devbuf 219/214K -> 619/814K after 200 cycles -> 819/1.09M after +100 (exactly 2 allocs per cycle); FIX section of run.log: patched module byte-flat 219/214K -> 219/214K across the same 200 cycles. Harness lpbbfake.c + Makefile.harness attach ic0 with no i2c hardware.

PoC changes

Pass-1-style PoC sketch did not exist for this finding (new pass-2 finding). Had to invent the reproduction environment: guest has no I2C controller, so a virtual 'lpbb' bit-bang bridge KLD (lpbbfake.c) was written to instantiate lpbb->iicbb->iicbus->ic0. Also: ifconfig requires a destination address for the point-to-point ic0 ('ifconfig ic0 A B netmask ... up').

Verified recommended fix

In icioctl SIOCSIFFLAGS up-branch, allocate ic_obuf/ic_ifbuf only if the pointer is NULL (reuse the pair retained across down), per fix.diff.

Verdict

REPRODUCED: every SIOCSIFFLAGS down->up cycle on if_ic leaks 2 x (if_mtu+4) bytes of M_DEVBUF because the down branch (if_ic.c:179-187) never frees ic_obuf/ic_ifbuf and the up branch (if_ic.c:193-197) unconditionally re-kmallocs both. Demonstrated on the QEMU guest with a virtual-I2C-bridge harness KLD: 300/300 cycles leaked, M_DEVBUF grew 219->619->819 allocs (214K->814K->1.09M) for 200+100 cycles, exactly +2 allocs/+3008B per cycle. Deterministic, unbounded, privileged trigger (root loop can exhaust kernel heap). Fix (allocate-only-if-NULL in the up branch) validated by rebuilding if_ic.ko on the guest: same 200-cycle workload is byte-flat (219/214K -> 219/214K).