DF-0768 / panic.txt
== DF-0768 panic signature (from dfbsd-qemu/boot.log, unpatched #0 kernel) ==
Trigger: unprivileged `ls -f /mnt` (getdents) as maxx (uid 1001) against a
malicious NFSv3 server mounted with `-o rdirplus` at /mnt. The malicious
READDIRPLUS reply contains one entry whose `name_attributes` has
attributes_follow = 0, immediately followed by the u32 0x7FFFFFFD, which the
client's buggy "else" branch in nfs_readdirplusrpc_uio()
(sys/vfs/nfs/nfs_vnops.c:2924-2928) reads as the (mythical) file-handle
length `i`:
NULLOUT(tl = nfsm_dissect(&info, NFSX_UNSIGNED));
i = fxdr_unsigned(int, *tl); /* i = 0x7FFFFFFD */
ERROROUT(nfsm_adv(&info, nfsm_rndup(i))); /* nfsm_rndup overflows to INT_MIN */
nfsm_rndup(a) = ((a)+3) & ~3 -> for a=0x7FFFFFFD: (0x80000000) & ~3 = 0x80000000
= INT_MIN (-2147483648). nfsm_adv(info, INT_MIN) does:
n = mtod(md)+md->m_len - dpos; /* small positive (bytes left in cluster) */
if (n >= len) /* n >= INT_MIN -> TRUE (positive >= negative) */
dpos += len; /* dpos += INT_MIN -> 2 GiB BELOW real data */
...returning success. The next nfsm_dissect(4) computes
n = (cluster_end - corrupted_dpos) which truncates to a NEGATIVE int, so it
routes to nfsm_disct(). Because the reply was padded to span >=2 mbuf
clusters, mp->m_next != NULL and nfsm_disct takes the mbuf pull-up branch:
MGET(mp2, ...);
mp->m_len -= left; /* left<0 -> m_len grows huge */
bcopy(*dposp, p, left); /* (size_t)left ~= 2^64 -> copies forever
from the wild dpos -> page fault */
Fatal trap 12: page fault while in kernel mode
cpuid = 3; lapic id = 3
fault virtual address = 0xfffff80016c7fff8
fault code = supervisor read data, page not present
instruction pointer = 0x8:0xffffffff80bcab4f
stack pointer = 0x10:0xfffff80117a8b2b0
frame pointer = 0x10:0xfffff80117a8b308
code segment = base 0x0, limit 0xffff, type 0x1b
= DPL 0, pres 1, long 0, def32 0, gran 1
processor eflags = interrupt enabled, resume, IOPL = 0
current process = 993
current thread = pri 10
kernel: type 12 trap, code=0
CPU3 stopping CPUs: 0x00000037
stopped
Stopped at memmove+0x24f: repe movsq (%rsi),%es:(%rdi)
db>
== interpretation ==
memmove+0x24f == the `repe movsq (%rsi),%es:(%rdi)` inside bcopy/memmove,
which is the `bcopy(*dposp, p, left)` in nfsm_disct()'s mbuf pull-up branch
(sys/vfs/nfs/nfsm_subs.c ~1352). %rsi = the corrupted dpos
(0xfffff80016c7fff8 -- note the 0xfffff800 high bits, ~2 GiB below the real
mbuf data at 0xfffff801..). The page fault is a supervisor READ at that wild
address -- a wild-pointer read, not a write. No write primitive -> no
privilege-escalation path; impact is local DoS / kernel panic.
The fault address 0xfffff80016c7fff8 ends in ...ff8, i.e. 8 bytes below a page
boundary (0x...000) -- consistent with bcopy reading 8-byte (movsq) words and
faulting at the start of an unmapped page just below the last mapped one.