# DF-0025 — VERDICT

**Status:** REPRODUCED (info leak) — and the proposed fix is VALIDATED on a
single-fix kernel.

## The claim

`sys_kldstat` (`sys/kern/kern_linker.c:940`) and `sys_kldsym`
(`sys/kern/kern_linker.c:1024`) lack the `caps_priv_check_self(SYSCAP_NOKLD)`
privilege gate that `sys_kldload` (`:794`) and `sys_kldunload` (`:841`) use.
As a result any local user can dump:

- the absolute runtime address of any kernel symbol (`kldsym`), via
  `link_elf_symbol_values` (`sys/kern/link_elf.c:872,880`) which returns
  `ef->address + es->st_value` and is copied out as
  `lookup.symvalue = (uintptr_t)symval.value` (`kern_linker.c:1053,1062`);
- the base address (`lf->address`) and size of every loaded KLD (`kldstat`,
  copyout at `kern_linker.c:983,985`).

## Reproduction (unpatched `#0` kernel, default GENERIC)

```
$ ssh dfbsd-maxx    # uid=1001, no wheel, no operator
$ cd poc/DF-0025 && cc -o kld_leak kld_leak.c && ./kld_leak
[+] kldsym("proc0") = 0xffffffff81176920  (unprivileged KASLR/symbol leak)
[+] kldstat: loaded modules (base + size):
    kernel               id=1 base=0xffffffff80200000 size=28149048 refs=5
    ehci.ko              id=2 base=0xffffffff81cd9000 size= 508552 refs=1
    xhci.ko              id=3 base=0xffffffff81d56000 size= 575352 refs=1
```

Cross-check (root, post-leak):

```
$ nm /boot/kernel/kernel.debug | grep -w proc0
ffffffff81176920 B proc0      <-- matches the unprivileged leak exactly
```

Three back-to-back runs produced byte-identical output (KASLR is OFF on this
audit guest, so addresses are stable across boots; on a KASLR-on kernel the
same calls would leak the actual randomized addresses and defeat KASLR).

## Path trace (every hop confirmed in `sys/`)

| Step | Where | What |
|------|-------|------|
| syscall entry | `sys/kern/kern_linker.c:1024` `sys_kldsym` | NO `caps_priv_check_self(SYSCAP_NOKLD)` gate (vs `:794` load / `:841` unload which DO have it) |
| symbol lookup | `sys/kern/kern_linker.c:1051,1060` `lf->ops->lookup_symbol` | resolves user-supplied name to a kernel symbol |
| absolute address | `sys/kern/link_elf.c:872,880` `link_elf_symbol_values` | `symval->value = ef->address + es->st_value` (raw runtime address) |
| copyout to user | `sys/kern/kern_linker.c:1053,1062` | `lookup.symvalue = (uintptr_t)symval.value` |
| kldstat entry | `sys/kern/kern_linker.c:940` | NO gate |
| module base/size copyout | `sys/kern/kern_linker.c:983,985` | `copyout(&lf->address, &stat->address, ...)` and `copyout(&lf->size, &stat->size, ...)` |

This is a genuine CWE-862 (Missing Authorization). Severity: **Low** — info-leak
only; no corruption, no escalation primitive derivable from these syscalls
alone. The realistic impact ceiling is a KASLR-defeat / kernel-symbol-map
primitive that materially lowers the bar for any *other* kernel memory-safety
bug (e.g. DF-0013).

## Why no escalation chain (per Phase 6)

This finding is a **pure info-leak** — `kldstat`/`kldsym` are read-only query
syscalls. The primitive is "learn a kernel address"; there is no write, no
corruption, no UAF, no refcount manipulation. The escalation-chain step is
therefore not applicable (the valid hard blocker: read-only primitive).
Documented impact ceiling: full kernel symbol map + module layout handed to any
local user.

## Fix.diff (verified)

`findings/poc/DF-0025/fix.diff` adds the missing `caps_priv_check_self(SYSCAP_NOKLD)`
gate to both `sys_kldstat` and `sys_kldsym`, matching the gating that
`sys_kldload` and `sys_kldunload` already use. The fix supersedes the finding
markdown's sketch (which only sketched the call sites without unified-diff
headers) and is `git apply`-able against `sys/kern/kern_linker.c`.

### Before/after (single-fix kernel)

| Run | Kernel | maxx output |
|-----|--------|-------------|
| baseline | `#0` unpatched (`5dc83dac…`) | `kldsym("proc0") = 0xffffffff81176920` + 3 modules with addresses/sizes |
| patched  | `#1` single-fix (`ae4e6e83…`) | `kldsym: Operation not permitted` + 0 modules |
| root (patched) | `#1` single-fix | full symbol+module data (root still allowed) |

`make -j6 nativekernel KERNCONF=X86_64_GENERIC && make installkernel` rebuilt
and installed cleanly (`rc=0`). The guest booted into the `#1` kernel and the
SAME PoC now returns `EPERM` to maxx for both syscalls while root retains full
visibility — the gate is privilege-correct, not blanket-blocking.

## PoC changes

None to `kld_leak.c` — the supplied trigger compiled and ran unmodified on the
first attempt (`cc -o kld_leak kld_leak.c` → `BUILD_EXIT=0`). The PoC source
correctly uses the public `kldsym(2)` / `kldstat(2)` / `kldnext(2)` libc
wrappers. Added: `build.sh`, `run.sh`, full logs, `leak_sample.txt`, `env.txt`,
`VERDICT.md`, `manifest.json`, and `fix.diff`.

## Files

- `kld_leak.c` — minimal trigger (unchanged from reviewer draft)
- `build.sh` / `run.sh` — exact repro commands
- `build.log` — final unprivileged build output
- `run.baseline.log` — decisive unpatched-`#0` run (leak)
- `run.log` — decisive patched-`#1` run (EPERM)
- `fix_run.log` — three patched-kernel runs (stable EPERM)
- `fix_build.log` — full nativekernel + installkernel output
- `leak_sample.txt` — three runs of the unprivileged leak + nm cross-check
- `env.txt` — guest environment (uname, cc, sysctls, user)
- `fix.diff` — git-apply-able fix for `sys/kern/kern_linker.c`
- `manifest.json` — machine-readable catalog
