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

afd_detach() in-flight teardown race: UAF of afd_softc/struct disk + shared dscheck/dsgone disk-layer race β€” twin of reproduced DF-4797 (ad)

Summary

disk_invalidate/disk_destroy run before ata_fail_requests, whose deferred taskqueue completion lets afd_done run after the softc free (near-NULL devstat write + biodone against a destroyed disk layer), and dsgone frees d_slice under storming dscheck readers. Identical shape to DF-4797 (REPRODUCED 2/2 in situ + fix validated on ad0); distinct from the DF-1944/2507 reinit path. PoC ported from the DF-4797 rig; QEMU 7.2 has no ATAPI direct-access emulation (hard blocker recorded on-guest). Port-the-validated-fix diff.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4874 Β· 13 files
FileTypeDescriptionSize
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 5.4 KB ↓ raw
Makefile β€” 80 B ↓ download
df4874_detach.c β€” 3.2 KB view raw
df4874_storm.c β€” 2.1 KB view raw
build.sh β€” 138 B view raw
run.sh β€” 158 B view raw
build.log β€” 5.9 KB view raw
run.log β€” 241 B view raw
env.txt β€” 195 B view raw
fix.diff β€” 3.2 KB view raw
manifest.json β€” 962 B view raw
verdict.json β€” 3.2 KB view raw

DF-4874 β€” afd_detach() in-flight teardown race (twin of reproduced DF-4797)

sys/dev/disk/nata/atapi-fd.c β€” afd_detach() (atapi-fd.c:134-156) frees the afd_softc while afd_strategy()/afd_done() can still be running against it, and while the shared disk layer (disk_invalidate()β†’dsgone()) frees dp->d_slice out from under concurrent diskstrategy()/dscheck().

This is the exact code shape of DF-4797 (ata-disk.c), which was REPRODUCED 2/2 on this very guest (fatal trap 12 in dscheck+0x48) and fix-validated β€” see findings/poc/DF-4797/. The afd driver is a byte-for-byte structural twin (same disk_invalidate β†’ disk_destroy β†’ ata_fail_requests β†’ kfree ordering, same non-draining completion path via taskqueue_swi_mp).

