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

xdr_string_encode() transmits 1-3 uninitialized kernel-heap bytes (XDR string padding) in every boot mountd MOUNT / NFS LOOKUP request

Summary

The boot-RPC string builder (krpc_subr.c:466-477) allocates its mbuf without M_ZERO, copies only the strlen(path) payload, but sets m_len to the 4-byte-padded XDR length and sends it: the 1-3 stale freelist bytes after any non-4-aligned path string are disclosed to the NFS/mountd server and any on-path LAN observer. Only callers: md_mount (v3 + v2 fallback) and md_lookup_swap. Boot-time window only, up to ~3 requests per boot; low sensitivity but a textbook uninitialized-memory disclosure and a one-line fix (bzero the pad tail). Lab repro in pack README (PXE + tcpdump); not runtime-testable on the disk-root audit guest - same ceiling as DF-3203/3204/3205.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3209 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.2 KB ↓ raw
VERDICT.md β€” 3.4 KB ↓ raw
verdict.json β€” 2.4 KB view raw
manifest.json β€” 845 B view raw
fix.diff β€” 371 B view raw

DF-3209 β€” xdr_string_encode() transmits uninitialized kernel-heap bytes (XDR pad) in every boot mountd/NFS LOOKUP request

File: sys/vfs/nfs/krpc_subr.c:458-477 (builder), called only from sys/vfs/nfs/nfs_mountrpc.c:143,160,245.

What

xdr_string_encode() allocates an mbuf without M_ZERO (m_getl(mlen, M_WAITOK, MT_DATA, 0, NULL) β€” flags=0), copies exactly len string bytes in, sets the transmitted length to the padded mlen = ((len+3)&~3)+4, and never initializes the [len, dlen) tail. Those 0–3 stale kernel-heap bytes are sent on the wire by sosend() inside krpc_call() (krpc_subr.c:322).

Why it is certain (no execution needed)

  • m_getl(len, how, type, flags=0, psize) β†’ m_get(how, type) for mlen < MINCLSIZE (sys/sys/mbuf.h:584-602); m_get never touches the data area β€” contents are whatever the mbuf last carried on the freelist.
  • m->m_len = mlen (krpc_subr.c:474) β€” the whole padded region is part of the datagram.
  • XDR strings with len % 4 != 0 always have 1–3 pad bytes; typical root paths ("/client/root", "/export/root") are not 4-aligned, so the pad window is usually non-empty.

Ceiling (why status=untested)

