Checksum Architecture
ReFS protects its metadata with a Merkle-tree variant: every B+-tree parent stores a checksum of each child page, so corruption is caught the moment the child is read by recomputing the checksum and comparing it to the value the parent recorded. For a forensic tool this has two consequences. First, a checksum you can recompute is a way to prove a metadata page is the original on-disk content and not a tampered or torn write. Second — and this is the trap — the checksums are not always enforced: on older volumes they are written but never checked, so the same byte that the driver would reject on a v3.14 volume mounts silently on a v3.4 one. A verifier has to know both the algorithm and whether the volume actually validates it.
The Merkle tree, level by level
The integrity model spans four levels, from the volume root down to individual metadata pages. Each level uses a different algorithm suited to its role, and the parent of each level is what stores the child’s checksum:
VBR
└─ ROR1+ADD self-checksum (offset 0x16)
└─ SUPB
└─ self-checksum (LcnWithChecksum @SUPB+0xD0, digest @+0x28) — cluster-size-dependent:
CRC32-C/4B (4K) · CRC64/8B (64K) · SHA-256/32B (SHA vol) — verified at mount + self-healed
└─ CHKP
└─ self-checksum (LcnWithChecksum) — same cluster-size-dependent rule — verified at mount
├─ Root page (e.g., Container Table)
│ └─ CRC64 (or SHA-256) stored in CHKP page reference
│ ├─ Internal node
│ │ └─ CRC64 stored in parent's page reference
│ └─ Leaf node
│ └─ CRC64 stored in parent's page reference
└─ Root page (e.g., Object Table)
└─ CRC64 stored in CHKP page reference
| Layer | Algorithm | Stored where | Purpose |
|---|---|---|---|
| VBR | ROR1+ADD (custom rotate-add, offset 0x16) | in the VBR itself | Boot-sector self-check |
| SUPB | self-checksum (LcnWithChecksum @+0xD0): cluster-size-dependent | in the SUPB itself | Superblock integrity — verified at mount + self-healed |
| CHKP | self-checksum (LcnWithChecksum): same rule | in the CHKP itself | Checkpoint self-integrity; this is the Merkle root — verified at mount |
| B+-tree pages | CRC64/NVME or SHA-256 | in the parent’s page reference | All metadata pages below the checkpoint |
The checkpoint (CHKP) is the root of the tree, and that is what makes the SUPB
and CHKP special. Below the checkpoint, every page’s checksum lives in its parent, so verifying a
child only requires already trusting the parent — the trust chains all the way up. But the checkpoint
has no parent to hold its checksum, so it (and the superblock above it) must carry its own
checksum instead: a LcnWithChecksum self-descriptor. The cktype byte at descriptor+0x22 selects the
algorithm, and that algorithm is cluster-size-dependent — CRC32-C 4-byte on 4 KiB-cluster volumes,
CRC64 8-byte on 64 KiB-cluster volumes, SHA-256 32-byte on SHA-256 volumes — computed over exactly one
cluster with the descriptor zeroed first. ComputeOrVerifySelfChecksumBlock does the zero-and-compute,
and the result is verified at mount by ValidateSuperBlock and ChooseCheckpointRecord. Note that this
self-checksum selector is separate from the page-reference data checksum below it: a 4 KiB-cluster
CRC64 volume uses CRC32-C for its SUPB/CHKP self-descriptor but CRC64 for its page references.
The page reference — the integrity building block
Every B+-tree parent stores a page reference for each child. This is the single structure that ties location to integrity: it carries both the child’s address (four VLCN slots) and a checksum of the child page’s data. The same structure does the virtual-to-physical addressing and the corruption detection, which is why a parser that mis-sizes it gets both wrong at once.
+0x00 8 bytes LCN slot 0 (first cluster)
+0x08 8 bytes LCN slot 1
+0x10 8 bytes LCN slot 2
+0x18 8 bytes LCN slot 3
────── 32 bytes: location ──────
+0x20 2 bytes Flags (0x0000)
+0x22 1 byte Checksum type: 0x00=None, 0x01=CRC32-C, 0x02=CRC64, 0x04=SHA-256
+0x23 1 byte Checksum data offset (0x08)
+0x24 4 bytes Checksum data length (4=CRC32-C, 8=CRC64, 32=SHA-256)
+0x28 N bytes Checksum value
────── variable: checksum blob ──────
The total size of the page reference is version- and checksum-dependent, and assuming the wrong size misaligns every field that follows it in a checkpoint root list or B+-tree node:
| Configuration | Total size | Checksum bytes | Notes |
|---|---|---|---|
| v3.4 (CRC64, padded) | 104 bytes (0x68) | 8 | 56 bytes zero-padding after checksum |
| v3.10+ CRC64 (compact) | 48 bytes (0x30) | 8 | No padding |
| v3.14 SHA-256 | 72 bytes (0x48) | 32 | No padding |
The v3.10 transition eliminated the 56-byte padding, shrinking the page reference from 104 to 48 bytes — a layout change that doubles as a version signal. The full byte layout and the size-detection logic live on the Page References page.
The checksum algorithms
CRC64 — this is CRC-64/NVME (Rocksoft), not ECMA-182
This is the primary metadata checksum (page-reference cktype = 2), applied to every B+-tree page when
CRC64 verification is active. The single most important fact for a verifier is that it is not
ECMA-182:
- Polynomial
0xAD93D23594C93659(normal) /0x9A6C9329AC4BC9B5(reflected) — the driver globalClMulCsCrc64. The polynomial is not ECMA-182’s0x42F0E1EBA9EA3693(0x42F0E1EBA9EA3693); recomputing with the
It is a named, standard CRC, so it does not need a bespoke implementation: ReFS uses CRC-64/NVME (also catalogued as CRC-64/Rocksoft) — polynomial 0xAD93D23594C93659 (reflected 0x9A6C9329AC4BC9B5), init and xorout 0xFFFFFFFFFFFFFFFF, reflected in and out. The catalogue check value for that CRC is 0xAE8B14860A799888, and the reader’s own implementation returns exactly that for "123456789" — so any crc64_nvme / crc64_rocksoft library reproduces ReFS metadata checksums directly.
wrong polynomial fails on every page.
- Reflected, init = xorout =
0xFFFFFFFFFFFFFFFF. - Computed over the full metadata page (all of the page reference’s LCN slots concatenated — 16 KiB on a 4 KiB-cluster volume), as one block.
- Collision sentinel: a computed result of
0xABBAFFFFABBAFFFEis stored as0xABBAFFFFABBAFFFF. A faithful reimplementation must apply the same substitution or it will report a false mismatch on the one page that happens to hash to the sentinel. - Stored in the parent’s page reference (
rec+0x28), never in the page itself.
The computation is the carry-less-multiply Crc64_ClMul instantiation of GenerateChecksum, and
SHA-256 pages (cktype = 4) are plain sha256(full_page). Recomputing CRC64 this way matches every
stored page-reference checksum across the corpus with zero mismatches; the production verifier
implements it as refs_crc64(), exposed through integrity --checksums (root tables) and
--fullchecksums, which walks the entire metadata tree by crossing the Object-Table leaf rows whose
values embed each object root’s page reference at value+0x20.
CRC32-C (Castagnoli)
CRC32-C appears as page-reference checksum type 0x01, used on the CHKP/SUPB self-descriptor — not as a page-reference data checksum (those use CRC64). It is also distinct from the SUPB/CHKP block-integrity self-checksum described above, even though that self-checksum is CRC32-C on 4 KiB-cluster volumes: the self-checksum’s algorithm is chosen by the cluster-size rule (the cktype byte at descriptor+0x22), whereas this type-0x01 code is the descriptor’s own format tag.
- Polynomial: Castagnoli (
0x1EDC6F41), init/xorout0xFFFFFFFF, reflected. - Computed over the page-referenced structure’s content.
- Stored in the page-reference descriptor (checksum-type byte = 0x01).
SHA-256
SHA-256 is a format-time option on Win11 24H2 (v3.14) and later — it is not Insider-only; v3.14
non-Insider SHA-256 volumes exist in the corpus. It is declared by VBR offset 0x2A = 0x04, fills the same
Merkle-tree role as CRC64 but with a 32-byte checksum value per page reference, and therefore uses
72-byte page references instead of 48-byte. The driver delegates SHA-256 to the imported cng.sys/BCrypt
provider (CmsBCrypt) rather than computing it in-line as it does for CRC64.
ROR1+ADD
A custom rotate-right-by-1-then-add algorithm used only for the VBR boot-sector
self-checksum at offset 0x16. It runs over the VBR content (skipping the two checksum bytes themselves)
and is validated during volume recognition by RefsIsBootSectorOurs; a mismatch prevents the volume
from mounting.
Checksums outside the Merkle tree
The four-level Merkle model above is not the complete integrity picture. Three further checksum families exist in parallel, none stored in B+-tree page references, and a forensic verifier that only implements the Merkle tree will silently skip them.
MLog log-record checksum (XOR-fold)
Each MLog log record carries its own integrity value — an 8-byte XOR-fold
(not a CRC) of the record body, stored in the entry header at +0x08 (= page+0x80). It is written by
LogCoreWriteDataRecord and verified by LogVerifyChecksumEntryHeader on recovery.
The algorithm. Zero the 8-byte field itself, then fold the log block: for each 8-byte little-endian word
of the block, XOR it into one of four accumulators chosen by its index modulo 4; XOR the four accumulators
together; and fold that 64-bit result down to 32 bits as (v >> 32) ^ (v & 0xFFFFFFFF). The block size is the
value at page+0x0C — 0x1000 on every volume, independent of the cluster size. Where a record spans several
blocks they chain: the next block’s first accumulator starts from the previous block’s result rotated left by
one bit. A record is exactly one block, so in practice the chain starts at zero.
The same value guards both kinds of log record — the control page and the data record — distinguished by the type field at entry header +0x30 (1 and 2 respectively; no other type occurs).
forefst mlog --stats recomputes it for every record and reports the verdict, so a torn or edited record is
named rather than parsed in silence. Across the image corpus — ReFS 3.4 through 3.15, 4 K and 64 K clusters —
all 599,039 data records and all 95 control pages recompute exactly.
It is an integrity check, not a signature. Being an XOR fold it catches any altered byte — a single-bit flip at any of the 4,096 offsets in a record is detected — but it is blind to a reordering of 8-byte words that leaves each word at the same position modulo 4. Treat a verified record as undamaged, not as authenticated.
The dword at MLog page +0x04 is NOT a CRC32: it is a per-volume constant format/log-instance magic, identical for every control page and data record in a volume and validated only by equality. Reading +0x04 as a per-record checksum (a common mistake) finds the same value on every record; the real per-record value is the XOR-fold at +0x80.
Container-compression per-unit checksums (24H2)
ReFS 24H2 volume compression introduces an integrity domain outside the metadata
Merkle tree, covering file-data compression units. The _SmsContainerCompressionHeader field at
+0x3C is a checksum-type selector (0 = none); when nonzero, per-unit checksums follow the
per-unit length array, and each decompressed unit is verified by
VerifyContainerCompressionHeaderChecksums against the volume checksum object at
volume+0xdd8 + (type × 8) before use. This reuses the volume’s CRC64/SHA-256 machinery but keys it
per compression unit rather than per page.
Integrity-stream per-cluster checksums (data files)
When a file is placed on an integrity stream (fsutil/Storage Spaces integrity, or Set-FileIntegrity),
ReFS checksums its data clusters too — separately from metadata. On a 4 KiB-cluster CRC32-C volume this checksum
lives inline in the file’s own extent map: the extent record carrying the clusters is followed by one
4-byte CRC32-C per cluster, and the record is padded to an 8-byte boundary. For the common single-cluster
case that makes the whole record 32 bytes — 24 of extent, 4 of checksum, 4 of padding.
The algorithm is CRC32-C (Castagnoli, 0x82F63B78) over the full
4096-byte cluster. extract recomputes and verifies each one as it recovers the file, and flags any mismatch as
corruption or tampering. SHA-256 and 64 KiB-cluster volumes keep their data checksums out of line (a 32-byte
SHA-256 cannot fit the inline slot), so there is nothing to verify inline on those.
The full integrity surface is therefore: Merkle tree (VBR → SUPB → CHKP → B+-tree pages) + MLog record XOR-folds + container-compression per-unit checksums + integrity-stream per-cluster CRC32-C. A complete verifier must handle all four.
Verification flow at mount time
When Windows mounts a ReFS volume with active checksums, the chain unwinds from the superblock down:
- Read the checkpoint (CHKP) from the location stored in the superblock.
- Verify the CHKP self-descriptor (CRC32-C on 4 KiB-cluster None/CRC64 volumes, CRC64 on 64 KiB-cluster, SHA-256 on SHA-256 volumes).
- For each root page reference stored in the checkpoint:
- Read the root page from disk.
- Compute CRC64 (or SHA-256) over the page data.
- Compare against the checksum stored in the CHKP page reference.
- On mismatch, try the duplicate (failover pair).
- If both copies fail, the mount fails.
For tables with failover pairs — Object Table roots 0/5, Schema Table roots 3/9, Container Table roots 7/8 — single-copy corruption triggers silent recovery from the duplicate. This silent redundancy is a forensic subtlety: a volume can mount cleanly even though one copy of a root is corrupt, so the absence of an error does not mean every metadata copy is intact.
When checksums are actually enforced
Checksum verification is not always active, and this is the most consequential point for an analyst. Two fields control the behaviour, and they can disagree:
| Field | Location | Meaning |
|---|---|---|
| Checksum algorithm | VBR offset 0x2A | Format-time declaration: 0x00=None, 0x02=CRC64, 0x04=SHA-256 |
| CHKP flag 0x400 | CHKP offset 0x78 | Runtime activation: checksums are verified only when this bit is set |
| Volume | VBR 0x2A | CHKP 0x400 | Verification |
|---|---|---|---|
| v3.4 (Win10) | 0x0000 | Not set | CRC64 values written but never verified |
| Upgraded v3.4-to-v3.14 | 0x0000 | Set | CRC64 verified at runtime |
| Native v3.14 (Win11 24H2) | 0x0002 | Set | CRC64 verified |
| v3.14 SHA-256 (Win11 24H2+) | 0x0004 | Set | SHA-256 verified |
On v3.4 volumes the driver instantiates CmsChecksumNone, whose VerifyChecksum method unconditionally
returns TRUE. CRC64 values are still computed and stored in page references, but they are never checked
on read — so a tampered metadata page on a v3.4 volume mounts without complaint, and the stored checksum
is useful to the analyst (it still detects the tamper on recompute) even though the driver ignores it.
The critical wrinkle is the upgraded volume. After upgrade to v3.14 the driver activates real
verification via the CmsChecksum class and sets CHKP flag 0x400 — even though VBR 0x2A remains
0x0000, because that field is an immutable format-time value never rewritten by the upgrade. The driver
keys runtime verification off the CHKP flags, not VBR 0x2A, so a tool that decides “checksums off”
purely from VBR 0x2A will be wrong on every upgraded volume. This is why version detection
must read the CHKP flags, not just the boot sector.
The superblock’s place in the chain
The superblock (SUPB) sits between the VBR and the checkpoint but is
outside the Merkle tree — no parent stores a checksum of it. Instead, each of the three SUPB copies
carries its own cluster-size-dependent self-checksum (LcnWithChecksum @SUPB+0xD0: read the cktype
byte at descriptor+0x22 → CRC32-C/4B on 4 KiB-cluster, CRC64/8B on 64 KiB, SHA-256/32B on SHA-256
volumes), and it is validated at mount by ValidateSuperBlock → ComputeOrVerifySelfChecksumBlock.
The authoritative copy is the one with the highest virtual clock (SUPB+0x68) among the copies that pass
validation — the primary at LCN 0x1E is not privileged, so a corrupt primary silently falls back to a
backup.
This self-checksum is also what drives the volume’s self-heal. Corrupting a single SUPB byte and mounting on Win11 causes the driver to mount the volume, silently repair the byte, and bump the checkpoint clock: the modified copy fails its self-checksum, is dropped, and the surviving winner is memcpy’d over it and re-checksummed. The repair happens precisely because the self-checksum caught the corruption — on a 4 KiB-cluster volume the digest that caught it is CRC32-C, not CRC64 — and it is enforced, not advisory. The forensic consequence is that mounting a corrupted ReFS volume read-write can overwrite the evidence of corruption; see Redundancy for the full self-heal mechanism and why imaging must precede any mount.
Cross-references
- VBR — the ROR1+ADD self-checksum (0x16) and the checksum-algorithm selector (0x2A)
- Checkpoint (CHKP) — the Merkle root: self-descriptor, root page references, and flag 0x400
- Page References — the location+checksum building block, full layout and the 104/48/72-byte size variants
- Version Detection — why checksum activation must be read from CHKP flags, not VBR 0x2A
- Copy-on-Write — CoW rewrites a child, so its checksum must be recomputed and propagated up the parent chain
- Redundancy — failover pairs, clock-based copy selection, and the SUPB/CHKP self-heal
- MLog — the per-record XOR-fold checksum (entry+0x08) outside the Merkle tree
- Compression — the 24H2 container-compression per-unit checksums, a parallel integrity domain
Evidence
The CRC64 page-checksum logic is the Crc64_ClMul instantiation of CmsChecksumBase::GenerateChecksum
(Win11 v3.14 driver). It iterates the page in stride-sized blocks, calling ClMulCrcBase<ClMulCrc64>::Compute
per block and applying the 0xABBAFFFFABBAFFFE → 0xABBAFFFFABBAFFFF collision sentinel. The polynomial is
not ECMA-182 — it lives in the ClMulCrc64 carry-less-multiply tables (the reflected
0x9A6C9329AC4BC9B5 / normal 0xAD93D23594C93659 driver global ClMulCsCrc64).
// GenerateChecksum (Crc64_ClMul instantiation; Win11 v3.14 driver)
// param_1 = ClMulCrcBase<ClMulCrc64> * (the CRC64 engine, NOT ECMA-182)
uVar7 = (ulonglong)*(uint *)(param_1 + 0x10); // block stride
CVar2 = param_1[0x16]; // collision-sentinel enable
for (lVar5 = param_2[1]; (p_Var4 != p_Var1 && (lVar5 != 0)); lVar5 = lVar5 - uVar7) {
_Var3 = ClMulCrcBase<class_ClMulCrc64>::Compute(param_1, pvVar6, uVar7, 0);
*p_Var4 = _Var3;
if ((CVar2 != 0) && (_Var3 == 0xabbaffffabbafffe)) // collision sentinel
*p_Var4 = 0xabbaffffabbaffff;
pvVar6 = (void *)((longlong)pvVar6 + uVar7);
p_Var4 = p_Var4 + 1;
}
Compute uses the SSE PCLMULQDQ carry-less multiply over the ClMulCrc64 constants; the engine
selection alone — distinct from the Crc32C_* and XXH64 siblings in the same vtable family — rules out
ECMA-182. The CRC64 page checksums and SHA-256 (cktype 4) are confirmed on disk by recomputation matching
every stored page-reference checksum with zero mismatches across v3.4 / v3.14 / 64 KiB / SHA-256 images,
and the full-tree walk gives full-coverage parity with zero false positives. The
SUPB/CHKP self-checksum’s cluster-size-dependent rule (CRC32-C/4B · CRC64/8B · SHA-256/32B), its mount-time
validation, and the self-heal are confirmed from ComputeOrVerifySelfChecksumBlock / ValidateSuperBlock /
ChooseCheckpointRecord and proven on disk by recomputation across all cluster sizes. The MLog
+0x04 field is a format magic, not a CRC. The verification class is CmsChecksum on v3.14, not
CmsCrc64 — that class is v3.4-only. Also registered for statements on this page:.