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

if_getgroupmembers() walks a stale struct ifg_group pointer across an ifgroup_lock release: SIOCGIFGMEMB use-after-free read reachable by unprivileged users

Summary

if_getgroupmembers() caches the struct ifg_group * found under LK_SHARED (:1324), releases ifgroup_lock (:1337), kmalloc()s (:1346), then re-acquires the lock and walks ifg->ifg_members through the unvalidated cached pointer, copying results to the caller. A concurrent SIOCDIFGROUP or if_detach() group teardown that removes the last member in that window drops ifg_refcnt to 0 and kfrees the group - the fill pass then walks freed memory. The sibling if_getgroups() does it correctly (re-walks under the re-acquired lock); only the members path caches the object. SIOCGIFGMEMB is dispatched with no privilege check - the reader side is any local user; the mutator side requires root or privileged interface destroy. DEMONSTRATED 2/9/3 stale reads across three independent runs (kernel heap contents of a freed/reused 48-byte M_IFNET object copied to userland as group-member data); no panic in ~10^7 cycles because the bucket is type-stable; no unprivileged-only trigger - ceiling is stale-read disclosure plus latent panic, not uid0. Fix validated (re-lookup + recompute members under the re-acquired lock): 0 anomalies over 1315 fetches vs 3.0M churn cycles. Also live-confirmed: DF-0272/0273/0274 all still UNFIXED (DF-0272's ifnet_mtx wedge observed twice during bring-up; SIOCDELMULTI's sa_equal bcmp belongs in DF-0274's fix scope).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3280 Β· 19 files
FileTypeDescriptionSize
README.md β€” 3.1 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
gmembr_reader.c β€” 2.6 KB view raw
gmembr_churn.c β€” 1.5 KB view raw
build.sh β€” 159 B view raw
run.sh β€” 193 B view raw
build.log β€” 9 B view raw
run.log β€” 1.3 KB view raw
run.2.log β€” 1.2 KB view raw
run.3.log β€” 655 B view raw
churn3.log β€” 426 B view raw
run.fix.log β€” 686 B view raw
churnFIX.log β€” 526 B view raw
fix.diff β€” 1.0 KB view raw
fix_validation.diff β€” 4.1 KB view raw
kbuild.log β€” 5.1 MB ↓ download
env.txt β€” 401 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.6 KB view raw

DF-3280 β€” use-after-free walk in if_getgroupmembers() (SIOCGIFGMEMB)

File: sys/net/if.c (pass 2) Severity: Medium Β· CWE: CWE-416 / CWE-367 Β· Bucket: memcorrupt (stale-pointer read)

Bug

if_getgroupmembers() (sys/net/if.c:1314-1370) implements the two-phase SIOCGIFGMEMB protocol (query length, then fetch members):

  1. First critical section (LK_SHARED, if.c:1322-1337): finds the target struct ifg_group *ifg in ifg_head and counts the members into len.
  2. Releases ifgroup_lock (if.c:1337), then kmalloc(len)s the reply buffer (if.c:1346).
  3. Second critical section (if.c:1350-1364): walks ifg->ifg_members through the pointer cached in step 1 β€” the pointer is never re-validated.

If the last member is removed between the two sections (SIOCDIFGROUP / if_detach() group teardown), if_delgroup_locked() drops ifg_refcnt to 0 (if.c:1234) and if_destroygroup() kfrees the group (if.c:1139). The fill pass then walks freed memory and copies what it finds to the (unprivileged!) caller.

The sibling function if_getgroups() is written correctly: its second pass re-walks &ifp->if_groups under the re-acquired lock and holds no cached group pointer (if.c:1289-1303). Only if_getgroupmembers() caches ifg.

Threat model

  • Reader side: any unprivileged user β€” SIOCGIFGMEMB is dispatched at if.c:2019-2020 before any privilege check.
  • Mutator side: root β€” SIOCAIFGROUP/SIOCDIFGROUP require SYSCAP_NONET_IFCONFIG (if.c:2387, if.c:2396); if_detach() group teardown is also privileged.
  • Consequence: stale kernel-heap contents copied to an unprivileged caller; the walk itself (ifgm->ifgm_ifp->if_xname through a dangling ifg) is a potential panic when the freed chunk is reused with non-group contents.

Reproduce

build.sh:  cc -O2 -o /root/gmembr_churn  /root/gmembr_churn.c
           cc -O2 -o /root/gmembr_reader /root/gmembr_reader.c
run.sh:    (root) nohup /root/gmembr_churn rgrpA lo0 >/root/churn.log 2>&1 &
           (user) /home/maxx/gmembr_reader rgrpA
expected:  reader prints "ANOMALY ... member[i]='<garbage>'" β€” data read
           through the dangling group pointer (only legit member is "lo0").
           A panic is possible; not observed (type-stable reuse, see VERDICT).

Observed anomalies: baseline runs 1/2/3 β†’ 2 / 9 / 3 stale reads (run.log, run.2.log, run.3.log). Fix kernel (fix.diff applied): 0 anomalies in run.fix.log with 1315 successful fetches.

NOTE (DF-0272 landmine): SIOCAIFGROUP error returns (EEXIST/EINVAL) leak ifnet_mtx (known DF-0272, unfixed in tree). Group names must not end in a digit (if.c:1155) β€” our first attempt used "g3280" (EINVAL on every add β†’ permanent ifnet_mtx wedge). Single churn process per ifp+group avoids EEXIST.

Fix

fix.diff β€” re-look up the group and recompute/verify the member count under the re-acquired ifgroup_lock before filling (mirrors if_getgroups). Validated: built as kernel #1 (Sep 10 03:02:42), anomalies gone, group machinery churned 3M cycles error-free (churnFIX.log).

VERDICT.md
↓ download raw

DF-3280 VERDICT β€” REPRODUCED (stale-pointer read via SIOCGIFGMEMB; impact: leak-class, panic potential)

Bottom line

The use-after-free walk in if_getgroupmembers() is real and was reproduced on the stock INVARIANTS guest three times (3 independent baseline runs: 2, 9, and 3 stale reads). An unprivileged user running the two-phase SIOCGIFGMEMB protocol against a group being created/destroyed by a privileged churner receives data read through a dangling ifg_group pointer β€” observed as successful member fetches whose contents are not the real membership (only possible member is "lo0"; the kernel returned empty/foreign entries, i.e. heap contents of a freed-and-recreated object). The fix.diff (re-lookup + recount under the re-acquired lock) was built into a full kernel and eliminated every anomaly (0/1315 fetches) while 3M+ group add/del cycles continued error-free.

How it manifests (mechanism, path-accurate)

  1. Reader's phase-2 ioctl enters if_getgroupmembers(); finds ifg for "rgrpA" and counts len=16 under LK_SHARED (sys/net/if.c:1322-1335).
  2. Lock released (if.c:1337); kmalloc(16) runs (if.c:1346).
  3. Churner's SIOCDIFGROUP(lo0) runs if_delgroup_locked(): removes the member, ifg_refcnt-- β†’ 0 (if.c:1231,1234) β†’ if_destroygroup() β†’ kfree(ifg) (if.c:1139).
  4. Reader re-acquires LK_SHARED (if.c:1350) and walks ifg->ifg_members through the stale pointer (if.c:1352-1363) β€” freed memory; whatever the chunk now contains (typically a freshly recreated group object in the same 48-byte M_IFNET bucket, mid-creation with an empty member list) is copied out at if.c:1366. Reader sees member[0]=''.

Evidence: run.log (6 ANOMALY), run.2.log (3 ANOMALY), run.3.log (3 ANOMALY) β€” grep ANOMALY. churn3.log shows the mutator side healthy (2.5M cycles, err=0).

Why impact is "leak", not uid0_privesc β€” hard blockers, documented

  • No unprivileged mutator exists. Every group-destroy path (SIOCDIFGROUP at if.c:2396, if_detach() teardown at if.c:993) is behind SYSCAP_NONET_IFCONFIG/device privilege. The unprivileged attacker can only be the reader; the race requires a concurrent privileged actor.
  • Type-stable reuse caps the primitive. sizeof(struct ifg_group) (~48B) has its own M_IFNET kmalloc bucket, populated almost exclusively by other ifg_group objects, so the stale walk overwhelmingly lands on valid (recreated) group objects β€” wrong data out, no fault. 45-160s of hammering (runs 1-3, ~10^7 ioctl cycles) produced no panic.
  • Escalation would require controlling the 48-byte bucket contents with attacker-chosen pointer values before the reader's Β΅s-scale window closes β€” no such allocation primitive is reachable from the unprivileged side.

Fix validation (mandatory β€” memory-corruption class)

  • fix.diff applied to guest /usr/src/sys/net/if.c; make nativekernel KERNCONF=X86_64_GENERIC (BUILD-EXIT=0, 1556s, kbuild.log) + make installkernel; rebooted into DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 03:02:42 UTC 2026.
  • Exact PoC re-run (run.fix.log, churnFIX.log): anom=0 over 1997 iterations / 1315 successful fetches / 110s; churn 3.0M adds+dels, err=0. Baseline bad behavior GONE; EINVAL count rose (15β†’44 baseline, 119 fixed) exactly as expected β€” the fix converts stale-walk cases into EINVAL.
  • Guest reset clean afterwards (vm.sh reset with-src).

Guest observations worth recording

  • Known finding DF-0272 reproduces trivially on the stock kernel: one SIOCAIFGROUP that returns an error (e.g. group name ending in a digit β†’ EINVAL at if.c:1155, or EEXIST) returns at if.c:2389/2391 holding ifnet_mtx β†’ next network ioctl on the box blocks forever in D-state (wchan ifnet). Observed twice during PoC bring-up (processes wedged; ps showed D0 ifnet). Not re-reported here (known), but the fix status note stands: DF-0272 unfixed in tree and trivially reachable.
  • DF-0273 (SIOCSIFDESCRβ†’SIOCSIFFLAGS fall-through) and DF-0274 (SIOCADDMULTI sa_len trust; note sa_equal() at sys/sys/socket.h:247 bcmps a1->sa_len bytes β€” same family) also unfixed in tree.

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

fix.diff applied to guest /usr/src/sys/net/if.c, kernel rebuilt (make nativekernel KERNCONF=X86_64_GENERIC, BUILD-EXIT=0) and installed; exact PoC re-run on kernel #1: 0 anomalies across 1997 iterations / 1315 successful fetches in 110s (baseline: 2/9/3 anomalies), churn error-free 3.0M cycles. Bad behavior GONE, group API preserved.

run.fix.log (anom=0), churnFIX.log (adds=dels=3030847 err=0), kbuild.log (BUILD-EXIT=0), fix_validation.diff (combined applied diff)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 10 03:02:42 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

unpriv reader -> SIOCGIFGMEMB phase-2 ioctl -> kernel walks freed ifg_group -> copies freed/reused heap contents (interface-name strings) to user. No escalation chain: (1) group destruction requires root on the mutator side (SIOCDIFGROUP SYSCAP_NONET_IFCONFIG at if.c:2396; if_detach teardown), (2) type-stable bucket reuse prevents pointer-controlled corruption, (3) window is ~us and only readable, not writable, from the unprivileged side.

Evidence (decisive lines)

["run.log / run.2.log / run.3.log: 'ANOMALY member[0]=...' lines from 3 independent baseline runs (2+9+3 events)", 'VERDICT.md: full mechanism with path:line and fix-validation narrative', "run.fix.log: 'anom=0' after 1315 ok fetches on fix kernel #1", 'churnFIX.log: 3.0M add/del cycles err=0 on fix kernel', 'fix.diff: re-lookup+recount under re-acquired ifgroup_lock']

PoC changes

group names may not end in a digit (if.c:1155 rejects, and the EINVAL error return leaks ifnet_mtx via known DF-0272 - first attempt wedged the guest); reader made robust against ENOENT spam (stats at loop top); churn takes group/ifp via argv; single churn per ifp+group to avoid EEXIST (also a DF-0272 landmine).

Verified recommended fix

In if_getgroupmembers(), re-look up the group in ifg_head and recompute/verify the member count under the re-acquired ifgroup_lock before filling the reply (see fix.diff); mirror of the safe pattern already used by if_getgroups().

Verdict

REPRODUCED on stock INVARIANTS guest (kernel #0 Jul 2 2026): an unprivileged SIOCGIFGMEMB reader racing a privileged group create/destroy churn receives data read through a dangling struct ifg_group pointer (if_getgroupmembers caches ifg at if.c:1324, releases ifgroup_lock at if.c:1337, walks ifg->ifg_members at if.c:1352 after the group may have been kfree()d by if_destroygroup if.c:1139). Three independent baseline runs: 2, 9, 3 stale reads (ANOMALY member='' where only 'lo0' is possible). No panic in ~10^7 cycles: the 48-byte M_IFNET bucket is type-stable (freed chunks recycled as recreated group objects), and every destroy path is privileged (SYSCAP_NONET_IFCONFIG / if_detach), so the primitive is capped at stale kernel-heap disclosure to an unprivileged reader - no uid0 chain exists (documented hard blockers). fix.diff (re-lookup + recount under the re-acquired lock, mirroring safe if_getgroups) built into kernel #1 Sep 10 03:02:42: 0 anomalies over 1315 successful fetches while 3.0M add/del churn cycles ran error-free. Guest reset clean.