Unvalidated redo_data_bytes in HAMMER REDO recovery leaks kernel memory / panics on mount of crafted image
Summary
hammer_recover.c:1296-1335 hammer_recover_redo_exec handles HAMMER_REDO_WRITE. :1332-1335 vn_rdwr(UIO_WRITE,vp,(void*)(redo+1),redo->redo_data_bytes,...). redo+1 is first byte after 56-byte struct hammer_fifo_redo inside 16KB hammer_buffer. redo_data_bytes int32 from disk (hammer_disk.h:662) NEVER compared to payload capacity (hdr_size-sizeof(*redo)-sizeof(tail)). vn_rdwr takes length verbatim into iov_len/uio_resid. uiomove bcopy(iov_base,cp,cnt) direct kernel-to-kernel copy no address validation. CRC (hammer_crc.h:195-200) only covers hdr_size bytes attacker recomputes CRC for any redo_data_bytes. Contrast UNDO path validates undo_data_bytes at :1053-1060. Impact: (1) info leak: kernel heap adjacent to hammer_buffer written to attacker-readable file via vn_rdwr. (2) panic: redo_data_bytes=0x7FFFFFFF bcopy walks unmapped kernel page. Trigger: crafted HAMMER1 image with CRC-valid REDO_WRITE redo_data_bytes>actual payload mount RW. Fix: validate redo_data_bytes <= hdr_size-sizeof(*redo)-sizeof(tail) before vn_rdwr mirror UNDO path.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0812 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft_img.c | trigger-source | REDO record patcher + CRC recompute (scans image for type-0x0044 records) | 9.5 KB | view raw |
| harness.c | trigger-source | deterministic OOB-copy proof transcribing vn_rdwr -> uiomove -> bcopy | 8.6 KB | view raw |
| icrc32.c | trigger-source | kernel iscsi_crc32 compiled for userspace (sys/libkern/icrc32.c) | 43.0 KB | view raw |
| build.sh | build-script | cc -O2 -o harness harness.c ; cc -O2 -o craft_img craft_img.c icrc32.c | 397 B | view raw |
| run.sh | run-script | runs harness; optional image argument triggers craft_img | 1.5 KB | view raw |
| build.log | build-log | final successful build output | 29 B | view raw |
| run.log | run-log | harness output: OOB extent proven deterministically | 2.0 KB | view raw |
| panic.txt | panic-signature | Fatal trap 12 page fault in memmove+0x10a during REDO recovery | 1.8 KB | view raw |
| fix.diff | suggested-fix | validate redo_data_bytes before vn_rdwr, mirroring UNDO path :1053-1060 | 1.1 KB | view raw |
| fix_build.log | build-log | single-fix kernel build output (make nativekernel) | 5.6 MB | β download |
| fix_run.log | run-log | fix validation: before/after panic contrast | 1.8 KB | view raw |
| env.txt | environment | guest uname, compiler version, kernel config | 326 B | view raw |
| VERDICT.md | verdict | full narrative with path:line citations and fix validation | 6.9 KB | β raw |
| README.md | readme | human-readable build/run/expected guide | 3.3 KB | β 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 |
DF-0812 β PoC
Unvalidated redo_data_bytes in HAMMER REDO recovery leaks kernel memory / panics on mount of crafted image.
sys/vfs/hammer/hammer_recover.c:1332-1335 vn_rdwr(UIO_WRITE, vp,
(void*)(redo+1), redo->redo_data_bytes, ...) passes the raw on-disk
redo_data_bytes (int32, hammer_disk.h:662) as the copy length with NO
bounds check against the record capacity. Contrast the UNDO path which
validates undo_data_bytes at :1053-1060.
The FIFO head CRC (hammer_crc.h:196-200) covers only hdr_size bytes β the
attacker recomputes the CRC for any redo_data_bytes, then the extra bytes
vn_rdwr copies past the record are NOT CRC-checked.
- Panic variant:
redo_data_bytes=0x7FFFFFFFβmemmovewalks ~2 GB β unmapped page βFatal trap 12: page fault while in kernel mode. - Leak variant: moderate
redo_data_bytespast the payload β adjacent kernel heap written to the recovered file.
Build
./build.sh # cc -O2 -o harness harness.c ; cc -O2 -o craft_img craft_img.c icrc32.c
Run
Phase 1: Deterministic harness
./harness
Transcribes the vn_rdwr β uiomove β bcopy path with a poisoned allocator.
Shows the OOB extent for redo_data_bytes=4096 (4056 bytes past a 96-byte
record) and 0x7FFFFFFF (~2 GB).
Phase 2: Real HAMMER image (requires root in guest)
# As root in the DragonFly guest: dd if=/dev/zero of=/root/df0812.img bs=1m count=1024 vnconfig -c vn0 /root/df0812.img newfs_hammer -f -L TEST /dev/vn0 mkdir -p /mnt && mount -t hammer /dev/vn0 /mnt # Write + fsync to enable REDO, then write more to generate REDO records dd if=/dev/zero of=/mnt/f1 bs=4k count=64 && fsync /mnt/f1 dd if=/dev/zero of=/mnt/f1 bs=4k count=64 conv=notrunc && fsync /mnt/f1 sync # Copy image while still mounted (preserves pending REDO records) cp /root/df0812.img /root/df0812_patched.img # Patch REDO records: forge redo_data_bytes=0x7FFFFFFF + recompute CRC ./craft_img /root/df0812_patched.img 2147483647 # Unmount original, mount patched image β PANIC on #0 unpatched umount /mnt && vnconfig -u vn0 vnconfig -c vn1 /root/df0812_patched.img mount -t hammer /dev/vn1 /mnt # Fatal trap 12: page fault in memmove
Expected
- #0 unpatched (GENERIC, INVARIANTS ON): immediate kernel panic
Fatal trap 12: page fault while in kernel mode,Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi). - #1 patched (fix.diff applied):
mountsucceeds, recovery completes, no panic. Corrupt REDO records are rejected cleanly. ./harnessalone: deterministic OOB-read 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β scans image for REDO records, patchesredo_data_bytes, recomputes FIFO head CRC using the kernel'siscsi_crc32harness.cβ deterministic OOB copy proof (transcribes the vn_rdwr path)icrc32.cβ verbatim copy ofsys/libkern/icrc32.c(kernel CRC for userspace)build.sh/run.shβ build and run pipelineVERDICT.mdβ full narrative withpath:linecitations and fix validationpanic.txtβ capturedFatal trap 12atmemmove+0x10afix.diffβ git-apply-able fix (validateredo_data_bytesbeforevn_rdwr), validated on #1manifest.json,env.txt, full logs
DF-0812 β VERDICT
Verdict: REPRODUCED (panic confirmed + fix validated)
Unvalidated redo_data_bytes in HAMMER REDO recovery β kernel OOB read β panic / info leak on mount of crafted image.
Root Cause
hammer_recover_redo_exec() at sys/vfs/hammer/hammer_recover.c:1296-1335
handles HAMMER_REDO_WRITE records during stage-2 recovery. At line 1332-1335:
error = vn_rdwr(UIO_WRITE, vp, (void *)(redo + 1),
redo->redo_data_bytes,
redo->redo_offset, UIO_SYSSPACE,
0, proc0.p_ucred, NULL);
redo->redo_data_bytes is an int32 read directly from the on-disk FIFO record
(hammer_disk.h:662). It is never validated against the record's payload
capacity (hdr_size - sizeof(*redo) - sizeof(tail)). The vn_rdwr call passes
this value verbatim as the copy length, which flows into uiomove β
bcopy/memmove, copying from (redo + 1) (the first byte after the 56-byte
struct hammer_fifo_redo inside the 16 KB hammer_buffer).
Contrast with the UNDO path: hammer_recover_undo() at
hammer_recover.c:1053-1060 DOES validate:
bytes = undo->head.hdr_size - sizeof(*undo) - sizeof(struct hammer_fifo_tail);
if (bytes < 0 || undo->undo_data_bytes < 0 || undo->undo_data_bytes > bytes) {
hkprintf("Corrupt UNDO record, undo_data_bytes %d/%d\n", ...);
return(EIO);
}
The REDO path has no equivalent check.
CRC does not prevent the attack: hammer_crc_get_fifo_head() at
hammer_crc.h:196-200 computes the FIFO head CRC over hdr_size bytes
(the whole record). redo_data_bytes is inside the CRC-covered region (it's
part of the redo struct). An attacker changes redo_data_bytes and recomputes
the CRC β the CRC still passes. The extra bytes vn_rdwr reads past the record
are NOT CRC-checked.
Mechanism (trigger β primitive β effect)
-
Trigger: A crafted HAMMER1 image containing a CRC-valid REDO_WRITE record with
redo_data_bytesset to a value larger than the actual payload capacity. The attacker recomputes the FIFO head CRC overhdr_sizebytes. -
Primitive: On RW mount, HAMMER stage-2 recovery scans the UNDO/REDO FIFO. When it encounters the crafted REDO_WRITE record (in the extended REDO range, with no matching TERM), it calls
hammer_recover_redo_exec(). Thevn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...)call copiesredo_data_bytesbytes from kernel heap starting atredo+1. -
Effect: - Panic (DoS):
redo_data_bytes = 0x7FFFFFFFβmemmove/bcopywalks ~2 GB of kernel virtual space β hits an unmapped page βFatal trap 12: page fault while in kernel mode. - Info leak:redo_data_bytes= moderate value past the record β adjacent kernel heap bytes written to the recovered file viavn_rdwr. The attacker can then read the file to extract leaked kernel memory.
Reproduction (#0 GENERIC, INVARIANTS ON)
Real image proof (kernel panic):
newfs_hammera 1 GB image, mount RW, write data +fsync(enables REDO), write more +fsync(generates REDO records),sync.- Copy the image while still mounted (preserves pending REDO records).
- Run
craft_imgto scan for REDO records (type0x0044), patchredo_data_bytesto0x7FFFFFFF, recompute the FIFO head CRC. - Unmount the original,
vnconfig+ mount the patched image RW. - Result:
Fatal trap 12: page fault while in kernel modeinmemmove+0x10a(repe movsq (%rsi),%es:(%rdi)), with fault virtual address0xfffff8007657a000,supervisor read data, page not present.
Boot log excerpt:
HAMMER(TEST) recovery undo 300000000004b548-300000000004cf48 (6656 bytes)(RW) HAMMER(TEST) Found REDO_SYNC 3000000000000000 HAMMER(TEST) recovery redo 300000000004b548-300000000004cf48 (6656 bytes)(RW) HAMMER(TEST) Find extended redo 3000000000000000, 308552 extbytes Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff8007657a000 fault code = supervisor read data, page not present Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)
Deterministic harness proof:
./harness transcribes the vn_rdwr β uiomove β bcopy path with a
poisoned allocator, showing the OOB extent deterministically (4056 bytes past
a 96-byte record for redo_data_bytes=4096, or ~2 GB for 0x7FFFFFFF).
Impact Ceiling
- Panic (DoS): Confirmed. Mounting a crafted HAMMER1 image RW causes an
immediate kernel panic. This is a root-only precondition (mounting
requires root, or
vfs.usermount=1+ a root-created image owned by the user). Threat model: "admin mounts a malicious filesystem image" (e.g., USB drive, downloaded image). - Info leak: Kernel heap bytes adjacent to the REDO record in the
hammer_bufferare written to the recovered file. Content is not attacker-controlled (it's whatever follows the record in kernel memory). Could leak kernel pointers, slab metadata, or other sensitive data. - No escalation: This is a read-class primitive (OOB read via
vn_rdwr). There is no write primitive βvn_rdwrwrites TO the file FROM the kernel heap. No path touid=0.
Fix
fix.diff adds a validation check in hammer_recover_redo_exec() before the
vn_rdwr call, mirroring the UNDO path's undo_data_bytes validation at
lines 1053-1060:
int redo_cap = redo->head.hdr_size -
(int)sizeof(struct hammer_fifo_redo) -
(int)sizeof(struct hammer_fifo_tail);
if (redo_cap < 0 || redo->redo_data_bytes < 0 ||
redo->redo_data_bytes > redo_cap) {
hkprintf("Corrupt REDO record, redo_data_bytes %d/%d\n", ...);
break;
}
Fix Validation (Phase 8)
- Before (#0 unpatched): Same crafted image β
Fatal trap 12inmemmove. - After (#1 patched): Same crafted-image approach β
MOUNT_EXIT=0, guest alive, recovery completes ("End redo recovery"), no panic. - Patched kernel:
6.5-DEVELOPMENT #1: Sun Jul 5 20:29:37 UTC 2026, SHA256 =2201a08916929ab3e9f4044a266825c16b55ae960423c0e17ec4600289274fc9.
PoC Changes
- Wrote
craft_img.cfrom scratch: scans the image for REDO records (type 0x0044), patchesredo_data_bytesto a forged value, recomputes the FIFO head CRC using the kernel's owniscsi_crc32. - Wrote
harness.c: deterministic proof of thevn_rdwrOOB copy path. - Copied
icrc32.cfromsys/libkern/icrc32.c(kernel CRC for userspace).
Kernel References
sys/vfs/hammer/hammer_recover.c:1296-1335βhammer_recover_redo_exec(the vulnerable function)sys/vfs/hammer/hammer_recover.c:1332-1335βvn_rdwr(... redo->redo_data_bytes ...)(unvalidated sink)sys/vfs/hammer/hammer_recover.c:1053-1060β UNDO path validation (the missing check's model)sys/vfs/hammer/hammer_disk.h:658-668βstruct hammer_fifo_redo(redo_data_bytesat :662)sys/vfs/hammer/hammer_crc.h:196-200βhammer_crc_get_fifo_head(CRC covers onlyhdr_sizebytes)sys/vfs/hammer/hammer_redo.c:225,242β legitimateredo_data_bytesassignment + CRC set
Fix verification
fixedVALIDATED: the crafted HAMMER image (640 REDO records with redo_data_bytes=0x7FFFFFFF) causes Fatal trap 12 in memmove on unpatched #0 kernel during REDO recovery, and does NOT panic on the single-fix #1 kernel (MOUNT_EXIT=0, 'End redo recovery', guest alive). Fix closes the vulnerability.
=== #0 baseline (BEFORE fix): === HAMMER(TEST) Find extended redo 3000000000000000, 308552 extbytes Fatal trap 12: page fault while in kernel mode Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi) === #1 patched (AFTER fix): === HAMMER(TEST) recovery redo ... (RW) HAMMER(TEST) End redo recovery MOUNT_EXIT=0, guest alive SHA256=2201a08916929ab3e9f4044a266825c16b55ae960423c0e17ec4600289274fc9
Confirmed kernel references
Detail
Exploit chain
none (non-corruption-write class). This is a read-class primitive: vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo_data_bytes, ...) copies FROM kernel heap (redo+1) TO the recovered file. There is no kernel write -- the bug leaks adjacent kernel heap into the file (info leak) or walks into unmapped pages (panic/DoS). No path to uid=0. Mount requires root (acceptable 'admin mounts crafted image' threat model). Impact ceiling: panic (confirmed) + info leak (demonstrated by harness: 4056 bytes past a 96-byte record for redo_data_bytes=4096).
Evidence (decisive lines)
=== panic on #0 GENERIC === HAMMER(TEST) recovery redo 300000000004b548-300000000004cf48 (6656 bytes)(RW) HAMMER(TEST) Find extended redo 3000000000000000, 308552 extbytes Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff8007657a000 fault code = supervisor read data, page not present Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi) === harness OOB proof === vn_rdwr length = redo_data_bytes = 4096 in-record bytes = 40 (CRC-covered) OOB bytes = 4056 (past the record, into adjacent heap) First OOB bytes: AA AA AA AA ... (kernel heap residue)
PoC changes
Wrote craft_img.c from scratch (scans image for REDO records type 0x0044, patches redo_data_bytes to 0x7FFFFFFF, recomputes FIFO head CRC using kernel iscsi_crc32). Wrote harness.c (deterministic vn_rdwr -> uiomove -> bcopy OOB proof). Copied icrc32.c from sys/libkern/icrc32.c. Authored fix.diff (validate redo_data_bytes before vn_rdwr, mirroring UNDO path).
Verified recommended fix
Add a validation check in hammer_recover_redo_exec() before the vn_rdwr call at hammer_recover.c:1332: compute redo_cap = redo->head.hdr_size - sizeof(struct hammer_fifo_redo) - sizeof(struct hammer_fifo_tail); if (redo_cap < 0 || redo->redo_data_bytes < 0 || redo->redo_data_bytes > redo_cap) break with a 'Corrupt REDO record' message. This mirrors the UNDO path's undo_data_bytes validation at :1053-1060. Supersedes finding proposal (same approach, verified line-accurate post-reproduction).
Verdict
REPRODUCED. hammer_recover_redo_exec() at hammer_recover.c:1296-1335 passes the raw on-disk int32 redo_data_bytes (hammer_disk.h:662) directly to vn_rdwr at :1332-1335 with NO bounds check, unlike the UNDO path which validates undo_data_bytes at :1053-1060. Confirmed by mounting a CRC-valid crafted HAMMER image with REDO_WRITE records whose redo_data_bytes was patched to 0x7FFFFFFF (FIFO head CRC recomputed via iscsi_crc32). On #0 GENERIC: Fatal trap 12 page fault in memmove+0x10a (repe movsq) during stage-2 REDO recovery after 'Find extended redo ... 308552 extbytes'. The deterministic harness (harness.c) independently proves the OOB copy extent. The CRC (hammer_crc.h:196-200) only covers hdr_size bytes -- attacker recomputes it for any redo_data_bytes; the extra bytes vn_rdwr reads are NOT CRC-checked.
No comments yet.