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

hammer2_fixup_pfses kprintf uses ripdata captured before hammer2_chain_modify COW'd and released the buffer

Field Value
ID DF-2623
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:L/I:N/A:N
CWE CWE-672 Operation on a Resource after Expiration or Release
File sys/vfs/hammer2/hammer2_vfsops.c
Lines 2399-2406
Area vfs
Confidence certain
Discovered 2026-08-28
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

In hammer2_fixup_pfses the PFS inode data pointer ripdata = &chain->data->ipdata is captured at vfsops.c:2399, then hammer2_chain_modify() (vfsops.c:2401) performs a copy-on-write: it allocates a new block, copies the data, releases the old DIO (hammer2_io_bqrelse, hammer2_chain.c:1857-1860) and repoints chain->data. The subsequent kprintf at vfsops.c:2405-2406 then reads ripdata->filename from the released buffer β€” a stale-pointer read whose contents may have been recycled by concurrent I/O to the same offset.

Root cause

hammer2_chain_modify on an INODE chain with MODIFIED clear takes the newmod=1 path (hammer2_chain.c:1485-1538): freemap_alloc assigns a new data_off, io_bread reads the new block, bcopy copies old→new, then lines 1857-1860 if ((tio = chain->dio) != NULL) hammer2_io_bqrelse(&tio); chain->data = bdata;. vfsops.c:2399's ripdata still points into the old, now-bqrelse'd buffer window; vfsops.c:2405-2406 dereferences it for the %s. The old DIO remains cached for its offset, so on a quiet system the bytes are usually intact (cosmetic), but the buffer can be re-dirtied or evicted under hammer2_dio_limit pressure, turning the log line into a read of recycled buffer-cache memory.

Threat model & preconditions

  • Attacker position: RW mount (root, or user with vfs.usermount on an RW device) of a crafted image containing a PFS inode whose bref lacks HAMMER2_BREF_FLAG_PFSROOT β€” exactly what fixup_pfses exists to correct.
  • Privileges gained or impact: up to 256 bytes of stale/recycled buffer-cache content emitted to the console log per mis-flagged PFS; no write primitive; dmesg visibility depends on local policy.
  • Required config or capabilities: crafted image + RW mount.
  • Reachability: mount-time fixup.

Proof of concept

Build & run

newfs image; mount RO; locate one PFS inode's parent blockref under the
super-root and clear its HAMMER2_BREF_FLAG_PFSROOT bit in the on-disk
indirect block; umount; mount RW

Expected output

console prints 'hammer2: Correct mis-flagged PFS <stale buffer bytes>'
during hammer2_fixup_pfses; under dio eviction pressure the bytes are
recycled buffer content.

Impact

Stale buffer-cache read to console; low fidelity.

Re-derive the pointer after the COW:

