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

BOOTP/DHCP reply acceptance has zero origin validation and a fully deterministic transaction XID β€” spoofed reply gives same-L2 attacker total control of the diskless client (root/swap NFS server, hostname, default route)

Field Value
ID DF-3044
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE CWE-345 / CWE-330
File sys/vfs/nfs/bootp_subr.c
Lines 595-642, 807, 838-848, 1142-1148, 1487, 1538, 1644-1666
Area vfs/nfs (netboot)
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:vfs
Reported pending
Known CVE none
CVE match variant (RFC 2131 Β§7 acknowledged gap; no RFC 3118)
Reported pending

Summary

The diskless-boot DHCP/BOOTP client accepts any UDP datagram landing on its unconnected *:68 socket as a valid reply. The entire acceptance test is xid==call.xid, hlen==, and bcmp(chaddr); soreceive is called with a NULL name so the source address and port are never inspected, and there is no giaddr/siaddr sanity check. Worse, the transaction id has zero entropy: gctx->xid is the constant 0xFFFF0000, advanced deterministically by 0x100 per interface and +1 per recompose, so an attacker can compute the exact XID of every query the client will ever send without ever seeing a packet. The only de-facto secret is the client MAC, which is broadcast in every DISCOVER on the local link. A same-L2 attacker races the legitimate server with a crafted OFFER/ACK, and the client then takes yiaddr/siaddr as its identity, mounts root (and optionally swap) from the attacker-chosen NFS server via md_mount, sets the attacker's hostname and default route β€” the client's entire operating system is supplied by the attacker: full pre-auth compromise of the netboot machine. This is the documented netboot threat model (RFC 2131 Β§7, no RFC 3118 authentication implemented) and DF-0802 already accepts the same-L2 attacker premise for its panics β€” but the deterministic XID plus absent source-port/address check removes even the weak off-link bar a randomized XID would have kept. Severity Medium: requires the non-default 'options BOOTP' diskless configuration (LINT64 only, absent from X86_64_GENERIC) and an adjacent attacker, but under that config impact is total client compromise β€” strictly worse than DF-0802's DoS. Not guest-testable (PXE harness; the audit guest boots from disk). Fix: karc4random() xid seed + require reply source port 67 (row diff).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of bootp_subr.c (GLM 5.3). DF-0802 re-verified, not re-reported (six panic consumers; the tag walk proven sound post-fix-analysis).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3044 Β· 17 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 5.0 KB ↓ raw
dfrig12_bootp.c β€” 31.8 KB view raw
build.sh β€” 251 B view raw
run.sh β€” 290 B view raw
run.log β€” 2.1 KB view raw
run.fixed.log β€” 3.3 KB view raw
panic.txt β€” 583 B view raw
panic_console_full.txt β€” 3.8 KB view raw
fix_validation.log β€” 2.9 KB view raw
fix.diff β€” 1.1 KB view raw
fix_combined.diff β€” 2.4 KB view raw
build.log β€” 5.7 MB ↓ download
build_fixed.log β€” 5.7 MB ↓ download
env.txt β€” 2.2 KB view raw
manifest.json β€” 2.0 KB view raw
verdict.json β€” 4.9 KB view raw

DF-3044 β€” BOOTP/DHCP reply acceptance: zero origin validation + deterministic XID

Verified in VERIFY mode (audit skipped) on the single-tenant QEMU guest, with the verbatim-interposition kernel-compiled harness (dfrig12_bootp.c, this directory) that #includes the unmodified production sys/vfs/nfs/bootp_subr.c (sha256-gated: build.sh refuses to build unless the guest source is byte-identical to the audited tree).

What was reproduced (baseline kernel)

  • Blind predicted-XID spoof, arbitrary origin, full client compromise of the netboot identity: a reply whose XID is computed purely from source constants β€” seed 0xFFFF0000 (bootp_subr.c:1538 gctx->xid = ~0xFFFF), +0x100 per interface (:435), +1 on the first compose of an unresolved ifctx (:1194-1195) β€” sent from 6.6.6.6:9999 (a port no DHCP server uses) is accepted by the verbatim bootpc_call matcher (:838-848) and the verbatim bootpc_decode_reply adopts the attacker's entire config: hostname, root NFS server+path, swap NFS server+path, default route. soreceive(so, NULL, ...) at :807 means the origin is never even requested (harness counter: 0 times).
  • DoS manifestation: the same predicted-XID ACK carrying TAG_SUBNETMASK with len 3 reaches bootpc_decode_reply and trips the verbatim panic("bootpc: subnet mask len is 3") (:1390) β†’ panic.txt.

Files

  • dfrig12_bootp.c β€” the harness (interposition seams: sockets, ifioctl, rtrequest, tsleep, setfs/nfs_mountopts/md_mount/md_lookup_swap, bcmp).
  • build.sh β€” verbatim sha256 gate + make nativekernel + installkernel.
  • run.sh β€” test selector (2 = blind spoof, 20 = wrong-xid control, 21/22 = fixed-kernel sniff variants, 4 = panic path, dedicated boot).
  • run.log β€” baseline test-2 console trace (verbatim Received DHCP Offer/Ack ... (accepted) lines + adopted-config decode line) + test-20 control status.
  • run.fixed.log β€” fixed-kernel runs (2/21/22 + test 4 without panic).
  • panic.txt β€” baseline test-4 panic + backtrace.
  • fix_validation.log β€” A/B table.
  • fix.diff β€” the validated fix (random XID seed + require source port 67).
  • build.log / build_fixed.log β€” full untrimmed kernel build logs.
  • env.txt β€” guest, compiler, rig, gate hashes.

