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

Unbounded HAMMER_UNDO_INDEX indexes vol0_undo_array[128] out of bounds β€” attacker-aimed kernel heap OOB read (up to ~4GB reach) and deterministic panic at mount of a crafted HAMMER1 image

Field Value
ID DF-3040
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-125 / CWE-20
File sys/vfs/hammer/hammer_ondisk.c
Lines 688-689 (macro: hammer_disk.h:824-829)
Area vfs/hammer
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

hammer_get_buffer dispatches zone-3 undo offsets to hammer_undo_lookup, whose only bounds are derived from the SAME attacker-crafted vol0_blockmap[3] in the root volume header. hammer_xlate_to_undo then loads vol0_undo_array[short(zone3)/8MB] where the array holds exactly HAMMER_MAX_UNDO_BIGBLOCKS=128 entries and no code in sys/ compares the index against it. A crafted image with vol0_blockmap[3].first_offset = UNDO|0x4000000000 makes the mount-time undo backscan index entry 32768 β€” reading ~240KB past the 16KB rootvol bp. Reproduced twice on the stock INVARIANTS guest: Fatal trap 12 at hammer_undo_lookup+0x84 with fault VAs tracking the crafted index (idx 32768 β†’ bp+263048); guest wedged at db> both runs. Index spans 2^29 (~4GB of attacker-chosen read reach); idx 128..1792 reads the attacker's own image bytes past the header struct, yielding a fully attacker-known zone-2 offset that bypasses the 128-entry map. No userspace disclosure of the OOB bytes demonstrated (the value only steers a zone-2 offset); on INVARIANTS kernels a deterministic mount-path panic. Mount of a crafted image requires root (or vfs.usermount=1) β€” same threat model as DF-0797/0798. Fix validated in-guest (bound the undo zone in hammer_undo_lookup β†’ EIO): same PoC β†’ 'Unable to read UNDO TAIL … Failed to recover HAMMER filesystem on mount', guest stays up.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of hammer_ondisk.c (GLM 5.3); mount-time panic reproduced 2/2 + fix validated. DF-0797/0798 re-verified, not re-reported; DF-3011's freemap off-by-8 note REFUTED on the guest (artifact of image grafting).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3040 Β· 15 files
FileTypeDescriptionSize
undo_craft.c β€” 4.1 KB view raw
build.sh β€” 431 B view raw
run.sh β€” 628 B view raw
freemap_probe.sh β€” 583 B view raw
run.log β€” 3.5 KB view raw
panic_idx2048.txt β€” 14.1 KB view raw
panic_idx32768.txt β€” 13.8 KB view raw
fix.diff β€” 965 B view raw
fix_build.log β€” 374 B view raw
fix_run.log β€” 393 B view raw
README.md β€” 4.4 KB ↓ raw
VERDICT.md β€” 4.9 KB ↓ raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 6.1 KB view raw
env.txt β€” 279 B view raw

DF-3040 β€” Unbounded HAMMER_UNDO_INDEX indexes vol0_undo_array[128] out of bounds: kernel heap OOB read at attacker-chosen offset on mount of a crafted HAMMER1 image

Kernel: DragonFly 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS), sys/vfs/hammer File under audit: sys/vfs/hammer/hammer_ondisk.c (pass 2); root cause sink in hammer_undo_lookup() / hammer_xlate_to_undo (hammer_disk.h).

Reproduce

# guest (root), after pushing undo_craft.c/build.sh/run.sh:
sh build.sh                       # cc undo_craft + newfs_hammer 12G base.img
sh run.sh 0x400000000             # idx 2048  -> read at bp+17288
sh run.sh 0x4000000000            # idx 32768 -> read at bp+263048
# expected (stock INVARIANTS kernel):
#   Fatal trap 12: page fault, supervisor read data, page not present
#   Stopped at hammer_undo_lookup+0x84: addq 0x388(%rcx,%rdx,8),%rbx
#   (guest wedged at db>; the mount never completes)

The faulting instruction is the vol0_undo_array[idx] load itself: 0x388 = 904 = offsetof(struct hammer_volume_ondisk, vol0_undo_array), %rdx = attacker-chosen index (short(zone3)/HAMMER_BIGBLOCK_SIZE), and the fault VA is 904 + idx*8 bytes into the 16KB rootvol bp mapping β€” i.e. past its end.

