ReFS Forensics Reference

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
LayerAlgorithmStored wherePurpose
VBRROR1+ADD (custom rotate-add, offset 0x16)in the VBR itselfBoot-sector self-check
SUPBself-checksum (LcnWithChecksum @+0xD0): cluster-size-dependentin the SUPB itselfSuperblock integrity — verified at mount + self-healed
CHKPself-checksum (LcnWithChecksum): same rulein the CHKP itselfCheckpoint self-integrity; this is the Merkle root — verified at mount
B+-tree pagesCRC64/NVME or SHA-256in the parent’s page referenceAll 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:

ConfigurationTotal sizeChecksum bytesNotes
v3.4 (CRC64, padded)104 bytes (0x68)856 bytes zero-padding after checksum
v3.10+ CRC64 (compact)48 bytes (0x30)8No padding
v3.14 SHA-25672 bytes (0x48)32No 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 global ClMulCsCrc64. The polynomial is not ECMA-182’s 0x42F0E1EBA9EA3693 (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 0xABBAFFFFABBAFFFE is stored as 0xABBAFFFFABBAFFFF. 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/xorout 0xFFFFFFFF, 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:

  1. Read the checkpoint (CHKP) from the location stored in the superblock.
  2. 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).
  3. 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.
  4. On mismatch, try the duplicate (failover pair).
  5. 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:

FieldLocationMeaning
Checksum algorithmVBR offset 0x2AFormat-time declaration: 0x00=None, 0x02=CRC64, 0x04=SHA-256
CHKP flag 0x400CHKP offset 0x78Runtime activation: checksums are verified only when this bit is set
VolumeVBR 0x2ACHKP 0x400Verification
v3.4 (Win10)0x0000Not setCRC64 values written but never verified
Upgraded v3.4-to-v3.140x0000SetCRC64 verified at runtime
Native v3.14 (Win11 24H2)0x0002SetCRC64 verified
v3.14 SHA-256 (Win11 24H2+)0x0004SetSHA-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 ValidateSuperBlockComputeOrVerifySelfChecksumBlock. 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:.