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)
PoC verification
Evidence pack
findings/poc/DF-2594 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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):
sys/vfs/hammer/hammer_recover.c:1332βvn_rdwr(UIO_WRITE, vp, (void *)(redo + 1), redo->redo_data_bytes, ...)
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)
newfs_hammera v7 image, create a victim regular file (so the recovery write has a writable, non-directory inode to target β the root dir objid 1 rejectsVOP_WRITEwithEINVAL).- Forge the image (
forge.c, links the kernel's ownicrc32.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 aDUMMYsentinel (mismatched seqno so Stage-1's forward seqno scan terminates via the discontinuity branch). Patch the UNDO blockmapfirst_offset/next_offsetand recompute the blockmapentry_crcand volume-headervol_crc. mount_hammerRW. Stage-1 findsREDO_SYNCβ setsREDO_RECOVERY_REQ. Stage-2 scans the extended range, finds theREDO_WRITE(unfiltered), callshammer_recover_redo_execβvn_rdwr(..., redo_data_bytes=0x4000).uiomove/memmovereads 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 returnsEINVAL(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: thevn_rdwractually 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 12page fault inmemmoveduring the recovery write. Guest down. - Single-fix kernel (
#1, only thisfix.diffapplied, rebuiltnativekernel): same PoC βHAMMER: Corrupt REDO record, redo_data_bytes 16384/0βEnd redo recoveryβ mount fails withEIO, no panic, guest stays up.fix_status = fixed.
Fix verification
fixedVALIDATED. 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.
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).
No comments yet.