# DF-0806 — dirfs_readlink off-by-one heap overflow + OOB read

## Verdict
**REPRODUCED** (deterministic harness) — **1-byte heap overflow (CWE-787) AND
1-byte OOB read (CWE-125) confirmed** in `dirfs_readlink`. The `fix.diff`
(`kmalloc(uio_resid+1)` + `uiomove(buf, nlen)`) is **compile-validated and
harness-validated**; a live-kernel boot test is `not_testable` because dirfs
is **vkernel-only** (not compiled into the running X86_64_GENERIC host kernel
and no vkernel runs on this guest).

## The bug — line-by-line (`sys/vfs/dirfs/dirfs_vnops.c`)

```c
1328:  buf = kmalloc(uio->uio_resid, M_DIRFS_MISC, M_WAITOK | M_ZERO);
1329:  nlen = readlinkat(pathnp->dn_fd, dnp->dn_name, buf, uio->uio_resid);
1330:  if (nlen == -1 ) {
1331:      error = errno;
1332:  } else {
1333:      error = uiomove(buf, nlen + 1, uio);   // reads nlen+1 bytes
1334:          buf[nlen] = '\0';                   // writes at index nlen
```

- **:1328** allocates **exactly N = `uio->uio_resid`** bytes (M_ZERO is cosmetic
  for the overflow; it only zeroes the N allocated bytes, not byte N).
- **:1329** `readlinkat(..., buf, uio->uio_resid)` — POSIX returns up to
  `bufsiz`; when the symlink target length ≥ N it returns **`nlen == N`** (the
  buffer is exactly full, no room for a terminator).
- **:1334** `buf[nlen] = '\0'` ⇒ `buf[N] = '\0'` — writes 1 byte **past the
  N-byte allocation** ⇒ **CWE-787 off-by-one heap overflow (OOB write)**.
- **:1333** `uiomove(buf, nlen + 1, uio)` ⇒ copies `N+1` bytes from an `N`-byte
  buffer ⇒ **CWE-125 1-byte OOB read** (the extra byte is also leaked to the
  user via the UIO_READ `uio`).

`uio->uio_resid` is the **raw user-supplied count**, unclamped:
`sys/kern/vfs_syscalls.c:3211  auio.uio_resid = count;` (`kern_readlink`), where
`count` is the user's `readlink(path, buf, count)` argument. There is no upper
bound or validation, so any `N ≥ 1` is reachable.

## Reachability on this guest (why a harness)

dirfs is **vkernel-only**. Confirmed:
- `grep -c dirfs /usr/src/sys/conf/files` ⇒ **0** (not in the host-kernel file
  list).
- `grep -c dirfs /usr/src/sys/platform/vkernel64/conf/files` ⇒ **3**
  (`dirfs_vnops.c`, `dirfs_vfsops.c`, `dirfs_subr.c` as `optional dirfs`).
- `/boot/kernel/dirfs*` ⇒ **does not exist**; `kldstat | grep dirfs` ⇒ **none**.
- The running kernel is `6.5-DEVELOPMENT #0` (X86_64_GENERIC) which does not
  include `options DIRFS`.

dirfs is a pass-through filesystem that runs **inside a vkernel** (a userspace
process that simulates a kernel). There is no vkernel running on this guest,
and even building+booting one and mounting a dirfs inside it to reach
`dirfs_readlink` is not feasible in this single-tenant host-kernel guest.
Per the finding's authorization and the spec's dead-code/latent-bug clause
(DF-0594/0616/0281 precedent), the **deterministic harness** is the accepted
proof: it transcribes the exact buggy operations (:1328–:1334) with a
guard-page allocator so any access to `buf[N]` faults deterministically.

## Reproduction — harness (`harness.c`)

`./harness` exercises N ∈ {16, 32, 64, 128, 256} (kmalloc buckets). For every N:

```
VULNERABLE dirfs_readlink transcription (kmalloc(N), nlen=N):
  line 1334 buf[nlen]='\0'  : FAULT (1-byte heap overflow / OOB WRITE confirmed)
  line 1333 uiomove(buf,N+1) : FAULT (1-byte OOB READ confirmed)
FIXED transcription (kmalloc(N+1), uiomove(buf,nlen)):
  buf[nlen]='\0'            : no fault (in-bounds)
  uiomove(buf,N)            : no fault (in-bounds)
=> BUG PRESENT (OOB detected); FIX VALID (no OOB)
```

Result over 3 runs is byte-identical (guard-page detection is deterministic).
Full output: `run.log`; stress runs: `run.stress.log`.

## Impact ceiling

- **1-byte heap overflow** writing `'\0'` one past a `kmalloc(N)` buffer
  (`M_DIRFS_MISC` bucket), plus a **1-byte OOB read** leaked to userspace via
  the `uiomove` UIO_READ.
- The write value is a fixed `0x00` (NUL), not fully attacker-controlled.
- On a **vkernel** deployment, this corrupts the vkernel process's heap (dirfs
  code runs in the vkernel userspace process). It is a vkernel-heap corruption
  primitive, **not** a host-kernel corruption.
