ReFS Forensics Reference

Directory Entries

key_flags records the record’s location, never the bytes’ location — the two are explained in Record placement and data residency.

Directory entries (type 0x30) are B+-tree rows within a per-directory B+-tree. Each file or subdirectory appears as a type 0x30 row in its parent directory’s tree. The record placement (embedded vs split) determines the value layout, and a parser must resolve that mode from the key flags before reading any field.

B+-tree row header — 16 bytes

Every row in a B+-tree leaf begins with this header:

OffsetSizeFieldDescription
0x004Row size (u32)total byte size of the row (0x70 is the common value for a typical embedded row, not a constant flag)
0x042Key offset (u16)Byte offset from row start to key data
0x062Key length (u16)
0x082Reserved (u16)a candidate deleted-row bit 0x04 here is confirmed-only and disk-unconfirmable — not relied upon
0x0A2Value offset (u16)Byte offset from row start to value data
0x0C2Value length (u16)
0x0E2Reserved (u16)

See B+-Tree Node for the page-level node structure that holds these rows.

Key format (type 0x30)

OffsetSizeFieldDescription
0x002Entry type (u16)Row type identifier (0x0030 = filename, 0x0020 = reverse index, 0x0040 = extent descriptor)
0x022Key flags (u16)Determines value interpretation
0x04varFilename (UTF-16LE)File or directory name

Key flags

Only two key_flags values exist on disk (distribution {0x01, 0x02} only, zero occurrences of 0x04):

ValueThe row isValue SizeDescription
0x01an embedded record>84 bytesThe file’s own metadata header follows in this row. Its $DATA may be inline or extent-backed — the flag does not say which
0x02an index entry84 bytes, or 72 if written by a pre-v3.10 driverA pointer; the record was split out into a type-0x40 row in the file’s home directory. On a file this is the fingerprint of a move or a hard link

key_flags is not a residency flag. Reading 0x01 as “resident” and 0x02 as “non-resident” is the most common way to mis-parse this structure. Every kf=0x01 file row is extent-backed on ReFS 3.4–3.10 (1,120 / 250 / 386 / 236 rows measured, none inline); on 3.14 about 93 % are inline but 16,809 are not. Determine residency from the $DATA attribute’s storage — see Record placement and data residency.

A directory is stored with key_flags 0x02 (the index-entry value layout) and is identified by the directory attribute bit 0x10000000 at value+0x40: RefsAddFileNameIndexEntry ORs the 0x10000000 bit into the attribute word. There is no separate 0x04 = directory flag.

The index-entry value length is 72 bytes below v3.10 and 84 bytes from v3.10 — the driver RefsAddFileNameIndexEntry sets 0x54 (84) on a v3.10+ volume and 0x48 (72) on an older one.

That length is fixed when the entry is written, and is never rewritten afterwards — so it is a property of the entry, not of the volume. A volume upgraded across the v3.10 boundary carries both layouts side by side: its pre-upgrade entries keep the 72-byte form while the newer driver writes 84-byte entries for anything new. Take the size from the row’s own length; keying on the volume version misparses every retained entry on an upgraded volume. Across the corpus every natively-formatted volume carries a single size matching its format-time version, and exactly the upgraded volumes carry a mixture — one of them 482 retained 72-byte entries beside 333 new 84-byte ones. Every field below sits at or under value+0x44, which is identical in both layouts, so testing the length as a range is both correct and version-independent.

Embedded-record value (key_flags = 0x01) — variable size

