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)
PoC verification
Evidence pack
findings/poc/DF-3099 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
-
whangβpwrite(fd, 1 byte, offset 0)as uid 1001 on the crafted image (alleaf{0, 0x800000, 0x50},fn_size=0x1000), mounted RW viamount_rwon 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 to11.99; subsequentls /tmp/hpfsmphung; ssh went toConnection timed out during banner exchange; VM management paths starved. The triggering thread never sleeps β unkillable. * Console:whang_console.txt. Run log:run.whang.log. -
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)withuio_rw=UIO_WRITEβcopyin(user_byte, b_data+off, 1). * The EFAULT proves the kernel aimed a controlled 1-byte copyin atb_data+offof 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 asdos, notmemcorrupt. * Run log:run.wprobe.2.log. -
readpanic(read-side sibling, negative result worth recording): the original hypothesis βxfersz > MAXBSIZEreachingpanic("getblk: size > MAXBSIZE")fromhpfs_readβ is impossible on DragonFly:minis au_intstatic inline (sys/sys/libkern.h:76), so the innermin(64*1024, (runl+1)*DEV_BSIZE)converts the wrapped (negative) product to a hugeu_intthat never wins;toreadis always β€ 65536 = MAXBSIZE, andtoread==0breaks out (vnops.c:331). Empiricallyread(fd, buf, 1MiB)on anal_len=0x400000extent returnedEINVALfrom 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)forcesMNT_RDONLY(sbin/mount_hpfs/mount_hpfs.c:106); the pack'smount_rw.chelper mounts viamount(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
fixedBaseline (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
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.
No comments yet.