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

Unvalidated redo_data_bytes and REDO record size in recovery cause OOB kernel-memory read on mount of crafted image

Summary

Stage2 REDO recovery (RW mount v4+ image) hammer_recover_redo_exec passes on-disk redo->redo_data_bytes directly as copy length to vn_rdwr(UIO_WRITE redo+1 redo_data_bytes) no bounds check. vn_rdwr sets iov_len=len no clamping. Unlike UNDO path (validates undo_data_bytes :1053-1060) REDO path never checks redo_data_bytes fits FIFO record nor hdr_size large enough to hold 56-byte hammer_fifo_redo struct (_hammer_check_signature only enforces hdr_size>=24 :937-942). Two missing validations: (a) no hdr_size>=sizeof(redo)+sizeof(tail)=64 before reading REDO fields past offset 24 (b) no redo_data_bytes bounded to payload hdr_size-sizeof(*redo)-sizeof(tail). Crafted image with valid CRCs attacker recompute. Reliable panic on mount or limited kmem disclosure to recovered file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2594 Β· 11 files
FileTypeDescriptionSize
forge.c trigger-source HAMMER image forger: injects valid-CRC REDO_WRITE w/ inflated redo_data_bytes + REDO_SYNC + sentinel 10.4 KB view raw
icrc32.c crc-source kernel iscsi_crc32 (userspace-buildable) for forged CRCs 43.0 KB view raw
crc32.c crc-source kernel crc32 (userspace-buildable) for v6 CRC fallback 6.1 KB view raw
build.sh build-script cc -I/usr/src/sys -o forge forge.c 203 B view raw
run.sh run-script newfs+forge+mount RW trigger (as root) 1.5 KB view raw
run.log run-log baseline run: recovery + panic signature 203 B view raw
panic.txt panic-signature Fatal trap 12 in memmove during REDO recovery vn_rdwr 1.2 KB view raw
fix_build.log build-log single-fix nativekernel build (rc=0) 4.2 MB ↓ download
env.txt environment uname / kern.version / cc version 311 B view raw
fix.diff suggested-fix validate redo_data_bytes in hammer_recover_redo_run 1.6 KB view raw
VERDICT.md verdict full analysis 5.8 KB ↓ raw
VERDICT.md verdict full analysis
↓ download raw

DF-2594 β€” Unvalidated redo_data_bytes in hammer REDO recovery β†’ OOB kernel read / panic on mount

Verdict

REPRODUCED (panic on mount of a crafted HAMMER image). Fix VALIDATED (single-fix kernel rejects the record, no panic).

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

HAMMER Stage-2 REDO recovery replays HAMMER_REDO_WRITE records found in the UNDO/REDO FIFO on a read-write (v4+) mount. hammer_recover_redo_exec() hands the on-disk redo->redo_data_bytes directly to vn_rdwr() as the copy length, reading from the in-kernel FIFO buffer at (redo + 1):

There is no validation that redo_data_bytes fits inside the FIFO record (hdr_size - sizeof(*redo) - sizeof(tail)). This is in stark contrast to the sibling UNDO path, hammer_recover_undo(), which does validate at hammer_recover.c:1053-1060:

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) {
    ... return(EIO);
}

The only checks a REDO record must pass before reaching the vulnerable line are _hammer_check_signature() (hammer_recover.c:930-942): a CRC over hdr_size bytes (which an attacker recomputes) and a minimum hdr_size >= sizeof(head)+sizeof(tail) = 24 bytes. A 64-byte REDO record (56-byte hammer_fifo_redo + 8-byte tail, zero payload) therefore sails past every gate while declaring redo_data_bytes = 16384 (or up to 0x7FFFFFFF). When vn_rdwr then copies redo_data_bytes bytes starting at (redo + 1) it reads past the 16 KB hammer buffer into adjacent kernel memory.

