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)
PoC verification
Evidence pack
findings/poc/DF-3150 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 β 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
fixedPatched kernel consumes the NODATA mrec without error and creates no record (readback clean) - the intended skip semantics. Fix proven.
run.fixed.log
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.
No comments yet.