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

Divide-by-zero kernel panic via HAMMER_IOC_PRUNE with mod_tid=0

Summary

hammer_prune.c:305-306 (create_tid-beg_tid)/scan->mod_tid == (delete_tid-beg_tid)/scan->mod_tid. mod_tid uint64 from user copyin :104 NO zero-check anywhere. Entry validation :67-81 checks nelms/localization/key-order/PRUNE_ALL only. Divide executes when :302-304 all true: delete_tid!=0 create_tid>=beg_tid delete_tid<=end_tid. With beg_tid=0 end_tid=UINT64_MAX every deleted record triggers /0 = #DE = kernel panic. Any local user with fd on HAMMER fs (SYSCAP_NOVFS_IOCTL default local). Deleted record trivially produced by creat+write+unlink+sync. Fix: reject mod_tid==0 EINVAL.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0910 Β· 13 files
FileTypeDescriptionSize
prune_div0.c trigger-source HAMMER_IOC_PRUNE ioctl with mod_tid=0 trigger; seeds a deleted B-tree record then fires the divide-by-zero 5.0 KB view raw
build.sh build-script cc -Wall -O2 -o prune_div0 prune_div0.c 149 B view raw
run.sh run-script sets up a HAMMER v1 vn-backed mount and fires the PoC 1.4 KB view raw
VERDICT.md verdict full narrative: mechanism, reproduction, fix validation 5.1 KB ↓ raw
panic.txt panic-signature Fatal trap 18 / divq at hammer_ioc_prune+0x31b from boot.log 1.4 KB view raw
run.log run-log baseline reproduction evidence (2/2 runs panicked identically) 1.1 KB view raw
fix_run.log fix-run-log patched #1 kernel: ioctl returns EINVAL, no panic 290 B view raw
fix_build.log fix-build-log full single-fix nativekernel build output (rc=0) 5.6 MB ↓ download
fix.diff suggested-fix reject mod_tid==0 with EINVAL after copyin in hammer_ioc_prune 795 B view raw
env.txt environment uname -a, cc version 247 B view raw
README.md readme human reproduce doc 963 B ↓ 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 human reproduce doc
↓ download raw

DF-0910 β€” Divide-by-zero via HAMMER_IOC_PRUNE mod_tid=0

Verdict: REPRODUCED β†’ fix VALIDATED.
Impact: panic (privileged-user DoS; root-only ioctl, no escalation).
Severity: Medium.

Bug

sys/vfs/hammer/hammer_prune.c:305-306 divides by scan->mod_tid (uint64, user-supplied via copyin at :104) with no zero-check. An ioctl with mod_tid=0 and a deleted B-tree record in range drives a CPU #DE β†’ kernel panic.

Reproduce

./build.sh                    # cc -o prune_div0 prune_div0.c
./run.sh                      # root: sets up HAMMER v1 mount, fires ioctl
  • Vulnerable kernel: Fatal trap 18: integer divide fault at hammer_ioc_prune+0x31b: divq %r9,%eax β†’ DDB.
  • Fixed kernel: ioctl returns EINVAL (errno 22), guest stays up.

Fix

