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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3139 Β· 12 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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):

  1. pread(fd, 512, 0x200000) -> lbn 0x1000 -> sector 0x10: returned the SUPERBLOCK, first 8 bytes = 0xfa53e9c5f995e849 (su_magic).
  2. pread(fd, 512, 0x204000) -> lbn 0x1020 -> sector 0x30: returned 0x00000038 (the bitmap-dir pointer word stored there).
  3. pwrite(fd, "DF3139-PWNED-RAW-SECTOR-WRITE", 0x204000) -> rc=512.
  4. 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).
  5. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

identical trigger on patched kernel: pread/pwrite return EFBIG, raw sector 0x30 unchanged

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #2: Mon Sep 7 00:17:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.