Root cause

  • hammer_get_buffer() (hammer_ondisk.c:688-689) dispatches zone-3 offsets to hammer_undo_lookup() (hammer_undo.c:58-76).
  • The only bounds applied anywhere on the undo path are:
  • hammer_recover_stage1 (hammer_recover.c:232): first_offset > alloc_offset || next_offset > alloc_offset β†’ EIO β€” but alloc_offset itself comes from the attacker-crafted vol0_blockmap[3] in the root volume header;
  • KKASSERT(zone3_off < undomap->alloc_offset) (hammer_undo.c:70) β€” same attacker-controlled reference, and INVARIANTS-only.
  • hammer_xlate_to_undo() (hammer_disk.h:827-829) then computes vol0_undo_array[HAMMER_UNDO_INDEX(zone3)] where HAMMER_UNDO_INDEX = short(zone3) / HAMMER_BIGBLOCK_SIZE (8MB). The array has exactly HAMMER_MAX_UNDO_BIGBLOCKS = 128 entries (hammer_disk.h:512,789); the kernel never checks the index against it (the constant is not referenced anywhere in sys/). short(zone3) spans 2^52, so idx spans 2^29 β†’ the load can be aimed up to ~4GB past the 16KB header buffer.
  • Trigger path: mount_hammer β†’ hammer_mount (vfsops.c:671 copies the crafted blockmap) β†’ hammer_recover_stage1 version-4 backscan (recover.c:257-263) β†’ hammer_recover_scan_rev β†’ hammer_bread(zone3) (recover.c:798) β†’ hammer_get_buffer β†’ hammer_undo_lookup β†’ OOB load.

Impact: crafted-image mount-time kernel OOB read (CWE-125) at an attacker-chosen offset up to ~4GB from the header bp. On INVARIANTS guests this manifests as the observed page-fault panic (deterministic local DoS from a root-mounted image; vfs.usermount=1 makes it unprivileged). On production (non-INVARIANTS) kernels the OOB value is used as a zone-2 offset; the read itself stays in-kernel (no demonstrated disclosure of the OOB bytes to userspace β€” the value only steers which device block is subsequently read). Mount of a crafted image requires root (or usermount), consistent with the DF-0797/DF-0798 threat model.

