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

hpfs_write consumes unvalidated on-disk extent length: (runl+1)*DEV_BSIZE int overflow β†’ towrite==0 β†’ zero-size getblk/bread + size_t underflow in uiomove β†’ unkillable kernel livelock (and latent OOB copyin)

Field Value
ID DF-3099
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:H
CWE CWE-190 / CWE-835 / CWE-787 (latent)
File sys/vfs/hpfs/hpfs_vnops.c
Lines 394-420 (feed: hpfs_alsubr.c:126-131)
Area vfs/hpfs
Confidence certain
Discovered 2026-09-06
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

hpfs_hpbmap computes the run hint from raw on-disk al_len in u32β†’int arithmetic; hpfs_write evaluates (runl+1)*DEV_BSIZE in signed 32-bit. A crafted first extent alleaf{al_off=0, al_len=0x800000} (an ordinary-looking 4GB declaration, never validated) makes the product wrap to exactly 0, so towrite=0 while the write is inside fn_size (no extend, no error). off==0: the getblk(0)+clrbuf+uiomove(n=0) branch makes no progress and while(uio_resid) spins forever holding the vnode lock β€” unkillable livelock. off>0: bread(devvp,bn,0) returns a zero-byte buffer and uiomove(bp->b_data+off,(size_t)(0βˆ’off)) underflows size_t, aiming an attacker-length UIO_WRITE copyin at a size-0 buffer (deterministically EFAULT on the stock build because zero-size buffers carry no mapped data area; latent OOB write destination elsewhere). hpfs_read's twin is protected (u_int min clamp + toread==0 break) β€” the write path lacks the equivalent check. Crafted HPFS image + RW mount (stock mount_hpfs forces RO β€” direct mount(2) or non-stock tooling, DF-0859 precedent); with the mountpoint handed to the user the trigger is a fully unprivileged pwrite. VERIFIED: 'whang' variant never returns (timeout kill, load 11.99, ssh starvation); 'wprobe' variant 13/13 EFAULT proving the underflowed uiomove. Fix validated (run clamp at hpbmap + towrite==0 β†’ EINVAL guard): both variants return EINVAL, no livelock. No uid0 route: livelock DoS + latent write primitive.

Timeline

  • 2026-09-06 Discovered during pass-2 audit of hpfs_vnops.c (GLM 5.3); livelock + underflow probe reproduced + fix validated. DF-0829/0830 re-verified, not re-reported.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3099 Β· 17 files
FileTypeDescriptionSize
craft_img.py β€” 5.6 KB view raw
poc.c β€” 3.7 KB view raw
mount_rw.c β€” 1.3 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 166 B view raw
build.log β€” 289 B view raw
run.readpanic.log β€” 234 B view raw
run.readpanic.2.log β€” 234 B view raw
run.wleak.5.log β€” 318 B view raw
run.wprobe.2.log β€” 780 B view raw
run.whang.log β€” 261 B view raw
whang_console.txt β€” 1.0 KB view raw
env.txt β€” 152 B view raw
fix.diff β€” 989 B view raw
fix_run.log β€” 481 B view raw
VERDICT.md β€” 4.3 KB ↓ raw
verdict.json β€” 4.6 KB view raw
VERDICT.md
↓ download raw

DF-3099 VERDICT

Verdict: REPRODUCED (deterministic unkillable kernel livelock / local DoS), with the OOB-write primitive honestly characterized as unmanifested on the stock build.

