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.usermounton an RW device) of a crafted image containing a PFS inode whose bref lacksHAMMER2_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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2623 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
- Cited:
sys/vfs/hammer2/hammer2_vfsops.c:2399-2406
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
- Guest:
newfs_hammer2 -L testvol64M image + one small file (seemkbases_dfv.shin the run's working dir), pull to host. - 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). - Push
craft2623.imgto 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 fromripdata->filenamewhereripdatawas captured beforehammer2_chain_modify()COW'd the inode (the old dio reference is released inside modify at chain.c:1858 andchain->dataredirected 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.
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
- Stock kernel #0 (
run_stock.log,console_excerpts.txt[A]): RW mount of the forged image printshammer2: Correct mis-flagged PFS testvol; a second mount prints nothing (the fixup re-set the flag and flushed it β the repair semantics work). - 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. - 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
fixedfix.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']
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.
No comments yet.