INQUIRY TRIM status writes past short inquiry buffer (heap overflow during DV probe)
Summary
ahci_xpt_scsi_disk_io INQUIRY at ahci_cam.c:1134-1139: vendor_specific1[0]/[1] at byte offset 96-97 from data_ptr written unconditionally for TRIM disks. CAM DV allocates 38-byte inquiry buffer. offset 96 -> 58 bytes past 38-byte buffer -> heap overflow into adjacent slab object. Automatic at boot for any TRIM-capable AHCI SSD. max_dsm_blocks from IDENTIFY word 105 controlled by malicious disk. Fix: check rdata_len>=offsetof(vendor_specific1)+2 before write.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1442 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace harness replicating the exact kernel structs + DV1 kmalloc(38) + offset-96/97 write, with canary detection; runs UNPATCHED + PATCHED logic modes | 9.2 KB | view raw |
| build.sh | build-script | cc -Wall -Wextra -O2 -o harness harness.c | 248 B | view raw |
| run.sh | run-script | ./harness | 95 B | view raw |
| fix.diff | suggested-fix | git-apply-able size guard: rdata_len >= offsetof(scsi_inquiry_data,vendor_specific1)+2 before the TRIM-status write | 819 B | view raw |
| VERDICT.md | verdict | full mechanism trace, threat model, harness result, fix validation | 7.3 KB | β raw |
| README.md | readme | human-facing build/run/expected + reachability note | 3.2 KB | β raw |
| build.log | build-log | harness build output (gcc 8.3 on guest) | 65 B | view raw |
| run.log | run-log | decisive harness run: UNPATCHED overflow + PATCHED clean | 1.7 KB | view raw |
| fix_build.log | build-log | single-fix kernel build (nativekernel) full output, rc=0 | 5.6 MB | β download |
| fix_run.log | run-log | harness run on the booted patched #1 kernel: PATCHED mode = no overflow | 1.7 KB | view raw |
| env.txt | environment | uname, kern.version, camcontrol devlist (no AHCI SATA disk), dmesg, ahci.ko presence, cc version | 1.2 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1442 β INQUIRY TRIM status writes past short inquiry buffer (heap overflow)
File: sys/dev/disk/ahci/ahci_cam.c (ahci_xpt_scsi_disk_io, INQUIRY case, lines 1134-1139)
Severity: High β kernel heap overflow at boot for any TRIM-capable AHCI SATA disk.
What the bug is
ahci_xpt_scsi_disk_io() unconditionally writes the disk's TRIM status into
rdata->inquiry_data.vendor_specific1[0]/[1] (byte offsets 96/97 of the
inquiry buffer) whenever the disk reports DSM/TRIM support. CAM Domain
Validation (PROBE_INQUIRY_BASIC_DV1/DV2 in sys/bus/cam/cam_xpt.c) allocates
only a kmalloc(38) inquiry buffer for AHCI disks (because AHCI sets
additional_length=32 β SID_ADDITIONAL_LENGTH=37 β roundup2(,2)=38). The
writes at byte 96/97 therefore land 58/59 bytes past the 38-byte allocation,
overflowing into the adjacent slab object. This fires automatically at boot.
Reachability note (why a harness, not a live in-kernel run)
The audit guest has no AHCI SATA disk (only vtblk0 virtio-blk + acd0
DVD-ROM on the legacy ata(4) driver). The ahci_xpt_scsi_disk_io path is
only reachable via an ahci(4) HBA with a disk, which the guest's QEMU config
does not provide. The driver is compiled in (device ahci) and the module
exists, but there is no hardware to probe. The struct layout and offset
arithmetic are identical in-kernel, so harness.c reproduces the exact
vulnerable write using the verbatim kernel structs and the exact DV1
allocation math, with a canary guard to detect the out-of-bounds write.
Build & run
./build.sh # cc -Wall -Wextra -O2 -o harness harness.c ./run.sh # ./harness (runs UNPATCHED + PATCHED logic modes)
Expected output
[UNPATCHED] mode=0 ... canary clobbered at 96/97: YES / YES *** HEAP OVERFLOW CONFIRMED (mode 0) *** [PATCHED] mode=1 ... canary clobbered at 96/97: no / no *** NO OVERFLOW (mode 1): write correctly skipped/guarded *** VERDICT: bug reproduced on unpatched logic, fixed on patched logic.
The fix
fix.diff adds a size guard before the write:
if (support_dsm && rdata_len >= offsetof(struct scsi_inquiry_data, vendor_specific1) + 2).
For the 38-byte DV buffer, 38 >= 98 is false β write skipped. For a full
inquiry buffer, the write proceeds as before. Apply with patch -p1 < fix.diff.
Threat model
Not an unprivileged-userβroot privesc. The written bytes come from the disk's own IDENTIFY data at boot-time DV probe; an unprivileged user cannot present a fake IDENTIFY, trigger the boot-time probe, or shape boot-time slab layout. The attacker is a malicious AHCI/USB-SATA device. On GENERIC (INVARIANTS ON) the adjacent-chunk corruption trips slab checks β panic/DoS at boot; on a non-INVARIANTS kernel it is silent heap corruption.
Files
harness.cβ deterministic userspace harness (verbatim kernel structs).build.sh/run.shβ build/run.fix.diffβ git-apply-able size-guard fix.VERDICT.mdβ full analysis.build.log,run.logβ harness build/run output.fix_build.log,fix_run.logβ patched-kernel build + harness-on-patched output.env.txtβ guest environment.manifest.jsonβ artifact catalog.
DF-1442 β INQUIRY TRIM status writes past short inquiry buffer (heap overflow)
Verdict: REPRODUCED (source-trace + deterministic harness); FIX VALIDATED
Severity (per finding): High
Impact: kernel heap overflow (2-byte write past a kmalloc(38) slab object),
fires automatically at boot for any TRIM-capable AHCI SATA disk.
The bug (mechanism, path:line at every hop)
sys/dev/disk/ahci/ahci_cam.c, function ahci_xpt_scsi_disk_io(), the
INQUIRY case. After the normal/EVPD inquiry body, the handler writes the
disk's TRIM status into the inquiry buffer unconditionally (if the disk
reports DSM/TRIM support):
/* sys/dev/disk/ahci/ahci_cam.c:1134-1139 (UNPATCHED) */
if (at->at_identify.support_dsm) {
rdata->inquiry_data.vendor_specific1[0] =
at->at_identify.support_dsm & ATA_SUPPORT_DSM_TRIM; /* byte 96 */
rdata->inquiry_data.vendor_specific1[1] =
at->at_identify.max_dsm_blocks; /* byte 97 */
}
rdata is csio->data_ptr (the caller-supplied inquiry buffer) and
rdata->inquiry_data.vendor_specific1 lives at byte offset 96 of
struct scsi_inquiry_data (verified: sizeof == 256,
offsetof(vendor_specific1) == 96; all fields are 1-byte aligned so there is
no padding to discount). So the two writes land at bytes 96/97 of the
buffer regardless of its actual length.
Who allocates a short buffer? CAM Domain Validation. In
sys/bus/cam/cam_xpt.c::probescsi(), the PROBE_INQUIRY_BASIC_DV1/DV2
states allocate a separate inquiry buffer sized to the device's reported
additional_length:
/* sys/bus/cam/cam_xpt.c:5864-5880 (abridged) */
if (softc->action == PROBE_INQUIRY)
inquiry_len = SHORT_INQUIRY_LENGTH; /* 36 */
else
inquiry_len = SID_ADDITIONAL_LENGTH(inq_buf); /* additional_length + 5 */
inquiry_len = roundup2(inquiry_len, 2);
if (softc->action == PROBE_INQUIRY_BASIC_DV1 || ...DV2) {
inq_buf = kmalloc(inquiry_len, M_CAMXPT, M_INTWAIT); /* <-- short buf */
}
scsi_inquiry(csio, ..., inq_buf, inquiry_len, evpd=FALSE, ...);
AHCI hardcodes additional_length = 32 (ahci_cam.c:1119), so
SID_ADDITIONAL_LENGTH = 32 + 5 = 37, roundup2(37,2) = 38 β kmalloc(38).
The subsequent vendor_specific1[0]/[1] writes at byte 96/97 are therefore
58/59 bytes past the 38-byte allocation β a heap overflow into the adjacent
slab object.
DV runs for all lun-0 devices (the SID_Sync gate is commented out at
cam_xpt.c:6433), so this fires at every boot for any TRIM-capable AHCI disk.
Why a harness, not a live in-kernel run
The audit guest has no AHCI SATA disk β only vtblk0 (virtio-blk root fs)
and acd0 (a DVD-ROM on the legacy ata(4) driver, ata1-master). The
ahci_xpt_scsi_disk_io path is only reachable via an ahci(4) HBA with a
disk, which the QEMU config (dfbsd-qemu/vm.sh lines 55-62) does not provide.
The driver is compiled in (device ahci, sys/config/X86_64_GENERIC:65) and
the module exists (/boot/kernel/ahci.ko), but there is no hardware to probe.
Because the struct layout is identical in-kernel and the offset arithmetic is
the entire bug, harness.c reproduces the exact vulnerable write using the
verbatim kernel structs from sys/bus/cam/scsi/scsi_all.h, the exact DV1
allocation math, and a canary guard to detect the out-of-bounds write.
Harness result
sizeof(struct scsi_inquiry_data) = 256 offsetof(vendor_specific1) = 96 CAM DV1 inquiry_len = 38 (additional_length=32) [UNPATCHED] mode=0, buffer=38 bytes, write at offset 96/97 canary clobbered at 96/97: YES / YES (0x01/0x02 vs canary 0xcd) *** HEAP OVERFLOW CONFIRMED (mode 0) *** wrote 0x01 at arena[96] and 0x02 at arena[97] (canary was 0xcd; these bytes are 58/59 past the 38-byte allocation) [PATCHED] mode=1, buffer=38 bytes, write at offset 96/97 canary clobbered at 96/97: no / no (0xcd/0xcd vs canary 0xcd) *** NO OVERFLOW (mode 1): write correctly skipped/guarded *** === SUMMARY === UNPATCHED logic: OVERFLOW (bug present) PATCHED logic: clean (fix holds)
Threat model & realistic impact ceiling
This is not an unprivileged-local-user-to-root escalation. The two written bytes come from the disk's own IDENTIFY data at boot-time DV probe:
vendor_specific1[0]=support_dsm & ATA_SUPPORT_DSM_TRIM= fixed 0x01 (TRIM bit), not attacker-shaped.vendor_specific1[1]=max_dsm_blocks(IDENTIFY word 105) β controlled by a malicious disk, not by a user process.
An unprivileged local user cannot present a fake IDENTIFY, cannot trigger
the boot-time DV probe on demand (camcontrol rescan is root-only), and cannot
shape the slab layout at boot (no user processes exist yet). The realistic
attacker is therefore a malicious AHCI SATA / USB-SATA device (or a crafted
disk image) that corrupts adjacent kernel heap during probe.
- Default GENERIC kernel (INVARIANTS ON): the slab allocator poisons free
chunks (
WEIRD_ADDR0xdeadc0de) and tracks chunk state inz_Bitmap(sys/kern/kern_slaballoc.c). Corrupting an adjacent chunk's bytes trips the INVARIANTS checks on its next alloc/free β panic (KASSERT) / DoS. - Non-INVARIANTS kernel: silent corruption of the adjacent slab object.
So the realistic impact ceiling on a stock system is a boot-time kernel panic / DoS whenever a TRIM-capable AHCI SSD is attached; a malicious device could in principle aim the corruption at a chosen slab neighbor. There is no unprivileged-user escalation chain β the valid blocker is that the write is reachable only from the disk's boot-time IDENTIFY, a context no local user can drive.
The fix (fix.diff)
Guard the TRIM-status write with a size check so it only fires when the inquiry
buffer actually extends to vendor_specific1[1]:
/* sys/dev/disk/ahci/ahci_cam.c (PATCHED) */
if (at->at_identify.support_dsm &&
rdata_len >= offsetof(struct scsi_inquiry_data, vendor_specific1) + 2) {
rdata->inquiry_data.vendor_specific1[0] = ...;
rdata->inquiry_data.vendor_specific1[1] = ...;
}
offsetof(... vendor_specific1) + 2 == 98. For the DV1/DV2 38-byte buffer,
38 >= 98 is false β write correctly skipped. For a full 256-byte inquiry
buffer, 256 >= 98 β write proceeds as before. Minimal, targeted at the root
cause; matches the finding's proposed fix.
Fix validation (Phase 8)
fix.diffapplies cleanly (patch -p1, Hunk #1 succeeded at 1129).- Single-fix kernel built from the patched source:
make -j6 nativekernel KERNCONF=X86_64_GENERICβ rc=0, no errors (fix_build.log). - Patched kernel installed (
/boot/kernel/kernel, sha2569f885e22..., BuildIDdd34bd78...) and boots cleanly asDragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 15:23:44 UTC 2026. - The harness PATCHED-mode (the same guard logic compiled into the kernel)
shows no overflow on the patched kernel, while UNPATCHED-mode still
overflows (
fix_run.log).
Because the in-kernel trigger needs AHCI SATA hardware absent from the audit guest, the before/after is demonstrated via the harness (which replicates the exact kernel code path) plus the patched-kernel build+boot; the guard is source-verified to close the path.
How to reproduce
./build.sh && ./run.sh
Builds harness and runs both UNPATCHED and PATCHED logic modes. UNPATCHED
prints *** HEAP OVERFLOW CONFIRMED ***; PATCHED prints *** NO OVERFLOW ***.
Fix verification
fixedVALIDATED: harness UNPATCHED canary clobbered; PATCHED clean. Kernel rebuild+boot.
BEFORE: canary 96/97 = 0x01/0x02. AFTER: 0xcd/0xcd (intact).
Confirmed kernel references
β
Detail
Exploit chain
none -- disk-hardware-driven (malicious-disk threat model), not user-syscall-driven.
Evidence (decisive lines)
β
Verdict
REPRODUCED (harness). ahci_cam INQUIRY vendor_specific1[0/1] at offset 96 written unconditionally for TRIM disks. CAM DV1 allocs 38B buffer -> 58B heap overflow. Boot-time for TRIM SSDs. No AHCI SATA disk on guest.
No comments yet.