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

Divide-by-zero and undefined-shift in BPB bpmftrec computation from crafted boot sector

Summary

ntfs_vfsops.c:343 only validates strncmp(NTFS). :349-354 int8_t cpr=ntm_mftrecsz. If cpr<=0: bpmftrec=(1<<(-cpr))/ntm_bps. bf_bps=0 => divide-by-zero panic at mount. bf_mftrecsz=0x80 => 1<<128 UB. bf_spc=0 with cpr>0 => bpmftrec=0. statfs:621 f_ffree=f_bfree/ntm_bpmftrec => divide-by-zero panic on first VFS_STATFS. Mount-time crafted image DoS. Fix: validate bps!=0 power-of-2>=512 spc!=0 mftrecsz shift < 32.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0872 Β· 20 files
FileTypeDescriptionSize
craft_ntfs.c trigger-source minimal trigger: crafts NTFS image with bf_bps=0, bf_mftrecsz=0xF6 -> div#0 at ntfs_vfsops.c:354 3.0 KB view raw
craft_variants.c trigger-source emits 4 images: bps=0, spc=0, mftrecsz=0x80 (UB shift), and a control 2.2 KB view raw
build.sh build-script cc -O2 -o craft_ntfs craft_ntfs.c 136 B view raw
run.sh run-script vnconfig + mount_ntfs crafted image (expects #DE panic on unpatched) 1018 B view raw
fix.diff suggested-fix validate bf_bps/bf_spc!=0 and shift count in [0,31] before division/shift at ntfs_vfsops.c:349-355; reject bpmftrec==0 1.6 KB view raw
VERDICT.md verdict full narrative: mechanism, reproduction, fix validation, path:line citations 8.9 KB ↓ raw
README.md readme how to reproduce + how it works 3.1 KB ↓ raw
panic.txt panic-signature Fatal trap 18 integer divide fault at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax 594 B view raw
build.log build-log trigger crafter build (cc -O2) 13 B view raw
run.log run-log first reproduction run (panic, guest down) 311 B view raw
run.2.log run-log second reproduction run (same panic, deterministic) 324 B view raw
fix_build.log build-log full single-fix nativekernel build output (rc=0, 35622 lines) 5.6 MB ↓ download
fix_run.log run-log patched-kernel PoC re-run: EINVAL exit 71, guest up 435 B view raw
fix_run.2.log run-log second patched-kernel run: same EINVAL, deterministic 422 B view raw
fix_variants.log run-log all 4 crafted variants rejected by fix (bps=0, spc=0, mft=0x80, control) 304 B view raw
dmesg.txt dmesg kldstat showing patched ntfs.ko loaded; no panic 291 B view raw
env.txt environment uname, cc version, sysctls 672 B view raw
manifest.json manifest this file 3.8 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to reproduce + how it works
↓ download raw

DF-0872 β€” NTFS BPB bpmftrec divide-by-zero / undefined-shift

Finding: Divide-by-zero and undefined-shift in BPB bpmftrec computation from crafted boot sector Severity: Medium (mount-time kernel DoS via crafted filesystem image) CWE: CWE-369 (Integer Divide-by-Zero) CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H

Reproduce

./build.sh &&
# as root on a DragonFlyBSD guest with vnconfig + mount_ntfs:
./run.sh

Expected on the unpatched 6.5-DEVELOPMENT #0 kernel: the guest kernel-panics with Fatal trap 18: integer divide fault ... Stopped at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax and drops to the db> DDB prompt. The ssh session dies; the guest must be reset.

Expected on the patched kernel (fixed ntfs.ko): mount_ntfs prints mount_ntfs: /dev/vn1: Invalid argument and exits 71 (EINVAL); the guest stays up.

How it works

ntfs_mountfs() reads the NTFS boot sector (BPB) and computes the bytes-per-MFT-record divisor at sys/vfs/ntfs/ntfs_vfsops.c:349-355:

int8_t cpr = ntmp->ntm_mftrecsz;          /* u8 from BPB offset 0x40 */
if( cpr > 0 )
    ntmp->ntm_bpmftrec = ntmp->ntm_spc * cpr;
else
    ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;   /* bps==0 => #DE */

bf_bps (bytes per sector) and bf_spc (sectors per cluster) are taken directly from attacker-controlled disk content with no validation β€” only the 8-byte OEM id ("NTFS ") is checked (:343). A crafted image with bf_bps = 0 and bf_mftrecsz <= 0 (signed) drives the (1 << (-cpr)) / ntm_bps division by zero β†’ CPU #DE β†’ non-resumable kernel trap β†’ panic.

Two related sub-bugs: - bf_mftrecsz == 0x80 (cpr = INT8_MIN = -128) β†’ 1 << 128, undefined shift. - bf_spc == 0 with cpr > 0 β†’ bpmftrec = 0, which later divides by zero in ntfs_statfs() at :620 / ntfs_statvfs() at :646.

Files

file purpose
craft_ntfs.c minimal trigger β€” crafts bf_bps=0, bf_mftrecsz=0xF6 image
craft_variants.c emits 4 images exercising all three sub-bugs + a control
build.sh cc -O2 -o craft_ntfs craft_ntfs.c
run.sh vnconfig + mount_ntfs the crafted image (expects panic on unpatched)
fix.diff git-apply-able fix β€” validates BPB divisors/shift before use
VERDICT.md full narrative with path:line citations
panic.txt the Fatal trap 18 ... ntfs_mountfs+0x5ca serial-console excerpt
build.log / run.log / run.2.log full untrimmed logs
fix_build.log full single-fix kernel build output (rc=0)
fix_run.log / fix_run.2.log patched-kernel PoC re-run (EINVAL, no panic)
fix_variants.log all 4 variants rejected by the fix
env.txt guest uname, cc, sysctls
manifest.json machine-readable catalog

Threat model

A local user who can cause a crafted NTFS image to be mounted instantly kernel-panics the machine. With vfs.usermount=1 and a root-created vnode disk owned by the attacker, the trigger is fully unprivileged. The standard model for this class (admin mounts an untrusted filesystem image) also applies. Impact is pure DoS β€” no memory corruption, no escalation.

VERDICT.md verdict full narrative: mechanism, reproduction, fix validation, path:line citations
↓ download raw

DF-0872 β€” Divide-by-zero and undefined-shift in BPB bpmftrec computation

Verdict: REPRODUCED (panic / kernel DoS) β†’ FIX VALIDATED

Bug class: integer divide-by-zero (#DE kernel trap) + undefined shift, both from attacker-controlled on-disk BPB fields parsed at NTFS mount time.

Impact: panic (deterministic, non-resumable kernel trap). This is a pure DoS β€” the #DE fires before any memory write, so there is no memory-corruption primitive and no escalation chain. No SMAP/SMEP/KASLR-bypass chain is relevant here.

Severity rationale: local attacker who can cause a crafted NTFS image to be mounted (or an admin who mounts an untrusted image) instantly kernel-panics the machine. Mount-time DoS from a filesystem image is the standard threat model for FS-parsing bugs.


Mechanism (trigger β†’ primitive β†’ effect)

Trigger: mount a crafted NTFS image via mount_ntfs -o ro <dev> <mnt>. The first sector (boot sector / BPB) is read by ntfs_mountfs() at sys/vfs/ntfs/ntfs_vfsops.c:327 (bread(devvp, BBLOCK, BBSIZE, &bp)) and bcopy'd into ntmp->ntm_bootfile at :331.

Guard that passes: the only validation before the vulnerable computation is a strncmp(ntm_bootfile.bf_sysid, "NTFS ", 8) at :343. A crafted image with the correct 8-byte OEM id passes this trivially.

Sink (the bug): sys/vfs/ntfs/ntfs_vfsops.c:349-355:

{
    int8_t cpr = ntmp->ntm_mftrecsz;          /* u8 at BPB offset 0x40 */
    if( cpr > 0 )
        ntmp->ntm_bpmftrec = ntmp->ntm_spc * cpr;
    else
        ntmp->ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps;   /* <-- #DE */
}

Three distinct sub-bugs the finding cites, all confirmed:

  1. Divide-by-zero at mount (:354) β€” ntm_bps (bytes-per-sector, u16 at BPB offset 0x0B) is never validated. With bf_bps == 0 and any bf_mftrecsz with signed value <= 0 (e.g. 0xF6 β†’ cpr = -10, the canonical "1<<10 bytes per MFT record" value), the else branch executes (1 << 10) / 0 β†’ CPU raises #DE β†’ Fatal trap 18 β†’ kernel panic. This is the primary trigger the PoC exercises.

  2. Undefined shift (:354) β€” if bf_mftrecsz == 0x80 (int8_t cpr = -128 = INT8_MIN), then -cpr promotes to int 128 and 1 << 128 is undefined behaviour (shift count >= type width). gcc 8.3 emits an idivl/shll sequence whose result is unpredictable; on this guest the shll $0x80,%eax yields 0 (masked to 5 bits β†’ shift by 0 β†’ 1), which then divides by bps β€” if bps != 0 this does not #DE but produces a garbage bpmftrec that later corrupts statfs arithmetic.

  3. Divide-by-zero at statfs (:620 / :646) β€” if bf_spc == 0 and bf_mftrecsz > 0 (e.g. 0x01), then bpmftrec = spc * cpr = 0 * 1 = 0. Mount succeeds past :354, but the very next VFS_STATFS at :263 (called immediately after ntfs_mountfs returns) reaches sbp->f_ffree = sbp->f_bfree / ntmp->ntm_bpmftrec at :620 β†’ divide by zero β†’ panic. (Variant v2 in craft_variants.c exercises this path.)

Effect: Fatal trap 18: integer divide fault while in kernel mode β€” the CPU raises a non-resumable trap, the kernel drops into DDB (db> prompt), the system is dead. Confirmed twice deterministically on the unpatched #0 baseline kernel.


Reproduction (unpatched baseline)

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

[*] crafting image
[+] crafted ntfs_crafted.img  (bf_bps=0, bf_mftrecsz=0xF6 -> div#0 at ntfs_vfsops.c:354)
[*] attaching ntfs_crafted.img to /dev/vn1
[*] mount_ntfs -o ro /dev/vn1 /mnt_df0872

Panic signature (from serial console dfbsd-qemu/boot.log, full excerpt in panic.txt):

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 2; lapic id = 2
instruction pointer  = 0x8:0xffffffff82600b1a
kernel: type 18 trap, code=0
CPU2 stopping CPUs: 0x0000003b
 stopped
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>

The idivl %ecx,%eax at ntfs_mountfs+0x5ca is the compiled form of the / ntmp->ntm_bps at :354. Reproduced twice (run.log + run.2.log) β€” fully deterministic.


Exploit chain

None. This is a pure integer-divide-by-zero (CWE-369). The #DE trap fires before any memory write occurs β€” there is no write primitive, no heap/stack corruption, no UAF, no type confusion. The only effect is a non-resumable kernel trap (DoS). No escalation chain exists or is relevant. The realistic impact ceiling is: a local user who can get a crafted NTFS image mounted instantly panics the kernel (mount-time DoS).

Reachability note: vfs.usermount defaults to 0 on this guest, so a non-root user cannot directly mount_ntfs. The realistic threat model is an administrator mounting an untrusted filesystem image (USB stick, downloaded VM image, NFS export) β€” the standard model for filesystem-parsing bugs. With vfs.usermount=1 and a root-created vnode disk owned by the attacker, the chain is fully unprivileged (the attacker crafts the image content, not the mount syscall β€” the mount itself is the trigger).


PoC changes

The finding had no pre-existing PoC folder (the evidence pack was authored from scratch during this verification). Files written:

  • craft_ntfs.c β€” minimal trigger: crafts a 1 MB NTFS image with bf_bps=0 and bf_mftrecsz=0xF6 (the canonical -10 value that forces the else branch into (1<<10)/0).
  • craft_variants.c β€” emits 4 images exercising all three sub-bugs (bps=0, spc=0, mftrecsz=0x80) plus a structurally-valid control.
  • build.sh / run.sh β€” exact build and run invocations (vnconfig + mount_ntfs).
  • fix.diff β€” the validated fix.

Fix (validated)

File: findings/poc/DF-0872/fix.diff (git-apply-able against sys/vfs/ntfs/ntfs_vfsops.c).

Root-cause fix: add validation of the BPB-derived divisor/shift fields immediately after the bf_sysid strncmp check at :343 and before the vulnerable computation at :349-355. Concretely:

  1. Reject ntm_bps == 0 || ntm_spc == 0 with EINVAL β€” closes the mount-time #DE at :354 and the statfs #DE at :620/:646 (bpmftrec can no longer be 0 via spc * cpr).
  2. Bound the shift count: compute int shift = -cpr and reject shift < 0 || shift >= 32 with EINVAL β€” closes the 1 << 128 undefined-shift case (bf_mftrecsz == 0x80).
  3. Final guard: reject ntm_bpmftrec == 0 with EINVAL β€” defense-in-depth for the statfs divisor (covers any future path that could zero bpmftrec).

Why this is correct: every divisor used downstream (ntm_bps at :354, ntm_bpmftrec at :620/:621/:646/:647) is now guaranteed non-zero before the first division, and the shift count is guaranteed in [0, 31]. The fix is minimal (one validation block, no control-flow rewrite) and uses the existing goto out error path that already frees ntmp and releases devvp.

Module vs kernel note: on DragonFlyBSD X86_64_GENERIC, NTFS is not compiled into the static kernel β€” it is a loadable module (ntfs.ko, auto-loaded on first mount_ntfs). The fix therefore lives entirely in the module. The single-fix build rebuilds ntfs.ko; the main kernel binary is unchanged.

Fix validation (Phase 8)

Before (unpatched #0 baseline, ntfs.ko sha256 = original):

mount_ntfs -o ro /dev/vn1 /mnt_df0872
Fatal trap 18: integer divide fault while in kernel mode
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>                                  <-- guest DEAD

After (patched ntfs.ko module, sha256 = 1906300f73d2c3b82e69e9ea628a028616f713c4b28afa72993ca90e1211be2f):

mount_ntfs -o ro /dev/vn1 /mnt_df0872
mount_ntfs: /dev/vn1: Invalid argument
MOUNT_EXIT=71                        <-- EINVAL, guest stays UP

All four crafted variants (bps=0, spc=0, mftrecsz=0x80, and a structurally-valid control) are rejected with EINVAL and no panic β€” the guest remains responsive. Fix is deterministic across two runs.

Build: make -j6 nativekernel KERNCONF=X86_64_GENERIC from /usr/src (rc=0, full log in fix_build.log). The standalone module build (cd sys/vfs/ntfs && make) also produces the patched ntfs.ko in seconds.


Kernel references (verified during this run)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. The PoC (mount_ntfs of a crafted bf_bps=0 image) panics the unpatched #0 baseline kernel with 'Fatal trap 18: integer divide fault at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax' (guest drops to db> and dies). On the patched ntfs.ko module the SAME PoC is rejected with 'mount_ntfs: /dev/vn1: Invalid argument' (exit 71 = EINVAL from the new validation block) and the guest stays fully responsive - no panic, no trap. All four crafted variants (bps=0 div#0, spc=0 statfs-div#0, mftrecsz=0x80 UB-shift, and a structurally-valid control) are rejected with EINVAL by the fix. Deterministic across two full runs on the patched module. The fix closes the bug completely: every divisor (ntm_bps at :354, ntm_bpmftrec at :620/:646) is guaranteed non-zero before first use, and the shift count is bounded to [0,31].

BEFORE (unpatched #0 ntfs.ko):
  mount_ntfs -o ro /dev/vn1 /mnt_df0872
  Fatal trap 18: integer divide fault while in kernel mode
  Stopped at ntfs_mountfs.isra.0+0x5ca: idivl %ecx,%eax
  db>   (guest DEAD - reproduced twice)

AFTER (patched ntfs.ko, sha256 1906300f...):
  mount_ntfs -o ro /dev/vn1 /mnt_df0872
  mount_ntfs: /dev/vn1: Invalid argument
  MOUNT_EXIT=71   (guest UP, no panic - deterministic across 2 runs + 4 variants)
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel binary unchanged) + patched ntfs.ko module (sha256 1906300f73d2c3b82e69e9ea628a028616f713c4b28afa72993ca90e1211be2f). Note: NTFS is a pure loadable module on X86_64_GENERIC (not in the static kernel), so the fix is validated at the module level - the main kernel binary is irrelevant to this bug. A full nativekernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0, full log in fix_build.log) also compiled the patched ntfs_vfsops.c into ntfs.ko.

Confirmed kernel references

Detail

Exploit chain

none. This is a pure integer-divide-by-zero (CWE-369), not memory corruption. The CPU #DE (trap 18) fires at the idivl instruction before any memory write occurs, so there is no write/UAF/double-free/type-confusion primitive to escalate. No slab grooming, victim object, or uid=0 chain is possible or relevant. The impact ceiling is a deterministic, non-resumable kernel panic (DoS): a local user who can get a crafted NTFS image mounted (standard FS-parsing threat model; fully unprivileged under vfs.usermount=1 + root-created vnode disk owned by attacker) instantly kills the kernel. No chain file was authored because no chain exists for this bug class.

Evidence (decisive lines)

=== Unpatched #0 baseline (run.log / run.2.log) ===
[*] mount_ntfs -o ro /dev/vn1 /mnt_df0872
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 2; lapic id = 2
instruction pointer  = 0x8:0xffffffff82600b1a
kernel: type 18 trap, code=0
CPU2 stopping CPUs: 0x0000003b
Stopped at      ntfs_mountfs.isra.0+0x5ca:      idivl   %ecx,%eax
db>   <-- guest DEAD (reproduced twice, deterministic)

=== Patched ntfs.ko (fix_run.log / fix_run.2.log) ===
[*] mount_ntfs -o ro /dev/vn1 /mnt_df0872
mount_ntfs: /dev/vn1: Invalid argument
MOUNT_EXIT=71   <-- EINVAL, guest stays UP

PoC changes

Evidence pack authored from scratch (the finding had no pre-existing poc/DF-0872/ folder). Wrote craft_ntfs.c (minimal trigger: bf_bps=0 + bf_mftrecsz=0xF6 -> div#0 at :354), craft_variants.c (4 images exercising bps=0 / spc=0 / mftrecsz=0x80-UB-shift / control), build.sh, run.sh (vnconfig + mount_ntfs), fix.diff (the validated fix), VERDICT.md, README.md, manifest.json. No source changes to the trigger were needed after first compile - the crafted-image approach worked on the first run.

Verified recommended fix

In sys/vfs/ntfs/ntfs_vfsops.c, immediately after the bf_sysid strncmp check at :343 and before the bpmftrec computation at :349-355, validate the BPB-derived divisor/shift fields and return EINVAL on any bad value: (1) reject ntm_bps==0 || ntm_spc==0 (closes the mount-time #DE at :354 and the statfs #DE at :620/:646); (2) bound the shift count int shift=-cpr to [0,31] (closes the 1<<128 UB when bf_mftrecsz==0x80 / cpr==INT8_MIN); (3) final guard reject ntm_bpmftrec==0 (defense-in-depth for the statfs divisor). Full git-apply-able diff in findings/poc/DF-0872/fix.diff. Note: ntfs is a loadable module (ntfs.ko), not compiled into the static X86_64_GENERIC kernel, so the fix lives in the module. This is a new fix authored during verification (no prior proposal in a finding markdown to supersede).

Verdict

REPRODUCED. The bug at sys/vfs/ntfs/ntfs_vfsops.c:349-354 is real and deterministic: ntfs_mountfs() computes ntm_bpmftrec = (1 << (-cpr)) / ntmp->ntm_bps using BPB fields (bf_bps, bf_mftrecsz) read directly from the boot sector with NO validation beyond an 8-byte OEM-id strncmp at :343. A crafted NTFS image with bf_bps=0 and bf_mftrecsz=0xF6 (signed -10, forcing the else branch) drives (1<<10)/0 -> CPU #DE trap -> 'Fatal trap 18: integer divide fault' panic at ntfs_mountfs.isra.0+0x5ca (idivl %ecx,%eax). Confirmed twice on the unpatched #0 baseline kernel with byte-identical panic signatures. Two further sub-bugs cited by the finding are also real and covered by the fix: bf_mftrecsz=0x80 (cpr=INT8_MIN) -> 1<<128 undefined shift, and bf_spc=0 -> bpmftrec=0 -> divide-by-zero later in ntfs_statfs() at :620/:646. This is a pure integer-divide-by-zero (CWE-369): the #DE fires BEFORE any memory write, so there is no memory-corruption primitive and no escalation path - the realistic ceiling is mount-time kernel DoS.