if_addgroup() increments ifg_refcnt outside ifgroup_lock and makes the find-or-create decision non-atomically: lost-update refcount -> premature group destruction with live references (latent UAF/double-free)
Summary
if_addgroup() performs its membership/existence scans under LK_SHARED, releases the lock, then executes ifg->ifg_refcnt++ with NO lock - a non-atomic RMW on the same field that if_delgroup_locked() decrements under LK_EXCLUSIVE, permitting lost updates. The existence decision also gates creation: two concurrent creators that both see the group absent each call if_creategroup(), which inserts without a duplicate check. A lost increment leaves refcount below real membership, so a later delete drops it to 0 while members remain -> if_destroygroup() kfrees the group with live references -> stale walks, list corruption, double-free on subsequent deletes or if_detach. NOT triggerable by userland racing alone: ifioctl holds the global ifnet_mtx across the entire ioctl body (proven empirically: 45s, ~3.8M cycles, 0 anomalies). The reachable races are kernel-side and privileged: if_attach's if_addgroup(IFG_ALL) runs before ifnet_mtx, and if_detach's group teardown runs after release - i.e. SIOCAIFGROUP racing interface destroy/hot-unplug. Impact if it fires: kernel memory corruption during legitimate privileged administration; root-vs-root, hence Low. Fix: hoist allocations, do scan+create+refcount+insert atomically under LK_EXCLUSIVE.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3281 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| addgrp_race.c | β | 3.0 KB | view raw | |
| build.sh | β | 108 B | view raw | |
| run.sh | β | 163 B | view raw | |
| run.log | β | 1.3 KB | view raw | |
| fix.diff | β | 3.0 KB | view raw | |
| env.txt | β | 401 B | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-3281 β unlocked ifg_refcnt++ + duplicate-capable group creation in if_addgroup()
File: sys/net/if.c (pass 2)
Severity: Low Β· CWE: CWE-362 / CWE-105 Β· Bucket: memcorrupt (latent UAF/double-free)
Bug
if_addgroup() (sys/net/if.c:1149-1193):
- Membership/group scans run under
LK_SHARED(if.c:1160-1174), then the lock is released (if.c:1174). ifg->ifg_refcnt++(if.c:1183) executes outside any lock β a non-atomic read-modify-write on a refcount thatif_delgroup_locked()decrements underLK_EXCLUSIVE(if.c:1231) β asymmetric protection, classic lost-update.- The find-or-create decision (group absent at
if.c:1169-1172β callif_creategroup) is also made outside exclusive locking, so two concurrent creators can both see "absent" and create duplicate same-named groups;if_creategroup()inserts without a duplicate check (if.c:1116-1118).
Lost update β refcount below real membership β if_delgroup_locked() drops
it to 0 while members remain β if_destroygroup() kfrees a group with live
ifgl/ifgm references β stale walks / list corruption / double-free on
subsequent deletes.
Why it could NOT be reproduced from userland (honest negative)
ifioctl() holds the global ifnet_mtx across the entire ioctl body
(if.c:2029 β¦ if.c:2450), so any two SIOCAIFGROUP/SIOCDIFGROUP calls are
fully serialized and can never overlap inside if_addgroup(). The unlocked
increment can only race:
if_attach()'sif_addgroup(ifp, IFG_ALL)(if.c:558) β runs withoutifnet_mtx(taken later atif.c:689); concurrent driver attaches on different CPUs can overlap, andif_detach()'s group teardown, which runs afterif_detach releasesifnet_mtx(if.c:991) underifgroup_lockonly (if.c:993-996`) β i.e. interface destroy/hot-unplug racing a concurrent SIOCAIFGROUP.
Both partners are privileged/kernel-side. PoC addgrp_race.c ran two
per-interface racers (lo0, vtnet0) for 45s β ~1.9M add/del cycles each,
0 anomalous ioctl returns (run.log), guest healthy. (A per-ifp racer
design is mandatory: any benign EEXIST/ENOENT error return leaks ifnet_mtx
via the known DF-0272 bug and wedges the guest.)
Fix
fix.diff β if_creategroup_locked() helper + if_addgroup() restructured
to perform membership scan, group lookup, creation, ifg_refcnt++, and both
list insertions atomically under one LK_EXCLUSIVE section (allocations
hoisted before the lock; events invoked after release; EEXIST path frees its
allocations). The fix kernel (built together with DF-3280's fix, kernel #1
Sep 10 03:02:42) compiled clean and churned 3M+ group add/del cycles with
zero errors (../DF-3280/churnFIX.log) β behavioral baseline for the race
itself does not exist, so fix_status is not_testable.
DF-3281 VERDICT β NOT REPRODUCED from userland (ifnet_mtx serialization); static race confirmed, Low
Bottom line
The code defect is real: ifg->ifg_refcnt++ at sys/net/if.c:1183 runs
outside ifgroup_lock while every other modification of ifg_refcnt is
lock-protected, and the find-or-create decision for new groups is also made
without exclusive locking (duplicate-group creation possible). However, a
userland-only race cannot reach it: ifioctl() holds the global
ifnet_mtx for the entire dispatch (if.c:2029βif.c:2450), so all
SIOCAIFGROUP/SIOCDIFGROUP calls are serialized end-to-end. The remaining
race partners are kernel-side:
if_attach()βif_addgroup(ifp, IFG_ALL)atif.c:558β runs before if_attach takesifnet_mtx(if.c:689); concurrent device attaches (different CPUs) can overlap with each other and with an SIOCAIFGROUP.if_detach()group teardown atif.c:993-996β runs after if_detach releasedifnet_mtx(if.c:991), i.e. an SIOCAIFGROUP on interface X can overlap X's detach-driven group destruction. This is the realistic scenario:ifconfig <if> group <g>racingifconfig <if> destroy(or a hot-unplug). Both sides privileged.
Attempt made
addgrp_race.c v2: two forked racers, one per interface (lo0 / vtnet0),
churning SIOCAIFGROUP+SIOCDIFGROUP on the same group rgrpB at ~42k
cycles/s each for 45s (~1.9M cycles per racer, ~3.8M total). Any ioctl
error would have been printed as a MARKER (per-ifp churn makes benign
EEXIST/ENOENT impossible β in a healthy serialized system every add succeeds
and every del of a just-added member succeeds). Result (run.log):
0 markers, 0 anomalies, guest healthy. This is the expected outcome
given the serialization: userland simply cannot produce two concurrent
if_addgroup() executions.
An earlier v1 design (both racers churning the same ifp) is unusable on
this tree: the first benign EEXIST return leaks ifnet_mtx via known
DF-0272 and permanently wedges all further interface ioctls (observed live:
processes stuck in D-state, wchan ifnet).
Conclusion & classification
- status
not_reproduced, impactnone(from userland), confidencelikelyfor the kernel-side race (static analysis certain; a concrete hot-plug overlap was not exercised). - Fix provided (
fix.diff:if_creategroup_locked()+ fully exclusiveif_addgroup(); allocations hoisted out of the lock, error path frees them). Built into the same fix kernel as DF-3280 (kernel #1, Sep 10 03:02:42) β compiles clean, and 3M+ real SIOCAIFGROUP/SIOCDIFGROUP cycles ran error-free on the patched kernel (../DF-3280/churnFIX.log), so the API regression surface is exercised; but with no userland baseline,fix_status = not_testable.
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_testablefix.diff compiled into fix kernel #1 (with DF-3280's fix) and 3.0M+ group add/del cycles ran error-free on it (churnFIX.log), so the restructure is build- and API-validated; the race itself had no userland-reproducible baseline, so behavioral fix validation is not possible (not_testable).
../DF-3280/kbuild.log (BUILD-EXIT=0), ../DF-3280/churnFIX.log (3.0M cycles err=0), fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log: racers healthy, 0 MARKER lines, ~1.9M add/del cycles per racer in 45s', 'VERDICT.md: serialization analysis (ifnet_mtx) + kernel-side race partners with path:line', 'fix.diff: atomic if_addgroup under LK_EXCLUSIVE', '../DF-3280/churnFIX.log: 3.0M SIOCAIFGROUP/SIOCDIFGROUP cycles err=0 on the fix kernel']
PoC changes
v1 (both racers churn the same ifp) wedges the guest on the first benign EEXIST via the known DF-0272 ifnet_mtx leak; v2 uses one racer per interface (lo0/vtnet0) so benign EEXIST/ENOENT are impossible and any error is a genuine desynchronization marker; group name must not end in a digit (if.c:1155).
Verified recommended fix
Hoist allocations, then perform membership scan, group lookup/creation, ifg_refcnt++ and both TAILQ insertions under a single LK_EXCLUSIVE section (if_creategroup_locked helper); free allocations on the EEXIST path (see fix.diff).
Verdict
Code defect certain, userland exploitation NOT possible. if_addgroup() executes ifg->ifg_refcnt++ (if.c:1183) outside ifgroup_lock while if_delgroup_locked() decrements under LK_EXCLUSIVE (if.c:1231) - asymmetric, lost-update-capable - and decides group creation without exclusive locking (duplicate same-named groups possible, if.c:1169-1177 + if_creategroup if.c:1106-1123). But ifioctl() holds the global ifnet_mtx across the whole ioctl body (if.c:2029-2450), fully serializing all userland SIOCAIFGROUP/SIOCDIFGROUP calls: a 45s two-racer run (~3.8M cycles) produced 0 anomalous returns (run.log). The live race partners are kernel-side only: if_attach's if_addgroup(IFG_ALL) before ifnet_mtx is taken (if.c:558 vs 689), and if_detach's group teardown which runs after ifnet_mtx is released (if.c:991-996) - i.e. SIOCAIFGROUP racing interface destroy/hot-unplug, both privileged. Consequence if it fires: refcount below membership -> premature if_destroygroup kfree with live ifgl/ifgm -> stale walks/double-free. Severity Low. fix.diff restructures if_addgroup to do scan/create/increment/insert atomically under LK_EXCLUSIVE (with if_creategroup_locked helper); it compiled into fix kernel #1 and 3M+ group cycles ran error-free, but with no userland race baseline fix_status=not_testable.
No comments yet.