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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0802 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md verdict Full narrative analysis
↓ download 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)

  1. During diskless boot, bootpc_init() β†’ bootpc_call() broadcasts a BOOTP/DHCP request. At bootp_subr.c:807, the reply is received via: c error = soreceive(so, NULL, &auio, NULL, NULL, &rcvflg); The second argument is NULL β€” 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.
  2. The reply is validated only for minimum length (:826, BOOTP_MIN_LEN=300) and cookie (bootpc_hascookie, :1256).
  3. bootpc_decode_reply() (:1341) then calls bootpc_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 triggers panic().
  4. bootpc_decode_reply() hits one of the six panic() 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

  1. git apply --check: clean (rc=0).
  2. Kernel compilation: applied fix.diff to /usr/src, added options BOOTP + options BOOTP_NFSROOT to X86_64_GENERIC, ran make -j6 nativekernel KERNCONF=X86_64_GENERIC. The fixed bootp_subr.c compiled with -Werror (zero warnings/errors); bootp_subr.o produced (275 KB); full kernel build completed rc=0.
  3. 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.
  4. Live-kernel trigger: not_testable β€” the bootpc_decode_reply runtime 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_testable
baseline reproduced→ patch + rebuild →patched clean

NOT_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).
↓ fix.diffbuilt X86_64_GENERIC+BOOTP with fix.diff applied (rc=0, -Werror clean, bootp_subr.o=275KB produced); NOT booted -- the bootpc_decode_reply runtime trigger (diskless BOOTP/NFS boot) cannot be exercised on this vtblk0-rooted guest

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.