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

krpc_call has zero-entropy transaction IDs and no reply-source filtering: deterministic xid (0xFFFFFF00+n), deterministic reserved-port sweep, and an unconnected socket whose from-address is discarded β€” any LAN host can blindly forge boot-critical RPC replies and become the diskless client's NFS/portmap authority

Summary

The ONLY filter applied to RPC replies is the xid comparison at :380. The xid sequence is 'static u_int32_t static_xid = ~0xFF' (:201) incremented by 1 per call - the first RPC of every diskless boot uses xid 0xFFFFFF00 with no randomness anywhere, and the client UDP port is a deterministic sweep from 1023 downward (:256-262). krpc_call never connect()s the socket and, for portmap/mount calls (from_p==NULL), soreceive's from-address is freed and discarded - so replies from ANY source address are accepted. Consequences: (a) any host on the boot LAN, with no IP spoofing, can answer GETPORT/MOUNT/LOOKUP RPCs by sending to client:1023 with the guessable xid; (b) a forged portmap GETPORT reply hands the client an attacker-chosen mountd/NFS port -> the attacker serves the entire root filesystem -> full compromise of the booting machine; (c) this is the off-path delivery vector that makes DF-0900 (verifier-length mbuf corruption), DF-3203 and DF-3204 trivially triggerable by third parties. Secondary: the resend loop has no global deadline and received-but-rejected replies do not decrement secs. Attack window: boot/pxeboot phase of NFS_ROOT kernels (stock X86_64_GENERIC). NetBSD fixed the same pattern by seeding xid from cprng. Phase V not_testable without a root-on-NFS rig; deterministic counter directly readable at :201. Fix: seed xid from karc4random + validate reply source address. [VERIFIED pass2: reproduced through verbatim production code over the real UDP stack - first-call xid 0xffffff00 predicted blind from source constants 3 boots, deterministic port-1023 sweep, wrong-origin reply from 127.0.0.2 accepted with soreceive source discarded, blind forged portmap GETPORT returned the attacker-chosen port; boot-path only on disk-root guests, deployment ceiling is attacker-served diskless root; karc4random xid plus origin-equality fix-validated - random xids, spoof dropped, blind pmap dead]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3205 Β· 26 files
FileTypeDescriptionSize
README.md β€” 3.9 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
dfrig13_krpc.c β€” 10.8 KB view raw
attacker.c β€” 7.6 KB view raw
build.sh β€” 194 B view raw
run.sh β€” 370 B view raw
blindboot.sh β€” 799 B view raw
run_fixed.sh β€” 2.8 KB view raw
run.log β€” 1.7 KB view raw
run.blindboot.log β€” 369 B view raw
run.fixed.log β€” 3.1 KB view raw
dump.log β€” 704 B view raw
spoof.log β€” 189 B view raw
pmap.log β€” 148 B view raw
pmap_blind.log β€” 147 B view raw
fx_dump.log β€” 704 B view raw
fx_spoof.log β€” 189 B view raw
fx_pmap.log β€” 148 B view raw
build.log β€” 5.7 MB ↓ download
build_fixed.log β€” 5.7 MB ↓ download
fix.diff β€” 1.4 KB view raw
fix_all.diff β€” 2.3 KB view raw
krpc_subr.fixed.c β€” 12.8 KB view raw
env.txt β€” 4.3 KB view raw
manifest.json β€” 1.8 KB view raw
verdict.json β€” 5.2 KB view raw

DF-3205 β€” krpc_call zero-entropy XIDs + no reply-source filtering

