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

hammer_reblock_data() bcopy OOB kernel write/read: unvalidated on-disk leaf data_len/data_offset plus >16KB allocations that are not buffer-contained

Summary

hammer_reblock_data() copies a record's payload with bcopy(cursor->data, ndata, elm->leaf.data_len) using lengths and offsets taken verbatim from the B-Tree leaf on disk (:487). For data_len > HAMMER_BUFSIZE the blockmap allocator only enforces the 8MB big-block boundary, not the 16KB buffer boundary (hammer_blockmap.c:177-188), and the META-zone append cursor is naturally misaligned (16-byte granularity), so the fresh allocation lands at intra-buffer offset xoff inside a getblk'd DOALIGN16K(data_len)-sized kernel buffer; the copy of data_len bytes then writes [xoff, xoff+data_len) and overruns the buffer end by up to 16368 bytes of fully attacker-controlled data. VERIFIED stock INVARIANTS guest with one forged direntry leaf (data_len=65536, crc32c-forged data_crc, source = bigfile's 'A' extent): Fatal trap 12 supervisor-write page fault at memmove+0x10a (the bcopy), 4912 bytes past the 64KB destination buffer, first attempt; guest dead at db>. The misaligned-source variant is an OOB read of adjacent kernel buffer-cache memory persisted (CRC'd) into the record's new data block. Same guard kills the data_len>64KB getblk-panic variant (DF-3033 class), the buffer-crossing small-record OOB read, the data_len==0 NULL-deref path, and the >16KB source-getblk bgetvp overlap panic observed during fix iteration. Threat model: attacker-supplied HAMMER1 image; root mounts rw + runs the standard 'hammer reblock' maintenance (free_level=0 default qualifies every record). Impact: controlled-content, controlled-length (up to 16368B) kernel heap overflow past a buffer-cache buffer -> crash (observed) or silent corruption of adjacent kernel objects (no KASLR/SMAP on the guest class) - potential kernel code execution from an untrusted image; also a kernel-memory disclosure variant. Not unpriv->root (mount+ioctl privileged). Fix validated in-guest (containment guard + buffer-aligned large allocations, kernel #1): 'illegal data range' EINVAL, zero traps, clean unmount; stock regressions pass.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3167 Β· 17 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 7.6 KB ↓ raw
mkimage.sh β€” 915 B view raw
patch_leaf.c β€” 5.1 KB view raw
trigger.sh β€” 353 B view raw
build.sh β€” 141 B view raw
run.sh β€” 178 B view raw
build.log β€” 540 B view raw
run.log β€” 2.8 KB view raw
panic.txt β€” 764 B view raw
fixrun.log β€” 837 B view raw
fixrun1-overlap.log β€” 565 B view raw
env.txt β€” 572 B view raw
code_hashes.txt β€” 233 B view raw
fix.diff β€” 2.5 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 4.9 KB view raw

DF-3167 β€” hammer_reblock_data() OOB kernel write via unvalidated on-disk

leaf->data_len + misaligned >16KB allocation

What this proves

A single forged B-Tree leaf element (direntry record) in a HAMMER1 image causes hammer reblock (root-run maintenance command, HAMMERIOC_REBLOCK ioctl) to write attacker-controlled data past the end of a 64KB kernel buffer:

hammer_reblock_data()                     sys/vfs/hammer/hammer_reblock.c:487
    bcopy(cursor->data, ndata, elm->leaf.data_len);   /* 65536 bytes */
  • elm->leaf.data_len comes straight from disk (only a KKASSERT guards it, hammer_btree.c:736).
  • hammer_alloc_data() (hammer_ondisk.c:1634) allocates data_len bytes; for records with data_len > HAMMER_BUFSIZE the blockmap allocator only enforces the big-block boundary (hammer_blockmap.c:183-188), so the allocation may land at a non-16KB-aligned cursor offset xoff (the META zone cursor is naturally misaligned β€” dirents/inodes pack at 16-byte granularity).
  • The destination buffer is getblk(DOALIGN16K(data_len)) bytes and ndata = buffer->ondisk + xoff (hammer_ondisk.c:1114-1143, 715-722).
  • The copy writes [xoff, xoff + data_len) into a DOALIGN16K(data_len)- sized buffer β†’ xoff bytes (up to 16368) past the end, with fully attacker-controlled content (the old record data, CRC-checked against the forged data_crc).

