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)
PoC verification
Evidence pack
findings/poc/DF-3209 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_getnever 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 != 0always 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)
- 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). - tcpdump on the server:
tcpdump -s0 -X udp port 111 or port 2000-3000during client boot. - 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 β 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)
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).sys/vfs/nfs/krpc_subr.c:466-467:dlen = (len + 3) & ~3; mlen = dlen + 4;β XDR string = 4-byte length + padded data.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.sys/vfs/nfs/krpc_subr.c:475-476β onlylenbytes are written (xs->len = txdr_unsigned(len); bcopy(str, xs->data, len);). Bytesxs->data[len .. dlen)(0β3 bytes) are uninitialized.sys/vfs/nfs/krpc_subr.c:474βm->m_len = mlen;β the datagram includes the uninitialized tail.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.
Recommended fix
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_testableConfirmed 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.
No comments yet.