DF-3205 / env.txt
Guest (single-tenant QEMU/KVM rig, owned for the whole verification run)
DragonFly dfbsd 6.5-DEVELOPMENT x86_64; baseline decisive boot
"DragonFly 6.5-DEVELOPMENT #1: Tue Oct 20 17:57:48 UTC 2026" (harness
kernel over the audited-verbatim krpc_subr.c); fixed decisive boot
"DragonFly 6.5-DEVELOPMENT #2: Tue Oct 20 18:25:39 UTC 2026" (same
harness over the fix_all.diff variant). Stock config X86_64_GENERIC
WITH INVARIANTS (options NFS_ROOT at sys/config/X86_64_GENERIC:29;
krpc_subr.c compiled in via "vfs/nfs/krpc_subr.c optional nfs").
CPU: QEMU Virtual CPU (6 vCPUs), 4 GB RAM.
Compiler: cc 8.3 [DragonFly] Release/2019-02-22; kern.osreldate=600519.
Rig (verbatim interposition, kernel-compiled; no socket seams)
- /usr/src/sys/vfs/nfs/dfrig13_krpc.c (pack copy = the exact file
compiled into the guest kernel; sha256
7e1e3ce06a6846a1b942d9d9f0098be5685fe36980db2620828f691ffb429520)
#defines the three external symbols of the production source
(krpc_portmap/krpc_call/xdr_string_encode -> dfx_*) and then
#includes krpc_subr.c VERBATIM. Everything under test -- the
static_xid = ~0xFF counter, the reserved-port sweep, the unconnected
socket + discarded reply source, the residual-length dead gate, the
pre-pullup rp_status read, the PROGMISMATCH exit -- is the
production code, byte for byte. The rig drives the REAL kernel UDP
stack over lo0 with REAL mbufs; the attacker is the userland
process attacker.c (sha256
d7337b8717b8d4edb990f51ff612ea099085970756a8314fbbe9ab328c0507f1)
playing the malicious boot-network host.
- /usr/src/sys/conf/files: one added line
"vfs/nfs/dfrig13_krpc.c optional nfs"
- built with: cd /usr/src && make -j6 nativekernel
KERNCONF=X86_64_GENERIC && make installkernel (build.sh performs
the sha256 verbatim gate FIRST and refuses to build otherwise).
- verbatim gate hashes:
baseline sys/vfs/nfs/krpc_subr.c:
284e77ed3ba5c8f640e4831f7a43f6a78fbcde363e86ded82a7088b7796e5d63
fixed (baseline + fix_all.diff, all three findings):
af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886
- trigger: sysctl -w kern.dfrig13_arg=<arg>; sysctl -w kern.dfrig13=
<test>; markers via sysctl -n kern.dfrig13_status; verbatim console
lines ("rpc denied, status=N", "RPC timeout for server ...") in
dmesg. CPU pinning with "usched :1 <cmd>" aligns the groom/attack
allocations to one objcache magazine (DragonFly has no cpuset(1)).
- 127.0.0.2 alias: "ifconfig lo0 127.0.0.2 netmask 255.0.0.0 alias"
(run scripts do this themselves; DragonFly lo0 does not accept
arbitrary 127/8 binds without the alias).
Reachability (honest scope)
- krpc_call/krpc_portmap have exactly two call sites, both in
sys/vfs/nfs/nfs_mountrpc.c (md_mount :140-163, md_lookup_swap
:202-256), reached only from nfs_mountroot (nfs_vfsops.c:557,
mountroot hook :943) and the BOOTP netboot path -- i.e. only while
a root-on-NFS (diskless) client boots. The stock guest boots from
disk, so the production path never runs at runtime; the rig drives
the same code directly. Attacker model: a malicious or compromised
host on the boot network (or the portmap/mountd/NFS server itself).
- No runtime syscall path reaches krpc on a disk-root machine (rg
over callers verified), so none of the three findings is a local
unpriv->root escalation on this guest; privesc assessment is in
each VERDICT.md.
Notable rig facts
- The infinite resend loop (krpc_subr.c:315 for(;;) with no global
deadline) was observed live twice when an attacker mode served
fewer requests than the harness attempts: the sysctl thread spins
forever printing "RPC timeout for server 127.0.0.1" ~every 5 s
(documented in the DF-3205 finding summary; recover by reboot).
- DF-3204 mbuf accounting via "netstat -m | head -1" deltas is exact:
50 PROGMISMATCH replies -> +50 mbufs that stay; honest control 0.
- DF-3203 stale-byte attribution: kernel-side groom (uniform byte
fill of freshly allocated plain+pkthdr mbufs, then freed) and a
purely userland "primer" (24-byte datagrams to a dead port carry
the pattern at offset 20..23; their freed mbuf slots keep it).
The primer is the stronger artifact: the attacker alone plants the
exact 4 bytes the kernel then reads out of bounds and prints.