# DF-0056 — Heap overflow via unchecked p_filesz > p_memsz in ELF loader

## Verdict: REPRODUCED (kernel heap overflow -> panic); root-only (defense-in-depth)

`link_elf_load_file()` (`sys/kern/link_elf.c:388-...`), the kernel-module
loader invoked by `kldload(2)`, sizes its buffer from `p_memsz` but writes
`p_filesz` bytes into it — with no `p_filesz <= p_memsz` validation:

```
link_elf.c:541-543   base_vlimit = round_page(segs[1]->p_vaddr + segs[1]->p_memsz);
                     mapsize     = base_vlimit - base_vaddr;
link_elf.c:546       ef->address = kmalloc(mapsize, M_LINKER, M_WAITOK);   /* sized by memsz */
link_elf.c:554-556   vn_rdwr(UIO_READ, vp, segbase, segs[i]->p_filesz, ...);  /* writes filesz */
link_elf.c:562-563   bzero(segbase + p_filesz, p_memsz - p_filesz);           /* underflow! */
```

If `p_filesz > p_memsz`:
1. `vn_rdwr` writes `p_filesz` bytes into a buffer sized for `p_memsz` →
   **heap overflow** of `p_filesz - round_page(...p_memsz...)` bytes;
2. `bzero(segbase + p_filesz, p_memsz - p_filesz)` — the length is negative,
   wraps to a huge `size_t` → **massive zero-fill past the buffer**.

## Reproduction (root only)

`craft_ko.py` builds a minimal `ET_DYN` ELF with a PT_LOAD segment where
`p_memsz=0x100`, `p_filesz=0x2000`.  `kldload` as root:

```
baseline (#0):  Fatal trap 12: page fault while in kernel mode
                fault virtual address = 0xfffff8011891f000   (supervisor WRITE, page not present)
                Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)
```

The page fault is in `memmove` (the `bcopy` inside `vn_rdwr`) writing
`p_filesz` bytes past the `mapsize`-sized heap allocation — the overflow is
real and the bytes (`0x41` from the file) are attacker-controlled.

## Why this is NOT an unpriv->root escalation (bright-line rule)

`sys_kldload` (`kern_linker.c:794`) gates on `caps_priv_check_self(SYSCAP_NOKLD)`
— **root only**.  There is no unprivileged path to this write; it is a
root->kernel hardening gap.  The primitive (controlled heap overflow +
underflow bzero) is genuine, but the privilege boundary to cross (root ->
kernel) is already game-over, so it does not yield `uid=0` from an
unprivileged user.  Reported as `panic`/`corruption` for the default kernel.

## Fix (validated on a built single-fix kernel)

`fix.diff` adds `if (phdr->p_filesz > phdr->p_memsz) { ENOEXEC; goto out; }`
in the `PT_LOAD` case, before the segment is recorded.  Validated end-to-end:

```
patched (#1):  kldload: p_filesz > p_memsz in PT_LOAD
               KLD_RC=1, module NOT loaded, guest stays UP
```

vs the baseline panic above.
