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

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, &param, 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4184 Β· 19 files
FileTypeDescriptionSize
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: CDRIOCFIXATE ioctl on /dev/acd0 (0640 root:operator, plus the DF disk layer's SYSCAP_RESTRICTEDROOT gate at sys/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)&param, sizeof(param))))    /* device fills it */
        return error;
param.data_length = 0;
...
if ((error = acd_mode_select(dev, (caddr_t)&param, 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 returns EINVAL immediately β€” 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 qwords
  • dfaud_dmesg_raw.txt β€” raw dmesg of the F1 boot
  • build.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 runs
  • env.txt, userland_runs.txt
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

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):

  1. CDRIOCFIXATE reaches acd_fixate() (traced: acd_ioctl cmd=8004636a, CDRIOCFIXATE entered, acd_fixate entered, calling mode_sense(0x05)).
  2. acd_mode_sense(..., sizeof(param)) runs as ata_atapicmd with count=64 (kernel-computed sizeof(struct write_param)).
  3. The crafted response sets param.page_length = 0xff (atapi-cd.c:1150 reads it back unvalidated).
  4. acd_mode_select(dev, &param, 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 of acd_fixate's kernel stack frame.
  5. The over-read bytes (captured in run.log / leak_sample.txt) contain canonical kernel pointers: 0xfffff800xxxxxxxx heap/stack pointers and 0xffffffff80xxxxxx kernel-text pointers (+0x38: 0xffffffff8062cf93, +0xb8: 0xffffffff8069471f, +0xd0: 0xffffffff806946c3) β€” direct KASLR-defeating material delivered to the device.
  6. 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 the capfail=1 panic 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/acd0 opens are gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT) in diskopen() (sys/kern/subr_disk.c:1071-1073) β€” verified live (a plain operator-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 of param uninitialized, 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 SELECT count=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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #6: Fri Sep 18 12:49:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted ATAPI device answers MODE SENSE(0x05) with page_length=0xff -> kernel CDRIOCFIXATE -> acd_fixate -> acd_mode_select(&param, 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.