ReFS Forensics Reference

USN Journal

The USN (Update Sequence Number) Journal records every change to files and directories on the volume. ReFS uses the USN_RECORD_V3 format with 128-bit file IDs (NTFS uses the older USN_RECORD_V2 form with a 64-bit file reference). The journal is not active by default — it must be enabled with fsutil usn createjournal.

On-disk ReFS journal records are version 3. ReFS’s 128-bit file IDs require the USN_RECORD_V3 layout; fsutil usn queryjournal on ReFS reports Maximum record version supported : 3. Version 2 (NTFS’s 64-bit form) and version 4 (USN_RECORD_V4, the NTFS-only range-tracking / extent record — fsutil usn enablerangetracking refuses a ReFS volume with “A local NTFS volume is required for this operation”) do not occur on a ReFS journal. The parser decodes every record with the V3 field layout; it still accepts a V2 or V4 record if one is present (a non-ReFS journal, or crafted/corrupt input) but prints a one-time note that such records are parsed with V3 offsets and their fields are not validated — it does not silently refuse them.

USN_RECORD_V3 layout

OffsetSizeFieldDescription
0x004Record length (u32)Total record size = pad8(0x4C + UTF-16 name byte length); name-dominated, driver minimum 80 (0x50), observed 80–312 B across the corpus, no fixed ceiling — all records 8-byte aligned
0x042Major version (u16)3 on every ReFS record (the 128-bit File ID requires the V3 layout). The parser decodes with V3 offsets and tolerates a stray V2/V4 — a non-ReFS/corrupt input — with a one-time note rather than refusing it (see above)
0x062Minor version (u16)Always 0
0x0816File ID (u128)See 128-bit File ID structure below
0x1816Parent file ID (u128)Same format, but this is the current parent directory’s reference. Unlike the File ID at 0x08 — the file’s fixed identity — this tracks where the name currently lives, so it changes when the file is moved to another directory. Its ordinal (lower 8 bytes) is always 0: a directory is addressed as OID:0, so the parent reference reduces to the parent directory’s own OID.
0x288USN (u64)The record’s own USN = its virtual byte offset in the journal (monotonic, never reused; can far exceed the live $J stream size because $J is a sliding window). A file’s $SI+0x40 LastUsn points at this value for its most recent record.
0x308Timestamp (FILETIME)100 ns ticks since 1601-01-01
0x384Reason (u32)See Reason codes below
0x3C4Source info (u32)Structurally 0 in every ReFS record (the source-info mechanism is unused on ReFS)
0x404Security ID (u32)Structurally 0 in every ReFS record (the record carries no SecurityId)
0x444File attributes (u32)Current WIN32_FILE_ATTRIBUTE flags
0x482File name length (u16)In bytes
0x4A2File name offset (u16)Offset from record start
0x4CvarFile name (UTF-16LE)

128-bit File ID structure

ComponentSizeMeaning
Upper 8 bytesu64The creation (home) directory’s Object Table OID — the directory the file was born in, frozen for the life of the object
Lower 8 bytesu64Per-directory entry ordinal — monotonically allocated, never reused after deletion (a deleted ordinal stays a permanent gap)

This File ID is exactly the (HomeOid, ordinal) FileRef described in File IDs. The upper 8 bytes name the directory whose B+-tree holds the file’s object (its type-0x40 backing), and the lower 8 bytes identify that object within the tree, so a USN record resolves to a B+-tree node location without a separate lookup table.

The File ID is stable in two senses. It is temporally stable — the ordinal is never re-used within its directory, so a deleted file’s File ID is never reassigned to a later file. It is also spatially stable — renaming or moving a file does not change its File ID. The upper half is the creation directory, not the current parent, so when a file is moved the File ID (0x08) stays fixed while the Parent file ID (0x18) changes to the new directory. Across 1,858 real cross-directory moves in the corpus, the File ID was unchanged in every case. A USN record therefore points to the same file across the whole journal, through any number of renames and moves.

Reason codes

Reason codes are bitmask flags. A single record can combine multiple flags (e.g. 0x80000100 = FILE_CREATE + CLOSE).

Code (hex)NameDescription
0x00000001DATA_OVERWRITEDefault data stream content overwritten
0x00000002DATA_EXTENDDefault data stream extended
0x00000004DATA_TRUNCATIONDefault data stream truncated
0x00000010NAMED_DATA_OVERWRITENamed data stream (ADS) overwritten
0x00000020NAMED_DATA_EXTENDNamed data stream extended
0x00000040NAMED_DATA_TRUNCATIONNamed data stream truncated
0x00000100FILE_CREATEFile or directory creation
0x00000200FILE_DELETEFile deleted
0x00000400EA_CHANGEExtended attributes changed
0x00000800SECURITY_CHANGESecurity descriptor (ACL) changed
0x00001000RENAME_OLD_NAMESource name in rename operation
0x00002000RENAME_NEW_NAMETarget name in rename operation
0x00004000INDEXABLE_CHANGEContent indexing attribute changed
0x00008000BASIC_INFO_CHANGETimestamps or file attributes changed
0x00010000HARD_LINK_CHANGEHard link count changed
0x00020000COMPRESSION_CHANGECompression state changed
0x00040000ENCRYPTION_CHANGEEFS encryption state changed
0x00080000OBJECT_ID_CHANGEObject ID changed
0x00100000REPARSE_POINT_CHANGEReparse point set or removed
0x00200000STREAM_CHANGENamed data stream added or removed
0x00400000TRANSACTED_CHANGEChange within a TxF transaction
0x00800000INTEGRITY_CHANGEData integrity attribute changed
0x80000000CLOSEHandle closed (OR-ed with the final reason)

