ReFS Reference

Cluster and Page Size

The cluster size is a single number chosen when a ReFS volume is formatted, and it quietly sets the geometry of almost everything else on disk: how many clusters fit in a container, the stride of the virtual-to-physical address arithmetic, the size of a Container Table row, and the ceiling on a resident stream. An analyst who reads it wrong from the VBR will mis-size structures and mis-translate addresses across the whole volume, so it is one of the first values any parser must fix and the value almost every other layout decision is keyed to. This page explains the two configurations, how the cluster size derives the page and container geometry, and the structures that change shape with it.

A file occupies whole clusters; the last one is only partly filled.file dataslackfree⟵ cluster ⟶last cluster: file data + slack space (old data may survive in the slack)
Sectors, clusters, file data, and slack space.

Two configurations, fixed at format time

ReFS supports exactly two cluster sizes — 4 KiB and 64 KiB — and the choice is permanent. It is recorded as a sector count at VBR offset 0x24 (sectors-per-cluster), and since the sector size is always 512 bytes, the cluster size is simply 512 × sectors_per_cluster:

Cluster sizeSectors/cluster (VBR 0x24)CPCPage size
4 KiB816,38416 KiB (4 clusters)
64 KiB1281,02464 KiB (1 cluster)

Nothing later changes this. The cluster size is not version-dependent — both 4 KiB and 64 KiB appear on v3.4 and v3.14 — and it is never modified by an in-place upgrade, so the value read at format time is the value for the life of the volume. Note that 64 KiB clusters are a relatively recent, less common configuration; most volumes in the wild are 4 KiB.

CPC: the number everything is derived from

The volume is carved into fixed 64 MiB containersbytes_per_container at VBR offset 0x40 is 0x04000000 and is invariant across every version and cluster size. Dividing that by the cluster size gives CPC, the clusters-per-container count:

CPC = bytes_per_container / cluster_size = 64 MiB / cluster_size
    = 16,384  (4 KiB clusters)
    =  1,024  (64 KiB clusters)

CPC is the constant that drives the virtual addressing arithmetic. Because a container is always 64 MiB, a larger cluster means fewer clusters per container, which is why CPC drops by a factor of 16 when the cluster size grows by 16. CPC is also stored on disk: every Container Table row carries it at value offset 0x18, so a parser can read CPC directly from a container row rather than recomputing it.

The metadata page

The page is the unit in which the B+-tree storage engine reads and writes durable metadata — the page header, the node, and its rows all live inside one page. The page size follows directly from the cluster size:

  • 4 KiB clusters → page = 16 KiB = 4 consecutive clusters
  • 64 KiB clusters → page = 64 KiB = 1 cluster

So a 64 KiB-cluster volume aligns one page to one cluster, while a 4 KiB-cluster volume packs four clusters into a page. This is purely a function of the cluster size and is identical on v3.4 and v3.14; the 80-byte page header layout is the same regardless of page size. What does differ between configurations is the page reference format that a parent uses to point at and checksum a child page — but that is driven by version and checksum type, not by the page size, and is covered on the checksum architecture page.

One deliberate exception is worth knowing: the transaction-log block stays 4 KiB even on a 64 KiB-cluster volume, so the MLog layout does not scale with the cluster size the way the metadata page does.

What changes shape with the cluster size

Address-translation stride

The cluster size sets the shift and mask used to split a VLCN into a container index and an in-container offset (the full mechanism is on the virtual addressing page):

Cluster sizeShift (CPC.bit_length())Mask (CPC − 1)
4 KiB150x3FFF
64 KiB110x3FF
container_index     = vlcn >> shift
offset_in_container = vlcn & mask
physical_LCN        = container_phys_start + offset_in_container

The shift is CPC.bit_length() (15 for 4 KiB, 11 for 64 KiB), one more than log2(CPC). The driver stores log2(CPC) in the volume structure and adds 1 before shifting in GetContainerIdFromRealRange; the extra bit is safe because IsValidContainerLcn guarantees that bit log2(CPC) is never set in a valid container LCN, so the CPC − 1 mask still captures the whole in-container offset. Using the unadjusted log2(CPC) (14 / 10) misroutes a VLCN to the wrong container, which is why the shift is CPC.bit_length() = 15 for 4 KiB clusters (11 for 64 KiB).