Fix validation

  • fix.diff (authored against the read-only audit tree; applied only to the guest's /usr/src copy): reject zone3_off >= HAMMER_ENCODE_UNDO(128 * 8MB) in hammer_undo_lookup() with EIO before hammer_xlate_to_undo().
  • make nativekernel KERNCONF=X86_64_GENERIC β†’ OK (fix_build.log); rebooted into kernel #1 (Sat Sep 5 16:07:13 UTC 2026).
  • Re-ran the exact PoC (run.sh 0x4000000000) on the fixed kernel: mount: Input/output error; console shows Unable to read UNDO TAIL at 3000003ffffffff8 / recovery failure during seqno backscan / Failed to recover HAMMER filesystem on mount; no panic, guest stays up (fix_run.log).
  • Regression on the fixed kernel: stock newfs_hammer image mount + create + rm + sync + umount cycle is clean (freemap_probe.sh, also refutes the DF-3011 "off-by-8" environmental artifact β€” see run.log).

Artifacts

  • undo_craft.c β€” image forger (patches vol0_blockmap[3] only; no CRC fixup needed because the kernel never verifies vol_crc β€” see DF-3042)
  • build.sh / run.sh / freemap_probe.sh
  • run.log β€” both baseline runs + console transcripts (full console in panic_idx2048.txt, panic_idx32768.txt)
  • fix.diff, fix_build.log, fix_run.log
  • manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-3040 VERDICT β€” unbounded HAMMER_UNDO_INDEX β†’ vol0_undo_array OOB kernel heap read

Status: REPRODUCED (baseline, twice, fresh boots) Β· FIXED (fix.diff validated on a rebuilt kernel).

What was claimed vs what happened

The pass-2 audit of sys/vfs/hammer/hammer_ondisk.c found that the zone-3 (undo) dispatch at hammer_ondisk.c:688-689 reaches hammer_undo_lookup() (hammer_undo.c:58-76), whose only offset bound β€” KKASSERT(zone3_off < undomap->alloc_offset) β€” references the same attacker-crafted vol0_blockmap[3] that supplies the offset. The final translation hammer_xlate_to_undo (hammer_disk.h:827-829) indexes vol0_undo_array[HAMMER_UNDO_INDEX(zone3)] with HAMMER_UNDO_INDEX = short(zone3)/8MB; the array holds 128 entries (HAMMER_MAX_UNDO_BIGBLOCKS, hammer_disk.h:512) and no code in sys/ compares the index against 128. The mount path reaches this with a fully crafted offset: hammer_recover_stage1 (hammer_recover.c:232) only checks first/next against the attacker's own alloc_offset, then the version-4 backscan (recover.c:257-263 β†’ :798) issues hammer_bread() on the crafted zone-3 offset.

Two baseline runs on the stock INVARIANTS kernel (#0, Jul 2 2026), each after a fresh vm.sh reset with-src, mounting a newfs_hammer image whose vol0_blockmap[3] was patched by undo_craft:

run short(zone3) idx load address in bp result
1 0x400000000 (2^34) 2048 904+16384 = bp+17288 (904B past end) Fatal trap 12, hammer_undo_lookup+0x84, fault VA 0xfffff80060cea380
2 0x4000000000 (2^38) 32768 904+262144 = bp+263048 (~240KB past end) Fatal trap 12, same instruction, fault VA 0xfffff800641f6380

The faulting instruction addq 0x388(%rcx,%rdx,8),%rbx is the vol0_undo_array[idx] load (0x388=904=offsetof(vol0_undo_array)); the fault VAs are past the 16KB rootvol bp mapping, at distances that track the attacker-chosen index exactly. The index is image-controlled up to 2^29 (~4GB reach). Both runs wedged the guest at db> (kernel panic β€” guest down), i.e. a deterministic DoS on mount of the crafted image.

Why not higher impact

The OOB value is consumed as a zone-2 offset (result_offset), not copied to userspace. On INVARIANTS kernels the subsequent KKASSERT(hammer_is_zone_raw_buffer(...)) (hammer_ondisk.c:703) or β€” as observed β€” the unmapped OOB load itself panics first. On production (non-INVARIANTS) kernels the garbage zone-2 offset leads to hammer_get_volume() ENOENT or an EIO device read in the common case; there is no demonstrated exfiltration channel for the OOB-read bytes. The primitive is an attacker-aimed kernel read (CWE-125) whose practical impact on this guest is the panic. Note the in-bounds half of the same bug: for idx in [128,1792] the load stays inside the 16KB bp but past the on-disk struct β€” the attacker then knows the value (it is their own image bytes at LBA1+), giving a fully controlled zone-2 offset bypassing the 128-entry map.

Severity Medium: mount-time (root/usermount preconditions, same as DF-0797 / DF-0798), memory-safety proven, disclosure not demonstrated, deterministic panic on default INVARIANTS builds.

Fix validation (mandatory: memory-corruption class reproduced)

fix.diff bounds zone3_off to the on-disk format's supported undo range in hammer_undo_lookup() (EIO before hammer_xlate_to_undo); authored against the read-only audit tree (git apply --check clean), applied only to the guest's /usr/src copy, make nativekernel KERNCONF=X86_64_GENERIC β†’ OK, rebooted into kernel #1 (Sat Sep 5 16:07:13 UTC 2026):

  • Baseline reproduced: 2/2 panics on stock kernel #0 (run.log, panic_idx*.txt).
  • Patched kernel: same PoC (run.sh 0x4000000000) β†’ mount: Input/output error, console Unable to read UNDO TAIL … recovery failure during seqno backscan / Failed to recover HAMMER filesystem on mount, guest up (fix_run.log).
  • Regression: stock newfs_hammer mount + create/rm/sync/umount clean on the fixed kernel (freemap probe in run.log).

Environmental check performed (priority 7 of this audit)

DF-3011's note about KKASSERT(layer2->zone==zone) (hammer_blockmap.c:832) and a suspected "off-by-8 between the on-disk layer1 table and the blockmap root" is not reproducible on stock images: a plain newfs_hammer image goes through mount + btree-node alloc/free (file create, delete, sync) without tripping any freemap KKASSERT, on both the stock and fixed kernels. The artifact seen in DF-3011 came from the deepforge grafting (nodes allocated at vol0_blockmap[8].next_offset without matching freemap layer2 zone bits), not from a kernel decode bug. No finding filed.

DF-0797 (vol_no β†’ volume_map OOB write) and DF-0798 (node in-buffer offset OOB read during CRC) remain unfixed in-tree, as does DF-0776's proposed count/type validation (hammer_load_node/hammer_btree.c carry only KKASSERTs).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src copy only (audit tree untouched; git apply --check clean against sys/), make nativekernel KERNCONF=X86_64_GENERIC + installkernel, reboot into kernel #1. Identical PoC (run.sh 0x4000000000) now fails cleanly: 'mount: Input/output error' via EIO from hammer_undo_lookup -> 'recovery failure during seqno backscan' -> 'Failed to recover HAMMER filesystem on mount'; no panic, guest stays up. Regression on fixed kernel: stock newfs_hammer image mount + file create/rm/sync/umount cycle clean.

['fix_run.log: mount: Input/output error (SSH_RC=1, guest up)', 'run.log console section (fixed kernel): HAMMER(uabase) Unable to read UNDO TAIL at 3000003ffffffff8 / recovery failure during seqno backscan / Failed to recover HAMMER filesystem on mount', 'fix_build.log: kernel build + install completed; uname #1 Sat Sep 5 16:07:13 UTC 2026']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 5 16:07:13 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["panic_idx32768.txt: 'Stopped at hammer_undo_lookup+0x84: addq 0x388(%rcx,%rdx,8),%rbx' with fault VA 0xfffff800641f6380 (page not present) β€” the vol0_undo_array[idx] load faulted 240KB past the 16KB bp", 'panic_idx2048.txt: same instruction, fault VA 0xfffff80060cea380 for idx 2048 (bp+17288) β€” distance tracks the crafted index exactly', "run.log: craft output 'undo AFTER first=3000004000000000 … idx(first)=32768 -> read at bp+263048' followed by the trap; both baseline runs wedged the guest (ssh timeout 124, vm.sh status down)", "fix_run.log (kernel #1 + fix.diff): same PoC -> 'mount: Input/output error', console 'Unable to read UNDO TAIL at 3000003ffffffff8 / recovery failure during seqno backscan / Failed to recover HAMMER filesystem on mount', guest stays up", 'fix_build.log: make nativekernel KERNCONF=X86_64_GENERIC completed; uname DragonFly 6.5-DEVELOPMENT #1 Sat Sep 5 16:07:13 UTC 2026', "run.log tail: freemap_probe on stock newfs image clean β€” DF-3011's blockmap.c:832 layer2 artifact is NOT a stock-kernel bug (deepforge grafting artifact)"]

PoC changes

Pass-2 author wrote the PoC from scratch (no seed): undo_craft.c patches only vol0_blockmap[3] of a real newfs_hammer image (no CRC fixup needed β€” vol_crc is never verified, see DF-3042). One iteration: the first constant 0x400000000 is 2^34 (idx 2048), kept as default because it already faults 904B past the bp; the parametrized run at 0x4000000000 (idx 32768) demonstrates the far reach. Fixed an argv-check bug found during run 2.

Verified recommended fix

In hammer_undo_lookup(), reject zone3_off >= HAMMER_ENCODE_UNDO(HAMMER_MAX_UNDO_BIGBLOCKS * HAMMER_BIGBLOCK_SIZE64) with EIO before calling hammer_xlate_to_undo() (see fix.diff)

Verdict

REPRODUCED twice on the stock INVARIANTS kernel (fresh vm.sh reset with-src between runs): mounting a newfs_hammer image whose vol0_blockmap[3] undo map was patched to zone3 short offset 0x400000000 (idx 2048, bp+17288) resp. 0x4000000000 (idx 32768, bp+263048) panics in hammer_undo_lookup+0x84 β€” the faulting instruction 'addq 0x388(%rcx,%rdx,8),%rbx' is exactly the unbounded vol0_undo_array[HAMMER_UNDO_INDEX(zone3)] load (0x388=904=offsetof), reading past the 16KB rootvol bp at attacker-chosen distance (up to ~4GB reach, idx<2^29). The only undo-path bounds (hammer_recover.c:232 first/next vs alloc_offset; hammer_undo.c:70 KKASSERT vs the same alloc_offset) are all derived from the attacker-crafted vol0_blockmap[3], so none constrains the index against HAMMER_MAX_UNDO_BIGBLOCKS=128 (constant unreferenced in sys/). Impact ceiling: deterministic mount-time kernel panic on INVARIANTS (guest wedged at db> both runs); on non-INVARIANTS kernels an attacker-aimed OOB kernel read whose value steers a zone-2 offset (no userspace disclosure of the OOB bytes demonstrated; the in-bounds idx 128..1792 variant additionally yields a fully attacker-known zone-2 offset from the attacker's own image bytes past the header struct). Fixed and validated: fix.diff bounds zone3_off to the format's 128-bigblock undo range in hammer_undo_lookup (EIO); on the rebuilt kernel #1 the identical PoC fails the mount cleanly with I/O error and the guest stays up; stock-image mount/create/rm/sync/umount regression passes.