ReFS Reference

Virtual Addressing

Virtual addressing is the rule that decides where ReFS data actually lives on disk, and it is the first thing a tool built for NTFS gets wrong. A cluster number found in a file’s extent table or in a B+-tree root is a virtual address — it is not a disk offset, and reading the sector at that number gives you the wrong bytes. Every such number must be translated through the Container Table before any disk access. This page explains the two-level scheme, the arithmetic that performs the translation, and why ReFS is built this way.

VLCNvirtual cluster>> shift = container idContainer Tablecid → physical base0 → CSC₀1 → CSC₁… → …+ (VLCN & mask)PLCNphysical clusterPLCN = ContainerTable[ VLCN >> shift ] + ( VLCN & mask )
Virtual-to-physical cluster translation through the Container Table.

Two levels of indirection

NTFS translates in one step: a data run maps a file’s VCN (virtual cluster number — the logical offset within the file) straight to an LCN (the physical cluster on disk). ReFS inserts a virtual layer between the two, so translation takes two steps:

  • Level 1 — per file (VCN → VLCN). Each file’s content is described by an extent table that maps the file’s VCNs to VLCNs (virtual logical cluster numbers — positions in ReFS’s volume-wide virtual address space). This is the analogue of an NTFS data run, except the result is still virtual. The byte layout of these entries is on the Extent Descriptors page.
  • Level 2 — per volume (VLCN → PLCN). The Container Table maps a VLCN to its PLCN — the real cluster on disk. This second step is what NTFS has no equivalent of.

So the full path from a file offset to a disk sector is VCN → VLCN → PLCN, and the Container Table is the only thing that knows the second arrow.

The translation arithmetic

The volume is carved into fixed 64 MiB containers. A VLCN is simply a container number in its high bits and an offset within that container in its low bits, so the translation is a shift, a mask, and one table lookup:

shift = log2(CPC) + 1      # 15 for 4 KiB clusters, 11 for 64 KiB
mask  = CPC - 1

container_index     = vlcn >> shift
offset_in_container = vlcn & mask
physical_LCN        = container_map[container_index] + offset_in_container

CPC is clusters per container = 64 MiB ÷ cluster size = 16,384 on 4 KiB clusters or 1,024 on 64 KiB (it depends only on the format-time cluster size — see Cluster and Page Size). container_map is the per-container physical start cluster held in the Container Table.

The one subtlety worth understanding is the shift. The driver stores log2(CPC) (= 14 for 4 KiB clusters) in the volume structure and shifts by that value plus one, so the effective shift is 15, not 14. The extra bit is safe because a valid container LCN never sets bit log2(CPC) — the driver’s own IsValidContainerLcn check enforces it — so the CPC − 1 mask (14 bits) still captures the whole in-container offset even though the shift discards 15. This +1 is exactly what the driver does in GetContainerIdFromRealRange:

// GetContainerIdFromRealRange (Win11 v3.14 driver)
lVar1 = RealRangeToContainerRange((CmsVolumeContainer *)local_18, param_1, &local_18, param_3, 0, 0);
if (-1 < lVar1) {
    // container_index = real_lcn >> (log2(CPC) + 1)
    *param_4 = *param_3 >> ((char)*(undefined4 *)(*(longlong *)(this + 8) + 0x50) + 1U & 0x3f);
}
return lVar1;

+0x50 is where the volume structure holds log2(CPC); the + 1U is the adjustment. (An earlier account of this shift used 14; the disk and the driver both give 15 — see Evidence below.)

Why a virtual layer at all

The indirection is what lets ReFS move data on disk without rewriting metadata. Because a file’s extents point at virtual clusters, the file system can relocate a whole container — defragmenting, or moving it between storage tiers — by changing a single physical-start value in the Container Table, with no edit to any per-file extent table. The same property underlies copy-on-write at the container level and lets the three-tier allocator manage virtual space independently of physical layout.

A forensic consequence follows directly: a file’s content can sit at a different physical location than it did yesterday even though none of its metadata or timestamps changed. Physical position is not a stable identifier for a file’s data.

Containers

A container is a fixed 64 MiB region; the size is bytes_per_container at VBR offset 0x40 (0x04000000, invariant across every volume). The volume holds one container per 64 MiB:

Volume sizeCluster sizeContainers
2 GiB4 KiB31
8 GiB4 KiB127
2 TiB4 KiB32,767
15 TiB64 KiB245,759

The bootstrap exception

Almost everything on the volume is virtually addressed — but the Container Table itself cannot be, or there would be no way to start translating. Three roots therefore store real (physical) LCNs:

They are read directly from the checkpoint root list at physical positions. This is the one place a cluster number in a ReFS root is a disk offset; everywhere else, translation is mandatory.

Forensic impact

Treating a VLCN as a physical address — the reflex of an NTFS-trained parser — reads clusters that are not the file’s data. This is the single most common ReFS parsing error, and it corrupts every result derived from it. The discipline is simple: load the Container Table first — it is the step of the bootstrap chain that makes address resolution possible — and route every VLCN through it before touching the disk. Any recovery or carving workflow that skips this step — see Deletion Recovery — will reconstruct content from the wrong sectors.

Cross-references

Evidence

The two-level scheme and the shift/mask arithmetic are confirmed in the driver — the translation is performed by GetContainerIdFromRealRange (calling RealRangeToContainerRange), with IsValidContainerLcn guaranteeing the boundary bit is zero — and on the raw-disk corpus: CPC at container value+0x18, the physical start at value[len−16], and PLCN = container_start + (vlcn & (CPC−1)) resolved consistently across the corpus, including the roots-7/8/12 real-LCN exception. The 15-cluster shift for 4 KiB clusters (11 for 64 KiB) is CPC.bit_length().