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)
PoC verification
Evidence pack
findings/poc/DF-3044 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:1538gctx->xid = ~0xFFFF),+0x100per interface (:435),+1on the first compose of an unresolved ifctx (:1194-1195) β sent from6.6.6.6:9999(a port no DHCP server uses) is accepted by the verbatimbootpc_callmatcher (:838-848) and the verbatimbootpc_decode_replyadopts 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_SUBNETMASKwith len 3 reachesbootpc_decode_replyand trips the verbatimpanic("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 (verbatimReceived 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.)
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
- 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 intocall.xid. Guest observation: the DISCOVER and the subsequent DHCP REQUEST both used xid bytes01 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). - Zero origin validation β :807-808
soreceive(so, NULL, &auio, ...)discards the source address/port; the entire acceptance test isop == 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. - 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 tosetfsβmd_mount(:1644-1666), TAG_ROUTERS β default route viartrequest_global(:1142-1148). All adoption markers captured (run.log), including the verbatim decode console linevtnet0 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 themd_mountseam β no real RPC leaves the box.) - Panic manifestation β a predicted-XID ACK with
TAG_SUBNETMASKlen 3 trips the verbatimpanic("bootpc: subnet mask len is 3")(:1389-1391):panic.txt, backtracebootpc_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 BOOTPdiskless 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 messagebootpc: 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 12bounds 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 blockingsoreceive(time_uptime is not volatile, sys/time.h:163); the stub models this withlwkt_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_testableConfirmed 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.
No comments yet.