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

Unvalidated vol_no from crafted volume header causes kernel heap OOB write in volume_map bitmap

Summary

hammer_ondisk.c:210 volume->vol_no=ondisk->vol_no trusts raw int32 from disk NO range check. :234 hammer_volume_number_add(hmp,volume). Inline hammer.h:1582 i=__hammer_vol_index(vol_no)=vol_no>>6 NO masking. :1583 hmp->volume_map[i] |= __hammer_vol_low(vol_no). volume_map is uint64_t[4] supports vol_no 0-255 (HAMMER_MAX_VOLUMES=256). vol_no=256: i=4 writes 8 bytes past volume_map (last field of hammer_mount). vol_no=0x7FFFFFFF: i=0x1FFFFFF write ~256MB past panic. vol_no=256+64*K: single-bit OR write at controllable heap offset K*8 past volume_map. RB_INSERT at :227 uses full int32 comparison does not constrain range. rootvol check :241 runs AFTER OOB write. HAMMER_VOL_ENCODE masks to 8 bits for offset encoding but __hammer_vol_index does NOT = asymmetry root cause. CRC of volume header never validated at mount (hammer_install_volume:199-209). Trigger: crafted HAMMER image vol_no=0x7FFFFFFF mount = immediate panic. Fix: if(vol_no<0||vol_no>=HAMMER_MAX_VOLUMES) EFTYPE + mask in __hammer_vol_index &3.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0797 Β· 17 files
FileTypeDescriptionSize
craft_img.c trigger-source HAMMER volume-header vol_no patcher (offset 0x90; CRC left stale -- never validated at mount) 5.5 KB view raw
harness.c trigger-source Deterministic OOB-write primitive proof (transcribes __hammer_vol_index / volume_map[] |=) 7.5 KB view raw
build.sh build-script cc -O2 -o craft_img craft_img.c ; cc -O2 -o harness harness.c 295 B view raw
run.sh run-script harness + image forge (vol_no=0x7FFFFFFF) + mount -> panic on #0 2.7 KB view raw
VERDICT.md verdict Full narrative: mechanism, path:line, panic decode, fix validation 8.5 KB ↓ raw
README.md readme Build/run/expected + preconditions 2.6 KB ↓ raw
build.log build-log Final successful build of craft_img + harness 238 B view raw
run.log run-log harness output: deterministic OOB extent for vol_no 0/256/0x200/0x7FFFFFFF/-1 2.3 KB view raw
panic.txt panic-signature Fatal trap 12 at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) -- the OOB OR-write instruction 1.8 KB view raw
baseline_mount.log run-log Baseline #0 mount attempt that triggered the panic 426 B view raw
craft_log.txt run-log First image forge + newfs_hammer pipeline output 1002 B view raw
fix_run.log run-log Patched #1 mount: 'mount: Invalid argument', no panic, dmesg rejection line 1.6 KB view raw
fix_build.log build-log Full make -j6 nativekernel output of the single-fix kernel (rc=0) 5.6 MB ↓ download
fix.diff suggested-fix git-apply-able: range check vol_no in hammer_ondisk.c + mask __hammer_vol_index in hammer.h 1.6 KB view raw
env.txt environment uname, kern.version, cc version, INVARIANTS in kernel config 596 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme Build/run/expected + preconditions
↓ download raw

DF-0797 β€” PoC

Unvalidated vol_no from crafted HAMMER volume header β†’ kernel heap OOB write in volume_map bitmap.

sys/vfs/hammer/hammer_ondisk.c:210 volume->vol_no = ondisk->vol_no trusts a raw int32 from disk. hammer.h:1582 __hammer_vol_index(vol_no) = vol_no >> 6 has NO mask; volume_map[] is uint64_t[4] (valid indices 0..3 = vol_no 0..255). A crafted vol_no outside [0,256) writes past volume_map (the last field of struct hammer_mount):

  • vol_no = 0x7FFFFFFF β†’ i = 0x1FFFFFF β†’ ~255 MiB OOB β†’ page fault panic at mount
  • vol_no = 256 β†’ i = 4 β†’ 8-byte OOB OR-write past volume_map[3]
  • vol_no = 256+64*K+N β†’ single-bit OR at byte (K*8 + N/8) past volume_map

