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

NULL request-descriptor dereference in mfi_tbolt_send_frame for PD_SCSI_IO with READ_10/WRITE_10 CDBs - any I/O to a Thunderbolt system PD panics the kernel

Summary

The 'DJA NA XXX SYSPDIO' else-branch only prints, leaving req_desc==NULL, then unconditionally posts through req_desc->words - faulting on address 0. mfi_build_syspdio builds exactly this frame for EVERY read/write bio to a JBOD system PD, and all TBOLT PCI IDs are also SKINNY so syspd probe attaches on every Thunderbolt card - mounting/fsck/swapping/dumping to a JBOD drive panics on first I/O (boot-time automatic); MFIIO_PASSTHRU reaches it with a fully user-controlled frame (mbox[4] aliases pass.cdb[0]). Verified on the guest to the hardware limit (trigger compiles/runs, ENOENT - QEMU megasas emulates only the non-TBOLT path). Build-the-descriptor fix (also makes syspd I/O and PD crash dumps work).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4390 Β· 11 files
FileTypeDescriptionSize
df4390_trigger.c β€” 4.2 KB view raw
build.sh β€” 93 B view raw
run.sh β€” 99 B view raw
build.log β€” 66 B view raw
run.log β€” 529 B view raw
env.txt β€” 477 B view raw
README.md β€” 2.3 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
fix.diff β€” 1.4 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 3.9 KB view raw

DF-4390 β€” NULL request-descriptor dereference in mfi_tbolt_send_frame()

File: sys/dev/raid/mfi/mfi_tbolt.c:1156-1168 (fall-through), deref at :1181-1182 Severity: Medium Β· Confidence: certain (static) Β· runtime test requires TBOLT hardware

Build

cc -O -Wall -o df4390 df4390_trigger.c        # verified on the DF guest

Run

./df4390 /dev/mfi0                            # or let it try the
                                              # /dev/megaraid_sas_ioctl_node alias

Expected

On a system with a Thunderbolt (LSI 2108/2208 family, MFI_FLAGS_TBOLT, so MFA_enabled == 1) megaraid controller:

  • via this ioctl (root or operator group, /dev/mfi0 is 0640 root:operator): MFIIO_PASSTHRU with header.cmd = MFI_CMD_PD_SCSI_IO (0x02) and mbox[4] = 0x28 (aliases pass.cdb[0] = READ_10) makes mfi_user_command() (mfi.c:2838 bcopy of the whole user frame) hand the frame to mfi_tbolt_send_frame(), which takes the cdb[0] == 0x28 else-branch, prints DJA NA XXX SYSPDIO, leaves req_desc == NULL, and then executes MFI_WRITE4(sc, MFI_ILQP, (req_desc->words & 0xFFFFFFFF)); β†’ immediate kernel panic (read fault at ~address 0).
  • without any ioctl at all: every read/write bio to a system PD (JBOD drive exposed to the host) builds exactly this frame (mfi_build_syspdio(), mfi.c:1992-2001: pass->header.cmd = MFI_CMD_PD_SCSI_IO; pass->cdb[0] = READ_10/WRITE_10), so mounting or fsck'ing a JBOD disk on a TBOLT card panics instantly. Crash dumps to a system PD hit the same path (mfi_dump_syspd_blocks(), mfi.c:2453-2461).

Observed on this audit's QEMU guest (honest negative)

The guest kernel (X86_64_GENERIC, DragonFly 6.5-DEVELOPMENT) has the mfi driver compiled in, but the guest has no LSI PCI device at all (virtio only), so no /dev/mfi0 exists and mfi_tbolt_send_frame() can never run:

$ kldload mfi
kldload: can't load mfi: module already loaded or in kernel
$ ls /dev/mfi*
ls: /dev/mfi*: No such file or directory
$ ./df4390
[-] open(/dev/mfi0): No such file or directory ...

The defect itself is proven statically end-to-end (see VERDICT.md). QEMU's megasas emulation models the 1078/Gen2 chips β€” which take the non-TBOLT mfi_send_frame() path β€” not the Thunderbolt 2108/2208, so no QEMU configuration can reach this code.

VERDICT.md
↓ download raw

