# DF-0023 — Verdict

**Verdict:** REPRODUCED (and the impact is stronger than the Info rating — it is
also a **local DoS**, not merely a correctness/defense-in-depth nit).
**Fix:** VALIDATED on a built-and-booted single-fix kernel.

---

## The bug (confirmed line-by-line)

`sys/kern/sys_generic.c` has three syscall entry points that validate the
user-supplied `nbyte` (a `size_t`) against `SSIZE_MAX` by assigning `EINVAL`
to a local `error` — **but never `return`ing**:

| Function      | Lines    | Code (buggy)                          |
|---------------|----------|---------------------------------------|
| `sys_read`    | 130–131  | `if ((ssize_t)uap->nbyte < 0) error = EINVAL;` |
| `sys_write`   | 336–337  | `if ((ssize_t)uap->nbyte < 0) error = EINVAL;` |
| `sys_extpwrite` | 368–369 | `if ((ssize_t)uap->nbyte < 0) error = EINVAL;` |

The dead-stored `EINVAL` is unconditionally overwritten a few lines later:

```c
error = kern_preadv(uap->fd, &auio, 0, &sysmsg->sysmsg_szresult);   /* sys_read  :143 */
error = kern_pwritev(uap->fd, &auio, 0, &sysmsg->sysmsg_szresult);  /* sys_write :349 */
error = kern_pwritev(uap->fd, &auio, flags, &sysmsg->sysmsg_szresult); /* sys_extpwrite :384 */
return(error);
```

so the `SSIZE_MAX` guard is a complete no-op.  The sibling `sys_extpread`
(`:161-162`) implements the identical check **correctly** as `return(EINVAL);`,
proving the intent and giving the fix its template.

## Reproduction (unpatched `#0` kernel)

Two observable effects, both reachable by any unprivileged user via the default
read(2)/write(2) surface:

### 1. Correctness bypass — `read`
```
$ ./einval_noop
read(fd,buf,SIZE_MAX) = 0, errno=0 (NOT EINVAL)
```
`read(/dev/null, buf, SIZE_MAX)` returns **0 with errno=0** instead of the
POSIX-mandated `-1/EINVAL`.  The `ssize_t` return-range contract is silently
broken for every file-ops implementation downstream.  (This is the
finding's documented Info-level effect.)

### 2. Local DoS — `write` (stronger than rated)
```
write child PID 869: probing write(fd,buf,SSIZE_MAX+1)...
write child PID 869: HUNG in kernel after 4s (DoS - uninterruptible infinite loop in mmrw)
after SIGKILL: child STILL ALIVE (unkillable in kernel loop)
```
`write(/dev/null, buf, 0x8000000000000000)` enters an **infinite,
uninterruptible kernel loop** and the calling process becomes **unkillable**
(`SIGKILL` cannot be delivered — it never leaves the kernel to take the
signal).  Mechanism, traced end-to-end:

```
sys_write        sys_generic.c:336-337   dead-store EINVAL -> falls through
  kern_pwritev   sys_generic.c:456       holdfp(FWRITE), no nbyte re-check
    dofilewrite  sys_generic.c:506       fo_write(fp, auio, ...)
      mmwrite    kern_memio.c:396        -> mmrw(dev, uio, flags)
        mmrw     kern_memio.c:222
```
Inside `mmrw` (`kern_memio.c:225-383`):
- line 225 declares `u_int c;`  (32-bit)
- the `/dev/null` write arm (minor 2, line 292-299) does `c = iov->iov_len;`
  where `iov->iov_len` is `size_t` (64-bit) = `0x8000000000000000`, so **`c`
  truncates to 0**;
- the loop tail (line 379-382) does `uio->uio_resid -= c;` ⇒ subtracts 0,
  so `while (uio->uio_resid > 0)` (line 232) **never terminates**;
- the loop body performs no signal/`CURSIG` check, so the LWP is never
  interrupted — `SIGKILL` is queued but never delivered.

An unprivileged user can therefore permanently pin every CPU core with
unkillable processes via one `write()` syscall each.  This is a genuine local
DoS — higher than the finding's Info rating, which anticipated the loop would
exit "at a natural boundary (EOF / empty socket buffer / EFAULT)".  For
`/dev/null` there is no such boundary because `c` truncates to 0.

> Note: this DoS is *caused by* DF-0023's missing return — without the
> missing return, `nbyte > SSIZE_MAX` is rejected at the syscall layer and
> `mmrw` is never reached with a pathological `uio_resid`.  The `u_int c`
> truncation in `mmrw` is a contributing latent defect, but DF-0023's
> missing return is the unprivileged trigger and the minimal fix.

## Non-corruption class → no escalation chain

This is a logic/DoS bug, not memory corruption.  No slab grooming, UAF,
type-confusion, or arbitrary write is produced (`uiomove`/`copyout`/`copyin`
bound any actual data transfer to the user address range).  Phase 6
(escalation to `uid=0`) is therefore **not applicable**.

## PoC changes

- `einval_noop.c` — reworked to (a) flush stderr before each syscall so
  output survives a panic/wedge, (b) probe `read` first (clean EINVAL-bypass
  demonstration), then (c) **fork** a child for the `write` probe so the
  parent survives to report the hang and prove the child is unkillable.
  Added `#include <signal.h>` (the original used `kill`/`SIGKILL` without it
  and failed to compile on DragonFlyBSD gcc 8.3).
- `write_only.c` — new, separate probe for the `sys_write` path so the
  `write` location can be exercised without forking (used for the patched-
  kernel confirmation that `write` now returns EINVAL).

## The fix (`fix.diff`)

Add the missing `return` in all three sites, mirroring the correct
`sys_extpread`:

```diff
-	if ((ssize_t)uap->nbyte < 0)
-		error = EINVAL;
+	if ((ssize_t)uap->nbyte < 0)
+		return (EINVAL);
```
at `sys/kern/sys_generic.c:130-131` (sys_read), `:336-337` (sys_write),
`:368-369` (sys_extpwrite).  This matches the finding markdown's `## Recommended
fix` proposal (the markdown also noted the correct `sys_extpread` template).
The diff is `git apply`-clean (`git apply --check` passes) with correct hunk
line numbers.

## Fix validation (Phase 8)

- **Baseline (unpatched `#0`, `6.5-DEVELOPMENT #0` Thu Jul 2 06:02:54 UTC
  2026):** `read` returns `0 errno=0` (NOT EINVAL); `write` hangs in an
  unkillable kernel loop.  *(run.log)*
- Applied ONLY `fix.diff` to `/usr/src`, built
  `make -j6 nativekernel KERNCONF=X86_64_GENERIC` (rc=0, ~7 min, warm obj),
  swapped `/boot/kernel/kernel` ← `kernel.stripped`, rebooted cleanly.
- **Patched (`#1`, `6.5-DEVELOPMENT #1` Tue Jul 14 19:31:57 UTC 2026,
  sha256 `c9a066…d7b8d43`):**
  - `read(fd,buf,SIZE_MAX) = -1, errno=22 (EINVAL)`  ✅
  - `write(fd,buf,SSIZE_MAX+1) = -1, errno=22 (EINVAL)`  ✅ (no hang)
  - guest stays up; deterministic across 3 runs.  *(fix_run.log)*

`fix_status = fixed` — clean before/after on a built-and-booted single-fix
kernel.  The EINVAL guard now behaves identically to `sys_extpread`, and the
local-DoS hang via `/dev/null` write is eliminated.
