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

vlan SIOCGIFMEDIA uses an unpinned parent ifnet pointer across a serializer drop: UAF (indirect if_ioctl call through freed ifnet) racing privileged parent destroy

Summary

vlan_ioctl() SIOCGIFMEDIA - reachable UNPRIVILEGED (no caps_priv_check on that case at if.c) - captures ifp_p = ifv->ifv_p (raw pointer), drops the vlan serializer, then ifnet_serialize_all(ifp_p) + ifp_p->if_ioctl(...). The post-hoc recheck proves the authors knew ifv_p can change concurrently, but nothing pins the parent: DFly ifnets have no refcount API. A concurrent privileged parent destroy frees the ifnet with no mutual exclusion - while the query thread is between capture and use holding nothing, or inside the parent's media ioctl whose copyout can sleep for milliseconds inside the freed object. NOT reproduced (4 attempts, ~10 min, 8.5M fast + 200k fault-stretched queries vs ~130k churn cycles: freed tap_softc slab instantly re-allocated by the churn's next tap create, so the UAF reads recycled-but-valid memory silently); race stands on line-level proof. Unpriv hammer + privileged free-side co-trigger. Ceiling: kernel code-exec via freed-object function pointer with grooming; floor: panic. Fix: query the parent on netisr0 (where vlan config/unconfig and the detach-event unconfig run) or add ifnet refcounting.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3394 Β· 8 files
FileTypeDescriptionSize
race_query.c β€” 1.4 KB view raw
race_query2.c β€” 2.0 KB view raw
race_churn.c β€” 1.6 KB view raw
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 3.8 KB ↓ raw
fix.diff β€” 620 B view raw
run.log β€” 746 B view raw
verdict.json β€” 3.1 KB view raw

DF-3394 β€” vlan SIOCGIFMEDIA uses unrefcounted parent ifnet across lock drop (UAF race)

Files

  • race_query.c β€” unpriv hammer: plain SIOCGIFMEDIA loop (v1)
  • race_query2.c β€” unpriv hammer: SIOCGIFMEDIA with cold file-backed ifm_ulist (copyout page-fault stretches the freed-ifnet use window)
  • race_churn.c β€” root: tap0 create -> SIOCSETVLAN(vlan0,tap0) -> tap0 destroy loop
  • rename_exclusion n/a

Build (in guest, as unpriv user)

cc -O2 -o race_query  race_query.c
cc -O2 -o race_query2 race_query2.c
cc -O2 -o race_churn  race_churn.c     # needs <net/ether.h> include, see source

Run

# terminal 1 (unpriv): queries
/tmp/race_query vlan0 &          # v1: ~70k ioctls/s
/tmp/race_query2 vlan0 &         # v2: ~1-2k ioctls/s, ms-scale copyout faults
# terminal 2 (root): parent lifetime churn (kldload if_tap first!)
kldload if_tap
/tmp/race_churn 120

Expected (if race is won)

Guest panic: use of freed tap_softc/ifnet inside ifp_p->if_ioctl()
(sys/net/vlan/if_vlan.c:960) or lwkt serializer corruption on
ifnet_serialize_all(ifp_p) (:959) / ifnet_deserialize_all (:961),
while root-side tapdestroy() kfree()s the softc with no reference
held by the query path.

Observed

No panic in ~10 minutes of combined stress (6 CPUs): 8.5M+ fast
queries, ~200k slow (cold-page) queries, ~130k create/config/destroy
churn cycles.  Race window confirmed real by analysis; winning it
needs the freed slab to be poisoned/reused observably.
VERDICT.md
↓ download raw

VERDICT β€” DF-3394: not_reproduced (race real, outcome silent under stress)

Root cause (line-level)

vlan_ioctl() case SIOCGIFMEDIA (sys/net/vlan/if_vlan.c:949-990):

951:  ifp_p = ifv->ifv_p;                 /* raw pointer, no refcount */
957:  ifnet_deserialize_all(ifp);         /* vlan serializer dropped  */
959:  ifnet_serialize_all(ifp_p);         /* parent may be freed here */
960:  error = ifp_p->if_ioctl(ifp_p, SIOCGIFMEDIA, data, cr);
961:  ifnet_deserialize_all(ifp_p);
965:  if (ifv->ifv_p == NULL || ifv->ifv_p != ifp_p) error = EINVAL;  /* too late */

The re-check at :965 proves the authors knew the parent can be torn down concurrently, but the pointer itself is never pinned: nothing between :951 and :961 holds a reference to the parent ifnet.

