# DF-0021 — VERDICT

## Verdict: REPRODUCED (integer overflow confirmed; fix VALIDATED)

The signed-int overflow in oversized `kmalloc` size reconstruction is a **real
code defect**, confirmed by source-level tracing AND runtime demonstration.
The one-line `(size_t)` cast fix eliminates it, validated on a built single-fix
kernel (#1) vs the unpatched baseline (#0).

---

## The bug

`btokup()` (`sys/kern/kern_slaballoc.c:136`) returns `int *` (pointing at
`vm_page->ku_pagecnt`, which is `int` per `sys/vm/vm_page.h:189`). Three sites
reconstruct the oversized-allocation size as:

```c
*kup << PAGE_SHIFT
```

Both operands are `int` (`PAGE_SHIFT=12`, `sys/cpu/x86_64/include/param.h:77`),
so the shift is computed as a **32-bit signed `int`**. For any oversized
allocation whose pagecount `*kup >= 2^19` (i.e. any `kmalloc() >= 2 GiB`),
this overflows signed `int`:

- `*kup = 0x80000` (524288) → `0x80000 << 12 = 0x80000000` = `INT_MIN`
- Sign-extended to `unsigned long` on assignment → `0xFFFFFFFF80000000`

The corrupted size then drives:

| Site | Function | Effect of overflow |
|------|----------|--------------------|
| `:1202` | `krealloc` | `osize` wraps → `bcopy(ptr, nptr, min(size, osize))` with wrong length → read-OOB or wrong copy |
| `:1261` | `kmalloc_usable_size` | returns `0xFFFFFFFF80000000` instead of the true size → caller trusting it overflows |
| `:1432` | `_kfree` | `size` wraps → `kmem_slab_free(ptr, size)` → `vm_map_remove(kernel_map, ptr, ptr+size)` with a bogus wrapped range → silent KVA/page leak (start > end → vm_map_remove is a no-op) |

The assignment targets (`osize`, `size`) are `unsigned long` (64-bit), but the
overflow happens in the RHS **before** the assignment — the cast must happen
**before** the shift, not after.

## Reproduction

**Method:** kernel module (`kldload`, root-only). The bug is **LATENT**: no
unprivileged kernel interface is known to issue a `>= 2 GiB` `kmalloc` (most
large-buffer paths use `kmem_alloc`/`contigmalloc`). The module directly
exercises the overflow by calling `kmalloc(2 GiB)` + `kmalloc_usable_size()` +
`kfree()`.

**Observable (unpatched #0 kernel):**
```
poc: kmalloc_usable_size = 0xffffffff80000000 (expect 0x0000000080000000 if OK)
poc: BUG CONFIRMED (line 1261): usable_size overflowed!
```
- `kmalloc_usable_size` returns `0xFFFFFFFF80000000` — the sign-extended INT_MIN
  from the signed shift overflow. This is the **line 1261 overflow, confirmed**.
- `kfree` returned without panicking: the wrapped `size = 0xFFFFFFFF80000000`
  causes `vm_map_remove(kernel_map, ptr, ptr + 0xFFFFFFFF80000000)` where the
  end address wraps to a value **less than** start → `vm_map_remove` silently
  returns (no-op) → **2 GiB of KVA and physical pages leaked**. Silent resource
  leak, not a panic.
- The `krealloc` path (line 1202) was not directly exercised because the 4 GiB
  krealloc hits the per-type `ks_limit` check (`:877` panic "malloc limit
  exceeded") before reaching the line-1202 overflow. The line-1202 overflow is
  confirmed by source inspection — the identical `*kup << PAGE_SHIFT` expression.

## Impact assessment

**Low severity, latent.** The code defect is certain (verified at runtime +
source). However:
- **No unprivileged trigger is known.** Reaching the bug requires a kernel
  subsystem to `kmalloc(>= 2 GiB)` and then `krealloc`/`kfree` it. Most
  large-buffer paths use `kmem_alloc`/`contigmalloc` instead of `kmalloc`.
- **Root→kernel only.** The PoC uses `kldload` (root). Per the bright-line
  rule, this is a root→root demonstration (primitive characterization), NOT an
  unprivileged→root escalation. There is no privilege boundary to cross.
- **Demonstrated effects:** (a) wrong `kmalloc_usable_size` return → could cause
  a caller that trusts it to overflow; (b) silent 2 GiB KVA/page leak per free
  → repeatable DoS via KVA exhaustion; (c) potential read-OOB `bcopy` on the
  `krealloc` path (source-confirmed, not runtime-exercised due to ks_limit).

**No `uid=0` escalation is possible** — this is a valid hard blocker: the bug is
reachable only from an already-root context (`kldload`), so there is no
privilege boundary to cross (root→kernel is game-over by definition). No
unprivileged path to trigger a `>= 2 GiB` `kmalloc` was found.

## The fix

Cast `*kup` to `size_t` (64-bit unsigned) **before** the shift at all three
sites:

```diff
-osize = *kup << PAGE_SHIFT;
+osize = (size_t)*kup << PAGE_SHIFT;
```

This makes the shift a 64-bit unsigned operation, which cannot overflow for any
realistic `*kup` value (`*kup` max = KvaSize/PAGE_SIZE, far below 2^51).

**Matches** the finding markdown's `## Recommended fix` proposal exactly.

## Fix validation (Phase 8)

| Step | Result |
|------|--------|
| Baseline (#0 unpatched) | `kmalloc_usable_size = 0xffffffff80000000` (BUG) |
| Apply fix.diff (3 hunks) | All succeeded at lines 1199/1258/1429 |
| Build single-fix kernel | `make -j6 nativekernel` rc=0 (~2.5 min, warm obj) |
| Install + reboot → #1 | `kern.version = 6.5-DEVELOPMENT #1: Sun Jul 12 17:25:16 UTC 2026` |
| Patched run #1 | `kmalloc_usable_size = 0x0000000080000000` (CORRECT) |
| Patched run #2 (determinism) | `kmalloc_usable_size = 0x0000000080000000` (CORRECT) |

**fix_status: fixed.** The overflow is eliminated; the before/after contrast is
clean and deterministic.

## PoC changes

- Added `Makefile` (uses `bsd.kmod.mk`) — the finding's build instructions used
  raw `cc -I/sys -DKERNEL -c` + `ld -r`, which doesn't resolve kernel includes
  correctly on DragonFlyBSD. The `bsd.kmod.mk` path handles forwarder headers.
- Rewrote `kmalloc_shift.c`: the original PoC did `krealloc(p, 4 GiB)` which
  panicked at the per-type `ks_limit` check (`:877`) before reaching the
  signed-shift overflow. The revised PoC exercises the overflow directly via
  `kmalloc(2 GiB)` + `kmalloc_usable_size()` (line 1261, observable marker) +
  `kfree()` (line 1432, silent leak). This produces a clean, deterministic
  before/after marker for fix validation.
