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

HAMMER1 mirror-write: HAMMER_MREC_TYPE_REC_NODATA case is dead code (mask strips the NODATA bit) β€” NODATA records installed as holes instead of skipped

Summary

The write dispatch masks type with HAMMER_MRECF_TYPE_MASK = 0x800000FF (:429) but the NODATA flag is bit 0x10000000, so the case label HAMMER_MREC_TYPE_REC_NODATA = 0x10000001 can never match: NODATA-flagged RECs - which the read side genuinely emits for HAMMER_IOC_MIRROR_NODATA streams (:244-248/:272) - fall into the plain REC install path and are created with data_len==0 (content hole) instead of being skipped as the case body intends. Silent data loss / integrity corruption on a mirror target fed a NODATA stream (crafted master, or a histogram-style stream misapplied). No kernel memory-safety effect. RUNTIME-PROVEN: baseline installs the record (readback shows it); with the fix the same mrec is consumed but readback shows no record. Fix validated (mask includes NODATA).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3150 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
mirror_poc.c β€” 6.6 KB view raw
build.sh β€” 92 B view raw
run.sh β€” 147 B view raw
run.log β€” 774 B view raw
run.fixed.log β€” 546 B view raw
env.txt β€” 651 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 2.8 KB view raw
manifest.json β€” 670 B view raw

DF-3150 β€” HAMMER_MREC_TYPE_REC_NODATA case is dead code; NODATA records are installed without data

What

hammer_ioc_mirror_write() dispatches on mrec.head.type & HAMMER_MRECF_TYPE_MASK = 0x800000FF (hammer_mirror.c:429, hammer_ioctl.h:383), but the NODATA flag lives in bit 0x10000000 (hammer_ioctl.h:388). The case label HAMMER_MREC_TYPE_REC_NODATA (0x10000001) can therefore never match β€” the masked value of a NODATA record is plain 1 (HAMMER_MREC_TYPE_REC) β€” so NODATA records take the full install path of hammer_ioc_mirror_write_rec instead of being skipped, and the record is created with data_len == 0 (a hole) even though the master has data for it.

The read side genuinely emits such records: hammer_mirror.c:244-248,272 sets type = HAMMER_MREC_TYPE_REC | HAMMER_MRECF_NODATA whenever the user asked for HAMMER_IOC_MIRROR_NODATA (histogram generation).

Impact

Silent data loss / integrity corruption on a mirror target fed a NODATA-flagged stream (malicious or accidental). No kernel memory-safety effect β€” hence severity Low.

Reproduce (runtime proof, stock kernel)

# fresh 2GB hammer fs on vn0, file /mnt/f1 (see README of DF-3148)
/tmp/mirror_poc /mnt/f1 nodata      # NODATA-flagged REC mrec
/tmp/mirror_poc /mnt/f1 readback    # HAMMERIOC_MIRROR_READ the range

Expected (buggy) result β€” the record IS created:

MIRROR_WRITE rc=0 head.error=0 head.flags=00000000 count=80   <- processed
MIRROR_READ rc=0 error=0 count=664
  mrec[0] ... obj=1234 key=100 ctid=1000 dtid=0 btype=52 rt=0012 ... dlen=0

Had the case HAMMER_MREC_TYPE_REC_NODATA: skip path been reachable, no obj=1234 record would exist and readback would show only the PFS root records.

Fix

fix.diff (shared with DF-3148/3149): include HAMMER_MRECF_NODATA in the switch mask so the existing skip case becomes reachable. Validated on a patched kernel: NODATA mrec consumed (count advances), readback shows no obj=1234 record.

VERDICT.md
↓ download raw

VERDICT β€” DF-3150

Status: REPRODUCED (logic/integrity) / fix VALIDATED

Baseline (stock kernel #0)

HAMMER_IOC_MIRROR_NODATA-style mrec (head.type = 0x10000001, HAMMER_MREC_TYPE_REC | HAMMER_MRECF_NODATA), data_len=0:

MIRROR_WRITE rc=0 head.error=0 head.flags=00000000 count=80   <- processed as REC
MIRROR_READ rc=0 error=0 count=664
  mrec[0] ... obj=1234 key=100 ctid=1000 dtid=0 btype=52 rt=0012 ... dlen=0

The record WAS installed on the mirror target β€” proving the switch in hammer_ioc_mirror_write (hammer_mirror.c:429) dispatched type & HAMMER_MRECF_TYPE_MASK (0x800000FF, hammer_ioctl.h:383) to case HAMMER_MREC_TYPE_REC and the case HAMMER_MREC_TYPE_REC_NODATA label (0x10000001, hammer_ioctl.h:379-380) is UNREACHABLE β€” the mask strips the NODATA bit 0x10000000 (hammer_ioctl.h:388). The read side genuinely emits this type for NODATA streams (hammer_mirror.c:244-248, :272), so such records are silently installed with data_len == 0 (content hole) instead of being skipped as the case body intends (mirror.c:442-452).

Impact ceiling: silent data loss / integrity corruption on a mirror target fed a NODATA stream (a crafted master can do this deliberately; the records become holes). No kernel memory-safety effect β†’ severity Low.

Fix validation (patched kernel #1, fix.diff: switch mask includes HAMMER_MRECF_NODATA)

=== nodata + readback on fresh fs (patched) ===
MIRROR_WRITE rc=0 head.error=0 head.flags=00000000 count=80   <- consumed
MIRROR_READ rc=0 error=0 count=584
  mrec[0] ... obj=1 key=0 ctid=100000001 dtid=0 btype=52 rt=0001 loc=00000001 dlen=128
  mrec[208] ... obj=1 ... rt=0015 ...

count=80 (mrec consumed, no error) but readback shows ONLY the PFS root records β€” no obj=1234 record exists: the REC_NODATA case now runs and skips the record, exactly as designed. Fix proven.

Bottom line

Certain. Dead switch case caused by mask/flag bit mismatch; NODATA mirror records installed instead of skipped. Low severity (integrity), fix proven.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel consumes the NODATA mrec without error and creates no record (readback clean) - the intended skip semantics. Fix proven.

run.fixed.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 05:10:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

['run.log: baseline - NODATA mrec processed (head.error=0, count=80) and readback shows the installed obj=1234 dlen=0 record', 'run.fixed.log: patched - same mrec consumed, readback shows only PFS root records (skip case reached)', 'VERDICT.md: mask arithmetic walkthrough']

PoC changes

nodata mode sets head.type = 0x10000001 (REC|MRECF_NODATA); readback mode parses returned mrecs to prove presence/absence of the record

Verified recommended fix

Include HAMMER_MRECF_NODATA in the dispatch mask so the existing REC_NODATA skip case is reachable (see fix.diff; kernel-local change, avoids rippling the shared header constant into userland dispatchers).

Verdict

Runtime-proven logic bug: HAMMER_MREC_TYPE_REC_NODATA case in hammer_ioc_mirror_write is dead code because the dispatch mask HAMMER_MRECF_TYPE_MASK (0x800000FF) strips the NODATA bit 0x10000000 that the case label requires. A NODATA-flagged REC (which the read side genuinely emits for HAMMER_IOC_MIRROR_NODATA streams, mirror.c:244-248,272) is dispatched to the plain REC install path and the record is created with data_len==0 (content hole) instead of being skipped. Baseline run.log shows obj=1234 dlen=0 installed and read back; patched run.fixed.log (switch mask extended with HAMMER_MRECF_NODATA) shows the mrec consumed but NO record created. Integrity/data-loss only (crafted master or misapplied histogram stream); no kernel memory-safety effect -> Low.