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

md_lookup_swap() reads only the low 32 bits of the 64-bit NFSv3 file size into a signed int β€” swap files in the 2-4 GiB window produce negative swap_nblks (negative swap vnode size, ~2^64-block swaponvp claim); >=4 GiB sizes silently wrap

Summary

fattr.size in NFSv3 spans wire words [5..6] (nfsuint64) but only fattribs.v3[6] is decoded into signed int 'size' (:286); swap_nblks = size/1024 (:296) into int. 2-4 GiB swap file -> negative swap_nblks; >=4 GiB -> mod-2^32 truncation (8 GiB -> swap silently disabled). The v2 twin at :292 lands a >=2 GiB u32 size in the same signed int. Consumer nfs_vfsops.c:666-701 doubles the negative count, sets VTONFS(vp)->n_size negative and passes it to swaponvp's u_quad_t nblks where it sign-extends to ~2^64 blocks - boot-path DoS; no memory-safety primitive. A legitimate server exporting a >2 GiB swap file, or any LAN forger answering the boot LOOKUP (DF-3205 vector), corrupts the diskless client's swap geometry. Fix: fxdr_hyper + accept only <= INT_MAX.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3211 Β· 5 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 3.1 KB ↓ raw
verdict.json β€” 2.5 KB view raw
manifest.json β€” 878 B view raw
fix.diff β€” 825 B view raw

DF-3211 β€” md_lookup_swap() decodes only the low 32 bits of the 64-bit NFSv3 file size into a signed int β†’ negative / truncated swap size

File: sys/vfs/nfs/nfs_mountrpc.c:286, 292-299; consumer sys/vfs/nfs/nfs_vfsops.c:666-701, sys/vm/vm_swap.c (swaponvp takes u_quad_t).

What

NFSv3 fattr.size is a 64-bit hyper (wire words [5]=hi, [6]=lo β€” sys/vfs/nfs/nfsproto.h:325 nfsuint64 nfsv3fa_size after type/mode/nlink/uid/gid). md_lookup_swap reads only:

size = fxdr_unsigned(u_int32_t, fattribs.v3[6]);   /* :286 β€” low word only, into int */
...
nfsv3_diskless.swap_nblks = size / 1024;           /* :296 β€” signed division */
  • swap file with size mod 2^32 in [2^31, 2^32) (e.g. a 2–4 GiB swap file) β†’ size negative β†’ swap_nblks negative.
  • swap file β‰₯ 4 GiB β†’ size silently wrong (mod 2^32): e.g. 8 GiB β†’ 0 β†’ swap silently disabled.
  • NFSv2 path has the same signed-int landing (:292, v2 size is a u32 word [5]): β‰₯ 2 GiB v2 swap file β†’ negative swap_nblks.

Downstream (nfs_mountroot, nfs_vfsops.c:666-701)

if (nd->swap_nblks) β€” a negative value is truthy β†’ swap_nblks *= 2 (still negative) β†’ VTONFS(vp)->n_size = nd->swap_nblks * DEV_BSIZE β€” negative file size on the swap vnode β†’ swaponvp(td, vp, nd->swap_nblks) β€” the int sign-extends into swaponvp's u_quad_t nblks (sys/vm/vm_swap.c) β†’ swap accounting believes ~2^64 blocks exist on an NFS file. Corrupt swap geometry / bogus vm_swap_size at best; boot-time failure at worst.

Threat

Trigger: a legitimate NFS server exporting a >2 GiB swap file (misconfig-sized or simply large), or β€” per DF-3205 β€” any LAN forger answering the boot LOOKUP (xid is deterministic, source unfiltered). Impact ceiling: boot-path DoS / swap-accounting corruption. Not runtime reachable post-boot.

Fix

Decode the full hyper (fxdr_hyper(&fattribs.v3[5]), xdr_subs.h:83) and bound-check before storing into the int field (see fix.diff).

VERDICT.md
↓ download raw

VERDICT β€” DF-3211

