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)
PoC verification
Evidence pack
findings/poc/DF-2554 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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(-Werrorclean; seefix_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 withbf_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
fixedVALIDATED: 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).
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'.
No comments yet.