Status: untested (in-situ) β€” hard blocker: no ATAPI floppy hardware

  • Guest kernel has the afd driver compiled in (device afd, config line 40 ata/afd), but the afd driver only attaches to ATAPI direct-access devices (atapi-fd.c:90-91, ATA_ATAPI_TYPE_DIRECT).
  • QEMU 7.2 (the audit guest's emulator) provides only ide-hd and ide-cd on the IDE bus β€” no ATAPI direct-access (floppy/ZIP-class) emulation exists, so no afd0 device can ever exist on this guest:
$ qemu-system-x86_64 -device help | grep -i ide
name "ide-cd", bus IDE, desc "virtual IDE CD-ROM"
name "ide-hd", bus IDE, desc "virtual IDE disk"
  • Recorded on the guest (run.log): the DF-4797-style detach trigger built and loaded cleanly against the afd devclass, and reported df4874: afd0 device_t is NULL β€” devclass present, zero devices.
  • The shared-code race this finding rides on (subr_disk.c dscheck vs dsgone) is the identical mechanism DF-4797 reproduced on ad0 in situ.

Reproduce (on hardware/VM that has an ATAPI floppy)

# on the audit guest (build):        ./build.sh
# on a system with a real afd0:      ./run.sh
./df4874_storm 16 25 &               # 16 threads raw-pread /dev/afd0
sysctl kern.df4874.cycles=4
sysctl kern.df4874.ms=300
sysctl kern.df4874.run=1             # device_detach(afd0) cycles

Expected baseline (bug present): fatal trap 12 in dscheck+0x48 (disk-layer race, fault address 0xc β€” same as DF-4797 run 1/2), or a NULL/UAF write in afd_done() (devstat_end_transaction_buf(&fdp->stats) with fdp == NULL, atapi-fd.c:321/329).

VERDICT.md
↓ download raw

DF-4874 VERDICT β€” afd_detach() in-flight teardown race

Status: untested (hardware absent from guest); root cause proven by line trace + reproduced twin DF-4797

What the finding is

afd_detach() (sys/dev/disk/nata/atapi-fd.c:134-156) tears the driver down in an order that leaves two independent windows for use-after-free:

  1. Disk-layer race (shared with DF-4797). disk_invalidate(&fdp->disk) (atapi-fd.c:144) β†’ dsgone() (sys/kern/subr_diskslice.c:348) kfree()s the slices struct and NULLs dp->d_slice while a concurrent raw read/write is inside diskstrategy() (sys/kern/subr_disk.c:1246) β†’ dscheck(dev, bio, NULL) dereferences ssp->dss_nslices at offset 0xc of NULL β†’ fatal trap 12. Open fds on the cdev keep the old ops vector alive across disk_destroy(), so post-detach I/O keeps entering afd_strategy() too.

  2. Driver-level UAF. ata_fail_requests(dev) (atapi-fd.c:148; sys/dev/disk/nata/ata-queue.c:565-605) pulls the channel-queued and running requests and β€” after releasing queue_mtx/state_mtx β€” hands each to ata_finish() (ata-queue.c:518-537), which normally defers to taskqueue_swi_mp. The completion callback afd_done() therefore runs asynchronously, after afd_detach() has already done device_set_ivars(dev, NULL) (atapi-fd.c:153) and kfree(fdp) (:154). afd_done() then executes fdp = device_get_ivars(request->dev) β†’ NULL (atapi-fd.c:321) and devstat_end_transaction_buf(&fdp->stats, bbp) (atapi-fd.c:329) β€” a write to NULL + offsetof(struct afd_softc, stats) β€” and biodone(bp) (:330) on a bio whose disk layer is already destroyed. The same window exists for a request that completes via the HW interrupt racing the detach.

afd_strategy() itself also runs lock-free against detach: devstat_start_transaction(&fdp->stats) (atapi-fd.c:313) can write into freed memory, exactly like ad_strategy() did for DF-4797 (ata-disk.c:360).

Why this is a distinct finding (not a DF-1944/DF-2507/DF-4797 re-report)

  • DF-1944/DF-2507 are the afd_reinit() path (atapi-fd.c:167-182): softc freed without disk_destroy() on a channel reinit, orphaning the cdev. This finding is the afd_detach() path β€” different entry, different trigger (device_detach / hot-unplug / kldunload), and the softc is freed after a teardown sequence that still fails to drain in-flight I/O.
  • DF-4797 is the same shape in ata-disk.c (ad), where it was REPRODUCED 2/2 on this guest (fatal trap 12 at dscheck+0x48, fault address 0xc) and whose fix was validated on a rebuilt kernel. The nata pass-set convention is per-file findings; atapi-fd.c carries its own copy of the defect.

Verification attempt (what was run, and the blocker)

Trigger infrastructure was ported from the proven DF-4797 pack to the afd devclass and built successfully on the audit guest (build.log: KLD df4874.ko + userland df4874_storm, cc 8.3, kernel sources in /usr/src):

$ kldload ./df4874.ko && sysctl kern.df4874.run=1
df4874: afd0 device_t is NULL
$ ./df4874_storm 4 3
storm: done, total_io=0

The afd devclass exists (the driver is compiled into X86_64_GENERIC, device afd), but there is no afd device: the afd driver only probes ATAPI direct-access devices (atapi-fd.c:90-91), and QEMU 7.2 cannot emulate one (only ide-hd/ide-cd exist on the IDE bus β€” checked with qemu-system-x86_64 -device help). So the race cannot be instantiated on this guest: untested, hard blocker documented above, not a refutation.

Corroboration: the shared disk-layer mechanism (subr_disk.c dscheck vs dsgone) plus the identical nata teardown ordering were proven live on this exact kernel build by DF-4797's in-situ reproduction (2/2 fatal trap 12).

Threat model

  • Attacker needs an open descriptor on /dev/afd0 (raw floppy device; typically group operator, same exposure DF-4797 had on /dev/ad0) to hold the cdev alive and storm afd_strategy().
  • The detach side is the standard PCI hot-unplug / devctl detach / kldunload teardown (privileged or hypervisor-initiated) β€” same threat model as DF-4797 / DF-4646 (nvme twin).
  • Impact: kernel UAF write (devstat_* on freed softc) + NULL-deref panic; realistic ceiling is memory corruption in the disk/stat structures, not a demonstrated uid=0 chain.

Mirror the validated DF-4797 fix in atapi-fd.c: refuse detach while the disk is open (disk.d_opencount), set a DETACHING flag under ch->queue_mtx before NULLing ivars, take a per-softc in-flight reference in afd_strategy() dropped in afd_done(), drain before kfree, and move ata_fail_requests() before disk_invalidate()/disk_destroy() so biodone()s land while the disk layer is intact. See fix.diff.

Fix validation

fix_status: not_testable β€” the baseline cannot be triggered on the audit guest (no ATAPI direct-access device can be attached under QEMU 7.2), so a patched-kernel A/B run is impossible here. fix.diff is a direct port of the DF-4797 fix whose own patched-kernel validation PASSED on this guest (run.fixed.log in findings/poc/DF-4797/).

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

Baseline untriggerable on guest (no afd hardware) so no A/B run; fix.diff is a direct port of DF-4797's fix.diff whose own patched-kernel validation passed (findings/poc/DF-4797/run.fixed.log)

fix.diff (git apply --check OK); findings/poc/DF-4797/run.fixed.log for the twin validation
↓ fix.diffper-fix-DF-4874

Confirmed kernel references

Detail

Exploit chain

open /dev/afd0 (group operator) and raw-pread storm to hold in-flight afd_strategy requests; concurrently device_detach(afd0) (hot-unplug/devctl/kldunload) -> disk_invalidate frees d_slice under dscheck (fatal trap 12, fault 0xc β€” the exact DF-4797 in-situ signature) or async taskqueue completion runs afd_done after kfree(fdp) -> devstat_end_transaction_buf(&NULL->stats) UAF write / biodone on destroyed disk

Evidence (decisive lines)

VERDICT.md (full trace); run.log (guest: 'df4874: afd0 device_t is NULL' + storm total_io=0); build.log (KLD+storm built on guest); findings/poc/DF-4797/ (reproduced twin, panic.txt 2/2)

PoC changes

Ported the proven DF-4797 detach KLD + storm to the afd devclass (devclass_find("afd"), /dev/afd0); no other changes

Verified recommended fix

Port the validated DF-4797 fix: EBUSY while disk open, AFD_F_DETACHING under ch->queue_mtx, in-flight refs drained before kfree, ata_fail_requests moved before disk_invalidate/destroy

Verdict

afd_detach() frees the afd_softc while afd_strategy()/afd_done() can still be running against it, and while disk_invalidate() frees dp->d_slice under concurrent dscheck() β€” a byte-for-byte structural twin of DF-4797 (ad), which was REPRODUCED 2/2 on this guest with fatal trap 12. In-situ verification of the afd copy is blocked: QEMU 7.2 has no ATAPI direct-access (floppy-class) emulation, so no afd device can attach; the DF-4797-style trigger was nonetheless built and loaded on the guest and recorded the blocker ('afd0 device_t is NULL').