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)
PoC verification
Evidence pack
findings/poc/DF-2426 Β· 32 filesDF-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()caseXPT_CALC_GEOMETRYcomputesccg->volume_size/((1024L*1024L)/ccg->block_size)withblock_sizetaken raw from the CCB and no zero guard.block_size == 0divides 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 fabricatedcam_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 noXPT_FC_XPT_ONLYbit, sopasssendccbaccepts it (scsi_pass.c:474), andxpt_action()calls(*(sim->sim_action))(sim, start_ccb)directly (cam_xpt.c:3070). The XPT-side garbage filter (cam_xpt.c:3062) rejects onlyblock_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) apass(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=100sysctl debug.df2426.trigger0=1β Fatal trap 18: divide by zero fault inamd_action(amd.c:683), guest drops to DDBsysctl 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.
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:
XPT_CALC_GEOMETRY = 0x17carries noXPT_FC_XPT_ONLYbit (cam_ccb.h:164), so pass(4) accepts it (scsi_pass.c:474) andpasssendccbβxpt_merge_ccbβxpt_actiondelivers the CCB.xpt_action()filters the exact case the finding describes: cam_xpt.c:3062-3068 ("Filter out garbage") answersblock_size == 0 || volume_size == 0itself with CAM_REQ_CMP/zeros and never callssim->sim_action. Only nonzero-block-size CCBs reach(*(sim->sim_action))(sim, start_ccb)(cam_xpt.c:3070-3071).- 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_sizeevaluates 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_testableConfirmed 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.
No comments yet.