# 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) |