The only callers are the boot-path mount RPCs (#if defined(BOOTP)||defined(NFS_ROOT)). On the audit guest the code is compiled in (X86_64_GENERIC:29 options NFS_ROOT) but the guest boots hammer2 from disk, so nfs_mountroot β†’ md_mount/md_lookup_swap never executes, and no runtime syscall path reaches them (nfs_mountdiskless is static, nfs_vfsops.c:724, called only from nfs_mountroot). Exercising it requires a loader-configured NFS root + server + reboot. Same not-runtime-testable ceiling as DF-3203/3204/3205.

Reproduce (on a lab PXE/diskless client)

  1. Configure a diskless client whose root path length is not a multiple of 4 (e.g. 10.0.0.1:/tftpboot/clientroot β†’ path string "clientroot" len 10 β†’ 2 pad bytes).
  2. tcpdump on the server: tcpdump -s0 -X udp port 111 or port 2000-3000 during client boot.
  3. The MOUNT/LOOKUP request's XDR string tail bytes after the string (and before the next 4-aligned field) contain uninitialized client kernel heap, varying across boots (freelist state).
VERDICT.md
↓ download raw

VERDICT β€” DF-3209

status: untested (boot-path-only code; not runtime-testable on the audit guest β€” same ceiling as DF-3203/3204/3205) impact: leak (limited: 0–3 uninitialized kernel-heap bytes per boot RPC, boot-time window) confidence: certain (pure source/arithmetic proof, no execution required)

Proof chain (path:line)

  1. sys/vfs/nfs/nfs_mountrpc.c:143 β€” m = xdr_string_encode(path, strlen(path)); (v3 mountd MOUNT request); :160 (v2 fallback request); :245 (NFSPROC_LOOKUP swap path). These are the only callers in the tree (rg -l xdr_string_encode sys/ β†’ krpc.h declaration + krpc_subr.c definition + this file).
  2. sys/vfs/nfs/krpc_subr.c:466-467: dlen = (len + 3) & ~3; mlen = dlen + 4; β€” XDR string = 4-byte length + padded data.
  3. sys/vfs/nfs/krpc_subr.c:472 β€” m = m_getl(mlen, M_WAITOK, MT_DATA, 0, NULL); β€” flags=0, no M_ZERO. sys/sys/mbuf.h:584-602 β€” m_getl(..., flags=0, ...) β†’ m_get(how, type) (plain MT_DATA mbuf from the freelist, data area untouched). Cluster path (m_getcl) also does not zero.
  4. sys/vfs/nfs/krpc_subr.c:475-476 β€” only len bytes are written (xs->len = txdr_unsigned(len); bcopy(str, xs->data, len);). Bytes xs->data[len .. dlen) (0–3 bytes) are uninitialized.
  5. sys/vfs/nfs/krpc_subr.c:474 β€” m->m_len = mlen; β€” the datagram includes the uninitialized tail.
  6. sys/vfs/nfs/krpc_subr.c:276 (mhead->m_next = *data), :300-306 (pkthdr.len includes it), :317-323 (m_copym + sosend(so, sa, ..., m, ...)) β€” the request, including the stale pad bytes, is transmitted as one UDP datagram to the mountd/NFS server (and observable by any on-path LAN device).

Why untested rather than reproduced

The execute path requires nfs_mountroot() at boot with an NFS root. Audit guest (DragonFly 6.5-DEVELOPMENT

0, X86_64_GENERIC, vbd0s1d on / (hammer2, local)) boots from disk; NFS_ROOT is compiled in

(sys/config/X86_64_GENERIC:29) but md_mount/md_lookup_swap are unreachable at runtime (static callers only: nfs_vfsops.c:724 nfs_mountdiskless ← nfs_vfsops.c:557 nfs_mountroot ← kernel mount-root machinery). No unprivileged (or any) runtime user can reach the builder.

Impact assessment

  • Unauthenticated LAN observer or the configured server receives 0–3 bytes of stale kernel heap per request, up to ~3 requests per boot (v3 mount, maybe v2 fallback mount, swap LOOKUP). Boot-time window only.
  • Contents: whatever the recycled mbuf last held (early-boot network traffic / loader data) β€” low sensitivity, but a textbook CWE-908 (uninitialized resource) disclosure and trivially fixable.

Zero the pad tail (or allocate with M_ZERO):

--- a/sys/vfs/nfs/krpc_subr.c
+++ b/sys/vfs/nfs/krpc_subr.c
@@ -473,6 +473,7 @@ xdr_string_encode(char *str, int len)
    m = m_getl(mlen, M_WAITOK, MT_DATA, 0, NULL);
    xs = mtod(m, struct xdr_string *);
    m->m_len = mlen;
    xs->len = txdr_unsigned(len);
    bcopy(str, xs->data, len);
+   bzero(xs->data + len, dlen - len);  /* do not leak stale pad bytes */
    return (m);
 }

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
↓ fix.diffper-fix-DF-3209

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3209/VERDICT.md (6-step path:line proof); decisive lines: krpc_subr.c:472 (m_getl flags=0, no M_ZERO), :474 (m->m_len=mlen includes pad), :476 (bcopy of only len bytes), mbuf.h:584-602 (m_getl->m_get does not zero); callers nfs_mountrpc.c:143,160,245

PoC changes

none β€” seed sketch not executable: builder only runs on the boot path; verdict derived from source arithmetic (dlen=(len+3)&~3 vs bcopy len) plus verified m_getl non-zeroing semantics

Verified recommended fix

bzero the XDR pad tail in xdr_string_encode (bcopy(str, xs->data, len); bzero(xs->data+len, dlen-len)) or allocate with M_ZERO

Verdict

xdr_string_encode() allocates its request mbuf without M_ZERO, copies only the strlen(path) payload bytes, but sets m_len to the 4-byte-padded XDR length and transmits it via sosend(); the 1-3 stale kernel-heap pad bytes after every non-4-aligned path string leak to the mountd/NFS server and any on-path LAN observer. Proven by inspection (krpc_subr.c:466-477, mbuf.h m_getl flags=0 path); not runtime-testable because the only callers are the #if BOOTP||NFS_ROOT boot-mount RPCs (nfs_mountrpc.c:143,160,245) and the audit guest boots from disk, so nfs_mountroot never executes β€” same boot-path ceiling as DF-3203/3204/3205.