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

volume-add trusts ioc->vol_size with no device-size validation; KKASSERT error paths and unconditional-success error overwrite β†’ deterministic kernel panic (INVARIANTS) or silent half-added volume (production)

Summary

hammer_format_volume_header derives vol_buf_end purely from the user-supplied ioc->vol_size (:662) with only a signedness check (:664); hammer_install_volume never compares it to the real device size (hammer_ondisk.c:109-272 - unlike HAMMER2). A vol_size claiming 2 GiB on a 2 MiB device passes every check; hammer_format_freemap's layer2 breads then walk past the device end and the stock INVARIANTS kernel panics deterministically ('Initialize freemap volume 1' -> panic: cluster_callback: unexpected EOF). On production kernels the failure is silent: KKASSERT(error==0) at :118/:122/:123 compile out, and hammer_update_volumes_header never sets error, overwriting the failure so the ioctl returns SUCCESS with the half-formatted volume installed, nvolumes/vol_count bumped, and root-freemap layer1 entries pointing at unreadable layer2 blocks; later volume-del trips more KKASSERTs. Same unwinding gap fires via transient device I/O error mid-add. Not a re-report of DF-0869 (that is the beg/end integer-overflow arithmetic; this is the missing device-size cross-check + error-path discipline - different trigger, failure mode, fix location). Privileged (SYSCAP_NOVFS_IOCTL); no unpriv route. VERIFIED 2 runs deterministic from clean resets: custom ioctl caller {vol_size=2147483648} on a 2 MiB vn1 -> 'panic: cluster_callback: unexpected EOF', guest in ddb>. Fix authored (hammer2-style DIOCGPART size check + real unwind path; dry-run verified).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3152 Β· 12 files
FileTypeDescriptionSize
README.md β€” 3.7 KB ↓ raw
VERDICT.md β€” 5.3 KB ↓ raw
voladd_lie.c β€” 1.9 KB view raw
build.sh β€” 98 B view raw
run.sh β€” 213 B view raw
build.log β€” 238 B view raw
run.log β€” 2.4 KB view raw
run.2.log β€” 2.3 KB view raw
env.txt β€” 190 B view raw
fix.diff β€” 2.4 KB view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 4.0 KB view raw

DF-3152 β€” HAMMER1 volume-add trusts ioc->vol_size; no device-size validation + KKASSERT error paths (deterministic kernel panic / silent half-add)

What

hammer_ioc_volume_add (sys/vfs/hammer/hammer_volume.c:63) accepts three fully attacker-controlled int64 fields from the privileged ioctl caller: vol_size, boot_area_size, memory_log_size. hammer_format_volume_header (hammer_volume.c:616) turns vol_size into vol_buf_end with only a signedness check (HAMMER_VOL_BUF_SIZE < 0, hammer_volume.c:664 β€” DF-0869 covers the arithmetic overflow variant). The claimed size is never compared against the actual size of the device that gets opened by hammer_install_volume (hammer_ondisk.c:109 β€” no DIOCGPART/media_size check anywhere on the HAMMER1 add path; contrast HAMMER2 which validates this: sys/vfs/hammer2/hammer2_ondisk.c:283-291).

When the claim exceeds the real device, hammer_format_freemap (hammer_volume.c:382) bread()s layer2 entries at device offsets [vol_buf_beg .. vol_buf_beg+8MB) β€” walking past the device end. The clustered read completes against the slice bounds check and the kernel panics in cluster_callback ("unexpected EOF"), reproducibly, from a single ioctl.

Even when the I/O error is returned cleanly instead of panicking, the add path cannot handle it: KKASSERT(error == 0) at hammer_volume.c:118/122/123 panics INVARIANTS kernels, and on production kernels the code continues and overwrites the error with hammer_update_volumes_header's unconditional 0 (hammer_volume.c:673-739 β€” error is never set) β€” the ioctl returns success with a half-formatted volume installed, nvolumes incremented, and layer1 entries on the root volume already pointing at unreadable layer2 blocks on the new volume. Every later allocation that dips into that volume's range fails (fs wedge), and volume-del of that volume then hits KKASSERT(layer1->phys_offset != HAMMER_BLOCKMAP_UNAVAIL) / bread-error KKASSERTs (hammer_volume.c:541/577, 232).

Privilege required

HAMMERIOC_ADD_VOLUME is gated twice by caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72,197) β†’ root-equivalent. Impact is a privileged-user-triggered kernel panic (deterministic, INVARIANTS kernels) or silent filesystem corruption/wedge (non-INVARIANTS kernels). Root can also hit this accidentally (transient device I/O error mid-add, e.g. USB yank β€” the error-path discipline bug, not the lie).

How to reproduce (on the audit QEMU guest)