Reproduce (on the DragonFly guest)

sh build.sh                      # gate + build + install (reboot after)
sysctl -w kern.dfrig12=2         # blind spoof   -> full adoption (baseline)
sysctl -w kern.dfrig12=20        # wrong-xid     -> ignored
sysctl -n kern.dfrig12_status    # verdict markers
# dedicated boot:
sysctl -w kern.dfrig12=4         # -> panic: bootpc: subnet mask len is 3

Expected (baseline)

dfrig12: client broadcasts query #1 xid_raw=0100ffff ...
dfrig12: ATTACKER injects OFFER xid_raw=0100ffff origin=6.6.6.6:9999 ...
Received DHCP Offer packet on vtnet0 from 6.6.6.6 (accepted) (no root path)
...
Received DHCP Ack packet on vtnet0 from 6.6.6.6 (accepted) (got root path)
vtnet0 at 66.6.6.6 server 6.6.6.6
subnet mask 255.255.0.0 router 1.6.6.6 rootfs 6.6.6.6:/pwn3d swapfs 6.6.6.6:/swpwn swapsize 65536 KB hostname pwn3d
dfrig12: DF3044: hostname ADOPTED 'pwn3d' ...
dfrig12: DF3044: root NFS mount ADOPTED from attacker 6.6.6.6 path=6.6.6.6:/pwn3d
dfrig12: DF3044: default route ADOPTED via attacker 6.6.6.1 flags=0x803

(with attacker constants stored big-endian; on-wire they appear as 66.6.6.6 / 6.6.6.6 / 1.6.6.6 β€” all attacker-chosen either way.)

VERDICT.md
↓ download raw

DF-3044 β€” VERDICT

Status: reproduced (mechanically, at every decision point of the verbatim production code), impact on this guest: panic (DoS manifestation), with the primary real-world impact β€” total identity/config compromise of a netboot client by an unauthenticated same-L2 (or XID-predicting off-path) attacker β€” demonstrated end-to-end through the verbatim path but not convertible to uid=0 on this guest because the guest boots from disk and its stock kernel does not compile this file (options BOOTP is LINT64-only; sys/conf/files:1894 gates bootp_subr.c on it).

What the run proved, line-accurately

  1. Deterministic XID β€” bootp_subr.c:1538 seeds gctx->xid = ~0xFFFF (0xFFFF0000), :429/:435 copies-and-increments per interface (+0x100), :1194-1196 adds +1 on the first compose of an unresolved ifctx and byte-swaps into call.xid. Guest observation: the DISCOVER and the subsequent DHCP REQUEST both used xid bytes 01 00 ff ff (0xFFFF0001); the xid only advances (+1) when a failed exchange resets the state to unresolved. The harness "attacker" computed this value from the source constants alone, never from an observed packet, and the client's first query matched the prediction exactly (client 1st xid_raw=0100ffff vs predicted 0100ffff: MATCH).
  2. Zero origin validation β€” :807-808 soreceive(so, NULL, &auio, ...) discards the source address/port; the entire acceptance test is op == BOOTP_REPLY (:831), xid == call.xid (:838), hlen == (:842), bcmp(chaddr) (:846-848). Replies from 6.6.6.6:9999 were accepted throughout the baseline runs; the harness counted 0 origin requests.
  3. Full compromise of the netboot identity β€” after acceptance, verbatim bootpc_decode_reply (:1341-1515) adopts attacker yiaddr/ siaddr/giaddr, TAG_HOSTNAME (strcpy(hostname, ...) :1487), TAG_ROOT/TAG_SWAP paths fed to setfs β†’ md_mount (:1644-1666), TAG_ROUTERS β†’ default route via rtrequest_global (:1142-1148). All adoption markers captured (run.log), including the verbatim decode console line vtnet0 at 66.6.6.6 server 6.6.6.6 ... hostname pwn3d. On a real diskless client the attacker-chosen root NFS server hands the machine its entire root filesystem β€” i.e. root-level control of the client by design of netboot, pre-authentication. (On the audit guest the mount is intercepted at the md_mount seam β€” no real RPC leaves the box.)
  4. Panic manifestation β€” a predicted-XID ACK with TAG_SUBNETMASK len 3 trips the verbatim panic("bootpc: subnet mask len is 3") (:1389-1391): panic.txt, backtrace bootpc_init <- sysctl_df_test <- sysctl_root <- userland_sysctl <- sys___sysctl. Same-L2 attacker β†’ repeatable boot-time crash of the netboot client (worse-case DoS at the worst possible time).

