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

Divide-by-zero in XPT_CALC_GEOMETRY from unvalidated block_size (local DoS / panic)

Summary

XPT_CALC_GEOMETRY handler computes size_mb=ccg->volume_size/((1024L*1024L)/ccg->block_size) with no check block_size non-zero or inner division non-zero. block_size fed directly from SCSI READ CAPACITY response (scsi_da.c:1831 scsi_4btoul(rdcap->length)) no validation or set arbitrarily by pass(4) CAMIOCOMMAND. block_size==0 inner division by-zero. block_size>1MB inner quotient 0 outer division by-zero. Either panics kernel.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2429 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
dfbt_rig.c β€” 26.9 KB view raw
dfbt_ctl.c β€” 8.0 KB view raw
Makefile β€” 127 B ↓ download
push_rig.sh β€” 975 B view raw
build.sh β€” 185 B view raw
run.sh β€” 90 B view raw
run.log β€” 362 B view raw
panic.txt β€” 2.3 KB view raw
env.txt β€” 249 B view raw
fix.diff β€” 6.0 KB view raw
manifest.json β€” 779 B view raw
verdict.json β€” 3.0 KB view raw

DF-2429 β€” Divide-by-zero in bt(4) XPT_CALC_GEOMETRY from device-fed block_size

Verdict: REPRODUCED (kernel panic via the production handler)

The QEMU guest has no BusLogic MultiMaster HBA, so the compiled-in bt(4) driver (bt at X86_64_GENERIC:102; btaction/btdone/btreset/ bttimeout present in /boot/kernel/kernel) never attaches. The rig (dfbt_rig.c) stands in only for the missing PCI hardware:

  • resolves the REAL static production handlers from the running kernel image (no KASLR; host-side nm + runtime prologue byte-verification of all six handlers before any dispatch);
  • registers a REAL cam_sim whose sim_action IS production btaction, over a fabricated post-attach softc (real lockmgr lock, real bus_dma tags, real DMA memory for CCBs/mailboxes);
  • drives the REAL CAM XPT layer with xpt_action() on real xpt_alloc_ccb'd CCBs;
  • scripts a 4-byte virtual register file behind bt->port (X86_64_BUS_SPACE_MEM tag β†’ plain volatile memory, see sys/cpu/x86_64/include/bus_dma.h:134-142).

Build

./push_rig.sh        # scp sources, build kld + ctl, kldload, prime
# then verify prologue magic words against the expected constants

Run

sh run.sh            # = run_df2429.sh

Expected output

  1. calcgeom 0 4194304 survives: heads=0 spt=0 cyl=0 status=0x1 β€” the CAM layer filters block_size == 0 at cam_xpt.c:3063-3071, so the ==0 variant NEVER reaches bt.c (partial false-positive in the original finding; documented in VERDICT.md).
  2. calcgeom 2097152 4194304 (block_size = 2 MiB > 1 MiB): the filter passes (both fields non-zero), production btaction's XPT_CALC_GEOMETRY case computes (1024*1024)/block_size == 0 and then divides volume_size by it (bt.c:1157-1158) β†’ Fatal trap 18: integer divide fault, Stopped at btaction+0x4ff: divq %rsi,%eax (see panic.txt).

Threat model

ccg->block_size is fed unvalidated from SCSI READ CAPACITY (scsi_da.c:1831 scsi_4btoul(rdcap->length) β†’ scsi_da.c:2302 ccg->block_size = dp->secsize), so a lying/hostile SCSI target behind a real bt(4) HBA reporting secsize > 1 MiB panics the kernel (local DoS from device-supplied data).

VERDICT.md
↓ download raw

DF-2429 VERDICT β€” XPT_CALC_GEOMETRY divide-by-zero in bt(4)

Bottom line

REPRODUCED β€” kernel panic (Fatal trap 18, integer divide fault) inside the production btaction handler, dispatched through the real CAM XPT layer. One honest correction to the finding: the block_size == 0 variant named in the summary never reaches bt.c, because the CAM layer filters it (cam_xpt.c:3063-3071, "Filter out garbage"). The second variant β€” block_size > 1 MiB, which zeroes the inner quotient (1024*1024)/block_size and then divides volume_size by zero β€” passes that filter and panics exactly as claimed.

How it was reproduced

Rig = dfbt_rig.c (details in README.md): real production btaction resolved from /boot/kernel/kernel (no KASLR; prologue bytes verified), real registered cam_sim + real xpt_action dispatch over a fabricated post-attach softc. xpt_action(XPT_CALC_GEOMETRY) calls sim->sim_action directly (cam_xpt.c:3073), so the CCB reaches production btaction unmodified.

