hpfs_hpbmap treats al_len==0 alleaf as an infinite extent: unprivileged arbitrary raw-device sector read/write via ordinary pread/pwrite
Summary
The leaf-match clause '!alp->al_len ||' (:118-119) makes a zero-length alleaf match every bn >= al_off, and :122 maps it to *bnp = bn - al_off + al_lsn on the raw device vnode. With al_off/al_lsn crafted, every file offset inside fn_size maps to a chosen raw sector of the whole backing device. Distinct from DF-3099 (hpfs_write's runl arithmetic; runp=3 merely fed by this branch). Unpriv user on a crafted RW-mounted HPFS reads and writes ANY sector of the backing disk device (superblock, disklabel, other slices' filesystems) through a plain file fd whose declared allocation is zero blocks. VERIFIED uid 1001: pread at file offset 0x200000 returned the mounted FS's superblock magic 0xfa53e9c5f995e849; pwrite at 0x204000 overwrote raw device sector 0x30 with 'DF3139-PWNED-RAW-SECTOR-WRITE' (verified by root dd of /dev/vn0; the file owns zero blocks). Fix validated (require al_len!=0): same PoC -> EFBIG, sector unchanged.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3139 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.py | β | 4.9 KB | view raw | |
| poc.c | β | 2.7 KB | view raw | |
| mount_rw.c | β | 1.3 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 140 B | view raw | |
| build.log | β | 89 B | view raw | |
| run.log | β | 934 B | view raw | |
| fix.diff | β | 4.3 KB | view raw | |
| fix_run.log | β | 520 B | view raw | |
| VERDICT.md | β | 2.8 KB | β raw | |
| README.md | β | 1.6 KB | β raw | |
| env.txt | β | 464 B | view raw |
DF-3139 β al_len == 0 alleaf treated as an infinite extent by hpfs_hpbmap: unprivileged arbitrary raw-device sector read/write
Build
./build.sh # crafts df3139.img on host, pushes + compiles on guest
Run
./run.sh # mounts RW on uid-1001-owned mountpoint, runs the PoC
# as uid 1001, then (root) dumps raw sector 0x30
Expected (and observed)
DF3139 read sector 0x10 via file offset 0x200000:
first8 = 0xfa53e9c5f995e849 (the HPFS SUPERBLOCK magic β read
through a file that owns 0 blocks)
DF3139 read sector 0x30 via file offset 0x204000:
first4 = 0x00000038 (the bitmap-dir word)
DF3139 write sector 0x30 via file offset 0x204000: rc=512
raw /dev/vn0 sector 0x30 afterwards: "DF3139-PWNED-RAW-SECTOR-WRITE"
Root cause: sys/vfs/hpfs/hpfs_alsubr.c:118-119 β
if ((bn >= alp->al_off) && (!alp->al_len || (bn < al_off+al_len)))
treats a zero-length alleaf as matching every bn >= al_off, and :122
maps it to *bnp = bn - al_off + al_lsn on the raw device vnode
(h_devvp). With al_off/al_lsn crafted, every file offset maps to a
chosen raw sector of the whole backing device β reading/writing the
superblock, disklabel area, or (on multi-slice disks) other partitions'
data from an unprivileged pread/pwrite on an ordinary file. Distinct
from DF-3099 (hpfs_write's (runl+1)*DEV_BSIZE arithmetic; the feed is
this branch's *runp = 3, but the arbitrary-device-addressing
consequence of the !alp->al_len clause itself is this finding).
DF-3139 VERDICT β REPRODUCED (unprivileged arbitrary raw-device sector read + write through a file descriptor)
Observed (run.log, guest DragonFly 6.5-DEVELOPMENT X86_64_GENERIC)
Crafted image: file FILE, fn_size = 4 MB, root alblk = single alleaf
{al_off=0x1000, al_len=0, al_lsn=0x10}; mounted RW via direct mount(2)
on a uid-1001 mountpoint. Executed as uid 1001 (maxx):
- pread(fd, 512, 0x200000) -> lbn 0x1000 -> sector 0x10: returned the SUPERBLOCK, first 8 bytes = 0xfa53e9c5f995e849 (su_magic).
- pread(fd, 512, 0x204000) -> lbn 0x1020 -> sector 0x30: returned 0x00000038 (the bitmap-dir pointer word stored there).
- pwrite(fd, "DF3139-PWNED-RAW-SECTOR-WRITE", 0x204000) -> rc=512.
- Root-side raw dump of /dev/vn0 sector 0x30 afterwards contains "DF3139-PWNED-RAW-SECTOR-WRITE" (hex 44 46 33 31 33 39 2d 50 57 4e 45 44 ...) β the unprivileged pwrite modified a raw device sector the file owns no allocation for (its declared allocation is ZERO blocks).
- File pread afterwards returns the marker β the mapping is live.
The write path: hpfs_write -> hpfs_hpbmap (offset inside fn_size, so no
extend/allocation happens) -> getblk/bread(hp->h_devvp, dbtodoff(bn))
-> uiomove β i.e. plain device-relative I/O at the crafted address.
Aiming at sector 0x10 would have overwritten the mounted filesystem's
superblock (we targeted the bitmap-dir sector instead to keep the mount
usable; nothing restricts the target except bn >= al_off and the
device size).
Impact
- On the audit guest: unprivileged destruction/disclosure of any sector of the backing device (dos-grade here β single-FS vn device).
- On a real system with the crafted image mounted (or via vfs.usermount): arbitrary read/write relative to the WHOLE disk device β other slices, other filesystems, disklabel β from an unprivileged file pwrite/pread. Filesystem-integrity and multi-tenant-disk confinement break; realistic host-compromise enabler when another slice holds the OS.
Fix validation
fix.diff (shared, DF-3137 pack) drops the !alp->al_len || escape so a
zero-length leaf matches nothing; patched kernel: the same pread/pwrite
return EFBIG, raw sector 0x30 unchanged (fix_run.log).
Verdict
REPRODUCED β arbitrary raw-device sector read/write from unprivileged
file I/O. Kernel refs: sys/vfs/hpfs/hpfs_alsubr.c:113-143 (leaf match +
mapping), :118-119 (the !alp->al_len clause), :122 (*bnp arithmetic),
sys/vfs/hpfs/hpfs_vnops.c:283-289 (VOP_BMAP exposure), :322-346 (read),
:385-430 (write).
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
fixedidentical trigger on patched kernel: pread/pwrite return EFBIG, raw sector 0x30 unchanged
fix_run.log
Confirmed kernel references
Detail
Exploit chain
crafted image (leaf {al_off=0x1000, al_len=0, al_lsn=0x10}, fn_size=4MB) -> RW mount -> unprivileged pread/pwrite at offsets >= 0x200000 stay inside fn_size (no extend/allocation) -> hpbmap maps lbn to al_lsn+(lbn-al_off) -> bread/getblk on h_devvp at the chosen raw sector -> data returned to user / user data written to raw device. Target restricted only by bn>=al_off and device size; on multi-slice disks this is cross-partition read/write.
Evidence (decisive lines)
["run.log: pread returned su_magic via file offset 0x200000; pwrite rc=512; raw sector 0x30 contains 'DF3139-PWNED-RAW-SECTOR-WRITE' (hex dump)", 'fix_run.log: patched kernel EFBIG, sector 0x30 still 38 00 00 00']
PoC changes
written fresh; related to DF-3099 (which covers hpfs_write's (runl+1)*DEV_BSIZE arithmetic fed by runp=3) but the arbitrary-device-addressing consequence of the !al_len clause itself is distinct and was not previously reported
Verified recommended fix
remove the '!alp->al_len ||' escape so a zero-length leaf matches nothing (hole semantics via EFBIG); OS/2 file alblks never carry 0-length leaves
Verdict
hpfs_hpbmap's leaf-match clause '!alp->al_len ||' (alsubr.c:118-119) treats a zero-length alleaf as an extent reaching to infinity, so *bnp = bn - al_off + al_lsn (:122) maps ANY file block number bn>=al_off to an arbitrary raw sector of the device backing the filesystem. Verified on the guest as uid 1001: pread at file offset 0x200000 returned the mounted filesystem's SUPERBLOCK (magic 0xfa53e9c5f995e849) and pread at 0x204000 returned the bitmap-dir word 0x38 β arbitrary-sector disclosure; pwrite at 0x204000 overwrote raw device sector 0x30 with a marker (verified by root dd of /dev/vn0) although the file's declared allocation is ZERO blocks β arbitrary-sector write. On a real multi-slice disk this reads/writes any sector of the whole device (disklabel, other slices/OSes) from an unprivileged file pwrite/pread; on the single-filesystem guest the demonstrated impact is destruction of device content (dos-class here, host-compromise enabler in deployment). Fix (drop the al_len==0 escape) validated: same PoC returns EFBIG, sector 0x30 unchanged.
No comments yet.