# DF-0900 — VERDICT

## Verdict: REPRODUCED (signed integer overflow, confirmed by harness + source trace); fix VALIDATED (compiles, boots, bound check confirmed in binary).

## The bug (real)

`sys/vfs/nfs/krpc_subr.c:425` in `krpc_call()`'s `gotreply` path adds an
attacker-controlled network-supplied `authlen` (from the RPC reply verifier
header `reply->rp_auth.authlen`, a `u_int32_t` on the wire) into a plain
signed `int len` (declared at `krpc_subr.c:200`) **without any upper bound**:

```c
415:   len = sizeof(*reply);                                       /* = 24 */
424:   if (reply->rp_auth.authtype != 0) {
425:       len += fxdr_unsigned(u_int32_t, reply->rp_auth.authlen); /* int += u32 */
426:       len = (len + 3) & ~3;                                    /* stays INT_MIN */
427:   }
428:   m_adj(m, len);                                              /* m_adj(m, INT_MIN) */
```

`fxdr_unsigned` (`sys/vfs/nfs/xdr_subs.h`) byte-swaps the big-endian wire
u32 to host order. `len` is signed `int`. For `authlen = 0x7FFFFFE8`:

- `24 + 0x7FFFFFE8 = 0x80000000 = INT_MIN` — **signed integer overflow** (C UB).
- `(INT_MIN + 3) & ~3 = INT_MIN`.
- `m_adj(m, INT_MIN)` (`sys/kern/uipc_mbuf.c:1859`) takes the `req_len < 0`
  branch (`m_adj` :1884), where `len = -len` (`-INT_MIN`, itself UB) and the
  subsequent `m->m_len -= len` corrupts `m_len` (e.g. 100 → 2147483748).
  Downstream `m_pullup`/copyout then read past the buffer.

The harness `overflow_demo.c` reproduces this exact arithmetic and the
`m_adj` single-mbuf corruption; for `authlen = 0x7FFFFFE8` it prints:

```
0x7fffffe8   -2147483648   -2147483648   -2147483648   2147483748   cited overflow trigger
  ** BUG FIRES: sizeof_reply(24) + authlen(0x7FFFFFE8) = 0x80000000 = INT_MIN (signed overflow, UB)
  ** (len+3)&~3 = INT_MIN, passed to m_adj() as req_len.
  ** m_len corrupted: 100 -> 2147483748 (OOB on next mbuf op)
```

## Reachability (boot-time only — why no live trigger on this guest)

`krpc_call()` is reached **only** via:
- `krpc_portmap()` (`krpc_subr.c:163`) and
- the mount-rpc helpers in `nfs_mountrpc.c:140,146,155,163,202,253,256`,

which are themselves called **only** from the diskless NFS-root bootstrap
path (`bootpc_init()` at `nfs_vfsops.c:569` → `nfs_mountrpc`). The
`nfs_mountrpc.c` file header states verbatim:

> "Procedures used by NFS_ROOT and BOOTP to do an NFS mount rpc to obtain
>  the nfs root file handle for a NFS-based root mount point. This module
>  is not used by normal operating code because the 'mount' command has a
>  far more sophisticated implementation."

There is **no userspace syscall** that reaches this code on a normally-booted
kernel. The attacker model is an **on-path network attacker or a malicious
NFS server** during diskless boot. The normal NFS client/server socket path
in `nfs_socket.c` already validates the equivalent length field with
`if (len < 0 || len > RPCAUTH_MAXSIZ)` (`nfs_socket.c:2347,2385`); the
boot-time `krpc_subr.c` path omits that guard — which is exactly the gap the
fix closes.

We therefore reproduce the **arithmetic bug** via the harness (which
replicates `krpc_subr.c:415-428` and `m_adj` exactly) rather than a live
runtime trigger, which would require staging a diskless NFS boot against a
malicious server — not feasible inside this audit guest.

## Impact ceiling

