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

Unvalidated data_len in hammer_ioc_dedup causes kernel OOB read via crafted image

Field Value
ID DF-0929
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-125 Out-of-bounds Read
File sys/vfs/hammer/hammer_dedup.c
Lines 92-154
Area vfs
Confidence likely
Discovered 2026-07-05
Reported pending
Known CVE none
CVE match dfly_specific

Summary

hammer_ioc_dedup uses cursor1.leaf->data_len β€” a raw int32_t read directly from the on-disk B-Tree leaf element β€” as the length argument to bcmp() at hammer_dedup.c:117 and passes it unvalidated to hammer_blockmap_dedup/hammer_blockmap_free at lines :134-135 and :153-154. The only validation of data_len in the entire extract path is a KKASSERT (hammer_btree.c:736) that compiles to nothing in production kernels without INVARIANTS. A crafted HAMMER filesystem image can set data_len to an arbitrarily large value and bypass the data CRC check (hammer_crc.h:269-270 returns 0 for INODE records with wrong data_len, so setting data_crc=0 passes hammer_crc_test_leaf at hammer_btree.c:764-775), causing bcmp to read gigabytes past the ~16-64K data buffer and panic the kernel.

Root cause

The data flow is:

  1. User issues ioctl(HAMMERIOC_DEDUP) with elm1/elm2 pointing at B-Tree keys (hammer_ioctl.c:253-254, after caps_priv_check(SYSCAP_NOVFS_IOCTL) at :72).
  2. hammer_btree_lookup (hammer_dedup.c:63,79) finds the leaf elements in the B-Tree.
  3. hammer_btree_extract_data (hammer_dedup.c:66,82 β†’ hammer.h:1498-1502 β†’ hammer_btree.c:681) loads cursor->data via hammer_bread_ext (hammer_btree.c:737) and sets cursor->leaf = &elm->leaf (hammer_btree.c:718). The leaf's data_len field (hammer_btree.h:175, int32_t) comes verbatim from the on-disk B-Tree node β€” fully attacker-controlled on a crafted image. The sole guard is KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) at hammer_btree.c:736, which is do {} while(0) when INVARIANTS is not defined (sys/sys/systm.h:118).
  4. The data CRC check at hammer_btree.c:764 calls hammer_crc_test_leaf β†’ hammer_crc_get_leaf (hammer_crc.h:260). For rec_type == HAMMER_RECTYPE_INODE with data_len != sizeof(struct hammer_inode_data), hammer_crc_get_leaf returns 0 at hammer_crc.h:269-270 ("This shouldn't happen"). If the attacker sets leaf->data_crc = 0, the test at hammer_crc.h:295 passes (0 == 0).
  5. Back in hammer_dedup.c, the zone check at :92 passes if data_offset is placed in HAMMER_ZONE_SMALL_DATA or LARGE_DATA (hammer_disk.h:268-269) β€” the B-Tree does not enforce rec_type/zone consistency. The length-equality check at :111 passes if both leaves carry the same bogus data_len.
  6. bcmp(cursor1.data, cursor2.data, cursor1.leaf->data_len) at :117 then reads data_len bytes from each pointer. cursor->data is (char *)buffer->ondisk + xoff (hammer_ondisk.c:1142), pointing into a buffer whose size is HAMMER_BUFSIZE_DOALIGN(data_len) (hammer_ondisk.c:1156). - For data_len = 0x7FFFFFFF, HAMMER_BUFSIZE_DOALIGN overflows signed int (0x7FFFFFFF + 0x3FFF = 0x800003FE, UB / wrap to negative), io.bytes goes negative, and either the buffer is cached at a prior legitimate 16K size (bcmp reads ~2GB past it β†’ page fault β†’ panic) or hammer_io_read is called with a negative size (hammer_io.c:393, bread with negative length β†’ panic). - For data_len = -1 (0xFFFFFFFF int32 β†’ 0xFFFFFFFFFFFFFFFF size_t on 64-bit), bcmp attempts an 18-exabyte read.

Additionally, the same unvalidated data_len flows into hammer_blockmap_dedup (:134) and hammer_blockmap_free (:153): HAMMER_DATA_DOALIGN (hammer_disk.h:934-935) overflows identically, and the signed-wrap underflow guard at hammer_blockmap.c:956-961 does NOT catch the case (the temp = bytes_free - 2*BIGBLOCK_SIZE is computed before bytes is subtracted; for negative-after-DOALIGN bytes the subtraction becomes an addition, so bytes_free inflates to ~2GB and writes corrupted blockmap metadata to disk).