Forensic patterns

The reason-code combination, not just the individual flag, is the forensic signature of an operation:

OperationUSN signature
Rename (same directory)RENAME_OLD_NAME (0x1000) + RENAME_NEW_NAME (0x2000); same File ID and same Parent file ID, only the name differs
Move (to another directory)RENAME_OLD_NAME + RENAME_NEW_NAME; same File ID, but the Parent file ID changes (old directory → new directory) — the file’s identity is preserved, only its location moves
Sparse flag setBasic info change (0x8000); file attributes change from 0x20 to 0x220
EFS encrypt/decryptTransient EFS0.LOG in system directory (OID 0x701); reason 0x40000
$RECYCLE.BIN creationCreated on demand at first Explorer deletion
Junction creationReparse point change (0x100000) as a separate event
Symlink creationReparse change combined with create (0x100100)
ADS on symlinkNot possible — ADS cannot be written to symlink files on ReFS

Storage location

The Change Journal is stored as a file entry inside OID 0x520, the FS Metadata directory — the ReFS equivalent of NTFS $Extend (see System OIDs). When journaling is active a type 0x30 B+-tree row appears with key "Change Journal" (UTF-16LE). Fresh volumes have no such entry — it is created by RefsCreateUsnJournal and removed by RefsDeleteUsnJournal.

Change Journal file entry structure

The “Change Journal” filename entry in OID 0x520 carries a stream_count field (3 on v3.14) and a value of ~720 bytes on v3.14. The value holds three sub-records after the standard directory-entry header (the embedded sub-record markers are described in Directory Entries):

Sub-recordTypeOffsetContent
1Multi-instance (0x80000002)varies (scan from 0xA8)Data stream extents (likely $J stream)
2Multi-instance (0x80000002)0x148Data stream extents (likely $Max stream)
3Single-instance (0x80000001)0x288Journal metadata, 240 bytes (likely $USN_INFO)

The $J data stream contains the actual USN_RECORD_V3 entries as non-resident extent data. The $Max stream holds the journal size parameters, and is also surfaced as the type-0xF0 $LOGGED_UTILITY_STREAM attribute (single-instance marker 0x80000001) on the Change Journal file — not to be confused with the type-0x100 $EFS attribute. The single-instance sub-record at 0x288 contains organizational metadata including a creation timestamp and journal configuration.

$J is a bounded rolling log: it is pre-allocated to a fixed size (typically 32 MiB or 128 MiB, set by $Max) and, once full, the oldest records are believed to be overwritten in place as new ones are appended — so its records fill the entire allocation. forefst therefore reads the whole allocated window (every extent), not a shorter “logical size” — the two multi-instance markers each carry a fixed data-stream type tag, not a byte length. Each record is self-describing (a record length and its own USN), so a partially-filled journal’s zero tail is skipped and the full read is complete. A record’s USN is its byte offset in $J, so a file’s LastUsn ($SI+0x40) is the offset of that file’s most recent record and is found in the journal while that record is still inside the live window; only changes older than the window’s oldest surviving record have been overwritten and are gone.

The journal is an in-place ring. A record whose USN is X sits at byte offset X modulo the allocation, the extents begin at file VCN 0, and old records are overwritten where they lie — the buffer is never trimmed from the front. The practical consequences: the whole allocation must be read; byte order is not time order, because after a wrap the newest records sit at the start of the buffer and the oldest in the middle; and a highest USN larger than the stream size is normal rather than a sign of damage. On the one corpus volume whose journal has wrapped, the buffer holds two generations side by side — the older one filling offsets 7,352,320–33,554,168 and the newer one 0–7,350,976, with the write head at about 7.35 MB of a 32 MiB window — and every one of its 217,892 records sits exactly at its USN modulo that window.

Activation detection

Image stateOID 0x520 row countChange Journal entry
Fresh (never activated)1 (descriptor only)Absent
Active journaling3+Present (type 0x30, “Change Journal”)
DeactivatedVariesMay persist with zeroed extents

Recreation: a journal that starts at USN 0 on a volume that has been used

fsutil usn createjournal does not resize the journal on ReFS and it does not merely reset a counter — it discards the existing history. The size arguments are not honoured: a request for a 1 MiB journal with a 256 KiB allocation delta produced a 32 MiB journal with delta 0, the same allocation every volume in this project’s corpus carries. Because the window never filled, nothing was lost to wrapping — and yet no record of any operation performed before the call survives.

