# DF-2979 VERDICT — reproduced (kernleak, cross-context infoleak of shared kernel pbuf contents)

**Guest:** DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS X86_64_GENERIC),
QEMU/KVM 6 CPU, root on virtio-blk (`vbd0s1d`, hammer2). `vm.pbuf_mem_count=394`.

## Root cause (path:line)

`physio()` consumes `bp->b_resid` after every transfer
(`sys/kern/kern_physio.c:112`, `iolen = bp->b_bcount - bp->b_resid`) but the
value is **never established for this transfer**:

- `physio` itself never sets it (kern_physio.c:59-109),
- `initpbuf()` resets `b_data/b_flags/b_cmd/b_error/b_bcount/b_bufsize` but
  **not `b_resid`** (`sys/vm/vm_pager.c:383-395`),
- `reinitbufbio()` only clears `bio_done/bio_offset`
  (`sys/kern/vfs_bio.c:743-751`),
- `bpdone()/biodone_sync()` never writes it (`sys/kern/vfs_bio.c:3512-3789`).

The `pbuf_mem` pool is shared kernel-wide (physio, `cam_periph.c:743` CAM
pass-through bounce buffers, disklabel/mbr/gpt scanners, `subr_diskiocom`),
and `relpbuf()` returns pbufs LIFO per bucket (`vm_pager.c:~660`). So the
`b_resid` read at kern_physio.c:112 is a **previous tenant's value** whenever
the device's strategy completion does not set it. Several in-tree
completions don't:

- `virtio_blk.c:918-927` — error completions set `B_ERROR`/`b_error` and
  never touch `b_resid` (this is the *default* block driver on QEMU/KVM),
- `xdisk.c:940-950, 1030-1041, 1250-1257` — timeout / lost-link completions,
  same omission (remotely triggerable by the xdisk server),
- `dscheck` truncation path (`subr_diskslice.c:275-276`) shrinks `b_bcount`
  without adjusting `b_resid` (benign only because current drivers set it
  later).

Additionally the copyout at `kern_physio.c:113-121` executes **before** the
`B_ERROR` check at kern_physio.c:128, so a failed read still transfers
stale bounce-buffer bytes to userspace.

## Trigger model

Any user with read permission on a physio-backed device (raw disks
`root:operator 640` on stock systems — group `operator` members, relaxed
perms on tapes/cdroms in common setups; `/dev/xdX` readers against a
misbehaving remote xdisk server). One failing I/O of the class above is
enough; no lying driver value is required (that is DF-0249's separate,
driver-side framing).

## Reproduction (all as uid 65534 `nobody`, stock kernel #0)

**T1 — stale-content disclosure (deterministic, `run.log`):**
NORMAL read(65536) fills the pbuf and leaves `b_resid=0`; ERR(NORESID)
read(512) → physio computes `iolen = 512-0 = 512` and copies the stale
bounce bytes out **before** noticing `B_ERROR`. Observed: `read = -1 errno=5`
with **510/512 stale kernel bytes** in the buffer (pattern dump in run.log).

**T3 — cross-context disclosure (`run_t3.log`):** six root feeders (pinned
one per CPU) raw-read the hammer2 root disk through physio; `nobody` spins
failing 128KB reads on `/dev/dfp`. Reproduced twice (attempt 426 and
attempt 1 under churn): **4096 bytes of `/dev/vbd0s1d` contents matched the
root-owned reference** — `nobody` cannot open that device (640
root:operator). `leak_sample_t3.bin` block 0 begins `… 32 4d 41 48` — the
big-endian **"HAM2" hammer2 volume-header magic**: raw on-disk data of the
root filesystem, disclosed by a failed unprivileged read.

**T2 — stale-resid underflow (`run_vuln_t2.log`):** EOF completion poisons
`b_resid=65536`; erroring read(512) → `iolen = 512-65536` →
`(size_t)0xFFFF…F0180` → physio attempts a ~2^64-byte `copyout`
(kern_physio.c:115). On x86-64 `std_copyout`'s wrap check
(`sys/platform/pc64/x86_64/support.s:247-249`, `addq %rdx,%rax; jc
copyout_fault`) rejects it instantly: **EFAULT returned to a 512-byte read
whose buffer was fully resident and mapped** — anomalous, clearly observed
(`errno=14` on attempt 1), but zero bytes copied. Consequence: correctness
bug + spurious EFAULT; *not* a mass leak on x86-64. (Pass-2 note: the same
wrap check bounds DF-0249's driver-side `b_resid>b_bcount` variant to
EFAULT-on-copyout on x86-64.)

No panic occurred in any run; guest stayed up throughout.

## Impact

Kernel information disclosure, cross-context: up to MAXPHYS (128KB) per
failing read of whatever the recycled pbuf last carried — other processes'
raw disk I/O (file contents, filesystem metadata), CAM pass-through
payloads, disklabel-scan sectors. DoS/corruption: none beyond the T2 EFAULT
anomaly (uio bookkeeping is internal). Not a write primitive; no uid0 chain
(leak-only class).

## Fix validation (fix.diff — `bp->b_resid = 0;` per transfer + `bzero()`
of the USERSPACE read bounce buffer before dispatch)

Patched kernel built in-guest (`make nativekernel`, #1 Fri Sep 4 13:57:06),
rebooted, PoC rebuilt and rerun (`run_fixed_t1t2.log`):

| test | stock #0 kernel | patched #1 kernel |
|------|-----------------|-------------------|
| T1   | LEAK 510/512 stale bytes | clean, buffer zeroed, EIO |
| T2   | EFAULT (underflow fired) | clean EIO, no EFAULT, 400 attempts |
| T3   | LEAK 4096B root-disk bytes | 0 matches / 5000 attempts under churn |

Raw I/O otherwise functional (NORMAL reads still return data; system boots
and runs hammer2 root on the patched kernel). Guest reset to clean-source
snapshot after validation.