DF-4390 VERDICT β€” NULL req_desc deref in mfi_tbolt_send_frame()

Bottom line

Static verdict: CONFIRMED kernel NULL-pointer dereference (panic/DoS) on all Thunderbolt megaraid controllers, reachable (a) by ordinary I/O to a system PD, (b) by crash dumps to a system PD, and (c) by a single management ioctl from root/operator.

Runtime verdict: not_reproduced on this QEMU guest β€” the guest has zero LSI PCI devices; the mfi driver is in-kernel but never attaches, so /dev/mfi0 does not exist and mfi_tbolt_send_frame() cannot execute. This is a hardware-absence limitation, not evidence against the finding.

The bug, traced line by line

mfi_tbolt_send_frame() (sys/dev/raid/mfi/mfi_tbolt.c):

1143:   hdr = &cm->cm_frame->header;
1144:   cdb = cm->cm_frame->pass.cdb;
...
1156:   if (hdr->cmd == MFI_CMD_PD_SCSI_IO) {
1157:       /* check for inquiry commands coming from CLI */
1158:       if (cdb[0] != 0x28 && cdb[0] != 0x2A) {
1159:           if ((req_desc = mfi_tbolt_build_mpt_cmd(sc, cm)) ==
1160:               NULL) {
...                 ... return 1;                    /* handled */
1164:           }
1165:       }
1166:       else
1167:           device_printf(sc->mfi_dev, "DJA NA XXX SYSPDIO\n");
1168:   }                                        /* req_desc still NULL here */
...
1181:   MFI_WRITE4(sc, MFI_ILQP, (req_desc->words & 0xFFFFFFFF));
1182:   MFI_WRITE4(sc, MFI_IHQP, (req_desc->words >>0x20));

req_desc is initialized NULL (line 1140) and the PD_SCSI_IO + READ_10/WRITE_10 case assigns nothing; req_desc->words then reads from address 0 β†’ page fault β†’ panic. No other guard exists between 1168 and 1181 (sc->adpreset was checked at 1145 and is 0 in normal operation).

Reachability (three independent triggers)

  1. System PD bios (no privileges beyond disk access): mfi_build_syspdio() (sys/dev/raid/mfi/mfi.c:1992-2001) sets pass->header.cmd = MFI_CMD_PD_SCSI_IO and pass->cdb[0] = READ_10 (0x28) / WRITE_10 (0x2A) for every bio on a JBOD system PD. mfi_mapcmd() β†’ mfi_data_cb() (mfi.c:2266-2267) β†’ mfi_tbolt_send_frame() when MFA_enabled (set for all TBOLT cards after IOC init, mfi_tbolt.c:430-432). System PDs are probed on every TBOLT card because all TBOLT PCI IDs also carry MFI_FLAGS_SKINNY (mfi_pci.c:119-149) and mfi_startup() runs mfi_syspdprobe() for SKINNY (mfi.c:1205-1206). β‡’ mounting, fsck'ing, swapping, or dumping to a JBOD drive on a TBOLT controller panics on first I/O.
  2. Crash dump: mfi_dump_syspd_blocks() (mfi.c:2453-2461) builds the identical frame (PD_SCSI_IO + WRITE_10) with MFI_CMD_POLLED.
  3. Management ioctl: mfi_user_command() (mfi.c:2838) bcopys the entire user-supplied struct mfi_dcmd_frame β€” header included β€” into the command frame. dcmd.mbox[4] (frame offset 32) aliases pass.cdb[0] (offset 32; MFI_MBOX_SIZE = 12, mfireg.h:462,556-570). MFIIO_PASSTHRU with cmd=0x02, mbox[4]=0x28 and buf_size=0 (cm_data == NULL β†’ direct mfi_tbolt_send_frame(), mfi.c:2167-2169) panics. /dev/mfi0 is root:operator 0640 (mfi.c:721-724).

Exploitability

