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)
PoC verification
Evidence pack
findings/poc/DF-3167 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_lencomes straight from disk (only a KKASSERT guards it,hammer_btree.c:736).hammer_alloc_data()(hammer_ondisk.c:1634) allocatesdata_lenbytes; for records withdata_len > HAMMER_BUFSIZEthe blockmap allocator only enforces the big-block boundary (hammer_blockmap.c:183-188), so the allocation may land at a non-16KB-aligned cursor offsetxoff(the META zone cursor is naturally misaligned β dirents/inodes pack at 16-byte granularity).- The destination buffer is
getblk(DOALIGN16K(data_len))bytes andndata = buffer->ondisk + xoff(hammer_ondisk.c:1114-1143, 715-722). - The copy writes
[xoff, xoff + data_len)into aDOALIGN16K(data_len)- sized buffer βxoffbytes (up to 16368) past the end, with fully attacker-controlled content (the old record data, CRC-checked against the forgeddata_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 filespatch_leaf.cβ stage 2: self-validating leaf forger (CRC32C/CRC32)trigger.shβ stage 3: mount, enable vfs.hammer.debug_general=0x4000, runhammer reblock /mntbuild.sh,run.sh,build.log,run.log,panic.txt,env.txtfix.diffβ verified fix (hammer_reblock.c + hammer_blockmap.c)
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:
hammer_reblock.c:464βhammer_btree_extract_data(cursor)loadscursor->data = buffer->ondisk + (data_offset & 0x3FFF); the only bound is the INVARIANTS-onlyKKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE)athammer_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.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), andhammer_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 cursorxoffinside a 16KB buffer (the META cursor is naturally misaligned; :372 even special-cases(next_offset & HAMMER_BUFMASK) != 0).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 exactlyDOALIGN16K(data_len)bytes (hammer_bread_extβ_hammer_breadhammer_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:
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 at0x9000000021800f90, i.e.xoff=0xf90).patch_leaf.c: forges elm 8 of leaf node0x8000000021000000(the PWNMARKER dirent, located/verified viahammer -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.trigger.sh: mount rw,vfs.hammer.debug_general=0x4000,hammer reblock /mnt(plain reblock:free_level=0from 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 hasdata_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_levelbounds all checked;vol_noused only for comparison (no indexing); whole ioctl privileged (hammer_ioctl.c:72caps_priv_check(SYSCAP_NOVFS_IOCTL)). - alloc/free balance: no error path between a successful
hammer_alloc_data/hammer_alloc_btreeand 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 staleelm; the uncache window (:344-367) reloadselmand 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 fixuphpanic(:680). - known findings DF-3033 (direct_uncache getblk entry :342-346) and
DF-3076 (blockmap_free bytes) not re-reported; the >64KB
data_lengetblk-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
fixedfix.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
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.
No comments yet.