# DF-2718 — PT_INTERP string never NUL-terminated → kernel heap OOB read + disclosure via uprintf

- **File**: sys/kern/imgact_elf.c
- **Severity**: Medium (kernel info leak, unprivileged local)
- **Confidence**: certain — reproduced on the guest
- **Class**: kernleak / CWE-125 (OOB read) + CWE-200 (info disclosure)

## Root cause

`exec_elfN(imgact)` allocates the interpreter-path buffer with the exact
PT_INTERP `p_filesz` and never requires NUL-termination:

- sys/kern/imgact_elf.c:663-673 — only checks `p_filesz == 0 || > PAGE_SIZE || > MAXPATHLEN`, then `interp = kmalloc(phdr[i].p_filesz, M_TEMP, M_WAITOK)` (no `+1`, no M_ZERO)
- sys/kern/imgact_elf.c:1817-1863 (`extract_interpreter`) — `bcopy`s exactly `pathsz` bytes and returns success; never checks `data[pathsz-1] == '\0'`

The unterminated buffer is then consumed as a C string:

- sys/kern/imgact_elf.c:584 — `strcmp(interp, bi->interp_path)`
- sys/kern/imgact_elf.c:440 (via `__elfN(load_file)` :840) — `nlookup_init(nd, file, UIO_SYSSPACE, ...)` → `copystr()` reads past the allocation (bounded by MAXPATHLEN, contents discarded)
- sys/kern/imgact_elf.c:844 — **`uprintf("ELF interpreter %s not found\n", interp)`** — `%s` formats the buffer until the first NUL **past the allocation**, writing adjacent kernel-heap contents to the executing user's controlling terminal.

## Reproduce

```
python3 make_poc.py && python3 make_poc2.py
# push seed1024 leak1000 poc_unterm to the guest, chmod +x
sh run2.sh          # seed/leak pairs, as unprivileged user, under script(1)
```

Expected (observed 18/20 iterations):

```
ELF interpreter AAAA…(1000 A's)BBBBBBBBBBBBBBBBBBBBBBBB(24) not found
```

The 24 `B` bytes are residue of the *previously freed* 1024-class M_TEMP
allocation seeded by the prior exec — i.e. kernel heap contents from a
different, freed allocation, disclosed to an unprivileged user. One
iteration additionally leaked genuine kernel path-string residue:

```
ELF interpreter AAAA…(1000)BBBB…(24)xx/.ssh/autho/home/maxx not found
```

`poc_unterm` (single run, 1024 A's) shows the raw OOB walk stopping at the
first zero byte past the slab slot.

## Fix

FreeBSD added the missing check in `extract_interpreter`
(`if (data[pathsz - 1] != '\0') return (result_failure);`). See fix.diff.