Observed on the guest: xoff = 0x1390 (4912 bytes) past the buffer end β†’ Fatal trap 12: page fault while in kernel mode, supervisor write data, page not present in memmove+0x10a (the bcopy). Guest dead at db>.

The mirror-symmetric variant (misaligned source data_offset) is an OOB read of adjacent kernel buffer-cache memory, CRC'd and persisted into the record's new data block (kernel memory disclosure to the image).

Preconditions / threat model

Same family as DF-3033 / DF-3076 / DF-3148: the mount+reblock operator is root (caps_priv_check(cred, SYSCAP_NOVFS_IOCTL) at hammer_ioctl.c:72); the attacker is the provider of the filesystem image (untrusted disk / backup / VM image the admin mounts and maintains). No unprivileged trigger.

Reproduce

Everything runs inside the QEMU guest as root (see VERDICT.md for the environment):

./build.sh          # compile patch_leaf (stage 2 forger)
./run.sh            # stage 1 (mkimage.sh: build legit fs image)
                    # stage 2 (patch_leaf: forge the dirent leaf)
                    # stage 3 (trigger.sh: mount + hammer reblock)

Expected (vulnerable kernel): console/syslog shows

hammer_blockmap_getfree: a000000022000000 -> 7340032
hammer_reblock_helper: D 7340032/0
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8005272a000
fault code = supervisor write data, page not present
Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)

and the guest hangs at the db> prompt (see panic.txt, run.log).

Expected (kernel with fix.diff): no trap; hammer reblock completes normally; guest stays up.

Artifacts

  • mkimage.sh β€” stage 1: vnconfig + newfs_hammer + payload files
  • patch_leaf.c β€” stage 2: self-validating leaf forger (CRC32C/CRC32)
  • trigger.sh β€” stage 3: mount, enable vfs.hammer.debug_general=0x4000, run hammer reblock /mnt
  • build.sh,run.sh, build.log, run.log, panic.txt, env.txt
  • fix.diff β€” verified fix (hammer_reblock.c + hammer_blockmap.c)
VERDICT.md
↓ download raw

DF-3167 β€” VERDICT

Status: reproduced (stock kernel) β€” fix validated (fixed kernel).

What was audited

Pass-2 (GLM 5.3) audit of sys/vfs/hammer/hammer_reblock.c (683 LOC): ioctl entry hammer_ioc_reblock, scan loop, hammer_reblock_helper / hammer_reblock_data / hammer_reblock_leaf_node / hammer_reblock_int_node / hammer_move_node, cursor invalidation protocol, freemap interplay, ioctl arg validation.

The bug (root cause, line-accurate)

hammer_reblock_data() consumes the on-disk elm->leaf.data_len / elm->leaf.data_offset of every record it moves with no production validation:

  1. hammer_reblock.c:464 β€” hammer_btree_extract_data(cursor) loads cursor->data = buffer->ondisk + (data_offset & 0x3FFF); the only bound is the INVARIANTS-only KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) at hammer_btree.c:736. The data CRC gate (hammer_btree.c:764) is satisfiable by the image author because the pointed-to bytes are his own.
  2. hammer_reblock.c:467 β€” hammer_alloc_data(trans, data_len, rec_type,…) (hammer_ondisk.c:1634): for non-DATA records the zone is META without any 16KB rounding (:1646-1655), and hammer_blockmap_alloc() only enforces the big-block boundary for requests > 16KB (hammer_blockmap.c:183-188) β€” the allocation may land at any 16-byte aligned append cursor xoff inside a 16KB buffer (the META cursor is naturally misaligned; :372 even special-cases (next_offset & HAMMER_BUFMASK) != 0).
  3. hammer_reblock.c:487 β€” bcopy(cursor->data, ndata, elm->leaf.data_len) writes [xoff, xoff+data_len) into a buffer whose kernel data area is exactly DOALIGN16K(data_len) bytes (hammer_bread_ext β†’ _hammer_bread hammer_ondisk.c:1116-1143, buffer->io.bytes = bytes, ondisk = bp->b_data :715-722/922).

β†’ For any data_len > 16384 with xoff + data_len > DOALIGN16K(data_len) the copy runs up to 16368 bytes past the end of a kernel buffer, with fully attacker-controlled bytes (the source record data). The symmetric misaligned-source variant is an OOB read of adjacent kernel buffer memory, CRC'd and persisted into the record's new data block.

