# DF-2552 — sbuf_extend / sbuf_extendsize int-truncation heap overflow

## Verdict: REPRODUCED (primitive confirmed at code + harness level); LATENT — not reachable from unprivileged userspace on the current kernel.

## Severity: Medium (latent code defect; defense-in-depth fix applied)

## The bug (confirmed)

`sys/kern/subr_sbuf.c:158` — `sbuf_extend()` computes the new buffer size as:
```c
newsize = sbuf_extendsize(s->s_size + addlen);
```
- `s->s_size` is `ssize_t` (64-bit on x86_64).
- `addlen` is `int` (32-bit).
- The sum `s->s_size + addlen` is computed as `ssize_t` (no 64-bit overflow for realistic values).
- But `sbuf_extendsize()` takes `int size` (`subr_sbuf.c:132`), so the `ssize_t` sum is **narrowed to `int`** when passed.

If `s->s_size + addlen > INT_MAX (0x7FFFFFFF)`, the narrowing produces a **negative** `int`. `sbuf_extendsize()` then:
1. Takes the `size < SBUF_MAXEXTENDSIZE (4096)` branch (true for any negative `int`).
2. Returns `SBUF_MINEXTENDSIZE = 16`.
3. The `KASSERT(newsize >= size)` at line 143 **passes** (16 >= negative).

Back in `sbuf_extend()`:
```c
newbuf = SBMALLOC(newsize);         // kmalloc(16, M_SBUF, M_WAITOK|M_ZERO)
memcpy(newbuf, s->s_buf, s->s_size); // memcpy(16-byte-buf, old-buf, s_size=4096) → 4080-byte overflow!
```

The `memcpy` writes `s->s_size` bytes (e.g. 4096) into the 16-byte allocation, producing a heap overflow of `s->s_size - 16` bytes.

## Arithmetic proof (run on guest as unprivileged user)

`arith_proof.c` replicates the exact `sbuf_extendsize`/`sbuf_extend` arithmetic. Key result for the trigger scenario:
```
s_size=4096, addlen=2147483647 (INT_MAX)
s_size + addlen = 2147487743  (as ssize_t)
(int)(sum)      = -2147479553  (narrowed — what sbuf_extendsize receives)
BUGGY newsize   = 16
*** HEAP OVERFLOW: kmalloc(16) then memcpy(4096 bytes) => 4080-byte overflow! ***
```

## Kernel-module harness (proves the actual primitive in the running kernel)

Since `sbuf_extend`/`sbuf_extendsize` are `static`, the only way to drive a large `addlen` into `sbuf_extend` from the exported API is through `sbuf_bcopyin()` (or `sbuf_copyin()`), which computes `addlen = (int)(len - SBUF_FREESPACE(s))`.

The harness module (`sbuftest_mod/`) creates `/dev/sbuftest` (mode 0666). Its ioctl handler:
1. Creates an sbuf via `sbuf_new(NULL, NULL, 4096, SBUF_AUTOEXTEND)` → `s_size=4096`.
2. Calls `sbuf_bcopyin(sb, &dummy, 2147487742)` → `addlen = (int)(2147487742 - 4095) = INT_MAX`.
3. Inside `sbuf_extend`: `s_size + addlen = 4096 + INT_MAX > INT_MAX` → narrowed to negative → `sbuf_extendsize` returns 16 → kmalloc(16) → memcpy(4096) → **4080-byte heap overflow**.

**Loading the module requires root (`kldload`).** This is a HARNESS that proves the primitive exists in the actual kernel code — NOT a valid unprivileged→root escalation chain (see bright-line rule). The trigger (ioctl) is unprivileged, but the module setup is root-only.