This is a NULL-deref at a fixed offset (read of req_desc->words at address 0): on this code base it is a denial of service (panic), not a control-flow hijack. There is no attacker-controlled index or write through the NULL pointer (the read value is only written to the card's doorbell). Impact ceiling: reliable local DoS with the triggers above β€” including automatic boot-time panics on JBOD configurations.

Why not reproduced at runtime (and why that is conclusive neither way)

  • Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (Jul 2 2026 build), mfi compiled in (kldload mfi β†’ "already loaded or in kernel").
  • pciconf -l shows only virtio devices β€” no vendor-0x1000 device, so no mfi_attach, no /dev/mfi0 (see env.txt).
  • QEMU's megasas device model emulates 1078/Gen2 controllers (non-TBOLT path mfi_send_frame()); it cannot emulate the 2108/2208 Thunderbolt interface, so no guest configuration can reach mfi_tbolt_send_frame().

Attempts: 1 (decisive β€” the trigger's precondition, the device node, does not exist; see run.log).

Fix

fix.diff in this directory routes the PD_SCSI_IO + READ_10/WRITE_10 case through mfi_tbolt_build_mpt_cmd() like every other PD passthru (this also makes system-PD I/O and PD dumps work, which the "DJA NA XXX" placeholder never did), and removes the now-unused cdb local. Fix validation (fix_status: not_testable): the patched kernel cannot be exercised without TBOLT hardware for the same reason as the baseline.

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_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored and git-apply-verified against sys/dev/raid/mfi/mfi_tbolt.c, but baseline and patched kernels cannot be exercised on this guest (no TBOLT HBA), so behavioral fix validation was not possible.

fix.diff in this pack; scratch-clone git-apply check passed; rationale in VERDICT.md
↓ fix.diffper-fix-DF-4390

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full line-by-line trace); df4390_trigger.c (trigger, with kernel-ABI mirror structs); run.log + env.txt (guest: mfi in-kernel, no LSI PCI device, ENOENT); fix.diff (git-apply verified on a scratch clone)

PoC changes

Seed sketch (none existed - finding is new in pass 2): authored trigger from scratch with layout-accurate packed structs mirroring mfireg.h/mfi_ioctl.h (mfi_dcmd_frame=56B, mfi_ioc_passthru=68B, mbox[4] aliases pass.cdb[0] at frame offset 32); verified it compiles clean with the guest cc and runs.

Verified recommended fix

Always build the MPT descriptor for MFI_CMD_PD_SCSI_IO (drop the cdb[0]!=0x28/0x2A exclusion in mfi_tbolt_send_frame) so req_desc can never be NULL at the doorbell write.

Verdict

Statically confirmed NULL-dereference in mfi_tbolt_send_frame(): for MFI_CMD_PD_SCSI_IO frames whose CDB[0] is READ_10(0x28)/WRITE_10(0x2A) the function prints 'DJA NA XXX SYSPDIO' (mfi_tbolt.c:1166-1167), leaves req_desc == NULL (init at 1140), and dereferences req_desc->words when posting (1181-1182) - guaranteed kernel panic on every Thunderbolt controller. Three live triggers: (1) every read/write bio to a system PD (mfi_build_syspdio, mfi.c:1992-2001, builds exactly this frame; TBOLT cards are all SKINNY so mfi_syspdprobe attaches JBOD devices, mfi_pci.c:119-149 + mfi.c:1205-1206), (2) crash dumps to a system PD (mfi_dump_syspd_blocks, mfi.c:2453-2461), (3) a single MFIIO_PASSTHRU ioctl with cmd=0x02 and mbox[4]=0x28 (aliases pass.cdb[0]; user frame copied verbatim at mfi.c:2838; /dev/mfi0 is root:operator 0640). Runtime reproduction was impossible on this audit guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC has mfi compiled in but the QEMU guest has zero vendor-0x1000 PCI devices, so no /dev/mfi0 exists and mfi_tbolt_send_frame() never executes; QEMU's megasas model (1078/Gen2) cannot emulate the Thunderbolt 2108/2208 path. The trigger compiled and ran on the guest, exiting ENOENT as the hardware-absent proof (run.log, env.txt). Impact field reflects runtime observation only; on real hardware the impact is a reliable local kernel panic (DoS), including automatic boot-time panics on JBOD configurations. Not an exploitation primitive beyond DoS: the only NULL-sourced value is written to the card doorbell, no attacker-controlled index or write.