# DF-0031 — VERDICT

## Verdict: NOT REPRODUCED (latent code bug confirmed; PoC cannot trigger it)

The **code bug is real** but **not reachable** by an unprivileged user on the
default-config guest. The PoC does not reproduce the claimed leak because its
precondition (kernel_map exhaustion) is impossible given default fd limits.

## The code bug (confirmed by source trace)

`pipe_create` (`sys/kern/sys_pipe.c:414-448`) sets `*pipep = pipe` (`:433`) and
calls `pipespace()` for bufferA (`:434`) and bufferB (`:437`) **before** setting
`pipe->open_count = 2` (`:445`). The pipe is allocated with `M_ZERO` (`:426`),
so on a partial failure (bufferA succeeds, bufferB's `vm_map_find` returns
`ENOMEM`), `pipe_create` returns non-zero with `open_count` still `0`.

`kern_pipe`'s error path (`sys/kern/sys_pipe.c:286-290`) then calls `pipeclose()`
twice. `pipeclose` (`sys/kern/sys_pipe.c:1272`) gates the free on
`atomic_fetchadd_int(&pipe->open_count, -1) == 1`. With `open_count = 0`:

| call | fetchadd returns | new open_count | frees? |
|------|------------------|----------------|--------|
| 1st pipeclose | 0   | 0xFFFFFFFF | no (0 != 1) |
| 2nd pipeclose | 0xFFFFFFFF | 0xFFFFFFFE | no (0xFFFFFFFF != 1) |

→ the pipe struct + bufferA's KVA leak permanently; `open_count` is corrupted
(underflow). This is a genuine correctness bug **on the failure path**.

## Why it is NOT reproducible here (reachability)

The failure path requires `pipespace(&bufferB)` to fail, i.e. `vm_map_find` on
`kernel_map` to return `ENOMEM`. That needs `kernel_map` exhaustion. On this guest:

| Fact | Value |
|------|-------|
| `vm.kvm_size` (kernel KVA) | 8,795,004,596,224 (~8.0 TB) |
| `vm.kvm_free` | ~8,790,277,971,276 (~8.0 TB free) |
| `kern.maxfiles` (system-wide fds) | 130,112 |
| `kern.maxfilesperproc` | 32,528 |
| max pipes system-wide | ~65,056 (= maxfiles/2) |
| KVA per pipe | ~64 KB (2 × `pipe_size` 32 KB) |
| **max KVA an unpriv user can pin via pipes** | **~4 GB (0.00005 % of kvm)** |

To exhaust `kernel_map` and trip the 2nd `pipespace` ENOMEM would need ~145
**million** pipes — impossible given fd limits. Direct measurement: exhausting
the process fd table, `pipe()` fails with **EMFILE (errno 24, "Too many open
files")** after **16,263 pipes**. That is `falloc()` failing at
`sys/kern/sys_pipe.c:292-297`, **not** `pipe_create()`'s `pipespace()`. The bug
path is never entered.

The PoC's own `HOLD = 200000` is itself impossible (`> maxfiles`).

## Run evidence (unpatched #0 kernel)

```
[*] holding 32526 pipe fds to build kernel_map pressure   <- hit fd limit, not kvm
[*] opened=0  failures(likely underflow-leak path)=1000000 <- all EMFILE, not the bug
vm.kvm_free before = 8790279712768
vm.kvm_free after  = 8790279712768   <- IDENTICAL (zero leak)
```

So: `not_reproduced`, category **(d) genuinely not reachable on this kernel via
the unprivileged PoC path** — but **not a false positive** (the code bug is real
and would manifest if kernel_map were ever exhausted, e.g. by root loading huge
modules, or on a system with a much smaller KVA window).

## Impact ceiling

Pure availability, and only latent: a self-amplifying KVA/struct leak **if** the
precondition can be met. On a default-config DragonFlyBSD guest the precondition
is unreachable for an unprivileged user, so the realistic impact is **none**.
This matches the finding's Low severity (CVSS A:L).

## No escalation (resource leak class, non-corruption)

Even if the leak fired, it is a resource leak (KVA + struct), not a memory-
corruption primitive — there is no write/UAF/double-free, so there is no path to
`uid=0`. The ceiling is DoS via KVA exhaustion, which is itself unreachable here.

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

Move `pipe->open_count = 2;` from after the two `pipespace()` calls to **before**
them (right after `*pipep = pipe;`). Then on a partial failure, `kern_pipe`'s
double-`pipeclose` cleanup decrements `2 → 1 → 0`; the second call sees old==1
and frees/caches the pipe (and `pipe_free_kmem` releases bufferA's KVA). No
underflow, no leak.

The cached-pipe path is unaffected: a cached pipe already has `open_count = 0`
(decremented to 0 when cached in `pipeclose`), and the early `open_count = 2`
overwrites it correctly on success.

### Fix validation (Phase 8)

| step | result |
|------|--------|
| `git apply --check -p1` | passes |
| in-guest `patch -p1` | both hunks succeeded |
| `make -j6 nativekernel` | **rc=0**, no errors/warnings |
| boot | `#1` (Sun Jul 12 22:43:48 UTC 2026), new BuildID `949f92ee…` |
| regression: 5000 pipe write/read round-trips | **OK** (no functional regression) |
| PoC on patched kernel | same EMFILE behavior, no panic, guest up |

`fix_status = not_testable`: the leak precondition is unreachable on default
config, so there is no observable bad behavior on either kernel to contrast. The
fix is validated by apply + compile + boot + no-regression + source inspection of
the closed error path.

## PoC changes

None to `pipe_leak.c` (it builds and runs as-is; it simply cannot reach the bug
path). Added: `build.sh`, `run.sh`, `fix.diff`, `VERDICT.md`, `manifest.json`,
full logs (`run.log`, `fix_build.log`, `fix_run.log`, `env.txt`).

## kernel_refs (confirmed)

- `sys/kern/sys_pipe.c:433` — `*pipep = pipe` handed out before pipespace.
- `sys/kern/sys_pipe.c:434,437` — the two `pipespace()` calls.
- `sys/kern/sys_pipe.c:445` — `open_count = 2` set only after both succeed (the bug).
- `sys/kern/sys_pipe.c:286-290` — `kern_pipe` double-`pipeclose` error cleanup.
- `sys/kern/sys_pipe.c:1272` — `pipeclose` `fetchadd == 1` free gate (underflow site).
