Windows Reparse Points: Safe Traversal, Inspection, and Cleanup
Inspect Windows reparse tags without following targets, safely traverse directory trees, and avoid junction loops, unexpected redirection, and destructive cleanup.
A Windows reparse point is a file-system object containing a tag and application- or file-system-specific data. The tag identifies the reparse-point type; a file-system filter can interpret the associated data and redirect or otherwise alter ordinary file operations. NTFS symbolic links, junctions, volume mount points, and cloud placeholders are familiar examples, but the general mechanism also supports third-party filters. A program that recursively walks a tree as if every directory entry were an ordinary directory can unexpectedly cross a volume boundary, leave its intended root, revisit a path, or trigger a provider’s hydration behavior.
The safety boundary is handle-based: inspect the entry itself before deciding whether to open its target. FILE_FLAG_OPEN_REPARSE_POINT tells CreateFile to open the reparse point rather than follow it for the final path component. Directory handles also require FILE_FLAG_BACKUP_SEMANTICS. Once open, query FileAttributeTagInfo to retrieve the reparse attribute and tag. Do not infer type from the printed path or from the FILE_ATTRIBUTE_DIRECTORY bit alone.
Open the reparse object itself
This minimal C example opens the final component without following it, then retrieves its attributes and tag. Access rights should be narrowed to the operation the tool actually needs; this example requests attribute access only. It handles both files and directories, and always closes the handle.
#include <windows.h>
#include <stdio.h>
int inspect_reparse_point(const wchar_t *path)
{
HANDLE handle = CreateFileW(
path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
NULL);
if (handle == INVALID_HANDLE_VALUE) {
fwprintf(stderr, L"CreateFileW failed: %lu\n", GetLastError());
return 1;
}
FILE_ATTRIBUTE_TAG_INFO info = {0};
BOOL ok = GetFileInformationByHandleEx(
handle, FileAttributeTagInfo, &info, sizeof(info));
DWORD error = ok ? ERROR_SUCCESS : GetLastError();
CloseHandle(handle);
if (!ok) {
fwprintf(stderr, L"FileAttributeTagInfo failed: %lu\n", error);
return 1;
}
if ((info.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) {
wprintf(L"reparse point tag=0x%08lx attributes=0x%08lx\n",
info.ReparseTag, info.FileAttributes);
} else {
wprintf(L"ordinary entry attributes=0x%08lx\n", info.FileAttributes);
}
return 0;
}
The example identifies a reparse point; it intentionally does not decode arbitrary reparse data or delete anything. A tag can belong to a Microsoft-defined or third-party format, and parsing rules are tag-specific. Use the relevant documented structure and validate its declared lengths, offsets, tag, optional GUID, and buffer bounds before accessing any fields. Never treat bytes returned from a file system as a trusted, null-terminated string.
Design recursive traversal as a policy decision
Before descending into a directory, decide whether your product should follow links at all. A backup walker may record a reparse entry without following it, while a migration tool may intentionally follow a known link type and preserve the target relationship. Neither policy is universally correct. The tool should expose the decision in its logs and configuration, because “copy everything under this root” is ambiguous when the tree can redirect elsewhere.
If following is allowed, track visited directory identities rather than only normalized path strings. Multiple paths can name the same underlying directory, and case normalization or relative components are not a reliable identity boundary. Obtain stable identity information from handles and volume identity, enforce a maximum traversal depth, and impose explicit root-containment policy on the resolved target. Treat network paths, mount points, and cloud-backed entries as separate trust and availability domains. Revalidate the opened handle after a path-based discovery step to reduce time-of-check/time-of-use races.
Do not build security policy solely by checking a path prefix. A junction can redirect a child beneath an apparently safe string to a location outside the intended tree. In sensitive code, hold directory handles and use handle-relative operations where supported by the API and filesystem, then verify the object actually opened. If a path can be modified by an untrusted process, permissions and race-resistant handle management are part of the design, not optional polish.
Inspect, change, and delete with the tag contract
DeviceIoControl supports operations such as FSCTL_GET_REPARSE_POINT, FSCTL_SET_REPARSE_POINT, and FSCTL_DELETE_REPARSE_POINT. Modify or delete requests must identify the tag present on the object, and custom tags can require the matching GUID. Mismatches fail rather than authorizing a generic “delete whatever is there” operation. This protects the filter’s namespace, but application code must still avoid deleting the target accidentally by opening the link in a way that follows it.
For administrative cleanup, first list the path, attributes, tag, target, and volume. Use the shell command appropriate to the link type, verify that it removes the link rather than recursively deleting a target, and test on a disposable directory. A junction, directory symbolic link, file symbolic link, and mounted folder have different creation and removal semantics. If the file-system filter responsible for a tag is unavailable, opening the path normally can fail; FILE_FLAG_OPEN_REPARSE_POINT can be useful when inspecting or replacing the entry itself.
Operational limits and diagnostics
Microsoft documents a maximum 16 KiB for reparse data, including the tag and optional GUID, and a limit of 63 reparse points on a path; the effective count can be lower when long fully qualified targets consume path capacity. Applications should not hard-code assumptions that every filesystem implements the same tags or behavior. Query volume capabilities, inspect errors, and preserve the raw tag in diagnostic output.
When an unexpected file appears outside a root or a cleanup tool loops indefinitely, preserve the path tree and tags before remediation. A loop may indicate a cycle through junctions; a surprising open may indicate a name-surrogate reparse point; and a failed open may indicate a missing filter or unsupported tag. Comparing the entry’s own attributes and tag to the resolved target is more informative than repeatedly retrying the same path.
Correct reparse-point handling begins with explicit policy: do not follow by default when containment matters, and do not delete or decode an unfamiliar tag as if it were a symbolic link. Inspect through a handle, classify the tag, and only then take the action the tool was designed to perform.
Related:
- NTFS Internals: the MFT, Journaling, and Alternate Data Streams
- Windows File-System Minifilters: Filter Manager, Altitudes, and I/O Interception
Sources: