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

NULL-pointer dereference in hammer2_freemap_adjust() when the freemap leaf is absent β€” mount-time panic from crafted image

Field Value
ID DF-2651
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-476 NULL Pointer Dereference
File sys/vfs/hammer2/hammer2_freemap.c
Lines 1013-1021 (create path :1037 proves NULL was meant to be handled)
Area vfs
Confidence certain
Discovered 2026-08-29
Pass 2 (GLM 5.3 second pass)
Bucket hammer2
Reported pending
Known CVE none
CVE match novel

Summary

hammer2_freemap_adjust() tests chain->error at freemap.c:1021 before ruling out chain == NULL. The NULL guard at :1016 (chain == NULL && how != DORECOVER) is structurally dead because how is KKASSERTed == HAMMER2_FREEMAP_DORECOVER at :972 (the only live mode; the free paths are #if 0). When the on-disk freemap topology has no FREEMAP_LEAF covering the bref's 1GB region (crafted image with blanked freemap_blockset), the mount-time recovery scan (vfsops.c:2234/:2325) calls adjust(DORECOVER), the lookup returns NULL, and the kernel page-faults reading NULL+0x170 (offsetof(hammer2_chain_t, error)). INVARIANTS-independent β€” no assertion is involved.

Threat model & preconditions

Crafted hammer2 image (blanked volume-header freemap_blockset + mirror_tids above freemap_tid); any mount (root, vfs.usermount=1, auto-mounter) takes a deterministic fatal trap 12 during mount. Ceiling is DoS (page 0 unmapped on amd64). The dedup re-registration path (chain.c:1627) reaches the same deref.

Proof of concept

VERIFIED on the guest (findings/poc/DF-2651/): forge_2651.py (blanks freemap_blockset, bumps sroot+testvol bref mirror_tids 0x11β†’0x20, CHECK_NONE ancestors, volhdr CRC32C recompute) β†’ vnconfig + mount β†’ Fatal trap 12 ... fault virtual address = 0x170 ... Stopped at hammer2_freemap_adjust+0xce: movl 0x170(%rax),%r9d. Baseline panic on stock #0; patched kernel #1 mounts the same image cleanly. No uid=0 route β€” NULL+const read on unmapped page 0.

--- a/sys/vfs/hammer2/hammer2_freemap.c
+++ b/sys/vfs/hammer2/hammer2_freemap.c
@@ -1013,13 +1013,17 @@
    /*
     * Stop early if we are trying to free something but no leaf exists.
     */
-   if (chain == NULL && how != HAMMER2_FREEMAP_DORECOVER) {
-       kprintf("hammer2_freemap_adjust: %016jx: no chain\n",
-           (intmax_t)bref->data_off);
-       goto done;
-   }
-   if (chain->error) {
+   /*
+    * DF-2651: chain can legitimately be NULL here (missing freemap
+    * leaf); only DORECOVER creates it further down.
+    */
+   if (chain == NULL) {
+       if (how != HAMMER2_FREEMAP_DORECOVER) {
+           kprintf("hammer2_freemap_adjust: %016jx: no chain\n",
+               (intmax_t)bref->data_off);
+           goto done;
+       }
+   } else if (chain->error) {
        kprintf("hammer2_freemap_adjust: %016jx: error %s\n",

(only test chain->error after ruling out NULL so DORECOVER falls through to the existing chain_create at :1037)

Timeline

  • 2026-08-29 Discovered during pass-2 audit of hammer2_freemap.c (GLM 5.3); verified reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2651 Β· 15 files
FileTypeDescriptionSize
forge_2651.py β€” 2.5 KB view raw
h2common.py β€” 3.8 KB view raw
base2651.img.gz β€” 2.1 MB ↓ download
forge2651.img.gz β€” 2.1 MB ↓ download
panic.txt β€” 754 B view raw
panic2651_full.txt β€” 2.8 KB view raw
run.log β€” 1.1 KB view raw
env.txt β€” 195 B view raw
fix.diff β€” 896 B view raw
fix_all_three_findings.diff β€” 2.2 KB view raw
fix_build.log β€” 254 B view raw
fix_run.log β€” 773 B view raw
build.sh β€” 299 B view raw
run.sh β€” 551 B view raw
verdict.json β€” 4.3 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff reorders the NULL handling in hammer2_freemap_adjust(): on kernel #1 the exact PoC image that fatal-faulted (movl 0x170(%rax)) on #0 mounts cleanly, files are created/read, unmount is clean, guest stays up. Baseline panic gone; no regression on clean images.

fix_run.log (MOUNT_OK + probe file round-trip on #1); fix_build.log (nativekernel BUILD_RC=0, installkernel INSTALL_RC=0, uname #1); panic.txt for the #0 baseline
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Aug 30 01:17:54 UTC 2026 root@dfbd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted hammer2 image (blanked freemap_blockset + bumped mirror_tids + recomputed volhdr CRC32C) -> victim mounts (root / vfs.usermount=1 / automounter) -> hammer2_recovery_scan -> hammer2_freemap_adjust(DORECOVER) -> chain==NULL -> read chain->error at freemap.c:1021 -> kernel page fault -> panic (DoS)

Evidence (decisive lines)

["panic.txt: 'Stopped at hammer2_freemap_adjust+0xce: movl 0x170(%rax),%r9d', fault VA 0x170 == offsetof(hammer2_chain_t, error)", 'run.log: full fatal-trap block from serial console; guest down', 'forge_2651.py: the three edits (blank freemap_blockset, mtid 0x11->0x20 on sroot+testvol brefs, CHECK_NONE ancestors, volhdr CRC recompute)', 'fix_run.log: patched kernel #1 mounts the same image with MOUNT_OK and stays up']

PoC changes

Forge built on DF-2650 tooling (h2common.py). Two iterations needed: first run mounted cleanly because only the in-sroot-block testvol bref was bumped - recovery never recursed since the volhdr sroot_blockset bref (the recursion gate) still had mirror_tid 0x11; fixed by bumping the volhdr sroot bref too. Guest /mnt/h2t had to pre-exist before vnconfig (set -e aborted runs otherwise).

Verified recommended fix

In hammer2_freemap_adjust(), only test chain->error after ruling out chain == NULL; let DORECOVER fall through to the existing chain_create at :1037.

Verdict

REPRODUCED on stock INVARIANTS kernel #0: mounting a crafted hammer2 image whose volume header freemap_blockset is blanked (no FREEMAP_LEAF for any 1GB region) and whose sroot/testvol brefs carry mirror_tid > freemap_tid makes the mount-time recovery scan call hammer2_freemap_adjust(DORECOVER) (vfsops.c:2234); the freemap lookup returns NULL, the guard at freemap.c:1016 is structurally dead (how is KKASSERTed == DORECOVER at :972), and the unconditional 'if (chain->error)' at freemap.c:1021 reads NULL+0x170 -> Fatal trap 12, guest down at db>. INVARIANTS-independent (no assertion involved). Ceiling is panic/DoS from crafted-image mount (page 0 unmapped on amd64); not escalatable to code execution. Fixed by reordering the NULL handling so DORECOVER falls through to the chain_create at :1037; validated on rebuilt kernel #1: same image mounts cleanly, files usable, guest stays up.