Unvalidated on-disk ab_freeoff used as insertion offset: controlled 12-byte kernel-heap OOB write at attacker-chosen offset (hpfs_addextent:452 / hpfs_addextentr:643 / hpfs_concatalsec:278)
Summary
ab_freeoff (u_int16_t, raw on-disk) is never validated - fnode load checks only fn_magic and hpfs_breadalsec only AS_MAGIC - yet AB_FREEALP/AB_FREEANP use it directly as the byte offset for new allocation entries. One unpriv pwrite that extends a file by an extent on a crafted RW-mounted HPFS image writes a 12-byte alleaf {al_off, al_len, al_lsn} (all three attacker-crafted via fn_size and the bitmap free-run layout) at a fully chosen 16-bit offset (0..0xFFFF) past a kernel heap object: the 512-byte AlSec buffer (addextentr:643), the kmalloc'd hpfsnode (addextent:452), or the concat destination (:278). Writes march forward (+12 per insert) giving a contiguous structured heap overwrite of up to ~60KB per pwrite. VERIFIED: freeoff=0x1000 -> 'Fatal trap 12 supervisor write' at hpfs_addextentr+0xed (the AL_SET store at :643); freeoff=500 -> write exactly at b_data[512..524), pwrite succeeds, guest stays up = silent adjacent-chunk corruption. uid0 route characterized: needs absolute chunk address + same-zone victim within +64K - DF-3138 proves live 512-byte buffer chunks sit adjacent; cross-zone targets need zone-adjacency grooming - documented hard blocker; panic + neighbor corruption fully proven. Fix validated (hpfs_alblk_sane() at every trust boundary: busycnt<=capacity, freeoff==sizeof+busycnt*sz, freecnt<=capacity-busycnt): same trigger -> EINVAL 'INSANE ALBLK', no panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3137 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.py | β | 6.8 KB | view raw | |
| poc.c | β | 1.0 KB | view raw | |
| mount_rw.c | β | 1.3 KB | view raw | |
| build.sh | β | 138 B | view raw | |
| run.sh | β | 112 B | view raw | |
| build.log | β | 96 B | view raw | |
| run.alsec.500.log | β | 463 B | view raw | |
| run.alsec.1000.log | β | 229 B | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| fix.diff | β | 4.3 KB | view raw | |
| fix_run.log | β | 762 B | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| README.md | β | 1.9 KB | β raw | |
| env.txt | β | 464 B | view raw |
DF-3137 β unvalidated ab_freeoff used as insertion offset: controlled 12-byte kernel-heap OOB write (hpfs_addextent:452 / hpfs_addextentr:643 / hpfs_concatalsec:278)
Build
./build.sh # crafts df3137.img on the host (VARIANT/FREEOFF
# env vars), pushes image + poc.c + mount_rw.c to
# the guest, compiles there with cc
Run
./run.sh # mounts /dev/vn0 (RW via /root/mount_rw) on
# /tmp/hpfsmp (owned by uid 1001), runs the PoC
# as the unprivileged user `maxx` (uid 1001)
VARIANT=alsec FREEOFF=
Expected
- FREEOFF=500 (write exactly at
bp->b_data[512..524)): pwrite returns 32768, guest stays up β SILENT corruption of the adjacent 512-byte kernel heap chunk with {al_off=1, al_len=0x41, al_lsn=0x200}. - FREEOFF=0x1000 (write 4 KB past the 512-byte AlSec buffer):
kernel panic β
Fatal trap 12: page fault while in kernel mode, supervisor write data, page not presentathpfs_addextentr+0xed: movl %edx,(%r12,%rax,1)(= the AL_SET store at sys/vfs/hpfs/hpfs_alsubr.c:643).
Root cause: ab_freeoff (u_int16_t) is raw on-disk data, never validated
(hpfs_vfsops.c:535 checks only fn_magic; hpfs_breadalsec only
AS_MAGIC), and AB_FREEALP(abp) = (alleaf_t *)((caddr_t)abp +
abp->ab_freeoff) (hpfs.h:154) uses it directly as the write offset for
new allocation entries. Trigger: one unprivileged pwrite(2) that extends
the file by one extent.
Files: craft_img.py (image generator), poc.c (trigger),
mount_rw.c (RW-mount helper, DF-0859 precedent), fix.diff
(validated fix), run.alsec.500.log / run.alsec.1000.log (runs),
panic.txt (captured panic), fix_run.log (patched-kernel rerun).
DF-3137 VERDICT β REPRODUCED (kernel-heap OOB write, panic + silent-neighbor variants)
What was run
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026,
X86_64_GENERIC (stock INVARIANTS kernel), single-tenant QEMU/KVM.
Crafted HPFS image (craft_img.py), mounted RW on /dev/vn0 via direct
mount(2) (mount_rw.c; stock mount_hpfs forces RO β DF-0859 precedent),
mountpoint owned by uid 1001. Trigger executed as uid 1001 (dfbsd-maxx).
Run 1 β adjacent-chunk silent corruption (FREEOFF=500)
pwrite(fd, 32768 bytes, offset 512) as uid 1001 returned 32768; guest
stayed alive (run.alsec.500.log). Analysis: the new alleaf
{al_off=1, al_len=0x41, al_lsn=0x200} was written at
C.b_data + 12 + 500 = b_data[512..524) β 12 fully attacker-chosen bytes
(al_len/al_lsn crafted via the bitmap free-run layout, al_off via
fn_size) starting exactly at the end of the 512-byte buffer-cache chunk.
No kernel object complained β silent corruption of the neighboring heap
chunk (another 512-byte buffer data area in the same kmalloc zone).
Run 2 β panic (FREEOFF=0x1000)
Same trigger with the write aimed 4 KB past the 512-byte AlSec buffer: the unprivileged pwrite never returned; the kernel took
Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff80065b4700c fault code = supervisor write data, page not present instruction pointer = hpfs_addextentr+0xed: movl %edx,(%r12,%rax,1)
hpfs_addextentr+0xed is exactly the AL_SET(AB_FREEALP(rabp), ...)
store at sys/vfs/hpfs/hpfs_alsubr.c:643 (AL_SET writes al_off/al_len/
al_lsn with three movl stores; this one is the al_len store at
abp + freeoff + 4). The fault VA is in the kernel heap (kmem map) β
the write reached unmapped kernel VA 0xfffff80065b4700c = the buffer
chunk + 0x1000 + 12. Guest died at db> (panic.txt).
Primitive characterization (uid0 assessment)
- 12-byte write, contents = three u32s the attacker chooses when crafting
the image (al_off <= 0x7FFFFF via fn_size; al_len/al_lsn via bitmap
free-run geometry), at a fully chosen 16-bit offset (freeoff) from the
start of a kmalloc'd 512-byte buffer (AlSec variant) or from
&hpfsnode->h_fn.fn_ab(fnode variant β writes past the ~850-byte kmalloc(1024-class) hpfsnode). - Repeatable and MARCHING: each additional extent insert in the same addextent loop writes another 12 bytes at freeoff+=12 β a contiguous structured heap overwrite of up to ~60 KB per pwrite (freecnt-bounded).
- On this no-KASLR/no-SMAP guest the write is kernel-heap-relative; a reliable uid0 chain additionally needs the absolute address of the AlSec data chunk (deterministic on a fresh boot but not directly observable from userspace) and a same-zone target at +0..+64K (neighbors are other 512-byte buffer-data chunks β the DF-3138 run demonstrates such adjacency is real and observable). Cross-zone targets (e.g. a neighboring hpfsnode's h_uid/h_gid/h_mode = exactly 12 bytes) require zone-adjacency grooming that was not demonstrated within budget β documented as the hard blocker for the uid0 chain; both silent-neighbor corruption and unmapped-VA panic are proven.
Fix validation
fix.diff adds hpfs_alblk_sane() (packed-array invariant:
busycnt <= capacity, freeoff == sizeof(alblk_t)+busycnt*sz,
freecnt <= capacity-busycnt) enforced at fnode load (hpfs_vfsops.c) and
at every AlSec read in hpbmap/addextentr/truncatealblk, plus the two
missing brelse()s (DF-3140) and the al_len==0 hole fix (DF-3139).
Applied to the guest's /usr/src, make nativekernel, rebooted:
the identical FREEOFF=0x1000 trigger returns EINVAL from pwrite
("hpfs_addextentr: INSANE ALBLK" on console), no panic, guest stays up
(fix_run.log). Baseline reproduced / patched not reproduced.
Verdict
REPRODUCED: unprivileged controlled-offset 12-byte kernel heap OOB write with attacker-chosen contents; panic at +4 KB, silent neighbor corruption at +0. Fix validated. Kernel refs: sys/vfs/hpfs/hpfs_alsubr.c:427-456 (root insert), :610-653 (AlSec insert), :278 (concat dest), hpfs.h:152-171 (macros), sys/vfs/hpfs/hpfs_vfsops.c:529-544 (no validation at load).
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 returns EINVAL ('hpfs_addextentr: INSANE ALBLK'), no panic, clean unmount
fix_run.log
Confirmed kernel references
Detail
Exploit chain
crafted HPFS image -> RW mount (mount(2) direct; mountpoint handed to uid 1001) -> open(file) -> pwrite extends file by one extent -> hpfs_addextent -> hpfs_addextentr(C): contiguity passes (crafted leaf[0]), bmlookup returns crafted {nlen,nlsn}, non-contiguous -> AL_SET writes 12 controlled bytes at abp+freeoff. Repeatable/marching (freeoff += 12 per insert) -> contiguous structured heap overwrite up to ~60KB per pwrite. uid0 route: needs absolute address of the buffer-data chunk (deterministic per boot, no KASLR, but not userspace-observable) and a same-zone (512-class) victim at +0..+64K β DF-3138 proves 512-chunk adjacency with live buffers is real; cross-zone targets (neighboring hpfsnode h_uid/h_gid/h_mode = 12 bytes) need zone-adjacency grooming not demonstrated within budget (documented hard blocker). Panic + silent-neighbor corruption fully proven.
Evidence (decisive lines)
['run.alsec.1000.log + panic.txt: Fatal trap 12 at hpfs_addextentr+0xed (AL_SET store), supervisor write to 0xfffff80065b4700c', 'run.alsec.500.log: freeoff=500 pwrite rc=32768, guest alive = silent adjacent-chunk corruption', "fix_run.log: patched kernel returns EINVAL, 'hpfs_addextentr: INSANE ALBLK', no panic"]
PoC changes
poc/craft written fresh for this finding (DF-0859/0857/0858 harness patterns reused: mount_rw.c, image skeleton). Two variants (alsec/fnode) + parameterized freeoff.
Verified recommended fix
validate the alblk packed-array invariant (busycnt<=cap, freeoff==8+busycnt*sz, freecnt<=cap-busycnt) at fnode load and every AlSec bread (hpfs_alblk_sane helper)
Verdict
Unvalidated on-disk ab_freeoff is used directly as the write offset for new allocation entries (AL_SET(AB_FREEALP(abp)) at hpfs_addextent:452 / hpfs_addextentr:643, bcopy dest at concatalsec:278): a single unprivileged pwrite that extends a file on a crafted RW-mounted HPFS image writes a 12-byte attacker-crafted alleaf {al_off,al_len,al_lsn} at an attacker-chosen 16-bit offset past a kernel heap object. Verified twice on the guest: freeoff=500 silently corrupts the adjacent 512-byte heap chunk (pwrite succeeds, guest up); freeoff=0x1000 deterministically panics the kernel with 'Fatal trap 12, supervisor write data, page not present' at hpfs_addextentr+0xed == the AL_SET store (alsubr.c:643). Fix (alblk packed-array invariant validation at fnode load and every AlSec read) validated on a rebuilt kernel: same trigger returns EINVAL ('INSANE ALBLK'), no panic.
No comments yet.