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

Divide-by-zero panic via XPT_CALC_GEOMETRY with block_size == 0

Summary

XPT_CALC_GEOMETRY handler computes ccg->volume_size/((1024L*1024L)/ccg->block_size) without verifying block_size non-zero. Raw CCB with block_size=0 (freely settable through pass CAMIOCOMMAND ioctl) traps kernel in integer divide-by-zero panicking system. ccg->block_size u_int32_t taken directly from CCB with no zero check. passsendccb -> xpt_merge_ccb reaches amd_action unchanged. [VERIFIED pass2: reproduced both forms - block_size 0 Fatal trap 18 idivq at amd_action+0x16d, and the block_size 2MiB form divq at +0x178 which PASSES the cam_xpt filter checking only ==0 so is the real userland-reachable variant via pass CAMIOCOMMAND on HBA hosts; as-filed zero form is XPT-filtered latent; cam_calc_geometry fix-validated matching trm idiom]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2426 Β· 32 files
FileTypeDescriptionSize
README.md β€” 2.5 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
amd.fixed β€” 63.3 KB ↓ download
amd.fixed.sha256 β€” 65 B ↓ download
build.log β€” 620.2 KB ↓ download
build.sh β€” 342 B view raw
build_outer.log β€” 313 B view raw
df_post.c β€” 26.3 KB view raw
df_pre.c β€” 3.6 KB view raw
env.txt β€” 611 B view raw
fix.diff β€” 827 B view raw
fix_build.log β€” 616.1 KB ↓ download
fix_installkernel.log β€” 80.4 KB view raw
fix_outer.log β€” 223 B view raw
gen.log β€” 105 B view raw
gen.sh β€” 1.6 KB view raw
gen_fixed.log β€” 105 B view raw
make_fixed.sh β€” 3.1 KB view raw
manifest.json β€” 2.1 KB view raw
panic0.txt β€” 4.1 KB view raw
panic_big.txt β€” 2.4 KB view raw
run.log β€” 2.4 KB view raw
run.sh β€” 379 B view raw
run_control.log β€” 29 B view raw
run_fixed.sh β€” 1.4 KB view raw
run_fixed_control.log β€” 29 B view raw
run_fixed_dmesg.log β€” 3.9 KB view raw
run_fixed_trigger0.log β€” 30 B view raw
run_fixed_trigger_big.log β€” 33 B view raw
run_trigger0.log β€” 24 B view raw
run_trigger_big.log β€” 27 B view raw
verdict.json β€” 4.0 KB view raw

DF-2426 PoC β€” Divide-by-zero in amd_action() XPT_CALC_GEOMETRY (amd.c:683)

  • File under test: sys/dev/disk/amd/amd.c (AMD53c974 / Tekram DC-390(T) SCSI HBA driver)
  • Bug: amd_action() case XPT_CALC_GEOMETRY computes ccg->volume_size/((1024L*1024L)/ccg->block_size) with block_size taken raw from the CCB and no zero guard. block_size == 0 divides by zero in the INNER division; block_size > 1 MiB (e.g. 2097152) makes the inner division yield 0 so the OUTER division is by zero.
  • The verbatim kernel-compiled harness (shared with DF-2427) drives the REAL amd_action() with a fabricated cam_sim + CCB.

Reachability (verified line-by-line, see VERDICT.md)

  • The cited userland route (pass(4) CAMIOCOMMAND) is REAL but gated: XPT_CALC_GEOMETRY (0x17) carries no XPT_FC_XPT_ONLY bit, so passsendccb accepts it (scsi_pass.c:474), and xpt_action() calls (*(sim->sim_action))(sim, start_ccb) directly (cam_xpt.c:3070). The XPT-side garbage filter (cam_xpt.c:3062) rejects only block_size == 0 || volume_size == 0 β€” so the block_size > 1 MiB variant passes the filter and reaches the driver. Gates: (a) an AMD53c974 HBA must be attached (PCI 1022:2020; 1990s hardware; not present in the QEMU guest and not emulated), (b) a pass(4) node on that bus, mode 0600 root:operator β†’ the caller needs root or operator group. No unprivileged-user route exists.

Build / run

Host-side (guest must be up and clean-source):

cd findings/poc/DF-2426
./build.sh          # sha256-gated verbatim TU -> nativekernel -> install -> reboot
./run.sh            # control(512)=OK, trigger(0)=panic, trigger(2MiB)=panic
./make_fixed.sh     # apply fix.diff (+ sibling DF-2427 fix) -> quickkernel -> reboot
./run_fixed.sh      # both triggers -> CAM_REQ_CMP_ERR, no panic

