Signed integer overflow in nvtruncbuf()/nvnode_pager_setsize() at file lengths within blksize of 2^63 β ~2^63-iteration unmap loop holding the vnode token and object lock: unprivileged, unkillable, permanent kernel livelock on HAMMER2 (guest becomes unshut-downable)
| Field | Value |
|---|---|
| ID | DF-2921 |
| 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 β CWE-835 |
| File | sys/kern/vfs_vm.c |
| Lines | 145-153, 459-495 |
| Area | kern/vfs + vm |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
HAMMER2 imposes no va_size clamp in setattr (hammer2_vnops.c:512-525) and always passes 64KB blocks. With length=0x7fffffffffff8000, boff=0x8000: nvtruncbuf's truncloffset = length + (blksize - boff) wraps to INT64_MIN (:149), making nvtruncbuf_bp_trunc_cmp match and B_INVAL-destroy every buffer on the vnode including dirty ones; nvnode_pager_setsize's nobjsize = OFF_TO_IDX(truncboffset + blksize + PAGE_MASK) wraps negative and arithmetic-shifts to 0xFFF8000000000000 (:464), so the unmap loop (:486-495) iterates from piβ2^51 toward ~2^63.9 β ~1.8e19 RB-lookup+yield iterations (years) β while holding vp->v_token and the vm_object lock. Kernel is built -fno-strict-overflow, so the wraparound is implemented behavior. Reproduced live as unprivileged user: thread stuck in state R0 >5 minutes, kill -9 ineffective, second process on the same file parked in D state, rm hung, three shutdown attempts hung, guest recoverable only by killing QEMU; crash dump taken. Repeatable per file to wedge additional CPUs. UFS/NFS/tmpfs unreachable (max file sizes βͺ 2^63); same-class overflow also present in vnode_pager_alloc (vnode_pager.c:151-154).
Proof of contest
VERIFIED (findings/poc/DF-2921/): op2921.c β ftruncate(0x7ffffffffffff000), pwrite near end, ftruncate(0x7fffffffffff8000). Vulnerable kernel: process never returns from the second ftruncate (R0), kill -9 ignored, rm/shutdown hang; crash dump captured. Patched kernel (clamp to OFF_MAX + unsigned page-count math): identical trigger completes instantly; shutdown works.
Recommended fix
Clamp the buffer-destruction boundary to OFF_MAX on would-be overflow and compute the object page count and loop start in unsigned 64-bit math. Validated fix.diff in findings/poc/DF-2921/.
Timeline
- 2026-09-02 Discovered during pass-2 audit of vfs_vm.c (GLM 5.3); unpriv livelock reproduced on HAMMER2 + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2921 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| op2921.c | β | 1.1 KB | view raw | |
| build.sh | β | 111 B | view raw | |
| run.sh | β | 610 B | view raw | |
| livelock-evidence.txt | β | 3.0 KB | view raw | |
| panic.txt | β | 814 B | view raw | |
| run.fixed.log | β | 140 B | view raw | |
| fix-build.log | β | 443 B | view raw | |
| VERDICT.md | β | 3.2 KB | β raw | |
| README.md | β | 1.3 KB | β raw | |
| fix.diff | β | 1.9 KB | view raw |
DF-2921 β INT64 overflow in nvtruncbuf/nvnode_pager_setsize β unkillable kernel livelock
Unprivileged 3-syscall trigger on HAMMER2 that spins a CPU inside the
kernel forever (unkillable), blocks all other access to the file's vnode
(rm hangs, shutdown hangs β guest only recoverable by killing QEMU).
Build (in guest)
./build.sh # cc -O2 -o /tmp/op2921 op2921.c
Run (as unprivileged user, THROWAWAY VM ONLY)
./run.sh # backgrounds the trigger, shows R0/D state after 15s # then: kill -9 <pid> β has no effect on a vulnerable kernel
Expected on a VULNERABLE kernel
- op2921 stuck in kernel (state R0), no output past (at most) the first two lines; kill -9 ineffective - second run on the same file parks in D state - `rm /home/maxx/df2921.bin` hangs (console: `cache_lock_shared: rm blocked ... "df2921.bin"`) - `shutdown -p now` hangs
Expected on a FIXED kernel (fix.diff applied)
All four steps of op2921 print OK and the process exits immediately (see run.fixed.log).
Files
op2921.c trigger livelock-evidence.txt observations + exact overflow math fix.diff fix (sys/kern/vfs_vm.c: clamp + unsigned math) run.fixed.log trigger on patched kernel VERDICT.md full narrative
DF-2921 β signed overflow β permanent unprivileged kernel livelock (unkillable)
Verdict (baseline, stock kernel): REPRODUCED
File: sys/kern/vfs_vm.c (nvtruncbuf() line 149, nvnode_pager_setsize()
lines 463-466, unmap loop 486-495)
Class: integer overflow (CWE-190) β infinite loop (CWE-835) β DoS
Impact: permanent, unkillable kernel livelock on one CPU per trigger,
plus permanent hang of every other thread touching the same vnode
(including rm, shutdown); guest becomes unshut-downable.
Confidence: certain β reproduced live on the guest as an unprivileged
user; source-level math exact; kernel compiled with -fno-strict-overflow
so the wraparound is the implemented behavior.
Trigger (unprivileged, HAMMER2, 3 syscalls β see op2921.c)
ftruncate(fd, 0x7ffffffffffff000LL); /* extend to 2^63-4096 (sparse) */
pwrite(fd, "A", 1, 0x7fffffffffffefffLL); /* dirty the last 64K block */
ftruncate(fd, 0x7fffffffffff8000LL); /* truncate mid-64K-block */
HAMMER2 imposes no va_size clamp (sys/vfs/hammer2/hammer2_vnops.c:512-525)
and passes nblksize = HAMMER2_PBUFSIZE = 64K to nvtruncbuf()
(hammer2_vnops.c:1260-1263, hammer2_calc_logical() always returns
HAMMER2_PBUFSIZE, sys/vfs/hammer2/hammer2_subr.c:276).
Observed on the guest
- Process enters kernel and spins in state
R0forever (first run pid 1025, observed > 5 minutes, stillR0). kill -9ineffective (the loop never returns to userland;lwkt_yield()does not process signals).- A second process touching the same file blocks forever (
D5) behind the spinner'svp->v_token(taken atvfs_vm.c:153) and vm_object lock. rmof the file hangs: serial console shows[diagnostic] cache_lock_shared: rm blocked on 0xfffff8008f54dc00 "df2921.bin".- Three
shutdown -p nowattempts all hung; guest only recoverable by killing QEMU. A crash dump was taken viadebug.panic=1(vmcore.0).
Root cause math
nvtruncbuf()vfs_vm.c:148-151:truncloffset = length + (blksize - boff)=0x7fffffffffff8000 + (65536 - 32768)=0x8000000000000000β INT64_MIN. The RB_SCAN compare (vfs_vm.c:273) then matches every buffer on the vnode (including negative-loffset metadata buffers on UFS) and destroys them withB_INVAL|B_NOCACHEβ silently discarding dirty buffers.nvnode_pager_setsize()vfs_vm.c:461-466:nobjsize = OFF_TO_IDX(truncboffset + blksize + PAGE_MASK)=OFF_TO_IDX(0x8000000000000fff)β the sum wraps negative and the arithmetic>>yields vm_pindex_t 0xFFF8000000000000.- Unmap loop
vfs_vm.c:486-495:pistarts at ~2^51 and the loop runs to 0xFFF8000000000000 β ~1.8Γ10^19 iterations ofvm_page_lookup_busy_wait()(RB miss) +lwkt_yield()β years, holding the vnode token and the object lock the whole time.
The extend path overflows the same expression (vfs_vm.c:464 via
nvextendbuf() at line 385) making object->size garbage-huge, which is
harmless until the later truncate hits the loop.
Fix validation
fix.diff (against sys/kern/vfs_vm.c): clamp truncloffset to OFF_MAX,
compute nobjsize/pi in unsigned 64-bit. Kernel rebuilt; re-run of the
identical trigger completes instantly β see run.fixed.log.
Fix verification
fixedRebuilt kernel with fix.diff; the identical 3-syscall trigger completes instantly (all four steps print OK, process exits) instead of wedging the CPU permanently; system stays healthy (shutdown works).
['findings/poc/DF-2921/run.fixed.log', 'findings/poc/DF-2921/fix-build.log']
Confirmed kernel references
Detail
Exploit chain
unprivileged ftruncate to near-2^63 on HAMMER2 + 1-byte pwrite + mid-block truncate-down -> one CPU permanently spun in-kernel holding vnode+object locks -> collateral permanent hangs of every thread touching the vnode (rm, fsync, shutdown) -> repeatable per file to wedge all CPUs; guest must be power-cycled externally
Evidence (decisive lines)
['findings/poc/DF-2921/livelock-evidence.txt (R0 forever, kill -9 ineffective, D-state second victim, blocked rm diagnostic, 3 hung shutdowns)', 'findings/poc/DF-2921/panic.txt (forced dump + console diagnostics)', 'findings/poc/DF-2921/run.fixed.log (all steps OK on patched kernel)']
PoC changes
First run lost stdout to ssh buffering while wedged; added setvbuf(_IONBF) and file redirection; background execution with nohup so the wedged process survives the ssh session.
Verified recommended fix
Clamp nvtruncbuf's truncloffset to OFF_MAX on would-be overflow and compute nobjsize/pi in nvnode_pager_setsize with unsigned 64-bit math; see fix.diff
Verdict
Reproduced live on the stock INVARIANTS kernel as unprivileged user maxx on HAMMER2: three syscalls (ftruncate up to 2^63-4096 sparse, pwrite of one byte at the top, ftruncate down to 2^63-32768 = mid-64K-block) drive the thread into nvnode_pager_setsize()'s unmap loop with an overflowed nobjsize of 0xFFF8000000000000 (signed wrap in truncboffset + blksize + PAGE_MASK, sys/kern/vfs_vm.c:464), spinning ~2^63 iterations while holding vp->v_token and the object lock. The thread never returns (R0, observed >5 min), kill -9 is ineffective, a second process touching the file parks forever in D state, rm hangs ([diagnostic] cache_lock_shared: rm blocked ... df2921.bin on the serial console), and three shutdown -p attempts hang - the guest is only recoverable by killing QEMU. nvtruncbuf's truncloffset (vfs_vm.c:149) overflows identically to INT64_MIN, making the RB_SCAN destroy every buffer on the vnode including dirty ones. The kernel is compiled -fno-strict-overflow so the wraparound is implemented behavior, and the overflow math is exact (HAMMER2 always passes 64K via hammer2_calc_logical). Patched kernel (fix.diff: clamp truncloffset to OFF_MAX, compute nobjsize/pi in unsigned 64-bit) completes the identical trigger instantly with all steps OK.
No comments yet.