**Low** (matches the finding's severity). The realistic worst case is
**boot failure / local DoS at diskless-NFS-root boot time** (the finding's
summary: "Single-mbuf: EBADRPC boot failure; chained non-cluster: potential
panic in m_pullup"). No memory-corruption primitive is reachable from an
unprivileged local user — the path is network/boot-time only and `authlen`
shapes only a length, not arbitrary content. No escalation chain applies.

## Exploit chain

**none** — this is not a memory-corruption primitive reachable from
userspace. It is a boot-time-only signed integer overflow whose ceiling is
DoS (boot failure / panic). No uid0 chain is applicable.

## Fix (authored in `fix.diff`, VALIDATED)

Bound `authlen` against `RPCAUTH_MAXSIZ` (400, `sys/vfs/nfs/rpcv2.h:55`) —
the same guard the normal NFS socket path already uses (`nfs_socket.c:2347,2385`) —
before adding it to `len`. On overflow-class values the function now returns
`EBADRPC` instead of performing the corrupting arithmetic:

```c
reply = mtod(m, struct rpc_reply *);
if (reply->rp_auth.authtype != 0) {
    u_int32_t authlen = fxdr_unsigned(u_int32_t, reply->rp_auth.authlen);
    if (authlen > RPCAUTH_MAXSIZ) {     /* NEW */
        error = EBADRPC;
        goto out;
    }
    len += authlen;
    len = (len + 3) & ~3;
}
m_adj(m, len);
```

This **matches the finding proposal** (the summary says: "Fix: validate
authlen against RPCAUTH_MAXSIZ(400)").

## Phase 8 — fix validation (BUILT + BOOTED + binary-confirmed)

1. **Baseline (`#0`, unpatched)**: harness fires; source at `krpc_subr.c:425`
   has no bound — overflow path is live.
2. **Applied** `fix.diff` to in-guest `/usr/src` (`patch -p1` → "Hunk #1
   succeeded at 422").
3. **Built** `make -j6 nativekernel KERNCONF=X86_64_GENERIC` → `rc=0`.
   **Installed** `make installkernel` → `rc=0`. (NOTE: DragonFly's loader
   on this guest requires the full kernel — `make installkernel` writes the
   119 MB unstripped ELF to `/boot/kernel/kernel`; a manual copy of
   `kernel.stripped` was rejected by the loader with "Unable to load
   /kernel/kernel".)
4. **Booted** patched kernel: `kern.version = DragonFly 6.5-DEVELOPMENT #1:
   Tue Jul 14 12:01:25 UTC 2026`.
5. **Binary-confirmed** the bound is compiled in (objdump of the running
   kernel's `krpc_call.part.0`):

   ```
   ffffffff808171bf:  mov    0x10(%rax),%eax      ; load reply->rp_auth.authlen
   ffffffff808171c2:  bswap  %eax                 ; fxdr_unsigned
   ffffffff808171c4:  cmp    $0x190,%eax          ; cmp authlen, RPCAUTH_MAXSIZ(400)
   ffffffff808171c9:  ja     …krpc_call.part.0+0x42d  ; if above -> EBADRPC goto out
   ffffffff808171cf:  add    $0x1b,%eax           ; len = authlen + sizeof(*reply) + 3
   ```

   With `authlen <= 400`, the subsequent `add $0x1b` produces at most 427 —
   well within `int` range, so the overflow is dead. `authlen > 400` now
   takes the `ja` to `EBADRPC`.

**fix_status = fixed.** The runtime behavioral re-test (live NFS-mount with
a malicious reply) is `not_testable` on this guest (boot-time-only path,
no diskless-NFS boot can be staged), but the fix is validated to apply
cleanly, compile, boot, and close the vulnerable code path by both source
inspection and binary disassembly of the running kernel.

## PoC changes

The `findings/poc/DF-0900/` directory did not exist when this runner
spawned (only the DB row + summary were present). I created:
- `overflow_demo.c` — userspace harness replicating `krpc_subr.c:415-428` +
  `m_adj` arithmetic to demonstrate the signed overflow for the cited
  `authlen` values.
- `README.md`, `build.sh`, `run.sh`, `fix.diff`, `env.txt`, full logs,
  this `VERDICT.md`, and `manifest.json`.