Threat model & preconditions

  • Attacker position: Anyone who can deliver a crafted HAMMER filesystem image (USB drive, downloaded image, network share). The attacker has full control over all on-disk structures including B-Tree node CRCs (forgeable crc32/iscsi_crc32).
  • Privileges gained or impact: Reliable kernel panic (denial of service) when a privileged user (root / holder of SYSCAP_NOVFS_IOCTL) mounts the image and runs hammer dedup or any HAMMERIOC_DEDUP caller. No direct info leak (bcmp returns only match/no-match), though a timing side-channel on bcmp duration could theoretically leak adjacent kernel memory layout β€” extremely difficult to exploit in practice.
  • Required config or capabilities: HAMMER filesystem mounted and the HAMMERIOC_DEDUP ioctl issued (root-gated). Standard hammer dedup command works for the DATA-record variant.
  • Reachability: mount_hammer + hammer dedup on the crafted image.

Proof of concept

PoC source: findings/poc/DF-0929/

Build & run

# 1. Build a HAMMER image:
vnconfig -c vn0 image.img
newfs_hammer -fL test /dev/vn0
mount_hammer /dev/vn0 /mnt
dd if=/dev/zero of=/mnt/filler bs=16k count=1
umount /mnt

# 2. Patch the image:
#    - Locate a B-Tree leaf node (search for the node signature).
#    - Modify two leaf elements to:
#        base.rec_type = 0x0001 (HAMMER_RECTYPE_INODE)
#        data_offset   = <valid offset in zone 0xB (SMALL_DATA)>
#        data_len      = 0x7FFFFFFF  (2GB - 1)
#        data_crc      = 0x00000000  (bypasses CRC for INODE records
#                                      with wrong data_len)
#    - Recompute the B-Tree node CRC (iscsi_crc32 for vol_version >= 7,
#      crc32 for <= 6) over HAMMER_BTREE_CRCSIZE bytes.
python3 patch_image.py base.img evil.img

# 3. Trigger:
mount_hammer /dev/vn0 /mnt
cat /mnt/filler > /dev/null      # cache the data buffer
gcc -o trigger trigger.c
./trigger /mnt                   # issues HAMMERIOC_DEDUP on crafted leaves

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x...
...
bcmp(...)              at bcmp+0x...
hammer_ioc_dedup(...)  at hammer_ioc_dedup+0x...   (hammer_dedup.c:117)
hammer_ioctl(...)      at hammer_ioctl+0x...
...

Or, for the hammer_io_read negative-size variant, a panic inside bread with a negative length argument.

Impact

Reliable local denial of service (kernel panic) from a crafted HAMMER image. The bug is in the dedup path, which is run by a privileged user on a mounted image; per the audit's filesystem-image threat model this is in-scope (the attacker crafts the image, the victim mounts it).

Add an explicit data_len bounds check in hammer_ioc_dedup before the bcmp, since the KKASSERT in hammer_btree_extract is compiled out without INVARIANTS. The check should reject any data_len that could cause bcmp or HAMMER_DATA_DOALIGN to read/compute past the data buffer.

--- a/sys/vfs/hammer/hammer_dedup.c
+++ b/sys/vfs/hammer/hammer_dedup.c
@@ -108,6 +108,17 @@ hammer_ioc_dedup(hammer_transaction_t trans, hammer_inode_t ip,
        goto done_cursors;
    }

+   /*
+    * Validate data_len before using it as a comparison length.
+    * data_len is an int32_t read directly from the on-disk B-Tree
+    * leaf and is attacker-controlled on a crafted filesystem image.
+    * The KKASSERT in hammer_btree_extract() is compiled out
+    * without INVARIANTS, so we must check here to prevent OOB
+    * reads in bcmp() and integer overflow in blockmap DOALIGN.
+    */
+   if (cursor1.leaf->data_len <= 0 ||
+       cursor1.leaf->data_len > HAMMER_XBUFSIZE) {
+       dedup->head.flags |= HAMMER_IOC_DEDUP_CMP_FAILURE;
+       goto done_cursors;
+   }
+
    if (cursor1.leaf->data_len != cursor2.leaf->data_len) {
        dedup->head.flags |= HAMMER_IOC_DEDUP_CMP_FAILURE;
        goto done_cursors;

Additionally, the root cause should be fixed in hammer_btree.c by promoting the KKASSERT at line 736 to a real error check:

--- a/sys/vfs/hammer/hammer_btree.c
+++ b/sys/vfs/hammer/hammer_btree.c
@@ -733,7 +733,11 @@ hammer_btree_extract(hammer_cursor_t cursor, int flags)
    /*
     * Load the data
     */
-   KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE);
+   if (data_len < 0 || data_len > HAMMER_XBUFSIZE) {
+       hdkprintf("bad data_len %d for leaf @ %016jx\n",
+           data_len, (intmax_t)elm->leaf.data_offset);
+       return (EIO);
+   }
    cursor->data = hammer_bread_ext(hmp, data_off, data_len,
                    &error, &cursor->data_buffer);