Run (run.log, panic.txt):

$R calcgeom 0 4194304
[rig] CALCGEOM: xpt_action(block_size=0 volume_size=4194304)
[rig] CALCGEOM survived: heads=0 spt=0 cyl=0 status=0x1     <- CAM filter
$R calcgeom 2097152 4194304
[rig] CALCGEOM: xpt_action(block_size=2097152 volume_size=4194304)
Fatal trap 18: integer divide fault while in kernel mode
instruction pointer = 0x8:0xffffffff80430bcf
kernel: type 18 trap, code=0
Stopped at      btaction+0x4ff: divq    %rsi,%eax

btaction+0x4ff disassembles to mov %rcx,%rax; divq %rsi; cmp $0x3ff,%eax β€” the size_mb = volume_size / ((1024*1024)/block_size) computation at bt.c:1157-1158 followed by the size_mb >= 1024 test at bt.c:1160. The divisor %rsi holds (1024*1024)/2097152 == 0.

Root cause (path:line)

Impact

Kernel panic (local DoS) from a hostile/lying SCSI target's READ CAPACITY data on real bt(4) hardware. Precondition: a BusLogic MultiMaster HBA with an attached target reporting secsize > 1 MiB (pass(4) CAMIOCOMMAND with a crafted CCB reaches the same handler on any CAM bus whose SIM is bt(4)).

Why the ==0 variant is filtered (negative result)

xpt_action(XPT_CALC_GEOMETRY) at cam_xpt.c:3062-3071 zeroes the geometry and completes with CAM_REQ_CMP when block_size == 0 β€” the demonstrated calcgeom 0 4194304 survived with status 0x1 (run.log). A direct btaction dispatch with block_size == 0 would still divide by zero (same bt.c:1157 line), but no in-tree caller can deliver it through the XPT.

Fix validation

fix.diff guards block_size == 0 || block_size > (1024*1024) with CAM_REQ_INVALID before the division. The patched bt.c + btreg.h were compiled with the X86_64_GENERIC kernel's own flags and headers on the guest: compiles clean under -Werror (FIXED-COMPILES-CLEAN, bt.fixed.o produced). No runtime A/B kernel build: this is a panic class finding, not memory corruption, so a fix kernel rebuild is not mandated by the audit contract; the fix is a two-line guard directly on the proven faulting line.

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 reproduced→ patch + rebuild →patched clean

object-compile validation only (panic class, not memory corruption; kernel A/B not mandated): patched bt.c+btreg.h compile clean with the X86_64_GENERIC kernel flags

fix.diff; guest build log line 'FIXED-COMPILES-CLEAN' with bt.fixed.o produced (cc -Werror, kernel headers)
↓ fix.diffper-fix-DF-2429

Confirmed kernel references

Detail

Exploit chain

lying SCSI target reports secsize>1MiB in READ CAPACITY -> da(4) dasetgeom -> xpt_action(XPT_CALC_GEOMETRY) (filter passes, both fields nonzero) -> production btaction bt.c:1157 computes (1024*1024)/2097152==0 then volume_size/0 -> #DE -> kernel panic

Evidence (decisive lines)

panic.txt (Fatal trap 18, Stopped at btaction+0x4ff: divq %rsi,%eax; disassembly context in VERDICT.md); run.log (variant 1 block_size=0 survives with heads=0 status=0x1 proving the CAM filter; variant 2 panics)

PoC changes

original finding cited block_size==0 as the trigger; the CAM layer filters that case (proven live), so the PoC uses block_size=2097152 (>1MiB) which zeroes the inner quotient -- same division, same line, actually reachable

Verified recommended fix

Reject block_size==0 or >1MiB with CAM_REQ_INVALID before the division (fix.diff, object-compile validated under -Werror with kernel headers)

Verdict

REPRODUCED: production btaction XPT_CALC_GEOMETRY handler panics the kernel (Fatal trap 18 integer divide fault at btaction+0x4ff, the bt.c:1157 size_mb division) when xpt_action delivers block_size=2MiB (device-fed READ CAPACITY value that passes the CAM-layer block_size==0/volume_size==0 filter at cam_xpt.c:3063-3071). Honest correction: the block_size==0 variant cited first in the finding never reaches bt.c -- the CAM filter completes it with CAM_REQ_CMP (demonstrated live); only the >1MiB variant (inner quotient 0) panics. Local DoS/panic from hostile SCSI target data on real bt(4) hardware.