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)
PoC verification
Evidence pack
findings/poc/DF-4874 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
afddriver compiled in (device afd, config line40 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-hdandide-cdon the IDE bus β no ATAPI direct-access (floppy/ZIP-class) emulation exists, so noafd0device 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
afddevclass, and reporteddf4874: afd0 device_t is NULLβ devclass present, zero devices. - The shared-code race this finding rides on (subr_disk.c
dscheckvsdsgone) is the identical mechanism DF-4797 reproduced onad0in 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).
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:
-
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 NULLsdp->d_slicewhile a concurrent raw read/write is insidediskstrategy()(sys/kern/subr_disk.c:1246) βdscheck(dev, bio, NULL)dereferencesssp->dss_nslicesat offset 0xc of NULL β fatal trap 12. Open fds on the cdev keep the old ops vector alive acrossdisk_destroy(), so post-detach I/O keeps enteringafd_strategy()too. -
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 toata_finish()(ata-queue.c:518-537), which normally defers totaskqueue_swi_mp. The completion callbackafd_done()therefore runs asynchronously, afterafd_detach()has already donedevice_set_ivars(dev, NULL)(atapi-fd.c:153) andkfree(fdp)(:154).afd_done()then executesfdp = device_get_ivars(request->dev)β NULL (atapi-fd.c:321) anddevstat_end_transaction_buf(&fdp->stats, bbp)(atapi-fd.c:329) β a write toNULL + offsetof(struct afd_softc, stats)β andbiodone(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 atdscheck+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 groupoperator, same exposure DF-4797 had on/dev/ad0) to hold the cdev alive and stormafd_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.
Recommended fix
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_testableBaseline 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
Confirmed kernel references
- sys/dev/disk/nata/atapi-fd.c:134
- sys/dev/disk/nata/atapi-fd.c:144
- sys/dev/disk/nata/atapi-fd.c:148
- sys/dev/disk/nata/atapi-fd.c:153
- sys/dev/disk/nata/atapi-fd.c:154
- sys/dev/disk/nata/atapi-fd.c:313
- sys/dev/disk/nata/atapi-fd.c:319
- sys/dev/disk/nata/atapi-fd.c:321
- sys/dev/disk/nata/atapi-fd.c:329
- sys/dev/disk/nata/ata-queue.c:518
- sys/dev/disk/nata/ata-queue.c:565
- sys/kern/subr_diskslice.c:348
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').
No comments yet.