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)
PoC verification
Evidence pack
findings/poc/DF-0910 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 faultathammer_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.
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)
-
Attacker input:
HAMMER_IOC_PRUNEioctl with ahammer_ioc_prune_elmwhosemod_tid == 0. The element array is copied in from userland atsys/vfs/hammer/hammer_prune.c:104(copyin(user_elms, copy_elms, elm_array_size)). The entry validation block at:67-81checksnelms, localization, key ordering, andPRUNE_ALLβ but never validatesmod_tid != 0. -
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 bycaps_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). -
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'sbeg_tid..end_tidrange (:302-304), the comparison at:305-306executes:c (elm->base.create_tid - scan->beg_tid) / scan->mod_tid == (elm->base.delete_tid - scan->beg_tid) / scan->mod_tidWithscan->mod_tid == 0, this isdivqby zero β CPU #DE trap. -
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.cfrom 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_deletewith adelete_tid != 0leaf, then issues the ioctl withelms[0] = { beg_tid=0, end_tid=UINT64_MAX, mod_tid=0 }. build.sh/run.shwrap 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
fixedVALIDATED. 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.
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.
No comments yet.