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

Use-after-free race in aac_getnext_aif: AIF context list traversed and dereferenced without aac_aifq_lock

Summary

aac_getnext_aif() at aac.c:3527-3531 walks sc->fibctx for(ctx;ctx;ctx=ctx->next) WITHOUT aac_aifq_lock. aac_close_aif at :3495-3510 takes lock, unlinks, kfrees. Concurrent close -> ctx freed while getnext walks/reads ctx->next -> UAF. Also tsleep at :3536 without lock: close can kfree(ctx) during sleep, aac_return_aif on wakeup derefs freed ctx->ctx_idx/ctx_wrap at :3562-3575. Operator group (0640). Race widened by growing list with many OPEN_GET_ADAPTER_FIB. Fix: take aac_aifq_lock across traversal, re-validate ctx after tsleep wakeup.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1231 Β· 12 files
FileTypeDescriptionSize
VERDICT.md verdict full path:line trace, threat model, fix rationale 5.0 KB ↓ raw
README.md readme claim, verdict, runnable-PoC instructions 2.4 KB ↓ raw
aac_getnext_aif_race.c trigger-source pthread-race PoC (operator-group reachable) 4.3 KB view raw
build.sh build-script cc -O -Wall -pthread -o aac_getnext_aif_race aac_getnext_aif_race.c 231 B view raw
run.sh run-script ./aac_getnext_aif_race 112 B view raw
build.log build-log PoC build, full output 149 B view raw
run.log run-log PoC run on this guest (open /dev/aacN ENOENT) 401 B view raw
fix.diff suggested-fix take aac_aifq_lock across getnext; tsleep -> lksleep 1.5 KB view raw
fix_build.log build-log kernel build rc=0 with all 5 fixes applied; aac.c compiled clean under -Werror 2.1 KB view raw
env.txt environment guest uname, cc version, PCI topology 2.0 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme claim, verdict, runnable-PoC instructions
↓ download raw

DF-1231 β€” README

Finding

aac_getnext_aif() at sys/dev/raid/aac/aac.c:3527-3546 walks sc->fibctx and tsleep()s WITHOUT aac_aifq_lock. Concurrent aac_close_aif() (aac.c:3489-3512) takes the lock, unlinks, and kfree()s the same context β†’ two UAF races:

  • traversal race β€” close runs between two iterations of getnext's lookup loop; getnext then reads ctx->next of freed memory.
  • sleep race β€” getnext tsleep()s without the lock; close frees ctx; on wakeup, getnext calls aac_return_aif() which derefs ctx->ctx_idx/ctx->ctx_wrap (aac.c:3562, 3574-3575) on freed memory.

Verdict

NOT REPRODUCED on this guest (latent): the audit guest has no Adaptec FSA RAID controller in pciconf -lv, so no /dev/aacN exists and the ioctls are not reachable. The driver is statically linked into X86_64_GENERIC (kldstat -v shows pci/aac), so the bug path is in the running kernel and fires on real DragonFly installs with an aac(4) adapter.

Confidence (bug is real): certain β€” traced line-by-line in sys/. Impact ceiling: UAF β†’ kernel heap corruption / DoS / potential control-flow hijack. Reachable by any local user in operator group once an aac controller is present.

How to reproduce

./build.sh && ./run.sh

Expected on this guest: PoC builds clean, prints "No /dev/aacN found: No such file or directory" and reachability analysis. On a host with an aac controller, two processes racing FSACTL_GET_NEXT_ADAPTER_FIB (Wait=1) vs FSACTL_CLOSE_FIB_ADAPTER on the same fib context trigger the UAF.

Files

Path Purpose
aac_getnext_aif_race.c PoC: pthread-race demonstrator (operator-group reachable)
build.sh / run.sh exact build/run commands
fix.diff take aac_aifq_lock across traversal; switch tsleep->lksleep
VERDICT.md full path:line trace, threat model, fix rationale
build.log / run.log PoC build + run outputs
fix_build.log kernel-build compile validation of fix.diff
env.txt guest uname / cc / device topology
VERDICT.md verdict full path:line trace, threat model, fix rationale
↓ download raw

DF-1231 β€” VERDICT

Finding: aac_getnext_aif() walks the per-controller sc->fibctx list and tsleep()s without holding sc->aac_aifq_lock, while concurrent aac_close_aif() (which DOES hold the lock, unlinks and kfree()s the context) creates a use-after-free.

Status: NOT REPRODUCED (latent β€” no Adaptec FSA RAID controller on this guest). Confidence (bug is real): certain (traced line-by-line in sys/). Impact ceiling: use-after-free; kernel memory corruption (slab poisoning or attacker-reclaimed slab), panic/DoS or control-flow hijack on INVARIANTS-OFF kernels. Reachable by any local user in the operator group once an aac(4) controller is present.

