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

hammer2 mount-time volume-size containment check silently skipped when DIOCGPART fails; forged volu_size beyond backing media accepted, short-read media-EOF semantics then serve uninitialized kernel memory as file data

Field Value
ID DF-2663
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
CWE CWE-754 (chaining CWE-909 uninitialized memory)
File sys/vfs/hammer2/hammer2_ondisk.c
Lines 283-291 (conditional check), 684 (raw volu_size)
Area vfs
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass β€” confirmed the bulkfree-audit lead)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_verify_volumes_common() executes the "volume size must not exceed device media size" check only inside the DIOCGPART-success branch (ondisk.c:283-291). vol->size is the raw on-media volu_size taken from the CRC-passing but attacker-forged volume header (:684). Whenever VOP_IOCTL(DIOCGPART) fails β€” real modes: async slice-probe race via disk_setdiskinfo() for md/dm (dp->d_slice==NULL β†’ ENOIOCTL), drivers whose probe never ran, raw-fallback ENOTTY β€” a volu_size far beyond the real backing media is accepted silently. Chain reads at data_off ∈ [real_media, forged_volu_size) hit media-EOF semantics that complete WITHOUT B_ERROR β€” dscheck clamps straddling bios (subr_diskslice.c:275-276), completes at-EOF reads with B_INVAL only (:266-269), vn's short VOP_READ sets no error (vn.c:342-371) β€” so breadnx/_biowait report success and hammer2_chain_load_data (chain.c:998-1009) consumes the buffer, which is backed by freshly allocated non-zeroed VM pages (bio_page_alloc, vfs_bio.c:4264-4291). With a CHECK_NONE DATA blockref that uninitialized kernel memory is copied to userspace by read(2).

Threat model & preconditions

