sastrategy integer divide-by-zero panic from device-driven min_blk==0 with non-power-of-2 fixed blocksize
Summary
sastrategy's fixed-mode validation does bcount % min_blk whenever blk_mask==~0, but min_blk comes verbatim from the device's READ BLOCK LIMITS minimum (or is zeroed on RBL failure) with no clamp: MTSETBSIZ with a non-pow2 size sets FIXED+blk_mask=~0 while min_blk stays 0 - the next read(2) is a kernel-mode div-by-zero. REPRODUCED twice from clean state (Fatal trap 18 at sastrategy+0x289 divl %ecx,%eax, stock + no-INVARIANTS kernels); clamp+guard fix kernel-validated (read returns 512, guest stays up). Pure #DE DoS, no corruption primitive.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5285 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| Makefile | β | 52 B | β download | |
| README.md | β | 1.9 KB | β raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| build.sh | β | 136 B | view raw | |
| env.txt | β | 352 B | view raw | |
| evilsa.c | β | 15.2 KB | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| fix_build.log | β | 5.1 MB | β download | |
| panic.noinv.txt | β | 552 B | view raw | |
| panic.txt | β | 825 B | view raw | |
| poc_setblk.c | β | 1.3 KB | view raw | |
| run.log | β | 317 B | view raw | |
| run.patched.log | β | 214 B | view raw | |
| run.sh | β | 135 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-5285 β sastrategy divide-by-zero panic via device-driven min_blk == 0
sys/bus/cam/scsi/scsi_sa.c β CAM SCSI sequential-access (tape) driver.
Build (inside the guest, as root)
cd /root/df5285
# evilsa.ko: fake CAM SIM presenting a malicious T_SEQUENTIAL target
# (see evilsa.c header for the full choreography)
touch opt_cam.h && make
cc -O -o poc_setblk poc_setblk.c
Run
sh run.sh # kldload evilsa, mount tape (RBL minimum=0), then:
# poc_setblk /dev/nsa0:
# open() -> samount() completes in VARIABLE mode, min_blk = 0
# MTSETBSIZ 3 -> non-pow2 fixed: blk_mask = ~0, min_blk still 0
# read(512) -> sastrategy: `b_bcount % min_blk` -> #DE -> panic
Expected result (baseline, unmodified kernel)
Serial console:
Fatal trap 18: integer divide fault while in kernel mode instruction pointer = 0x8:0xffffffff802bf319 Stopped at sastrategy+0x289: divl %ecx,%eax
Full kernel panic from the SCSI bus stopping. See panic.txt, run.log.
Why it works
samount()setssoftc->min_blkstraight from the device's READ BLOCK LIMITS response (scsi_2btoul(rblim->minimum), scsi_sa.c:2019) or to 0 when RBL fails (scsi_sa.c:2005) β no lower clamp of 1.MTSETBSIZwith a non-power-of-2 count sets FIXED mode withblk_mask = ~0and leavesmin_blkuntouched (scsi_sa.c:1198-1242).sastrategy()then evaluates(bp->b_bcount % softc->min_blk)(scsi_sa.c:756) wheneverblk_mask == ~0β integer division by zero, kernel mode, instant panic.
An evil/compromised tape device (or evil SIM, as here) controls the RBL
minimum; the setblk+read trigger is ordinary operator/api usage on any
system where the device reports minimum 0 (common for RBL-less or quirky
drives: the code explicitly expects min_blk = 0 there).
Fix
fix.diff: clamp min_blk to >= 1 in samount() after RBL parsing and
guard the modulo in sastrategy().
DF-5285 VERDICT β reproduced (kernel panic / local DoS) and fix validated
What was claimed
sastrategy() divides by softc->min_blk; a device-driven
min_blk == 0 combined with fixed mode and a non-power-of-2 blocksize
(blk_mask == ~0) turns the next read(2)/write(2) into an integer
divide-by-zero in kernel mode.
Reproduced? Yes β twice, decisively.
- Stock INVARIANTS kernel (fresh
vm.sh reset with-src):panic.txtβFatal trap 18: integer divide fault while in kernel mode ... Stopped at sastrategy+0x289: divl %ecx,%eax, all CPUs stopped, system in DDB. - Same-source no-INVARIANTS kernel:
panic.noinv.txtβ identical trap,sastrategy+0x279: divl %ecx,%eax,current process 868(poc_setblk).
Trigger chain (evilsa KLD + 30-line userland program)
- evilsa reports READ BLOCK LIMITS
{gran=0, max=0xFFFFFF, min=0}β exactly the response the driver expects from RBL-less drives (the failure path atscsi_sa.c:2003-2005also setsmin_blk = 0explicitly). open("/dev/nsa0")mounts in variable mode:min_blk = 0survives every step (scsi_sa.c:2019, no lower clamp anywhere).MTSETBSIZ 3(the ordinary "set block size to 3" that an operator issues when a drive reports odd blocking):sasetparams()mode select succeeds, thenscsi_sa.c:1208-1217sets FIXED +blk_mask = ~0(3 is not a power of two) βmin_blkuntouched.read(fd, buf, 512)βsastrategy()evaluates(bp->b_bcount % softc->min_blk)atscsi_sa.c:756β #DE β panic.
The device fully controls the enabling state (RBL minimum); the triggering I/O is normal API use. Opening the sa node requires SYSCAP_RESTRICTEDROOT, so the local actor is privileged/operator β hence Medium (DoS), not High.
Why this is not memory corruption
The fault is a pure divide error (#DE, trap 18) β no overwrite, no controlled redirect. Impact ceiling: reliable kernel panic of any system with the tape open (all CPUs stopped).
Fix validation
fix.diff:
* clamps min_blk to >= 1 in samount() after RBL parsing (covers
both the parsed value and the RBL-failure path),
* guards the modulo in sastrategy() (min_blk > 0),
* (same diff) M_ZERO for the 8192-byte RBL scratch (DF-5286) and for
ccomp (DF-5284).
Same-source NOINV kernel rebuilt incrementally (fix_build.log),
installed, rebooted; identical PoC re-run:
MTSETBSIZ 3 accepted: FIXED mode, blk_mask=~0, min_blk still 0 issuing read(512) -> sastrategy: 512 % min_blk(0) ... NO PANIC: read returned 512
Guest stayed up (run.patched.log). Baseline on the identical
unpatched kernel panicked (panic.noinv.txt).
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
fixedRebuilt same-source X86_64_NOINV kernel with fix.diff (incremental nativekernel, log fix_build.log); exact same PoC re-run: MTSETBSIZ 3 accepted, read(512) returns normally, no trap, guest stays up (run.patched.log). Baseline on the identical unpatched kernel panicked.
run.patched.log; fix_build.log; diff hunks at scsi_sa.c:752-756 (guard), :2021-2029 (clamp), :1961 (rblim M_ZERO), :2765 (ccomp M_ZERO)
Confirmed kernel references
Detail
Exploit chain
evil/compromised tape device (evil SIM KLD) -> READ BLOCK LIMITS {minimum=0} -> open(/dev/nsa0) mounts variable, min_blk=0 -> MTSETBSIZ 3 (non-pow2): FIXED + blk_mask=~0, min_blk untouched -> read(512) -> sastrategy: 512 % 0 -> fatal trap 18 -> kernel panic (local DoS; no memory-corruption primitive, division fault only)
Evidence (decisive lines)
panic.txt (Fatal trap 18 ... Stopped at sastrategy+0x289: divl %ecx,%eax, stock INVARIANTS kernel); panic.noinv.txt + run.noinv.log (same on no-INVARIANTS kernel, sastrategy+0x279); run.log (poc output); evilsa.c (READ BLOCK LIMITS minimum=0 choreography); run.patched.log (post-fix: read returns, no panic)
PoC changes
seed sketch replaced by evilsa KLD (fake T_SEQUENTIAL CAM SIM adapted from the DF-5271 evilcd rig) + a 30-line userland trigger (open, MTIOCTOP MTSETBSIZ 3, read). Mode-select policy accepts blocksize selects (density 0x7f) and rejects the 15 samount() density probes so the mount settles in variable mode with min_blk=0.
Verified recommended fix
Clamp softc->min_blk to >= 1 after READ BLOCK LIMITS parsing in samount() and guard the modulo in sastrategy() (see fix.diff).
Verdict
Kernel divide-by-zero panic (fatal trap 18, divl in sastrategy+0x279/+0x289) reproduced twice from a clean vm.sh reset and on the no-INVARIANTS test kernel: a malicious/quirky sequential-access device reports READ BLOCK LIMITS minimum=0 (or omits RBL, code path sets min_blk=0), the ordinary MTSETBSIZ ioctl for a non-power-of-2 size flips the driver into fixed mode with blk_mask=~0 while min_blk stays 0, and the very next read(2)/write(2) executes b_bcount % min_blk in sastrategy() scsi_sa.c:756 -> integer divide fault -> all CPUs stopped, system dead. Fix validated: with fix.diff (clamp min_blk>=1 in samount + guard the modulo) the same PoC completes the read normally with no panic.
No comments yet.