DragonFlyBSD Kernel Audit
DF-0768 / panic.txt
← back to finding ↓ download raw
== 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.