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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5285 Β· 15 files
FileTypeDescriptionSize
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() sets softc->min_blk straight 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.
  • MTSETBSIZ with a non-power-of-2 count sets FIXED mode with blk_mask = ~0 and leaves min_blk untouched (scsi_sa.c:1198-1242).
  • sastrategy() then evaluates (bp->b_bcount % softc->min_blk) (scsi_sa.c:756) whenever blk_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().

VERDICT.md
↓ download raw

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)

  1. evilsa reports READ BLOCK LIMITS {gran=0, max=0xFFFFFF, min=0} β€” exactly the response the driver expects from RBL-less drives (the failure path at scsi_sa.c:2003-2005 also sets min_blk = 0 explicitly).
  2. open("/dev/nsa0") mounts in variable mode: min_blk = 0 survives every step (scsi_sa.c:2019, no lower clamp anywhere).
  3. MTSETBSIZ 3 (the ordinary "set block size to 3" that an operator issues when a drive reports odd blocking): sasetparams() mode select succeeds, then scsi_sa.c:1208-1217 sets FIXED + blk_mask = ~0 (3 is not a power of two) β€” min_blk untouched.
  4. read(fd, buf, 512) β†’ sastrategy() evaluates (bp->b_bcount % softc->min_blk) at scsi_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt 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)
↓ fix.diffper-fix-DF-5285

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.