Crafted-filesystem-image threat model (mount of a malicious image, root or vfs.usermount=1 with an owned device; the DIOCGPART-failure enabler occurs on md/dm probe races or non-probed disks). The identical sink is equally reachable when the backing media shrinks below its registered size after device registration (vn backing truncated β€” stock-kernel proof). Yields unbounded, repeatable disclosure of recycled kernel/user memory contents (other principals' freed heap/pages) through ordinary read(2), plus silent data fabrication instead of EIO. Demonstrated on the STOCK kernel: 64KB fabricated file data per read, md5 varying per run, a decisive run containing a freed userland process's heap marker on 14 of 16 pages (cross-principal kernel-memory disclosure). Secondary: silent write loss past EOF; composition with DF-0875 (panic when data_off β‰₯ volu_size; window shifts with forged size β€” captured live).

Proof of concept

VERIFIED (findings/poc/DF-2663/): injector kernel A accepted size=0x14000000 (320MB) on a 256MB device when the ioctl failed and rejected the identical image when it succeeded; stock kernel accepted the same oversized volu_size on a dm-linear volume (run.log). Stock leak chain: forged DATA bref at 192MB|radix16 with CHECK_NONE + full XXH64/CRC32C cascade recompute β†’ vnconfig registers 256MB+32KB β†’ truncate backing to 192MB β†’ mount ro β†’ cat returns 64KB of stale kernel memory; heap-groom marker captured on 14/16 pages (mk2.bin).

Fail closed on unknown media size, and never consume short reads β€” see findings/poc/DF-2663/fix.diff (ondisk.c: refuse mount when DIOCGPART fails; io.c: treat b_resid != 0 || b_bcount != psize as EIO). Validated on kernel B: oversized+ioctl-fail mount rejected fail-closed; the vn-truncation stale read returns EIO with 0 bytes; normal mounts/reads unregressed (md5 match).

References

  • DF-0875 (panic on out-of-range data_off), DF-2661/DF-2662 (the EIO-path family), DF-2616 (forging family)

Timeline

  • 2026-08-29 Confirmed and expanded from the bulkfree-audit lead; stock-kernel leak reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2663 Β· 28 files
FileTypeDescriptionSize
mk2.bin β€” 64.0 KB ↓ download
leakA.bin β€” 64.0 KB ↓ download
leak1.bin β€” 64.0 KB ↓ download
stale_read.bin β€” 64.0 KB ↓ download
seed_read.bin β€” 64.0 KB ↓ download
run.log β€” 1.5 KB view raw
run3.log β€” 91.3 KB view raw
run4.log β€” 1.7 KB view raw
runA.log β€” 2.4 KB view raw
runB.log β€” 2.9 KB view raw
forge2663.c β€” 4.0 KB view raw
diocgtest.c β€” 825 B view raw
df2663_mark.c β€” 351 B view raw
xxtest.c β€” 684 B view raw
stage1_df2663.sh β€” 882 B view raw
stage2_vn_trunc.sh β€” 1.4 KB view raw
stage2_leak_df2663.sh β€” 1.3 KB view raw
stage3_seed_df2663.sh β€” 2.3 KB view raw
stage4_samedev.sh β€” 1.3 KB view raw
stageA_df2663.sh β€” 1.3 KB view raw
stageB_df2663.sh β€” 1.8 KB view raw
inject2663.diff β€” 1.6 KB view raw
fix2663.diff β€” 2.0 KB view raw
fixB_on_A.diff β€” 1.6 KB view raw
buildA.log.gz β€” 199.3 KB ↓ download
buildB.log.gz β€” 200.3 KB ↓ download
panic_df0875_interaction.txt β€” 1.4 KB view raw
env.txt β€” 1.1 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

kernel B (stock+injector+fix2663.diff): B1 DIOCGPART-failure mount of the oversized forged volume now rejected fail-closed ('media size could not be determined, refusing to mount', RC=1) vs accepted on baseline; B2 stock vn-truncation scenario read returns EIO with 0 bytes (was 64KB stale); B3 normal intact-image mount/read unregressed (md5 matches). Bad behavior gone.

["findings/poc/DF-2663/runB.log - B1_MOUNT_RC=1 + fail-closed dmesg line; B2_READ_RC=1 'Input/output error'; B3 md5 match", 'findings/poc/DF-2663/fix2663.diff (vs pristine) and fixB_on_A.diff (as applied on the injector kernel)', 'findings/poc/DF-2663/buildB.log.gz - full untrimmed kernel build/install log']
DragonFly dfbsd 6.5-DEVELOPMENT #3: Sun Aug 30 08:36:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted hammer2 image (CHECK_NONE DATA blockref past readable EOF) -> victim mounts on device whose registered media > readable backing (vn backing truncated/shrunk after registration, or oversized dm table) [or, per the filed bug: forged volu_size beyond media accepted whenever DIOCGPART fails at mount: probe race / d_slice==NULL / raw-fallback ENOTTY] -> hammer2_chain_load_data trusts hammer2_io_bread success -> breadnx returns 0 because vn's short VOP_READ sets no B_ERROR (dscheck clamp/B_INVAL paths equivalent) -> buffer backed by uninitialized VM pages from the free queue -> read(2) copies 64KB of recycled kernel/user memory to the mounting user

Evidence (decisive lines)

['findings/poc/DF-2663/mk2.bin - 64KB read(2) output containing DF2663USERHEAP-PAGE-MARKER on 14/16 pages (freed heap of another process); md5 206b21b974edb3ac4f1985659886acbf', "findings/poc/DF-2663/runA.log - A1 (ioctl OK): 'size 0x14000000 exceeds device size 0x10008000' mount EINVAL vs A2 (ioctl failed): A2_MOUNT_RC=0 + A2_READ_RC=0 leakA.bin md5 93736e0709f4cca3dcd13277d019acca", "findings/poc/DF-2663/runB.log - B1 'media size could not be determined, refusing to mount' RC=1; B2 'Input/output error' RC=1 0 bytes; B3 md5 regression clean", 'findings/poc/DF-2663/run.log - stock dm acceptance of size=0x14000000 with diocgtest showing media_size=table size', 'findings/poc/DF-2663/panic_df0875_interaction.txt - live DF-0875 composition', 'findings/poc/DF-2663/forge2663.c - bref retarget + XXH64/CRC32C cascade (kernel xxhash.c linked, hashes verified equal to on-media values)']

PoC changes

rebuilt the PoC from scratch: original sketch had no working image forger - wrote forge2663.c (blockref data_off is at bref+0x20, not +0x38/+0x18; methods at +0x01; full XXH64 check cascade through inode/iroot/suproot/volhdr levels; volhdr CRC32C write ORDER matters because icrc_sects[6] is covered by sect0); data_off must be strictly < volu_size or get_volume panics (DF-0875); at-exact-EOF reads EIO under default cluster_data_read=4 (merged RA bio) so the soft-EOF vn-truncation variant is the reliable trigger; dm-oversized media proves the oversized-volu_size acceptance but dm's nestiobuf converts short reads to EIO

Verified recommended fix

fail closed when DIOCGPART fails (ondisk.c) + treat short reads (b_resid!=0 || b_bcount!=psize) as EIO in hammer2_io.c; see fix2663.diff (validated: fixed)

Verdict

REPRODUCED (leak). The mount-time volume-size containment check at sys/vfs/hammer2/hammer2_ondisk.c:283-291 only runs when VOP_IOCTL(DIOCGPART) returns 0; on any failure it is skipped silently and vol->size is the raw forged on-media volu_size (ondisk.c:684). Demonstrated three ways on the QEMU guest: (1) STOCK kernel sink - a crafted image whose file DATA blockref (methods=0x00 CHECK_NONE) points past the readable EOF of a vn device whose registered media is larger (backing truncated after vnconfig) makes cat(1) return 64KB of FABRICATED data: bread() succeeds on vn's 0-byte short VOP_READ (vn.c:342-371, no B_ERROR; _biowait only checks B_ERROR at vfs_bio.c:3287) over uninitialized pages (bio_page_alloc, no VM_ALLOC_ZERO); decisive artifact mk2.bin contains the DF2663USERHEAP marker of a just-exited userland process's heap on 14 of 16 pages - cross-principal kernel-memory disclosure via read(2); three prior runs returned three different buffers (live memory). (2) INJECTOR kernel A (DF-2644-precedented env simulation, sysctl-gated DIOCGPART failure): the identical forged 320MB volu_size image is rejected when the ioctl succeeds (size exceeds device size) and ACCEPTED when it fails (runA.log A1 vs A2) - the skip itself, live; the read then leaks (leakA.bin). Stock support: dm-linear with oversized table reported media_size=320MB and the mount accepted size=0x14000000 (run.log), though dm's nestiobuf wrapper converts later short reads to EIO. (3) FIX kernel B (fail-closed check + short-read guard): oversized+ioctl-fail mount rejected with 'media size could not be determined', the stock vn-truncation read now returns EIO instead of stale bytes, normal mounts/reads unregressed. Composes with known DF-0875 (panic when data_off >= volu_size; window shrinks with oversized forged size) - captured in panic_df0875_interaction.txt, not re-reported. No write primitive; uid=0 not applicable to this class.