# DF-0053 — PoC (master DEV re-verification)

**Status: REPRODUCED** on `DragonFly v6.5.0.1712.g89e6a-DEVELOPMENT` (built
2026-06-29, x86_64, `X86_64_GENERIC` config with INVARIANTS).

The prior `not_reproduced` verdict was an OID-name typo: the sysctl is
**`jail.list`** (top-level, `SYSCTL_OID(_jail, OID_AUTO, list, ...)` at
`sys/kern/kern_jail.c:757`), NOT `kern.jail.list`. With the correct OID the
bug fires immediately.

## The bug

`sysctl_jail_list` (`sys/kern/kern_jail.c:661`) sizes its output buffer as
`count * 1024` bytes (one 1024-byte budget per jail) but the per-jail
formatted output (`"%d %s %s"` = `pr_id` + `pr_host` + `fullpath`,
kern_jail.c:704-706) can reach ~1282 bytes (`MAXHOSTNAMELEN=256` +
`cache_fullpath` MAXPATHLEN=1024 + small jid). `ksnprintf` returns the
**would-be** (untruncated) length (`sys/kern/subr_prf.c:549` `retval++` is
unconditional; `snprintf_func` at `:494` only writes while `remain>=2`), so
`jlsused += count` (`kern_jail.c:710`) makes `jlsused` exceed `jlssize`. The
IP loop's bounds check `(jlssize - jlsused)` (`:733`) then **underflows** (both
are `unsigned int`, `:671`) to ~`UINT_MAX` → check `(huge < strlen(oip)+1)` is
FALSE → `ksnprintf(jls+jlsused, ~UINT_MAX, " %s", oip)` at `:737` writes the IP
string at `jls+jlsused` **past the buffer end** → kernel heap OOB write. The
final `SYSCTL_OUT(req, jls, jlsused)` at `:749` copies `jlsused` bytes from the
allocation to userspace → kernel heap OOB read (info leak of adjacent slab
slack).

## Reachability

The handler's only check is `jailed(td->td_ucred)` (`:679`); any
**unprivileged unjailed** user can read the sysctl. Precondition: at least one
jail whose `host + fullpath` formatted length exceeds ~1024 bytes (a deep
chroot path created by root — realistic on jail/container hosts).

## Reproduce

```
# Phase 1 (root): create the long-path jail
ssh dfbsd   # or: vm.sh run_root 'sh /path/to/setup_jail_v3.sh 60 4'
sh findings/poc/DF-0053/setup_jail_v3.sh 60 4

# Phase 2 (any user): trigger the bug
ssh dfbsd-maxx
cd findings/poc/DF-0053 && cc -O2 -o jail_list_trigger jail_list_trigger.c
./jail_list_trigger
```

Or use the convenience scripts:

```
./build.sh                       # cc -O2 -o jail_list_trigger jail_list_trigger.c
sudo sh ./run.sh setup           # root: provision the jail
su maxx -c ./run.sh              # maxx: trigger (or any unprivileged user)
```

## Expected output (bug present)

```
[+] BUG DF-0053 CONFIRMED:
    kernel returned 1262 bytes
    jlssize (count*1024)         = 1024
    kmalloc bucket (alloc)       = 1152
    OOB READ vs jlssize          = 238 bytes
    OOB READ vs actual alloc end = 110 bytes (info leak of adjacent slab slack)
    OOB WRITE (IPs written past alloc end during IP loop) also occurred in kernel heap
    non-zero bytes in OOB-vs-alloc region: 37 (our written IPs + any stale slab data)
```

The kernel returned 1262 bytes from a buffer that is logically 1024 bytes
(`jlssize`) and physically 1152 bytes (the slab bucket `zoneindex()` rounds
`kmalloc(1025)` up to — `sys/kern/kern_slaballoc.c:663` `(1025+127) & ~127 =
1152`). The trailing 110 bytes (offsets 1152..1262) were never part of the
allocation — they are adjacent slab slack that the kernel both **wrote into**
(the IP strings, OOB write) and **copied out to userspace** (OOB read / info
leak).

A fixed kernel would clamp `jlsused` to `jlssize` whenever truncation occurs,
and would never return more bytes than `jlssize`.

## Impact

- **OOB write** into adjacent kernel heap slab memory (the IP strings, ~36 bytes
  with 4 IPs; grows linearly with the jail's IP count up to jail(8)'s IP-parse
  limit). Content is the jail's formatted IP/hostname/path — set by root at
  `jail(2)` time, not directly attacker-shaped, but the write POSITION and
  LENGTH are attacker-observable and the trigger is freely repeatable. With
  heap grooming this is a memory-corruption primitive that could be converted
  to local privilege escalation (corrupt an adjacent `struct file`/`ucred`/
  function-pointer victim in the 1152-byte bucket).
- **OOB read / info leak** of up to ~110 bytes of adjacent slab slack to
  userspace per read. In this run the leaked bytes are zero (the adjacent
  chunk was freshly `M_ZERO`-allocated) plus our own written IPs; with heap
  grooming (spray the 1152-byte bucket with pointer-bearing objects before
  triggering), this leaks kernel pointers / stale slab data.
- **Local DoS** is straightforward (the OOB write corrupts adjacent slab;
  repeated triggers can panic a less-fortunate kernel layout — not observed
  in 50x rapid repeats on this build, but the corruption is real).

High severity: confirmed memory corruption + info leak, reachable by any
unprivileged local user via a sysctl read, with a realistic precondition
(a long-chroot-path jail).