Reproduction (baseline, stock kernel #0 Jul 2 06:02:54)

Crafted in-guest, no kernel changes:

  1. mkimage.sh: 12G file-backed vn, newfs_hammer (v7), 1MB of 'A' in /d/bigfile, marker dirent /d/PWNMARKER_TARGET, 8 long-named dirents (they keep the META append cursor misaligned β€” it sits at 0x9000000021800f90, i.e. xoff=0xf90).
  2. patch_leaf.c: forges elm 8 of leaf node 0x8000000021000000 (the PWNMARKER dirent, located/verified via hammer -f /dev/vn0 show): data_offset=0xa000000022000000 (bigfile's all-'A' large-data extent, buffer-aligned), data_len=65536 (≀ HAMMER_XBUFSIZE β†’ passes every KKASSERT even on INVARIANTS kernels), data_crc=crc32c('A'Γ—65536) (v7 iscsi CRC), node CRC recomputed.
  3. trigger.sh: mount rw, vfs.hammer.debug_general=0x4000, hammer reblock /mnt (plain reblock: free_level=0 from the default 100% fill β†’ every record qualifies).

Observed (serial console, run.log / panic.txt):

hammer_reblock_data: 00000001 9000000021800450 -> 9000000021801330   <- meta cursor drift
hammer_blockmap_getfree: a000000022000000 -> 7340032                 <- forged record
hammer_reblock_helper: D 7340032/0                                   <- move starts
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8005272a000
fault code           = supervisor write data, page not present
instruction pointer  = 0x8:0xffffffff80bcaa0a
current process      = 1388 (hammer reblock)
Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)               <- the bcopy

Geometry check: the last meta move allocated at 0x…01330, so the forged record's 65536-byte allocation landed at xoff = 0x1390 (4912) into the 64KB destination buffer β†’ 4912 bytes of 'A' written past its end before the first unmapped page faulted the kernel. Guest dead at db>.

Primitive: controlled-content, controlled-length (≀16368B) OOB write past a getblk'd kernel buffer, from a privileged-mount image via a standard maintenance command. When the adjacent KVA is mapped (other buffers), it is silent kernel-memory corruption; here the first unmapped page made it a fatal trap. Not an unprivβ†’root path (mount+ioctl are root), impact ceiling = kernel memory corruption / host crash from a malicious filesystem image.

Fix validation

fix.diff (two hunks, generated against the read-only tree):

  • hammer_reblock.c β€” hammer_reblock_data() rejects any record whose (data_offset, data_len) is not fully contained in one 16KB buffer or has data_len <= 0 (legit records never cross a 16KB buffer; large-data is written one record per 16KB buffer) β†’ EINVAL, logged.
  • hammer_blockmap.c β€” hammer_blockmap_alloc() buffer-aligns allocations > 16KB (defense-in-depth for every other alloc_data caller).

Build: make -j6 nativekernel in-guest (BUILD_RC=0), installkernel, reboot into #1: Mon Sep 7 12:28:26 UTC 2026.

Re-run of the exact PoC on the fixed kernel:

hammer_blockmap_getfree: a000000022000000 -> 7340032
hammer_reblock_helper: D 7340032/0
HAMMER: reblock a000000022000000/65536: illegal data range      <- guard fires
Reblock /mnt failed: Invalid argument
TRIGGER-DONE   (no trap; guest up; unmount clean; 0 "Fatal trap" in messages)

An intermediate fix-v1 run (bounds-check only, no containment check) is also documented: it stopped the OOB write (the forged record moved to a buffer-aligned offset) but the source-side 64KB getblk then tripped a separate panic: bgetvp - overlapping buffer β€” the same unvalidated data_len class surfacing at a second sink, which motivated the final 16KB-containment guard. fix-v2 kills both at this call site.

Negative results (classes hunted, killed)

  • ioctl arg validation (hammer_reblock.c:70-103): localization PSEUDOFS bits, obj_id range, free_level bounds all checked; vol_no used only for comparison (no indexing); whole ioctl privileged (hammer_ioctl.c:72 caps_priv_check(SYSCAP_NOVFS_IOCTL)).
  • alloc/free balance: no error path between a successful hammer_alloc_data/hammer_alloc_btree and the corresponding free/delete (helper :467-513, :534-565, :594-627).
  • scan-loop progress: each iteration advances or exits; EDEADLK retry re-seeds from key_cur (:148-149, :241); EINTR escape (:162, :243-246); checkspace bounded at 10 β†’ ENOSPC (:176-179).
  • cursor protocol: the checkspace (:180-186) and bd_heatup (:223-227) unlock windows goto skip/iterate without using the stale elm; the uncache window (:344-367) reloads elm and revalidates (RETEST/type/index/bcmp) before use; hammer_init_cursor's error IS checked here (:108-112) unlike DF-3109's broken callers; DF-3110's moved-element defect lives in hammer_btree.c (not re-reported).
  • node moves: fixed-size bcopy (:641), children-lock and parent/root update ordering match the btree code; only an unreachable fixup hpanic (:680).
  • known findings DF-3033 (direct_uncache getblk entry :342-346) and DF-3076 (blockmap_free bytes) not re-reported; the >64KB data_len getblk-panic variant at this call site is covered by the same guard.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to in-guest /usr/src, make -j6 nativekernel + installkernel, reboot into #1 Sep 7 12:28:26. Exact PoC re-run: 'HAMMER: reblock a000000022000000/65536: illegal data range' on console, hammer reblock returns 'Invalid argument', NO trap, guest up, clean unmount, 0 'Fatal trap' in /var/log/messages. Baseline (stock #0 Jul 2) fatal trap reproduced in the same session.

fixrun.log (patched-run transcript + dmesg), fix.diff, VERDICT.md fix section
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Sep 7 12:28:26 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted HAMMER1 image (single forged B-Tree leaf elm: direntry with data_offset -> attacker-known 'A' extent, data_len=65536, forged data_crc + node crc) -> root mounts image rw -> 'hammer reblock' (default free_level=0) -> hammer_reblock_helper selects record -> hammer_btree_extract_data passes CRC gate -> hammer_alloc_data allocates 65536B in META zone at misaligned cursor xoff -> bcopy writes [xoff,xoff+65536) into 64KB buffer -> xoff bytes OOB write of controlled data -> observed: page fault (unmapped neighbor) = panic; with mapped neighbor: silent kernel heap corruption (victim: adjacent buffer-cache objects, no KASLR/SMAP on guest). uid0 escalation N/A (trigger already requires root).

Evidence (decisive lines)

['run.log (serial console: getfree of forged a000000022000000, D-line, Fatal trap 12 supervisor write at memmove+0x10a)', 'panic.txt (trap block)', 'VERDICT.md (geometry: xoff=0x1390=4912 bytes past buffer end)', "fixrun.log (fixed kernel: 'HAMMER: reblock a000000022000000/65536: illegal data range', EINVAL, 0 Fatal traps)", 'build.log (patcher self-validation: node CRC algo, elm match)']

PoC changes

PoC authored fresh for this finding (no seed). Required guest work: vnconfig WITHOUT -T (which truncates the backing file); image >=10GB sparse on /build; locate leaf elm via 'hammer -f /dev/vn0 show'; patcher self-validates via node-CRC recomputation and source-pattern check; CRC32C (v7 iscsi) implemented in C.

Verified recommended fix

hammer_reblock_data(): reject records whose (data_offset,data_len) is not fully contained in one 16KB buffer (data_len<=0 || data_len>HAMMER_BUFSIZE || (data_offset&BUFMASK)+data_len>HAMMER_BUFSIZE -> EINVAL); plus buffer-align >16KB allocations in hammer_blockmap_alloc()

Verdict

hammer_reblock_data() bcopy() consumes the on-disk leaf data_len/data_offset with no production validation; for data_len>16KB the blockmap allocator only enforces the big-block boundary, so with the (naturally misaligned) META append cursor the copy writes up to 16368 attacker-controlled bytes past the end of a getblk'd 64KB kernel buffer. Demonstrated first-shot on the stock INVARIANTS guest: forged direntry (data_len=65536, CRC-forged, source=bigfile 'A' extent) -> Fatal trap 12 supervisor-write in memmove+0x10a at hammer_reblock.c:487, guest dead. Primitive = controlled kernel heap overflow from a crafted HAMMER1 image triggered by the root-run 'hammer reblock'; silent corruption when the adjacent buffer KVA is mapped (the fault only occurs on unmapped pages). Precondition: privileged mount + privileged ioctl (same threat model as DF-3033/DF-3076/DF-3148), so not an unpriv->root path; ceiling = kernel memory corruption / host crash / potential code exec from a malicious image. Fixed and validated in-guest.