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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1442 Β· 13 files
FileTypeDescriptionSize
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
README.md readme human-facing build/run/expected + reachability note
↓ download 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.
VERDICT.md verdict full mechanism trace, threat model, harness result, fix validation
↓ download raw

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_ADDR 0xdeadc0de) and tracks chunk state in z_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.diff applies 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, sha256 9f885e22..., BuildID dd34bd78...) and boots cleanly as DragonFly 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

fixed

VALIDATED: harness UNPATCHED canary clobbered; PATCHED clean. Kernel rebuild+boot.

BEFORE: canary 96/97 = 0x01/0x02. AFTER: 0xcd/0xcd (intact).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Fri Jul 17 15:23:44 UTC 2026

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.