DF-2921 / livelock-evidence.txt
DF-2921 — unprivileged permanent kernel livelock (unkillable) via signed
overflow in nvnode_pager_setsize()/nvtruncbuf()
OBSERVED ON GUEST (DragonFly 6.5-DEVELOPMENT #0, stock INVARIANTS kernel,
hammer2 root), triggered as unprivileged user "maxx":
1. run: /tmp/op2921b (source: op2921.c)
- ftruncate(fd, 0x7ffffffffffff000) [extend, sparse, 8 EB]
- pwrite 1 byte @ 0x7fffffffffffefff [dirty the last 64K block]
- ftruncate(fd, 0x7fffffffffff8000) [truncate mid-64K-block]
2. Process enters state R0 (running on cpu) inside the kernel and never
returns:
PID STAT WCHAN COMMAND
1025 R0 - /tmp/op2921x <- first run, observed >5 min
3. kill -9 has NO effect (syscall never returns; lwkt_yield() does not
check signals). Verified twice, >5 minutes each time.
4. A second process touching the same file blocks forever behind the
spinner's vp->v_token / vm_object lock:
944 R0 /tmp/op2921b
953 D5 /tmp/op2921b
5. Even `rm` of the file hangs (serial console):
[diagnostic] cache_lock_shared: rm blocked on 0xfffff8008f54dc00 "df2921.bin"
6. Guest can no longer be shut down: three `shutdown -p now` attempts
logged and all hung; guest only recoverable by killing QEMU.
7. A forced crash dump was taken (debug.panic=1) after first wedging.
ROOT CAUSE (sys/kern/vfs_vm.c):
- nvtruncbuf() line 149:
info.truncloffset = length + (blksize - boff);
with length = 0x7fffffffffff8000, H2 nblksize = HAMMER2_PBUFSIZE = 64K
(hammer2_calc_logical() always returns HAMMER2_PBUFSIZE,
sys/vfs/hammer2/hammer2_subr.c:276), boff = 0x8000:
0x7fffffffffff8000 + 0x8000 = 0x8000000000000000 (signed overflow)
-> truncloffset = INT64_MIN -> nvtruncbuf_bp_trunc_cmp() matches EVERY
buffer on the vnode (including negative-loffset metadata buffers on
UFS) -> all destroyed with B_INVAL (silently discards dirty buffers).
- nvnode_pager_setsize() line 464:
nobjsize = OFF_TO_IDX(truncboffset + blksize + PAGE_MASK);
truncboffset = 0x7fffffffffff0000; + 0x10000 + 0xfff =
0x8000000000000fff -> negative -> arithmetic >> 12 -> vm_pindex_t
0xFFF8000000000000.
- unmap loop line 486-495:
pi = OFF_TO_IDX(length + PAGE_MASK); /* ~ 2^51 */
while (pi < nobjsize) { /* nobjsize ~ 2^63.9 */
vm_page_lookup_busy_wait(...); ++pi; lwkt_yield();
}
~1.8e19 iterations, each an RB-tree miss + yield -> years, while
holding vp->v_token (acquired at nvtruncbuf:153) and the vm_object
hold -> everything touching the vnode blocks forever.
REACHABILITY: unprivileged local user on any filesystem whose nvtruncbuf
block size can straddle a length within blksize of 2^63 (hammer2 always
passes 64K; tmpfs/ufs/nfs unreachable because their max file sizes are far
below; ext2fs/hpfs use the old vnode_pager_setsize path). HAMMER2 imposes
no va_size clamp in setattr (hammer2_vnops.c:512-525) and ftruncate()
accepts any non-negative off_t.