NULL parent->data dereference in hammer2_bulkfree_scan's PFSROOT kprintfs when a PFS-root chain fails to load with a non-CHECK error
| Field | Value |
|---|---|
| ID | DF-2649 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/vfs/hammer2/hammer2_bulkfree.c |
| Lines | 135 vs 143-147, 393-398 |
| Area | vfs |
| Confidence | likely |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer2_bulkfree_scan locks each recursable chain with RESOLVE_ALWAYS
and then guards only HAMMER2_ERROR_CHECK (bulkfree.c:135) before
unconditionally dereferencing parent->data->ipdata.filename for
PFSROOT inodes in its two kprintf()s (:143-147 and :393-398).
hammer2_chain_load_data leaves chain->data == NULL with
error = EIO when bread() fails (chain.c:1003-1008), and
hammer2_chain_scan itself refuses such parents (chain.c:2876-2879) β
the bulkfree prints are the sole unguarded consumers. A PFS-root inode
on unreadable media (real bad sector) therefore turns a routine
bulkfree into a kernel page fault (va ~0x100) instead of an error
report.
Threat model & preconditions
Kernel panic (availability) whenever a bulkfree pass β run by root via
hammer2(8) or by any local user via the ungated HAMMER2IOC_BULKFREE_SCAN
(DF-0815) β encounters an unreadable PFS-root inode on failing media.
Robustness defect rather than a forged-image corruption primitive
(mount-time volu_size <= media_size checks close the forged route).
Proof of concept
Guest verification reached the exact crash precondition (unprivileged ungated ioctl driving the scan into chain_lock β load_data β _hammer2_io_bread on a forged PFSROOT chain; panic_getvolume.txt backtrace), but the final EIO link could not be materialized: hammer2 enforces volu_size β€ media_size at mount (ondisk.c:283-290) so every in-window read succeeds on vn media, and out-of-volume offsets panic earlier in hammer2_get_volume (already filed as DF-0875). Honest classification: not_reproduced; the defect is line-proven and the reachability demonstrated; triggering requires real unreadable media. Evidence: findings/poc/DF-2649/.
Recommended fix
--- a/sys/vfs/hammer2/hammer2_bulkfree.c
+++ b/sys/vfs/hammer2/hammer2_bulkfree.c
@@ -138,6 +138,19 @@
}
/*
+ * DF-2649: a pure I/O error while resolving the parent leaves
+ * chain->data == NULL with a non-CHECK error (and a zero data_off
+ * can leave it NULL with no error at all). We can neither
+ * traverse nor print such a chain - its block table was never
+ * read - so skip it instead of dereferencing parent->data in the
+ * PFSROOT kprintf()s below.
+ */
+ if (parent->data == NULL) {
+ error = parent->error | HAMMER2_ERROR_EIO;
+ goto done;
+ }
+
+ /*
* Report which PFS is being scanned
*/
Validated to compile/boot in the rebuilt kernel (no regression); behavior not observable in-guest (fix_status: not_testable).
References
- chain.c:1003-1008 (error state), chain.c:2876-2879 (the working guard in chain_scan), DF-0875 (the earlier get_volume panic)
Timeline
- 2026-08-29 Discovered during pass-2 audit of hammer2_bulkfree.c (GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2649 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| forge_2649.py | β | 3.7 KB | view raw | |
| h2common.py | β | 3.8 KB | view raw | |
| trigger.c | β | 1.2 KB | view raw | |
| build.sh | β | 1.8 KB | view raw | |
| run.sh | β | 813 B | view raw | |
| craft2649_v1.img | β | 64.0 MB | β download | |
| craft2649_volusize.img | β | 64.0 MB | β download | |
| panic_getvolume.txt | β | 1.4 KB | view raw | |
| mount_reject.log | β | 817 B | view raw | |
| env.txt | β | 190 B | view raw | |
| fix.diff | β | 680 B | view raw | |
| verdict.json | β | 4.6 KB | view raw | |
| manifest.json | β | 1.2 KB | view raw |
Fix verification
not_testablefix.diff compiled and booted in the validation kernel (see DF-2650 fix_build.log / fix_run.log for the shared build); the triggering condition (in-volume bread EIO) cannot be produced on the QEMU guest, so the guard's behavior change is not observable there; no regression on healthy media.
['DF-2650/fix_build.log', 'DF-2650/fix_run.log']
Confirmed kernel references
Detail
Evidence (decisive lines)
['panic_getvolume.txt: unprivileged run on craft2649_v1.img -> panic: no volume for offset 0x100000000 with backtrace hammer2_get_volume <- _hammer2_io_getblk <- _hammer2_io_bread <- hammer2_chain_load_data <- hammer2_chain_lock (proves the scan recursed into and locked the forged PFSROOT chain; DF-0875 manifestation)', "mount_reject.log: volu_size-inflated variant rejected at mount ('size 0x8000000 exceeds device size 0x4000000', hammer2_ondisk.c:283-290) - closes the forged-image route to an in-volume EIO", 'VERDICT.md: full 3-attempt narrative incl. the key-sorting discovery (hammer2_base_find requires sorted blockset slots)', 'fix.diff: parent->data == NULL guard (covers EIO and zero-data_off chains at both kprintf sites)']
PoC changes
Seed concept extended: poison bref placed in the mounted PFS's own root-inode blockset (mount never walks it, bulkfree does); key must sort above the dirent key; out-of-volume data_off manifests DF-0875 before the EIO can be observed; volu_size inflation is blocked by the mount-time device-size cross-check (whose DIOCGPART-failure skip is itself an ondisk.c-scope gap, not re-filed here).
Verified recommended fix
In hammer2_bulkfree_scan(), skip the chain (goto done with error |= EIO) when parent->data == NULL after the CHECK guard, before the PFSROOT kprintf at :143-147; the same guard protects :393-398.
Verdict
Code-proven NULL dereference not materializable on the QEMU guest. hammer2_bulkfree_scan guards only HAMMER2_ERROR_CHECK (bulkfree.c:135) before unconditionally dereferencing parent->data->ipdata.filename for PFSROOT inodes in its two kprintf()s (bulkfree.c:143-147 and :393-398); hammer2_chain_load_data leaves chain->data == NULL with error = EIO when bread() fails (hammer2_chain.c:1003-1008). Reachability of the crash precondition was demonstrated: a forged PFSROOT INODE bref in the mounted PFS's root-inode blockset drives the unprivileged (ungated, DF-0815) bulkfree scan into hammer2_chain_lock -> hammer2_chain_load_data -> _hammer2_io_bread on the poison chain (panic_getvolume.txt backtrace). But an in-window failing read cannot be produced on a validly mounted volume: hammer2 enforces volu_size <= media_size at mount (hammer2_ondisk.c:283-290; the inflated-volu_size variant is rejected, mount_reject.log), and out-of-volume offsets panic earlier in hammer2_get_volume (already filed as DF-0875). The triggering condition therefore requires real unreadable media (failing disk sector under a PFS-root inode), turning a routine bulkfree into a kernel panic instead of an error report. Not reproduced per the honest-status table; the finding stands on line-proven analysis plus demonstrated reachability up to the exact load site.
No comments yet.