Container Table row size

A Container Table row is 160 bytes in the common case and grows to 224 bytes when the extra integrity buffer is needed. Two independent conditions each force the larger row, and they do not stack:

Cluster sizeChecksumRow sizePhysical-start offset
4 KiBCRC64 or None1600x90
4 KiBSHA-2562240xD0
64 KiBany2240xD0

The difference is the per-container integrity checksum buffer that starts at row offset 0x50: 4 slots (64 bytes) on a 160-byte row, 8 slots (128 bytes) on a 224-byte row. Because the physical-start cluster sits after that buffer, its offset moves with the row size — but the universal parsing rule absorbs that: the physical start is always at value[len − 16], and CPC is always at value + 0x18, regardless of which row size is in play. A parser that hard-codes 0x90 will read garbage on a 64 KiB or SHA-256 volume.

Resident stream ceiling

A small Alternate Data Stream (content below 2 KB) is kept inline in a B+-tree row; a larger ADS is converted to non-resident extents at the ~2 KB threshold, like any large stream. So the inline-ADS ceiling is the 2 KB conversion threshold (well within a single row on any page size), not the page capacity — the page size instead bounds how much resident metadata a directory entry can hold before the tree splits.

Self-checksum coverage

The SUPB and CHKP records have no parent to hold their checksum, so each verifies itself over exactly one cluster in ComputeOrVerifySelfChecksumBlock. The covered range is therefore 4 KiB or 64 KiB depending on the cluster size, and the algorithm itself is cluster-size-dependent — another place where the format-time cluster choice reaches into the integrity machinery. The detail is on the checksum architecture page.

Forensic impact

The cluster size is read once and trusted everywhere, so getting it wrong is a single mistake that silently corrupts many results. It fixes CPC, which fixes the address-translation shift, which decides whether every VLCN resolves to the right container; it fixes the page size, which decides how many bytes the parser walks per metadata page; and it fixes the Container Table row size, which decides where the physical-start cluster lives. The robust discipline is to read sectors-per-cluster from the VBR first, derive CPC and the page size from it, and never hard-code a row offset — use value[len − 16] for the physical start and value + 0x18 for CPC so the same code handles both configurations.

Cross-references

  • VBRsectors_per_cluster (0x24) and bytes_per_container (0x40), the two fields this page derives everything from
  • Virtual Addressing — uses CPC to set the shift/mask that translate a VLCN to a PLCN
  • Container Table — the row whose size (160 vs 224) and physical-start offset depend on cluster size and checksum
  • Page Header — the 80-byte header is the same regardless of page size
  • Page References — the parent-to-child reference that varies by version/checksum, not page size
  • Resident vs Non-Resident Storage — the ADS ceiling is bounded by single-row capacity in one page
  • Checksum Architecture — the self-checksum covers exactly one cluster, sized by the cluster choice
  • MLog — the log block stays 4 KiB even on 64 KiB-cluster volumes

Evidence

Sectors-per-cluster at VBR 0x24 (8 / 128) and bytes_per_container = 0x04000000 at VBR 0x40 are confirmed in the driver and on the raw-disk corpus. CPC at container-row offset 0x18 and the universal value[len − 16] physical-start rule are raw-disk decoded across the corpus, and the 160-vs-224 row sizing — with 64 KiB clusters and SHA-256 each independently forcing 224 and not stacking — is confirmed on disk. The address-translation shift of 15 (4 KiB) / 11 (64 KiB) is confirmed by decompilation of GetContainerIdFromRealRange (with IsValidContainerLcn guaranteeing the boundary bit) and on disk. The inline-ADS ceiling is the ~2 KB RefsConvertToNonResident threshold — a larger ADS is extent-backed — which is driver- and disk-supported. The single-cluster self-checksum is proven by recomputation across the corpus in ComputeOrVerifySelfChecksumBlock.