Directory Entries
key_flagsrecords 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:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | Row size (u32) | total byte size of the row (0x70 is the common value for a typical embedded row, not a constant flag) |
| 0x04 | 2 | Key offset (u16) | Byte offset from row start to key data |
| 0x06 | 2 | Key length (u16) | – |
| 0x08 | 2 | Reserved (u16) | a candidate deleted-row bit 0x04 here is confirmed-only and disk-unconfirmable — not relied upon |
| 0x0A | 2 | Value offset (u16) | Byte offset from row start to value data |
| 0x0C | 2 | Value length (u16) | – |
| 0x0E | 2 | Reserved (u16) | – |
See B+-Tree Node for the page-level node structure that holds these rows.
Key format (type 0x30)
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 2 | Entry type (u16) | Row type identifier (0x0030 = filename, 0x0020 = reverse index, 0x0040 = extent descriptor) |
| 0x02 | 2 | Key flags (u16) | Determines value interpretation |
| 0x04 | var | Filename (UTF-16LE) | File or directory name |
Key flags
Only two key_flags values exist on disk (distribution {0x01, 0x02} only, zero occurrences of 0x04):
| Value | The row is | Value Size | Description |
|---|---|---|---|
| 0x01 | an embedded record | >84 bytes | The file’s own metadata header follows in this row. Its $DATA may be inline or extent-backed — the flag does not say which |
| 0x02 | an index entry | 84 bytes, or 72 if written by a pre-v3.10 driver | A 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_flagsis 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$DATAattribute’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
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 40 | Structural metadata | Stream index (8B) + internal B+-tree management fields (32B) |
| 0x28 | 8 | Creation time | FILETIME (100 ns since 1601-01-01) |
| 0x30 | 8 | Modification time | FILETIME |
| 0x38 | 8 | Metadata change time | FILETIME |
| 0x40 | 8 | Last access time | FILETIME |
| 0x48 | 4 | File attributes (u32) | Win32 flags + ReFS extensions |
| 0x4C | 4 | Internal flags (u32) | ReFS-specific state |
| 0x50 | 8 | Security ID (u64) | Links to Security Descriptors table (OID 0x530) |
| 0x58 | 8 | File 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. |
| 0x60 | 8 | Allocated size (u64) | Allocation ≥ file size (roundup-to-cluster on most; inline byte count for tiny files). |
| 0x68 | 8 | LastUsn (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. |
| 0x70 | 8 | UsnJournalId (u64) | Journal epoch ID (one per volume); 0 if journal inactive. |
| 0x80 | 8 | NextFileId (u64) | Directory child-creation ordinal (mirrors $SI+0x58). |
| 0xA8+ | var | Embedded 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 Count | Common case |
|---|---|
| 1 | Normal file (default data stream only) |
| 2 | One extra component — sparse file or reparse point/symlink |
| 3 | Encrypted file ($DATA + $EFS), or EA/WSL file, or file with 1 ADS |
| 4 | File 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:
| Field | Leaf — attributes are here | Index — attributes are in a child |
|---|---|---|
| Level (descriptor + 0x0C) | 0 | non-zero |
| Flags (descriptor + 0x0D) | bit 0 clear | bit 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
| Marker | Meaning |
|---|---|
| 0x80000001 | Single-instance attribute (reparse 0xC0, EA info 0xD0, EA 0xE0) |
| 0x80000002 | Multi-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.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | Marker (u32) | 0x80000002 (multi-instance) |
| 0x04 | 4 | Descriptor (u32) | 0x000500B0 |
| 0x08 | var | Stream name | UTF-16LE null-terminated |
| – | 0-6 | Alignment padding | Pad to (offset - marker_start) % 8 == 4 |
Sub-record header (immediately after alignment):
| Offset | Size | Field | Description |
|---|---|---|---|
| +0x00 | 4 | Padding (u32) | Always 0 |
| +0x04 | 4 | Data area size (u32) | Value length minus 12 |
| +0x08 | 4 | Content offset (u32) | Always 0x0C |
| +0x0C | 4 | Summary size (u32) | Always 0x30 (48 bytes) |
| +0x10 | 2 | StreamSummary flags (u16) | 0 = ADS, 2 = snapshot (bit 1 set by RefsCreateStreamSnapshot) |
| +0x12 | 6 | Reserved | Always 0 |
| +0x18 | 8 | Allocated size (u64) | Cluster-aligned allocation |
| +0x20 | 8 | Stream size (u64) | Logical content length |
| +0x3C | var | Inline content | Present when SS flags = 0 (ADS only) |
| +0x44 | 8 | Stream 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).
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 | Marker (u32) | 0x80000002 |
| 0x04 | 4 | Descriptor (u32) | 0x000E0080 |
| 0x08 | 8 | Stream index (u64) | Matches ADS header+0x44 or default stream 0x0008 |
| 0x10 | var | Extent table | See 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.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 8 | Child 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. |
| 0x08 | 8 | Home-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). |
| 0x10 | 8 | Creation time | FILETIME |
| 0x18 | 8 | Modification time | FILETIME |
| 0x20 | 8 | Metadata change time | FILETIME |
| 0x28 | 8 | Last access time | FILETIME |
| 0x30 | 8 | Allocated size (u64) | Total allocated bytes (cluster-aligned) |
| 0x38 | 8 | File size (u64) | Logical content length |
| 0x40 | 4 | File attributes (u32) | Win32 flags + ReFS extensions (dir bit 0x10000000) |
| 0x44 | 4 / 12 | Padding | Zero; 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:
| Field | Embedded record (kf 0x01) | Index entry (kf 0x02) |
|---|---|---|
| Creation time | +0x28 | +0x10 |
| File attributes | +0x48 | +0x40 |
| Security ID | +0x50 | Not 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).
Hard links
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
- Reverse Index – the type 0x20 FileId-resolution rows in the same directory tree
- Extent Descriptors – extent-backed files link to type 0x40 extent rows
- Object Table – the home-dir backref in index entries resolves via the Object Table
- Standard Information – $SI layout differs by version
- Record placement and data residency – the two independent axes
- Hard Links – multi-name files and the size-matched link count
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:.