The teardown side runs with no mutual exclusion against :959-961: - SIOCGIFMEDIA is unprivileged (if.c:2364-2374 β€” no caps_priv_check). - Parent destroy: tapdestroy() (sys/net/tap/if_tap.c:472-495) does a brief serialize/deserialize for tapifstop, then ether_ifdetach() -> if_detach() (if.c:946) fires ifnet_detach_event at if.c:958 -> vlan_ifdetach() -> vlan_unconfig() (nulls ifv->ifv_p), then returns and kfree(sc, M_TAP) frees the tap_softc with the ifnet embedded in it, while the query thread may hold nothing (between :957 and :959) or only the parent serializer (:959-961) which the free path never takes again.

Attack model

Unprivileged local user hammers SIOCGIFMEDIA on an existing vlan(4); a concurrent privileged operation (ifconfig destroy, device detach, module unload of the parent driver) frees the parent ifnet inside the window -> indirect call through freed ifp->if_ioctl, serialize/ deserialize on freed embedded serializer, and reads of freed driver softc inside the parent's media ioctl. DFly ifnets have no refcount infrastructure (no if_ref), so this class is systemic.

Why stress did not win it

  1. The tight window (:951->:959, no lock) is a few instructions wide; preemption must land exactly there while the destroy completes.
  2. The wide window (inside :960) was stretched ~1000x using cold file-backed ifm_ulist pages (copyout faults -> ms-scale sleeps inside the parent's ifmedia_ioctl) β€” race_query2. But a freed tap_softc is immediately re-allocated by the next tap0 create in the churn loop (zeroed, not unmapped; DFly objcache does not poison), so the UAF reads valid-but-recycled memory and returns silently. Observable corruption (serializer-exit-without-enter INVARIANTS trip, garbage media) requires the reuse to interleave differently than it did.

Numbers (6 vCPU guest)

  • v1 fast query: 8.5M+ iterations (race_query.log excerpt in run.log)
  • churn: 26,530 + 27,975 + 4x~48k create/config/destroy cycles
  • v2 slow query: 200k+ iterations, ok-path 23% (parent attached fraction)
  • zero panics, zero INVARIANTS trips, guest stayed up throughout

Classification

status=not_reproduced, reproduced=0, impact=none(observed). The finding stands on line-level analysis (missing lifetime protection is provable from the source); reproduction requires winning a scheduling race whose failure mode is masked by immediate slab reuse.

Give vlan a stable parent reference across the outcall. Minimal hardening (without adding an ifnet refcount API): perform the parent query through netisr0 (like vlan_config/vlan_unconfig do), where configuration changes are serialized, instead of calling the parent from the ioctl thread with a raw pointer.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored (netisr0-serialization comment + intent) but kernel rebuild not performed: baseline itself did not reproduce, so post-fix delta is not observable.

fix.diff
↓ fix.diffper-fix-DF-3394

Confirmed kernel references

Detail

Evidence (decisive lines)

['VERDICT.md β€” line-level race analysis: if_vlan.c:951 read / :957 lock drop / :959-961 use; tapdestroy free with no exclusion (if_tap.c:472-495)', 'race_query2.c β€” cold file-backed ifm_ulist stretches the in-parent-ioctl window ~1000x via copyout page faults', 'run.log β€” stress numbers, zero panics across 4 runs']

PoC changes

Seed sketch rewritten entirely: v1 plain SIOCGIFMEDIA hammer; v2 added cold file-backed ifm_ulist (mmap+ftruncate+MADV_DONTNEED cycling) to park the thread inside the parent's if_ioctl via copyout faults; churner needed include for if_vlan_var.h and kldload if_tap on the guest.

Verified recommended fix

Serialize the parent media query on netisr0 (where vlan config/unconfig runs) or add ifnet refcount acquisition before the outcall at if_vlan.c:959.

Verdict

Race confirmed at source level (unprivileged SIOCGIFMEDIA on vlan uses a raw, unpinned parent ifnet pointer across a serializer drop at if_vlan.c:951-961 while the parent free path (tapdestroy kfree, if_tap.c:495) holds nothing), but 10 minutes of multi-variant stress on a 6-CPU guest (8.5M fast + 200k cold-page-faulted queries vs ~130k parent create/config/destroy cycles) produced no observable panic or INVARIANTS trip: the freed tap_softc slab is immediately re-allocated by the churn's next tap create, so the UAF reads recycled-but-valid memory silently. Filed as a real, hard-to-win UAF race; impact ceiling is an indirect call through freed kernel memory (privileged co-trigger required for the free side).