Honest reachability

  • Attacker = malicious DHCP/BOOTP server on the client boot network (AV:A at boot; the deterministic XID also removes the need to observe any packet β€” a blind spoofer who knows only the client MAC, which every on-link host hears in each broadcast DISCOVER, succeeds).
  • Requires the non-default options BOOTP diskless configuration (LINT64-only in this tree) β€” the stock X86_64_GENERIC guest kernel does not contain this code; that is exactly why the rig compiles the file verbatim into the harness kernel instead.
  • Unprivileged-local reach on a normal system: none (code runs at boot, before userland).
  • Privesc on THIS guest: not reachable (see above) β€” the escalation ceiling is on real netboot deployments, where it is total (attacker serves the root filesystem).

Fix validation (fix.diff, A/B on the same harness)

  • gctx->xid = karc4random() at :1550 β†’ blind prediction dead (3c53bacd vs predicted 0100ffff: differ).
  • soreceive(so, &fromp, ...) + sin_port == htons(67) requirement β†’ sniffed-XID spoof from :9999 rejected with the verbatim message bootpc: reply not from server port 67, ignored (run.fixed.log); test-4 panic gone (timeout, clean return).
  • Documented residual: a same-L2 sniffer spoofing source port 67 still controls the client (test 22) β€” inherent to unauthenticated BOOTP/DHCP (RFC 2131 sec 7; no RFC 3118); the fix removes the deterministic XID and the any-origin acceptance, not the on-link attacker.

Rig artifacts worth noting

  • Harness-only knob #define BOOTP_TIMEOUT 12 bounds the retry loop for negative tests (the shipped default retries forever; unrelated to the cited lines).
  • The production while (time_uptime < atimo) liveness relies on the real blocking soreceive (time_uptime is not volatile, sys/time.h:163); the stub models this with lwkt_yield() (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-3044

Confirmed kernel references

Detail

Exploit chain

malicious peer on the boot network computes xid = 0xFFFF0000 + 0x100*ifindex + 1 from source constants (no packet observation), knows the client MAC (broadcast in every DISCOVER), sends BOOTREPLY op=2 with matching xid/hlen/chaddr from any address and any port; verbatim matcher accepts; bootpc_received promotes state; bootpc_decode_reply adopts attacker yiaddr/siaddr/hostname/root path/swap path/router; bootpc_init mounts root and swap from the attacker NFS server via md_mount and installs the attacker default route; variant with malformed TAG_SUBNETMASK length 3 panics the client at boot (DoS)

Evidence (decisive lines)

run.log: verbatim console lines Received DHCP Offer packet on vtnet0 from 6.6.6.6 (accepted), Received DHCP Ack ... (got root path), decode line vtnet0 at 66.6.6.6 server 6.6.6.6 ... rootfs 6.6.6.6:/pwn3d ... hostname pwn3d, markers hostname ADOPTED / root NFS mount ADOPTED / default route ADOPTED via attacker / xid MATCH / origin requested 0 times; panic.txt: panic bootpc: subnet mask len is 3 with backtrace bootpc_init <- sysctl_df_test <- sysctl_root <- userland_sysctl; run.fixed.log: xid differ, bootpc: reply not from server port 67 ignored, test 4 without panic, test 22 residual documented

PoC changes

replaced the assumed-impossible PXE harness with a verbatim-interposition kernel-compiled rig (stock guest has no options BOOTP); corrected the prediction model after guest observation: bootpc_compose_query bumps xid only when the ifctx is unresolved, so the OFFER and ACK share the DISCOVER xid and only a failed exchange advances it; harness stub yields via lwkt_yield on the EWOULDBLOCK path because the production wait loop relies on the blocking soreceive to re-read the non-volatile time_uptime

Verified recommended fix

seed gctx->xid with karc4random() and require BOOTP replies to originate from UDP port 67 by passing a name to soreceive and checking sin_port (validated in fix.diff)

Verdict

Every claim of the finding was demonstrated on the guest through the verbatim production code (sha256-gated #include of bootp_subr.c compiled into the kernel): the reply XID is fully predictable from source constants (client DISCOVER and REQUEST both carried xid bytes 0100ffff = 0xFFFF0001 computed blind by the harness attacker), the acceptance test at bootp_subr.c:838-848 never inspects the reply origin because soreceive is called with a NULL name at :807 (harness counter: 0 origin requests; replies from port 9999 accepted), and a single blind OFFER+ACK exchange from 6.6.6.6:9996 gave the attacker complete control of the netboot client identity: hostname, root NFS server and path, swap server and path, and default route were all adopted via verbatim bootpc_decode_reply and fed to setfs/md_mount/rtrequest_global. A predicted-XID ACK with a 3-byte subnet mask additionally tripped the verbatim panic at bootp_subr.c:1390 (panic.txt), proving the boot-time crash path. uid=0 is not reachable on this guest because the stock kernel omits options BOOTP and boots from disk; on real netboot deployments the demonstrated control implies total (root filesystem) compromise. The validated fix (karc4random xid seed + require reply source port 67) killed the blind prediction, the arbitrary-origin acceptance and the panic in A/B runs; a same-L2 sniffer spoofing port 67 remains, which is the documented unauthenticated-DHCP residual.