The volume-header CRC is NEVER validated at mount (hammer_crc_test_volume has zero callers), so patching vol_no requires no CRC recompute.

Build

./build.sh     # cc -O2 -o craft_img craft_img.c ; cc -O2 -o harness harness.c

Run

./run.sh                          # full pipeline: harness + image forge + mount
# or, manually as root inside the DragonFly guest:
dd if=/dev/zero of=/root/df0797.img bs=1m count=1024
vnconfig -c vn0 /root/df0797.img
newfs_hammer -f -L TEST /dev/vn0
mkdir -p /mnt && mount -t hammer -o nohistory /dev/vn0 /mnt
echo seed > /mnt/s.txt && sync && umount /mnt && vnconfig -u vn0
./craft_img /root/df0797.img 2147483647   # forge vol_no = 0x7FFFFFFF
vnconfig -c vn0 /root/df0797.img
mount -t hammer -o nohistory /dev/vn0 /mnt   # PANIC on #0 ; EINVAL on patched #1

Expected

  • #0 unpatched (GENERIC, INVARIANTS ON): immediate kernel panic Fatal trap 12: page fault while in kernel mode Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)
  • #1 patched (fix.diff applied): mount: Invalid argument, dmesg: HAMMER: volume /dev/vn0 has invalid vol_no 2147483647, no panic.
  • ./harness alone: deterministic OOB-write proof (no kernel needed).

Preconditions

Root inside the DragonFly guest (mount is root-only; vfs.usermount=0). Acceptable "admin mounted a crafted filesystem image" threat model.

Files

  • craft_img.c β€” volume-header vol_no patcher (offset 0x90 in hammer_volume_ondisk)
  • harness.c β€” deterministic OOB-write primitive proof (transcribes the inline fns)
  • build.sh / run.sh β€” exact build/run pipeline
  • VERDICT.md β€” full narrative with path:line citations and fix validation
  • panic.txt β€” captured fatal trap 12 at hammer_install_volume+0x519
  • fix.diff β€” git-apply-able fix (range check + mask), validated on #1
  • manifest.json, env.txt, full logs
VERDICT.md verdict Full narrative: mechanism, path:line, panic decode, fix validation
↓ download raw

DF-0797 β€” Unvalidated vol_no from crafted HAMMER volume header β†’ kernel heap OOB write in volume_map bitmap

Verdict

REPRODUCED β€” immediate kernel panic on GENERIC #0 from a crafted HAMMER image; deterministic OOB-write primitive proven by harness; fix VALIDATED on a single-fix kernel (#1) β€” the bad vol_no is rejected with EFTYPE/EINVAL before the OOB write, no panic.

Mechanism (trigger β†’ primitive β†’ effect)

A HAMMER v1 volume header carries int32_t vol_no at byte offset 0x90 in struct hammer_volume_ondisk (sys/vfs/hammer/hammer_disk.h:754). At mount:

  1. hammer_install_volume() reads the header into a buffer and at sys/vfs/hammer/hammer_ondisk.c:210 does volume->vol_no = ondisk->vol_no; β€” a raw int32 trusted from disk with NO range check.
  2. At :234 it calls hammer_volume_number_add(hmp, volume).
  3. hammer_volume_number_add (sys/vfs/hammer/hammer.h:1579-1584) computes int i = __hammer_vol_index(vol->vol_no) and writes hmp->volume_map[i] |= __hammer_vol_low(vol->vol_no).
  4. __hammer_vol_index (sys/vfs/hammer/hammer.h:1567-1571) is just vol_no >> 6 with NO mask. hmp->volume_map[] is uint64_t[4] (sys/vfs/hammer/hammer.h:875, the LAST field of struct hammer_mount), so valid indices are 0..3 (covers vol_no 0..255 = HAMMER_MAX_VOLUMES).

For a crafted vol_no outside [0, 256):

vol_no __hammer_vol_index effect of volume_map[i] |= bit
0 0 in-bounds (legitimate root volume)
256 4 8-byte OOB OR-write past volume_map[3]
256+64*K+N 4+K OOB OR-write of bit N at byte (K*8 + N/8) past end
0x7FFFFFFF 0x1FFFFFF ~255 MiB past volume_map β†’ unmapped page β†’ panic
-1 -1 (0x3FFFFFF) ~512 MiB past β†’ unmapped page β†’ panic

