# DF-2586 — `sysctl_kern_msgbuf` branch-3 integer underflow (OOB read)

**Severity (finding):** Medium
**Verified verdict:** REPRODUCED — bug is real; OOB read proven by kernel panic.
**Realistic impact ceiling:** local kernel OOB read / panic, **root-triggerable only**
(stale `msg_bufr` geometry requires `kern.msgbuf_clear=1`, which is wheel-only).
The unprivileged-reachability claim in the finding is **not** supported.

## The bug

`sys/kern/subr_prf.c:1177-1184`, third branch of `sysctl_kern_msgbuf`:

```c
} else if (n <= mbp->msg_size - rindex_modulo) {
    /* Can handle in one linear section. */
    error = sysctl_handle_opaque(oidp,
                                 mbp->msg_ptr + rindex_modulo,
                                 n - rindex_modulo,     /* BUG: should be n */
                                 req);
}
```

Branches 1 and 4 correctly pass a *byte length* (`xindex_modulo - rindex_modulo`,
and `n` / `msg_size - rindex_modulo`). Branch 3 mixes a byte count (`n`) with a
buffer offset (`rindex_modulo`). When `rindex_modulo > n`, the `u_int` subtraction
silently wraps to ~4 GiB. `sysctl_old_user` (`sys/kern/kern_sysctl.c`) clips that
to the user-supplied `oldlen` and `copyout`s up to `oldlen` bytes from
`msg_ptr + rindex_modulo` — past `msg_ptr + msg_size` into adjacent kernel memory.

The branch-3 condition is `rindex_modulo > xindex_modulo` (else of branch 1) AND
`n <= msg_size - rindex_modulo` ⟺ `rindex_modulo + n <= msg_size`. In the wrap
case where `xindex_modulo == 0`, `rindex_modulo + n == msg_size`, so the correct
length is exactly `n` (== `msg_size - rindex_modulo`). The buggy formula is
`n - rindex_modulo == msg_size - 2*rindex_modulo`, which underflows whenever
`2*rindex_modulo > msg_size`, i.e. `rindex_modulo > msg_size/2`.

## Reproduce

```sh
./build.sh                 # cc -O2 -o msgbuf_oob_decisive ... -lkvm ; + dump_msgbuf, msgbuf_diag
./run.sh geometry          # dump current msgbuf + branch decision (root, non-destructive)
./run.sh decisive          # DECISIVE root-only trigger -> PANIC on #0, rc=0/in-bounds on #1
./run.sh unpriv            # unprivileged poll (300k reads) -> 0 OOB (run as maxx)
```

### Expected (bug present, unpatched `#0` kernel)

`./run.sh decisive` (as root) sets `msg_bufx = msg_size`, `msg_bufr = msg_size/2 + 100000`
via `kvm_write` (the same stale geometry `kern.msgbuf_clear=1` produces), then reads
`kern.msgbuf` with `oldlen = 1 MiB`. The kernel **panics**:

```
panic: assertion "obj != NULL" failed in vm_object_hold_shared at vm_object.c:330
--- trap 0xc, rip = std_copyout+0x15a ---
```

`trap 0xc` is a page fault raised inside `std_copyout`'s source-side read — the
underflowed length made `copyout` walk off `msg_ptr`'s mapped pages into unmapped
kernel memory. Had the adjacent memory been mapped, the same read would have
**leaked kernel-heap residue** to userspace instead of crashing. (Decisive run
stdout is lost to the panic — ssh dies before flush — but `dfbsd-qemu/boot.log`
captures the signature; see `panic.txt`.)

### Expected (FIXED, single-fix `#1` kernel)

Same PoC returns `sysctl rc=0, returned length l=424272` (= `n`, exactly in-bounds),
no panic, guest stays up. Deterministic across 2 runs (`fix_run.log`, `fix_run.2.log`).

## Honest reachability note

The OOB-underflow geometry requires `rindex_modulo > msg_size/2`, i.e. `msg_bufr`
to be "stale" (lagging far behind `msg_bufx`). In steady state `msgaddchar` keeps
`msg_bufr = msg_bufx - msg_size + 2048`, so `rindex_modulo == 2048` and the bug is
only a benign 2048-byte **under-read** (no OOB, no leak). The stale state is
produced **only** by root writing `kern.msgbuf_clear=1` (which sets `msg_bufr := msg_bufx`)
— confirmed EPERM for the unprivileged `maxx` user. A 300k-iteration unprivileged
poll produced 0 over-long / 0 suspect-tail reads (`run_unpriv.log`). So the bug does
**not** cross a privilege boundary; its realistic ceiling is root-triggerable local
kernel OOB read / DoS (panic). The code fix is still warranted (latent OOB defect,
wrong length math).

## The fix

One line — `n - rindex_modulo` → `n` in branch 3 (matches branches 1 and 4).
See `fix.diff`. Validated on a single-fix kernel built from `with-src` + `fix.diff`
only (`make installkernel`): panic on `#0`, in-bounds `rc=0` on `#1`. See `VERDICT.md`.

> **Note:** this is the same defect as DF-0035 (identical claim, file, lines, fix).
> This evidence pack reproduces and validates it independently for DF-2586.