OffsetSizeFieldDescription
0x0040Structural metadataStream index (8B) + internal B+-tree management fields (32B)
0x288Creation timeFILETIME (100 ns since 1601-01-01)
0x308Modification timeFILETIME
0x388Metadata change timeFILETIME
0x408Last access timeFILETIME
0x484File attributes (u32)Win32 flags + ReFS extensions
0x4C4Internal flags (u32)ReFS-specific state
0x508Security ID (u64)Links to Security Descriptors table (OID 0x530)
0x588File size (u64)Logical file/stream size (= $DATA size). Equals the $DATA sub-record size on every measured v3.14 file, and is nonzero even on USN-inactive images — decisively FileSize.
0x608Allocated size (u64)Allocation ≥ file size (roundup-to-cluster on most; inline byte count for tiny files).
0x688LastUsn (u64)File’s last USN = virtual byte offset of its most recent $UsnJrnl:$J record (OID 0x520). 0 if the journal is inactive. This (offset 0x68) is the per-file USN.
0x708UsnJournalId (u64)Journal epoch ID (one per volume); 0 if journal inactive.
0x808NextFileId (u64)Directory child-creation ordinal (mirrors $SI+0x58).
0xA8+varEmbedded sub-record chain$DATA / ADS / snapshot / EA / $EFS rows. There is NO embedded $SI sub-record: the type-0x30 value mirrors the $SI fields and carries the USN-journal fields inline at 0x68/0x70. Version-dependent; see below.

The embedded value mirrors the file’s own type-0x10 $STANDARD_INFORMATION at every offset except 0x58/0x60, where it carries FileSize / AllocatedSize instead of the $SI’s (always-0) USN / DataSize fields. The own-row $SI total size is 116 bytes (0x74) on Win10 and 124 bytes (0x7C) on Win11. See Standard Information for that separate structure, and Record placement and data residency for the rule that decides whether the bytes sit inline.

Sub-record row count (offset 0x20 in the embedded value)

The value+0x20 field is a count of embedded sub-record rows in the embedded record’s inline B+-tree (the inline file object’s sub-record table: 1 default $DATA + one row per ADS, snapshot, reparse, EA, and $EFS). It is NOT a fixed 1-6 enum. It equals the parsed sub-record row count in the large majority of embedded records (the residual are deleted/marker rows). The count is unbounded: values up to 14 have been observed (e.g. a file with 12 ADS).

The values below are common cases (consequences of the count), not an exhaustive enumeration:

Row CountCommon case
1Normal file (default data stream only)
2One extra component — sparse file or reparse point/symlink
3Encrypted file ($DATA + $EFS), or EA/WSL file, or file with 1 ADS
4File with 1 snapshot, or 2 ADS, or reparse + EA
5+More ADS and/or snapshots (e.g. count=14 with 12 ADS)

When the sub-record table moves into a child node

The chain above sits directly in the value only while it is small enough to fit in one node. The inline table is a real B+-tree, and when a file accumulates enough attributes — many alternate data streams, for instance — ReFS promotes it: the node inside the value becomes an index node, and the sub-records themselves move to a child page elsewhere on the volume.

The node descriptor tells you which case you are in:

FieldLeaf — attributes are hereIndex — attributes are in a child
Level (descriptor + 0x0C)0non-zero
Flags (descriptor + 0x0D)bit 0 clearbit 0 set

An index node carries one row per child. That row has an empty key (key offset 0x10, key size 0) and a 48-byte value which is a node reference: four cluster numbers, where 0 and 0xFFFFFFFFFFFFFFFF mean “unused”. Follow a valid one and you land on an ordinary MSB+ page — read its rows exactly as you would read the inline ones.

Why this matters when you are reading evidence. In the index case the value contains no attributes at all. A reader that walks only the inline offsets array finds nothing, and will report that the file has no alternate data streams, no reparse data and no $DATA record. That is a confident wrong answer rather than an error, which is the most dangerous shape a forensic result can take. On the reference corpus this affects 1,329 files across 19 volumes and hides 4,094 attribute records, 548 of them alternate data streams — one volume that appeared to have no ADS whatsoever in fact has 60 files carrying them. The file-system driver descends into the child here, and so must any tool that wants a complete answer.

Embedded sub-record markers

MarkerMeaning
0x80000001Single-instance attribute (reparse 0xC0, EA info 0xD0, EA 0xE0)
0x80000002Multi-instance attribute ($DATA 0x80, $SNAPSHOT 0xB0)

Sub-records begin at offset 0xA8+ and form a chain. Single-instance sub-records carry one attribute each; multi-instance sub-records may appear multiple times for streams with the same descriptor.

