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)
PoC verification
Evidence pack
findings/poc/DF-0872 Β· 20 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
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:
-
Divide-by-zero at mount (
:354) βntm_bps(bytes-per-sector,u16at BPB offset0x0B) is never validated. Withbf_bps == 0and anybf_mftrecszwith signed value<= 0(e.g.0xF6βcpr = -10, the canonical "1<<10 bytes per MFT record" value), theelsebranch executes(1 << 10) / 0β CPU raises#DEβFatal trap 18β kernel panic. This is the primary trigger the PoC exercises. -
Undefined shift (
:354) β ifbf_mftrecsz == 0x80(int8_t cpr = -128 = INT8_MIN), then-cprpromotes toint 128and1 << 128is undefined behaviour (shift count>=type width). gcc 8.3 emits anidivl/shllsequence whose result is unpredictable; on this guest theshll $0x80,%eaxyields 0 (masked to 5 bits β shift by 0 β 1), which then divides bybpsβ ifbps != 0this does not #DE but produces a garbagebpmftrecthat later corrupts statfs arithmetic. -
Divide-by-zero at statfs (
:620/:646) β ifbf_spc == 0andbf_mftrecsz > 0(e.g.0x01), thenbpmftrec = spc * cpr = 0 * 1 = 0. Mount succeeds past:354, but the very nextVFS_STATFSat:263(called immediately afterntfs_mountfsreturns) reachessbp->f_ffree = sbp->f_bfree / ntmp->ntm_bpmftrecat:620β divide by zero β panic. (Variant v2 incraft_variants.cexercises 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 withbf_bps=0andbf_mftrecsz=0xF6(the canonical-10value that forces theelsebranch 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:
- Reject
ntm_bps == 0 || ntm_spc == 0withEINVALβ closes the mount-time#DEat:354and the statfs#DEat:620/:646(bpmftrec can no longer be 0 viaspc * cpr). - Bound the shift count: compute
int shift = -cprand rejectshift < 0 || shift >= 32withEINVALβ closes the1 << 128undefined-shift case (bf_mftrecsz == 0x80). - Final guard: reject
ntm_bpmftrec == 0withEINVALβ 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)
sys/vfs/ntfs/ntfs_vfsops.c:327βbread(devvp, BBLOCK, BBSIZE, &bp)reads the boot sectorsys/vfs/ntfs/ntfs_vfsops.c:331βbcopyintontm_bootfilesys/vfs/ntfs/ntfs_vfsops.c:343β the only pre-vulnerability guard (strncmpsysid)sys/vfs/ntfs/ntfs_vfsops.c:349-355β the vulnerable bpmftrec computation (divide-by-zero + UB shift)sys/vfs/ntfs/ntfs_vfsops.c:263βVFS_STATFScalled immediately after mountsys/vfs/ntfs/ntfs_vfsops.c:620βf_bfree / ntm_bpmftrec(statfs divide-by-zero)sys/vfs/ntfs/ntfs_vfsops.c:646β same divide inntfs_statvfssys/vfs/ntfs/ntfs.h:223-241βstruct bootfileBPB layout (#pragma pack(1))sys/vfs/ntfs/ntfs.h:267-269βntm_mftrecsz/ntm_spc/ntm_bpsmacros
Fix verification
fixedVALIDATED. 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)
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.
No comments yet.