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)
PoC verification
Evidence pack
findings/poc/DF-3822 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_aiflist walks), 3306-3326 (its locked removals), 3664-3687 (aac_query_disklocked walk), 625-626 (interrupts torn down only later, insideaac_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 concurrentFSACTL_QUERY_DISKioctl.
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 fromaac_new_intr()while interrupts are still live (they are masked only later, inaac_shutdown()at :696/767, andbus_teardown_intrhappens even later insideaac_free()at :625) β walksaac_container_tqhwith noaac_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 underaac_container_lock(aac.c:3664-3667) β serialized againstaac_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 β 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 readsco_linkof an entry freed at :684 β UAF read β bogus pointer βco->co_mntobj.ObjectIdderef at :3272 β arbitrary read/panic.aac_query_diskunder 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_testableRace requires attached AAC hardware; absent on the guest. fix.diff adds the missing lock, mirroring aac_handle_aif's removal discipline.
fix.diff
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.
No comments yet.