What was run (stock INVARIANTS kernel #0, guest 6.5-DEVELOPMENT)

  1. whang β€” pwrite(fd, 1 byte, offset 0) as uid 1001 on the crafted image (alleaf{0, 0x800000, 0x50}, fn_size=0x1000), mounted RW via mount_rw on a uid-1001-owned mountpoint. * Trace: hpfs_write β†’ uio_offset(0)+resid(1) ≀ fn_size β†’ no extend β†’ hpfs_hpbmap(0) matches alleaf[0] β†’ runl = 0x7FFFFF β†’ (runl+1)*512 = 0x800000*512 = 2^32 ≑ 0 (int32) β†’ towrite = szmin(1, umin(65536, 0)) = 0, xfersz = 0 β†’ off==0 && towrite==xfersz β†’ getblk(devvp, bn, 0) + clrbuf + uiomove(b_data+0, 0, uio) β€” no progress β†’ while (uio->uio_resid) loops forever holding the vnode lock (hpfs_vnops.c:385-430). * Observed: RUN_EXIT=124 (pwrite never returned); guest load rose to 11.99; subsequent ls /tmp/hpfsmp hung; ssh went to Connection timed out during banner exchange; VM management paths starved. The triggering thread never sleeps β†’ unkillable. * Console: whang_console.txt. Run log: run.whang.log.

  2. wprobe β€” 13Γ— pwrite(1 byte, off ∈ {1..511}) (only lbn 0 triggers): all returned -1 EFAULT. * Trace: towrite = szmin(off+1, 0) = 0, xfersz = 0, off != 0 β†’ bread(devvp, bn, 0) β†’ zero-byte buffer β†’ uiomove(b_data+off, (size_t)(0-off) β‰ˆ 2^64, uio) with uio_rw=UIO_WRITE β†’ copyin(user_byte, b_data+off, 1). * The EFAULT proves the kernel aimed a controlled 1-byte copyin at b_data+off of a size-0 buffer β€” an OOB-write destination. On this kernel build the data kva of a size-0 getblk buffer is unmapped, so the access faults cleanly instead of corrupting (13/13 deterministic). The memory-safety defect is real (length/destination computed from an uninitialized-by-size buffer region), but no heap corruption could be landed on the stock build, so impact is recorded as dos, not memcorrupt. * Run log: run.wprobe.2.log.

  3. readpanic (read-side sibling, negative result worth recording): the original hypothesis β€” xfersz > MAXBSIZE reaching panic("getblk: size > MAXBSIZE") from hpfs_read β€” is impossible on DragonFly: min is a u_int static inline (sys/sys/libkern.h:76), so the inner min(64*1024, (runl+1)*DEV_BSIZE) converts the wrapped (negative) product to a huge u_int that never wins; toread is always ≀ 65536 = MAXBSIZE, and toread==0 breaks out (vnops.c:331). Empirically read(fd, buf, 1MiB) on an al_len=0x400000 extent returned EINVAL from a benign 64 KB device read past EOF (run.readpanic.2.log, first attempt with al_len=0x80100000 in run.readpanic.log).

Preconditions (honest)

  • Crafted HPFS image (mount by root β€” vfs.usermount=0).
  • RW mount required: stock mount_hpfs(8) forces MNT_RDONLY (sbin/mount_hpfs/mount_hpfs.c:106); the pack's mount_rw.c helper mounts via mount(2) directly, mirroring the DF-0859 precedent.
  • Trigger itself is fully unprivileged (uid 1001 pwrite), with file ownership handed over via the RW mount's uid.

Impact ceiling

Unprivileged local DoS: permanent kernel livelock with vnode lock held (system-wide degradation, mountpoint unresponsive, management starvation). The uiomove size underflow additionally constitutes a latent attacker-controlled OOB-write primitive that manifests only where a zero-size buffer's b_data is mapped kva; on the audited build it is a clean EFAULT.

Fix validation

fix.diff (run-hint clamp in hpfs_hpbmap + towrite==0 progress guard in hpfs_write), applied to the guest's /usr/src, make nativekernel, rebooted: whang returns immediately (pwrite = 1), wprobe/wleak perform normal in-bounds writes (pwrite = 512), benign control image reads unchanged. No regression. See fix_run.log.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline (unpatched #0): whang livelock (124, guest starvation), wprobe EFAULT x13. Patched (fix.diff): whang returns immediately (pwrite=1), wleak/wprobe do normal in-bounds writes (pwrite=512), control image reads unchanged. No regression.

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 09:09:15 UTC 2026 (fix.diff applied, make nativekernel, X86_64_GENERIC)

Confirmed kernel references

Detail

Exploit chain

crafted image (alleaf{0,0x800000,lsn}) -> root mounts RW via mount(2) helper with uid handed to user -> unprivileged pwrite(fd, 1, offset 0) -> hpfs_write (vnops.c:385-430) -> hpfs_hpbmap returns runl=0x7FFFFF -> (runl+1)*512 == 0 (int32 wrap) -> towrite=0 -> getblk(0)+clrbuf+uiomove(0 bytes) -> infinite kernel loop holding vnode lock -> system-wide starvation DoS (no uid0 path: livelock only; the off>0 uiomove underflow is a latent OOB copyin that faults on this build).

Evidence (decisive lines)

['run.whang.log: pwrite trigger never returned (RUN_EXIT=124), guest load 11.99, subsequent ssh timed out', 'whang_console.txt: console excerpt around the wedge', 'run.wprobe.2.log: 13/13 pwrite(1 @ 1..511) = -1 EFAULT (underflowed uiomove aimed at unmapped kva of size-0 buffer)', 'run.readpanic.2.log: read-side negative result (umin clamp; EINVAL from benign device OOB read)', 'run.control log path in fix_run.log control test: benign image reads fine (no regression)', 'fix_run.log: patched kernel - whang returns instantly, wleak/wprobe perform normal in-bounds writes']

PoC changes

Built from scratch; initial readpanic hypothesis (getblk size>MAXBSIZE panic) was disproven by source analysis of DFly's u_int min() inline and empirically (EINVAL); write-side whang/wprobe variants are the reproducible manifestations.

Verified recommended fix

Clamp the hpbmap run hint to [0, MAXBSIZE/DEV_BSIZE-1] and add a towrite==0 progress guard in hpfs_write (see fix.diff).

Verdict

REPRODUCED as a deterministic unprivileged local DoS, with the OOB-write sibling honestly characterized. Root cause: hpfs_hpbmap (alsubr.c:126-131) hands hpfs_write a run hint computed from the raw on-disk al_len; (runl+1)*DEV_BSIZE at hpfs_vnops.c:394-396 is signed-32-bit multiplication that wraps to exactly 0 for al_len=0x800000 (a benign-looking 4GB first extent), so towrite=szmin(off+resid, 0)=0, xfersz=0. off==0: getblk(size 0)+clrbuf+uiomove(n=0) make no progress and the while(uio_resid) loop (vnops.c:385) spins forever holding the vnode lock - unkillable livelock (observed: RUN_EXIT=124, load 11.99, mountpoint and ssh management starved). off>0: bread(size 0) + uiomove(b_data+off, (size_t)(0-off)) underflows size_t and aims an attacker-controlled copyin at a size-0 buffer - on the stock build the zero-size buffer's data kva is unmapped, so all 13 probe offsets returned deterministic EFAULT (memory-safety defect real, corruption not landed on this build). Read-side sibling is safe: DFly's min is a u_int inline (libkern.h:76) so toread<=MAXBSIZE always and toread==0 breaks (vnops.c:331); hpfs_write lacks the equivalent check. Requires crafted image + RW mount (stock mount_hpfs forces MNT_RDONLY); trigger is unprivileged pwrite. Fix validated.