# 1. 12 GiB sparse backing file + HAMMER1 fs on vn0; 2 MiB device on vn1
mkdir -p /root/h1test /mnt/h1
dd if=/dev/zero of=/root/h1test/base.img bs=1m count=0 seek=12288
dd if=/dev/zero of=/root/h1test/small.img bs=1m count=2
vnconfig -c vn0 /root/h1test/base.img
vnconfig -c vn1 /root/h1test/small.img
newfs_hammer -f -L test /dev/vn0
mount_hammer /dev/vn0 /mnt/h1

# 2. build + run the trigger (claims vol_size = 2 GiB on the 2 MiB vn1)
cc -O -I/usr/src -o voladd_lie voladd_lie.c
./voladd_lie /mnt/h1 /dev/vn1 2147483648

Expected result

Serial console (vm.sh log):

HAMMER(test) Initialize freemap volume 1
panic: cluster_callback: unexpected EOF on cluster 0xfffff8004f485cb0!
cpuid = 0
Trace beginning at frame ...
cluster_callback() at cluster_callback+0x192
vnstrategy() at vnstrategy+0x125
...
Debugger("panic")
db>

Observed twice (run.log, run.2.log), deterministic. On a non-INVARIANTS kernel the failure instead surfaces as the ioctl returning success with a broken volume installed (see VERDICT.md).

Fix

fix.diff (not applied to sys/): DIOCGPART size check in hammer_install_volume (mirroring HAMMER2), plus real error unwinding in hammer_ioc_volume_add instead of KKASSERTs + error overwrite.

VERDICT.md
↓ download raw

DF-3152 VERDICT β€” reproduced (kernel panic, deterministic), privileged trigger

Bottom line

reproduced / panic / certain. A single HAMMERIOC_ADD_VOLUME ioctl whose vol_size field claims more capacity than the target device physically has panics the stock INVARIANTS DragonFly kernel, deterministically, in the freemap-formatting stage of the add. Reproduced twice from a clean vm.sh reset with-src guest (identical signature both times).

Guest: DragonFly 6.5-DEVELOPMENT #0 X86_64_GENERIC (see env.txt). Test fs: HAMMER1 v7, 12 GiB sparse vn0, mounted rw at /mnt/h1. Lying volume: /dev/vn1, real size 2 MiB, claimed vol_size 2147483648 (2 GiB).

