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

hammer_generate_redo panics on UNDO/REDO FIFO exhaustion β€” unprivileged local kernel DoS via write()+fsync() (no backpressure on the frontend REDO path)

Summary

hammer_generate_redo() panics the kernel when free UNDO/REDO FIFO space drops below len+48KB (:90-91). REDO records (with full write payloads) are laid into the FIFO by the frontend write() path at memory speed with no waiting/throttling anywhere; space is reclaimed only by flusher finalize cycles whose on-disk first_offset lags a full cycle behind. Default vfs.hammer.fsync_mode=3 takes the REDO fast-fsync path which re-arms the 4MB per-inode budget on every fsync - an unprivileged write()+fsync() loop therefore lays unbounded REDO between reclaims. The trailing REDO_SYNC emission (:288-291) can panic even when the caller's own record fit. VERIFIED as nobody on a HAMMER v7 fs with the mkhammer-supported minimum 8MB FIFO (newfs_hammer -f -u 8m): 'panic: hammer_generate_redo: insufficient UNDO/REDO FIFO space for redo!' after ~6MB of REDO (~90s), stack hammer_generate_redo <- hammer_vop_write <- vn_write, guest in ddb. Default 512MB FIFO not overflowed on the QEMU guest (page-cached reclaim kept up over 3.84GB laid) but the same window exists on real storage; no FIFO-size validation at mount, so crafted images equally exposed. Sibling hpanic in hammer_generate_undo (hammer_undo.c:136-137) is the same class on the flusher-side path. Not memory corruption - no uid=0 route; ceiling is the panic. Fix validated in-guest (graceful ENOSPC + FIFO-pressure REDO gate in vop_write, kernel #1): identical 30-round trigger completes, mount healthy.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3215 Β· 17 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 5.6 KB ↓ raw
redo_fill.c β€” 3.7 KB view raw
setup_v2.sh β€” 719 B view raw
setup_v1.sh β€” 604 B view raw
run.sh β€” 149 B view raw
run_v2.sh β€” 640 B view raw
run_v1.sh β€” 987 B view raw
build.sh β€” 110 B view raw
build.log β€” 880 B view raw
run.log β€” 335 B view raw
run.2.log β€” 2.8 KB view raw
run.3.log β€” 1.2 KB view raw
panic.txt β€” 1.1 KB view raw
env.txt β€” 748 B view raw
fix.diff β€” 2.2 KB view raw
verdict.json β€” 5.0 KB view raw

DF-3215 β€” HAMMER1 UNDO/REDO FIFO exhaustion panic in hammer_generate_redo

What

hammer_generate_redo() (sys/vfs/hammer/hammer_redo.c:90-91) panics the kernel when the UNDO/REDO FIFO has less than len + 48KB free:

    if (hammer_undo_space(trans) < len + HAMMER_BUFSIZE*3)
        hpanic("insufficient UNDO/REDO FIFO space for redo!");

There is no waiting/throttling path for REDO generation. The frontend write() lays REDO records (with full data payloads) into the FIFO at memory speed under undo_lock; space is reclaimed only when the flusher completes a full finalize cycle, and the on-disk first_offset that governs hammer_undo_space() (hammer_undo.c:477-498) lags at least one full cycle behind (hammer_flusher.c:793-836). Default vfs.hammer.fsync_mode = 3 (hammer_vfsops.c:100) makes fsync() take the REDO fast path, which resets the 4MB per-inode REDO budget (hammer_vnops.c:239) forever β€” so an unprivileged write()+fsync() loop lays unbounded REDO records between reclaims.

The trailing REDO_SYNC emission (hammer_redo.c:288-291) runs after the main record has been laid and next_offset advanced, so the panic can fire there even when the caller's own record fit.

Reproduce (on the audit guest)

# one-time setup (root): HAMMER1 fs with the minimum 8MB UNDO/REDO FIFO
# (newfs_hammer -f -u 8m β€” a supported configuration; mkhammer only warns)
sh /tmp/df3215/setup_v2.sh        # vn0 image + newfs_hammer + mount /mnt/h1

# trigger (unprivileged):
su -m nobody -c "/tmp/df3215/redo_fill /mnt/h1/scratch 1 1048576 30"

Expected output (stock kernel, unprivileged)

Guest panics after ~6 rounds (~6MB of REDO records into the 8MB FIFO):

panic: hammer_generate_redo: insufficient UNDO/REDO FIFO space for redo!
hammer_generate_redo() at hammer_generate_redo+0x575
hammer_generate_redo() at hammer_generate_redo+0x575   <- trailing SYNC emit
hammer_vop_write() at hammer_vop_write+0x675
vn_write() -> kern_pwritev()

