β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2876

BLK_READ replies disclose stale shared pbuf-mem contents on EOF/error/short reads (cross-process, cross-device kernel-mediated info leak)

Field Value
ID DF-2876
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-908 / CWE-200
File sys/kern/subr_diskiocom.c
Lines 594-597, 651-655 (pool vm_pager.c:505)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket kernleak
Reported pending
Known CVE none
CVE match novel

Summary

diskiodone sets data=bp->b_data, bytes=bp->b_count for BUF_CMD_READ before inspecting B_ERROR/b_resid and attaches that buffer to the reply unconditionally. A read that transfers nothing (vn disk at EOF: VOP_READ succeeds with untouched uio_resid β†’ b_resid=b_bcount, no B_ERROR; or any erroring read where the driver never touched the buffer) returns error=0 with a 64KB aux containing the previous contents of the getpbuf_mem KVA arena β€” shared with every process's raw-device physio, disklabel probes and other dmsg I/Os. Each EOF/failed read harvests up to MAXPHYS of the most recent raw-I/O buffer of ANY process on the machine.

Proof of contest

VERIFIED on the stock guest (findings/poc/DF-2876/): vn0 backed by a 16MB urandom "secret" image, pool seeded via dd; peer issues BLK_READ at exactly offset==media_size β†’ reply error=0 resid=65536 aux=65536 whose bytes are md5-IDENTICAL to the secret image block (deterministic across sessions); a second run leaked the boot disk's MBR boot code through a DIFFERENT disk's channel β€” cross-device disclosure. Fix (no aux on B_ERROR; only completed bytes on short reads) validated: aux=0 on all runs.

Validated fix.diff in findings/poc/DF-2876/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_diskiocom.c (GLM 5.3); cross-process disclosure reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2876 Β· 14 files
FileTypeDescriptionSize
dfpeer.c β€” 15.4 KB view raw
run_f3.sh β€” 1.7 KB view raw
run.log β€” 3.8 KB view raw
leak_sample.txt β€” 422 B view raw
f3_leak_1.bin β€” 64.0 KB ↓ download
f3_secret.bin β€” 64.0 KB ↓ download
fix.diff β€” 2.3 KB view raw
build.sh β€” 51 B view raw
run.sh β€” 143 B view raw
env.txt β€” 373 B view raw
VERDICT.md β€” 1.9 KB ↓ raw
run.fix.log β€” 625 B view raw
verdict.json β€” 2.8 KB view raw
README.md β€” 1.9 KB ↓ raw

DF-2876 β€” BLK_READ at/after EOF returns stale shared pbuf memory to the

dmsg peer (cross-process / cross-device kernel-mediated info leak)

What this pack contains

  • dfpeer.c β€” dmsg wire peer (see DF-2875 pack).
  • run_f3.sh β€” sets up a vn(4) disk containing 16MB of "secret" pattern, seeds the shared pbuf-mem pool by physio (dd if=/dev/vn0 of=/dev/null), then issues BLK_READ at EOF through the dmsg channel.
  • leak_sample.txt β€” the three leak runs + md5s; run 1 returned a full 64KB block identical to the secret image (md5 7081aa33...), run 2 returned the boot disk's MBR boot code (fc 31 c0 8e c0 8e d8 8e d0 bc 00 7c...) through a different disk's channel, run 3 returned stale dmsg write aux (0xAA fill).
  • fix.diff β€” diskiodone READ: on B_ERROR attach no data; on short reads attach only completed bytes.

Root cause