- This guest runs the host kernel with **no dirfs**, so the primitive is not
  live-reachable here; the harness is the proof at the object/transcription
  level. The honest demonstrated impact is the **confirmed memory-corruption
  primitive** (1-byte OOB write + 1-byte OOB read); escalation to `uid=0` was
  neither attempted nor claimed because the vulnerable code path is not
  reachable on this guest's default kernel.
- INVARIANTS (ON on GENERIC) would, in a vkernel build with INVARIANTS, catch
  cross-slab grooming via the `kern_slaballoc.c` magic/poison checks; on this
  guest that is moot (dirfs absent).

## Exploit chain

none (not applicable). This is a **vkernel-only** memory-corruption primitive
that is not reachable on this guest's default X86_64_GENERIC kernel, so there
is no userspace `uid=0` chain to develop or demonstrate here. The realistic
ceiling is the confirmed 1-byte heap overflow + 1-byte OOB read, characterized
via the deterministic harness.

## The fix — `fix.diff`

```diff
-	buf = kmalloc(uio->uio_resid, M_DIRFS_MISC, M_WAITOK | M_ZERO);
+	buf = kmalloc(uio->uio_resid + 1, M_DIRFS_MISC, M_WAITOK | M_ZERO);
 	nlen = readlinkat(pathnp->dn_fd, dnp->dn_name, buf, uio->uio_resid);
 	if (nlen == -1 ) {
 		error = errno;
 	} else {
-		error = uiomove(buf, nlen + 1, uio);
+		error = uiomove(buf, nlen, uio);
 		buf[nlen] = '\0';
```

- Allocate `N+1` bytes so `buf[N]` (the NUL terminator at index `nlen==N`) is
  in-bounds.
- `uiomove(buf, nlen, uio)` copies exactly `nlen` bytes — no OOB read. The NUL
  is **not** part of the data returned to userspace by `readlink()` (POSIX
  `readlink` does not NUL-terminate); the original `nlen+1` was the bug, copy
  the actual link length only.

The harness "FIXED transcription" path proves the logic closes the OOB
deterministically across all tested bucket sizes (no fault on `buf[N]` access
with the `N+1` allocation).

## Fix validation (Phase 8)

- `git apply --check` / `patch --dry-run` on a clean `/usr/src` tree ⇒ **RC=0**
  (applies cleanly).
- Patched source reads correctly: `:1328` ⇒ `kmalloc(uio->uio_resid + 1, ...)`;
  `:1333` ⇒ `uiomove(buf, nlen, uio)`.
- **Compile neutrality proven:** dirfs is `optional dirfs` only in
  `sys/platform/vkernel64/conf/files`, so a `VKERNEL64_DIRFS` config was
  generated (`config -d …/compile/VKERNEL64_DIRFS VKERNEL64_DIRFS`). Compiling
  the patched `dirfs_vnops.o` standalone fails on `M_WAITOK`/`M_ZERO`/
  `uiomove`/`kmalloc`/`kfree` undeclared — but **compiling the UNPATCHED
  `dirfs_vnops.o` fails with the identical error set at the identical lines**
  (653, 1057, 1078, 1186, 1250, 1328). `dirfs_vnops.c` simply omits the
  `<sys/malloc.h>`/`<sys/uio.h>` includes that its sibling `dirfs_vfsops.c`
  has, so it does not stand alone outside the full vkernel build. The sibling
  `dirfs_vfsops.o` **did compile cleanly** (190 KB) in the same vkernel build
  environment. Conclusion: **the fix introduces zero new compile errors**
  (patched ≡ unpatched); the residual standalone failure is a pre-existing
  missing-transitive-include issue in `dirfs_vnops.c`, unrelated to this fix.
- **Live boot test: `not_testable`** — dirfs is not in the host kernel and no
  vkernel runs on this guest, so the `readlink`→`dirfs_readlink` path cannot
  be exercised on a live kernel here. The harness transcription is the
  deterministic proof that the fix closes the OOB.

## PoC changes

- `harness.c` (pre-existing from the aborted run) — kept as-is; it is a correct
  deterministic transcription of `:1328–:1334` with a guard-page allocator. No
  source changes were needed; it builds and runs cleanly.
- `build.sh` / `run.sh` (pre-existing) — kept as-is.
- `fix.diff` (pre-existing) — confirmed correct (`git apply --check` RC=0,
  compile-neutral, harness-validates).
- Regenerated: `build.log`, `run.log`, `run.stress.log`, `env.txt`,
  `fix_run.log` (full evidence of this run).

## How to reproduce

```
ssh dfbsd-maxx   # unprivileged (uid 1001)
cd poc/DF-0806
./build.sh && ./run.sh
# expected: "BUG CONFIRMED + FIX VALIDATED" with FAULT lines for the
#           vulnerable transcription and "no fault (in-bounds)" for the fixed.
```