That gives the examiner a signal worth checking early, because it is cheap and it changes what the journal can be used for:

What you seeWhat it means
USN range starts at 0 on a volume with substantial prior activityThe journal was recreated. Everything before that point is gone; the volume is not young
USN range starts at a non-zero value, spanning about one allocationA normal rolling window that has wrapped once or more — the oldest records aged out
USN range starts at 0 and the volume is genuinely newNothing to conclude

The middle row is what a busy volume looks like: on one Insider system volume the range runs 40,906,752 → 74,459,840 across a 32 MiB allocation — a span of exactly one window. The first row is what a recreated journal looks like: range 0 → 3,146,304 in the same 32 MiB, under a tenth of the window used, on a volume that had already had thousands of files written and deleted.

A recreated journal is not proof of intent — installers and management tools call createjournal too — but it does mean absence of a record in the journal is not evidence the operation did not happen, and any timeline built from it starts at the recreation, not at the volume’s birth.

Tooling

USN Journal parsing is integrated into forefst.py (parsing + display). Available via forefst.py <image> usn:

ModeDescription
DefaultAn activity summary (record count, reason-code distribution, time span)
--list (or -v)The on-screen record list with reason codes and file names
--csv [FILE]Export as CSV (a file export also prints the summary stats)
--json [FILE]Machine-readable JSON output

Every ReFS journal record is version 3.0 (the 128-bit File ID requires the V3 layout), so the export carries no version column.

CSV / JSON columns

The --csv and --json exports share one field set (17 columns, in this order):

#ColumnMeaning
1usnThe record’s own USN (virtual byte offset in the journal)
2timestampRecord time (FILETIME rendered as a date)
3reason_hexRaw reason bitmask
4reasonDecoded reason flag names
5filenameThe file or directory name
6entry_typedir when file_id == 0 (a directory self-reference), else file
7file_refhome_oid:file_id — the same value as files.FileRef
8home_oidThe containing/creation directory for a file; the object’s own OID for a directory
9file_idThe entry ordinal (lower half of the File ID); 0 for a directory
10parent_oidThe current parent directory’s OID
11pathThe resolved path for the record’s file
12attrs_hexRaw file-attribute bitmask
13attrsDecoded attribute names
14record_lengthThe on-disk record length
15security_idStructurally 0 on ReFS (see the layout above)
16source_infoStructurally 0 on ReFS
17parent_idxStructurally 0 on ReFS (a parent is a directory addressed as OID:0)

Directories do get USN records: a directory self-reference has file_id == 0, which is why entry_type reads dir. The security_id, source_info, and parent_idx columns are placed last because they are structurally 0 on every ReFS record and are retained only for completeness.

Cross-references

  • Directory Entries — per-file LastUsn at resident value offset 0x68; value+0x58 holds FileSize; UsnJournalId at 0x70
  • $STANDARD_INFORMATION — per-file LastUsn at $SI+0x40, UsnJournalId at $SI+0x48 ($SI+0x30 is an unpopulated slot)
  • System OIDs — OID 0x520 hosts the Change Journal file entry
  • File IDs — how the 128-bit File ID maps to a B+-tree location

Evidence

The recreation signal is confirmed, measured in the lab: 1 MiB requested, 32 MiB and delta 0 granted, no wrap, and no pre-call record surviving; contrasted against an Insider system volume whose range spans exactly one allocation.

The USN_RECORD_V3 layout, the 128-bit File ID split (upper = the creation/home directory OID, lower = entry ordinal), and the reason-code catalog are raw-disk decoded across the corpus. That the upper half is the creation directory and is frozen across rename and move — while the Parent file ID at 0x18 tracks the current parent — is disk-proven on 1,858 cross-directory RENAME_OLD_NAME/RENAME_NEW_NAME pairs across nine v3.14 volumes (File ID unchanged in every case) and corroborated in the driver (RefsMoveFile migrates only the FileId-resolution index, never the stored reference). The record length range and the pad8 rule are measured corpus-wide (driver minimum 80 (0x50), observed 80–312 B, 8-byte aligned). The USN ↔ $SI+0x40 LastUsn link is both decompiled and disk-proven — every sampled file’s LastUsn resolved to a $J record naming that file. Journal activation, the OID 0x520 Change Journal entry, and the type-0xF0 $Max attribute are corroborated in the driver (RefsSetupUsnJournal, RefsWriteFcbUsnRecordToJournal) and verified on disk. The in-place ring is observed, not assumed: across 14 corpus journals all 338,439 records sit at their USN modulo the allocation, and on the one volume whose journal has wrapped — 2.22 windows’ worth of records, every one of them past the end of the window — the buffer holds two generations cleanly split by position, which front-trimming cannot produce. The two quantities compared there are independent: the offset comes from the linear scan position, the USN from the record body.

The remaining field-level statements on this page are registered as — each with its own evidence tier and witness in the claim register.