File: sys/vfs/nfs/krpc_subr.c:201,:256-262,:282,:348-351,:380,:432-435 Severity: Medium Β· CWE-330 Β· verified in VERIFY mode on the QEMU guest with the verbatim-interposition kernel-compiled rig dfrig13_krpc.c (sha256-gated #include of the production krpc_subr.c; no socket seams β€” the rig calls the real krpc_call/krpc_portmap over the real UDP stack on lo0, and the attacker is a real userland process).

What was reproduced (baseline kernel #1)

  1. Blind predicted-XID spoof of the first RPC of a boot (blindboot.sh, run as the first rig activity after a fresh boot): the client call used xid 0xffffff00 β€” computed by the attacker purely from the source constants (static_xid = ~0xFF, krpc_subr.c:201, incremented once per call at :282) without ever reading a request payload β€” and the forged portmap GETPORT reply was accepted: krpc_portmap err=0 port bytes ca fe, i.e. the attacker chose the port the diskless client will contact next (the first step of becoming its NFS authority).
  2. Determinism dump (test 5): six sequential calls carried xids 0xffffff01..0xffffff06 (+1 per call, continuing the counter) and every request came from reserved UDP port 1023 (the deterministic sweep :256-262 rebinding the same free port per call).
  3. Wrong-origin acceptance (test 1): a reply sent from 127.0.0.2 (not the server address in the request) was accepted β€” payload ATTA… from ATTACKER-PAYLOAD. krpc_call never connect()s the socket and frees/discards the soreceive source address (:348-351, :432-435); the only content filter is the xid compare at :380.
  4. End-to-end portmap authority hijack (test 4): spoofed GETPORT reply β†’ client adopts the attacker-chosen port (ca fe).
  5. Live side note: the resend loop (:315 for(;;)) has no global deadline β€” a run whose attacker stops replying spins forever printing RPC timeout for server 127.0.0.1 every ~5 s (observed twice).

Fix validation (kernel #2, fix_all.diff; single-finding hunk = fix.diff)

Same tests on the fixed kernel: xids are karc4random()-backed and non-sequential (0x07d0b1bd, 0x01e5b8d7, 0xb88a504a, …; blind predictions all differ), the wrong-origin reply from 127.0.0.2 is dropped (call completes on the legitimate-origin follow-up with payload SERV…), the blind portmap spoof is rejected (client takes the follow-up answer, port bytes 77 77), and honest replies still work (6/6, no regression). See run.fixed.log / fx_*.log.

Reproduce (on the DragonFly guest)

sh build.sh          # verbatim sha256 gate + make nativekernel + installkernel; reboot
sh blindboot.sh      # decisive: first-call-of-boot blind spoof (fresh boot only!)
sh run.sh            # determinism dump + wrong-origin + portmap (sniffed) tests
# fix validation:
sh build.sh af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886 ; reboot
sh run_fixed.sh

Files in this folder

file purpose
dfrig13_krpc.c verbatim-interposition kernel harness (all rig tests)
attacker.c userland malicious RPC server (dump/spoofsrc/short/short_p/pmis/pmap, -b blind)
build.sh / run.sh / blindboot.sh / run_fixed.sh exact build & run commands
run.log / run.blindboot.log baseline decisive outputs
run.fixed.log fixed-kernel A/B outputs
dump.log spoof.log pmap.log pmap_blind.log attacker-side logs (xids, client ports)
fx_dump.log fx_spoof.log fx_pmap.log fixed-kernel attacker-side logs
build.log / build_fixed.log FULL untrimmed kernel build logs (gate lines included)
fix.diff / fix_all.diff / krpc_subr.fixed.c validated fixes (single-finding / combined / full fixed file)
env.txt guest, compiler, gate hashes, reachability notes
VERDICT.md / manifest.json / verdict.json narrative + machine verdict
VERDICT.md
↓ download raw

DF-3205 β€” VERDICT (verified 2026-10-20, VERIFY mode)

REPRODUCED β€” every element of the finding demonstrated on the guest through the verbatim production code (sha256-gated #include of krpc_subr.c compiled into kernel #1; rig drives the real UDP stack; attacker = userland process).

  1. Zero-entropy XID, proven blind krpc_subr.c:201 static u_int32_t static_xid = ~0xFF; and :282 xid = atomic_fetchadd_int(&static_xid, 1);. On a fresh boot the first krpc call of the machine used xid 0xffffff00 β€” predicted by the attacker from the source constants alone (attacker flag -b: it never reads the request payload; blindboot.log: "REQ #1 ... xid=0xffffff00 ... blind prediction for this call: 0xffffff00 MATCH"). The subsequent counter sequence 0xffffff01..06 was observed one-per-call (dump.log), and the coupling run (run.couple.log, another fresh boot) matched predictions 3/3. NetBSD seeds this counter from cprng; DragonFly does not.

  2. No reply-source filtering krpc_call never connect()s the UDP socket; the only content filter is the xid compare at :380. soreceive captures the reply origin into from, but :348-351 frees it every loop iteration and :432-435 only hands it out for broadcast callers β€” the real callers (md_mount, md_lookup_swap in nfs_mountrpc.c:146,163,253,256) all pass from_p == NULL. Demonstrated: a reply from 127.0.0.2 (not the requested server 127.0.0.1) was accepted, payload ATTACKER-PAYLOAD (run.log T1 marker "err=0 payload='ATTA'"; attacker log "spoof sent from 127.0.0.2").

  3. Deterministic reserved-port sweep :256-262 sweeps 1023 downward; every request of every run came from UDP port 1023 (all attacker logs), because each call rebinds the same first free reserved port. Prediction of the client endpoint is therefore trivial.

  4. Authority hijack end-to-end krpc_portmap (krpc_subr.c:127-178) hands its caller the port from whatever reply krpc_call accepted: the blind spoof made the verbatim code return the attacker-chosen bytes (err=0, port bytes ca fe; blindboot.log). In the boot scenario (md_mount -> krpc_portmap for MOUNT, then krpc_call to the returned port) this is precisely the primitive that lets a third party on the boot LAN serve the root filesystem of a diskless client. The rig stops at the portmap seam (md_mount itself is out of scope for this finding), so full root-FS adoption was not re-driven here; the identical chain was however demonstrated end-to-end for the sibling BOOTP rig DF-3044 (bootpc_decode_reply -> md_mount adoption markers).

  5. Resend loop with no global deadline (secondary, observed live) When the attacker stops answering, the for(;;) at :315 resends forever, printing "RPC timeout for server ..." about every 5 s (observed twice; the stuck sysctl thread never returns). The finding notes this; no extra test was needed.

Impact / privesc assessment On the standard disk-root guest there is NO runtime path to krpc (callers: nfs_mountrpc.c only, reached from nfs_mountroot at nfs_vfsops.c:557/943 during diskless mountroot), so this is not a local unpriv->root escalation here. The threat is the boot network: any LAN host (no IP spoofing needed β€” delivery is to the client 1023/UDP endpoint, xid guessable, origin unchecked) becomes the portmap/mountd/NFS authority of a booting diskless client, which in deployment means attacker-chosen root filesystem = full compromise of the client. In-rig the demonstrated manifestation is acceptance of the forged authority (integrity) plus the infinite-resend hang (availability); the poc impact enum has no auth-bypass value, so the closest manifested value is recorded ("dos") with this ceiling documented β€” same treatment as the DF-3044 BOOTP sibling.

Fix validation (kernel #2 = baseline + fix_all.diff, gate sha af834f6e30225b1af68cc83cbcdaf25e29e0e39ca6deaec6f3c13663a661b886) - xid = karc4random() (fix hunk 1): fx_dump.log shows 0x07d0b1bd/0x01e5b8d7/0xb88a504a/0x6d5d8e23/0xe974921d/0x77a3ea43 β€” no sequence; blind predictions all "differ". - reply source must equal sa (address AND port) (fix hunk 2): the 127.0.0.2 spoof is dropped; T1 completes on the legitimate-origin follow-up (payload SERV). The blind pmap spoof is dropped; T4 completes on the follow-up answer (port bytes 77 77). - no regression: honest replies still complete 6/6 (T5) and the pmis/control loops behave as on baseline for their own findings.

Rig honesty notes - The blind attacker still learns the client UDP endpoint from recvfrom() β€” equivalent to a boot-LAN sniffer seeing the unicast request go by; the quantity the xid is supposed to protect (the ability to forge a reply without observing the request) is what was defeated, and on the baseline it did not exist. - "usched :1" pins rig and attacker to CPU0 for objcache magazine locality (DragonFly has no cpuset(1)); 127.0.0.2 requires an lo0 alias (scripts add it).

Artifacts: run.blindboot.log (decisive blind acceptance), run.log, dump.log/spoof.log/pmap.log (attacker view), run.fixed.log + fx_*.log (A/B), build.log/build_fixed.log (full builds + gate lines), env.txt.

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-3205

Confirmed kernel references

Detail

Exploit chain

boot-LAN attacker predicts xid = 0xFFFFFF00 + n from source constants and knows the client endpoint (reserved port 1023 sweep, observed on the wire); sends a forged portmap GETPORT reply with matching xid from any origin; verbatim krpc_call accepts it (no source check, xid is the only filter); krpc_portmap returns the attacker-chosen mountd/NFS port; md_mount then mounts the diskless client root from the attacker (root filesystem authority = full client compromise); variant: attacker stops replying mid-boot and the for(;;) resend loop hangs the boot RPC forever

Evidence (decisive lines)

run.blindboot.log: REQ #1 xid=0xffffff00 blind prediction MATCH + marker krpc_portmap err=0 port bytes ca fe; run.log: T5 6/6 with dump.log xids 0xffffff01..06 all from 127.0.0.1:1023, T1 err=0 payload ATTA with spoof.log spoof sent from 127.0.0.2, T4 ca fe; run.fixed.log: T1 payload SERV, T4 port bytes 77 77, fx_dump.log xids 0x07d0b1bd 0x01e5b8d7 0xb88a504a 0x6d5d8e23 0xe974921d 0x77a3ea43 with blind predictions all differ; build.log/build_fixed.log VERBATIM GATE lines

PoC changes

replaced the assumed-impossible diskless-boot lab with the established verbatim-interposition kernel-compiled rig (stock guest boots from disk; krpc is boot-path only): dfrig13_krpc.c renames the three external symbols of krpc_subr.c and includes the file verbatim, driving the REAL UDP stack on lo0 with a REAL userland attacker server; added usched :1 CPU pinning for objcache locality (DragonFly has no cpuset(1)) and an lo0 127.0.0.2 alias (binds are refused without it); the blind attacker learns only the client endpoint via recvfrom (equivalent to a boot-LAN sniffer) and predicts the xid purely from source constants

Verified recommended fix

xid = karc4random() instead of the ~0xFF counter and require the reply source address and port to equal the requested server before parsing (validated in fix.diff)

Verdict

All claims demonstrated on the guest through the verbatim production code (sha256-gated include of krpc_subr.c compiled into the kernel; the rig calls the real krpc_call and krpc_portmap over the real UDP stack, the attacker is a userland process): the first krpc call of a fresh boot used xid 0xffffff00, predicted by the attacker purely from the source constants (static_xid = ~0xFF at krpc_subr.c:201, +1 per call at :282, never reading the request payload), the client bound reserved UDP port 1023 every call (deterministic sweep :256-262), replies from an address that is not the requested server were accepted because the socket is never connect()ed and the soreceive source is freed and discarded (:348-351, :432-435; only the xid at :380 is checked), and a blind forged portmap GETPORT reply made the verbatim krpc_portmap return an attacker-chosen port (err=0, port bytes ca fe) -- the first step of serving a diskless client its root filesystem. The resend loop with no global deadline was also observed live (eternal RPC timeout spam when the attacker stops answering). uid=0 is not reachable on this disk-root guest because krpc has no runtime callers (nfs_mountrpc.c only, reached from nfs_mountroot during diskless mountroot); the real-world ceiling is full compromise of a root-on-NFS booting machine by any host on its boot LAN, and the poc impact enum has no auth-bypass value so the manifested availability effect is recorded. The validated fix (karc4random xid + require reply source address and port to equal the requested server) killed the blind prediction, the wrong-origin acceptance and the portmap hijack in A/B runs with no regression.