afd_sense(): three uninitialized stack structs consumed as ATAPI response buffers β short (but successful) device responses publish stale kernel stack as disk geometry
Summary
The response structs are never zeroed and ata_atapicmd treats short data-in underruns as success; only page_code is validated while geometry fields are consumed directly - stale stack becomes mediasize/geometry, weakly leaked via disk-info ioctls and used in LBA math (the !mediasize guard keeps the DF-1945 div0 closed). Hostile-device model. bzero-before-each-command fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4876 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 2.7 KB | β raw | |
| env.txt | β | 195 B | view raw | |
| fix.diff | β | 738 B | view raw | |
| manifest.json | β | 582 B | view raw | |
| verdict.json | β | 2.5 KB | view raw |
DF-4876 β afd_sense() consumes uninitialized stack on short ATAPI responses
sys/dev/disk/nata/atapi-fd.c:341-417: afd_sense() declares three
uninitialized stack structs and passes them to ata_atapicmd():
struct afd_capacity capacity; /* :345 */
struct afd_capacity_big capacity_big;/* :346 */
struct afd_capabilities capabilities;/* :347 */
...
if (!ata_atapicmd(dev, ccb1, (caddr_t)&capacity, sizeof(...), ATA_R_READ, 30)) {
fdp->sectorsize = be32toh(capacity.blocksize);
fdp->mediasize = (u_int64_t)be32toh(capacity.capacity) * fdp->sectorsize; ...
...
if (!ata_atapicmd(dev, ccb3, (caddr_t)&capabilities, 40, ATA_R_READ, 30) &&
capabilities.page_code == ATAPI_REWRITEABLE_CAP_PAGE) {
fdp->heads = capabilities.heads; /* struct offset 12 */
fdp->sectors = capabilities.sectors; /* offset 13 */
fdp->sectorsize = be16toh(capabilities.sector_size); /* 14-15 */
fdp->mediasize = be16toh(capabilities.cylinders) * /* 16-17 */
fdp->heads * fdp->sectors * fdp->sectorsize;
ata_atapicmd() (sys/dev/disk/nata/ata-queue.c:188-214) issues the packet
command and returns request->result. ATAPI data-in phases routinely
underrun (device returns fewer bytes than the allocation length) while
still completing with success status β donecount < bytecount is not an
error in the nata completion path. The kernel never zeroes request->data.
Therefore a device that returns, e.g., an 8-byte MODE SENSE(10) response
(header only) or a truncated capabilities page leaves
capabilities.heads/sectors/sector_size/cylinders (offsets 12..17) as
stale kernel stack bytes, which are then:
- multiplied into
fdp->mediasize(u32 wrap already tracked as DF-2508) and published as disk geometry viadisk_setdiskinfo()(atapi-fd.c:217) β a (weak) kernel-stack info leak observable through the disk-info ioctls, and - used as divisors/geometry in
afd_open()(atapi-fd.c:211-215) andafd_strategy()'s LBA math (atapi-fd.c:258-259), producing garbage I/O placement; a zero divisor here is pre-conditioned by the!mediasizeguard, but arbitrary non-zero garbage is not.
Only capabilities.page_code (offset 8) is validated (:405) β the
consumed fields sit past it.
Status: untested (in situ) β requires an ATAPI direct-access device
Same blocker as DF-4874/4875: the audit guest cannot attach an afd
device (QEMU 7.2 has no ATAPI direct-access emulation), so a short-response
device cannot be presented. The defect is established by trace:
ata_atapicmd() never zeroes the buffer, short-but-successful ATAPI
data-in is not an error (ata-queue.c:188-214 + nata interrupt completion),
and the struct fields past the validated page_code are consumed directly
(atapi-fd.c:406-410).
Fix
fix.diff: bzero() all three response structs before each command, so a
short device response yields zeros (which the existing !mediasize /
ENXIO paths then reject cleanly) instead of stale stack.
DF-4876 VERDICT β afd_sense() uninitialized-stack consumption
Status: untested in situ (hardware absent); established by line trace
Root cause
afd_sense() uses three uninitialized stack structs as ATAPI response
buffers (sys/dev/disk/nata/atapi-fd.c:345-347). ata_atapicmd()
(sys/dev/disk/nata/ata-queue.c:188-214) sets request->data = data,
request->bytecount = count, queues, and returns request->result; the
nata completion path treats a successful command with a short data-in
transfer (donecount < bytecount) as success β no zero-fill of the
untransferred tail exists anywhere on this path (verified in ata-queue.c
and the request setup; bzero of request->data is absent).
The only field validated before use is capabilities.page_code
(atapi-fd.c:405). A device returning a short MODE SENSE(10) page (or a
short READ_CAPACITY / READ_CAPACITY_16 block descriptor) leaves
offsets 12-17 of struct afd_capabilities (heads, sectors, sector_size,
cylinders β sys/dev/disk/nata/atapi-fd.h:62-65) or the tail of the
capacity structs as stale kernel stack, which the driver then publishes as
disk geometry (atapi-fd.c:406-410, 383-384, 395-396).
Consequences
- Kernel stack bytes are multiplied into
fdp->mediasize/d_ncylinders/d_secpertrackand exposed to userland through the disk-info ioctls after open (atapi-fd.c:207-217) β a degraded (product-of-fields) stack-content leak, CWE-457. - Garbage geometry feeds
afd_strategy()'sbio_offset / fdp->sectorsizeLBA math (atapi-fd.c:258) β misplaced raw I/O on a device whose actual geometry differs. - Zero-valued garbage is caught by the existing
!mediasizeβ ENXIO guard (atapi-fd.c:204), so this does not reopen the known DF-1945 div0 sink.
Why untested, honestly
Triggering requires an ATAPI direct-access device that returns a short
data-in response β the audit guest cannot attach ANY afd device (QEMU 7.2
-device help has no ATAPI floppy; recorded in DF-4874/run.log:
"afd0 device_t is NULL"). This is a hardware-availability blocker, not a
refutation: every hop of the trace (uninitialized declaration β non-
zero-filling transport β unvalidated consumption) was verified in source.
Fix validation
fix_status: not_testable β same hardware blocker. The fix (bzero of the
three structs before the commands, fix.diff) converts any short response
into zeros, which the driver already rejects via !mediasize ENXIO.
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_testableNo afd hardware on guest; fix converts short responses into zeros which the existing !mediasize ENXIO path rejects
fix.diff (git apply --check OK)
Confirmed kernel references
Detail
Exploit chain
crafted ATAPI floppy returns short MODE SENSE(10)/READ_CAPACITY data with success status -> struct tail stays stale stack (atapi-fd.c:345-347, consumed at :383-384/:395-396/:406-410) -> garbage geometry via disk_setdiskinfo (atapi-fd.c:207-217) -> observable via DIOCG*-style disk info (weak kernel-stack leak) + misplaced raw I/O from afd_strategy LBA math
Evidence (decisive lines)
VERDICT.md (full trace incl. ata_atapicmd non-zeroing path); README.md; DF-4874/run.log (shared hardware blocker)
PoC changes
n/a β source-trace finding; hardware blocker shared with DF-4874
Verified recommended fix
bzero(&capacity)/(&capacity_big)/(&capabilities) before each ata_atapicmd in afd_sense()
Verdict
afd_sense() hands three uninitialized stack structs to ata_atapicmd(); short-but-successful ATAPI data-in responses (normal underrun, not an error in the nata completion path, no zero-fill of request->data) leave heads/sectors/sector_size/cylinders past the only validated field (page_code) as stale kernel stack, which is then published as disk geometry β CWE-457 with a degraded stack-content leak via disk-info ioctls and garbage LBA math in afd_strategy. Verified hop-by-hop in source; not runnable because the guest cannot attach an ATAPI direct-access device.
No comments yet.