Panic on malformed BOOTP/DHCP options from unauthenticated network reply during diskless boot β pre-auth remote DoS
Summary
bootp_subr.c bootpc_decode_reply 6 panic() calls driven by attacker-controlled DHCP option values: :1390 panic("subnet mask len is %d") TAG_SUBNETMASK taglen!=4. :1404 panic("Router Len is %d") TAG_ROUTERS taglen%4!=0. :1435 panic("Failed to set rootfs to %s") TAG_ROOT not A.B.C.D:path format. :1464 panic("Expected 4 bytes for swaplen") TAG_SWAPSIZE taglen!=4. :1474 panic("Failed to set swapfs to %s") TAG_SWAP not IP:path. :1481 panic("hostname >= %d bytes") TAG_HOSTNAME taglen>=256. No source-address filtering: soreceive called with NULL addr bootpc_call:807 accepts reply from ANY source. xid/hlen/chaddr visible in broadcast request. Any L2-segment attacker forges BOOTP reply with malformed option = deterministic kernel panic during boot prevents system from booting. 6 trigger variants each one-packet. options BOOTP not default X86_64_GENERIC only LINT64/custom diskless. Fix: replace each panic with kprintf+skip-option.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0802 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| bootp_panic_harness.c | trigger-source | Faithful userspace replica of kernel BOOTP parser; reproduces all 6 panic() sites | 15.3 KB | view raw |
| bootp_panic_harness_fixed.c | exploit-chain | Harness with fix.diff transformation applied; demonstrates 0/6 panics (the fix) | 15.3 KB | view raw |
| build.sh | build-script | Exact cc build command | 328 B | view raw |
| run.sh | run-script | Exact run invocation | 306 B | view raw |
| build.log | build-log | Full compiler output, final successful build | 1.3 KB | view raw |
| run.log | run-log | Decisive harness run: 6/6 PANIC | 1.1 KB | view raw |
| fix_harness_build.log | build-log | Fixed-harness compilation output | 1.3 KB | view raw |
| fix_harness_run.log | run-log | Fixed-harness run: 0/6 no-panic | 691 B | view raw |
| fix_build.log | build-log | Kernel build evidence (BOOTP + fix.diff, rc=0, -Werror clean) | 2.5 KB | view raw |
| fix.diff | suggested-fix | git-apply-able fix: 6 panic() -> kprintf()+skip | 2.8 KB | view raw |
| VERDICT.md | verdict | Full narrative analysis | 6.2 KB | β raw |
| env.txt | environment | uname, cc version, BOOTP reachability in config | 722 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0802 β Panic on malformed BOOTP/DHCP options from unauthenticated network reply during diskless boot
Verdict
REPRODUCED (code-level harness) β pre-auth remote DoS via kernel panic().
Impact: panic / DoS (no write primitive, no escalation path).
Fix: VALIDATED β fix.diff applies cleanly, compiles in a BOOTP kernel
build (rc=0, -Werror), and the harness before/after shows 6/6 β 0/6 panics.
The bug
sys/vfs/nfs/bootp_subr.c :: bootpc_decode_reply() contains six panic()
calls (lines 1390/1404/1435/1464/1474/1481) that fire on attacker-controlled
DHCP/BOOTP option values received from an unauthenticated network reply.
Each is reachable from a single forged BOOTP/DHCP packet:
| # | Tag | Condition | Panic site | Message |
|---|---|---|---|---|
| 1 | TAG_SUBNETMASK(1) | taglen != 4 | :1390 |
subnet mask len is %d |
| 2 | TAG_ROUTERS(3) | taglen % 4 != 0 | :1404 |
Router Len is %d |
| 3 | TAG_ROOT(17) | setfs() fails (not A.B.C.D:path) | :1435 |
Failed to set rootfs to %s |
| 4 | TAG_SWAP(128) | setfs() fails (not A.B.C.D:path) | :1474 |
Failed to set swapfs to %s |
| 5 | TAG_SWAPSIZE(129) | taglen != 4 | :1464 |
Expected 4 bytes for swaplen, not %d bytes |
| 6 | TAG_HOSTNAME(12) | taglen >= MAXHOSTNAMELEN(256) | :1481 |
hostname >= %d bytes |
Call path (network β panic)
- During diskless boot,
bootpc_init()βbootpc_call()broadcasts a BOOTP/DHCP request. Atbootp_subr.c:807, the reply is received via:c error = soreceive(so, NULL, &auio, NULL, NULL, &rcvflg);The second argument isNULLβ the source address is never captured, so there is no source-IP filtering: any host on the L2 segment that sees the broadcast request (xid/hlen/chaddr are all visible in it) can forge a reply. - The reply is validated only for minimum length (
:826,BOOTP_MIN_LEN=300) and cookie (bootpc_hascookie,:1256). bootpc_decode_reply()(:1341) then callsbootpc_tag()(:1307) βbootpc_tag_helper()(:1264) to parse the option TLVs. The parser itself is safe β it bounds-checks TLV lengths at:1283(if (j >= ej || j + *j + 1 > ej) badopt=1). So the option is well-formed in structure; it is the semantic value (wrong length for the tag, or a non-IP-path string) that triggerspanic().bootpc_decode_reply()hits one of the sixpanic()sites.
Each variant is a single forged packet = deterministic kernel panic.
Reachability (important caveat)
bootp_subr.c is optional bootp in sys/conf/files:1894 and is NOT
compiled into the default X86_64_GENERIC kernel (which has only
options NFS_ROOT). It is only built with options BOOTP (present in
LINT64, or a custom diskless-boot config). Its sole live trigger is the
early-boot diskless path (nfs_boot β bootpc_init), which cannot be staged
on this guest (it boots from vtblk0, not NFS root, and the code is absent
from the running kernel).
For these reasons the reproduction is a deterministic code-level harness
(bootp_panic_harness.c) that copies the kernel's parser functions verbatim
(bootpc_hascookie, bootpc_tag_helper, bootpc_tag, bootpc_decode_reply,
setfs, getdec) and feeds them crafted BOOTP packets with malformed options.
panic() is defined as fprintf(stderr,...) + abort() to faithfully model the
kernel's halt.
Reproduction result
$ ./bootp_panic_harness [1] V1 TAG_SUBNETMASK len=3 : PANIC (kernel would halt) [2] V2 TAG_ROUTERS len=5 : PANIC (kernel would halt) [3] V3 TAG_ROOT bad-host : PANIC (kernel would halt) [4] V4 TAG_SWAP bad-host : PANIC (kernel would halt) [5] V5 TAG_SWAPSIZE len=3 : PANIC (kernel would halt) [6] V6 TAG_HOSTNAME len=256 : PANIC (kernel would halt) 6/6 malformed options cause kernel panic() in bootpc_decode_reply.
All 6 panic sites fire. Each is an unauthenticated one-packet DoS.
Escalation
None. The primitive is a kernel panic (read-and-compare of attacker-supplied
option length/value, then panic()). There is no memory write, no corruption
of adjacent memory β the option parser bounds-checks TLV lengths before any
bcopy, so the panic fires before any data is written. The impact ceiling
is a pre-auth remote DoS (prevents the system from booting). No uid=0 chain
is derivable.
Fix (fix.diff)
Replace each of the 6 panic() calls with kprintf() + skip-the-option
(graceful degradation, matching how a network client should handle malformed
server input). The diff restructures each site into an if/else so that the
malformed option is logged and skipped while valid options continue to be
processed.
Fix validation
git apply --check: clean (rc=0).- Kernel compilation: applied
fix.diffto/usr/src, addedoptions BOOTP+options BOOTP_NFSROOTtoX86_64_GENERIC, ranmake -j6 nativekernel KERNCONF=X86_64_GENERIC. The fixedbootp_subr.ccompiled with-Werror(zero warnings/errors);bootp_subr.oproduced (275 KB); full kernel build completedrc=0. - Harness before/after (faithful code-level analog of kernel before/after):
- Before (unpatched logic): 6/6 malformed options β panic.
- After (fixed logic, same transformation as
fix.diff): 0/6 β all gracefully skipped with a warning. - Live-kernel trigger:
not_testableβ thebootpc_decode_replyruntime path (diskless boot) cannot be exercised on this guest; the fix is validated at the compilation + faithful-logic level.
Files
| File | Description |
|---|---|
bootp_panic_harness.c |
Faithful userspace replica of the kernel parser; reproduces all 6 panics |
bootp_panic_harness_fixed.c |
Same harness with the fix.diff transformation applied; 0 panics |
build.sh / run.sh |
Exact build & run commands |
build.log |
Full compiler output (final successful build) |
run.log |
Full harness run output (decisive: 6/6 PANIC) |
fix_harness_run.log |
Fixed-harness run (decisive: 0/6 no-panic) |
fix_build.log |
Kernel build evidence (BOOTP + fix.diff, rc=0) |
fix.diff |
Standalone git-apply-able fix (6 panicβkprintf+skip) |
env.txt |
Guest environment (uname, cc, config reachability) |
Fix verification
not_testableNOT_TESTABLE (live kernel trigger unstageable) but extensively validated at code level: (1) git apply --check rc=0; (2) fix.diff applied to /usr/src, BOOTP kernel built rc=0 with -Werror -- bootp_subr.c compiled clean; (3) harness before/after: baseline logic 6/6 PANIC vs fixed logic 0/6 no-panic. The live runtime path (diskless boot processing a forged DHCP reply) cannot be triggered on this guest because bootpc_init only runs during NFS-root boot and this guest boots from vtblk0, so a kernel-level run-against-patched-kernel test is not possible. The fix correctness is established by the faithful harness before/after + kernel compilation validation.
BASELINE (unpatched harness logic): 6/6 PANIC -- subnet mask len=3, Router Len=5, Failed to set rootfs, Failed to set swapfs, swaplen not 4 bytes, hostname>=256. PATCHED (fixed harness logic): 0/6 no-panic -- all malformed options gracefully skipped with kprintf warning. KERNEL COMPILATION: make -j6 nativekernel KERNCONF=X86_64_GENERIC (with options BOOTP + fix.diff) => NK_DONE rc=0, zero errors/warnings with -Werror, bootp_subr.o produced (275792 bytes).
Confirmed kernel references
Detail
Exploit chain
none (pure panic/DoS -- no memory corruption). The option parser bounds-checks TLV lengths before any bcopy, so panic() fires before any data is written. There is no write primitive, no adjacent-memory corruption, and therefore no uid=0 chain is derivable. The impact ceiling is a pre-auth remote DoS: a single forged BOOTP reply with a malformed option halts the kernel during diskless boot.
Evidence (decisive lines)
kernel: panic: bootpc: subnet mask len is 3 [1] V1 TAG_SUBNETMASK len=3 : PANIC (kernel would halt) kernel: panic: bootpc: Router Len is 5 [2] V2 TAG_ROUTERS len=5 : PANIC (kernel would halt) kernel: panic: Failed to set rootfs to not-an-ip-and-no-colon [3] V3 TAG_ROOT bad-host : PANIC (kernel would halt) kernel: panic: Failed to set swapfs to not-an-ip-and-no-colon [4] V4 TAG_SWAP bad-host : PANIC (kernel would halt) kernel: panic: bootpc: Expected 4 bytes for swaplen, not 3 bytes [5] V5 TAG_SWAPSIZE len=3 : PANIC (kernel would halt) kernel: panic: bootpc: hostname >= 256 bytes [6] V6 TAG_HOSTNAME len=256 : PANIC (kernel would halt) 6/6 malformed options cause kernel panic() in bootpc_decode_reply.
PoC changes
Wrote bootp_panic_harness.c from scratch -- a faithful userspace replica of the kernel's BOOTP option parser (bootpc_hascookie, bootpc_tag_helper, bootpc_tag, bootpc_decode_reply, setfs, getdec copied verbatim from sys/vfs/nfs/bootp_subr.c and nfs_mountrpc.c). panic() is modeled as fprintf+abort to faithfully halt. The harness feeds 6 crafted BOOTP packets, each with one malformed DHCP option, and reports which trigger kernel panic(). Also wrote bootp_panic_harness_fixed.c (same harness with the fix.diff transformation applied) for the before/after fix validation. No prior PoC existed (finding markdown + poc dir were unseeded).
Verified recommended fix
Replace each of the 6 panic() calls in bootpc_decode_reply() with kprintf()+skip-the-option (graceful degradation for malformed network input). The fix.diff restructures each site into if/else so the malformed option is logged and skipped while valid options continue processing. This is a new fix authored during verification (no prior proposal existed in a finding markdown). The full git-apply-able diff is in findings/poc/DF-0802/fix.diff.
Verdict
REPRODUCED. sys/vfs/nfs/bootp_subr.c::bootpc_decode_reply() contains 6 panic() calls (lines 1390/1404/1435/1464/1474/1481) that fire on attacker-controlled DHCP/BOOTP option values from an unauthenticated network reply. The reply is received via soreceive(so, NULL, ...) at bootp_subr.c:807 with NO source-address filtering, so any L2-segment attacker who sees the broadcast request can forge a reply. The option TLV parser itself is safe (bounds-checks at :1283), so the panic fires on a well-formed-but-semantically-invalid option (wrong length for tag, or non-IP-path string) -- each variant is a single forged packet = deterministic kernel panic. A faithful code-level harness (bootp_panic_harness.c) copies the kernel parser functions verbatim and reproduces all 6 panics. The code is optional bootp (sys/conf/files:1894), NOT compiled into default X86_64_GENERIC (only in LINT64/custom diskless config), and its sole live trigger is the diskless-boot path which cannot be staged on this guest -- hence the harness reproduction. Impact is pre-auth remote DoS (panic during boot prevents system startup); no write primitive, no escalation path.
No comments yet.