Mechanism (line-accurate)

  1. hammer_ioctl dispatches HAMMERIOC_ADD_VOLUME after caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) (hammer_ioctl.c:72,197) β€” root-only. The ioctl struct is copied in wholesale; vol_size/boot_area_size/memory_log_size are raw user int64s.
  2. hammer_format_volume_header (hammer_volume.c:661-662) computes vol_buf_end = ioc->vol_size & ~HAMMER_BUFMASK. The only validation is HAMMER_VOL_BUF_SIZE(ondisk) < 0 (hammer_volume.c:664) β€” a signedness check, no comparison against the device. (The overflow variant of this check's inadequacy is DF-0869; the missing device-size comparison is separate and unfixed.)
  3. hammer_install_volume (hammer_ondisk.c:109) opens the device, never queries its size (no DIOCGPART; HAMMER2 does: hammer2_ondisk.c:283-291), computes maxbuf_off from the claimed size (hammer_ondisk.c:212-213), writes the header at offset 0 (succeeds even on a tiny device), inserts the volume.
  4. hammer_format_freemap (hammer_volume.c:415 "Initialize freemap volume 1" printed on console) bread()s the new volume's layer2 entries. Layer2 breads target device offsets [vol_buf_beg .. vol_buf_beg + 8 MiB) (zone2 phys = vol_buf_beg + off, hammer_ondisk.c:721) β€” past the 2 MiB device end. KKASSERT(zone2_offset < volume->maxbuf_off) (hammer_ondisk.c:709) passes because maxbuf_off is derived from the lie.
  5. The clustered read past EOF is completed by the slice/disk layer with a residual; the kernel panics in cluster_callback β€” panic: cluster_callback: unexpected EOF on cluster ... β€” with the stack cluster_callback β†’ vnstrategy β†’ dev_dstrategy β†’ diskstrategy. See run.log:45-55, run.2.log.

Why the error-path discipline matters (production-kernel variant)

Had the read been failed cleanly with EIO instead, the add path still cannot cope: hammer_volume.c:117-123 is

error = hammer_format_freemap(trans, volume);
KKASSERT(error == 0);                      /* panic on INVARIANTS */
error = hammer_count_bigblocks(...);
KKASSERT(error == 0);
KKASSERT(total_bigblocks == empty_bigblocks);
...
++hmp->nvolumes;
error = hammer_update_volumes_header(...); /* ALWAYS returns 0 */
KKASSERT(error == 0);

hammer_update_volumes_header (hammer_volume.c:673-739) never sets error, so on a non-INVARIANTS kernel any format/count failure is overwritten with 0 and the ioctl reports success: the volume is installed and counted in vol_count on every member header, but its freemap is un/partially formatted, and layer1 entries on the root volume (written by the first loop of hammer_format_freemap, hammer_volume.c:417-434) already point at unreadable layer2 blocks. Later allocations dipping into that volume error out; hammer volume-del of it later trips KKASSERT(layer1->phys_offset != HAMMER_BLOCKMAP_UNAVAIL) / bread-error KKASSERTs (hammer_volume.c:541,577,232). The same unwinding gap is triggered without any "lie" by a transient device I/O error mid-add (USB yank) β€” root does not have to be malicious, just unlucky.

Not a duplicate of DF-0869

DF-0869 = integer-overflow of vol_buf_beg/end arithmetic (negative boot_area_size etc.) β†’ oversized freemap loops. DF-3152 = the claimed size is never checked against the real device size (a perfectly in-range, non-overflowing vol_size on a device smaller than claimed) + the add path's KKASSERT/error-overwrite discipline. Different trigger, different failure mode (EOF panic in cluster code / silent success vs livelock), different fix location (device-size check at install + error unwinding).

Exploit chain

Not applicable β€” this is a privileged-trigger panic/DoS and filesystem corruption primitive, not memory corruption. No uid=0 route exists for an unprivileged user: the ioctl is double-gated on SYSCAP_NOVFS_IOCTL.

Fix validation

fix.diff authored (DIOCGPART media_size check in hammer_install_volume + error unwinding in hammer_ioc_volume_add). Kernel rebuild + patch validation not performed: finding is Low severity / privileged DoS, not in the memcorrupt/privesc buckets, and fix validation is mandatory only for reproduced memory-corruption findings. The patch is mechanical and mirrors the already-shipped HAMMER2 check.

Guest hygiene

Guest panicked twice and was reset with vm.sh reset with-src after each run; final state: up, clean snapshot, no leftovers.

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

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored and verified to apply cleanly (patch -p1 --dry-run) but kernel rebuild validation not performed: Low-severity privileged DoS, not a reproduced memory-corruption finding (mandatory fix-build class)

['fix.diff']
↓ fix.diffper-fix-DF-3152

Confirmed kernel references

Detail

Exploit chain

n/a - privileged kernel panic/DoS + silent filesystem corruption primitive; no unprivileged route (ioctl double-gated on SYSCAP_NOVFS_IOCTL at hammer_ioctl.c:72,197)

Evidence (decisive lines)

["run.log:45-55 'Initialize freemap volume 1' -> 'panic: cluster_callback: unexpected EOF' with cluster_callback/vnstrategy stack, guest at db>", 'run.2.log identical signature from a clean vm.sh reset (deterministic)', 'VERDICT.md line-accurate trace incl. the production-kernel error-overwrite analysis (hammer_volume.c:117-130, hammer_volume.c:673-739)', 'build.log full compiler output']

PoC changes

Seed sketch did not exist; authored from scratch. Notes: struct hammer_ioc_volume layout taken from /usr/src/vfs/hammer/hammer_ioctl.h in-guest (device_name is char[MAXPATHLEN], vol_size int64); include path is with -I/usr/src; ioctl issued on an O_RDONLY fd of the HAMMER1 mountpoint root.

Verified recommended fix

Validate the formatted vol_buf_end against the device's real size via DIOCGPART in hammer_install_volume (mirror HAMMER2 hammer2_ondisk.c:283-291), and replace the add path's KKASSERT/error-overwrite with real unwinding that uninstalls the volume and returns the error

Verdict

Single privileged HAMMERIOC_ADD_VOLUME ioctl with vol_size claiming 2 GiB on a 2 MiB vn device panics the stock INVARIANTS kernel deterministically (twice, identical signature): hammer_format_volume_header derives vol_buf_end from the unchecked user value, hammer_install_volume never compares it against the device's real size, and hammer_format_freemap's layer2 breads walk past the device end -> 'panic: cluster_callback: unexpected EOF'. On non-INVARIANTS kernels the same failure is worse silently: KKASSERTs compile out and hammer_update_volumes_header's unconditional return 0 overwrites the error, so the ioctl returns SUCCESS with a half-formatted volume installed and vol_count bumped on every member. Privileged (SYSCAP_NOVFS_IOCTL) trigger; also reachable as operator bad luck via transient device I/O error mid-add. Distinct from DF-0869 (integer overflow of the beg/end arithmetic); this is the missing device-size cross-check plus broken error-path discipline.