# DF-0070 PoC — elf_getnote heap OOB read via crafted checkpoint image

**Status: REPRODUCED (kernel panic / local DoS).** Verified on DragonFlyBSD
master DEV `v6.5.0.1712.g89e6a-DEVELOPMENT` (build 2026-06-29, X86_64_GENERIC).
See `VERDICT.md` for the full analysis.

## What this proves

That `elf_getnote` (`sys/kern/kern_checkpoint.c:313-352`) advances a parse
offset using an attacker-supplied `n_namesz` (read from the untrusted note
buffer at `:325`, applied at `:339`) with **no bounds check** against the
allocated buffer size `notesz`. A crafted checkpoint image with
`n_namesz = 0x10000000` causes the subsequent `bcopy` at `:346` to read
`sizeof(prpsinfo_t)=120` bytes from KVM 256 MB past the `kmalloc(880)` note
buffer allocated at `:194`. The `bcopy` (backed by `memmove`) hits unmapped
memory and the kernel page-faults in kernel mode (`Fatal trap 12`) → panic
(`vm_object_hold_shared` assertion `obj != NULL`).

## Build

```sh
./build.sh            # cc -o df0070 df0070.c   (on a DragonFlyBSD guest)
```

## Run

Default `kern.ckptgroup=0` (wheel-only) — run as **root**.

```sh
./run.sh              # ./df0070 evil.ckpt panic  -- kernel PANICS
./run.sh leak         # ./df0070 evil.ckpt leak   -- silent slab-adjacent
                      #                              116-byte OOB; returns
                      #                              EINVAL, no panic
```

## Expected result (panic mode)

```
[*] DF-0070 PoC: building evil.ckpt  (notesz=880, n_namesz=0x10000000, n_descsz=120, mode=panic)
[*] calling sys_checkpoint(CKPT_THAW, fd=3, pid=-1, retval=0) [syscall #467]...
<ssh dies -- guest in DDB>

# in dfbsd-qemu/boot.log:
panic: assertion "obj != NULL" failed in vm_object_hold_shared at /usr/src/sys/vm/vm_object.c:330
cpuid = 0
...
--- trap 000000000000000c, rip = ffffffff80bca038, ... ---
memmove() at memmove+0x28 0xffffffff80bca038
Debugger("panic")
db>
```

The page fault is in `memmove+0x28` — that is the inner `bcopy` backing the
descriptor copy at `kern_checkpoint.c:346`. Reproduced twice with
byte-identical code offsets.

## Expected result (leak mode)

The kernel silently performs a 116-byte slab-adjacent OOB read (no page
fault — the read stays inside the 1024-byte slab chunk), then
`elf_loadnotes` rejects the leaked garbage at the `pr_version`/`pr_psinfosz`
validation (`:292-301`) and returns `EINVAL`. The OOB read is real but
silent; control does not reach the `strlcpy(p->p_comm, ...)` at `:306`, so
the leak is not observable in `ps`/`sysctl`.

## How the sizes were derived

`probe.c` prints the actual kernel-side struct sizes:

```
sizeof(prpsinfo_t)   = 120
sizeof(prstatus_t)   = 248
sizeof(prfpregset_t) = 512
sizeof(Elf_Note)     = 12
```

So the `nthreads` formula at `:185`, `(notesz - 120) / 760`, yields `1` for
`notesz = 880` — inside the `[1, CKPT_MAXTHREADS=256]` gate at `:188`.

## Notes

- `n_descsz` **must equal** the kernel struct size at the `:340` check, so
  for the first (`NT_PRPSINFO`) call it is `120` (`sizeof(prpsinfo_t)`).
- `strncmp("CORE", src+*off, n_namesz)` at `:335` stops at the `'\0'` in
  `"CORE\0"` within 5 bytes regardless of `n_namesz`, so a giant
  `n_namesz` does not stop the match — it only inflates the subsequent
  `*off` advance.
- The original (pre-verification) PoC assumed wrong struct sizes
  (`PRPSINFO_SZ=128`, `PRSTATUS_SZ=504`); those were corrected.
- `CKPT_THAW` requires membership in `kern.ckptgroup` (default `0` = wheel).
  With `kern.ckptgroup=-1` any local user can trigger the panic.

## Files

- `df0070.c` — the generator + inline trigger.
- `probe.c` — struct-size probe (used to derive `notesz=880`).
- `build.sh`, `run.sh` — exact reproduce commands.
- `build.log`, `run.log`, `run.2.log`, `run.leak.log` — full untrimmed logs.
- `panic.txt` — the panic signature excerpted from `boot.log`.
- `env.txt` — guest environment.
- `fix.diff` — `git apply`-able fix (thread `srcsz`/`notesz` into
  `elf_getnote`, bounds-check every access).
- `VERDICT.md`, `manifest.json`.