Expected on the verbatim kernel (baseline):

  • sysctl debug.df2426.control=1 β†’ CAM_REQ_CMP heads=64 secs=32 cylinders=100
  • sysctl debug.df2426.trigger0=1 β†’ Fatal trap 18: divide by zero fault in amd_action (amd.c:683), guest drops to DDB
  • sysctl debug.df2426.trigger_big=1 β†’ same trap via the XPT-filter-passing block_size=2 MiB form

Expected on the fixed kernel (cam_calc_geometry, same idiom as sys/dev/disk/trm/trm.c:994): both triggers return CAM_REQ_CMP_ERR, control unchanged, no trap.

Fix

Replace the open-coded division with the shared CAM helper cam_calc_geometry() (sys/bus/cam/cam.c:376), which rejects block_size == 0 and (1 MiB)/block_size == 0 with CAM_REQ_CMP_ERR β€” see fix.diff.

VERDICT.md
↓ download raw

DF-2426 VERDICT β€” Divide-by-zero at amd.c:683 in XPT_CALC_GEOMETRY

Bottom line

REPRODUCED (impact: panic, on the sink as filed; severity should be re-graded DOWN because the exact userland route claimed in the finding is filtered elsewhere β€” and UP in one variant the finding missed).

The unguarded division at sys/dev/disk/amd/amd.c:683

size_mb = ccg->volume_size/((1024L * 1024L)/ccg->block_size);

is real, divides by a CCB-controlled value with no zero check, and traps the kernel. Both zero-forms were mechanically verified on the real amd_action() through the kernel-compiled verbatim harness:

  • block_size == 0 β†’ inner division by zero β†’ Fatal trap 18: integer divide fault while in kernel mode, Stopped at amd_action+0x16d: idivq %rsi,%eax (panic0.txt).
  • block_size == 2097152 (2 MiB) β†’ inner division yields 0 β†’ the OUTER division is by zero β†’ Fatal trap 18 ... Stopped at amd_action+0x178: divq %rsi,%eax (panic_big.txt).

Control (block_size=512) computes 64/32/100 and returns CAM_REQ_CMP on the same path (no panic), proving the path is correct when the divisor is sane.

Reachability β€” the honest accounting

The finding claims block_size is "freely settable through pass CAMIOCOMMAND". Line-by-line:

  1. XPT_CALC_GEOMETRY = 0x17 carries no XPT_FC_XPT_ONLY bit (cam_ccb.h:164), so pass(4) accepts it (scsi_pass.c:474) and passsendccb β†’ xpt_merge_ccb β†’ xpt_action delivers the CCB.
  2. xpt_action() filters the exact case the finding describes: cam_xpt.c:3062-3068 ("Filter out garbage") answers block_size == 0 || volume_size == 0 itself with CAM_REQ_CMP/zeros and never calls sim->sim_action. Only nonzero-block-size CCBs reach (*(sim->sim_action))(sim, start_ccb) (cam_xpt.c:3070-3071).
  3. But the filter is incomplete in exactly the way amd.c needs: any block_size > 1048576 (e.g. 2 MiB) passes the filter with both fields nonzero, reaches amd_action, and the inner (1024L*1024L)/block_size evaluates to 0 β€” the outer division traps. This variant (trigger2) is a REAL userland-reachable panic on machines with the HBA, via the very route the finding cites. The in-kernel caller (scsi_da.c:2301) goes through the same filtered xpt_action.

Gates on a real machine: (a) an AMD53c974 HBA must be attached (PCI 1022:2020 β€” Tekram DC-390(T), late-1990s hardware; absent from the QEMU guest and from any modern system), (b) a pass(4) node for a target on that bus β€” /dev/passN is 0600 root:operator, so the caller needs root or operator. No unprivileged-user route exists, and no route at all without the HBA. This confines the finding to privileged-local DoS on rare hardware (the filed Medium is acceptable for the reachable 2 MiB form; the block_size==0 form as filed is XPT-filtered and would be Info/defense-in-depth on its own).

A/B fix validation

fix.diff replaces the open-coded math with the shared CAM helper cam_calc_geometry() (cam.c:376, the idiom the sibling Tekram driver trm.c:994 already uses), which rejects block_size == 0 and (1MiB)/block_size == 0 with CAM_REQ_CMP_ERR.

  • Baseline kernel (verbatim amd.c, sha256-gated): control passes; trigger0 β†’ Fatal trap 18 (idivq); trigger2 β†’ Fatal trap 18 (divq).
  • Fixed kernel (both DF-2426+DF-2427 fixes applied): control unchanged (CAM_REQ_CMP 64/32/100); trigger0 β†’ status=0x04 (CAM_REQ_CMP_ERR), heads/secs/cylinders 0, no trap; trigger2 β†’ same, no trap; guest stays up (run_fixed_dmesg.log).

