Windows File IDs: Compare Open Handles Without Trusting Paths
Query FILE_ID_INFO from an open handle, compare volume-plus-file identifiers, and learn why a file ID is not a path or permanent global key.
A pathname is a lookup instruction, not a permanent identity. A file can have more than one name through hard links, and a name can later be renamed or replaced while a process still holds an open handle to the original object. When a watcher, indexer, or diagnostic tool needs to compare two already-open files, Windows can return identity information from the handles themselves.
The modern Win32 structure for this job is FILE_ID_INFO, queried with GetFileInformationByHandleEx and the FileIdInfo information class. The comparison key is the pair (VolumeSerialNumber, FileId). It is useful within the scope documented by the filesystem, but it is not a security credential, path, content hash, or forever-unique identifier.
Query identity from the handle you actually opened
FILE_ID_INFO contains a 64-bit volume serial number and a 128-bit FILE_ID_128. Microsoft documents combining those values to determine whether two open handles refer to the same file. The handle matters: resolving a path and querying its metadata are separate operations, and another process can change the namespace between a preliminary check and a later open.
This helper opens a file or directory for attribute access, queries the ID, saves the Win32 error before closing the handle, and returns the result to its caller. It deliberately does not change contents or permissions.
#include <windows.h>
bool queryFileId(const wchar_t* path, FILE_ID_INFO& info, DWORD& error)
{
HANDLE file = CreateFileW(
path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
nullptr);
if (file == INVALID_HANDLE_VALUE) {
error = GetLastError();
return false;
}
if (!GetFileInformationByHandleEx(
file, FileIdInfo, &info, sizeof(info))) {
error = GetLastError();
CloseHandle(file);
return false;
}
CloseHandle(file);
error = ERROR_SUCCESS;
return true;
}
FILE_FLAG_BACKUP_SEMANTICS permits opening a directory handle with CreateFileW; it does not grant backup privilege. If the object of interest is the reparse point itself rather than the final target, include FILE_FLAG_OPEN_REPARSE_POINT and define that choice explicitly. Otherwise, the identity describes the object the open operation resolved to.
Compare the two fields, not the path strings and not only the 128-bit member:
#include <windows.h>
#include <cstring>
enum class FileIdComparison {
Same,
Different,
Unknown
};
FileIdComparison compareFileId(const FILE_ID_INFO& left,
const FILE_ID_INFO& right)
{
bool leftZero = true, leftOnes = true;
bool rightZero = true, rightOnes = true;
for (unsigned i = 0; i < 16; ++i) {
leftZero &= left.FileId.Identifier[i] == 0;
leftOnes &= left.FileId.Identifier[i] == 0xFF;
rightZero &= right.FileId.Identifier[i] == 0;
rightOnes &= right.FileId.Identifier[i] == 0xFF;
}
// The protocol allows all-zero/all-ones IDs when uniqueness is unavailable.
if (leftZero || leftOnes || rightZero || rightOnes)
return FileIdComparison::Unknown;
const bool same = left.VolumeSerialNumber == right.VolumeSerialNumber &&
std::memcmp(left.FileId.Identifier,
right.FileId.Identifier, 16) == 0;
return same ? FileIdComparison::Same : FileIdComparison::Different;
}
The comparison returns three outcomes so an unavailable identifier cannot be mistaken for a known mismatch. Handle-query failure is a separate error path and should also remain distinguishable from a successful comparison. The [MS-FSCC] protocol specification says unsupported 128-bit IDs are zero and an ID that cannot be made unique is all FF; both sentinel values must be treated as unknown.
What this identity does and does not promise
Names and IDs answer different questions. Two hard-link names for one file can resolve to the same identity. Renaming a file normally changes its name, not its identity, while deleting and recreating a path creates a new file object. That makes a handle-derived ID useful for correlating observations across renames, detecting that a path now names a different object, and avoiding duplicate work for hard-linked entries.
The scope is constrained. The protocol describes a file ID as generated by the filesystem and says it should be unique within a volume and stable until the file is deleted. Therefore:
- Pair the ID with its volume serial number. The same ID bytes on another volume do not identify the same file.
- Do not retain an ID forever as a database primary key. After deletion, a filesystem may reuse an ID; an offline backup, cloned image, or volume replacement also changes the identity context.
- Do not compare IDs across computers as a globally unique namespace. Microsoft describes the pair as identifying a file on a single computer; remote filesystems and redirected paths have their own behavior.
- Do not treat an ID as proof of authorization or content integrity. Anyone who can change the file may replace its contents while preserving the same file object. Hash the content if content identity matters, and enforce access control separately.
There is also a time-of-check issue in path-based workflows. If code first asks for the ID of C:\data\item.bin, closes the handle, then reopens that string for a privileged operation, the second open may resolve to another file. Keep and use the validated handle for the operation when possible; if a later open is necessary, query and revalidate the new handle rather than trusting the old pathname comparison.
Filesystem and Windows-version boundaries
GetFileInformationByHandleEx accepts a file handle, an information-class selector, an output buffer, and that buffer’s size. The FileIdInfo information class is unsupported before Windows 8 and Windows Server 2012, even though the API itself is available on older systems. Microsoft lists SMB 3.0, CSVFS, and ReFS among technologies supported for the function on Windows 8/Server 2012; that is not a promise about every SMB server, redirector, virtual filesystem, or older target. Query the actual handle, check the return value, and preserve GetLastError immediately on failure.
When the query fails, record the path used to open the handle, the filesystem or remote provider, the requested information class, and the error code. Do not silently fall back to a path string and label it a stable identity. A fallback can be useful for a best-effort inventory, but callers must know that it is weaker and must not use it as a containment or authorization check.
Test the behavior on the filesystems that matter to the application: two hard links, a rename while a handle stays open, delete-and-recreate, a directory, and each supported network share. Also test an unsupported or synthetic provider and verify the “unknown” outcome does not become a false match. For a reparse point, test both opening the final target and opening the reparse object itself; these are different identity questions.
Use file IDs as a narrow handle-correlation tool. Keep volume scope and lifetime visible in the data model, preserve an explicit unknown state, and retain a handle whenever the next operation must apply to the same object that was checked.
Related:
- Windows CreateFile Sharing Semantics: Access, Rename, Delete, and Byte-Range Locks
- NTFS USN Change Journal: Incremental File Tracking Without False Guarantees
Sources: