hammer(1) setattr/write have no va_size clamp: DF-2921-class nvtruncbuf/nvnode_pager_setsize signed-overflow β unprivileged permanent kernel livelock on any HAMMER1 mount
| Field | Value |
|---|---|
| ID | DF-2999 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-190 |
| File | sys/vfs/hammer/hammer_vnops.c |
| Lines | 670, 2269, 2276 (sinks: vfs_vm.c:149, :464-495) |
| Area | vfs/hammer |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | hammer2? no β base:vfs (hammer1) |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer_vop_setattr passes the fully unclamped 64-bit vap->va_size to nvtruncbuf()/nvextendbuf() and hammer_vop_write does the same for implicit extends; for lengths within HAMMER_XBUFSIZE(64K) of 2^63 the block round-up wraps to INT64_MIN and the page-unmap loop iterates ~2^63 times holding vp->v_token and the vm_object lock. This is the HAMMER1 call-site of the bug verified as DF-2921 on HAMMER2 β same vfs_vm.c arithmetic, distinct trigger path (every HAMMER1 mount). Unprivileged local user with write access to any file on a HAMMER1 filesystem (mountable via vnconfig+newfs_hammer+mount -t hammer; still-supported root FS on older systems): one ftruncate-pair wedges a CPU permanently and poisons the vnode β unkillable (kill -9 ignored), concurrent unlink parks in D state, shutdown hangs; repeatable per file/CPU. VERIFIED live on stock kernel #0; fix (clamp to OFF_MAX β HAMMER_XBUFSIZE) validated on rebuilt kernel: instant EFBIG, smoke test clean.
Timeline
- 2026-09-02 Discovered during pass-2 audit of hammer_vnops.c (GLM 5.3); unpriv livelock reproduced + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2999 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| op2999.c | β | 1.8 KB | view raw | |
| build.sh | β | 78 B | view raw | |
| run.sh | β | 583 B | view raw | |
| run.log | β | 934 B | view raw | |
| build.log | β | 1.6 KB | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| env.txt | β | 436 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw | |
| manifest.json | β | 972 B | view raw |
DF-2999 VERDICT (updated after verification)
Baseline (stock kernel): REPRODUCED β unprivileged permanent kernel livelock
Guest: DragonFly 6.5-DEVELOPMENT #0 (INVARIANTS), HAMMER1 on /dev/vn0 at
/mnt/h1 (vn-backed image), run as unprivileged user maxx:
/tmp/op2999 /mnt/h1/maxx2999.bin
extend to 0x7ffffffffffff000: OK <- hammer_vop_setattr:2276 nvextendbuf
pwrite top byte: OK <- hammer_vop_write:670 nvextendbuf
<-- ftruncate(0x7fffffffffff8000) never returns
<- hammer_vop_setattr:2269 nvtruncbuf
vfs_vm.c:149 truncloffset = INT64_MIN
vfs_vm.c:464 nobjsize ~0xFFF8000000000000
vfs_vm.c:486 unmap loop ~2^63 iterations
ps: 1047 R4 (spinning in-kernel), kill -9 Γ2 ineffective,
concurrent `rm` of the file parks in D5 state,
`ls -la /mnt/h1` shows directory total corrupted by the 2^63-scale size
("total 18014398509543426"), one cpu 100% until guest reset.
Identical signature to DF-2921 (hammer2 trigger), confirming HAMMER1 has the same missing va_size clamp at hammer_vnops.c:2261-2286 (setattr) and :664-677 (write-path nvextendbuf).
Root cause chain (path:line)
- sys/kern/vfs_vfsops? no β ftruncate passes any 0 < len < 2^63 through VOP_SETATTR (kern_utimensat-style pre-checks only for utimes; for ftruncate: sys/kern/vfs_syscalls.c kern_ftruncate -> VOP_SETATTR).
- sys/vfs/hammer/hammer_vnops.c:2261-2286 β no va_size bound check for VREG; blksize = hammer_blocksize(va_size) = HAMMER_XBUFSIZE (64K) for any offset >= HAMMER_XDEMARC (1MB); calls nvtruncbuf()/nvextendbuf() with the raw 64-bit length.
- sys/kern/vfs_vm.c:145-153 β info.truncloffset = length + (blksize-boff) wraps to INT64_MIN for length = 0x7fffffffffff8000, boff = 0x8000.
- sys/kern/vfs_vm.c:459-495 β signed OFF_TO_IDX overflow -> nobjsize ~0xFFF8000000000000; shrink branch unmap loop from piβ2^51 to 2^63.9 with lwkt_yield() each iteration while holding vp->v_token and the vm_object lock: unkillable (thread never returns to userland), vnode poisoned, shutdown hangs.
Impact
Unprivileged local permanent denial of service on every HAMMER1 mount (one wedge per triggered file/cpu; guest recoverable only by hard reset). No memory-safety impact (pure livelock). Same severity class as DF-2921.
Fix validation
fix.diff (in this pack) clamps va_size in hammer_vop_setattr and the write
end-offset in hammer_vop_write to OFF_MAX - HAMMER_XBUFSIZE and rejects
negative va_size. Built with make nativekernel in-guest and re-tested:
see "Fix validation" section appended below after the patched run.
Fix validation (kernel #2, Sat Sep 5 01:44:36 UTC 2026)
fix.diff applied in-guest, make nativekernel + make installkernel,
guest rebooted into the patched kernel:
$ time /tmp/op2999 /mnt/h1/maxx2999.bin
extend-> File too large
0.00 real 0.00 user 0.00 sys
EXIT=1
The wrap-prone ftruncate(0x7ffffffffffff000) is now rejected at hammer_vnops.c (clamp) with EFBIG before nvextendbuf()/nvtruncbuf() are ever reached. No wedge, no R4 thread, second ftruncate not attempted, guest fully healthy afterwards (smoke test: create/write/truncate/read/ unlink on HAMMER1 all normal β see build.log).
fix_status: fixed (baseline reproduced wedge + unkillable R4; patched kernel returns EFBIG instantly; behavior change is exactly the intended one). Note: kernel #2 also carried a non-semantic kprintf probe for the DF-3000 investigation in the same file (verified present via strings on kernel.stripped); it does not affect this validation.
Fix verification
fixedBaseline kernel #0: unpriv ftruncate wedge (unkillable R4, D-state rm, unshut-downable guest). Patched kernel #2 (fix.diff): same PoC returns EFBIG in 0.00s; no wedge; HAMMER1 smoke test (create/write/truncate/read/unlink) clean.
build.log (fix validation section); VERDICT.md (Fix validation section)
Confirmed kernel references
Detail
Exploit chain
unpriv user -> ftruncate(fd,0x7ffffffffffff000) [extend via nvextendbuf] -> pwrite 1 byte near EOF -> ftruncate(fd,0x7fffffffffff8000) [truncate via nvtruncbuf: 0x7fffffffffff8000+(65536-0x8000) wraps to 2^63] -> nvnode_pager_setsize unmap loop ~2^63 iterations -> unkillable kernel livelock, poisoned vnode, guest unshut-downable
Evidence (decisive lines)
run.log: baseline output - extend OK, pwrite OK, second ftruncate never returns; ps shows PID 1047 R4; kill -9 ineffective; rm in D5; 'total 18014398509543426' directory corruption | build.log: fix validation on kernel #2 - 'extend-> File too large', 0.00s, EXIT=1; smoke test OK | fix.diff: va_size clamp in hammer_vop_setattr + end-offset clamp in hammer_vop_write
PoC changes
adapted DF-2921's op2921.c trigger (same offsets) to a HAMMER1 vn-backed mount; path taken from argv; added cleanup path (unreachable on vulnerable kernel)
Verified recommended fix
Clamp va_size (and write end-offset) to OFF_MAX - HAMMER_XBUFSIZE in hammer_vnops.c; reject negative va_size (see fix.diff); alternatively apply DF-2921's vfs_vm.c clamp globally.
Verdict
REPRODUCED unprivileged permanent kernel livelock on a HAMMER1 mount, then fix validated. hammer_vop_setattr (sys/vfs/hammer/hammer_vnops.c:2261-2286) passes the unclamped 64-bit va_size to nvtruncbuf()/nvextendbuf(); ftruncate(0x7ffffffffffff000)+pwrite+ftruncate(0x7fffffffffff8000) by an unprivileged user puts the thread into an unkillable R4 kernel loop (nvtruncbuf truncloffset wraps to INT64_MIN at sys/kern/vfs_vm.c:149; nvnode_pager_setsize unmap loop at vfs_vm.c:486-495 iterates ~2^63 times holding vp->v_token and the vm_object lock). kill -9 ineffective; concurrent rm parks in D state; directory listing corrupted by the 2^63-scale size; guest recoverable only by killing QEMU. Identical signature to DF-2921 (the HAMMER2 trigger of the same vfs_vm.c bug) - this is the HAMMER1 call-site. Patched kernel (va_size clamped to OFF_MAX - HAMMER_XBUFSIZE in setattr + write path, fix.diff) returns EFBIG instantly; smoke test clean.
No comments yet.