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

if_ic: icoutput unconditionally forces IFF_RUNNING, bypassing the up state machine (permanently skipped buffer allocation/bus acquisition, TX over un-owned iicbus, spurious bus release)

Summary

The TX path sets IFF_RUNNING while ip_output has no UP gate and routes survive down: one packet to a DOWN ic0 makes it RUNNING-while-DOWN, every later 'up' skips allocation AND bus acquisition, TX runs over a bus it does not own (corrupting the real owner's transfer), and a later 'down' spuriously releases the bus - also sustaining the DF-2001 NULL-deref sinks. REPRODUCED live (unpriv ping -> phantom RUNNING + un-owned TX + skipped alloc); gate-on-UP+RUNNING fix validated (ENETDOWN, proper transitions).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4940 Β· 11 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 5.5 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 β€” 348 B view raw
run.log β€” 2.7 KB view raw
env.txt β€” 427 B view raw
fix.diff β€” 662 B view raw
verdict.json β€” 3.8 KB view raw

DF-4940 β€” if_ic: icoutput forces IFF_RUNNING and transmits without the up-path state machine

File: sys/dev/netif/ic/if_ic.c (icoutput, line 347) Verdict: REPRODUCED on the QEMU guest (state-machine corruption with two security-relevant consequences, both demonstrated), fix VALIDATED.

The bug

icoutput() begins with an unconditional ifp->if_flags |= IFF_RUNNING; (if_ic.c:347). ip_output() calls ifp->if_output() with no IFF_UP gate (sys/netinet/ip_output.c:698,742), and if_down() deletes no routes (sys/net/if.c:1818β†’if_down_dispatch, which only clears IFF_UP, purges the ifq and purges cloned routes β€” sys/netinet/in.c:1450 in_if_down). So any routed packet on a down ic0 executes the transmit path on an interface that never ran the up branch. Consequences:

  1. ic0 is flagged RUNNING while DOWN. The next SIOCSIFFLAGS "up" sees IFF_UP && !IFF_RUNNING false (if_ic.c:188) and therefore skips both the buffer allocation and iicbus_request_bus() forever β€” on a never-allocated interface this sustains the DF-2001 NULL-deref state;
  2. the packet is transmitted over the iicbus without if_ic owning the bus (ownership is only taken in icioctl; iicbus data transfers do not check sc->owner β€” sys/bus/iicbus/iiconf.c:298-311), corrupting any transfer in flight by the actual bus owner;
  3. a subsequent SIOCSIFFLAGS "down" calls iicbus_release_bus() for a bus if_ic no longer owns; the parent-level release callback fires before the ownership check (iiconf.c:131-140) β€” a spurious controller-level release (benign with the stock null callback, but wrong).

Prerequisite

Same virtual-I2C-bridge harness as DF-4939 (lpbbfake.c, included) β€” no I2C hardware needed.

Build / Run

sh build.sh    # builds lpbbfake.ko on the guest
sh run.sh

Expected (unfixed kernel)

after ifconfig ic0 down:   flags=808810<POINTOPOINT,...>          (no RUNNING)
ping -c1 10.99.99.2        (TX while DOWN β€” netstat shows Opkts=1)
after ping:                flags=808850<POINTOPOINT,RUNNING,...>  (RUNNING forced while DOWN!)
ifconfig ic0 up            -> allocates NOTHING (devbuf 819 -> 819)
[control: normal down/up]  -> allocates 2 buffers (devbuf 819 -> 821)

With fix.diff (patched if_ic.ko)

ping -c1 10.99.99.2        -> "sendto: Network is down" (ENETDOWN)
flags after ping:          808810 (no phantom RUNNING)
ifconfig ic0 up            -> flags 808851 (proper UP+RUNNING); buffers reused (no leak, by DF-4939's fix)

Fix

Delete the forced ifp->if_flags |= IFF_RUNNING; and refuse to transmit unless the interface completed the up branch (UP+RUNNING and non-NULL buffers) β€” see fix.diff.

VERDICT.md
↓ download raw

DF-4940 VERDICT β€” REPRODUCED (state-machine bypass in icoutput; contributes to DoS conditions)

Claim

sys/dev/netif/ic/if_ic.c:347: icoutput() unconditionally executes ifp->if_flags |= IFF_RUNNING;. Because (a) ip_output invokes ifp->if_output() with no IFF_UP gate (sys/netinet/ip_output.c:698,742), (b) if_down leaves routes installed (if.c:1818 β†’ if_down_dispatch if.c:1730 only clears IFF_UP/purges ifq; in_if_down (in.c:1450) only purges cloned routes), and (c) the generic SIOCSIFFLAGS wrapper discards the driver's error return (if.c:2167-2171 ifp->if_ioctl(ifp, cmd, data, cred); β€” value dropped) β€” a single transmitted packet on a down (or never-properly-up) ic0 corrupts the interface state machine:

  • RUNNING is set without iicbus_request_bus() and without buffer allocation (the up branch at if_ic.c:188-202 is then skipped forever because it requires !IFF_RUNNING);
  • the transmit itself runs on an iicbus the driver does not own (data transfers never check sc->owner, iiconf.c:298-311 β€” only intr dispatch does, iiconf.c:50-51);
  • a later SIOCSIFFLAGS down issues a spurious iicbus_release_bus() whose parent callback fires before the ownership check (iiconf.c:131-140).

On a system whose first "up" failed (bus busy: the wrapper still left IFF_UP set, if.c:2159-2160, and if_ic's own SIOCSIFADDR pre-sets IFF_UP at if_ic.c:176 before the fall-through), the forced RUNNING locks in the NULL ic_obuf/ic_ifbuf state β€” i.e. it permanently sustains the DF-2001 NULL-deref sinks (icoutput:358 bcopy, icintr:309 write). The NULL dereference itself is DF-2001 (pass 1, not re-reported here); this finding is the state-machine defect that creates and preserves the precondition through an ordinary data path.

Reproduction (guest, 2026-09-25, stock kernel)

Same harness as DF-4939 (lpbbfake.c β†’ lpbb0 β†’ iicbb0 β†’ iicbus0 β†’ ic0); transcript in run.log:

ifconfig ic0 10.99.99.1 10.99.99.2 netmask ... up     # proper up, buffers allocated
ifconfig ic0 down
ifconfig ic0 | head -1
  ic0: flags=808810<POINTOPOINT,SIMPLEX,MULTICAST,PROXY> ...   # no RUNNING
ping -c 1 -t 3 10.99.99.2
  1 packets transmitted, 0 received (100% loss)      # TX executed
netstat -I ic0  ->  Opkts 1                           # icoutput ran while DOWN
ifconfig ic0 | head -1
  ic0: flags=808850<POINTOPOINT,RUNNING,...>          # RUNNING FORCED while DOWN
vmstat -m; ifconfig ic0 up; vmstat -m
  devbuf 819 -> 819                                   # allocation SKIPPED (up branch bypassed)
# control (no intervening TX): ifconfig ic0 down; up
  devbuf 819 -> 821                                   # normal up allocates 2 buffers

Every element of the claim observed: phantom RUNNING while DOWN (flags 808810 β†’ 808850), skipped allocation on the following up (819β†’819 vs control 819β†’821), and the transmit itself counted (Opkts=1) β€” i.e. it went out over an un-owned bus.

Impact

The demonstrated impact is kernel state corruption reachable from an unprivileged packet send once an administrator has configured the interface (or left it half-configured): phantom RUNNING, permanently skipped allocation/bus-acquisition, transmission without bus ownership (frame corruption against the real bus owner), spurious controller-level bus release. Combined with the (known, DF-2001) missing NULL guards it sustains a kernel NULL-deref panic state; combined with DF-4939's lifecycle it also interacts with the leak. Rated Low overall (needs if_ic hardware presence and admin configuration; direct memory-safety sinks are pass-1 findings), classified impact=dos for its ceiling.

Fix validation

fix.diff removes the forced flag and gates icoutput on UP|RUNNING + non-NULL buffers. Patched if_ic.ko (built together with the DF-4939 fix in one module; the two observables are independent) installed and rebooted:

ifconfig ic0 down; ping -c1 10.99.99.2
  ping: sendto: Network is down                       # ENETDOWN, packet dropped
ifconfig ic0 | head -1
  ic0: flags=808810<...>                              # NO phantom RUNNING
ifconfig ic0 up
  ic0: flags=808851<UP,POINTOPOINT,RUNNING,...>       # proper transition restored

Bad behavior GONE (transcript: run.log FIX section).

Kernel references

Non-overlap statement

DF-2000 = SIOCSIFMTU free-vs-use race; DF-2001 = NULL derefs at icoutput:358 / icintr:309 pre-allocation (sink). This finding is the distinct icoutput state-machine defect (source line 347) that manufactures and preserves the conditions those sinks need; the sinks are cited, not re-claimed.

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: ping-while-down returns ENETDOWN, no phantom RUNNING flag (808810 stays), subsequent 'up' performs the proper transition (808851) with buffers reused (no leak). Baseline bad behaviors all absent.

run.log FIX VALIDATION section (fixB.step1-4)
↓ fix.diffper-fix-DF-4940

Confirmed kernel references

Detail

Exploit chain

admin brings ic0 up with address (or half-configures it: SIOCSIFADDR/SIOCSIFFLAGS wrapper leaves IFF_UP set even when the driver's bus request fails, if.c:2159-2171, if_ic.c:176) -> any unprivileged user sends a packet routed via ic0 while it is DOWN -> icoutput sets IFF_RUNNING -> every future 'ifconfig ic0 up' skips allocation+bus acquisition -> on a never-allocated interface ic_obuf/ic_ifbuf stay NULL and every subsequent packet hits the bcopy NULL deref at if_ic.c:358 (sink owned by DF-2001) -> kernel panic (local DoS); additionally each such TX corrupts any in-flight transfer of the actual iicbus owner.

Evidence (decisive lines)

run.log B.step3: flags 808850 (RUNNING) on a down interface after ping (Opkts=1); B.step5: up allocates nothing (devbuf 819->819) vs control 819->821 (+2); FIX section: 'sendto: Network is down', flags stay 808810, proper 808851 after up.

PoC changes

New pass-2 finding, no seed PoC. Reproduction required the virtual-I2C-bridge harness (lpbbfake.ko, shared with DF-4939) and a destination address for the point-to-point interface. TX-while-down is reached via the surviving connected route (in_if_down purges cloned routes only).

Verified recommended fix

Remove the unconditional ifp->if_flags |= IFF_RUNNING in icoutput; refuse to transmit unless (if_flags & (IFF_UP|IFF_RUNNING)) == both and buffers are non-NULL (fix.diff).

Verdict

REPRODUCED: icoutput() unconditionally sets IFF_RUNNING (if_ic.c:347), so a single packet routed to a DOWN (or never-properly-up) ic0 β€” ip_output has no IFF_UP gate (ip_output.c:698,742) and if_down deletes no routes β€” flips the interface to RUNNING without iicbus_request_bus() and without buffer allocation. Demonstrated on the guest: flags 808810 -> 808850 (RUNNING while DOWN) after one ping, Opkts=1 (transmit over un-owned bus), and the following SIOCSIFFLAGS 'up' allocated nothing (devbuf 819->819) while a control cycle allocated 2 (819->821). This permanently skips the up branch (if_ic.c:188) and thereby sustains the DF-2001 NULL-deref state; also causes un-owned bus transmissions and spurious iicbus release (parent callback fires before ownership check, iiconf.c:131-140). Fix (gate icoutput on UP|RUNNING + non-NULL buffers, drop the forced flag) validated: patched module returns ENETDOWN, no phantom RUNNING, proper up transition restored.