SSH dies, guest sits in ddb. See panic.txt, run_v2.log.

Scope / variants

  • V2 (deterministic, reproduced): 8MB FIFO (newfs_hammer minimum with -f).
  • V1 (default 512MB FIFO): NOT reproduced on the QEMU guest β€” the page-cached virtio flusher reclaimed fast enough (3.84GB of REDO laid, no overflow). On real (especially spinning) storage the same race window exists, but that claim is not demonstrated here.

Fix

fix.diff replaces the hpanic with a graceful ENOSPC failure and adds a FIFO-pressure gate to the write path that disables REDO (the existing degradation idiom) until space returns. Validated: patched kernel does not panic; writes succeed with REDO disabled; mount stays healthy.

VERDICT.md
↓ download raw

DF-3215 β€” VERDICT

Finding: hammer_generate_redo() panics the kernel when the UNDO/REDO FIFO is exhausted β€” reachable by an unprivileged write()/fsync() loop on any HAMMER (v4+) filesystem whose UNDO/REDO FIFO is small enough for the frontend to fill between two flusher finalize cycles.

Status: REPRODUCED (panic / unprivileged local DoS), fix validated.

How it reproduces (baseline kernel #0, stock INVARIANTS)

  1. setup_v2.sh (root, one-time, realistic admin action): a 12GB vn image formatted newfs_hammer -f -u 8m β€” the minimum UNDO/REDO FIFO size mkhammer supports (sbin/hammer/ondisk.c:format_undomap, min = one 8MB big-block; mkhammer merely warns "may lead to VFS panics") β€” mounted at /mnt/h1 with a 1777 scratch dir.
  2. redo_fill as nobody: first fsync() per file enables HAMMER_INODE_REDO (hammer_vnops.c:253-259); then repeated (write 1MB, fsync) pairs. Each fast-fsync (default vfs.hammer.fsync_mode=3, hammer_vfsops.c:100) resets the 4MB per-inode REDO budget (hammer_vnops.c:239), so the write path keeps calling hammer_generate_redo() (hammer_vnops.c:746), which lays REDO records with the full data payload into the FIFO at memory speed (hammer_redo.c:97-277).
  3. Space is only reclaimed when the flusher completes a full finalize cycle, and the on-disk first_offset that hammer_undo_space() uses (hammer_undo.c:477-498) lags at least one full cycle behind (hammer_flusher.c:736-800, comment "REDOs can be added at any time"). After ~6 rounds (~6MB of REDO into the 8MB FIFO) the check at hammer_redo.c:90 trips:
panic: hammer_generate_redo: insufficient UNDO/REDO FIFO space for redo!
hammer_generate_redo() at hammer_generate_redo+0x575
hammer_generate_redo() at hammer_generate_redo+0x575   <- trailing REDO_SYNC
hammer_vop_write() at hammer_vop_write+0x675
vop_write() -> vn_write() -> kern_pwritev()

The doubled frame shows the panic fired in the trailing REDO_SYNC emission (hammer_redo.c:288-291 β†’ hammer_generate_redo_sync β†’ hammer_generate_redo) β€” that path runs after the caller's record was already laid and next_offset advanced, so even a caller whose own record fit can panic there. Guest sat in ddb; ssh dead. Evidence: panic.txt, run.log.

Scope limits (honest negatives)

  • Default 512MB FIFO: NOT reproduced on this guest (run.3.log). 2.56GB of REDO laid by 4 parallel unprivileged writers, plus a second attempt with 4GB of co-resident dirty data stalling finalize reclaim β€” the QEMU page-cached virtio flusher always reclaimed in time (vfs.hammer.stats_redo reached 3.84GB without exhaustion). On real storage (esp. HDD sync latencies) the same window exists, but that is not demonstrated here. The demonstrated precondition is a small-undo filesystem (8MB, mkhammer-supported).
  • Not memory corruption: the primitive is a deterministic hpanic, i.e. denial of service. No escalation path.

Why this is a real defect

  • There is no waiting/throttling mechanism anywhere on the REDO generation path: the only backpressure is the panic itself. Compare hammer_generate_undo() (hammer_undo.c:136-137) β€” same panic, but its callers are flusher-side; the REDO one is frontend write()-reachable.
  • mkhammer permits the triggering configuration (-f -u 8m) and the kernel does not reject small UNDO FIFOs at mount; the resulting panic is triggered by an unprivileged user, not by the admin who mounted.
  • REDO is an optional optimization (disabled by hammer_skip_redo, degraded per-inode when the 4MB budget is exceeded, hammer_vnops.c:751-753) β€” panic is never the correct degradation.

Fix validation (kernel #1, built in-guest from fix.diff)

  • Baseline reproduced (panic, above). fix.diff applied to the guest's /usr/src (repo sys/ untouched), make nativekernel KERNCONF=X86_64_GENERIC + installkernel, reboot.
  • Exact same run.sh re-run: all 30 rounds complete in ~1s, no panic, guest up, mount healthy (run.2.log). The FIFO-pressure gate in hammer_vop_write disables REDO (the existing degradation idiom) once free FIFO space drops below 2x the per-inode budget; the redo.c change (ENOSPC instead of hpanic) backstops the ungated callers (REDO_TRUNC at hammer_vnops.c:2256, backend TERM_WRITE/TERM_TRUNC at hammer_inode.c:2856/3038, and the trailing SYNC emission).
  • Note: on an 8MB FIFO the gate (2x4MB) disables REDO before exhaustion, so the ENOSPC backstop itself was not directly exercised by this run; it is a 10-line early-return on the failure path.

Kernel references

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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to the guest's /usr/src copy (repo sys/ untouched); make nativekernel KERNCONF=X86_64_GENERIC + installkernel, reboot into kernel #1. Baseline (stock #0): panic at round 6. Patched (#1): identical run.sh completes all 30 rounds in ~1s, no panic, guest and mount healthy (run.2.log). The FIFO-pressure gate disables REDO before exhaustion; the ENOSPC backstop covers ungated callers (REDO_TRUNC, backend TERMs, trailing SYNC) and was not directly exercised by this run.

run.2.log (patched run, no panic); build.log (kernel install completed); fix.diff
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 8 15:56:24 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["panic.txt β€” serial-console panic: 'panic: hammer_generate_redo: insufficient UNDO/REDO FIFO space for redo!' with double hammer_generate_redo frame + hammer_vop_write/vn_write/kern_pwritev", 'run.log β€” baseline V2 run as nobody: rounds 1-6 progress into the 8MB FIFO, then session death (panic)', 'run.2.log β€” patched kernel (fix.diff): identical trigger, all 30 rounds complete, guest up, no panic', 'run.3.log β€” V1 default-512MB-FIFO negative: 2.56-3.84GB of REDO laid unprivileged, no overflow on this guest', 'setup_v2.sh / redo_fill.c β€” the vn-image harness (newfs_hammer -f -u 8m) and the unprivileged trigger', 'fix.diff β€” git-apply clean against sys/: hpanic -> graceful ENOSPC + write-path FIFO-pressure gate']

PoC changes

Authored fresh (no seed): redo_fill.c (first fsync enables HAMMER_INODE_REDO, then write+fsync pairs re-arm the 4MB budget); fixed newfs_hammer -u suffix ('8m' not '8388608'), vn image path moved off the 975MB tmpfs, mount|grep label fix. Two V1 default-config variants added to probe the 512MB FIFO race (negative on this guest).

Verified recommended fix

Replace the redo.c hpanic with a graceful ENOSPC failure and gate REDO generation in hammer_vop_write on FIFO headroom (disable REDO under pressure, the existing degradation idiom); see findings/poc/DF-3215/fix.diff.

Verdict

REPRODUCED on the stock INVARIANTS guest: an unprivileged user (nobody) running a write()+fsync() loop on a HAMMER v7 filesystem with the mkhammer-supported minimum 8MB UNDO/REDO FIFO panics the kernel ('panic: hammer_generate_redo: insufficient UNDO/REDO FIFO space for redo!') within ~6 rounds / ~90s. Root cause: hammer_generate_redo (sys/vfs/hammer/hammer_redo.c:90-91) panics instead of failing or waiting, and there is NO backpressure anywhere on the frontend REDO path β€” default vfs.hammer.fsync_mode=3 takes the REDO fast-fsync (hammer_vnops.c:225-246) which re-arms the 4MB per-inode budget forever, while FIFO space is only reclaimed by flusher finalize cycles whose on-disk first_offset lags a full cycle behind (hammer_flusher.c:736-800). The panic actually fired in the trailing REDO_SYNC emission (hammer_redo.c:288-291) which runs after the caller's own record was laid. Default 512MB FIFO NOT overflowed on this QEMU guest (page-cached virtio reclaim kept up over 3.84GB of laid REDO β€” see run.3.log), so the demonstrated precondition is a small-undo filesystem. Not memory corruption: impact is a deterministic unprivileged local DoS. Fix validated: hpanic->ENOSPC + FIFO-pressure REDO gate in the write path; patched kernel completes the identical trigger with no panic and a healthy mount.