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

Divide-by-zero kernel panic from unvalidated boot sector bf_bps=0 during mount

Summary

NTFS boot sector bf_bps field copied from untrusted disk at :331 never validated. Only check before use is 8-byte magic string at :343. When bf_mftrecsz encodes fractional MFT record size (signed<=0 standard NTFS encoding for records <1 cluster) line :354 divides by ntm_bps. Crafted image with bf_bps=0 causes immediate CPU divide-by-zero trap panicking kernel. ntm_bps is u_int16_t from boot sector offset 11 zero validation anywhere in sys/vfs/ntfs/. Mount requires root or vfs.usermount=1. Auto-mounting infrastructure (automountd removable media VM shared folders) mount arbitrary block devices as root crafted USB sufficient.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2554 Β· 14 files
FileTypeDescriptionSize
gen_image.py trigger-source writes 1024-byte NTFS boot sector: bf_sysid='NTFS ', bf_bps=0, bf_spc=1, bf_mftrecsz=0xF6(-10) 2.2 KB view raw
evil.ntfs artifact generated crafted image 1.0 KB ↓ download
poc.c trigger-source stub documenting that the trigger is an on-disk image (no userspace program) 495 B view raw
run.sh run-script kldload ntfs; vnconfig /dev/vn0; mount_ntfs -o ro /dev/vn0 /mnt -> #DE panic 566 B view raw
build.sh build-script python3 gen_image.py evil.ntfs 141 B view raw
build.log build-log image gen output 118 B view raw
run.log run-log unpatched: mount -> panic (ssh died) 381 B view raw
panic.txt panic-signature Fatal trap 18 integer divide fault; Stopped at ntfs_mountfs.isra.0+0x5ca idivl 155 B view raw
env.txt environment uname, cc, vfs.usermount=0 205 B view raw
fix.diff suggested-fix validate ntm_bps/ntm_spc != 0 after magic check, return EINVAL 649 B view raw
fix_build.log build-log ntfs.ko rebuild with fix, rc=0 (-Werror clean) 16.7 KB view raw
fix_run.log run-log patched ntfs.ko: 3x EINVAL, no panic, guest up 405 B view raw
fix_kernel.txt environment patched kern.version + ntfs.ko info 110 B view raw
VERDICT.md verdict full analysis 4.8 KB ↓ raw
VERDICT.md verdict full analysis
↓ download raw

DF-2554 β€” NTFS divide-by-zero on unvalidated boot sector (bf_bps=0) β€” VERDICT

