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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3137 Β· 14 files
FileTypeDescriptionSize
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= ./build.sh # re-craft with a different target # offset before ./run.sh

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 present at hpfs_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).

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

identical trigger on patched kernel returns EINVAL ('hpfs_addextentr: INSANE ALBLK'), no panic, clean unmount

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 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.