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

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

## Why the prior run said "not_reproduced"

The prior test (on 6.4.2-RELEASE) reported "KEY VERSION DIFF: the vulnerable
sysctl is named `jail.list` on 6.4.2 (`kern.jail.list` is 'unknown o...')".
That was mis-read as a version difference. It is **not** — it is an OID-name
typo in the original finding markdown. The OID has always been the top-level
**`jail.list`** (declared `SYSCTL_OID(_jail, OID_AUTO, list, ...)` at
`sys/kern/kern_jail.c:757`); there is no `kern.jail.list`. The same OID exists
on both 6.4.2 and 6.5-DEVELOPMENT (master DEV). With the correct name, the bug
fires immediately on master DEV.

## Confirmed on master DEV

```
$ sysctl -aN | grep -iE '^jail\.'
jail.jailed
jail.list            <-- the vulnerable OID
jail.defaults.vfs_mount_fusefs
...
$ sysctl -d jail.list
jail.list: List of active jails
```

`sysctlnametomib("jail.list")` resolves to OID `{258, 257}` (258 = `jail`
subtree). Reading it as the unprivileged user `maxx` (uid 1001, not in wheel):

```
[*] MIB for jail.list: 258 257
[*] kernel reports jail.list length = 1262 bytes
[*] sysctl read returned 1262 bytes
[*] 1 jail lines -> jlssize = 1024 bytes (kmalloc(1024+1) -> bucket 1152)
[+] 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)
```

Reproduced 3× (byte-identical output — see `run.3x.log`). Hammered 50×
back-to-back: no panic, but every read does a fresh OOB write into adjacent
slab memory.

## Mechanism (cited `path:line`)

Setup: a single jail with a deep chroot path (60 levels × `lllllllllllllll`
= path_len 967), `MAXHOSTNAMELEN-1 = 255`-char hostname, and 4 IPv4 IPs.
Created as root via `jail(2)` (`sys/kern/kern_jail.c:255 sys_jail`, gated by
`caps_priv_check_self(SYSCAP_NOJAIL_CREATE)` at `:265` — the only privilege
boundary; **reading** `jail.list` has no such gate).

Trigger: any unprivileged user reads `sysctl jail.list` → handler
`sysctl_jail_list` (`sys/kern/kern_jail.c:661`):

1. **Types** (`:671`): `unsigned int jlssize, jlsused;` — both 32-bit unsigned.
2. **Size + alloc** (`:688-689`): `jlssize = (count * 1024);` → `1024` for 1
   jail. `jls = kmalloc(jlssize + 1, M_TEMP, M_WAITOK | M_ZERO);` → requests
   1025 bytes. `zoneindex()` (`sys/kern/kern_slaballoc.c:663`) rounds to bucket
   `(1025+127) & ~127 = 1152`. **Physical allocation is 1152 bytes.**
3. **First ksnprintf** (`:704`): `count = ksnprintf(jls + jlsused, (jlssize - jlsused), "%d %s %s", pr->pr_id, pr->pr_host, fullpath);`
   - size arg = `1024 - 0 = 1024`.
   - format expands to `"11 " + 255*'h' + " " + 967-char path` = **1226 bytes**.
   - `snprintf_func` (`sys/kern/subr_prf.c:494`) writes only while `remain >= 2`
     → writes 1023 chars + NUL into `jls[0..1023]` (truncated, in-bounds).
   - But `PCHAR` (`subr_prf.c:549`) does `retval++` **unconditionally** — the
     return value is the **would-be** length 1226, not the truncated length.
4. **jlsused += count** (`:710`): `jlsused = 0 + 1226 = 1226`. **Now
   `jlsused > jlssize` (1226 > 1024).**
5. **IP loop bounds check** (`:733`): `if ((jlssize - jlsused) < (strlen(oip) + 1))`
   → `(1024 - 1226)` in `unsigned int` arithmetic = **`0xFFFFFB6E` (~4294966062)**.
   `4294966062 < 9` is FALSE → does NOT trip `ERANGE`.
6. **IP loop ksnprintf** (`:737`): `count = ksnprintf(jls + jlsused, (jlssize - jlsused), " %s", oip);`
   - writes at `jls + 1226` with size `~UINT_MAX` → no truncation.
   - `jls + 1226` is **74 bytes past the 1152-byte allocation end** →
     **OOB WRITE** of `" 10.0.0.1"` (9 bytes) into adjacent slab memory.
   - repeats for each IP → OOB writes at `jls+1226, +1235, +1244, +1253`
     (4 IPs). `jlsused` grows to 1262.
