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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3281 Β· 10 files
FileTypeDescriptionSize
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):

  1. Membership/group scans run under LK_SHARED (if.c:1160-1174), then the lock is released (if.c:1174).
  2. ifg->ifg_refcnt++ (if.c:1183) executes outside any lock β€” a non-atomic read-modify-write on a refcount that if_delgroup_locked() decrements under LK_EXCLUSIVE (if.c:1231) β€” asymmetric protection, classic lost-update.
  3. The find-or-create decision (group absent at if.c:1169-1172 β†’ call if_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()'s if_addgroup(ifp, IFG_ALL) (if.c:558) β€” runs without ifnet_mtx (taken later at if.c:689); concurrent driver attaches on different CPUs can overlap, and
  • if_detach()'s group teardown, which runs after if_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.

VERDICT.md
↓ download raw

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) at if.c:558 β€” runs before if_attach takes ifnet_mtx (if.c:689); concurrent device attaches (different CPUs) can overlap with each other and with an SIOCAIFGROUP.
  • if_detach() group teardown at if.c:993-996 β€” runs after if_detach released ifnet_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> racing ifconfig <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, impact none (from userland), confidence likely for the kernel-side race (static analysis certain; a concrete hot-plug overlap was not exercised).
  • Fix provided (fix.diff: if_creategroup_locked() + fully exclusive if_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ 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

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.