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)
PoC verification
Evidence pack
findings/poc/DF-3280 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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):
- First critical section (LK_SHARED,
if.c:1322-1337): finds the targetstruct ifg_group *ifginifg_headand counts the members intolen. - Releases
ifgroup_lock(if.c:1337), thenkmalloc(len)s the reply buffer (if.c:1346). - Second critical section (
if.c:1350-1364): walksifg->ifg_membersthrough 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-2020before 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_xnamethrough a danglingifg) 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).
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)
- Reader's phase-2 ioctl enters
if_getgroupmembers(); findsifgfor "rgrpA" and countslen=16under LK_SHARED (sys/net/if.c:1322-1335). - Lock released (
if.c:1337);kmalloc(16)runs (if.c:1346). - Churner's
SIOCDIFGROUP(lo0)runsif_delgroup_locked(): removes the member,ifg_refcnt--β 0 (if.c:1231,1234) βif_destroygroup()βkfree(ifg)(if.c:1139). - Reader re-acquires LK_SHARED (
if.c:1350) and walksifg->ifg_membersthrough 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 atif.c:1366. Reader seesmember[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 atif.c:993) is behindSYSCAP_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 otherifg_groupobjects, 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 intoDragonFly 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 atif.c:2389/2391holdingifnet_mtxβ next network ioctl on the box blocks forever in D-state (wchanifnet). Observed twice during PoC bring-up (processes wedged;psshowedD0 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()atsys/sys/socket.h:247bcmpsa1->sa_lenbytes β 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
fixedfix.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)
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.
No comments yet.