Loading learning content...
If NTFS were a city, the Master File Table (MFT) would be its comprehensive registry—a database that knows the identity, location, and properties of every single resident. The MFT is not merely important to NTFS; it is NTFS in a very real sense. Every file, every directory, every piece of metadata on an NTFS volume exists as an entry in this table.
Unlike simpler file systems where directories directly contain file data, NTFS maintains a relational model: the MFT holds all metadata, and directories merely reference MFT records. This architecture enables NTFS's advanced features—multiple names for the same file, alternate data streams, extensive security descriptors—while maintaining the performance characteristics demanded by modern operating systems.
By the end of this page, you will understand MFT record structure at the byte level, how attributes are organized within records, the difference between resident and non-resident data, how NTFS handles small files efficiently, and why the MFT is critical for file system performance and recovery.
The Master File Table is an array of fixed-size records, each describing one file or directory on the volume. Think of it as a database table where each row represents a file system object.
MFT Record Basics:
The $MFT File:
The MFT is stored as a file ($MFT) within the file system it describes—a self-referential structure. Record 0 of the MFT is the MFT record for $MFT itself. This record contains the data runs (cluster locations) of the entire MFT, enabling Windows to locate all other MFT records.
File References:
When NTFS needs to identify a file (in directories, security descriptors, etc.), it uses a File Reference—a 64-bit value containing:
The sequence number increments each time an MFT record is reused for a new file. If a program holds a reference to a deleted file's record number, the sequence number mismatch reveals that the reference is stale.
123456789101112131415161718192021222324252627282930
// NTFS File Reference structure (8 bytes)typedef struct _MFT_SEGMENT_REFERENCE { // Lower 48 bits: MFT record number // Upper 16 bits: Sequence number uint64_t value;} MFT_SEGMENT_REFERENCE, FILE_REFERENCE; // Extract components from a file reference#define MFT_RECORD_NUMBER(ref) ((ref).value & 0x0000FFFFFFFFFFFF)#define SEQUENCE_NUMBER(ref) ((ref).value >> 48) // Example: File reference 0x0003000000000042// MFT Record Number: 0x42 (66)// Sequence Number: 0x0003 (3)// This is the 3rd version of file at MFT record 66 // Create a file referenceFILE_REFERENCE make_file_reference(uint64_t record_num, uint16_t seq) { FILE_REFERENCE ref; ref.value = (uint64_t)record_num | ((uint64_t)seq << 48); return ref;} // Validate a file reference against current MFT recordbool validate_reference(FILE_REFERENCE ref, MFT_RECORD *record) { // The sequence number in the reference must match // the sequence number in the MFT record header return SEQUENCE_NUMBER(ref) == record->sequence_number && (record->flags & FILE_RECORD_IN_USE);}Consider this scenario: Process A opens file X (MFT record 100, sequence 5). File X is deleted and a new file Y is created (reusing MFT record 100, sequence 6). If Process A tries to access the file using its old handle, the system detects the sequence number mismatch and returns STATUS_INVALID_PARAMETER rather than corrupting or leaking data from file Y.
Every MFT record begins with a fixed header, followed by a variable collection of attributes. Let's examine the structure in detail.
MFT Record Header:
The first 42-48 bytes of each record contain the header:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 bytes | Signature | Magic number 'FILE' (0x454C4946) or 'BAAD' for bad record |
| 0x04 | 2 bytes | Update Sequence Offset | Offset to fixup array |
| 0x06 | 2 bytes | Update Sequence Count | Size of fixup array in words |
| 0x08 | 8 bytes | $LogFile Sequence Number | LSN of last logged operation |
| 0x10 | 2 bytes | Sequence Number | Times this record has been reused |
| 0x12 | 2 bytes | Hard Link Count | Number of directory entries referencing this file |
| 0x14 | 2 bytes | First Attribute Offset | Byte offset to first attribute |
| 0x16 | 2 bytes | Flags | In-use, directory, view index, etc. |
| 0x18 | 4 bytes | Used Size | Actual bytes used in this record |
| 0x1C | 4 bytes | Allocated Size | Total bytes allocated (usually 1024) |
| 0x20 | 8 bytes | Base Record Reference | For extension records, points to base |
| 0x28 | 2 bytes | Next Attribute ID | Next available attribute identifier |
| 0x2A | 2 bytes | Record Number (XP+) | MFT record number (Windows XP and later) |
| 0x2C | 4 bytes | Record Number (XP+) | High part of record number |
The Update Sequence Array (USA):
NTFS uses the Update Sequence Array (also called 'fixup') to detect partial writes that could corrupt data. Here's how it works:
If a power failure occurs mid-write, some sectors will have the new sequence value while others have the old (or original data). NTFS detects this mismatch and knows the record is corrupt—triggering recovery from $LogFile.
+------------------+------------------+
| Sector 0 (512B) | Sector 1 (512B) | <- 1024-byte MFT record
+------------------+------------------+
| ^ | ^
| | | |
Original Replaced Original Replaced
bytes with seq# bytes with seq#
value value
1234567891011121314151617181920212223242526272829303132333435363738394041424344
// MFT Record Header structuretypedef struct _MFT_RECORD_HEADER { char signature[4]; // "FILE" or "BAAD" uint16_t usa_offset; // Update sequence array offset uint16_t usa_count; // USA size in words uint64_t lsn; // $LogFile sequence number uint16_t sequence_number; // Reuse counter uint16_t hard_link_count; // Directory entry count uint16_t first_attribute_offset; // Where attributes begin uint16_t flags; // FILE_RECORD_IN_USE, etc. uint32_t used_size; // Bytes actually used uint32_t allocated_size; // Bytes allocated (1024) FILE_REFERENCE base_record; // For extension records uint16_t next_attribute_id; // Next attr ID to assign uint16_t align; // Alignment padding uint32_t mft_record_number; // This record's number} MFT_RECORD_HEADER; // MFT Record Flags#define FILE_RECORD_IN_USE 0x0001 // Record is allocated#define FILE_RECORD_IS_DIRECTORY 0x0002 // This is a directory#define FILE_RECORD_IS_4 0x0004 // Unknown/internal#define FILE_RECORD_IS_VIEW_INDEX 0x0008 // Has view index // Reading and validating an MFT recordbool read_mft_record(HANDLE volume, uint64_t record_num, MFT_RECORD_HEADER *record) { // Calculate byte offset from MFT start uint64_t offset = record_num * MFT_RECORD_SIZE; // Read the record if (!read_from_mft(volume, offset, record, MFT_RECORD_SIZE)) return false; // Validate signature if (memcmp(record->signature, "FILE", 4) != 0) { if (memcmp(record->signature, "BAAD", 4) == 0) return false; // Corrupted record return false; // Unknown signature } // Apply fixup (restore USA-replaced bytes) return apply_fixup(record);}After the MFT record header, the remainder of the record contains attributes—the building blocks of all NTFS file information. Each attribute has a header followed by either the attribute's data (resident) or pointers to external clusters (non-resident).
Attribute Header:
Every attribute begins with a common header structure:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 4 bytes | Type | Attribute type code (e.g., 0x30 = $FILE_NAME) |
| 0x04 | 4 bytes | Length | Total length of this attribute |
| 0x08 | 1 byte | Non-resident Flag | 0 = resident, 1 = non-resident |
| 0x09 | 1 byte | Name Length | Attribute name length in characters |
| 0x0A | 2 bytes | Name Offset | Offset to attribute name (if named) |
| 0x0C | 2 bytes | Flags | Compressed, encrypted, sparse |
| 0x0E | 2 bytes | Attribute ID | Unique ID within this MFT record |
Resident Attribute Layout:
If non-resident flag is 0, the attribute data is stored directly within the MFT record:
+----------------+ +-- Common Header (14 bytes) --+
| Type (4) | | |
| Length (4) | | Followed by: |
| Flags (2) | | |
| Non-res (1) | +-------------------------------+
| Name len (1) |
| Name off (2) | +-- Resident Part (8 bytes) ---+
| Attr flags (2) | | Content Size (4) |
| ID (2) | | Content Offset (2) |
+----------------+ | Indexed Flag (1) |
| [Name if any] | | Padding (1) |
+----------------+ +-------------------------------+
| CONTENT | <-- Actual attribute data
+----------------+
Non-Resident Attribute Layout:
If non-resident flag is 1, the attribute data resides on disk clusters. The attribute contains data runs describing where the data is located:
+----------------+
| Common Header |
+----------------+
| Starting VCN | (8 bytes) First VCN of this portion
| Last VCN | (8 bytes) Last VCN
| Data Run Off | (2 bytes) Offset to data runs
| Compr Unit Size| (2 bytes) Compression unit (if compressed)
| Padding (4) |
| Allocated Size | (8 bytes) Clusters allocated × cluster size
| Data Size | (8 bytes) Actual data length
| Initialized Sz | (8 bytes) Initialized portion length
| [Compressed Sz]| (8 bytes) If compressed
+----------------+
| Data Runs | Variable-length run encodings
+----------------+
NTFS tracks three sizes for non-resident attributes:
• Allocated Size: Total disk space reserved (may exceed data size due to cluster rounding) • Data Size: Logical size of the data (what applications see) • Initialized Size: How much has been written. Reading beyond initialized size returns zeros without touching disk.
This distinction enables fast file extension—NTFS can allocate space without initializing it, then lazily zero-fill on read.
NTFS defines a set of standard attribute types, each identified by a type code. Every file has at least two required attributes: $STANDARD_INFORMATION and $FILE_NAME.
| Type Code | Name | Description |
|---|---|---|
| 0x10 | $STANDARD_INFORMATION | Timestamps, flags, security ID, owner ID, quota info |
| 0x20 | $ATTRIBUTE_LIST | List of attributes when they span multiple MFT records |
| 0x30 | $FILE_NAME | File name(s) and parent directory reference |
| 0x40 | $OBJECT_ID | Unique object identifier (GUID) |
| 0x50 | $SECURITY_DESCRIPTOR | Legacy ACL storage (rarely used; see $Secure) |
| 0x60 | $VOLUME_NAME | Volume label (only in $Volume) |
| 0x70 | $VOLUME_INFORMATION | Volume version and flags (only in $Volume) |
| 0x80 | $DATA | File contents or unnamed data stream |
| 0x90 | $INDEX_ROOT | B+ tree root for directory indices |
| 0xA0 | $INDEX_ALLOCATION | B+ tree nodes for large directories |
| 0xB0 | $BITMAP | Allocation bitmap for indices or MFT |
| 0xC0 | $REPARSE_POINT | Symbolic link, junction, or other reparse data |
| 0xD0 | $EA_INFORMATION | Extended attributes metadata |
| 0xE0 | $EA | Extended attributes (legacy and WSL metadata) |
| 0x100 | $LOGGED_UTILITY_STREAM | EFS encryption keys |
$STANDARD_INFORMATION (0x10):
Every file has this attribute. It contains:
$FILE_NAME (0x30):
Contains the file's name and parent directory reference. A file may have multiple $FILE_NAME attributes:
Each $FILE_NAME also includes a copy of timestamps—these are the timestamps displayed in Explorer and returned by basic directory enumeration.
$DATA (0x80):
The file's actual content. Most files have one unnamed $DATA attribute. However, NTFS supports named data streams—additional $DATA attributes with names:
file.txt -> default (unnamed) $DATA
file.txt:secret -> named $DATA stream 'secret'
file.txt:Zone.Identifier -> named stream added by IE/Edge
This Alternate Data Stream (ADS) capability is used by Windows for various purposes (downloaded file warnings, resource forks for Mac compatibility) and has also been exploited by malware to hide data.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
// NTFS Attribute Type Codestypedef enum _ATTRIBUTE_TYPE { AT_UNUSED = 0x00, AT_STANDARD_INFORMATION = 0x10, AT_ATTRIBUTE_LIST = 0x20, AT_FILE_NAME = 0x30, AT_OBJECT_ID = 0x40, AT_SECURITY_DESCRIPTOR = 0x50, AT_VOLUME_NAME = 0x60, AT_VOLUME_INFORMATION = 0x70, AT_DATA = 0x80, AT_INDEX_ROOT = 0x90, AT_INDEX_ALLOCATION = 0xA0, AT_BITMAP = 0xB0, AT_REPARSE_POINT = 0xC0, AT_EA_INFORMATION = 0xD0, AT_EA = 0xE0, AT_LOGGED_UTILITY_STREAM = 0x100, AT_END = 0xFFFFFFFF // Marks end of attributes} ATTRIBUTE_TYPE; // $STANDARD_INFORMATION structuretypedef struct _STANDARD_INFORMATION { int64_t creation_time; // FILETIME format int64_t modification_time; // Last data modification int64_t mft_change_time; // Last MFT record modification int64_t access_time; // Last access uint32_t file_attributes; // DOS attributes uint32_t max_versions; // Max number of versions (0) uint32_t version_number; // Current version (0) uint32_t class_id; // Classification ID // NTFS 3.0+ fields: uint32_t owner_id; // Owner ID from $Quota uint32_t security_id; // Index into $Secure uint64_t quota_charged; // Bytes charged to quota uint64_t usn; // Update Sequence Number} STANDARD_INFORMATION; // $FILE_NAME structuretypedef struct _FILE_NAME { FILE_REFERENCE parent_directory; // Parent directory MFT ref int64_t creation_time; int64_t modification_time; int64_t mft_change_time; int64_t access_time; int64_t allocated_size; // Disk space allocated int64_t data_size; // Actual data size uint32_t flags; // File attributes uint32_t reparse_tag; // Reparse point tag if applicable uint8_t name_length; // Name length in characters uint8_t name_type; // POSIX, Win32, DOS, Win32+DOS wchar_t name[1]; // Variable-length Unicode name} FILE_NAME; // File name types#define FILE_NAME_POSIX 0x00 // Case-sensitive, any chars#define FILE_NAME_WIN32 0x01 // Standard Windows name#define FILE_NAME_DOS 0x02 // 8.3 DOS name#define FILE_NAME_WIN32_DOS 0x03 // Both Win32 and 8.3 compatibleWhen attribute data is too large to fit in the MFT record (non-resident), NTFS stores it in clusters on the volume. The attribute contains data runs (also called run lists)—a compact encoding that maps Virtual Cluster Numbers (VCNs within the file) to Logical Cluster Numbers (LCNs on the disk).
Data Run Encoding:
Each data run describes a contiguous extent (sequence of clusters). Runs are encoded using a variable-length format that's extremely space-efficient:
+--------+----------------+----------------+
| Header | Length (1-8 B) | Offset (1-8 B) |
+--------+----------------+----------------+
1 byte Variable Variable
The header byte encodes the sizes of the length and offset fields:
Important: The offset is a signed value relative to the previous run's LCN (delta encoding). This allows negative offsets and makes the encoding more compact.
Header Byte Examples:
0x11: 1 byte for length, 1 byte for offset0x21: 1 byte for length, 2 bytes for offset0x31: 1 byte for length, 3 bytes for offset0x42: 2 bytes for length, 4 bytes for offset0x00: End of run list marker12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// Data Run parsing exampletypedef struct _DATA_RUN { uint64_t starting_vcn; // First VCN covered by this run uint64_t cluster_count; // Number of clusters in run int64_t starting_lcn; // LCN on disk (or -1 for sparse hole)} DATA_RUN; // Parse a data run list into an array of runsint parse_data_runs(uint8_t *run_list, DATA_RUN *runs, int max_runs) { int run_count = 0; int64_t prev_lcn = 0; uint64_t current_vcn = 0; while (*run_list != 0x00 && run_count < max_runs) { uint8_t header = *run_list++; uint8_t length_size = header & 0x0F; // Lower nibble uint8_t offset_size = (header >> 4) & 0x0F; // Upper nibble if (length_size == 0) break; // Invalid // Read cluster count (length_size bytes, little-endian) uint64_t cluster_count = 0; for (int i = 0; i < length_size; i++) { cluster_count |= (uint64_t)(*run_list++) << (8 * i); } // Read LCN offset (offset_size bytes, signed, little-endian) int64_t lcn_offset = 0; if (offset_size > 0) { for (int i = 0; i < offset_size; i++) { lcn_offset |= (int64_t)(*run_list++) << (8 * i); } // Sign extend if highest bit is set if (run_list[-1] & 0x80) { for (int i = offset_size; i < 8; i++) { lcn_offset |= (int64_t)0xFF << (8 * i); } } } // Build the run entry runs[run_count].starting_vcn = current_vcn; runs[run_count].cluster_count = cluster_count; if (offset_size == 0) { // Sparse run (hole): no actual clusters allocated runs[run_count].starting_lcn = -1; // Sparse marker } else { // Normal run: calculate absolute LCN prev_lcn += lcn_offset; runs[run_count].starting_lcn = prev_lcn; } current_vcn += cluster_count; run_count++; } return run_count;} // Example: Data runs for a 300KB file with 4KB clusters (75 clusters)// Split into 3 extents at LCNs 1000, 5000, and 800//// Run 1: VCN 0-24 -> LCN 1000-1024 (25 clusters)// Header: 0x21 (1 byte count, 2 bytes offset)// Bytes: 21 19 E8 03// ^ ^ ^^^^^---- LCN offset: 0x03E8 = 1000// | +----------- Cluster count: 25 (0x19)// +-------------- Header//// Run 2: VCN 25-49 -> LCN 5000-5024 (25 clusters)// Header: 0x21// Bytes: 21 19 A0 0F (offset = 5000-1000 = 4000 = 0x0FA0)//// Run 3: VCN 50-74 -> LCN 800-824 (25 clusters)// Header: 0x22 (offset needs sign extension for negative)// Bytes: 22 19 60 EE FF (offset = 800-5000 = -4200, signed)//// Terminator: 00When offset_size is 0 (header like 0x01, 0x02, etc.), the run represents a sparse hole—clusters that are logically part of the file but have no physical storage. Reading these clusters returns zeros. This is how NTFS implements sparse files without wasting disk space on empty regions.
One of NTFS's most elegant optimizations is the ability to store small files entirely within their MFT records. Understanding when data is resident versus non-resident is crucial for both performance analysis and forensic investigation.
Resident Data:
When a file is small enough, its $DATA attribute content is stored directly in the MFT record. Given a 1KB MFT record size:
Practically, files up to approximately 700-800 bytes can be stored entirely resident (the exact limit depends on filename length and other attributes).
Benefits of Resident Storage:
Transition to Non-Resident:
When a file grows beyond the resident threshold:
This transition is transparent to applications—they see a normal file that has simply grown.
Resident data is particularly important for digital forensics. When a file is deleted, its MFT record is marked as unused but not immediately overwritten. If the file was resident, its complete contents remain in the MFT record until it's reused for a new file—potentially much longer than deleted non-resident data, which can be overwritten by normal file operations.
What happens when a file has too many attributes to fit in a single 1KB MFT record? NTFS uses extension records and an $ATTRIBUTE_LIST to span multiple records.
When Extension Is Needed:
The $ATTRIBUTE_LIST Attribute (Type 0x20):
When attributes won't fit, NTFS:
The $ATTRIBUTE_LIST enumerates ALL attributes (regardless of which record contains them):
+-------------------+
| Attribute Type | 4 bytes
+-------------------+
| Record Length | 2 bytes
+-------------------+
| Name Length | 1 byte
+-------------------+
| Name Offset | 1 byte
+-------------------+
| Starting VCN | 8 bytes (for non-resident, else 0)
+-------------------+
| MFT Reference | 8 bytes (which record contains this attr)
+-------------------+
| Attribute ID | 2 bytes
+-------------------+
| [Attribute Name] | Variable
+-------------------+
Extension Record Structure:
Extension records are valid MFT records with:
A file may have many extension records. To read any attribute, NTFS:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// $ATTRIBUTE_LIST entry structuretypedef struct _ATTRIBUTE_LIST_ENTRY { uint32_t type; // Attribute type code uint16_t length; // Length of this entry uint8_t name_length; // Name length in characters uint8_t name_offset; // Offset to attribute name uint64_t starting_vcn; // For non-resident attrs FILE_REFERENCE mft_reference; // Which MFT record has this attr uint16_t attribute_id; // Attribute's ID // wchar_t name[]; // Optional attribute name} ATTRIBUTE_LIST_ENTRY; // Example: File with 500 hard links// Each hard link requires a $FILE_NAME attribute (~80 bytes)// 500 × 80 = 40,000 bytes of $FILE_NAME attributes alone// // This requires approximately 40 MFT extension records!// The base record contains:// - $STANDARD_INFORMATION// - $ATTRIBUTE_LIST (non-resident, listing all 500 $FILE_NAMEs)// - $DATA (or data runs)// // Extension records 1-N each contain ~12 $FILE_NAME attributes // Finding an attribute using the attribute listvoid *find_attribute(MFT_RECORD *base, ATTRIBUTE_TYPE type, wchar_t *name, HANDLE volume) { // First check if base record has an $ATTRIBUTE_LIST void *attr_list = find_attribute_in_record(base, AT_ATTRIBUTE_LIST, NULL); if (attr_list == NULL) { // No attribute list—all attributes are in the base record return find_attribute_in_record(base, type, name); } // Parse the attribute list ATTRIBUTE_LIST_ENTRY *entry = get_attr_list_data(attr_list); while (entry->type != 0xFFFFFFFF) { if (entry->type == type && name_matches(entry, name)) { // Found it—load the containing record MFT_RECORD *target_record = load_mft_record( volume, MFT_RECORD_NUMBER(entry->mft_reference) ); return find_attribute_in_record(target_record, type, name); } entry = next_attr_list_entry(entry); } return NULL; // Attribute not found}The MFT is central to NTFS operation, and its placement and management significantly affect performance.
Initial Placement:
When formatting a volume, NTFS places the MFT near the beginning (but not at the very start—the boot sector comes first). The exact location is recorded in the boot sector's MFT LCN field.
The MFT Zone:
NTFS reserves space around the MFT for future growth—the MFT Zone. By default, this is 12.5% of the volume:
NtfsReservedMftZonePossible in NTFS driversHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem
NtfsMftZoneReservation = 1 (12.5%), 2 (25%), 3 (37.5%), 4 (50%)
MFT Growth:
As files are created, the MFT grows:
MFT Fragmentation Impact:
MFT fragmentation severely impacts performance:
Windows includes defrag.exe with MFT optimization capabilities:
defrag C: /X # Defragment MFT and system files
defrag C: /U /V # Analyze fragmentation levels
NTFS maintains $MFTMirr—a backup of the first 4 MFT records (records 0-3). These are the most critical: $MFT itself, $MFTMirr, $LogFile, and $Volume. If the primary MFT's start is damaged, recovery tools can use $MFTMirr (typically placed at the volume's midpoint) to bootstrap recovery.
Viewing MFT Information:
You can examine MFT statistics using built-in tools:
# NTFS volume information including MFT zone
fsutil fsinfo ntfsinfo C:
# Specific MFT statistics
fsutil fsinfo mftzone C:
# Defrag analysis shows MFT fragmentation
defrag C: /A /V
Output Example:
Ntfs Volume Serial Number : 0x1234567812345678
NTFS Version : 3.1
LFS Version : 2.0
Number Sectors : 0x000000001d5fffff
Total Clusters : 0x0000000003abffff
Free Clusters : 0x0000000001234567
Total Reserved : 0x0000000000000800
Bytes Per Sector : 512
Bytes Per Physical Sector : 4096
Bytes Per Cluster : 4096
Bytes Per FileRecord Segment : 1024
Clusters Per FileRecord Segment : 0
Mft Valid Data Length : 0x0000000012340000
Mft Start Lcn : 0x00000000000c0000
Mft2 Start Lcn : 0x0000000000000002
We've explored the Master File Table in depth—the data structure that defines every file and directory in NTFS. Let's consolidate our understanding:
What's Next:
Now that we understand MFT structure and attributes in general, we'll focus on a particularly important attribute type: resident attributes. We'll examine how small file data, $STANDARD_INFORMATION, $FILE_NAME, and other attributes that always reside within MFT records enable NTFS's efficiency and provide crucial information for system operations and forensic analysis.
You now understand the Master File Table's architecture—from record headers and file references to attribute structures and data runs. You can explain how NTFS locates file metadata, why resident storage is efficient, and how extension records handle complex files. Next, we'll explore resident attributes in detail.