acd_fixate() sizes MODE SELECT data-out from device-fed write-param page_length: 265-byte transfer out of a 64-byte stack struct discloses ~201 bytes of kernel stack to the device
Summary
acd_fixate MODE SENSEs the write-parameters page into an uninitialized 64-byte stack struct, then issues acd_mode_select(dev, ¶m, param.page_length + 10) where page_length is an unvalidated device-controlled byte. A crafted ATAPI device answering page_length=0xff makes the kernel send 265 bytes out of the 64-byte stack object - 201 bytes of adjacent kernel stack (frame pointers, heap/stack pointers, kernel-TEXT pointers: direct KASLR-defeating material) leave the machine in the data-out phase. acd_init_track/send_cue hard-set page_length=0x32 before the identical call; acd_fixate alone keeps the device-fed value. REPRODUCED on the audit guest (QEMU IDE DVD-ROM + DF-3690-family crafted-device emulation via a rebuilt kernel response hook): 265-byte data-out captured with kernel pointers in the hexdump, two independent samples; fix validated (EINVAL, no data-out; pl=0x32 sized 60 bytes in-bounds). Clamp-page_length fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4184 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| fixate_stackread.c | β | 2.2 KB | view raw | |
| build.sh | β | 238 B | view raw | |
| run.sh | β | 112 B | view raw | |
| run.log | β | 2.9 KB | view raw | |
| leak_sample.txt | β | 2.4 KB | view raw | |
| dfaud_dmesg_raw.txt | β | 4.6 KB | view raw | |
| build.log | β | 8.0 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_run.log | β | 820 B | view raw | |
| fix.2.log | β | 1.2 KB | view raw | |
| env.txt | β | 381 B | view raw | |
| userland_runs.txt | β | 315 B | view raw | |
| fix_userland_runs.txt | β | 315 B | view raw | |
| fix_dmesg_full.txt | β | 23.6 KB | view raw | |
| emu_notes.txt | β | 2.6 KB | view raw | |
| verdict.json | β | 4.1 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw |
DF-4184 β acd_fixate() sends MODE SELECT sized by device-fed page_length out of a 64-byte stack buffer
- File:
sys/dev/disk/nata/atapi-cd.c(pass 2) - Class: crafted-device kernel stack information disclosure (CWE-909/CWE-786 read side)
- Reachability:
CDRIOCFIXATEioctl on/dev/acd0(0640 root:operator, plus the DF disk layer'sSYSCAP_RESTRICTEDROOTgate atsys/kern/subr_disk.c:1072), i.e. root / RESTRICTEDROOT-capable processes such as burning tools talking to an attached ATAPI drive whose MODE SENSE (page 0x05) response is attacker-controlled (malicious/firmware-compromised USB ODD, device passthrough).
Root cause
acd_fixate() (sys/dev/disk/nata/atapi-cd.c:1140-1150):
struct write_param param; /* 64-byte __packed stack object */
if ((error = acd_mode_sense(dev, ATAPI_CDROM_WRITE_PARAMETERS_PAGE,
(caddr_t)¶m, sizeof(param)))) /* device fills it */
return error;
param.data_length = 0;
...
if ((error = acd_mode_select(dev, (caddr_t)¶m, param.page_length + 10)))
param.page_length (offset 9 of the MODE SENSE response) is an unvalidated
device-controlled byte (0..255). acd_mode_select() passes that value +10
straight as the data-out bytecount:
return ata_atapicmd(dev, ccb, pagebuf, pagesize, 0, 30); /* flags=0 => DATA-OUT */
A device answering page_length = 0xff makes the kernel push 265 bytes out of
the 64-byte stack object β 201 bytes of adjacent kernel stack (frame pointers,
kernel text/heap pointers) are transferred to the device. (Unlike
acd_init_track/acd_send_cue, which hard-set param.page_length = 0x32
before the same call, acd_fixate keeps the device-fed value.)
Reproduction (crafted-device emulation on the audit guest)
Stock QEMU ATAPI refuses MODE SENSE page 0x05, so the guest kernel was rebuilt
with a small device-side emulator hook in ata_atapicmd() (guest-local only;
same methodology as the DF-3690..3696 crafted-disk family) that makes the drive
(1) answer MODE SENSE(0x05) with page_length=0xff, (2) accept MODE SELECT /
CLOSE TRACK. df_audit.mode=1 enables it; nothing in acd_fixate is modified.
./build.sh # cc -O -o fixate_stackread fixate_stackread.c (on the guest) ./run.sh # /tmp/fixate_stackread /dev/acd0
Expected (and observed) decisive kernel console output β see run.log:
DFAUDIT: hook ccb0=5a ccb2=05 cnt=64 ... ata_atapicmd(..., sizeof(param))
DFAUDIT: forged MODE_SENSE(0x05) page_length=0xff
DFAUDIT: hook ccb0=55 ccb2=00 cnt=265 ... MODE SELECT data-out of 265 bytes
DFAUDIT: MODE_SELECT count=265 from 65-byte stack param, over-read bytes:
... 201 bytes containing 0xfffff800/0xfffff8xxxxxxxxxx kernel pointers and
0xffffffff80xxxxxx kernel-text pointers (KASLR-defeating material)
The hexdump is produced by the emulation hook reading the same buffer the DMA engine reads; on real crafted hardware those bytes leave the machine toward the device.
Fix validation
fix.diff clamps the device-fed page length to the MMC write-parameters-page
maximum (0x32) before any data-out. Validated on the guest with the same
emulated attack:
df_audit.pl=255(same attack): fixate now returnsEINVALimmediately β no MODE SELECT is issued, no bytes leave the stack object, no panic.df_audit.pl=50(valid page) +df_audit.capfail=1: MODE SELECT sized 60 bytes (in-bounds), driver continues normally, ioctl completes β no over-read.
Both runs: fix_run.log, fix.2.log.
Contents
fixate_stackread.cβ trigger (CDRIOCFIXATE + bounds negative tests)run.logβ decisive baseline run (program output + kernel console)leak_sample.txtβ annotated leaked qwordsdfaud_dmesg_raw.txtβ raw dmesg of the F1 bootbuild.logβ kernel build excerpts (atapi-cd.c compile + RC=0)fix.diffβ the verified fix (against pristine sys/)fix_run.log,fix.2.logβ fix-validation runsenv.txt,userland_runs.txtmanifest.json,verdict.json
DF-4184 VERDICT
Status: reproduced (impact: leak β kernel stack disclosure to a crafted ATAPI device)
Confidence: certain.
What was proven, and how
The stock audit guest (DragonFly 6.5-DEVELOPMENT x86_64, QEMU 7.2 IDE DVD-ROM,
/dev/acd0 root:operator 0640) cannot by itself emit a MODE SENSE page-0x05
response (QEMU answers ILLEGAL REQUEST asc=0x24 β verified live, run.log).
Following the DF-3690..3696 crafted-disk methodology, the guest kernel was
rebuilt with a device-response emulator hook in ata_atapicmd()
(ata-queue.c): when df_audit.mode=1 it (a) rewrites the MODE SENSE(0x05)
response to page_length=0xff, (b) accepts the resulting MODE SELECT and
CLOSE TRACK, and (c) dumps the exact data-out bytes the device would receive.
sys/dev/disk/nata/atapi-cd.c itself was not modified for the baseline
reproduction (only kprintf tracing of the ioctl entry), so the vulnerability
demonstrated is the pristine driver's behavior under a malicious drive.
Observed (kernel #5, df_audit.mode=1, df_audit.capfail=0):
CDRIOCFIXATEreachesacd_fixate()(traced:acd_ioctl cmd=8004636a,CDRIOCFIXATE entered,acd_fixate entered, calling mode_sense(0x05)).acd_mode_sense(..., sizeof(param))runs asata_atapicmdwithcount=64(kernel-computedsizeof(struct write_param)).- The crafted response sets
param.page_length = 0xff(atapi-cd.c:1150 reads it back unvalidated). acd_mode_select(dev, ¶m, param.page_length + 10)issues MODE SELECT with count=265 β a 265-byte DATA-OUT transfer sourced from the 64-byte stack object: 201 bytes over-read ofacd_fixate's kernel stack frame.- The over-read bytes (captured in
run.log/leak_sample.txt) contain canonical kernel pointers:0xfffff800xxxxxxxxheap/stack pointers and0xffffffff80xxxxxxkernel-text pointers (+0x38: 0xffffffff8062cf93,+0xb8: 0xffffffff8069471f,+0xd0: 0xffffffff806946c3) β direct KASLR-defeating material delivered to the device. - The ioctl completed (
ret=0) after the normal fixate wait; the guest stayed up. A second independent sample of the same leak was captured in thecapfail=1panic run (../DF-4185/panic_serial_full.log).
Direction note: MODE SELECT (opcode 0x55) with flags=0 is hostβdevice
(ata-queue.c:204 request->flags = flags | ATA_R_ATAPI with no ATA_R_READ).
On the QEMU guest the (emulated) device rejects the command before the data
phase, so the bytes were captured by the hook reading the same source buffer
the DMA engine reads; on crafted hardware the device simply accepts the data
phase and receives them. The over-read length (265 vs 64) is proven by the
command itself, independent of the device's acceptance.
Threat model honesty
/dev/acd0opens are gated bycaps_priv_check_self(SYSCAP_RESTRICTEDROOT)indiskopen()(sys/kern/subr_disk.c:1071-1073) β verified live (a plainoperator-group user gets EPERM). So the attacker on stock DF is root / RESTRICTEDROOT software (e.g. cdrtools) combined with a malicious drive, or any configuration that relaxes that gate. The device-side input is the attacker-controlled element; the kernel must not trust it.- Secondary variant: because MODE SENSE may transfer fewer bytes than
sizeof(param), a short-but-valid response leaves part ofparamuninitialized, and the follow-up MODE SELECT sends it β same disclosure family, subsumed by the same fix (bzero the struct or clamp+bound).
Fix validation (both fixes exercised)
Kernel rebuilt with fix.diff on top of the same emulation:
df_audit.pl=255+capfail=1(identical attack):CDRIOCFIXATE -> ret=-1 errno=22(EINVAL). No MODE SELECT issued at all, no over-read dump, no panic (fix_run.log).df_audit.pl=50(valid page) +capfail=1: MODE SELECTcount=60(in-bounds), driver proceeds through CLOSE TRACK / TEST_UNIT_READY / the timeout division, ioctl returns EIO after the normal wait β no trap (fix.2.log; this run also validates the DF-4185 divisor clamp).
Path references
sys/dev/disk/nata/atapi-cd.c:1140-1150β mode_sense into 64-byte stackparam;acd_mode_select(dev, ¶m, param.page_length + 10)sys/dev/disk/nata/atapi-cd.c:1674-1681βacd_mode_selectpasses pagesize straight toata_atapicmd(DATA-OUT)sys/dev/disk/nata/ata-queue.c:188-212βata_atapicmdsetsbytecount = countfor the transfersys/dev/disk/nata/atapi-cd.hβstruct write_param(64-byte packed,page_lengthat offset 9)
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
fixedfix.diff applied on top of the same crafted-device emulation and rebuilt: with the identical attack (page_length=0xff) acd_fixate returns EINVAL and issues no MODE SELECT at all (no data-out, no disclosure); with a valid page (0x32) MODE SELECT is sized 60 bytes (in-bounds) and the driver completes the ioctl without the trap that the same configuration caused pre-fix (fix.2.log). Guest kernel #6 (uname '#6: Fri Sep 18 12:49:47 UTC 2026')
fix_run.log: 'CDRIOCFIXATE -> ret=-1 errno=22' and NO 'ccb0=55' line; fix.2.log: 'hook ccb0=55 ccb2=00 cnt=60' (was 265) + 'survived (no panic)' with capfail=1 (baseline: Fatal trap 18 idivl panic)
Confirmed kernel references
Detail
Exploit chain
crafted ATAPI device answers MODE SENSE(0x05) with page_length=0xff -> kernel CDRIOCFIXATE -> acd_fixate -> acd_mode_select(¶m, 0xff+10) -> 265-byte DATA-OUT from 64-byte stack object -> device harvests 201 bytes of kernel stack incl. kernel text/heap/stack pointers (KASLR defeat / target reconnaissance for a follow-up memory-corruption exploit)
Evidence (decisive lines)
run.log (decisive run: 'hook ccb0=55 ... cnt=265' + over-read hexdump with annotated kernel pointers); leak_sample.txt (two independent samples); dfaud_dmesg_raw.txt; fix_run.log + fix.2.log (fix validation: count=60 in-bounds / EINVAL with no data-out); VERDICT.md
PoC changes
initial draft passed (void*)1 as the ioctl arg (EFAULT at copyin); fixed to a real int pointer; added TOC/blocksize sanity probes; verification required a guest-local crafted-device emulation hook (emu_notes.txt) because stock QEMU ATAPI refuses MODE SENSE page 0x05 (verified: ILLEGAL REQUEST asc=0x24)
Verified recommended fix
clamp the device-fed page length in acd_fixate to the MMC maximum (1..0x32) before acd_mode_select
Verdict
acd_fixate() sizes its MODE SELECT data-out transfer from the device-fed write-parameters page_length (offset 9 of the MODE SENSE 0x05 response) with no validation, sending page_length+10 bytes out of a 64-byte __packed stack struct: a crafted ATAPI device answering page_length=0xff receives 201 bytes of adjacent kernel stack. Reproduced on the audit guest using the DF-3690-family crafted-device emulation (guest kernel rebuilt with a device-response hook in ata_atapicmd; atapi-cd.c unmodified): MODE SELECT count=265 observed, and the over-read payload contains 0xfffff800xxxxxxxx kernel pointers and 0xffffffff80xxxxxx kernel-text pointers (KASLR-defeating material). Reachability on stock DF is gated by the SYSCAP_RESTRICTEDROOT disk-open check (subr_disk.c:1072), so the realistic pairing is root/RESTRICTEDROOT burning software against a malicious or firmware-compromised drive; the device-controlled input must not be trusted regardless.
No comments yet.