Rig

Kernel-compiled verbatim interposition (same class as the scsi_cd / scsi_da rigs): df_pre.c + <byte-verbatim amd.c, sha256 c531c55efc80b8de2ee406d6e582f78abef78c7bc724ad0e91f04ec475cdd67a> + df_post.c compiled into X86_64_GENERIC as dev/disk/amd/dfamd_harness.c ("device amd" commented out in the guest's config so the verbatim TU is the driver's only instance). amd_action() runs unmodified on a fabricated cam_sim/union ccb; only xpt_done is interposed (records status). Triggers: sysctl debug.df2426.{control,trigger0,trigger_big}.

Artifacts

  • panic0.txt β€” serial console: control PASS + trigger0 Fatal trap 18, amd_action+0x16d idivq, DDB stop.
  • panic_big.txt β€” trigger2 Fatal trap 18, amd_action+0x178 divq (XPT-filter-passing form).
  • run_fixed_dmesg.log β€” fixed kernel: both triggers rejected with CAM_REQ_CMP_ERR, control unchanged, no trap.
  • run_control.log / run_trigger0.log / run_trigger_big.log β€” sysctl session logs; fix_build.log / fix_installkernel.log / gen_fixed.log β€” fixed build; build.log β€” kernel build; env.txt β€” guest state (INVARIANTS kernel, cc 8.3, no AMD53c974 present, /dev/pass0 exists but belongs to a non-amd SIM).

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
↓ fix.diffper-fix-DF-2426

Confirmed kernel references

Detail

Exploit chain

open /dev/passN on the amd bus (root or operator) -> CAMIOCOMMAND with XPT_CALC_GEOMETRY CCB, block_size=2097152, volume_size nonzero -> xpt_action filter (cam_xpt.c:3062) passes because both fields are nonzero -> ((sim->sim_action))(sim, ccb) (cam_xpt.c:3070) -> amd_action XPT_CALC_GEOMETRY -> (1024L1024L)/2097152 == 0 -> volume_size/0 -> Fatal trap 18, kernel panic (local DoS; no memory-corruption primitive).

Evidence (decisive lines)

panic0.txt lines 40-60: Fatal trap 18 integer divide fault, Stopped at amd_action+0x16d: idivq %rsi,%eax (block_size=0). panic_big.txt: Fatal trap 18, Stopped at amd_action+0x178: divq %rsi,%eax (block_size=2MiB, the XPT-filter-passing form). run.log: control block_size=512 -> CAM_REQ_CMP heads=64 secs=32 cylinders=100 on the same path. run_fixed_dmesg.log: fixed kernel rejects both with status=0x04 CAM_REQ_CMP_ERR, no trap, guest up.

PoC changes

The finding sketch had no runnable harness: built the established kernel-compiled verbatim-interposition rig for amd.c (first rig for this driver). amd.c compiled unmodified inside the TU against a fabricated cam_sim/ccb; only xpt_done interposed. Discovered during verification: (1) the stock kernel config enables device amd, so the guest config line is commented out to avoid duplicate globals (same as the DF-2269 cd rig); (2) the XPT core already filters block_size==0 (cam_xpt.c:3062), killing the as-filed route - added the block_size=2MiB trigger which passes the filter and is the actually reachable form; (3) harness constants iterated twice (format-string arg count; tinfo[-1] field offsets) before the clean 0-fail battery.

Verified recommended fix

Replace the open-coded division in amd_action XPT_CALC_GEOMETRY with cam_calc_geometry(&pccb->ccg, extended), which returns CAM_REQ_CMP_ERR for block_size==0 and for (1MiB)/block_size==0 (same idiom as trm.c).

Verdict

The divide-by-zero at amd.c:683 is real and mechanically proven on the verbatim driver through the real amd_action(): block_size=0 traps in the inner division (Fatal trap 18, Stopped at amd_action+0x16d: idivq) and block_size=2MiB traps in the outer division (Fatal trap 18, amd_action+0x178: divq). Reachability of the exact userland route claimed in the finding is only half true on this tree: xpt_action filters block_size==0 at cam_xpt.c:3062 before the SIM ever runs, so the as-filed block_size==0 form is unreachable from pass(4) CAMIOCOMMAND; however any block_size over 1MiB (e.g. 2097152) passes that filter, reaches amd_action, and panics - a real privileged-local DoS on machines with an AMD53c974 HBA (PCI 1022:2020) plus a pass(4) node (0600 root:operator). No unprivileged route and no route at all without the HBA; the block_size==0 form is defense-in-depth. Fixed kernel returns CAM_REQ_CMP_ERR for both forms with the control unchanged.