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

aac_detach frees the container list with no aac_container_lock while the still-live interrupt path aac_handle_aif and the aac_query_disk ioctl traverse it β€” use-after-free race

Summary

aac_detach removes/kfree's containers unlocked while aac_handle_aif - still reachable from aac_new_intr because bus_teardown_intr happens later in aac_free and interrupts are masked later in aac_shutdown - walks the same list unlocked and takes aac_container_lock only around its own removals; aac_query_disk walks under the lock. The interleaving dereferences freed struct aac_container through TAILQ_NEXT/co_mntobj/co_disk. Root-initiated detach racing a device AIF re-enumeration (or an operator query_disk) -> UAF read/panic; list corruption when both sides remove concurrently. Hold the lock in detach.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3822 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.4 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
fix.diff β€” 694 B view raw
env.txt β€” 323 B view raw
verdict.json β€” 2.3 KB view raw

DF-3822 β€” aac_detach() frees the container list unlocked while the interrupt path and user ioctls walk it (detach vs in-flight AIF β†’ use-after-free)

  • File: sys/dev/raid/aac/aac.c
  • Lines: 678-685 (detach, unlocked removal/free), 3269-3326 (aac_handle_aif list walks), 3306-3326 (its locked removals), 3664-3687 (aac_query_disk locked walk), 625-626 (interrupts torn down only later, inside aac_free)
  • Class: CWE-362/CWE-416 β€” missing-lock list teardown β†’ UAF
  • Reach: root-triggered kldunload/detach racing a device-initiated AIF re-enumeration (AifEnAddContainer/AifEnDeleteContainer) or a concurrent FSACTL_QUERY_DISK ioctl.

Analysis

aac_detach() removes and frees every container without aac_container_lock:

679:    while ((co = TAILQ_FIRST(&sc->aac_container_tqh)) != NULL) {
680:        error = device_delete_child(dev, co->co_disk);
681:        if (error)
682:            return (error);
683:        TAILQ_REMOVE(&sc->aac_container_tqh, co, co_link);
684:        kfree(co, M_AACBUF);
685:    }

Meanwhile:

  • aac_handle_aif() β€” reachable from aac_new_intr() while interrupts are still live (they are masked only later, in aac_shutdown() at :696/767, and bus_teardown_intr happens even later inside aac_free() at :625) β€” walks aac_container_tqh with no aac_container_lock (aac.c:3269-3278) and takes it only around its own removals (aac.c:3316-3319).
  • aac_query_disk() (operator-reachable ioctl) walks the same list under aac_container_lock (aac.c:3664-3667) β€” serialized against aac_handle_aif's removals but not against detach's unlocked ones.

Interleaving detach's TAILQ_REMOVE/kfree(co) with either walk yields a TAILQ_FOREACH/TAILQ_NEXT dereference of freed co (UAF read β†’ co->co_mntobj copy / device_get_softc(co->co_disk) on freed memory), or list corruption when both sides remove concurrently.

Preconditions (why Low)

detach requires root, and the racing AIF requires the controller to send an add/delete-container event inside the window; the query_disk variant needs an operator-group user holding the lock at the right moment. The code defect (no lock on a shared-list teardown) is, however, unambiguous.

Verdict

inconclusive β€” needs AAC hardware to attach (absent on the audit guest, ../DF-3819/env.txt); the race cannot be staged without it. Source-level: certain (locking asymmetry provable by inspection).

VERDICT.md
↓ download raw

VERDICT β€” DF-3822

Status: inconclusive (race requires an attached AAC controller; none on the audit guest). Source-level confidence: certain for the locking asymmetry; likely for exploitability end-to-end (window is narrow but device-driven events are repeatable).

Lock-state matrix for sc->aac_container_tqh

actor trigger lock held while walking lock held while removing
aac_handle_aif AIF re-enum (aac.c:3269-3326) adapter interrupt (aac.c:842) or aif thread (aac.c:1054) none aac_container_lock (aac.c:3316)
aac_query_disk (aac.c:3665) operator ioctl aac_container_lock n/a (walk only)
aac_detach (aac.c:679-685) root kldunload/detach none none

The interrupt handler is still registered while detach runs the container loop: bus_teardown_intr happens inside aac_free() (aac.c:625-626), after the loop; interrupt masking happens in aac_shutdown() (:767) which detach calls at :696 β€” also after the container loop. So an AIF landing during the loop runs aac_handle_aif() concurrently with the unlocked TAILQ_REMOVE/kfree(co) at :683-684:

  • TAILQ_FOREACH(co, &sc->aac_container_tqh, co_link) at :3269 reads co_link of an entry freed at :684 β†’ UAF read β†’ bogus pointer β†’ co->co_mntobj.ObjectId deref at :3272 β†’ arbitrary read/panic.
  • aac_query_disk under the lock can equally deref memory freed by the unlocked detach loop.

Additionally the walk in aac_handle_aif (:3269-3278, :3306-3326) itself runs without the container lock even against its own removal phase at :3316 β€” the code drops/retakes aac_io_lock and mplock around device_delete_child (:3309-3314) but the TAILQ_NEXT(co, co_link) at :3315 can already point at an entry another CPU removed.

Why not reproduced

Race needs: (1) AAC controller attached β€” impossible on the guest (zero matching PCI devices, no /dev/aacd0); (2) root detach concurrent with a device AIF or operator ioctl. Nothing to stage on the audit guest beyond proving absence (env.txt).

Fix

fix.diff wraps detach's removal/free in aac_container_lock, matching the discipline aac_handle_aif already uses for its removals. (A full fix would also lock the aac_handle_aif walk itself; the diff keeps the minimal, obviously-correct delta and notes this.)

Fix validation

not_testable β€” same hardware precondition.

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

Race requires attached AAC hardware; absent on the guest. fix.diff adds the missing lock, mirroring aac_handle_aif's removal discipline.

fix.diff
↓ fix.diffper-fix-DF-3822

Confirmed kernel references

Detail

Exploit chain

root detach || adapter AIF re-enumeration -> unlocked TAILQ_REMOVE/kfree races aac_handle_aif's or aac_query_disk's walk -> use-after-free read through co->co_link/co_mntobj/co_disk -> panic or info disclosure of freed heap

Evidence (decisive lines)

VERDICT.md lock-state matrix; fix.diff (lock the detach removal); ../DF-3819/env.txt (guest hardware absence)

PoC changes

No seed; analysis pack - the race needs an attached controller.

Verified recommended fix

Hold aac_container_lock around the container TAILQ_REMOVE/kfree in aac_detach (aac.c:683).

Verdict

aac_detach() removes and frees entries of sc->aac_container_tqh with no aac_container_lock (aac.c:679-685) while (a) aac_handle_aif() - still reachable from aac_new_intr, because bus_teardown_intr only happens later in aac_free (aac.c:625) and interrupts are masked later in aac_shutdown (aac.c:767) - walks the same list unlocked (aac.c:3269-3278) and (b) aac_query_disk() walks it under the lock (aac.c:3664-3687). The interleaving yields TAILQ_NEXT/FREE-dereference of kfree'd struct aac_container (UAF). Root-detach + device-AIF/operator-ioctl preconditions; cannot be staged on the audit guest (no AAC HBA, no /dev/aacd0, zero matching PCI vendors). Locking asymmetry itself is source-certain.