The HAMMER_VOL_ENCODE/HAMMER_VOL_DECODE macros (hammer_disk.h:291-294) mask to 8 bits for offset encoding; __hammer_vol_index does NOT β€” this asymmetry is the root cause.

The rootvol sanity check at hammer_ondisk.c:241 runs AFTER the OOB write at :234, so it cannot prevent it.

CRC is NOT validated at mount

hammer_crc_test_volume() (sys/vfs/hammer/hammer_crc.h:180) is defined but has ZERO callers anywhere in sys/vfs/hammer/ (verified by grep). Therefore patching vol_no in the image requires NO CRC recompute β€” the volume header CRC is never checked at mount time. (A separate hardening gap.)

Reproduction

Trigger (acceptable precondition: admin mounts a crafted filesystem image):

  1. dd if=/dev/zero a 1 GB image; vnconfig -c vn0 img; newfs_hammer -f -L TEST /dev/vn0.
  2. Mount, seed a file, sync, unmount, vnconfig -u.
  3. ./craft_img img 2147483647 β€” patches vol_no at offset 0x90 to 0x7FFFFFFF.
  4. vnconfig -c vn0 img; mount -t hammer -o nohistory /dev/vn0 /mnt β†’ panic.

Captured panic (GENERIC #0, with-src baseline, from dfbsd-qemu/boot.log):

Fatal trap 12: page fault while in kernel mode
cpuid = 2; lapic id = 2
fault virtual address   = 0xfffff80128a50a28
fault code      = supervisor write data, page not present
instruction pointer = 0x8:0xffffffff80948789
current process     = 979
kernel: type 12 trap, code=2
Stopped at      hammer_install_volume+0x519:    orq     %rdi,0x10a30(%rbx,%rdx,8)
db>

The faulting instruction orq %rdi,0x10a30(%rbx,%rdx,8) is the EXACT compilation of hmp->volume_map[i] |= bit: - %rbx = hmp; 0x10a30 = offset of volume_map[] in struct hammer_mount - %rdx = i = 0x1FFFFFF; %rdx,8 = i*8 - %rdi = the bit (1<<63) to OR

So the kernel page-faulted executing volume_map[0x1FFFFFF] |= bit β€” definitive in-kernel proof of the DF-0797 OOB write at hammer.h:1583 called from hammer_ondisk.c:234.

Deterministic OOB proof (./harness)

The harness transcribes __hammer_vol_index/__hammer_vol_low/volume_map[] |= verbatim against a poisoned heap and shows the OOB extent for vol_no = 0, 256, 0x200, 0x7FFFFFFF, -1. It proves the controllable-offset primitive: vol_no = 256 + 64*K + N β†’ single-bit OR-write of bit N at qword (4+K), i.e. byte offset (K*8 + N/8) past volume_map end. Both qword offset and bit-within-qword are attacker-controlled.

Impact ceiling

  • GENERIC (INVARIANTS ON, the default): immediate, deterministic kernel panic / DoS at mount time via the far-OOB variant (vol_no=0x7FFFFFFF). The small-OOB variant (vol_no=256..319) silently ORs a bit into adjacent kernel heap past struct hammer_mount β€” content of the write is a single set bit at a controllable offset, value not fully attacker-shaped (only OR-set, not arbitrary). On GENERIC, no INVARIANTS guard fires on this specific write (it is a plain array index, not a slab-redzone violation), so the small-OOB case corrupts silently.
  • Production (INVARIANTS OFF): the controllable-offset single-bit OR is a constrained heap-write primitive, but escalation is bounded by: (a) the write is OR-only (can set a bit, not clear or write arbitrary bytes), (b) it requires a victim object with a security-interesting single-bit flip to land exactly at the controllable offset, (c) the target offset is relative to the end of struct hammer_mount, a large (>64 KiB) allocation, so the reachable victim window is whatever is allocated immediately after it β€” not easily groomable to a specific victim. No escalation chain was developed: the primitive (single-bit OR at a controllable but hammer_mount-relative offset, triggered only at mount of a crafted image by root) is too narrow to convert to uid=0 on this guest. The realistic ceiling is therefore panic / DoS on GENERIC (reliable, mount-time) and a latent constrained heap-write primitive on production.

Threat model / preconditions: the bug requires mounting a crafted HAMMER filesystem image. vfs.usermount=0 on this guest, so the mount itself is a root-only action (acceptable: "admin mounted a crafted filesystem image"); the panic is the demonstration of impact. There is no unprivileged-only path to the OOB β€” the corruption happens DURING mount, before any unprivileged file operation.

PoC changes (what was built from scratch)

  • craft_img.c β€” volume-header vol_no patcher. Locates the HAMMER_FSBUF_VOLUME signature at block 0, patches the 4-byte vol_no field at offset 0x90, leaves vol_crc stale (verified: CRC is never validated at mount). No CRC32C machinery needed (unlike DF-0769/0776).
  • harness.c β€” deterministic transcription of __hammer_vol_index / volume_map[] |= showing the OOB extent for several vol_no values against a poisoned heap model.
  • build.sh / run.sh β€” exact build & run pipeline (image create + forge + mount, plus harness).

Fix (fix.diff β€” git-apply-able, validated)

Two-part, minimal:

  1. hammer_ondisk.c (before :234 hammer_volume_number_add): reject out-of-range vol_no with EFTYPE: c if (volume->vol_no < 0 || volume->vol_no >= HAMMER_MAX_VOLUMES) { hkprintf("volume %s has invalid vol_no %d\n", volume->vol_name, volume->vol_no); error = EFTYPE; goto late_failure; }
  2. hammer.h __hammer_vol_index (defense in depth): mask the index to the array size: return ((vol_no >> 6) & 0x3);

This matches (and slightly broadens, with the inline mask as defense in depth) the finding markdown's ## Recommended fix proposal.

Fix validation (Phase 8)

  • Baseline #0 (with-src, unpatched): crafted image mount β†’ Fatal trap 12 ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) β†’ guest wedged in DDB. βœ“ bug present.
  • Single-fix kernel #1 (built with make -j6 nativekernel KERNCONF=X86_64_GENERIC, rc=0, sha256 3b1974ce583a3d22c5f67ee1e07999b1b7ddff8f6f3067b636c55d15d0af6d16): same crafted image mount β†’ mount: Invalid argument (rc=1), dmesg shows HAMMER: volume /dev/vn0 has invalid vol_no 2147483647, no panic, guest stays up, kern.version=#1. βœ“ fix closes the bug.
  • The same vol_no < 0 || vol_no >= HAMMER_MAX_VOLUMES guard also rejects the small-OOB variant (vol_no=256, 256>=256) and the negative variant (vol_no=-1, <0) by code inspection.

Build: make -j6 nativekernel (HAMMER is in X86_64_GENERIC); ~6 min for a .c+.h change; only hammer_ondisk.c + hammer.h recompiled + link.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: on baseline #0 (with-src, unpatched) the crafted image (vol_no=0x7FFFFFFF) mount triggered 'Fatal trap 12 ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)' and wedged the guest in DDB. On the single-fix kernel #1 (fix.diff applied, built with make -j6 nativekernel KERNCONF=X86_64_GENERIC rc=0) the SAME crafted image mount returned 'mount: Invalid argument' (rc=1), dmesg showed 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647', no panic, guest stayed up. Re-ran twice, deterministic. The same vol_no<0||vol_no>=HAMMER_MAX_VOLUMES guard also rejects the small-OOB (256>=256) and negative (-1<0) variants by code inspection. fix.diff passes 'git apply --check -p1' on the clean sys/ tree.

BASELINE #0: 'Fatal trap 12: page fault while in kernel mode / Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) / db>' (guest down). PATCHED #1: 'mount: Invalid argument (rc=1)' + dmesg 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647' (guest up, kern.version=#1). Build: fix_build.log shows 'Kernel build for X86_64_GENERIC completed' with 'NK_DONE rc=0', no errors.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 5 14:18:31 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 3b1974ce583a3d22c5f67ee1e07999b1b7ddff8f6f3067b636c55d15d0af6d16)

Confirmed kernel references

Detail

Exploit chain

none (not a write-capable escalation): the controllable-offset variant (vol_no=256+64K+N -> single-bit OR at byte K8+N/8 past volume_map) is characterized by harness.c and is a constrained heap-write primitive on production (INVARIANTS-OFF), BUT: (1) it is OR-only (can set one bit, not write arbitrary bytes), (2) the trigger requires root to mount a crafted HAMMER image (vfs.usermount=0) -- the corruption happens DURING mount, before any unprivileged file op, so there is no unpriv->root boundary being crossed (root->kernel is game-over by definition), (3) the offset is relative to the end of struct hammer_mount (a >64KiB allocation), making the victim window hard to groom to a specific target. Valid hard blocker: 'the write is reachable only from an already-root context (mount of a crafted image, root-only)'. On GENERIC (the default, INVARIANTS ON) the realistic ceiling is deterministic mount-time DoS/panic. Harness transcribes __hammer_vol_index/__hammer_vol_low/volume_map[] |= verbatim against a poisoned heap and shows OOB extents for vol_no 0/256/0x200/0x7FFFFFFF/-1; chain file: harness.c. No uid0 escalation attempted because the precondition is root-only mount (no privilege boundary to cross) -- reported honestly as panic/DoS.

Evidence (decisive lines)

panic.txt: 'Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff80128a50a28 / fault code = supervisor write data, page not present / Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8) / db>'. harness run.log shows '__hammer_vol_index(2147483647) = 33554431 ... FATAL: +255 MiB past volume_map -> unmapped page fault'. dmesg on #1 patched: 'HAMMER: volume /dev/vn0 has invalid vol_no 2147483647', mount rc=1 'Invalid argument', no panic. Full untrimmed logs in findings/poc/DF-0797/{run.log,panic.txt,fix_run.log,fix_build.log}.

PoC changes

Built findings/poc/DF-0797/ from scratch: craft_img.c (HAMMER volume-header vol_no patcher at offset 0x90 in struct hammer_volume_ondisk; verifies HAMMER_FSBUF_VOLUME signature; leaves vol_crc stale because hammer_crc_test_volume is never called at mount -- verified), harness.c (deterministic transcription of __hammer_vol_index/__hammer_vol_low/volume_map[] |= against a poisoned heap model showing the OOB extent for vol_no 0/256/0x200/0x7FFFFFFF/-1), build.sh/run.sh (exact pipeline), fix.diff (range check + mask), VERDICT.md/README.md/manifest.json/env.txt, full logs.

Verified recommended fix

Two-part minimal fix in findings/poc/DF-0797/fix.diff (git-apply-able, validated): (1) in hammer_ondisk.c before line 234 add 'if (volume->vol_no < 0 || volume->vol_no >= HAMMER_MAX_VOLUMES) { hkprintf(...invalid vol_no...); error = EFTYPE; goto late_failure; }' to reject out-of-range vol_no before hammer_volume_number_add indexes volume_map; (2) in hammer.h __hammer_vol_index mask the index '(vol_no >> 6) & 0x3' as defense in depth. Matches the finding markdown's proposal (broadened with the inline mask).

Verdict

REPRODUCED on GENERIC #0 (with-src baseline) as an immediate mount-time kernel panic; root cause confirmed by source trace and panic-instruction decode. hammer_ondisk.c:210 assigns volume->vol_no = ondisk->vol_no (raw int32 from disk, NO range check); :234 calls hammer_volume_number_add(hmp, volume); hammer.h:1582-1583 computes i=__hammer_vol_index(vol_no)=vol_no>>6 (NO mask, hammer.h:1567-1571) then writes hmp->volume_map[i] |= __hammer_vol_low(vol_no). volume_map is uint64_t[4] (hammer.h:875, the LAST field of struct hammer_mount), so valid i is 0..3 (vol_no 0..255). A crafted image with vol_no=0x7FFFFFFF yields i=0x1FFFFFF, ~255 MiB OOB. Captured panic from dfbsd-qemu/boot.log: 'Fatal trap 12: page fault while in kernel mode ... supervisor write data, page not present ... Stopped at hammer_install_volume+0x519: orq %rdi,0x10a30(%rbx,%rdx,8)' -- this instruction is the EXACT compilation of volume_map[i] |= bit (%rbx=hmp, 0x10a30=offset of volume_map, %rdx=i*8, %rdi=bit). Definitive in-kernel proof of the OOB write. Also verified hammer_crc_test_volume (hammer_crc.h:180) has ZERO callers -- the volume-header CRC is never validated at mount, so patching vol_no needs no CRC recompute.