ADS and snapshot sub-records (descriptor 0x000500B0)

Both alternate data streams (ADS) and snapshot streams are stored as multi-instance sub-records with the same descriptor 0x000500B0. The StreamSummary.Flags field (u16) at the start of the StreamSummary region discriminates them: 0 = ADS, 2 = snapshot. See $SNAPSHOT for the value format.

OffsetSizeFieldDescription
0x004Marker (u32)0x80000002 (multi-instance)
0x044Descriptor (u32)0x000500B0
0x08varStream nameUTF-16LE null-terminated
0-6Alignment paddingPad to (offset - marker_start) % 8 == 4

Sub-record header (immediately after alignment):

OffsetSizeFieldDescription
+0x004Padding (u32)Always 0
+0x044Data area size (u32)Value length minus 12
+0x084Content offset (u32)Always 0x0C
+0x0C4Summary size (u32)Always 0x30 (48 bytes)
+0x102StreamSummary flags (u16)0 = ADS, 2 = snapshot (bit 1 set by RefsCreateStreamSnapshot)
+0x126ReservedAlways 0
+0x188Allocated size (u64)Cluster-aligned allocation
+0x208Stream size (u64)Logical content length
+0x3CvarInline contentPresent when SS flags = 0 (ADS only)
+0x448Stream index (u64)Present when SS flags = 2 (snapshots only); links to DATA sub-record

Snapshot extent linking: When SS flags = 2, the entry is a snapshot stream (NOT an ADS). The stream_index at header+0x44 matches a 0x000E0080 (DATA) sub-record’s stream_index at sub_rec+0x08 within the same value, providing the extent table for the snapshot’s content. See $NAMED_DATA and $SNAPSHOT for the value format.

DATA sub-records (descriptor 0x000E0080)

Extent tables for extent-backed content are stored in multi-instance sub-records with descriptor 0x000E0080. Multiple DATA sub-records may coexist in one value (default $DATA, ADS streams, internal streams).

OffsetSizeFieldDescription
0x004Marker (u32)0x80000002
0x044Descriptor (u32)0x000E0080
0x088Stream index (u64)Matches ADS header+0x44 or default stream 0x0008
0x10varExtent tableSee Extent Descriptors

Index-entry value (key_flags = 0x02) — 84 bytes, or 72 from a pre-v3.10 driver

Directories also use this layout (key_flags 0x02) and are identified by the directory attribute bit 0x10000000 at offset 0x40.

OffsetSizeFieldDescription
0x008Child ordinal (u64)Per-directory child ordinal (= the NextFileId ordinal of $SI+0x58). Shared by all hard-link names of one file; it is reused per directory and collides across sibling dirs under a shared home, so it is not a globally-unique FileId. Also indexes the type-0x40 extent descriptor. See Hard links.
0x088Home-dir backref (u64)OID of the directory the file was first created in — not necessarily its current parent. Frozen for the life of the object: it is unchanged by a rename or a move to another directory, and is shared by every hard-link name of the file. Together with value+0x00 it is the file’s (HomeOid, FileId) identity (see File IDs).
0x108Creation timeFILETIME
0x188Modification timeFILETIME
0x208Metadata change timeFILETIME
0x288Last access timeFILETIME
0x308Allocated size (u64)Total allocated bytes (cluster-aligned)
0x388File size (u64)Logical content length
0x404File attributes (u32)Win32 flags + ReFS extensions (dir bit 0x10000000)
0x444 / 12PaddingZero; 4 bytes in the 72-byte form, 12 bytes in the 84-byte form

Relocated files — the entry moves, the identity does not