Mechanism (confirmed line-by-line in sys/)

  1. aac_getnext_aif at sys/dev/raid/aac/aac.c:3519: c if ((error = copyin(arg, &agf, sizeof(agf))) == 0) { for (ctx = sc->fibctx; ctx; ctx = ctx->next) { /* :3528 */ if (agf.AdapterFibContext == ctx->unique) break; } if (!ctx) return (EFAULT); error = aac_return_aif(sc, ctx, agf.AifFib); if (error == EAGAIN && agf.Wait) { ... while (error == EAGAIN) { error = tsleep(sc->aac_aifq, PCATCH, "aacaif", 0); /* :3540 */ if (error == 0) error = aac_return_aif(sc, ctx, agf.AifFib); /* :3543 */ } The lookup loop at :3528 and the tsleep at :3540 execute WITHOUT aac_aifq_lock held.

  2. aac_close_aif at sys/dev/raid/aac/aac.c:3489-3512: c lockmgr(&sc->aac_aifq_lock, LK_EXCLUSIVE); /* :3495 */ for (ctx = sc->fibctx; ctx; ctx = ctx->next) { ... unlink ... } lockmgr(&sc->aac_aifq_lock, LK_RELEASE); /* :3508 */ if (ctx) kfree(ctx, M_AACBUF); /* :3510 */

  3. Race A (traversal): a concurrent close runs between two iterations of the getnext lookup loop. close takes the lock, unlinks ctx, releases the lock, and kfree()s ctx. The next loop iteration in getnext then reads ctx->next of freed memory β†’ UAF read.

  4. Race B (sleep): getnext enters tsleep at :3540 without the lock. close runs end-to-end (lock, unlink, release, kfree(ctx)). When getnext wakes, it calls aac_return_aif(sc, ctx, agf.AifFib) at :3543, which dereferences ctx->ctx_idx (aac.c:3562) and writes ctx->ctx_wrap / ctx->ctx_idx (aac.c:3574-3575) β€” UAF read+write on freed memory.

  5. aac_softc->aac_aifq_lock is a recursive lockmgr lock (aac.c:264 lockinit(..., LK_CANRECURSE)), and aac_return_aif itself takes it (aac.c:3561), so holding it across the lookup AND across an lksleep is safe.

  6. lksleep(ident, &lock, ...) is the standard DragonFly primitive that atomically releases the lock during sleep and reacquires before returning; it is already used elsewhere in this driver (aac.c:672, 1014, 1352, 3003, 3094). With the lock held, aac_close_aif cannot unlink or free ctx while getnext is sleeping β€” the race is closed.

Why it is NOT REPRODUCED on this guest

  • pciconf -lv shows only QEMU i440BX/PIIX3/virtio devices. No Adaptec AAC RAID controller is present.
  • kldstat -v confirms pci/aac, aac/aacd, aac/aacp, pci/aacch are statically linked into X86_64_GENERIC (the driver code IS in the running kernel), but aac_attach never runs without matching HW, so no /dev/aacN device node is created.
  • ls /dev/aac* returns no device nodes. PoC aac_getnext_aif_race.c confirms this at runtime.

Threat model & privilege boundary

/dev/aacN is created with default devfs perms (0640 root:operator). Any local user in the operator group (commonly granted to administrative staff for tape/CD-ROM access) can open the device and issue FSACTL_OPEN_GET_ADAPTER_FIB / FSACTL_GET_NEXT_ADAPTER_FIB / FSACTL_CLOSE_FIB_ADAPTER ioctls. Racing two of these is therefore a real local-user DoS / kernel-corruption vector on any host with an aac(4) RAID adapter β€” not requiring root. On the audit guest there is no such adapter, so the bug is dormant.

Fix (authored in fix.diff, applied + compile-validated)

Take sc->aac_aifq_lock across the entire aac_getnext_aif body: the list traversal, the aac_return_aif call (recursive take is fine β€” the lock is LK_CANRECURSE), and the sleep. The tsleep is replaced by lksleep(sc->aac_aifq, &sc->aac_aifq_lock, PCATCH, "aacaif", 0) so the lock is dropped during sleep but aac_close_aif cannot proceed (it needs the same lock to unlink+free), eliminating both races.

This mirrors how the driver already uses lksleep(..., &sc->aac_io_lock, ...) at lines 672, 1014, 1352.

Validation

  • fix.diff applies cleanly with patch -p1 --forward (verified).
  • All 5 audit fixes applied together; make -j6 nativekernel KERNCONF=X86_64_GENERIC returned rc=0 with no errors / warnings under -Werror. aac.c was compiled cleanly into both the kernel and the aac.ko module.
  • Fix is not_testable at runtime on this guest (no AAC controller).

Fix verification

not_testable

compile validated

nativekernel rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. aac_getnext_aif UAF race vs aac_close_aif. aac in GENERIC, no Adaptec HW.