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).
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2663 Β· 28 filesFix verification
fixedkernel 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']
Confirmed kernel references
- sys/vfs/hammer2/hammer2_ondisk.c:283
- sys/vfs/hammer2/hammer2_ondisk.c:684
- sys/vfs/hammer2/hammer2_ondisk.c:731
- sys/vfs/hammer2/hammer2_chain.c:998
- sys/vfs/hammer2/hammer2_chain.c:5530
- sys/vfs/hammer2/hammer2_io.c:276
- sys/vfs/hammer2/hammer2_io.c:356
- sys/kern/subr_diskslice.c:253
- sys/kern/subr_diskslice.c:266
- sys/kern/subr_diskslice.c:275
- sys/kern/vfs_bio.c:3287
- sys/kern/vfs_bio.c:4462
- sys/kern/vfs_bio.c:4264
- sys/dev/disk/vn/vn.c:342
- sys/dev/disk/dm/device-mapper.c:399
- sys/kern/subr_disk.c:853
- sys/kern/subr_disk.c:1196
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.
No comments yet.