# DF-0775 — VERDICT

## Verdict: NOT REPRODUCED (false positive for claimed impact)

**Finding title:** RPC reply verifier-length integer overflow corrupts
XDR cursor — wild pointer deref client kernel panic or OOB read

**Claimed impact:** kernel panic (wild pointer dereference) or OOB
kernel memory read when a malicious NFS server replies with
verifier_len ∈ {0x7FFFFFFD, 0x7FFFFFFE, 0x7FFFFFFF}.

**Actual observed impact:** `EBADRPC` — the NFS operation fails
gracefully.  **No panic, no OOB read, no kernel memory disclosure.**
The guest remains fully stable after 21+ malicious replies.

---

## What IS real

The finding correctly identifies three things in
`sys/vfs/nfs/nfs_socket.c:nfs_request_processreply()`:

1. **No bounds check on verifier length** (line 1498):
   ```c
   i = fxdr_unsigned(int32_t, *tl);   /* full int32 range, no bound */
   ```
   The server-side path at line 2347 checks
   `if (len < 0 || len > RPCAUTH_MAXSIZ)` — the client-side does not.

2. **Signed overflow in nfsm_rndup** (line 1505):
   ```c
   ERROROUT(nfsm_adv(info, nfsm_rndup(i)));
   /* nfsm_rndup(a) = (((a)+3) & (~0x3))  — overflows for a near INT_MAX */
   ```
   For `i = 0x7FFFFFFD`: `(0x7FFFFFFD + 3) & ~3 = 0x80000000 = INT_MIN`.

3. **dpos corruption in nfsm_adv** (nfsm_subs.c:916-918):
   ```c
   n = mtod(info->md, caddr_t) + info->md->m_len - info->dpos;
   if (n >= len) {        /* 0 >= INT_MIN → TRUE (signed) */
       info->dpos += len; /* dpos += INT_MIN → wild pointer, ~2 GiB back */
   ```
   **Confirmed by disassembly** of `nfsm_adv` @ `0xffffffff80814520`:
   the `cmp %esi,%ecx` / `jl` at `8081453f-80814541` does NOT jump for
   `ecx=0, esi=0x80000000` (because `0 >= INT_MIN` is true in signed
   comparison), so the in-place `dpos += sign_extend(INT_MIN)` at
   `80814543-8081454b` executes, corrupting dpos.

## What does NOT happen (the claimed impact)

The finding claims that after dpos is corrupted, `nfsm_dissect(info, 4)`
at line 1507 **returns the wild pointer**, which is then dereferenced at
line 1509 (`if (*tl == 0)`).

**This is false.**  `nfsm_dissect()` never returns the wild pointer.
The reason is a **`int n` truncation** in the bounds check:

```c
void *nfsm_dissect(nfsm_info_t info, int bytes) {
    int n;                                              /* ← 32-bit! */
    n = mtod(info->md, caddr_t) + info->md->m_len - info->dpos;
    /*      ↑ 64-bit pointer arithmetic, result is ptrdiff_t (64-bit)   */
    if (bytes <= n) {        /* n is truncated to int before compare    */
        ptr = info->dpos;    /* ← wild pointer — ONLY reached if n >= 4 */
        ...
    } else {
        error = nfsm_disct(...);  /* ← returns EBADRPC for our case     */
    }
}
```

After the dpos corruption, the pointer difference
`(m_data + m_len - dpos_corrupted)` is a huge 64-bit value
(≈ 2³¹ + original_remaining).  Truncated to `int`, this is **negative**
(0x80000000 + small → still bit-31 set → negative as signed int).
The signed comparison `4 <= (negative)` is **FALSE**, so the code routes
to `nfsm_disct()`, which also detects the bad state and returns
`EBADRPC`.  The wild pointer is never returned, never dereferenced.

**Disassembly proof** — `nfsm_dissect` @ `0xffffffff80815170`:
```asm
80815191:  sub    %rax,%rcx       ; rcx = (m_data+m_len) - dpos  [64-bit]
80815194:  cmp    %esi,%ecx       ; 32-BIT: compares ecx (lower 32) with bytes
80815196:  jl     808151b0        ; ecx(0x80000000) < esi(4) signed → JUMP TAKEN
                                  ; → falls into nfsm_disct → EBADRPC
```

