# DF-3213 — stale `RC_REPSTATUS` survives dup-reply-cache slot reuse → `rc_status`/`rc_reply` union type confusion → kernel mbuf pointer material + wrong replies sent to an unauthenticated remote NFS client

File: `sys/vfs/nfs/nfs_srvcache.c` (pass 2)

## What this is

The NFS server duplicate-reply cache (`nfsrv_getcache` / `nfsrv_updatecache`)
reuses LRU slots when the cache is full. Slot reuse clears only `RC_REPMBUF`
and `RC_NAM`:

```c
	} else {
		rp = TAILQ_FIRST(&nfsrvlruhead);
		...
		rp->rc_flag |= RC_LOCKED;
		LIST_REMOVE(rp, rc_hash);
		TAILQ_REMOVE(&nfsrvlruhead, rp, rc_lru);
		if (rp->rc_flag & RC_REPMBUF) {          /* nfs_srvcache.c:253 */
			m_freem(rp->rc_reply);
			...
		}
		if (rp->rc_flag & RC_NAM) { ... }
	}
```

`RC_REPSTATUS` (set at nfs_srvcache.c:338) is **never cleared anywhere in the
tree** (grep: the only clear sites for flags are REPMBUF/NAM). When the reused
slot later stores a full reply mbuf via the v3 (or v2-non-repstat) branch
(nfs_srvcache.c:340-347 — which also does not clear RC_REPSTATUS), the entry
ends up with **both** `RC_REPSTATUS|RC_REPMBUF` set and the union
`rc_un`/`rc_haddr` holding an `mbuf *`.

On a cache hit, `nfsrv_getcache` tests `RC_REPSTATUS` **first**
(nfs_srvcache.c:212), so the retransmission is served by:

```c
	nfs_rephead(0, nd, slp, rp->rc_status, repp, &mb, &bpos);
```

where `rp->rc_status` (`int`) aliases the low 32 bits of `rp->rc_reply`
(`struct mbuf *`) — `struct nfsrvcache` unions them (nfsrvcache.h:54-65).
`nfs_rephead` (nfs_socket.c:1614-1620) then either

* emits the raw value with only bit 30 masked to the wire
  (`NFSERR_AUTHERR` = 0x40000000, nfsproto.h:100 → MSG_DENIED / rpc_autherr /
  `err & ~NFSERR_AUTHERR`), when bit 30 of the pointer is set — a 31-bit
  kernel heap pointer disclosure, or
* clamps it through `nfsrv_errmap` (whitelist / NFSERR_IO default), producing
  a *status-only* reply that is not the cached reply at all — the duplicate
  reply cache answers retransmissions of non-idempotent NFS operations with a
  wrong, structurally different result.

Attacker: any UDP client that can reach the NFS server (no credentials, no
filehandle, no export access needed — every request that parses creates a
cache entry before authorization runs).

## PoC

`leakforger.c` — raw RPC/UDP forger, run as unprivileged `nobody` from
127.0.0.1 against the in-guest kernel nfsd (UDP):

1. **Phase A** — `M` NFSv2 SYMLINK (wire proc 13 → generic 10) requests with
   distinct xids and an all-zero filehandle. v2 SYMLINK is nonidempotent and
   v2-repstat, so each reply is cached with `RC_REPSTATUS`, `rc_status = 13`
   (EACCES; the phase-A reply shows status `0x0d`).
2. **Phase B** — `2048 - M` v2 NULL fillers fill `desirednfsrvcache`
   (= min(nmbclusters/2, 2048) = 2048 on this guest, nmbclusters=33296); the
   M stale entries become the LRU-oldest.
3. **Phase C** — `M` NFSv3 REMOVE (wire proc 12 → generic 12) requests with
   distinct xids; each MISS evicts one stale entry and reuses the slot
   (RC_REPSTATUS survives); the v3 completion stores the reply mbuf
   (`RC_REPMBUF`, `rc_reply = m_copym(...)`).
4. **Phase D** — retransmit each v3 REMOVE. The hit takes the RC_REPSTATUS
   branch and answers from `rc_status` = low-32 of the cached mbuf pointer.
5. **Control** — a fresh v3 REMOVE in a clean slot: its retransmit is
   byte-identical (legit `RC_REPMBUF` resend), proving the divergence above
   is the stale-flag bug and not normal behavior.

## Build (in-guest)

```
sh build.sh        # pushes leakforger.c, cc -O2 -Wall
```

## Run (in-guest, root starts nfsd; forger runs as nobody)

```
sh run.sh          # /sbin/nfsd -u -n 4 ; su -m nobody -c /tmp/leakforger 192 2048
```

## Expected / observed (stock INVARIANTS kernel, DF 6.5-DEVELOPMENT)

* Phase-A replies: status 13 (`0x0d`) — EACCES for the zero filehandle.
* Phase-D retransmits: **all 192/192 (and 192/192, 256/256 in repeats)**
  diverge from the original reply: instead of the cached v3 error reply the
  server returns a 7-word status-only reply whose NFS status is **5
  (NFSERR_IO)** — the `nfsrv_errmap` default for a non-whitelisted value.
  This is the discrimination proof: the genuine stale status would have been
  13, which **is** whitelisted for REMOVE and would have been returned
  unchanged; only a non-small value (the aliased `rc_reply` mbuf pointer
  low-32) clamps to 5. The reply to a retransmission of a non-idempotent
  operation is therefore (a) wrong and (b) derived from a kernel pointer.
* The raw 31-bit pointer escape (MSG_DENIED/auth_stat = `ptr & 0xBFFFFFFF`)
  requires bit 30 of the mbuf address to be set; across 640 poisoned slots on
  this guest the mbuf zone never had bit 30 set, so the raw value was always
  clamped. The mechanism is placement-dependent — on systems whose mbuf zone
  crosses the bit-30 band the raw low-32 (minus bit 30, which is then known)
  is disclosed to any LAN client.
* Control entry: byte-identical resend (expected).

Exit code 2 = corruptions reproduced; 0 = raw leak fired; 1 = nothing.