status: untested (boot-path-only; not runtime-testable on the audit guest) impact: dos (boot-path: corrupt/negative swap geometry, boot failure; forged-reply vector covered by DF-3205's ceiling) confidence: certain (arithmetic proof; downstream behavior described from source)

Proof chain (path:line)

  1. sys/vfs/nfs/nfsproto.h:306-335 β€” v3 fattr wire layout: fa_type[0], fa_mode[1], fa_nlink[2], fa_uid[3], fa_gid[4], nfsv3fa_size[5..6] (nfsuint64), … β€” size is a 64-bit hyper spanning words 5-6.
  2. sys/vfs/nfs/nfs_mountrpc.c:283-286 β€” reply fattr decoded into fattribs.v3[21], then size = fxdr_unsigned(u_int32_t, fattribs.v3[6]); β€” only word [6] (low 32 bits) is read, high word [5] discarded; destination int size (:227) is signed. * low word ∈ [2^31, 2^32) β†’ size < 0 (e.g. swap file of 3 GiB β†’ low word 0xC0000000 β†’ size = -1073741824). * file β‰₯ 4 GiB β†’ wraps mod 2^32 (8 GiB β†’ low word 0 β†’ swap "disabled").
  3. sys/vfs/nfs/nfs_mountrpc.c:295-296 β€” nfsv3_diskless.swap_nblks = size / 1024; β€” swap_nblks is int (sys/vfs/nfs/nfsdiskless.h:70) β†’ negative KB for the 2–4 GiB window. v2 twin at :292: fxdr_unsigned(u_int32_t, fattribs.v2[5]) into the same signed size (v2 fattr size is u32 at word [5], nfsproto.h:314) β€” same negative landing for β‰₯2 GiB.
  4. Consumer sys/vfs/nfs/nfs_vfsops.c:666-701 β€” if (nd->swap_nblks) (negative = true) β†’ nd->swap_nblks *= 2 (:669) β†’ VTONFS(vp)->n_size = VTONFS(vp)->n_vattr.va_size = nd->swap_nblks * DEV_BSIZE; (:691-692) β€” negative size on a VREG swap vnode β†’ swaponvp(td, vp, nd->swap_nblks) (:701) where the parameter is u_quad_t nblks (sys/vm/vm_swap.c) β€” sign extension to β‰ˆ2^64 blocks feeding the swap bitmap/accounting.
  5. sys/vfs/nfs/xdr_subs.h:83-84 β€” fxdr_hyper(f) exists and is the intended decoder for words [5..6]; other consumers (e.g. nfs_subs.c attribute handling) use it for exactly this field.

Why untested

Executes only during nfs_mountroot boot with an NFS root + swap path (#if BOOTP||NFS_ROOT). Audit guest (DragonFly 6.5-DEVELOPMENT #0, X86_64_GENERIC) boots hammer2 from disk; no runtime path reaches md_lookup_swap. Trigger on a real diskless client requires either a >2 GiB server swap file (plausible misconfig) or the DF-3205 forged-LOOKUP vector.

Impact assessment

Worst realistic: corrupt swap geometry at boot (negative vnode size, ~2^64-block swap claim) β†’ boot failure / vm accounting corruption β€” boot-path DoS ceiling. No post-boot runtime reachability, no memory-safety primitive (values flow into sizes/counts, not pointers or copy lengths).

Decode the full 64-bit size and range-check before storing (see fix.diff).

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
↓ fix.diffper-fix-DF-3211

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-3211/VERDICT.md steps 1-5; decisive lines: nfs_mountrpc.c:286 (v3[6] only, into signed int), :292 (v2 signed landing), :295-296 (swap_nblks = size/1024), nfsdiskless.h:70 (int swap_nblks), nfs_vfsops.c:691-692 (negative n_size), :701 + vm_swap.c swaponvp(u_quad_t) (sign extension), xdr_subs.h:83 (fxdr_hyper is the intended decoder)

PoC changes

none β€” seed not runnable (boot path); verdict is a wire-layout + signedness derivation cross-checked against nfsproto.h fattr layout and the fxdr_hyper macro

Verified recommended fix

decode fattr.size with fxdr_hyper(&fattribs.v3[5]) into u_quad_t and only accept sizes <= INT_MAX before storing into swap_nblks (fix.diff applies clean)

Verdict

md_lookup_swap reads only word [6] (low 32 bits) of the 64-bit NFSv3 fattr.size into a signed int (nfs_mountrpc.c:286; layout nfsproto.h:325: size spans words [5..6]) and divides it into the int swap_nblks (:296): a 2-4 GiB swap file yields a negative swap_nblks, >=4 GiB wraps mod 2^32 (8 GiB -> swap silently disabled); the v2 twin at :292 lands a >=2 GiB u32 size in the same signed int. nfs_mountroot then treats the negative count as truthy, doubles it, sets a negative n_size on the swap vnode, and passes it to swaponvp's u_quad_t parameter where it sign-extends to ~2^64 blocks (nfs_vfsops.c:666-701, vm_swap.c) β€” corrupt swap geometry/boot failure. Arithmetic is certain; not runtime-testable because the code runs only during an NFS-root boot and the audit guest boots hammer2 from disk.