physio consumes stale/uninitialized b_resid from the shared pbuf_mem pool and copyouts stale bounce-buffer contents before the B_ERROR check β failed raw reads disclose other contexts' kernel-resident I/O data
| Field | Value |
|---|---|
| ID | DF-2979 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:L |
| CWE | CWE-908 / CWE-459 |
| File | sys/kern/kern_physio.c |
| Lines | 98-128 (gap: initpbuf vm_pager.c:383-395) |
| Area | kern/vm + disk |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | kernleak |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
physio() derives the transfer length as iolen = bp->b_bcount β bp->b_resid (:112) but neither physio, initpbuf() (resets b_data/ b_flags/b_cmd/b_error/b_bcount/b_bufsize β not b_resid), nor reinitbufbio() ever establishes b_resid for the transfer, and bpdone()/biodone_sync() never write it. pbuf_mem pbufs are recycled kernel-wide (physio, CAM pass-through bounce, disklabel/mbr/gpt scanners, subr_diskiocom) LIFO per bucket. Any device strategy completion that does not itself set b_resid β in-tree: virtio_blk error completions (the default block driver on QEMU/KVM), xdisk timeout/lost-link completions (remotely triggerable by the xdisk server), and the dscheck truncation path β makes physio consume the pbuf's previous tenant's value. Because the copyout at :113-121 executes before the B_ERROR check at :128, a failing read still transfers up to MAXPHYS (128KB) of the recycled bounce buffer to userspace. Local unprivileged user with read permission on any physio-backed device: one failing I/O of the virtio_blk/xdisk class discloses up to 128KB of whatever the recycled pbuf last carried β other users' raw disk reads, CAM pass-through payloads, disklabel-scan sectors. Demonstrated on the stock INVARIANTS guest as uid 65534: deterministic 510/512-byte stale disclosure on a failed read, and 4096 bytes of root-only raw hammer2 root-disk data (HAM2 volume-header magic confirmed) disclosed across contexts. The stale>bcount variant underflows iolen to ~2^64 (DF-0249's sink) but x86-64 std_copyout's wrap check bounds it to a spurious EFAULT. Leak-only: no write primitive, no uid=0 chain for this class.
Proof of contest
VERIFIED (findings/poc/DF-2979/): dfp.ko char device wired to the STOCK physread/physwrite with a strategy replaying in-tree completion classes. T1 (deterministic): read 64KB then failing read 512 β EIO with buffer holding 510/512 bytes of the previous transfer's kernel bounce data. T3 (cross-context): six root feeders raw-reading the root-only disk while nobody spins failing 128KB reads β 4096 bytes of root-only disk data matched, HAM2 volume-header magic in the captured sample. T2: EOF-poison b_resid β iolen=(size_t)(512β65536) β ~2^64 copyout rejected by support.s wrap check β EFAULT on a fully-resident buffer (anomaly proof). Fix validated in-guest (b_resid=0 per iteration + bzero of the USERSPACE read bounce before dispatch): T1 zeros, T2 clean EIO Γ400, T3 zero matches Γ5000 under full churn, raw I/O otherwise functional.
Recommended fix
Validated fix.diff in findings/poc/DF-2979/ (plus belt-and-braces: clear b_resid in initpbuf()).
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_physio.c (GLM 5.3); cross-context kernel-heap disclosure reproduced + fix validated. DF-0249/0250 re-verified still present, not re-reported.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2979 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| dfp.c | β | 4.1 KB | view raw | |
| poc.c | β | 8.6 KB | view raw | |
| feeder.c | β | 1.2 KB | view raw | |
| Makefile | β | 67 B | β download | |
| build.sh | β | 301 B | view raw | |
| run.sh | β | 940 B | view raw | |
| build.log | β | 205 B | view raw | |
| run.log | β | 1020 B | view raw | |
| run_vuln_t2.log | β | 699 B | view raw | |
| run_t3.log | β | 977 B | view raw | |
| run_fixed_t1t2.log | β | 1.3 KB | view raw | |
| env.txt | β | 414 B | view raw | |
| leak_sample_t3.bin | β | 128.0 KB | β download | |
| leak_sample_t2.bin | β | 256.0 KB | β download | |
| fix.diff | β | 1.1 KB | view raw | |
| VERDICT.md | β | 5.1 KB | β raw |
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:
physioitself never sets it (kern_physio.c:59-109),initpbuf()resetsb_data/b_flags/b_cmd/b_error/b_bcount/b_bufsizebut notb_resid(sys/vm/vm_pager.c:383-395),reinitbufbio()only clearsbio_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 setB_ERROR/b_errorand never touchb_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),dschecktruncation path (subr_diskslice.c:275-276) shrinksb_bcountwithout adjustingb_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.
Fix verification
fixedfix.diff applied to /usr/src in-guest, nativekernel built+installed, rebooted into #1: T1 disclosed 0/512 bytes (was 510/512), T2 never EFAULTed in 400 attempts (was instant), T3 matched 0 bytes in 5000 attempts under 6-CPU root-disk churn (was 4096B root-disk leak). No functional regression.
run_fixed_t1t2.log (all three tests on patched kernel), fix.diff, VERDICT.md fix table
Confirmed kernel references
- sys/kern/kern_physio.c:112
- sys/kern/kern_physio.c:113-121
- sys/kern/kern_physio.c:128
- sys/vm/vm_pager.c:383-395
- sys/kern/vfs_bio.c:743-751
- sys/dev/virtual/virtio/block/virtio_blk.c:918-927
- sys/dev/disk/xdisk/xdisk.c:943
- sys/dev/disk/xdisk/xdisk.c:1035
- sys/dev/disk/xdisk/xdisk.c:1250
- sys/kern/subr_diskslice.c:266-270
- sys/kern/subr_diskslice.c:275-276
- sys/vm/vm_pager.c:660
Detail
Exploit chain
unpriv user with read perm on a physio-backed device -> (victim pbuf previously carried root's raw-disk read / CAM pass-through data; LIFO pbuf_mem pool recycling) -> one device-error completion of the virtio_blk/xdisk class (b_resid untouched, iolen = b_bcount - stale) -> physio copyout of stale bounce buffer before B_ERROR check -> up to 128KB of another context's kernel-resident I/O data per failed read
Evidence (decisive lines)
["run.log: T1 'LEAK -- failed read disclosed 510/512 stale kernel bounce-buffer bytes' + pattern hexdump despite errno=5", "run_t3.log: 'CROSS-CONTEXT LEAK at attempt 426 -- unprivileged FAILED read disclosed 4096 bytes of raw root-disk contents' (also hit at attempt 1 on rerun)", "leak_sample_t3.bin: block 0 begins '... 32 4d 41 48' = big-endian HAM2 hammer2 volume-header magic from /dev/vbd0s1d (unreadable by the triggering user)", 'run_vuln_t2.log: errno=14 (EFAULT) on a 512-byte read into a fully resident 256MB mapping = the ~2^64 copyout attempt, rejected by support.s:247-249', 'run_fixed_t1t2.log: with fix.diff kernel #1 -- T1 zeros, T2 clean EIO 400 attempts, T3 zero matches in 5000 attempts']
PoC changes
Seed had no runnable PoC. Wrote dfp.c KLD (stock physread/physwrite + strategy modes mirroring virtio_blk error/ dscheck EOF completions), poc.c (T1/T2/T3 + leak sampling), feeder.c (per-CPU root raw-disk churn for the cross-context race). Fixed along the way: dev_ops_add() no longer exists in DF 6.5 (make_dev only), no sys/ioctl.h in kernel (ioccom.h), device_if.h/bus_if.h SRCS, anon mmap must be prefaulted for copyout evidence, T3 leak path moved to /tmp (nobody unwritable in /tmp/dfp), nobody cannot usched_set (SYSCAP) so pinning is root-side only + unpriv spins.
Verified recommended fix
kern_physio.c: initialize bp->b_resid=0 per transfer iteration and bzero() the USERSPACE read bounce buffer before dispatch; harden initpbuf() to also clear b_resid
Verdict
physio consumes a b_resid value that neither it, initpbuf() (vm_pager.c:383) nor reinitbufbio() (vfs_bio.c:743) initializes, and copyout (kern_physio.c:113-121) runs before the B_ERROR check (line 128). With any strategy error-completion that skips b_resid -- in-tree: virtio_blk.c:918-927 error completions, xdisk.c:943/1035/1250 timeouts/lost-links -- a failing raw read copies the recycled pbuf's previous contents to the user. Reproduced as uid 65534: deterministic 510/512 stale-bytes disclosure (T1), and 4096 bytes of root-only raw hammer2 root-disk data (HAM2 volume-header magic) disclosed via a failed read while root feeders raw-read /dev/vbd0s1d (T3). The stale> bcount variant underflows iolen to ~2^64 but x86-64 std_copyout's address-wrap check (support.s:247-249) bounds it to a spurious EFAULT (T2, observed) -- which also bounds the related DF-0249 driver-side mechanism. Leak-only class; no write primitive.
No comments yet.