The `cmp %esi,%ecx` instruction explicitly compares the **lower 32 bits**
(`%ecx`) of the 64-bit pointer difference against `bytes` (`%esi`).  The
compiler did NOT optimize away the `int n` intermediate — it honors the C
type and truncates.  For any practical mbuf size (≤ 4 KiB), the truncated
value is always negative, so the wild-pointer path is unreachable.

### Mathematical proof the wild pointer is unreachable

Let `R` = original remaining bytes in the current mbuf (0 ≤ R ≤ ~4096).
After corruption: `ptrdiff = R + 2³¹`.  Truncated to `int`:
`n = (int)(R + 0x80000000)`.  For 0 ≤ R < 2³¹ (always true for mbufs):
`n = R + 0x80000000` which has bit 31 set → `n < 0`.  Since `bytes = 4 > 0`,
`bytes <= n` is `4 <= negative` → **always FALSE**.  The wild pointer is
returned only if `n >= 4`, which requires `R >= 2³¹` — impossible for an
mbuf.

---

## Reproduction evidence

A malicious NFS/RPC server (`malicious_server.c`) was built and run on
the guest.  It implements:
- portmap v2 (UDP/TCP 111) → returns port 2049 for all GETPORT
- rpcbind v3/v4 → PROG_MISMATCH (forces fallback to portmap v2)
- NFS NULL proc → valid reply (satisfies mount_nfs userspace probe)
- NFSv2 MOUNT proc → valid reply with a 32-byte dummy file handle
- **All other NFS procs (kernel RPCs) → reply with verifier_len=0x7FFFFFFD**

`mount_nfs -2 -o tcp,port=2049,ro 127.0.0.1:/x /mnt` succeeded.  The
kernel NFS client then sent 21+ GETATTR RPCs, each receiving the
malicious verifier.  Every operation returned `EBADRPC`:

```
$ ls /mnt_df0775
ls: /mnt_df0775: RPC struct is bad
```

**The guest never panicked.**  No trap, no page fault, no kernel message.
The kernel was completely stable throughout.

Server log excerpt:
```
[ok]   prog=100003 vers=2 proc=0 xid=0x... (NULL — mount_nfs userspace)
[ok]   prog=100000 vers=2 proc=3 xid=0x... (portmap GETPORT → 2049)
[ok]   prog=100005 vers=1 proc=1 xid=0x... (MOUNT → dummy FH)
[EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DD -> verifier_len=0x7FFFFFFD
[EVIL] prog=100003 vers=2 proc=1 xid=0x5E5944DE -> verifier_len=0x7FFFFFFD
... (21 total kernel RPCs with malicious verifier)
```

---

## Exploit chain

Not applicable — this is not a memory-corruption primitive.  The dpos
corruption is real but the corrupted pointer is never dereferenced due
to the `int n` truncation defense in `nfsm_dissect()`.  There is no
write primitive, no read primitive, no control-flow hijack.  The only
observable effect is `EBADRPC`, which is the same error any malformed
RPC reply would produce.  This is not exploitable for privilege
escalation.

---

## Recommended fix (defense-in-depth)

Although the claimed impact does not manifest, the underlying code has a
legitimate hardening gap: the client-side verifier length is unbounded
while the server-side caps at `RPCAUTH_MAXSIZ` (400).  A future compiler
change, code refactor, or different architecture could remove the `int`
truncation defense and expose the dpos corruption.  The fix matches the
existing server-side check:

```c
i = fxdr_unsigned(int32_t, *tl);
if (i < 0 || i > RPCAUTH_MAXSIZ) {   /* ← add this */
    error = EBADRPC;
    goto nfsmout;
}
```

See `fix.diff`.  This **supersedes** the finding markdown's proposal
(which suggested the same bound check plus a redundant `nfsm_adv` length
check — the latter is unnecessary since the bound check eliminates the
overflow entirely).