--- a/sys/vfs/hammer2/hammer2_vfsops.c
+++ b/sys/vfs/hammer2/hammer2_vfsops.c
@@ -2396,14 +2396,14 @@ hammer2_fixup_pfses(hammer2_dev_t *hmp)
        } else if ((chain->bref.flags &
                HAMMER2_BREF_FLAG_PFSROOT) == 0) {
            int error2;

-           ripdata = &chain->data->ipdata;
            hammer2_trans_init(hmp->spmp, 0);
            error2 = hammer2_chain_modify(chain,
                              chain->bref.modify_tid,
                              0, 0);
            if (error2 == 0) {
+               /* chain->data was COW'd, use the new buffer */
                kprintf("hammer2: Correct mis-flagged PFS %s\n",
-                   ripdata->filename);
+                   chain->data->ipdata.filename);
                chain->bref.flags |= HAMMER2_BREF_FLAG_PFSROOT;
            } else {
                error |= error2;

Timeline

  • 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2623 Β· 19 files
FileTypeDescriptionSize
forge_2623.py β€” 1.6 KB view raw
h2common.py β€” 3.8 KB view raw
run_2623.sh β€” 616 B view raw
run_stock.log β€” 238 B view raw
run_instr.log β€” 238 B view raw
console_excerpts.txt β€” 2.0 KB view raw
fix.diff β€” 565 B view raw
instr_build.log β€” 1.2 MB ↓ download
instr_build2.log β€” 5.6 MB ↓ download
instr_install.log β€” 80.4 KB view raw
fix_build.log β€” 5.6 MB ↓ download
fix_install.log β€” 80.4 KB view raw
env.txt β€” 505 B view raw
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
build.sh β€” 795 B view raw
run.sh β€” 211 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 5.1 KB view raw

DF-2623 β€” hammer2_fixup_pfses kprintf reads filename through pointer into the released (COW'd-away) buffer

What this pack contains

file what
forge_2623.py + h2common.py image forger: clears HAMMER2_BREF_FLAG_PFSROOT on the testvol PFS inode bref
run_2623.sh guest trigger (RW mount β†’ hammer2_fixup_pfses)
run_stock.log stock kernel #0 output (console message fires)
run_instr.log instrumented kernel #1 output
console_excerpts.txt stock + instrumented + fix-kernel console lines (pointer provenance proof)
fix.diff print from chain->data->ipdata.filename post-modify

Build

  1. Guest: newfs_hammer2 -L testvol 64M image + one small file (see mkbases_dfv.sh in the run's working dir), pull to host.
  2. Host: python3 forge_2623.py base2623.img craft2623.img (clears the PFSROOT bit in the sroot-blockset bref; sroot bref β†’ CHECK_NONE; volhdr CRCs recomputed).
  3. Push craft2623.img to guest.

Run (root, RW mount)

vnconfig -c vn0 craft2623.img
mount -t hammer2 /dev/vn0@testvol /mnt/h2x     # RW! fixup runs after recovery

Expected

  • Console: hammer2: Correct mis-flagged PFS testvol β€” printed from ripdata->filename where ripdata was captured before hammer2_chain_modify() COW'd the inode (the old dio reference is released inside modify at chain.c:1858 and chain->data redirected at :1859).
  • Instrumented kernel proves the read is through the stale pointer: ripdata=0xfffff80051cc6800 vs chain->data=0xfffff80051cc7000 (same=0) β€” while the content printed correctly on every attempt (the released buffer retains its bytes; no I/O lands in the window between the release and the kprintf).
  • Fixed kernel: message unchanged, now sourced from the always-valid post-COW chain->data.

Honest classification: the use-after-release read is confirmed (path, ordering, pointer provenance); observable stale content was never produced on this guest (bqrelse retains cached contents and the COW landed in the same 64KB window). Low severity as filed β€” correct.

VERDICT.md
↓ download raw

DF-2623 VERDICT

Status: reproduced (use-after-release read confirmed with pointer provenance; observable stale content NOT produced). Impact: none. Confidence: certain (mechanics).

Root cause (line-precise)

sys/vfs/hammer2/hammer2_vfsops.c:2399-2406   (hammer2_fixup_pfses)
        ripdata = &chain->data->ipdata;                 /* :2399 into DIO */
        hammer2_trans_init(hmp->spmp, 0);
        error2 = hammer2_chain_modify(chain,            /* :2401 COW */
                          chain->bref.modify_tid, 0, 0);
        if (error2 == 0) {
            kprintf("hammer2: Correct mis-flagged PFS %s\n",
                ripdata->filename);           /* :2405-06 UAR read */

hammer2_chain_modify() for a first-time-modified INODE chain takes the COW path (newmod=1, chain.c:1503-1522 β€” INODE chains never qualify for overwrite-in-place), allocates a new block, and then:

sys/vfs/hammer2/hammer2_chain.c:1797   io_bread(new data_off)         -> new dio
sys/vfs/hammer2/hammer2_chain.c:1827   bcopy(chain->data, bdata, bytes)
sys/vfs/hammer2/hammer2_chain.c:1858   hammer2_io_bqrelse(&tio);       /* OLD dio released */
sys/vfs/hammer2/hammer2_chain.c:1859   chain->data = (void *)bdata;    /* redirected */

ripdata still points into the old buffer after :1858; the kprintf at :2405 reads through it. Reachability: hammer2_fixup_pfses() runs on every first RW mount of a device (vfsops.c:1336-1338, after hammer2_recovery()) and on RO→RW remount (:1605); the kprintf arm requires a PFS inode bref under the sroot with PFSROOT cleared — trivially forged (forge_2623.py), and the very condition the function exists to repair ("earlier H2 implementations" bug).

Observed

  1. Stock kernel #0 (run_stock.log, console_excerpts.txt [A]): RW mount of the forged image prints hammer2: Correct mis-flagged PFS testvol; a second mount prints nothing (the fixup re-set the flag and flushed it β€” the repair semantics work).
  2. Instrumented kernel #1 (console_excerpts.txt [B]): DF2623: ripdata=0xfffff80051cc6800 vs chain->data=0xfffff80051cc7000 (same=0) name-by-old-ptr="testvol" name-by-new-ptr="testvol" β€” the kprintf's source pointer is not the chain's data anymore; the old dio's reference was dropped inside modify. On this run the COW allocated the new block in the same 64KB window (0x800 apart) and the old buffer still holds the bcopy'd content, so the name printed correctly.
  3. Repeated attempts never produced garbage: hammer2_io_bqrelse (io.c:676 β†’ putblk) returns the buffer to the cache with its data intact, and nothing issues I/O into that buffer between :1858 and :2405 within the same call chain.

Honest classification

The read-after-release is real and demonstrated (ordering + pointer provenance, instrumented). The claimed worse face β€” printing recycled buffer content β€” did not manifest: the release window is sub-microsecond, the buffer cache retains released contents, and recycling requires unrelated I/O to land in that exact window. Impact: none observable; a theoretical info-leak-to-console (root console) at worst. Low severity as filed β€” correct.

Fix

            kprintf("hammer2: Correct mis-flagged PFS %s\n",
-               ripdata->filename);
+               chain->data->ipdata.filename);

The COW copied the inode data (chain.c:1827), so the post-modify chain->data->ipdata.filename holds the identical string in memory the chain still references.

Fix validation (kernel #2)

Freshly re-forged image, RW mount: hammer2: Correct mis-flagged PFS testvol prints once (identical text, now from the valid buffer), second mount silent, directory lists normally (console_excerpts.txt [C]).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (single-line source change) included in kernel #2 (fix_build.log). Freshly re-forged image, RW mount: 'hammer2: Correct mis-flagged PFS testvol' prints once with the correct name from the post-COW chain->data; second mount silent; directory lists normally. The kprintf can no longer read through a released buffer because its argument is the chain's current data by construction.

['fix.diff', 'console_excerpts.txt [C]', 'fix_build.log / fix_install.log']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Sat Aug 29 00:17:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run_stock.log - stock kernel: first RW mount prints 'hammer2: Correct mis-flagged PFS testvol'; directory lists; second mount silent (repair flushed)", 'console_excerpts.txt [B] - instrumented kernel: DF2623: ripdata=0xfffff80051cc6800 vs chain->data=0xfffff80051cc7000 (same=0), names equal - read-through-released-pointer proven with content intact', 'console_excerpts.txt [A]/[C] - stock and fix kernels both print the message once per freshly-forged image', 'forge_2623.py output - PFS bref flags 0x1 -> 0x0 (PFSROOT cleared), sroot CHECK_NONE, volhdr CRCs recomputed']

PoC changes

No seed code. Needed three passes at the image: the fixup REPAIRS the flag and flushes it to the vn-backed file on first RW mount, so the image must be re-forged from base before each kernel's run (documented in run.sh/VERDICT). An unrelated latent bug noticed while tracing (out of scope, not filed here): the 'chain->bref.type != INODE -> continue' at vfsops.c:2390-2391 does not advance the iterator - a non-INODE chain directly under the sroot would spin forever in this loop.

Verified recommended fix

Print the name from chain->data->ipdata.filename (post-COW, always valid) instead of the pre-modify ripdata pointer in hammer2_fixup_pfses.

Verdict

The cited use-after-release READ is confirmed end-to-end; the claimed worse face (printing recycled buffer content) did NOT manifest, honestly recorded. Trace: hammer2_fixup_pfses runs on every first RW mount (vfsops.c:1336-1338); with the testvol PFS inode bref's PFSROOT bit cleared (forge_2623.py), the kprintf arm at :2404-2406 executes. ripdata is captured at :2399 pointing into the DIO buffer; hammer2_chain_modify COWs the inode (chain.c:1797 io_bread of the new block, :1827 bcopy, :1858 hammer2_io_bqrelse of the OLD dio, :1859 chain->data redirected), then the kprintf reads ripdata->filename through the released buffer. Instrumented kernel proof: 'DF2623: ripdata=0xfffff80051cc6800 vs chain->data=0xfffff80051cc7000 (same=0) name-by-old-ptr="testvol" name-by-new-ptr="testvol"' - the print source is no longer the chain's data. Content stayed intact on every attempt: bqrelse returns the buffer to the cache with data preserved, the COW landed in the same 64KB window on the observed run, and no I/O can land between :1858 and the kprintf within the same call chain - so no observable stale bytes, no panic, no userland disclosure (console message is privileged context anyway). Low severity as filed is right: a latent wrong-pointer read with a sub-microsecond recycling race as the only theoretical harm. fix.diff prints from chain->data->ipdata.filename (the COW copy, always valid); on kernel #2 the message prints identically and the fixup semantics (flag repair + flush, silent second mount) are unchanged.