When a file is moved to another directory, ReFS relocates this type-0x30 entry into the new parent’s B+-tree, but the object’s data/metadata (its type-0x40 backing) stays in the creation directory’s tree. The entry’s value+0x00 ordinal and value+0x08 home-backref are copied unchanged, so a moved file is recognisable on disk as one whose value+0x08 differs from the directory it now sits in. For example, on a real Win11 volume the file Generate-FSActivity.ps1 was created in directory OID 0x9586 and later moved under OID 0x9e25 (\tools); on disk its entry sits in 0x9e25 but still carries value+0x08 = 0x9586 and its ordinal value+0x00 = 3, and its type-0x40 backing is found in 0x9586, not 0x9e25. The same value+0x08 != containing directory shape also arises for a hard-link name placed in a second directory (told apart from a move by the derived link count — see below).

This is also why a relocated file always appears in the split layout. The frozen creation-directory home is carried by value+0x08, and only the split value has that field — the embedded value (key_flags 0x01) does not. So a file’s entry takes the split layout the moment it is moved or hard-linked, and it never reverts. A file whose record is still embedded has therefore never been relocated: its home always equals its current parent.

Note carefully what this does not say. The split moves the record, not the bytes. A moved file’s $DATA is untouched, and for a small file it stays inline inside the backing record. Measured on a controlled before/after move of a whole volume: of 9,521 files present in both images, 5 changed placement and 0 changed data residency, and the moved 16-byte file’s $DATA sub-record was byte-identical either side — read out of the name row before the move and out of its backing record after, with value+0x08 still pointing at its creation directory. See Record placement and data residency for the two axes.

Critical layout differences

Timestamps and most fields are at different offsets between an embedded record and an index entry:

FieldEmbedded record (kf 0x01)Index entry (kf 0x02)
Creation time+0x28+0x10
File attributes+0x48+0x40
Security ID+0x50Not in the dir entry — in the type-0x40 backing record @+0x50
File size+0x58+0x38
Allocated size+0x60+0x30
LastUsn (per-file USN)+0x68 (= $SI+0x40)Not in the dir entry — in the type-0x40 backing @+0x68
UsnJournalId+0x70 (= $SI+0x48)Not in the dir entry — in the type-0x40 backing @+0x70

A parser must resolve the storage mode from key_flags before reading any timestamp. Applying the wrong offset layout will misparse every field.

SecurityId / USN of a split file: a split file’s type-0x30 index entry does not carry these, but they are not reached through the Object Table — a file has no Object-Table OID. They live in the file’s own type-0x40 backing record (its $SI / stream-summary, keyed by home-dir OID + FileId) at backing value+0x50 (SecurityId), +0x68 (LastUsn), +0x70 (UsnJournalId).

When a file gains a second name its record is split out into the index-entry layout above (key_flags 0x02), and each name becomes its own type-0x30 entry. All of one file’s names share value+0x00 (the per-directory child ordinal) and value+0x08 (the home-dir backref) — both already in the index-entry value table above. There is no explicit on-disk HardLinkCount field, and $SI+0x70 is an embedded-layout scalar that always reads 1, so the link count is derived by resolving each name to its size-matched type-0x40 stream rather than read from a field. Hard links are a native v3.14 feature. See Hard Links for the full resolution model and forensic implications.

Reverse index (type 0x20)

A directory’s tree also carries type 0x20 rows: a per-object FileId-resolution index that maps a child index back to either the object’s name (Format A) or its home-directory back-reference (Format B). Not every type 0x30 child has a corresponding type 0x20 row. The full key and value layouts are in Reverse Index.

Cross-references

Evidence

The key/value layouts, the 84/72-byte size split and its fixing at write time (proven by two controlled before/after upgrade pairs, where the pre-upgrade entries are retained byte-for-byte beside the new ones), the {0x01, 0x02}-only key-flag census, the embedded/index-entry field offsets, the sub-record markers and descriptors, and the 0 = ADS / 2 = snapshot discriminator are raw-disk decoded across the corpus and corroborated in the driver: RefsAddFileNameIndexEntry ORs the directory bit and gates the 84/72-byte length, RefsCreateStreamSnapshot sets the snapshot StreamSummary bit, and the hard-link identity pair is written by RefsLinkFileToSelf. The remaining field-level statements on this page are registered as — each with its own evidence tier and witness in the claim register. Also registered for statements on this page:.