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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2921 Β· 10 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download raw

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

  1. Process enters kernel and spins in state R0 forever (first run pid 1025, observed > 5 minutes, still R0).
  2. kill -9 ineffective (the loop never returns to userland; lwkt_yield() does not process signals).
  3. A second process touching the same file blocks forever (D5) behind the spinner's vp->v_token (taken at vfs_vm.c:153) and vm_object lock.
  4. rm of the file hangs: serial console shows [diagnostic] cache_lock_shared: rm blocked on 0xfffff8008f54dc00 "df2921.bin".
  5. Three shutdown -p now attempts all hung; guest only recoverable by killing QEMU. A crash dump was taken via debug.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 with B_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: pi starts at ~2^51 and the loop runs to 0xFFF8000000000000 β€” ~1.8Γ—10^19 iterations of vm_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

fixed
baseline reproduced→ patch + rebuild →patched clean

Rebuilt 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']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 3 12:10:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.