Trigger path (confirmed on the running kernel)

  1. newfs_hammer a v7 image, create a victim regular file (so the recovery write has a writable, non-directory inode to target β€” the root dir objid 1 rejects VOP_WRITE with EINVAL).
  2. Forge the image (forge.c, links the kernel's own icrc32.c/crc32.c): inject three valid-CRC FIFO records at the start of the UNDO zone β€” REDO_WRITE (seqno S, redo_data_bytes = 0x4000, objid = victim file), REDO_SYNC (seqno S+1, redo_offset = extended-range start), and a DUMMY sentinel (mismatched seqno so Stage-1's forward seqno scan terminates via the discontinuity branch). Patch the UNDO blockmap first_offset/next_offset and recompute the blockmap entry_crc and volume-header vol_crc.
  3. mount_hammer RW. Stage-1 finds REDO_SYNC β†’ sets REDO_RECOVERY_REQ. Stage-2 scans the extended range, finds the REDO_WRITE (unfiltered), calls hammer_recover_redo_exec β†’ vn_rdwr(..., redo_data_bytes=0x4000).
  4. uiomove/memmove reads 16384 bytes from (redo+1) (buffer offset 56) β†’ runs off the 16 KB buffer β†’ page fault on the next unmapped kernel page.

Observed crash (unpatched #0 baseline)

HAMMER(TEST) Found REDO_SYNC 3000000000000000
HAMMER(TEST) recovery redo  3000000000000040-3000000000000080 (64 bytes)(RW)
HAMMER(TEST) Find extended redo  3000000000000000, 64 extbytes
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xfffff8007522a000   (page just past the hammer buffer)
fault code             = supervisor read data, page not present
instruction pointer    = 0x8:0xffffffff80bcaa0a
Stopped at  memmove+0x10a:  repe movsq  (%rsi),%es:(%rdi)

Impact ceiling

Mount requires root (or an auto-mount / vfs.usermount threat model), so this is not an unprivileged→root escalation. It is a root-mountable-image kernel OOB read / DoS:

  • With a huge redo_data_bytes (e.g. 0x7FFFF000): the recovery write is attempted but returns EINVAL (the root dir is not writable / length cap) β€” the vulnerable line is still reached and executed with the attacker length, but no memory is copied in that degenerate config (confirmed: "write ... returned 22").
  • With a moderate redo_data_bytes (e.g. 0x4000 = 16384) targeting a real regular-file inode: the vn_rdwr actually performs the read past the buffer β†’ reliable panic (page fault), or, with a smaller over-read that stays within mapped pages, kernel-heap bytes written into the recovered file (kernel memory disclosure readable after mount).

The two confirmed effects: panic (DoS) and OOB kernel-memory read.

Exploit chain

Not an unprivileged-corruption class β€” this is a filesystem-parsing bug triggered by mounting a crafted image (root/auto-mount precondition). No uid0 chain applies (Phase 6 N/A). The primitive is characterized above; the deliverable is the panic + the disclosed-kernel-memory ceiling.

PoC changes

Authored the full image-forger (forge.c) plus build.sh/run.sh and icrc32.c/crc32.c (kernel CRC sources, userspace-buildable, so forged CRCs are byte-identical to the kernel's). No prior PoC scaffolding existed.

Fix (fix.diff)

Adds the missing validation to hammer_recover_redo_run(), mirroring the UNDO path: require hdr_size >= sizeof(*redo)+sizeof(tail) (so the REDO struct fields are in-record), and for REDO_WRITE require 0 <= redo_data_bytes <= hdr_size - sizeof(*redo) - sizeof(tail). A crafted record now returns EIO and recovery ends cleanly instead of feeding an attacker length to vn_rdwr. Supersedes any pre-verification proposal (this is the verified, line-accurate fix).

Fix validation

  • Baseline (#0, unpatched): PoC β†’ Fatal trap 12 page fault in memmove during the recovery write. Guest down.
  • Single-fix kernel (#1, only this fix.diff applied, rebuilt nativekernel): same PoC β†’ HAMMER: Corrupt REDO record, redo_data_bytes 16384/0 β†’ End redo recovery β†’ mount fails with EIO, no panic, guest stays up. fix_status = fixed.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. Forged image panics on unpatched #0 baseline (Fatal trap 12 in memmove during REDO recovery vn_rdwr OOB read). On single-fix #1 kernel (only this fix.diff, rebuilt nativekernel) same PoC now hits added guard: 'HAMMER: Corrupt REDO record, redo_data_bytes 16384/0' -> 'End redo recovery' -> mount fails with EIO, no panic, guest stays up.

baseline #0: Fatal trap 12 page fault in memmove at 0xfffff8007522a000 (redo_data_bytes=0x4000 OOB read past hammer buffer) -> panic. fixed #1: HAMMER: Corrupt REDO record, redo_data_bytes 16384/0 / HAMMER(TEST) End redo recovery / mount: Input/output error, guest UP, no panic.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Aug 9 06:31:00 UTC 2026 (single-fix nativekernel, only DF-2594 fix.diff applied; strings confirm 'Corrupt REDO record, redo_data_bytes' present in kernel.stripped)

Confirmed kernel references

Detail

Exploit chain

none (not unprivileged-corruption class). Filesystem-parsing bug triggered by mounting crafted HAMMER image (root/auto-mount precondition); realistic impact ceiling OOB kernel-memory read written into recovered file (info leak) or panic on mount. No uid0 chain applies. With redo_data_bytes=0x7FFFF000 vulnerable vn_rdwr line reached/executed with attacker length but root-dir target returns EINVAL before copying; with moderate redo_data_bytes (0x4000) targeting regular file inode the read actually runs past buffer -> demonstrated page-fault panic.

Evidence (decisive lines)

HAMMER(TEST) Found REDO_SYNC 3000000000000000 | recovery redo 3000000000000040-3000000000000080 (64 bytes)(RW) | Find extended redo 3000000000000000, 64 extbytes | Fatal trap 12: page fault while in kernel mode | fault virtual address = 0xfffff8007522a000 | fault code = supervisor read data, page not present | Stopped at memmove+0x10a: repe movsq (%rsi),%es:(%rdi)

PoC changes

Authored full image forger forge.c (injects valid-CRC REDO_WRITE w/ inflated redo_data_bytes + REDO_SYNC + seqno-mismatch sentinel, patches UNDO blockmap first/next + recomputes blockmap entry_crc and volume-header vol_crc), build.sh, run.sh, plus icrc32.c/crc32.c (kernel CRC sources, userspace-buildable). Fixed CRC region offset (HAMMER_FIFO_HEAD_CRCOFF=12 not 16) and added sentinel record so Stage1's forward seqno scan terminates cleanly.

Verified recommended fix

In hammer_recover_redo_run() (sys/vfs/hammer/hammer_recover.c) before dispatching, require hdr_size >= sizeof(redo)+sizeof(tail) and for REDO_WRITE require 0 <= redo_data_bytes <= hdr_size-sizeof(redo)-sizeof(tail), returning EIO otherwise β€” directly mirroring UNDO validation at lines 1053-1060. Full git-apply-able diff in findings/poc/DF-2594/fix.diff.

Verdict

REPRODUCED. hammer Stage2 REDO recovery (hammer_recover.c:1332) passes the on-disk redo->redo_data_bytes directly to vn_rdwr with NO validation, unlike sibling UNDO path which validates at hammer_recover.c:1053-1060. Forged a HAMMER v7 image (forge.c, linking kernel's own icrc32.c so CRCs byte-identical) carrying a REDO_WRITE record with redo_data_bytes=0x4000 but zero real payload + REDO_SYNC + seqno-mismatch sentinel, then mount_hammer RW: Stage1 found REDO_SYNC, Stage2 replayed REDO_WRITE, vn_rdwr read 16384 bytes from (redo+1) off end of 16KB hammer buffer -> Fatal trap 12 page fault in memmove at fault addr 0xfffff8007522a000 (page just past buffer).