diskiodone (sys/kern/subr_diskiocom.c:594-597) sets data = bp->b_data; bytes = bp->b_bcount for BUF_CMD_READ before looking at B_ERROR / b_resid, and unconditionally attaches bytes of buffer content to the reply (:651-655). A read that transfers nothing (EOF: vn's VOP_READ returns success with untouched uio_resid β†’ b_resid = b_bcount, no B_ERROR) therefore ships the previous contents of the shared getpbuf_mem KVA arena to the peer. That arena is shared with kern_physio (every raw device I/O of every process), disklabel/GPT/MBR probes, and other dmsg I/Os (sys/vm/vm_pager.c:505, sys/kern/kern_physio.c:43).

Build & run (root on the DF guest)

cc -O -Wall -o /tmp/dfpeer dfpeer.c
sh run_f3.sh

Expected output (stock kernel)

eofread off=16777216 len=65536 -> reply cmd=... error=0 resid=65536 aux=65536 β€” an "error-free" reply whose 64KB aux equals the previous process's physio buffer (the secret image block), verified by md5.

On the fix kernel the same command returns aux=0.

VERDICT.md
↓ download raw

DF-2876 VERDICT

Status: reproduced (leak β€” deterministic cross-process /

cross-device kernel-mediated info leak to the dmsg peer)

Guest: DragonFly 6.5-DEVELOPMENT X86_64_GENERIC (env.txt).

What was run

run_f3.sh: 1. root creates vn0 backed by a 16MB urandom image (the "secret"), 2. root seeds the shared pbuf-mem pool: dd if=/dev/vn0 of=/dev/null (physio β†’ getpbuf_mem β†’ the KVA arena holds the secret, sys/kern/kern_physio.c:43), 3. the dmsg peer issues BLK_READ at exactly EOF (offset = media size) on the same disk.

Observed (stock kernel), 3 runs

  • run 1: reply error=0 resid=65536 aux=65536; the 64KB aux is byte-identical to the secret image block (md5 7081aa33… both) β€” the read transferred nothing yet returned a previous process's physio buffer in full.
  • run 2: aux starts fc 31 c0 8e c0 8e d8 8e d0 bc 00 7c … β€” the boot disk's MBR boot code, leaked through vn0's channel (cross-device disclosure; the arena is global).
  • run 3: aux = 0xAA fill β€” stale dmsg write aux from earlier testing.

Root cause: diskiodone sets data = bp->b_data; bytes = bp->b_bcount before checking B_ERROR/b_resid (sys/kern/subr_diskiocom.c:594-597) and attaches the buffer unconditionally (:651-655). An EOF read on a vnode-backed vn disk returns success with untouched resid (vnstrategy copies auio.uio_resid to b_resid, sys/dev/disk/vn/vn.c:337,385-389), so the stale arena contents ship to the peer. The same happens for B_ERROR completions (driver never touched the buffer).

Impact ceiling: any dmsg peer can repeatedly harvest up to MAXPHYS of the most recent raw-I/O buffer of ANY process on the machine (backups, fsck raw scans, other cluster nodes' disk traffic through diskiocom itself). Cluster-remote (AV:A) or local privileged.

Fix validation

fix.diff (no aux on B_ERROR; only completed bytes on short reads) applied, kernel rebuilt, PoC re-run: eofread … aux=0 β€” leak dead (run.fix.log).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Fix kernel: all three EOF-read runs return aux=0 (resid still reported). Leak dead.

['run.fix.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 18:43:16 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (fix.diff applied, make nativekernel)

Confirmed kernel references

Detail

Exploit chain

dmsg peer -> BLK_READ at/after media EOF (or any erroring read) -> reply aux = most recent raw-I/O buffer of ANY process on the machine (up to MAXPHYS per request, repeatable) -> harvest other tenants'/root's disk traffic (backups, fsck scans, other cluster nodes' data)

Evidence (decisive lines)

['run.log (error=0 resid=65536 aux=65536; LEAK_RUN_1 FULL_64K_BLOCK_MATCHES_SECRET; run2 leaked boot-sector bytes fc310c8e...)', 'leak_sample.txt', 'f3_leak_1.bin == f3_secret.bin (md5 7081aa33857826393038748400c1ee27)', 'run.fix.log (aux=0 x3 on fix kernel)']

PoC changes

No seed; wrote dfpeer.c eofread mode; deterministic EOF trigger discovered via vn(4) vnode strategy (success + untouched uio_resid at EOF)

Verified recommended fix

On B_ERROR attach no data; on short reads attach only completed bytes (b_bcount - b_resid)

Verdict

diskiodone attaches bp->b_data/b_bcount to the BLK reply before checking B_ERROR/b_resid (subr_diskiocom.c:594-597, :651-655), so a read that transfers nothing ships the stale contents of the SHARED getpbuf_mem KVA arena (vm_pager.c:505 - reused by every process's raw-device physio, kern_physio.c:43, and disk label probes). Verified: a BLK_READ at exact EOF on a vnode-backed vn disk (VOP_READ returns success with untouched resid) returned error=0 resid=65536 with a 64KB aux byte-identical (md5) to a previous root process's physio buffer of a 'secret' image; a second run leaked the BOOT DISK's MBR boot code through a different disk's channel (cross-device). Fixed kernel returns aux=0.