NTFS $LogFile Redo/Undo Operations Explained
The NTFS log record header byte by byte, the 38 redo/undo opcodes, which ones matter in forensics, and how create, delete, rename and timestamp changes look.
TL;DR. An NTFS log record is an LFS header (LSN, previous LSN, transaction ID…) followed by an NTFS header: redo opcode, undo opcode, where the redo and undo bytes are, which open attribute they target, and where inside it. Thirty-eight opcodes exist (0x00–0x25), but a handful carry most of the forensic value: InitializeFileRecordSegment, DeallocateFileRecordSegment, CreateAttribute / DeleteAttribute, UpdateResidentValue, and the four Add/DeleteIndexEntry* operations. Everything else is bookkeeping you still need to resolve targets and group records.
If $LogFile feels opaque, it is because a single user action — "rename this file" — becomes several records, each of which only makes sense with the right context: which attribute is open under which index, which cluster size the volume uses, what the FILE record looked like before. This article is the decoder ring. The broader picture is in the complete $LogFile guide.
Two headers per record
Every log record starts with the LFS record header, followed by the client data. For the NTFS client, the client data starts with its own header. Offsets from the Linux ntfs3 fslog.c:
LFS record header (0x30 bytes)
| Offset | Size | Field |
|---|---|---|
0x00 | 8 | This LSN |
0x08 | 8 | Client previous LSN (same transaction) |
0x10 | 8 | Client undo-next LSN |
0x18 | 4 | Client data length |
0x1C | 4 | Client ID |
0x20 | 4 | Record type: 1 = client record, 2 = client restart |
0x24 | 4 | Transaction ID |
0x28 | 2 | Flags (0x1 = record continues on the next page) |
NTFS log record header (start of client data)
| Offset | Size | Field | Use |
|---|---|---|---|
0x00 | 2 | Redo operation | What to do |
0x02 | 2 | Undo operation | How to reverse it |
0x04 / 0x06 | 2 + 2 | Redo offset / length | Where the redo bytes are |
0x08 / 0x0A | 2 + 2 | Undo offset / length | Where the undo bytes are |
0x0C | 2 | Target attribute | Index into the open attribute table |
0x0E | 2 | LCNs to follow | Number of cluster numbers at 0x20 |
0x10 | 2 | Record offset | Offset of the attribute inside the FILE record (or of the entry in an index buffer) |
0x12 | 2 | Attribute offset | Offset of the change inside that attribute |
0x14 | 2 | Cluster block offset | In 512-byte units, inside the target cluster |
0x18 | 8 | Target VCN | Virtual cluster in the target attribute |
0x20 | 8 × n | Target LCNs | Physical clusters |
Record type 2 records are checkpoints (client restart areas), not changes. The restart area and LSN article covers them.
Finding the MFT entry a record touches
Records do not say "MFT entry 322". They say "attribute #N in the open attribute table, VCN v, cluster block offset c". When the target attribute is $MFT:$DATA, the entry is:
entry = (VCN × cluster_size + cluster_block_offset × 512) / mft_record_size
Two volume parameters are needed. A parser can take them from the boot sector or, as the in-browser $LogFile parser does, calibrate them from the log itself: InitializeFileRecordSegment records contain a full FILE record image whose header states its own record number, so the combination of cluster size and record size that maps most of them correctly wins.
The open attribute table is rebuilt from OpenNonresidentAttribute records and from the OpenAttributeTableDump and AttributeNamesDump written at each checkpoint. Its entry layout differs between NTFS client versions (0x28 vs 0x2C bytes), which is one of the places parsers can disagree.
The 38 operations, grouped
The names below are those of ntfs3's enum NTFS_LOG_OPERATION, which the parser also uses.
| Code | Operation | Group | Forensic value |
|---|---|---|---|
0x00 | Noop | — | Undo side of many redo-only records |
0x01 | CompensationLogRecord | Transaction | Written during rollback |
0x02 | InitializeFileRecordSegment | FILE record | High: full FILE record image of a new or reused entry |
0x03 | DeallocateFileRecordSegment | FILE record | High: entry freed (deletion) |
0x04 | WriteEndOfFileRecordSegment | FILE record | Low |
0x05 | CreateAttribute | Attribute | High: new attribute, e.g. $FILE_NAME or resident $DATA |
0x06 | DeleteAttribute | Attribute | High: attribute removed, e.g. old $FILE_NAME on rename |
0x07 | UpdateResidentValue | Attribute | High: $STANDARD_INFORMATION times, small resident values |
0x08 | UpdateNonresidentValue | Attribute | Medium: index buffers, $UsnJrnl writes, other streams |
0x09 | UpdateMappingPairs | Attribute | Medium: data runs (file growth) |
0x0A | DeleteDirtyClusters | Attribute | Low |
0x0B | SetNewAttributeSizes | Attribute | Medium: size changes |
0x0C / 0x0D | Add / DeleteIndexEntryRoot | Index | High: directory entries in $INDEX_ROOT |
0x0E / 0x0F | Add / DeleteIndexEntryAllocation | Index | High: directory entries in $INDEX_ALLOCATION |
0x10 | WriteEndOfIndexBuffer | Index | Low |
0x11 / 0x12 | SetIndexEntryVcnRoot / Allocation | Index | Low (B-tree plumbing) |
0x13 / 0x14 | UpdateFileNameRoot / Allocation | Index | Medium: duplicated $FILE_NAME info in $I30 |
0x15 / 0x16 | Set / ClearBitsInNonresidentBitMap | Bitmap | Low alone: allocation of clusters or MFT entries |
0x17 | HotFix | — | Rare |
0x18 | EndTopLevelAction | Transaction | Structure |
0x19–0x1B | Prepare / Commit / ForgetTransaction | Transaction | Transaction boundaries |
0x1C | OpenNonresidentAttribute | Table | Needed to resolve targets |
0x1D–0x20 | OpenAttributeTable / AttributeNames / DirtyPageTable / TransactionTable dumps | Checkpoint | Needed to resolve targets |
0x21 / 0x22 | UpdateRecordDataRoot / Allocation | Index | Medium: non-$I30 indexes ($ObjId, $Quota, $Secure) |
0x23 / 0x24 | UpdateRelativeDataInIndex / 2 | Index | Low |
0x25 | ZeroEndOfFileRecord | FILE record | Low |
Undo is usually the inverse of redo: AddIndexEntryRoot is undone by DeleteIndexEntryRoot, CreateAttribute by DeleteAttribute, SetBits… by ClearBits…, and UpdateResidentValue by another UpdateResidentValue carrying the old bytes. Operations whose rollback needs nothing, such as initialising a fresh FILE record, often have Noop as undo.
What common actions look like
These are the patterns a parser looks for. Real sequences contain more records (bitmaps, sizes, security, USN writes) and the exact order can vary by Windows version.
File creation
InitializeFileRecordSegmenton the new entry. Redo = complete FILE record image: header (sequence number, flags),$STANDARD_INFORMATION,$FILE_NAME(name, parent reference, four times), possibly a small$DATA.AddIndexEntryRootorAddIndexEntryAllocationin the parent directory's$I30index. The index entry embeds a$FILE_NAMEcopy.
Deletion
DeleteIndexEntryRoot/DeleteIndexEntryAllocationremoving the name from the parent (the removed entry is in the undo bytes, sometimes in redo).DeallocateFileRecordSegmenton the entry, withClearBitsInNonresidentBitMapon the MFT bitmap.
Rename or move
- dfir.ru documents a rename as
DeleteIndexEntryRoot,DeleteAttribute(old$FILE_NAME),CreateAttribute(new$FILE_NAME),AddIndexEntryRoot(How the $LogFile works?). A move is the same with a different parent in the added entry. For large directories the…Allocationvariants appear instead of…Root.
Timestamp change
UpdateResidentValuetargeting the$STANDARD_INFORMATIONattribute (the first attribute of a FILE record, at offset0x38in NTFS 3.1 records). Redo holds the new bytes, undo the old ones. The update can be partial — dfir.ru notes an update may start at the M timestamp — so a parser must use the attribute offset to know which of the four times it is reading. Timestomping detection builds on this.
Small file content
- A resident
$DATAattribute created withCreateAttributeor included in the FILE record image can carry the first bytes of a small file. Later edits are a different story: LogFileParser's documentation states that on modern Windows the content of resident data changes is generally not stored, only the fact that a change happened. See recovering deleted-file evidence.
Grouping records into actions
Two approaches:
- By transaction. Use the transaction ID and previous-LSN chain, bounded by
ForgetTransactionor commit records. Most faithful, harder to get right across checkpoints. - By MFT entry and proximity. Take records that touch the same entry within a window of LSNs. Simpler and robust to damaged pages, but two unrelated actions on the same file close together can merge.
The in-browser parser currently uses the second approach (a ±256-record window per MFT entry) and exposes every raw record so you can check the grouping. Transaction-based grouping is on its roadmap; LogFileParser and NTFS Log Tracker should be used to confirm.
Reading the bytes yourself
When an event matters, open the raw record: check the redo/undo opcodes, target attribute (for example #322:$STANDARD_INFORMATION or $MFT:$DATA), record offset, attribute offset and the hex. A 32-byte undo at attribute offset 0 of $STANDARD_INFORMATION holds four little-endian FILETIME values: created, modified, MFT changed, accessed.
Frequently asked questions
What are redo and undo operations in the $LogFile?
Each NTFS log record describes one metadata change twice: the redo operation and bytes say how to apply the change again, the undo operation and bytes say how to reverse it. Recovery replays redo for committed transactions and applies undo for uncommitted ones.
Which $LogFile operations show a file being created or deleted?
A creation usually shows InitializeFileRecordSegment on the new FILE record plus AddIndexEntryRoot or AddIndexEntryAllocation in the parent directory. A deletion shows DeleteIndexEntryRoot or DeleteIndexEntryAllocation followed by DeallocateFileRecordSegment.
Further reading
- Linux kernel,
fs/ntfs3/fslog.c— structure definitions and a full replay implementation. - Joakim Schicht, LogFileParser readme — which operations it decodes and what they contain.
- Maxim Suhanov, How the $LogFile works?