fix.diff rejects mod_tid==0 (EINVAL) right after the element copyin. Validated on a single-fix kernel (6.5-DEVELOPMENT #1).

Files

See manifest.json.

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

DF-0910 β€” Divide-by-zero kernel panic via HAMMER_IOC_PRUNE with mod_tid=0

Verdict

REPRODUCED β€” and FIX VALIDATED. The HAMMER v1 prune ioctl divides by a user-supplied mod_tid with no zero-check, driving a CPU #DE (integer divide fault) β†’ non-resumable kernel trap β†’ panic. Confirmed deterministically on the unpatched #0 kernel. The single-fix kernel (#1) rejects mod_tid==0 with EINVAL and does not panic.

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

  1. Attacker input: HAMMER_IOC_PRUNE ioctl with a hammer_ioc_prune_elm whose mod_tid == 0. The element array is copied in from userland at sys/vfs/hammer/hammer_prune.c:104 (copyin(user_elms, copy_elms, elm_array_size)). The entry validation block at :67-81 checks nelms, localization, key ordering, and PRUNE_ALL β€” but never validates mod_tid != 0.

  2. Reachability: the ioctl is dispatched via hammer_vop_ioctl (sys/vfs/hammer/hammer_vnops.c:2517) β†’ hammer_ioctl (sys/vfs/hammer/hammer_ioctl.c:65), gated by caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) at :72 β€” root-only. Reachability requires a HAMMER v1 mount (the guest's root is HAMMER2, so a HAMMER v1 image must be created and mounted; this is a realistic admin-precondition, not an attacker action).

  3. The divide: during the B-tree scan, each leaf is passed to prune_should_delete() (sys/vfs/hammer/hammer_prune.c:272). When the leaf is a deleted record (delete_tid != 0) and falls in the element's beg_tid..end_tid range (:302-304), the comparison at :305-306 executes: c (elm->base.create_tid - scan->beg_tid) / scan->mod_tid == (elm->base.delete_tid - scan->beg_tid) / scan->mod_tid With scan->mod_tid == 0, this is divq by zero β†’ CPU #DE trap.

  4. Effect: trap 18 (#DE) from kernel mode is non-resumable β†’ kernel panic, guest drops to DDB (db>), all I/O ceases. This is a privileged user β†’ kernel DoS (Medium severity). No memory corruption, no escalation path β€” the CPU traps before any memory write occurs.

Exploit chain / escalation

Not applicable β€” this is a divide-by-zero, not memory corruption. The #DE trap fires before any store, so there is no write primitive, no slab corruption, no UAF. The impact ceiling is denial of service (kernel panic β†’ reboot). No escalation chain is derivable.

Reproduction

Build: cc -Wall -O2 -o prune_div0 prune_div0.c

Run (as root, on a guest with HAMMER v1 support):

truncate -s 12G /root/df910.img
vnconfig vn0 /root/df910.img
newfs_hammer -f -L df910 /dev/vn0
mkdir -p /df910 && mount -t hammer /dev/vn0 /df910
./prune_div0 /df910   # creat+write+unlink+sync seeds a deleted record,
                      # then issues HAMMER_IOC_PRUNE with mod_tid=0

On unpatched #0 kernel (baseline):

Fatal trap 18: integer divide fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff8094bcfb
Stopped at      hammer_ioc_prune+0x31b: divq    %r9,%eax
db>

(The static helper prune_should_delete is inlined into hammer_ioc_prune by gcc, hence the crash frame names the caller.) Reproduced twice deterministically β€” identical panic both times.

On patched #1 kernel (single-fix):

[!] ioctl returned rc=-1 errno=22 (Invalid argument) -- kernel NOT vulnerable
    stat_scanrecords=0 stat_rawrecords=0

Guest stays up. Confirmed across 3 consecutive runs.

The fix

fix.diff adds a post-copyin validation loop that rejects any prune element with mod_tid == 0 (returns EINVAL). Placed right after the copyin at hammer_prune.c:104-105 β€” before prune->elms = copy_elms and well before the scan loop β€” so no caller can reach the divide with a zero divisor.

for (i = 0; i < prune->nelms; ++i) {
    if (copy_elms[i].mod_tid == 0) {
        error = EINVAL;
        goto failed;
    }
}

Built and validated on a single-fix kernel (6.5-DEVELOPMENT #1: Sun Jul 12 02:48:50 UTC 2026): the previously-fatal ioctl now returns EINVAL and the guest survives.

PoC changes

  • Wrote prune_div0.c from scratch (no prior PoC existed in this checkout β€” only the static-site HTML summary was present). The PoC mirrors the exact kernel struct layouts (hammer_base_elm, hammer_ioc_head, hammer_ioc_prune_elm, hammer_ioc_prune) and computes the ioctl number via _IOWR('h', 1, struct hammer_ioc_prune).
  • It seeds a deleted B-tree record (creat+write+unlink+sync) so the scan reaches prune_should_delete with a delete_tid != 0 leaf, then issues the ioctl with elms[0] = { beg_tid=0, end_tid=UINT64_MAX, mod_tid=0 }.
  • build.sh / run.sh wrap the build and the HAMMER-v1 mount setup.

Severity / threat model

Medium (matches the finding). The ioctl requires SYSCAP_NOVFS_IOCTL (root). A privileged user issuing a malformed prune ioctl can deterministically panic the kernel. No unprivileged path was found: caps_priv_check is a hard capability gate on the credential, not bypassable via vfs.usermount or file-descriptor tricks. This is a root→kernel DoS / robustness bug.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. prune_div0 panics the unpatched #0 baseline (Fatal trap 18 / divq at hammer_ioc_prune+0x31b, 3/3 runs) and does NOT panic the single-fix #1 kernel -- the ioctl returns EINVAL (errno 22) and stat_scanrecords=0 (scan never starts), guest stays up across 3/3 runs. The fix closes the bug cleanly: mod_tid==0 is rejected before the divide is reachable.

BASELINE (#0 unpatched): 'Fatal trap 18: integer divide fault while in kernel mode / Stopped at hammer_ioc_prune+0x31b: divq %r9,%eax / db>' -- guest DOWN. PATCHED (#1 single-fix): 'ioctl returned rc=-1 errno=22 (Invalid argument) -- kernel NOT vulnerable / stat_scanrecords=0 stat_rawrecords=0' -- guest UP. Before/after contrast: #DE panic vs clean EINVAL return.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 02:48:50 UTC 2026 (single-fix kernel: fix.diff applied to hammer_prune.c, make -j6 nativekernel, make installkernel)

Confirmed kernel references

Detail

Exploit chain

none -- this is a divide-by-zero (CPU #DE trap), not memory corruption. The trap fires before any store instruction executes, so there is no write/UAF/double-free primitive to groom or convert. The impact ceiling is denial of service (deterministic kernel panic -> reboot). No slab corruption, no credential overwrite, no code-exec path derivable. The ioctl gate (caps_priv_check SYSCAP_NOVFS_IOCTL at hammer_ioctl.c:72) makes this root-only, so even the DoS requires an already-privileged credential.

Evidence (decisive lines)

BASELINE (#0 kernel, unpatched): ./prune_div0 /df910 -> ssh times out (kernel panicked). boot.log: 'Fatal trap 18: integer divide fault while in kernel mode / cpuid = 0; lapic id = 0 / instruction pointer = 0x8:0xffffffff8094bcfb / kernel: type 18 trap, code=0 / Stopped at hammer_ioc_prune+0x31b: divq %r9,%eax / db>'. Reproduced 3x deterministically (identical panic each time). PATCHED (#1 kernel): ./prune_div0 /df910 -> 'ioctl returned rc=-1 errno=22 (Invalid argument) -- kernel NOT vulnerable / stat_scanrecords=0'. Guest stays up. Confirmed across 3 consecutive runs.

PoC changes

Wrote prune_div0.c from scratch (no prior PoC source existed in this checkout -- only the static-site HTML summary). The PoC mirrors the exact kernel struct layouts (hammer_base_elm, hammer_ioc_head, hammer_ioc_prune_elm, hammer_ioc_prune) and computes the ioctl number via _IOWR('h',1,struct hammer_ioc_prune). It seeds a deleted B-tree record (creat+write+unlink+sync) so the scan reaches prune_should_delete with a delete_tid!=0 leaf, then issues the ioctl with elms[0]={beg_tid=0,end_tid=UINT64_MAX,mod_tid=0}. Also wrote build.sh, run.sh (sets up a HAMMER v1 vn-backed sparse 12GB image since the guest root is HAMMER2), VERDICT.md, manifest.json, fix.diff.

Verified recommended fix

fix.diff adds a post-copyin validation loop in hammer_ioc_prune (hammer_prune.c, right after the copyin at :104-105) that rejects any prune element with mod_tid==0 by returning EINVAL, before the scan loop can reach the divide at :305-306. This is a new fix authored during verification; no prior fix proposal existed in a finding markdown (only the HTML summary was present). The full git-apply-able diff lives in findings/poc/DF-0910/fix.diff.

Verdict

REPRODUCED and FIX VALIDATED. The HAMMER v1 prune ioctl (HAMMERIOC_PRUNE) divides by the user-supplied mod_tid field at sys/vfs/hammer/hammer_prune.c:305-306 with no zero-check anywhere (entry validation at :67-81 checks nelms/localization/key-order/PRUNE_ALL only). The mod_tid is copied in from userland at :104 (copyin) into hammer_ioc_prune_elm.mod_tid. When a HAMMER_IOC_PRUNE ioctl is issued with elms[0].mod_tid=0 and a deleted B-tree record (delete_tid!=0) is in scan range, the comparison (create_tid-beg_tid)/mod_tid == (delete_tid-beg_tid)/mod_tid executes a divq by zero -> CPU #DE (trap 18) -> non-resumable kernel panic. Confirmed 3x on the unpatched #0 kernel, all panicking at hammer_ioc_prune+0x31b: divq %r9,%eax (the static helper prune_should_delete is inlined by gcc). The ioctl is root-only via caps_priv_check(SYSCAP_NOVFS_IOCTL) at hammer_ioctl.c:72, so this is a privileged-user->kernel DoS (Medium). No escalation path exists: #DE fires before any memory write, so there is no write primitive.