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

cluster_wbuild installs the pbuf KVA window before validating it, with the first component exempt from the page guard and the window outside the bkvareset() protocol

Summary

cluster_wbuild qmaps the cluster window with pmap_qenter_noinval and only then checks b_bufsize > b_kvasize β€” inverting the safe order cluster_rbuild uses (panic first, map second); the per-component page bound runs only for i>0; and both build functions set B_KVABIO on the pbuf without ever calling bkvareset() after qentering, unlike every qenter site in vfs_bio.c, while initpbuf never resets b_cpumask. Latent memory-corruption amplifier: today wbuild's exact page guard plus forced-B_VMIO buffers plus vmaxiosize<=MAXPHYS keep npages<=32 β€” but if component accounting ever slips the way DF-2725 proved on the read side, the write side performs the OOB PTE install into the adjacent pbuf's contiguous KVA window (cross-mapping a neighbor's in-flight I/O to wrong physical pages) before the panic fires; any future CPU-touching window consumer inherits stale-TLB wrong-page access. No trigger on default config (honest untested). Hoist-the-panic + bkvareset fix git-apply-clean.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5837 Β· 5 files
FileTypeDescriptionSize
README.md β€” 4.1 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
fix.diff β€” 1.7 KB view raw
verdict.json β€” 3.8 KB view raw
manifest.json β€” 1.1 KB view raw

DF-5837 β€” cluster_wbuild maps the pbuf KVA window before validating it (and outside the KVABIO bkvareset protocol)

File: sys/kern/vfs_cluster.c (pass-3 fresh-perspective re-audit)

What (code facts, all certain)

  1. Ordering inversion vs the read-side twin. cluster_wbuild() installs the cluster KVA window first and bounds-checks second:

cluster_rbuild() does it the safe way around β€” validate at sys/kern/vfs_cluster.c:1094-1097, then map at 1098-1100.

  1. First component exempt from the page-count guard. The per-component bound ((tbp->b_xio.xio_npages + bp->b_xio.xio_npages) > (maxiosize / PAGE_SIZE), sys/kern/vfs_cluster.c:1606-1616) is evaluated only on the i > 0 path; the i == 0 component's pages are appended unconditionally (sys/kern/vfs_cluster.c:1636-1668). Non-VMIO components additionally accumulate bp->b_bcount/bp->b_bufsize (1670-1671) without appending any pages β€” today unreachable because getblk forces B_VMIO (sys/kern/vfs_bio.c:2810, 2974), but the guard does not enforce it.

  2. Pbuf window never enters the KVABIO cpumask protocol. rbuild/wbuild set B_KVABIO on the pbuf (sys/kern/vfs_cluster.c:940, 1551) and pmap_qenter_noinval() stores PTEs with no TLB invalidation (sys/platform/pc64/x86_64/pmap.c:2164-2166), but neither site calls bkvareset(bp). Every pmap_qenter_noinval site in sys/kern/vfs_bio.c pairs with bkvareset() (vfs_bio.c:3216, 3838, 3994); initpbuf() (sys/vm/vm_pager.c:382-395) also never resets b_cpumask, so a kva-pbuf's mask is carried across recycling.

Why it matters (latent impact)

The pbuf KVA arena is one contiguous nswbuf_kva * MAXPHYS region (sys/vm/vm_pager.c:239-242); each pbuf window is MAXPHYS. If component accounting ever lets xio_npages/b_bufsize exceed the window β€” precisely the defect class proven on the read side by DF-2725 (component b_bufsize exceeding blksize under-counts pages vs the xio_pages[] array/KVA window) β€” then on the write side the out-of-bounds PTEs are actually installed into the adjacent pbuf's window first, cross-mapping the next pbuf's I/O to the wrong physical pages, and only then does the panic fire. The rbuild ordering (panic first) contains exactly this scenario to a clean panic; wbuild does not.

The stale-cpumask half is currently benign by luck: nothing in the DMA strategy path calls bkvasync() on the cluster pbuf (nvme/cam build SGLs via vtophys/pmap_kextract, which walk page tables β€” TLB-immune: sys/dev/disk/nvme/nvme.c:390, sys/platform/pc64/x86_64/pmap.c:1865-1890), so the pbuf mask stays 0 and any future correct bkvasync() caller would invalidate properly. It becomes live the moment any CPU-touching consumer (bcopy-style driver, in-kernel copy path) participates β€” see the md(4) observation in the audit notes (md.c bcpopies through bp->b_data with no bkvasync at all β€” a cross-file defect worth its own audit of sys/dev/disk/md/md.c).

Reachability today (why Low / not verified on the guest)

The exact page-count guard at 1609-1610 (unlike rbuild's round_page(blksize) estimate) plus forced-B_VMIO buffers plus vmaxiosize() clamped to MAXPHYS (sys/kern/vfs_subr.c:1519-1531) make both halves unreachable without a second kernel bug: - unique page tiling of contiguous components bounds b_bufsize <= npages*PAGE_SIZE <= MAXPHYS = b_kvasize; - xio_pages[] holds XIO_INTERNAL_PAGES == btoc(MAXPHYS) == 32 entries and the guard caps cumulative pages at maxiosize/PAGE_SIZE <= 32.

Therefore no unprivileged (or any) local program can trigger this on a default-config guest; Phase V verification is intentionally skipped and the finding stands as defense-in-depth/hardening with a concrete fix.

Fix

Validate before mapping, and bring the pbuf window into the bkvareset protocol (both build functions). See fix.diff.

VERDICT.md
↓ download raw

DF-5837 β€” VERDICT

status: untested (deliberately not executed β€” latent defect, unreachable without a second kernel bug) impact: none demonstrated (latent OOB-PTE/cross-pbuf-mapping hardening) confidence: certain (code facts), speculative (exploitability)

What was verified statically (line-accurate)

  1. sys/kern/vfs_cluster.c:1691-1698 β€” pmap_qenter_noinval() runs BEFORE the bp->b_bufsize > bp->b_kvasize panic check in cluster_wbuild(). Differential: cluster_rbuild() at sys/kern/vfs_cluster.c:1094-1100 panics first, maps second. If the invariant ever breaks (the DF-2725 defect class: component b_bufsize > blksize under-counting pages), wbuild installs xio_npages PTEs starting at trunc_page(bp->b_data) (the pbuf's KVA base, sys/vm/vm_pager.c:299-301 β€” contiguous arena nswbuf_kva * MAXPHYS at sys/vm/vm_pager.c:239-242) and then detects the overrun. Overflowing PTEs land in the next pbuf's window, so the neighbor's in-flight I/O gets cross-mapped before the panic stops the system.

  2. The wbuild per-component page guard (sys/kern/vfs_cluster.c:1606-1616) uses actual xio_npages (unlike rbuild's round_page(blksize) estimate at 956-958) β€” so wbuild does NOT have DF-2725's counting flaw β€” but it only runs for i > 0; the first component (from bpp match at 1475-1478 or the outer findblk at 1480-1492) is appended unguarded (1636-1668). With MAXBSIZE = 64KB (sys/sys/param.h:372) a first component contributes ≀ 17 pages (allocbuf math sys/kern/vfs_bio.c:3045-3047), so ≀ 32 (XIO_INTERNAL_PAGES, sys/sys/xio.h:66) β€” safe today, fragile tomorrow.

  3. Neither cluster_rbuild (sys/kern/vfs_cluster.c:1098) nor cluster_wbuild (:1691) calls bkvareset(bp) after qentering, although both set B_KVABIO (:940, :1551) and pmap_qenter_noinval performs no TLB invalidation (sys/platform/pc64/x86_64/pmap.c:2164-2166). initpbuf() (sys/vm/vm_pager.c:382-395) never resets b_cpumask, so the mask persists across pbuf recycling. Every qenter site in sys/kern/vfs_bio.c (3216, 3838, 3994) pairs with bkvareset().

Why not executed on the guest

  • Triggering half 1 requires breaking the exact page/bufsize accounting β€” i.e. requires a second, unknown kernel bug (DF-2725 is read-side-only; wbuild's exact guard closes it on the write side).
  • Triggering half 3 requires a CPU-touching (bcopy) consumer of the cluster window; none exists in the default strategy chain (nvme/cam derive physical addresses via vtophys()β†’page-table walk, sys/dev/disk/nvme/nvme.c:390, sys/platform/pc64/x86_64/pmap.c:1873-1887 β€” TLB-immune). The one in-tree bcopy consumer that could be paired with clustered file I/O is md(4) (sys/dev/disk/md/md.c:254-333, bcopy/bzero through bp->b_data with no bkvasync) β€” a defect in that file, filed here only as a cross-reference note, not as part of this finding.

Guest reference (idle, not dirtied): DragonFly dfbsd 6.5-DEVELOPMENT #0 X86_64_GENERIC.

fix.diff in this directory: (a) hoist the kvasize check above the qenter in cluster_wbuild (mirroring rbuild), (b) bkvareset(bp) after both qenter sites so the window re-enters the KVABIO protocol. Fix not built/validated on the guest (nothing observable changes without a triggering bug; fix_status: not_testable).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored against the read-only sys/ tree (guard hoist + bkvareset x2); not built on the guest because the guarded condition is unreachable on a stock kernel - no observable behavior changes to measure (fix_status not_testable).

findings/poc/DF-5837/fix.diff
↓ fix.diffper-fix-DF-5837

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5837/VERDICT.md (full path:line analysis); findings/poc/DF-5837/fix.diff (hoisted guard + bkvareset at both qenter sites); key lines sys/kern/vfs_cluster.c:1691-1698 vs 1094-1100, sys/kern/vfs_cluster.c:1606-1616 (i>0-only guard), sys/kern/vfs_cluster.c:940+1551 (B_KVABIO), sys/vm/vm_pager.c:239-242+299-301+382-395, sys/kern/vfs_bio.c:3216+3838+3994 (qenter/bkvareset pairing), sys/platform/pc64/x86_64/pmap.c:2164-2166 (qenter_noinval semantics)

PoC changes

no PoC built - latent defect not reachable by any local program on a default-config guest without a second kernel bug; verification intentionally skipped per contract for Low hardening findings

Verified recommended fix

Hoist the b_bufsize>b_kvasize panic above pmap_qenter_noinval in cluster_wbuild (mirror cluster_rbuild) and call bkvareset(bp) after both qenter sites so the pbuf window re-enters the KVABIO cpumask protocol.

Verdict

Code defect confirmed by line-accurate static analysis: cluster_wbuild() installs the pbuf KVA window with pmap_qenter_noinval (sys/kern/vfs_cluster.c:1691-1693) BEFORE the b_bufsize>b_kvasize guard (1694-1698), inverting the safe ordering used by cluster_rbuild (1094-1100), and neither build function calls bkvareset() after qentering although both set B_KVABIO (940, 1551) and initpbuf never resets b_cpumask (sys/vm/vm_pager.c:382-395). Latent impact: any future component-accounting slip of the DF-2725 class turns into an actual out-of-bounds PTE install into the adjacent pbuf's contiguous KVA window (arena at sys/vm/vm_pager.c:239-242) before the panic fires; the stale cpumask becomes live for any CPU-touching window consumer. Not executed on the guest: unreachable without a second kernel bug - wbuild's exact per-component page guard (1606-1616, actual xio_npages), getblk forcing B_VMIO on all buffers (sys/kern/vfs_bio.c:2810,2974), vmaxiosize clamped to MAXPHYS (sys/kern/vfs_subr.c:1519-1531) and xio_pages[] sized btoc(MAXPHYS)=32 (sys/sys/xio.h:66) close every reachable path; the default disk strategy chain only derives physical addresses via TLB-immune page-table walks (sys/dev/disk/nvme/nvme.c:390, sys/platform/pc64/x86_64/pmap.c:1873-1887). Defense-in-depth fix authored (check-before-map + bkvareset).