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

aac_ioctl_send_raw_srb sleeps on channel NULL while its CMFREE callback wakes channel &cm β€” permanent unkillable process when the FIB pool is exhausted

Summary

aac.c:3094 does lksleep(cm, ...) with cm==NULL; the CMFREE event callback stores the command through arg==&cm and calls wakeup(arg) i.e. wakeup(&cm). The channels never match, so when aac_alloc_command() fails the ioctl thread sleeps without PCATCH forever - D-state, unkillable. The sibling aac_ioctl_sendfib() gets it right with lksleep(&cm,...). One-token typo. Any operator-group user (dev is 0640 root:operator, aac_open has no priv check) on an AAC-equipped host hangs an unkillable process per attempt once all FIBs are outstanding - local DoS; no memory corruption.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3819 Β· 10 files
FileTypeDescriptionSize
aac_rawsrb_hang.c β€” 2.6 KB view raw
build.sh β€” 104 B view raw
run.sh β€” 109 B view raw
build.log β€” 193 B view raw
run.log β€” 396 B view raw
run.2.log β€” 396 B view raw
env.txt β€” 323 B view raw
fix.diff β€” 396 B view raw
VERDICT.md β€” 3.1 KB ↓ raw
verdict.json β€” 2.3 KB view raw
VERDICT.md
↓ download raw

VERDICT β€” DF-3819

Status: not_reproduced (hardware-gated; source-certain defect)

Root cause

sys/dev/raid/aac/aac.c:aac_ioctl_send_raw_srb():

3076:   cm = NULL;
...
3082:   if (aac_alloc_command(sc, &cm)) {
3083:       event = kmalloc(sizeof(struct aac_event), M_AACBUF, M_NOWAIT|M_ZERO);
...
3090:       event->ev_type = AAC_EVENT_CMFREE;
3091:       event->ev_callback = aac_ioctl_event;
3092:       event->ev_arg = &cm;                    /* waker will wakeup(&cm) */
3093:       aac_add_event(sc, event);
3094:       lksleep(cm, &sc->aac_io_lock, 0, "aacraw", 0);   /* BUG: cm == NULL */

The completion callback aac_ioctl_event() (aac.c:2959-2971) runs from aac_release_command() (aac.c:1417-1420) when a FIB is freed, stores the new command through arg and calls wakeup(arg) where arg == &cm β€” the address of the kernel stack slot. The sleeper, however, sleeps on the channel whose value is cm, i.e. NULL at that point (it is only assigned by the callback after the wakeup that never reaches it).

The sibling function aac_ioctl_sendfib() gets it right:

3003:       lksleep(&cm, &sc->aac_io_lock, 0, "sendfib", 0);

lksleep() without PCATCH is uninterruptible, and no other code path in the driver does wakeup(NULL), so the mismatched sleeper never wakes: the calling thread hangs permanently in state D and cannot be killed.

Trigger / reachability

  • Device node /dev/aacd0 (aliases afa%d, hpn%d) is created 0640 root:operator at attach (aac.c:317-320) β€” no privilege check exists in aac_open() (aac.c:2780-2791), so any member of group operator can reach the ioctl.
  • The hang requires exactly one precondition: aac_alloc_command() returns EBUSY once (aac.c:1370-1377) β€” i.e. all FIBs outstanding, trivially arranged on real hardware by parallel management traffic.

Why not reproduced on the guest

The QEMU audit guest has no Adaptec AAC PCI HBA:

  • ls /dev/aac* /dev/asr* /dev/afa* /dev/hpn* β†’ No such file or directory (env.txt, run.log)
  • pciconf -l shows zero devices with aac vendor IDs (0x9005/0x9004 Adaptec, 0x1028 Dell PERC, 0x1044 DPT) β€” env.txt
  • the aac driver is compiled into X86_64_GENERIC (kldload aac β†’ "module aac/aacd already exists"), but attach never runs without hardware, so the control device never exists and the ioctl path cannot be entered at all.

The defect itself is deterministic and visible in the source: a one-token typo (lksleep(cm vs lksleep(&cm), proven by direct comparison with the correct sibling two hundred lines above.

Fix

fix.diff: sleep on &cm. (Upstream FreeBSD's ancestor of this code uses tsleep(&cm, ...) β€” the DragonFly port mangled the &.)

Fix validation

Not possible on this guest (no hardware to attach the driver to); 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 cannot be exercised: baseline path requires an attached AAC controller, absent from the QEMU guest (no /dev/aacd0, no matching PCI vendor IDs). fix.diff is the one-token correction mirroring aac.c:3003.

fix.diff
↓ fix.diffper-fix-DF-3819

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (source excerpt + guest-absence proof); run.log (open ENOENT as root and as user); env.txt (uname, no aac PCI devices, driver compiled into kernel); fix.diff (one-token fix)

PoC changes

Trigger written fresh (no seed): native FSACTL_SEND_RAW_SRB = _IO('8',19) computed from aac_ioctl.h, arg passed as pointer-to-pointer per aac.c:2832 'arg = (caddr_t)arg'.

Verified recommended fix

Sleep on the address of the local variable: lksleep(&cm, ...) in aac_ioctl_send_raw_srb (aac.c:3094).

Verdict

aac_ioctl_send_raw_srb() sleeps on chan NULL (lksleep(cm,...) with cm==NULL at aac.c:3094) while its CMFREE callback wakes chan &cm (aac.c:2970); the correct sibling aac_ioctl_sendfib() uses lksleep(&cm,...) at aac.c:3003. Once the FIB pool is exhausted the ioctl thread sleeps uninterruptibly forever - unkillable D-state process reachable by group operator through /dev/aacd0. Hardware-gated: the audit guest has no AAC HBA (no /dev/aacd0, zero matching PCI vendors), so the path cannot be entered; defect is certain from source comparison.