Verdict: REPRODUCED (deterministic #DE panic at mount); fix VALIDATED

  • status: reproduced
  • reproduced: 1
  • impact: panic (local root-mount DoS; auto-mount threat model)
  • confidence: certain
  • fix_status: fixed

The bug (confirmed in source, line-by-line)

sys/vfs/ntfs/ntfs_vfsops.c ntfs_mountfs() reads the boot sector with bread(devvp, BBLOCK, BBSIZE, &bp) at :327 and bcopy()'s it into ntmp->ntm_bootfile at :331. struct bootfile (sys/vfs/ntfs/ntfs.h:223, #pragma pack(1)) lays out the on-disk fields directly, so attacker-controlled boot-sector bytes become kernel fields with no normalization. The only validation is the 8-byte magic check at :343 (strncmp(bf_sysid, "NTFS ", 8)).

ntm_bps (= bf_bps, u_int16_t at boot-sector offset 11) is then used as a divisor at :354:

349:    int8_t cpr = ntmp->ntm_mftrecsz;
350:    if (cpr > 0)
351:        ntmp->ntm_bpmftrec = ntmp->ntm_spc * cpr;
352:    else
354:        ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;  /* #DE if bps==0 */

A crafted image with bf_bps == 0 and bf_mftrecsz <= 0 (signed; e.g. 0xF6 = -10, the documented fractional-MFT-record encoding) reaches the else branch and executes a idivl by zero β†’ CPU integer divide fault (#DE) β†’ kernel panic. ntm_bps/ntm_spc are also divisors in the conversion macros ntfs_btocn/ntfs_cntob (ntfs.h:289-291), so a zero geometry field is fatal in multiple places; :354 is simply the first hit during mount.

Reproduction (unpatched #0 baseline)

gen_image.py writes a 1024-byte (BBSIZE) image with: - offset 3 bf_sysid = "NTFS " (passes the magic check) - offset 11 bf_bps = 0 (the unvalidated divisor) - offset 13 bf_spc = 1 - offset 64 bf_mftrecsz = 0xF6 (-10 signed β†’ else branch)

Trigger (root; vfs.usermount=0 so mount is privileged):

kldload ntfs
vnconfig /dev/vn0 evil.ntfs
mount_ntfs -o ro /dev/vn0 /mnt

Result on the unpatched 6.5-DEVELOPMENT #0 kernel (boot.log):

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
CPU0 stopping CPUs: 0x0000003e
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>

The faulting instruction is the idivl generated from line 354's / ntmp->ntm_bps. Guest dead (ssh gone), exactly as claimed.

Threat model / reachability

mount_ntfs requires root (or vfs.usermount=1, which is 0 on the guest). This is a root/local-mount DoS: an attacker who controls a filesystem image that an administrator (or an auto-mounter) mounts can panic the kernel instantly β€” e.g. a malicious USB mass-storage device, an install image, or any flow that mounts attacker-supplied media. It is a DoS, not a memory-corruption primitive (it is a deterministic #DE, no attacker-controlled write), so there is no escalation chain.

Fix

fix.diff adds geometry validation immediately after the magic check and before any division:

if (ntmp->ntm_bps == 0 || ntmp->ntm_spc == 0) {
    error = EINVAL;
    dprintf(("ntfs_mountfs: invalid geometry (bps/spc)\n"));
    goto out;
}

This closes both the line-354 divisor and the ntfs_btocn/cntob denominator (ntfs.h:289-291). The mount fails with EINVAL instead of dividing by zero.

Fix validation

NTFS is a loadable module (optional ntfs in sys/conf/files; not in X86_64_GENERIC; built as /boot/kernel/ntfs.ko), so the fix was validated by rebuilding just ntfs.ko (faster and equivalent to a full kernel rebuild for this file), reinstalling it, and re-running the same PoC:

  • Apply: patch -p1 < fix.diff β†’ Hunk #1 succeeded at 346.
  • Build: cd sys/vfs/ntfs && make β†’ rc=0 (-Werror clean; see fix_build.log).
  • Install: cp ntfs.ko /boot/kernel/ntfs.ko.
  • Re-run (3Γ—, deterministic): mount_ntfs: /dev/vn0: Invalid argument mount returned 71 (no panic) ; guest up

Before/after contrast: | kernel | same PoC result | |---------------------|------------------------------------------------------------| | unpatched #0 | Fatal trap 18: integer divide fault at ntfs_mountfs+0x5ca, guest dead | | patched ntfs.ko | EINVAL: Invalid argument, no panic, guest up |

The fix closes the bug.

PoC changes / artifacts

  • gen_image.py β€” host-side image forger (matches DF-2555 pattern); writes the minimal 1024-byte boot sector with bf_bps=0, bf_mftrecsz=0xF6.
  • evil.ntfs β€” the generated image.
  • run.sh β€” kldload ntfs + vnconfig + mount_ntfs trigger.
  • poc.c β€” stub documenting that the trigger is an on-disk image, not a program.
  • build.sh, build.log, run.log, fix_build.log, fix_run.log, panic.txt, env.txt, fix_kernel.txt, fix.diff, manifest.json.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: same PoC (mount_ntfs of bf_bps=0 image) PANICS on unpatched #0 baseline ('Fatal trap 18: integer divide fault ... ntfs_mountfs.isra.0+0x5ca: idivl', guest dead) and does NOT panic on single-fix ntfs.ko ('mount_ntfs: /dev/vn0: Invalid argument', EINVAL, guest up β€” deterministic over 3 runs). Geometry guard returns EINVAL before divisor reached.

baseline: 'Fatal trap 18: integer divide fault while in kernel mode' / 'Stopped at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax' (guest DOWN). patched ntfs.ko run1/2/3: 'mount_ntfs: /dev/vn0: Invalid argument' / 'mount returned 71 (no panic)' (guest UP). fix_build: 'Hunk #1 succeeded at 346' / '=== NTFS_BUILD_DONE rc=0 ===' (-Werror clean).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 (base kernel unchanged; ntfs is module-only, so only /boot/kernel/ntfs.ko rebuilt+reinstalled with fix)

Confirmed kernel references

Detail

Exploit chain

none β€” deterministic integer divide-by-zero (#DE), not memory-corruption primitive. No attacker-controlled write, no escalation chain. Impact is local root/auto-mount DoS panic (mount_ntfs privileged; vfs.usermount=0 on guest; threat model administrator or auto-mounter mounting attacker-supplied image, e.g. malicious USB media).

Evidence (decisive lines)

[*] mounting crafted NTFS image (evil.ntfs) on /dev/vn0 ... (ssh channel died -- kernel panicked) === panic signature (boot.log) === Fatal trap 18: integer divide fault while in kernel mode / cpuid = 0; lapic id = 0 / CPU0 stopping CPUs: 0x0000003e / Stopped at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax / db>

PoC changes

Wrote gen_image.py (host-side image forger: 1024-byte boot sector with bf_sysid='NTFS ', bf_bps=0, bf_spc=1, bf_mftrecsz=0xF6), evil.ntfs (generated image), run.sh (kldload ntfs + vnconfig /dev/vn0 + mount_ntfs trigger), poc.c stub, build.sh, VERDICT.md, fix.diff (validate ntm_bps/ntm_spc != 0 after magic check, return EINVAL), manifest.json. ntfs is module-only on this guest (optional ntfs in sys/conf/files; not in X86_64_GENERIC; built as /boot/kernel/ntfs.ko), so fix validated by rebuilding just ntfs.ko.

Verified recommended fix

In sys/vfs/ntfs/ntfs_vfsops.c ntfs_mountfs, immediately after bf_sysid magic check at :343-347 and before division at :354, add: if (ntmp->ntm_bps == 0 || ntmp->ntm_spc == 0) { error = EINVAL; dprintf(("ntfs_mountfs: invalid geometry (bps/spc)\n")); goto out; }. Closes both line-354 divisor and ntfs_btocn/cntob denominators (ntfs.h:289-291). Supersedes finding proposal (adds spc too). Full git-apply-able diff in findings/poc/DF-2554/fix.diff.

Verdict

REPRODUCED. ntfs_mountfs reads the boot sector via bread at sys/vfs/ntfs/ntfs_vfsops.c:327, bcopy's it into ntm_bootfile at :331, and the ONLY validation is 8-byte 'NTFS ' magic at :343. ntm_bps (= bf_bps, u_int16 at boot-sector offset 11) is never validated and is used as a divisor at :354: ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;. A crafted 1024-byte image with bf_bps=0 and bf_mftrecsz=0xF6 (-10 signed -> else branch) reaches the idivl and triggers CPU #DE. Confirmed on unpatched 6.5-DEVELOPMENT #0: 'Fatal trap 18: integer divide fault while in kernel mode ... Stopped at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax'.