### Baseline (#0 unpatched kernel) — module output (dmesg):
```
SBUFTEST: sbuf created, s_size=4096, s_len=0, freespace=4095
SBUFTEST: trigger_len=2147487742, addlen will be (int)2147483647 = 2147483647
SBUFTEST: calling sbuf_bcopyin -> about to overflow kernel heap!
SBUFTEST: SURVIVED (unexpected — heap is corrupted, expect panic soon)
SBUFTEST: post-overflow s_size=16 (should be 16)
```
`post-overflow s_size=16` is the smoking gun: `sbuf_extend` allocated 16 bytes and copied 4096 bytes into it. The kernel survived because the old buffer was zeroed (M_ZERO), so the overflow wrote zeros into adjacent slab chunks — a **silent heap corruption**, even more dangerous than a panic.

## Reachability analysis (why this is LATENT)

`sbuf_bcopyin()`, `sbuf_copyin()`, and `sbuf_uionew()` are the **only** functions that pass a user-influenced `addlen` to `sbuf_extend()`. Verified via `rg -n 'sbuf_bcopyin|sbuf_copyin|sbuf_uionew' sys/` (excluding `subr_sbuf.c` and `sbuf.h`):

**ZERO in-tree callers.** These functions are exported (`T` in `nm /boot/kernel/kernel`) but never called by any kernel code.

All live sbuf growth in the kernel happens one byte at a time:
- `sbuf_put_byte()` → `sbuf_extend(s, 1)` — `addlen` is always the constant 1.
- `sbuf_bcat()`, `sbuf_cat()`, `sbuf_printf()` all route through `sbuf_put_byte()`.

For the `sbuf_extend(s, 1)` path to trigger the bug, `s_size` would need to already be at or above `INT_MAX - 1` (~2 GiB). Reaching that via byte-at-a-time growth requires writing ~2 GiB of data into an sbuf and ~28 doublings of the allocation — an enormous kernel heap allocation that would fail (OOM) long before approaching `INT_MAX`. No kernel path does this.

**Conclusion:** The int-truncation defect is real at the code level and confirmed by the harness, but it is **not reachable from unprivileged userspace** on the current kernel. It is a latent code defect — a future caller that wires `sbuf_bcopyin`/`sbuf_copyin` into a syscall/ioctl path would instantly turn it into an exploitable heap overflow.

## Fix

`fix.diff` adds an overflow guard in `sbuf_extend()` before the `sbuf_extendsize()` call:
```c
if (addlen < 0 || s->s_size > (ssize_t)0x7fffffff - addlen)
    return (-1);
```
This rejects any extend request where `s->s_size + addlen` would exceed `INT_MAX`, preventing the narrowing-to-negative and the resulting undersized allocation. The caller (`sbuf_bcopyin`/`sbuf_copyin`) already handles a failed extend gracefully (clamps the write to `SBUF_FREESPACE`); `sbuf_put_byte` sets `s->s_error = ENOMEM`.

## Fix validation (single-fix kernel)

Built `X86_64_GENERIC` with only this diff applied (`#1`, `Sat Aug  8 21:04:27 UTC 2026`).

| Kernel | Post-overflow `s_size` | Overflow? |
|--------|------------------------|-----------|
| #0 unpatched | **16** | **YES** — kmalloc(16), memcpy(4096), 4080-byte overflow |
| #1 patched   | **4096** (unchanged)  | **NO** — sbuf_extend returns -1, no realloc |

The fix is **validated**: on the patched kernel, `sbuf_extend` correctly detects the overflow and returns `-1`, leaving `s_size` at 4096. No heap overflow occurs.

## How to reproduce

```sh
# Build the arithmetic proof + trigger
cd findings/poc/DF-2552
./build.sh          # builds arith_proof and trigger

# Run the arithmetic proof (non-destructive, runs as unprivileged user)
./run.sh            # shows the int-truncation arithmetic

# Kernel-module harness (requires root to load):
# On the guest:
cd /root/sbuftest_mod && make obj && make
cp /usr/obj/root/sbuftest_mod/sbuftest.ko /root/sbuftest.ko
kldload /root/sbuftest.ko
# As unprivileged user:
./trigger           # triggers sbuf_bcopyin with crafted length
# Check dmesg for "post-overflow s_size=16" (unpatched) or "s_size=4096" (patched)
```