7. **SYSCTL_OUT** (`:749`): `error = SYSCTL_OUT(req, jls, jlsused);`
   - copies `jlsused = 1262` bytes from the 1152-byte allocation to userspace.
   - bytes `[1152..1262]` (110 bytes) are **past the allocation end** →
     **OOB READ / info leak** of adjacent slab slack.

The leaked bytes observed:
- Offsets `[1152..1225]`: zero (slab slack, freshly zeroed by `M_ZERO`).
- Offsets `[1226..1261]`: the IPs we wrote (`" 10.0.0.4 10.0.0.3 ..."` in
  SLIST-reverse order).

## Exploit chain (memory-corruption primitive)

**Bucket:** the 1152-byte slab zone (`zoneindex` of size 1025; align 128).
**Victim objects:** any `kmalloc(N)` whose `N ∈ (1024, 1152]` lands in the same
zone. Candidate victim objects containing attacker-interesting fields in this
size range should be grepped in `sys/` (function-pointer ops vectors,
`struct ucred *`/`struct file *`/`struct proc *` pointers, refcounts, uids).
**Grooming:** spray the 1152-byte bucket (sockets, pipes, file opens, mmaps)
to fill partial slabs; punch a hole adjacent to the next `jail.list` buffer;
trigger the sysctl → OOB write the IP strings into the victim object.
**Conversion:** depends on the victim — overwrite a function pointer → pivot;
overwrite a `ucred *` → forge; overwrite a refcount → UAF → re-claim → write.
**Content control:** the OOB write content is the jail's formatted IP strings,
set by root at `jail(2)` time — not directly attacker-shaped in the default
unprivileged-attacker model. A semi-privileged attacker (one who can create
jails) fully controls it. The OOB write POSITION/LENGTH is attacker-observable
(via `jls`) and the trigger is freely repeatable, so the corruption primitive
is deterministic even with root-set content (corrupt → observe via leak →
re-corrupt).

**Where this verification stopped:** confirmed the OOB write + OOB read
primitive end-to-end on master DEV. Did not develop a full heap-grooming +
victim-corruption → `uid=0` chain (the OOB write content is not
attacker-controlled in the strict unprivileged model, so a uid0 chain would
require either (a) root cooperation to set IP strings to gadget-shaped bytes,
or (b) finding a victim object in the 1152-byte bucket whose corruption with
printable-IP bytes yields a usable primitive). The realistic impact ceiling
on the strict model is **local DoS + info leak of adjacent slab** (with
grooming for pointer leakage); with jail-creation capability it escalates to
full memory corruption → likely privesc. 50× repeated triggers did not panic
this build (the adjacent slab chunks were benign), but the corruption is real
and a less-fortunate heap layout would crash.

## PoC changes vs the original

- **OID name:** original PoC/finding used `kern.jail.list`; corrected to the
  actual top-level `jail.list` (`SYSCTL_OID(_jail, OID_AUTO, list, ...)` at
  `kern_jail.c:757`). This was the sole blocker for the prior
  `not_reproduced` verdict — not a version difference.
- **Setup script:** rewrote as `setup_jail_v3.sh` — fully self-contained,
  builds the deep path one `mkdir` per level (because `mkdir -p` silently
  truncates long paths on DragonFly), copies a static `sleeper` binary into
  the chroot as `/s`, launches `jail(8)` backgrounded so the prison survives
  ssh disconnect (the sleeper keeps it alive).
- **Trigger:** rewrote `jail_list_trigger.c` to (a) use `sysctlnametomib()`
  for the OID, (b) compare the returned byte count against both `jlssize`
  (`count*1024`) AND the actual slab bucket (`1152`), and (c) hexdump the
  OOB tail so the leaked bytes are visible.
- **Did NOT need to change:** the underlying bug claim, the cited
  `path:line`s, or the recommended fix — all are accurate against the
  audited master DEV tree.

## Decisive evidence

- `run.log` — single decisive run; `[+] BUG DF-0053 CONFIRMED` with the
  byte accounting.
- `run.3x.log` — three back-to-back runs, byte-identical (deterministic
  because the adjacent slab was `M_ZERO`-zeroed; the OOB read LENGTH is
  proven regardless of content variance).
- `leak_sample.txt` — the OOB tail bytes.
- `env.txt` — `uname -a`, `cc --version`, `sysctl jail.list`, `jls`.
- `dmesg.txt` — full guest dmesg (no slab-corruption warnings on this build
  — INVARIANTS in `kern_slaballoc.c` only catches use-after-free via the
  `weirdary` pattern at `:1566-1572`, not real-time OOB-write detection).
- No panic (`panic.txt` not applicable — guest stayed up across 50+ triggers).