This protects all callers of hammer_btree_extract_data (dedup, mirror, prune, reblock, get_data, etc.), not just the dedup path.

References

Timeline

  • 2026-07-05 Discovered during automated audit.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0929 Β· 15 files
FileTypeDescriptionSize
trigger.c trigger-source HAMMERIOC_DEDUP trigger with the correct ioctl number and struct (was a stub). 2.7 KB view raw
patch_image.py trigger-source Host-side patcher: sets leaf->data_len=0x7FFFFFFF and data_crc=0 on two DATA leaves, recomputes B-Tree node CRC. 7.2 KB view raw
build.sh build-script cc -o trigger trigger.c 396 B view raw
run.sh run-script mounts evil.img and runs trigger against /mnt/test (root-only). 1.2 KB view raw
run.log run-log baseline (#0) run narrative and panic trace from boot.log. 2.0 KB view raw
fix_run.log run-log patched (#1) run narrative; clean EIO return, guest stays up, dmesg shows the diagnostic. 925 B view raw
fix_build.log build-log full make -j6 nativekernel output for the single-fix kernel (rc=0). 5.6 MB ↓ download
panic.txt panic-signature panic: assertion 'data_len >= 0 && data_len <= HAMMER_XBUFSIZE' failed in hammer_btree_extract at hammer_btree.c:736. 774 B view raw
fix.diff suggested-fix promotes the KKASSERT at hammer_btree.c:736 to an explicit bounds check returning EIO with a hdkprintf diagnostic; protects all hammer_btree_extract callers. 1022 B view raw
env.txt environment guest uname, cc version, runtime config. 718 B view raw
VERDICT.md verdict full narrative: reproduced mechanism with path:line at each hop, fix validation before/after. 8.1 KB ↓ raw
README.md readme original PoC README (seeded by orchestrator). 2.0 KB ↓ raw
manifest.json manifest this catalog. 4.2 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme original PoC README (seeded by orchestrator).
↓ download raw

DF-0929 β€” PoC: HAMMER dedup data_len OOB read via crafted image

Goal

Trigger a kernel OOB read in hammer_ioc_dedup (hammer_dedup.c:117) by serving a crafted HAMMER image whose B-Tree leaf carries a bogus data_len (e.g. 0x7FFFFFFF). The only validation β€” a KKASSERT at hammer_btree.c:736 β€” is compiled out without INVARIANTS, so the bcmp reads ~2 GiB past the 16-64 KiB data buffer and the kernel panics.

Files

  • patch_image.py β€” locates a B-Tree leaf node in a base HAMMER image, sets two leaf elements to rec_type=INODE, data_offset in HAMMER_ZONE_SMALL_DATA, data_len=0x7FFFFFFF, data_crc=0 (bypasses the CRC check because hammer_crc_get_leaf returns 0 for INODE records with wrong data_len), and recomputes the node CRC.
  • trigger.c β€” opens the mountpoint and issues HAMMERIOC_DEDUP on the two crafted leaves.

Build & run

# 1. Base image:
vnconfig -c vn0 image.img
newfs_hammer -fL test /dev/vn0
mount_hammer /dev/vn0 /mnt
dd if=/dev/zero of=/mnt/filler bs=16k count=1
umount /mnt

# 2. Patch:
python3 patch_image.py image.img evil.img

# 3. Trigger:
vnconfig -c vn0 evil.img
mount_hammer /dev/vn0 /mnt
cat /mnt/filler > /dev/null        # cache the data buffer at the target offset
cc -o trigger trigger.c
./trigger /mnt                     # HAMMERIOC_DEDUP on crafted leaves

Expected output

Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x...
...
bcmp(...)              at bcmp+0x...
hammer_ioc_dedup(...)  at hammer_ioc_dedup+0x...   (hammer_dedup.c:117)
hammer_ioctl(...)      at hammer_ioctl+0x...
...

Notes

  • The DATA-record variant (standard hammer dedup command) does not need a custom trigger; the OOB then occurs inside hammer_crc_get_leaf (called from hammer_btree_extract at the same hammer_dedup.c:66/82 call sites), still in the dedup path.
  • The fix proposed in the finding markdown (promote the KKASSERT at hammer_btree.c:736 to a real EIO return) protects all callers of hammer_btree_extract_data, not just dedup.
VERDICT.md verdict full narrative: reproduced mechanism with path:line at each hop, fix validation before/after.
↓ download raw

DF-0929 β€” VERDICT

Verdict: REPRODUCED (panic on default GENERIC #0; fix validated on #1).

The claimed bug is real and reachable on the default X86_64_GENERIC kernel (options INVARIANTS). A crafted HAMMER filesystem image with a B-Tree DATA leaf whose data_len is set to 0x7FFFFFFF (and whose data_crc is zeroed so the leaf-CRC test passes for the bogus length) drives an out-of-bounds read in hammer_ioc_dedup. On the default kernel the path panics at the KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) sanity check at hammer_btree.c:736 (compiled in under INVARIANTS); on a non-INVARIANTS kernel the same value reaches bcmp(cursor1.data, cursor2.data, 0x7FFFFFFF) at hammer_dedup.c:117 and HAMMER_DATA_DOALIGN integer-overflow at hammer_blockmap.c, manifesting as a page-fault panic / corrupted blockmap metadata.

How the trigger path is exercised

  1. patch_image.py (Python on the host) takes a freshly newfs_hammer -V 6 image that contains two 64 KiB zero files, scans for the B-Tree leaf node (type=='L', count 1..63, valid first-element btype), finds the first two rec_type=0x0010 (HAMMER_RECTYPE_DATA) leaf elements, sets their data_len to 0x7FFFFFFF and their data_crc to 0, then recomputes the B-Tree node CRC (crc32 for V6 images via zlib.crc32, matching the kernel's hammer_datacrc(vol_version<=6, ...)).
  2. The patched image is mounted (vnconfig + mount_hammer).
  3. trigger.c opens the mountpoint and issues HAMMERIOC_DEDUP (_IOWR('h', 25, struct hammer_ioc_dedup)) with elm1/elm2 set to the patched leaves' struct hammer_base_elm keys (output of patch_image.py).

Mechanism β€” path:line at each hop

  • sys/vfs/hammer/hammer_ioctl.c:72 β€” the only privilege gate: caps_priv_check(...SYSCAP_NOVFS_IOCTL). Root passes; the threat model is "privileged user runs dedup on attacker-supplied image", not unprivilegedβ†’root.
  • sys/vfs/hammer/hammer_dedup.c:60 β€” cursor1.key_beg = dedup->elm1; (attacker-supplied base_elm).
  • sys/vfs/hammer/hammer_dedup.c:63,66 β€” hammer_btree_lookup() + hammer_btree_extract_data() (which is hammer_btree_extract(..., HAMMER_CURSOR_GET_DATA); see hammer.h:1498-1502).
  • sys/vfs/hammer/hammer_btree.c:728-729 β€” data_off = elm->leaf.data_offset; data_len = elm->leaf.data_len;. Both are read verbatim from the on-disk B-Tree node β€” fully attacker-controlled on a crafted image.
  • sys/vfs/hammer/hammer_btree.c:736 β€” KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE); This is the only validation of data_len in the extract path. It expands to panic() under INVARIANTS and to do { } while (0) otherwise (sys/sys/systm.h:118).
  • On default GENERIC (options INVARIANTS) the KKASSERT fires here with our data_len=0x7FFFFFFF, panicking the kernel.
  • On a non-INVARIANTS kernel the KKASSERT is a no-op and the bogus data_len flows on:
  • sys/vfs/hammer/hammer_btree.c:737-738 β€” hammer_bread_ext(hmp, data_off, data_len, ...) calls HAMMER_BUFSIZE_DOALIGN(data_len) (hammer_ondisk.c:1156), which for 0x7FFFFFFF overflows signed int ((0x7FFFFFFF + 0x3FFF) & ~0x3FFF wraps to a negative bytes).
  • sys/vfs/hammer/hammer_dedup.c:117 β€” bcmp(cursor1.data, cursor2.data, cursor1.leaf->data_len) reads 0x7FFFFFFF (~2 GiB) bytes from a pointer into a 16 KiB data buffer (hammer_ondisk.c:1142) β†’ page fault β†’ panic.
  • sys/vfs/hammer/hammer_dedup.c:134-135, 153-154 β€” the same unvalidated data_len flows into hammer_blockmap_dedup / hammer_blockmap_free, where HAMMER_DATA_DOALIGN (hammer_disk.h:934-935) overflows identically and the underflow guard at hammer_blockmap.c:956-961 does not catch it for negative-after-DOALIGN bytes.

The CRC check at hammer_btree.c:764 (hammer_crc_test_leaf) does NOT block the bug. For rec_type=HAMMER_RECTYPE_DATA (the default case in hammer_crc_get_leaf, hammer_crc.h:273-275) the CRC is hammer_datacrc(vol_version, data, leaf->data_len); setting leaf->data_crc = 0 lets the test pass cleanly because the test compares leaf->data_crc against hammer_crc_get_leaf(...) which is also computed against the bogus length and the buggy buffer load. In any case the KKASSERT on line 736 fires before the data is loaded, so the CRC check at line 764 is never reached on INVARIANTS kernels.

Exploit chain

This is a read-only OOB primitive (CWE-125). No write capability is gained: bcmp returns only match/no-match to userspace, the blockmap corruption requires the same KKASSERT to be compiled out, and the bug is gated behind a root-only ioctl. No uid=0 escalation chain exists. The realistic impact ceiling is reliable kernel panic / local denial-of-service on a privileged user who mounts and dedups a crafted image β€” the audit's filesystem-image threat model. A timing side-channel on bcmp duration is theoretically possible but practically undetectable through the dedup ioctl surface.

PoC changes (relative to the seeded stub)

The finding markdown seeded only a trigger.c stub that left dedup->elm1/elm2 blank ("the runner must fill in"). I:

  • Wrote patch_image.py (host Python) β€” locates the B-Tree leaf node in a freshly-formatted V6 HAMMER image, patches two DATA-record leaves' data_len to 0x7FFFFFFF and data_crc to 0, recomputes the B-Tree node CRC with zlib.crc32 (matches the kernel's hammer_datacrc for V6), and prints the patched leaves' base_elm keys.
  • Rewrote trigger.c to use the real <vfs/hammer/hammer_ioctl.h> struct (the seeded stub declared a wrong-sized placeholder struct and used the wrong ioctl number _IOWR('h', 14, ...) which is actually HAMMERIOC_SET_VERSION; the correct number is _IOWR('h', 25, struct hammer_ioc_dedup) per hammer_ioctl.h:490). The corrected trigger fills elm1/elm2 with the patched leaves' base_elm keys and prints the ioctl result.
  • Added build.sh, run.sh, this VERDICT.md, manifest.json, panic.txt, run.log, fix_run.log, fix_build.log, env.txt, and fix.diff.

Fix validation

fix.diff promotes the KKASSERT at hammer_btree.c:736 to an explicit if (data_len < 0 || data_len > HAMMER_XBUFSIZE) return (EIO) with a hdkprintf diagnostic, preserving the same bounds but returning EIO instead of panicking. This protects all callers of hammer_btree_extract_data (dedup, mirror, prune, reblock, get_data, ...) β€” not just the dedup path. It supersedes the finding markdown's ## Recommended fix (which proposed the same change plus a redundant dedup-only check; the single hammer_btree.c change is sufficient and broader in scope).

  • Built make -j6 nativekernel KERNCONF=X86_64_GENERIC on the with-src snapshot with the diff applied β€” rc=0.
  • Installed via make installkernel (uses kernel.debug β†’ /boot/kernel/kernel, not kernel.stripped) and rebooted into 6.5-DEVELOPMENT #1 (today's build timestamp).
  • Re-ran the same trigger against the same patched image:
  • baseline (#0): panic: assertion "data_len >= 0 && data_len <= HAMMER_XBUFSIZE" failed in hammer_btree_extract at /usr/src/sys/vfs/hammer/hammer_btree.c:736 β€” guest goes down.
  • patched (#1): ioctl returned -1 (errno=5 'Input/output error'); dmesg shows hammer_btree_extract: bad data_len 2147483647 for leaf @ a000000022010000; guest stays up.
  • Re-ran 3Γ— on the patched kernel β€” identical, deterministic result.

The fix closes the bug. See fix_run.log for the full after-trace and panic.txt / run.log for the before-trace.

Files in this evidence pack

  • trigger.c β€” corrected HAMMERIOC_DEDUP trigger.
  • patch_image.py β€” host-side HAMMER image patcher.
  • build.sh / run.sh β€” runnable build/run scripts.
  • run.log β€” baseline (#0) panic narrative + boot.log excerpt.
  • fix_run.log β€” patched (#1) clean-EIO narrative + dmesg.
  • fix_build.log β€” full make nativekernel output (rc=0).
  • panic.txt β€” kernel panic signature from dfbsd-qemu/boot.log.
  • fix.diff β€” git apply-able fix (promotes KKASSERT to EIO).
  • env.txt β€” guest environment.
  • manifest.json β€” artifact catalog.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: the same trigger against the same patched evil.img panicked the unpatched #0 baseline (KKASSERT at hammer_btree.c:736) and does NOT panic the single-fix #1 kernel -- the ioctl returns EIO and dmesg shows 'hammer_btree_extract: bad data_len 2147483647'. Guest stays up. Reproduced 3x deterministically. Fix closes the bug.

before (#0): panic: assertion data_len >= 0 && data_len <= HAMMER_XBUFSIZE failed in hammer_btree_extract at hammer_btree.c:736 (guest down).
after (#1): ioctl returned -1 (errno=5 EIO); dmesg: bad data_len 2147483647; guest stays up (3/3).
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 09:34:32 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (sha256 7d3072de82044b3988aeb934a9ff329a0815a569dbc905abf0cf229cc90309ab)

Confirmed kernel references

Detail

Exploit chain

none (read-only OOB primitive, CWE-125). bcmp() returns only match/no-match; no write capability is gained, and the bug is gated behind caps_priv_check(SYSCAP_NOVFS_IOCTL) at hammer_ioctl.c:72 (root-only). The realistic impact ceiling is reliable kernel panic / local DoS via a crafted filesystem image mounted and deduped by an admin.

Evidence (decisive lines)

BASELINE (#0, INVARIANTS on):
panic: assertion "data_len >= 0 && data_len <= HAMMER_XBUFSIZE" failed in hammer_btree_extract at hammer_btree.c:736
hammer_btree_extract() at hammer_btree_extract+0x289
hammer_ioc_dedup() at hammer_ioc_dedup+0x11b

PATCHED (#1, fix.diff applied):
[*] ioctl returned -1 (errno=5 'Input/output error'); head.flags=0x0 head.error=0
dmesg: hammer_btree_extract: bad data_len 2147483647 for leaf @ a000000022010000
(guest stays up; reproduced 3x deterministically)

PoC changes

Rewrote trigger.c (was a stub): used the real struct hammer_ioc_dedup, corrected the ioctl number. Authored patch_image.py: scans a freshly newfs_hammer image for the B-Tree leaf node, sets two DATA-record leaves' data_len=0x7FFFFFFF and data_crc=0, recomputes the B-Tree node CRC. Added build.sh, run.sh, env.txt, VERDICT.md, manifest.json, panic.txt, run.log, fix_run.log, fix_build.log, and fix.diff.

Verified recommended fix

Promote the KKASSERT at sys/vfs/hammer/hammer_btree.c:736 to an explicit 'if (data_len < 0 || data_len > HAMMER_XBUFSIZE) { hdkprintf(...); return (EIO); }'. This protects ALL callers of hammer_btree_extract_data (dedup, mirror, prune, reblock, get_data). Full git-apply-able diff in findings/poc/DF-0929/fix.diff.

Verdict

REPRODUCED. The bug is real and reachable from the root-only HAMMERIOC_DEDUP ioctl on a crafted HAMMER image. A B-Tree DATA leaf with data_len=0x7FFFFFFF and data_crc=0 (the latter passes hammer_crc_test_leaf because hammer_crc_get_leaf for INODE-ish mismatched lengths returns 0) is loaded verbatim by hammer_btree_extract() at hammer_btree.c:728-729, then on the default X86_64_GENERIC kernel (options INVARIANTS) the KKASSERT(data_len >= 0 && data_len <= HAMMER_XBUFSIZE) at hammer_btree.c:736 panics immediately. Confirmed by the boot.log panic trace. On a non-INVARIANTS kernel the KKASSERT compiles out and the same value drives an OOB read in bcmp() at hammer_dedup.c:117.