Loading content...
Most file systems treat metadata and data as fundamentally different entities—metadata in special structures, data in allocated blocks. NTFS takes a more unified approach: both metadata and small file data can reside directly within MFT records. This design decision—resident attributes—is central to NTFS's efficiency and elegance.
When you create a tiny text file or a Windows shortcut (.lnk), the file's entire contents may never touch the volume's data clusters. Instead, everything—timestamps, permissions, filename, AND the actual data—lives within a single MFT record. This locality has profound implications for performance, fragmentation, and even forensic analysis.
By the end of this page, you will understand which attributes are always resident, how small file optimization works, the anatomy of key resident attributes ($STANDARD_INFORMATION, $FILE_NAME), and the implications for system performance and data recovery.
What Makes an Attribute Resident?
An attribute is resident when its data is stored directly within the attribute's MFT record entry, immediately following the attribute header. No external clusters are allocated; no data runs are needed. The attribute is self-contained.
Resident Attribute Layout:
+----------------------------+
| Common Attribute Header | 14 bytes
| Type, Length, Flags... |
+----------------------------+
| Resident Header | 8 bytes
| Content Length (4) |
| Content Offset (2) |
| Indexed Flag (1) |
| Padding (1) |
+----------------------------+
| [Attribute Name if any] | Variable
+----------------------------+
| CONTENT DATA | Variable (within record limits)
+----------------------------+
The Resident Header Fields:
Why Resident Storage Matters:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Resident attribute structuretypedef struct _RESIDENT_ATTRIBUTE_HEADER { // Common header fields (14 bytes) uint32_t type; // Attribute type code uint32_t length; // Total attribute length uint8_t non_resident_flag; // 0 for resident uint8_t name_length; // Attribute name length (chars) uint16_t name_offset; // Offset to attribute name uint16_t flags; // Attribute flags uint16_t attribute_id; // Unique ID in this record // Resident-specific fields (8 bytes) uint32_t content_length; // Length of attribute data uint16_t content_offset; // Offset to attribute data uint8_t indexed_flag; // 1 if indexed by directory uint8_t padding;} RESIDENT_ATTRIBUTE_HEADER; // Reading resident attribute datavoid *get_resident_content(void *attribute) { RESIDENT_ATTRIBUTE_HEADER *header = (RESIDENT_ATTRIBUTE_HEADER *)attribute; // Verify this is actually resident if (header->non_resident_flag != 0) { return NULL; // This is non-resident, different handling needed } // Content is at attribute base + content_offset return (uint8_t *)attribute + header->content_offset;} // Example: Reading a small file's databool read_small_file(MFT_RECORD *record, void *buffer, size_t *size) { // Find the $DATA attribute void *data_attr = find_attribute(record, AT_DATA, NULL); if (data_attr == NULL) return false; RESIDENT_ATTRIBUTE_HEADER *header = data_attr; // Check if resident if (header->non_resident_flag == 0) { // Resident: copy directly from MFT record void *content = get_resident_content(data_attr); *size = header->content_length; memcpy(buffer, content, *size); return true; } else { // Non-resident: need to read clusters return read_non_resident_data(data_attr, buffer, size); }}Certain NTFS attributes are always resident by design. They contain critical metadata that must be immediately accessible when reading an MFT record.
$STANDARD_INFORMATION (Type 0x10):
Every file and directory has exactly one $STANDARD_INFORMATION attribute, always resident. It contains:
Offset Size Field
0x00 8 Creation Time
0x08 8 Modification Time
0x10 8 MFT Record Change Time
0x18 8 Access Time
0x20 4 File Attributes (DOS flags)
0x24 4 Maximum Versions
0x28 4 Version Number
0x2C 4 Class ID
--- NTFS 3.0+ extended fields ---
0x30 4 Owner ID
0x34 4 Security ID
0x38 8 Quota Charged
0x40 8 Update Sequence Number (USN)
Time Stamps:
NTFS stores timestamps as 64-bit values representing 100-nanosecond intervals since January 1, 1601 (UTC). This is Windows FILETIME format:
// Convert FILETIME to human-readable
void filetime_to_string(int64_t ft, char *buffer) {
// FILETIME is 100ns intervals since 1601-01-01
// Unix epoch (1970-01-01) = 116444736000000000 * 100ns
time_t unix_time = (ft - 116444736000000000LL) / 10000000LL;
strftime(buffer, 32, "%Y-%m-%d %H:%M:%S", gmtime(&unix_time));
}
File Attributes (DOS Flags):
The attributes field contains traditional DOS flags plus NTFS additions:
| Flag | Value | Meaning |
|---|---|---|
| FILE_ATTRIBUTE_READONLY | 0x0001 | File is read-only |
| FILE_ATTRIBUTE_HIDDEN | 0x0002 | File is hidden |
| FILE_ATTRIBUTE_SYSTEM | 0x0004 | System file |
| FILE_ATTRIBUTE_DIRECTORY | 0x0010 | This is a directory |
| FILE_ATTRIBUTE_ARCHIVE | 0x0020 | File needs archiving |
| FILE_ATTRIBUTE_DEVICE | 0x0040 | Reserved for device files |
| FILE_ATTRIBUTE_NORMAL | 0x0080 | Normal file (no other flags) |
| FILE_ATTRIBUTE_TEMPORARY | 0x0100 | Temporary file |
| FILE_ATTRIBUTE_SPARSE_FILE | 0x0200 | Sparse file |
| FILE_ATTRIBUTE_REPARSE_POINT | 0x0400 | Has reparse point |
| FILE_ATTRIBUTE_COMPRESSED | 0x0800 | Compressed file |
| FILE_ATTRIBUTE_OFFLINE | 0x1000 | Data not immediately available |
| FILE_ATTRIBUTE_NOT_INDEXED | 0x2000 | Not indexed by content indexing |
| FILE_ATTRIBUTE_ENCRYPTED | 0x4000 | Encrypted (EFS) |
The Security ID in $STANDARD_INFORMATION is an index into the $Secure system file—NOT an inline security descriptor. This deduplication means identical permissions don't consume extra space per file. The $Secure file is a B+ tree indexed by Security ID, containing the actual security descriptors.
The $FILE_NAME attribute (Type 0x30) is always resident and contains the file's name, parent directory reference, and a copy of key timestamps. A file may have multiple $FILE_NAME attributes:
$FILE_NAME Structure:
Offset Size Field
0x00 8 Parent Directory MFT Reference
0x08 8 Creation Time
0x10 8 Modification Time
0x18 8 MFT Record Change Time
0x20 8 Access Time
0x28 8 Allocated Size (disk space)
0x30 8 Data Size (actual size)
0x38 4 Flags
0x3C 4 Reparse Tag (if reparse point)
0x40 1 Name Length (characters)
0x41 1 Name Type (namespace)
0x42 var Filename (UTF-16LE)
Name Types (Namespaces):
| Value | Type | Description |
|---|---|---|
| 0 | POSIX | Case-sensitive, allows almost any character including colons |
| 1 | Win32 | Standard Windows naming (case-insensitive for comparison) |
| 2 | DOS | 8.3 format short name, uppercase |
| 3 | Win32+DOS | Name satisfies both Win32 and DOS requirements |
Why Two Sets of Timestamps?
Both $STANDARD_INFORMATION and $FILE_NAME contain timestamps. Why?
$STANDARD_INFORMATION timestamps: Updated by file operations (write, attribute changes). These are the 'real' file times.
$FILE_NAME timestamps: Updated only when the filename is modified (renamed). These are the times shown in directory listings.
This distinction is important for forensics: malware might modify $SI timestamps to hide activity, but $FN timestamps in directory entries may still reveal the truth.
Example: File with Multiple $FILE_NAME Attributes:
File: "MyDocument.txt"
MFT Record 1234:
$STANDARD_INFORMATION (1 instance)
Creation: 2024-01-15 10:30:00
Modified: 2024-01-20 14:22:00
$FILE_NAME #1 (Win32)
Parent: MFT Ref 5 (root directory)
Name: "MyDocument.txt"
Namespace: Win32 (1)
$FILE_NAME #2 (DOS)
Parent: MFT Ref 5 (root directory)
Name: "MYDOCU~1.TXT"
Namespace: DOS (2)
$DATA (unnamed)
Size: 156 bytes (resident)
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
// $FILE_NAME attribute structuretypedef struct _FILE_NAME_ATTRIBUTE { FILE_REFERENCE parent_directory; // Parent dir MFT reference int64_t creation_time; // Time file was created int64_t modification_time; // Time data was modified int64_t mft_change_time; // Time MFT record was modified int64_t access_time; // Time file was last accessed int64_t allocated_size; // Bytes allocated on disk int64_t data_size; // Actual file size uint32_t flags; // File attributes (same as DOS) uint32_t reparse_tag; // Reparse point tag (if applicable) uint8_t name_length; // Filename length in characters uint8_t name_type; // Namespace type wchar_t name[1]; // Variable-length filename (UTF-16)} FILE_NAME_ATTRIBUTE; // Filename namespace types#define FILE_NAME_POSIX 0 // Case-sensitive Unix-like names#define FILE_NAME_WIN32 1 // Standard Windows names#define FILE_NAME_DOS 2 // 8.3 DOS names only#define FILE_NAME_WIN32_DOS 3 // Satisfies both Win32 and DOS // Enumerate all filenames of a filevoid list_filenames(MFT_RECORD *record) { void *attr = NULL; int count = 0; while ((attr = find_next_attribute(record, AT_FILE_NAME, attr)) != NULL) { FILE_NAME_ATTRIBUTE *fn = get_resident_content(attr); wprintf(L"Filename #%d", ++count); wprintf(L" Name: %.*s", fn->name_length, fn->name); wprintf(L" Namespace: %s", fn->name_type == 0 ? "POSIX" : fn->name_type == 1 ? "Win32" : fn->name_type == 2 ? "DOS" : "Win32+DOS"); wprintf(L" Parent MFT Record: %llu", MFT_RECORD_NUMBER(fn->parent_directory)); wprintf(L" Size: %lld bytes", fn->data_size); }} // Generate DOS 8.3 short name from long namevoid generate_dos_name(const wchar_t *long_name, wchar_t *short_name) { // Rules for 8.3 generation: // 1. Convert to uppercase // 2. Remove spaces and most special characters // 3. Take first 6 valid characters + ~N + ext (first 3 chars) // 4. N is a sequence number to ensure uniqueness // Example: "My Long Document.txtx" -> "MYLONGD~1.TXT" // ... implementation ...}NTFS's ability to store $DATA attributes as resident enables a crucial optimization: small files require no cluster allocation. This is particularly significant for systems with millions of small files.
Resident $DATA Threshold:
The maximum resident $DATA size depends on:
Typical available space for resident $DATA:
MFT Record: 1024 bytes
- Header: ~48 bytes
- $STANDARD_INFO: ~72 bytes
- $FILE_NAME (Win32): ~90 bytes (assuming 20-char name)
- $FILE_NAME (DOS): ~76 bytes
- End marker: ~8 bytes
----------------------------
Remaining for $DATA: ~730 bytes
Files up to approximately 700-750 bytes can be fully resident (varies with filename length).
Performance Benefits:
| Operation | Resident File | Non-Resident File |
|---|---|---|
| Read entire file | 1 MFT read | 1 MFT read + N data reads |
| Read file size | 1 MFT read | 1 MFT read |
| Read metadata + data | 1 MFT read | 1 MFT read + N data reads |
| Disk seeks (random access) | 1 seek | 1 + N seeks |
| Create small file | 1 MFT write | 1 MFT write + cluster allocation + N data writes |
| Delete small file | 1 MFT write | 1 MFT write + bitmap update |
Real-World Impact:
Consider a code repository with 50,000 files where 60% are under 700 bytes (source files, configuration, scripts):
This explains why NTFS handles large numbers of small files more efficiently than file systems that always allocate external blocks.
Transition Mechanics:
When a resident file grows beyond the threshold:
When a non-resident file shrinks below the threshold:
compact command or defragmentation can force reconversionTo see if a file's data is resident, use tools like nfi (from the Windows OEM Support Tools) or third-party utilities like NTFSInfo. In the file's MFT record, look for the $DATA attribute: if its non-resident flag is 0, the data is resident.
Alternatively, the PowerShell command fsutil file layout <filename> shows detailed MFT information including residency status.
Beyond $STANDARD_INFORMATION and $FILE_NAME, several other attributes are typically or always resident:
$OBJECT_ID (Type 0x40) — Always Resident
A 64-byte attribute containing:
Object IDs enable file tracking across renames and moves. The Distributed Link Tracking service uses these to update shortcuts when target files move.
$VOLUME_NAME (Type 0x60) — Always Resident
Found only in $Volume (MFT record 3). Contains the volume label as a Unicode string.
$VOLUME_INFORMATION (Type 0x70) — Always Resident
Also only in $Volume. Contains:
$INDEX_ROOT (Type 0x90) — Always Resident
The root of a B+ tree index, always stored in the MFT record. For directories, this is the $I30 index (index of $FILE_NAME attributes). Contains:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// $OBJECT_ID attribute structure (always resident)typedef struct _OBJECT_ID_ATTRIBUTE { GUID object_id; // 16 bytes - unique identifier GUID birth_volume_id; // 16 bytes - volume where created GUID birth_object_id; // 16 bytes - original object ID GUID domain_id; // 16 bytes - reserved} OBJECT_ID_ATTRIBUTE; // Total: 64 bytes // $VOLUME_INFORMATION attribute (always resident)typedef struct _VOLUME_INFORMATION { uint64_t reserved; uint8_t major_version; // e.g., 3 uint8_t minor_version; // e.g., 1 = NTFS 3.1 uint16_t flags;} VOLUME_INFORMATION; // Volume flags#define VOLUME_IS_DIRTY 0x0001 // Needs chkdsk#define VOLUME_RESIZE_LOG_FILE 0x0002 // $LogFile needs resize#define VOLUME_UPGRADE_ON_MOUNT 0x0004 // Upgrade NTFS version#define VOLUME_MOUNTED_ON_NT4 0x0008 // Was mounted by NT4#define VOLUME_DELETE_USN_UNDERWAY 0x0010 // USN journal deletion#define VOLUME_REPAIR_OBJECT_ID 0x0020 // Repair $ObjId#define VOLUME_CHKDSK_UNDERWAY 0x4000 // chkdsk in progress#define VOLUME_MODIFIED_BY_CHKDSK 0x8000 // Modified by chkdsk // $EA_INFORMATION (always resident, if present)typedef struct _EA_INFORMATION { uint16_t ea_size; // Total size of $EA attribute uint16_t ea_count; // Number of EA entries uint32_t ea_query_size; // Size needed for EA query buffer} EA_INFORMATION; // $REPARSE_POINT header (attribute often under 16KB, usually resident)typedef struct _REPARSE_POINT_HEADER { uint32_t reparse_tag; // Type of reparse point uint16_t data_length; // Reparse data length uint16_t reserved; // Followed by reparse-specific data} REPARSE_POINT_HEADER; // Common reparse tags#define IO_REPARSE_TAG_MOUNT_POINT 0xA0000003 // Junction/Volume mount#define IO_REPARSE_TAG_HSM 0xC0000004 // Hierarchical Storage#define IO_REPARSE_TAG_HSM2 0x80000006 // HSM2#define IO_REPARSE_TAG_SIS 0x80000007 // Single Instance Storage#define IO_REPARSE_TAG_WIM 0x80000008 // Windows Imaging#define IO_REPARSE_TAG_CSV 0x80000009 // Cluster Shared Volume#define IO_REPARSE_TAG_DFS 0x8000000A // DFS#define IO_REPARSE_TAG_SYMLINK 0xA000000C // Symbolic link#define IO_REPARSE_TAG_DFSR 0x80000012 // DFS-R#define IO_REPARSE_TAG_DEDUP 0x80000013 // Deduplication#define IO_REPARSE_TAG_NFS 0x80000014 // NFS#define IO_REPARSE_TAG_WOF 0x80000017 // Windows Overlay Filter#define IO_REPARSE_TAG_WCI 0x80000018 // Windows Container#define IO_REPARSE_TAG_CLOUD 0x9000001A // Cloud Files (OneDrive)The $REPARSE_POINT attribute is usually resident because reparse data is typically small (paths for symbolic links, volume GUIDs for junctions). However, complex reparse points with large data payloads may become non-resident. The reparse tag determines how the file system filter driver interprets the data.
Some resident attributes participate in NTFS's B+ tree indexing system. The Indexed Flag in the resident attribute header indicates whether an attribute is indexed.
The Indexed Flag:
In the resident attribute header, byte 0x16 (the indexed_flag field) indicates:
$FILE_NAME and Directory Indices:
The primary use of indexed attributes is directory organization. Each directory has an index named $I30 (for $INDEX_ALLOCATION, type 0x30 = $FILE_NAME). This index is a B+ tree containing references to files in that directory.
Each $FILE_NAME attribute in a file's MFT record has its indexed flag set to 1 (if the file is in a directory). The corresponding directory's $I30 index contains an entry for this filename.
Index Structure:
Directory MFT Record:
├── $STANDARD_INFORMATION
├── $FILE_NAME
├── $INDEX_ROOT (name="$I30")
│ └── Contains index entries for small directories
└── $INDEX_ALLOCATION (name="$I30") [if directory is large]
└── B+ tree nodes on allocated clusters
Index Entry Format:
+------------------------+
| File Reference (8) | MFT reference to the file
+------------------------+
| Entry Length (2) | Total length of this entry
+------------------------+
| Attr Value Length (2) | Length of indexed attribute
+------------------------+
| Flags (4) | INDEX_ENTRY_NODE, INDEX_ENTRY_END
+------------------------+
| [Indexed Attribute | Copy of $FILE_NAME for file
| value (variable)] |
+------------------------+
| [Sub-node VCN (8)] | Only if INDEX_ENTRY_NODE flag set
+------------------------+
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
// Index entry structure (used in $INDEX_ROOT and $INDEX_ALLOCATION)typedef struct _INDEX_ENTRY { FILE_REFERENCE file_reference; // MFT reference (0 if end/subnodes) uint16_t entry_length; // Total length of this entry uint16_t attr_value_length; // Length of indexed attribute uint32_t flags; // INDEX_ENTRY_NODE, INDEX_ENTRY_END // Followed by: // - Indexed attribute value (if attr_value_length > 0) // - Sub-node VCN (if INDEX_ENTRY_NODE flag set)} INDEX_ENTRY; // Index entry flags#define INDEX_ENTRY_NODE 0x0001 // Entry points to sub-node#define INDEX_ENTRY_END 0x0002 // Last entry in this block // $INDEX_ROOT attribute structure (always resident)typedef struct _INDEX_ROOT { uint32_t attribute_type; // Type of indexed attr (0x30 for $FILE_NAME) uint32_t collation_rule; // Sort order uint32_t index_block_size; // Bytes per $INDEX_ALLOCATION block uint8_t clusters_per_block; // Clusters per index block uint8_t padding[3]; // Followed by INDEX_HEADER} INDEX_ROOT; // Index header (present in both $INDEX_ROOT and index blocks)typedef struct _INDEX_HEADER { uint32_t entries_offset; // Offset to first index entry uint32_t index_length; // Size of index entries + header uint32_t allocated_size; // Allocated size for entries uint32_t flags; // LARGE_INDEX if $INDEX_ALLOCATION exists} INDEX_HEADER; // Collation rules#define COLLATION_BINARY 0x00 // Binary comparison#define COLLATION_FILENAME 0x01 // Case-insensitive Unicode#define COLLATION_UNICODE_STRING 0x02 // Case-sensitive Unicode#define COLLATION_NTOFS_SID 0x10 // Security ID#define COLLATION_NTOFS_SECURITY 0x11 // Security hash#define COLLATION_NTOFS_ULONGS 0x12 // Array of ULONGs // Directory lookup using B+ treeFILE_REFERENCE lookup_file_in_directory( MFT_RECORD *dir_record, const wchar_t *filename, HANDLE volume) { // Get $INDEX_ROOT INDEX_ROOT *root = find_attribute(dir_record, AT_INDEX_ROOT, L"$I30"); if (root == NULL) return INVALID_FILE_REFERENCE; INDEX_HEADER *header = (INDEX_HEADER *)((char *)root + sizeof(INDEX_ROOT)); INDEX_ENTRY *entry = (INDEX_ENTRY *)((char *)header + header->entries_offset); while (!(entry->flags & INDEX_ENTRY_END)) { // Get $FILE_NAME from index entry FILE_NAME_ATTRIBUTE *fn = (FILE_NAME_ATTRIBUTE *)(entry + 1); int cmp = compare_filenames(filename, fn->name, fn->name_length); if (cmp == 0) { // Found it! return entry->file_reference; } else if (cmp < 0) { // Might be in sub-node if (entry->flags & INDEX_ENTRY_NODE) { // Read sub-node from $INDEX_ALLOCATION uint64_t vcn = get_subnode_vcn(entry); return search_index_block(dir_record, vcn, filename, volume); } return INVALID_FILE_REFERENCE; // Not found } entry = next_index_entry(entry); } // Check final sub-node if (entry->flags & INDEX_ENTRY_NODE) { uint64_t vcn = get_subnode_vcn(entry); return search_index_block(dir_record, vcn, filename, volume); } return INVALID_FILE_REFERENCE;}Resident attributes have significant implications for digital forensics and data recovery. Understanding these characteristics is essential for security professionals and incident responders.
Deleted File Recovery:
When a file is deleted:
For resident files, this means:
MFT records are typically reused in order (though not strictly FIFO), so a deleted resident file often survives longer than deleted non-resident data, which can be overwritten by any file operation allocating clusters.
Timestamp Discrepancies:
As mentioned earlier, $STANDARD_INFORMATION and $FILE_NAME contain separate timestamps:
Malware or anti-forensics tools often modify $SI timestamps to hide activity. But they frequently forget to also modify $FN timestamps. This discrepancy (called 'time stomping detection') can reveal:
The Update Sequence Number (USN) stored in $STANDARD_INFORMATION references the $UsnJrnl (Change Journal). Even if $SI timestamps are tampered with, the USN Journal maintains an independent record of file operations. Forensic investigators should always correlate MFT resident data with the Change Journal for a complete picture.
Resident attributes significantly impact NTFS performance. Understanding these effects helps system administrators and developers optimize their workloads.
MFT as Performance Critical Path:
Every file operation begins with an MFT lookup. Resident attributes determine how much information is immediately available:
Operation: GetFileAttributes("C:\config.ini")
If config.ini is resident:
1. Locate directory's MFT record
2. Search $I30 index for "config.ini"
3. Read file's MFT record
4. Return $STANDARD_INFORMATION attributes
→ 2-3 MFT reads total
If file is non-resident:
→ Same 2-3 MFT reads (attributes come from resident $SI)
// The difference comes with GetFileTime + ReadFile:
If data is resident:
→ 2-3 MFT reads (everything in MFT record)
If data is non-resident:
→ 2-3 MFT reads + 1-N data cluster reads
MFT Caching:
Windows aggressively caches MFT records in memory. The file system cache keeps frequently accessed MFT records resident, making repeated access to small files extremely fast. This is why listing directories with many small files can be faster than you'd expect.
Impact of Filename Length:
Longer filenames consume more space in MFT records, leaving less room for resident $DATA:
With a 255-character filename plus Win32+DOS names, almost no space remains for resident data. This is another reason to prefer reasonable filename lengths.
| Filename Length (chars) | Approx. Resident $DATA Capacity |
|---|---|
| 8 (short) | ~780 bytes |
| 20 (typical) | ~730 bytes |
| 50 (long) | ~670 bytes |
| 100 (very long) | ~570 bytes |
| 200 (excessive) | ~370 bytes |
| 255 (maximum) | ~260 bytes |
To maximize resident storage utilization:
For applications generating many small files (logging, caching), keeping files under 600 bytes ensures residency and optimal performance.
We've explored NTFS resident attributes in depth—the attributes that live entirely within MFT records. Let's consolidate our understanding:
What's Next:
Now that we understand the MFT and resident attributes, we'll explore the NTFS Change Journal ($UsnJrnl)—a powerful feature that tracks all file system modifications. The Change Journal enables applications like search indexers, backup software, and security tools to efficiently detect changes without scanning the entire volume.
You now understand NTFS resident attributes—which ones are always resident, how small file optimization works, and the forensic and performance implications. You can analyze MFT records knowing which data lives in the record itself versus external clusters. Next, we'll explore the Change Journal.