Repository navigation
dart:io on Windows treats every reparse point as a link instead of checking IsReparseTagNameSurrogate #64535
Description
Problem
On Windows, dart:io classifies any entity with FILE_ATTRIBUTE_REPARSE_POINT (except IO_REPARSE_TAG_AF_UNIX) as a Link (in HandleFindFile, GetTypeAndAttributes, DeleteLinkHelper, and RecursiveDeleter::DeleteDirectory).
Only name-surrogate reparse points (IsReparseTagNameSurrogate(tag), i.e. IO_REPARSE_TAG_SYMLINK and IO_REPARSE_TAG_MOUNT_POINT) redirect to another path. Windows also uses reparse points for ordinary files and directories that carry filter-driver metadata:
- Cloud Files sync roots and placeholders (
IO_REPARSE_TAG_CLOUD*, used by OneDrive Files On-Demand) - App execution aliases in
%LOCALAPPDATA%\Microsoft\WindowsApps(IO_REPARSE_TAG_APPEXECLINK, e.g.wt.exe,winget.exe,python3.exe) - Projected File System directories/files (
IO_REPARSE_TAG_PROJFS, e.g. VFSForGit) - WOF-compressed (
IO_REPARSE_TAG_WOF) and deduplicated (IO_REPARSE_TAG_DEDUP) files
As Microsoft notes in Build a cloud file sync engine: "A common misconception about reparse points is that they are the same as symbolic links."
What breaks
When a non-surrogate reparse point has FILE_ATTRIBUTE_REPARSE_POINT visible to the process:
Directory.list(followLinks: false)yieldsLinkinstead ofFileorDirectory, and withrecursive: true, followLinks: falsedoes not descend into reparse-point directories (HandleFindFileinruntime/bin/directory_win.cc).FileSystemEntity.typeSync(path, followLinks: false)returnsFileSystemEntityType.linkandisLinkSync(path)returnstrue(GetTypeAndAttributesinruntime/bin/file_win.cc).FileSystemEntity.typeSync(path, followLinks: true)andFile(path).existsSync()returnnotFound/falsefor app execution aliases (IO_REPARSE_TAG_APPEXECLINKin%LOCALAPPDATA%\Microsoft\WindowsApps), becauseGetTypeAndAttributescallsResolveReparsePoint(CreateFileWwithoutFILE_FLAG_OPEN_REPARSE_POINT), which fails onIO_REPARSE_TAG_APPEXECLINKstubs even thoughGetFileAttributesExWalready succeeded.Link(path).targetSync()throwsFileSystemException: Cannot read link ... (OS Error: The file or directory is not a reparse point, errno = 4390), becauseFile::LinkTargetinruntime/bin/file_win.cconly acceptsIO_REPARSE_TAG_SYMLINKandIO_REPARSE_TAG_MOUNT_POINT.Directory.delete(recursive: true)on a tree containing a non-empty reparse-point directory fails withOS Error: The directory is not empty, errno = 145, becauseRecursiveDeleter::DeleteDirectoryinruntime/bin/directory_win.ccassumes any reparse point is a junction (Recursive directory delete traverses into junctions (symlinks) on Windows #4560) and callsRemoveDirectoryWwithout deleting children.
Note on OneDrive / Cloud Files (cldflt.sys)
By default, cldflt.sys runs non-%systemroot% processes such as dart.exe in PHCM_DISGUISE_PLACEHOLDER (1) mode, which hides FILE_ATTRIBUTE_REPARSE_POINT on files and subfolders inside a sync root. Even in default mode, however, cldflt.sys does not hide the attribute on:
- The sync root directory itself when listing its parent (e.g.
Directory(r'C:\Users\<user>').listSync(followLinks: false)reportsC:\Users\<user>\OneDriveas aLink, whiletypeSync(r'C:\Users\<user>\OneDrive', followLinks: false)returnsdirectory). .and..entries inside placeholder directories duringFindFirstFileW(caused [Bug]: RangeError in ignore.dart 582 when running pub publish/dry-run (Dart 3.11.0, Windows) pub#4765; addressed in https://dart-review.googlesource.com/c/sdk/+/560300).
When PHCM_EXPOSE_PLACEHOLDERS (2) is enabled via RtlSetProcessPlaceholderCompatibilityMode — and unconditionally for non-Cloud-Filter tags (IO_REPARSE_TAG_APPEXECLINK, IO_REPARSE_TAG_PROJFS, IO_REPARSE_TAG_DEDUP, IO_REPARSE_TAG_WOF), which have no disguise shim — all symptoms above occur on the files and directories themselves.
Reproduction
1. App execution aliases (no FFI, any Windows 10/11 or Server 2022/2025 machine)
import 'dart:io';
void main() {
final apps = Directory(
'${Platform.environment['LOCALAPPDATA']}\\Microsoft\\WindowsApps',
);
final alias = apps
.listSync(followLinks: false)
.firstWhere((e) => e.path.endsWith('.exe'));
print('listed as: ${alias.runtimeType} (${alias.path})');
print('typeSync(followLinks: false): '
'${FileSystemEntity.typeSync(alias.path, followLinks: false)}');
print('typeSync(followLinks: true): '
'${FileSystemEntity.typeSync(alias.path, followLinks: true)}');
print('File.existsSync(): ${File(alias.path).existsSync()}');
print('Link.targetSync(): ${Link(alias.path).targetSync()}');
}Output on stock Dart 3.13.5 (stable):
listed as: _Link (C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps\ActionsMcpHost.exe)
typeSync(followLinks: false): link
typeSync(followLinks: true): notFound
File.existsSync(): false
Unhandled exception:
FileSystemException: Cannot read link, path = 'C:\Users\runneradmin\AppData\Local\Microsoft\WindowsApps\ActionsMcpHost.exe' (OS Error: The file or directory is not a reparse point, errno = 4390)
Output with the proposed IsReparseTagNameSurrogate fix:
listed as: _File (C:\Users\chrome-bot\AppData\Local\Microsoft\WindowsApps\ActionsMcpHost.exe)
typeSync(followLinks: false): file
typeSync(followLinks: true): file
File.existsSync(): true
Unhandled exception:
FileSystemException: Cannot read link, path = 'C:\Users\chrome-bot\AppData\Local\Microsoft\WindowsApps\ActionsMcpHost.exe' (OS Error: The file or directory is not a reparse point, errno = 4390)
(The same applies to winget.exe, wt.exe, and python3.exe in WindowsApps.)
2. Cloud Files sync root and placeholders (directories + files)
Registers a temporary Cloud Filter sync root and converts sub, sub\sub_sub, and plain.txt into placeholders (ffi: ^2.1.0):
issue_repro.dart
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
final class GUID extends Struct {
@Uint32()
external int data1;
@Uint16()
external int data2;
@Uint16()
external int data3;
@Array(8)
external Array<Uint8> data4;
}
final class CF_SYNC_REGISTRATION extends Struct {
@Uint32()
external int structSize;
external Pointer<Utf16> providerName;
external Pointer<Utf16> providerVersion;
external Pointer<Void> syncRootIdentity;
@Uint32()
external int syncRootIdentityLength;
external Pointer<Void> fileIdentity;
@Uint32()
external int fileIdentityLength;
external GUID providerId;
}
final class CF_SYNC_POLICIES extends Struct {
@Uint32()
external int structSize;
@Uint16()
external int hydrationPrimary;
@Uint16()
external int hydrationModifier;
@Uint16()
external int populationPrimary;
@Uint16()
external int populationModifier;
@Uint32()
external int inSync;
@Uint32()
external int hardLink;
@Uint32()
external int placeholderManagement;
}
final cldapi = DynamicLibrary.open('cldapi.dll');
final kernel32 = DynamicLibrary.open('kernel32.dll');
final ntdll = DynamicLibrary.open('ntdll.dll');
final cfRegisterSyncRoot = cldapi.lookupFunction<
Int32 Function(Pointer<Utf16>, Pointer<CF_SYNC_REGISTRATION>,
Pointer<CF_SYNC_POLICIES>, Int32),
int Function(Pointer<Utf16>, Pointer<CF_SYNC_REGISTRATION>,
Pointer<CF_SYNC_POLICIES>, int)>('CfRegisterSyncRoot');
final cfConvertToPlaceholder = cldapi.lookupFunction<
Int32 Function(Pointer<Void>, Pointer<Void>, Uint32, Int32, Pointer<Int64>,
Pointer<Void>),
int Function(Pointer<Void>, Pointer<Void>, int, int, Pointer<Int64>,
Pointer<Void>)>('CfConvertToPlaceholder');
final cfUnregisterSyncRoot = cldapi.lookupFunction<
Int32 Function(Pointer<Utf16>),
int Function(Pointer<Utf16>)>('CfUnregisterSyncRoot');
final createFileW = kernel32.lookupFunction<
Pointer<Void> Function(Pointer<Utf16>, Uint32, Uint32, Pointer<Void>,
Uint32, Uint32, Pointer<Void>),
Pointer<Void> Function(Pointer<Utf16>, int, int, Pointer<Void>, int, int,
Pointer<Void>)>('CreateFileW');
final closeHandle = kernel32.lookupFunction<Int32 Function(Pointer<Void>),
int Function(Pointer<Void>)>('CloseHandle');
final getFileAttributesW = kernel32.lookupFunction<
Uint32 Function(Pointer<Utf16>),
int Function(Pointer<Utf16>)>('GetFileAttributesW');
final queryPlaceholderCompatibilityMode =
ntdll.lookupFunction<Int8 Function(), int Function()>(
'RtlQueryProcessPlaceholderCompatibilityMode');
final setPlaceholderCompatibilityMode =
ntdll.lookupFunction<Int8 Function(Int8), int Function(int)>(
'RtlSetProcessPlaceholderCompatibilityMode');
void check(int hresult, String call) {
if (hresult != 0) {
throw StateError(
'$call failed: 0x${hresult.toUnsigned(32).toRadixString(16)}');
}
}
void convertToPlaceholder(Pointer<Utf16> path) {
final handle = createFileW(path, 0x80000000 | 0x40000000, 0x1 | 0x2 | 0x4,
nullptr, 3, 0x02000000, nullptr);
if (handle.address == -1) throw StateError('CreateFileW failed');
try {
check(cfConvertToPlaceholder(handle, nullptr, 0, 0x1, nullptr, nullptr),
'CfConvertToPlaceholder(${path.toDartString()})');
} finally {
closeHandle(handle);
}
}
bool hasReparseAttribute(Pointer<Utf16> path) =>
getFileAttributesW(path) & 0x400 != 0;
String describe(String path) =>
'typeSync(followLinks: false)=${FileSystemEntity.typeSync(path, followLinks: false)} '
'typeSync(followLinks: true)=${FileSystemEntity.typeSync(path, followLinks: true)} '
'isLinkSync=${FileSystemEntity.isLinkSync(path)}';
List<String> names(Directory directory,
{required bool recursive, required bool followLinks}) =>
directory
.listSync(recursive: recursive, followLinks: followLinks)
.map((e) =>
'${e.runtimeType}(${e.path.substring(directory.path.length + 1)})')
.toList()
..sort();
void main() {
final temp = Directory.systemTemp.createTempSync('placeholders_');
final syncRoot = Directory('${temp.path}\\sync_root')..createSync();
final sub = Directory('${syncRoot.path}\\sub')..createSync();
final subSub = Directory('${sub.path}\\sub_sub')..createSync();
final nested = File('${subSub.path}\\nested.txt')..writeAsStringSync('n');
final plain = File('${syncRoot.path}\\plain.txt')..writeAsStringSync('p');
final arena = Arena();
final syncRootPath = syncRoot.path.toNativeUtf16(allocator: arena);
final subPath = sub.path.toNativeUtf16(allocator: arena);
final subSubPath = subSub.path.toNativeUtf16(allocator: arena);
final plainPath = plain.path.toNativeUtf16(allocator: arena);
var oldMode = -1;
try {
final registration = arena<CF_SYNC_REGISTRATION>();
registration.ref
..structSize = sizeOf<CF_SYNC_REGISTRATION>()
..providerName = 'ReproProvider'.toNativeUtf16(allocator: arena)
..providerVersion = '1.0'.toNativeUtf16(allocator: arena)
..syncRootIdentity = (arena<Uint32>()..value = 1).cast()
..syncRootIdentityLength = 4
..providerId.data1 = 0x52657072;
final policies = arena<CF_SYNC_POLICIES>()
..ref.structSize = sizeOf<CF_SYNC_POLICIES>();
check(cfRegisterSyncRoot(syncRootPath, registration, policies, 0x2 | 0x4),
'CfRegisterSyncRoot');
convertToPlaceholder(subPath);
convertToPlaceholder(subSubPath);
convertToPlaceholder(plainPath);
void report(String phase) {
print('[$phase] mode=${queryPlaceholderCompatibilityMode()}');
print(
'[$phase] reparse attr: sync_root=${hasReparseAttribute(syncRootPath)} '
'sub=${hasReparseAttribute(subPath)} '
'sub_sub=${hasReparseAttribute(subSubPath)} '
'plain.txt=${hasReparseAttribute(plainPath)}');
print('[$phase] sync_root: ${describe(syncRoot.path)}');
print('[$phase] sub: ${describe(sub.path)}');
print('[$phase] plain.txt: ${describe(plain.path)}');
print('[$phase] temp.listSync(followLinks: false): '
'${names(temp, recursive: false, followLinks: false)}');
print('[$phase] sync_root.listSync(followLinks: false): '
'${names(syncRoot, recursive: false, followLinks: false)}');
print('[$phase] sync_root recursive/noFollow: '
'${names(syncRoot, recursive: true, followLinks: false)}');
}
report('default');
oldMode = setPlaceholderCompatibilityMode(2);
report('exposed');
try {
print('[exposed] Link(sub).targetSync(): ${Link(sub.path).targetSync()}');
} on FileSystemException catch (e) {
print('[exposed] Link(sub).targetSync(): $e');
}
try {
syncRoot.deleteSync(recursive: true);
print('[exposed] deleteSync(recursive: true): ok');
} on FileSystemException catch (e) {
print('[exposed] deleteSync(recursive: true): $e');
}
} finally {
cfUnregisterSyncRoot(syncRootPath);
arena.releaseAll();
if (oldMode >= 0) setPlaceholderCompatibilityMode(oldMode);
for (final entity in [nested, subSub, plain, sub, syncRoot, temp]) {
try {
entity.deleteSync();
} on FileSystemException catch (_) {}
}
}
}Output on stock Dart 3.13.5 (stable), Windows Server 2025 (Build 26100):
[default] mode=1
[default] reparse attr: sync_root=false sub=false sub_sub=false plain.txt=false
[default] sync_root: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[default] sub: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[default] plain.txt: typeSync(followLinks: false)=file typeSync(followLinks: true)=file isLinkSync=false
[default] temp.listSync(followLinks: false): [_Link(sync_root)]
[default] sync_root.listSync(followLinks: false): [_Directory(sub), _File(plain.txt)]
[default] sync_root recursive/noFollow: [_Directory(sub), _Directory(sub\sub_sub), _File(plain.txt), _File(sub\sub_sub\nested.txt)]
[exposed] mode=2
[exposed] reparse attr: sync_root=true sub=true sub_sub=true plain.txt=true
[exposed] sync_root: typeSync(followLinks: false)=link typeSync(followLinks: true)=directory isLinkSync=true
[exposed] sub: typeSync(followLinks: false)=link typeSync(followLinks: true)=directory isLinkSync=true
[exposed] plain.txt: typeSync(followLinks: false)=link typeSync(followLinks: true)=file isLinkSync=true
[exposed] temp.listSync(followLinks: false): [_Link(sync_root)]
[exposed] sync_root.listSync(followLinks: false): [_Link(.), _Link(plain.txt), _Link(sub)]
[exposed] sync_root recursive/noFollow: [_Link(.), _Link(plain.txt), _Link(sub)]
[exposed] Link(sub).targetSync(): FileSystemException: Cannot read link, path = 'C:\Users\RUNNER~1\AppData\Local\Temp\placeholders_225f0798\sync_root\sub' (OS Error: The file or directory is not a reparse point, errno = 4390)
[exposed] deleteSync(recursive: true): FileSystemException: Deletion failed, path = 'C:\Users\RUNNER~1\AppData\Local\Temp\placeholders_225f0798\sync_root' (OS Error: The directory is not empty, errno = 145)
(In [exposed] mode, _Link(.) is the dot-entry bug addressed in https://dart-review.googlesource.com/c/sdk/+/560300; _Link(plain.txt) and _Link(sub) remain even after that CL.)
Output with the proposed IsReparseTagNameSurrogate fix (Windows 11 Build 26100):
[default] mode=0
[default] reparse attr: sync_root=false sub=false sub_sub=false plain.txt=false
[default] sync_root: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[default] sub: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[default] plain.txt: typeSync(followLinks: false)=file typeSync(followLinks: true)=file isLinkSync=false
[default] temp.listSync(followLinks: false): [_Directory(sync_root)]
[default] sync_root.listSync(followLinks: false): [_Directory(sub), _File(plain.txt)]
[default] sync_root recursive/noFollow: [_Directory(sub), _Directory(sub\sub_sub), _File(plain.txt), _File(sub\sub_sub\nested.txt)]
[exposed] mode=2
[exposed] reparse attr: sync_root=true sub=true sub_sub=true plain.txt=true
[exposed] sync_root: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[exposed] sub: typeSync(followLinks: false)=directory typeSync(followLinks: true)=directory isLinkSync=false
[exposed] plain.txt: typeSync(followLinks: false)=file typeSync(followLinks: true)=file isLinkSync=false
[exposed] temp.listSync(followLinks: false): [_Directory(sync_root)]
[exposed] sync_root.listSync(followLinks: false): [_Directory(sub), _File(plain.txt)]
[exposed] sync_root recursive/noFollow: [_Directory(sub), _Directory(sub\sub_sub), _File(plain.txt), _File(sub\sub_sub\nested.txt)]
[exposed] Link(sub).targetSync(): FileSystemException: Cannot read link, path = 'C:\b\s\w\ir\x\t\placeholders_6ee951d1\sync_root\sub' (OS Error: The file or directory is not a reparse point, errno = 4390)
[exposed] deleteSync(recursive: true): ok
Proposed fix
Proposed CL: https://dart-review.googlesource.com/c/sdk/+/561560
Check IsReparseTagNameSurrogate(tag) (tag & 0x20000000 != 0) instead of treating every FILE_ATTRIBUTE_REPARSE_POINT as a link:
HandleFindFile(runtime/bin/directory_win.cc):find_file_data.dwReserved0already holds the reparse tag during enumeration. ReturnkListLink(and run the link-loop check) only whenIsReparseTagNameSurrogate(find_file_data.dwReserved0)is true; otherwise fall through to normal directory/file handling.GetTypeAndAttributes(runtime/bin/file_win.cc): already reads the tag viaGetReparsePointTagto excludeIO_REPARSE_TAG_AF_UNIX; treat the entity as a link (returningFile::kIsLinkwhen!follow_linksor callingResolveReparsePointwhenfollow_links) only whenIsReparseTagNameSurrogate(tag)is true.DeleteLinkHelper(runtime/bin/file_win.cc): verifyGetReparsePointTag(path, &tag) && IsReparseTagNameSurrogate(tag)soLink.deletefails withERROR_NOT_A_REPARSE_POINTon non-surrogate reparse points instead of deleting ordinary files/directories.RecursiveDeleter::DeleteDirectory(runtime/bin/directory_win.cc): only skip recursion for name-surrogate reparse points (junctions and directory symlinks), recursing normally into other reparse-point directories.
Compatibility / breaking change impact
Symbolic links (IO_REPARSE_TAG_SYMLINK), junctions / mount points (IO_REPARSE_TAG_MOUNT_POINT), and Unix domain sockets (IO_REPARSE_TAG_AF_UNIX) are unaffected.
For non-surrogate reparse points (IO_REPARSE_TAG_CLOUD*, IO_REPARSE_TAG_APPEXECLINK, IO_REPARSE_TAG_PROJFS, IO_REPARSE_TAG_DEDUP, IO_REPARSE_TAG_WOF):
type(followLinks: false)/isLink/Directory.list(followLinks: false)will reportfileordirectoryinstead oflink. Existing code that receivedLinkinstances for these entries could not callLink.targetorLink.updateon them (both fail witherrno = 4390), andDirectory.list(recursive: true, followLinks: false)will now traverse into non-surrogate reparse-point directories instead of skipping them.Directory.delete(recursive: true)will recurse into and delete non-empty non-surrogate reparse-point directories instead of failing withERROR_DIR_NOT_EMPTY(errno = 145). Unlike junctions/symlinks (Recursive directory delete traverses into junctions (symlinks) on Windows #4560), non-surrogate reparse-point directories do not point to an external target directory.type(followLinks: true)/File.existson%LOCALAPPDATA%\Microsoft\WindowsApps\*.exe(IO_REPARSE_TAG_APPEXECLINK) will returnfile/trueinstead ofnotFound/false. Note thatWindowsAppsis onPATHby default and includes both real MSIX app aliases (winget.exe,wt.exe) and the built-in 0-byte Microsoft Store redirector stubs (python.exe,python3.exewhen Python is not installed), so naivePATHlookups that check onlyFile.existsSync()(without checking file size or executing) will seeWindowsApps\python3.exeas existing, matchingGetFileAttributesExW, PowerShellTest-Path, and Python/Go/Rust.
Python 3.8+ (bpo-37834), Go 1.23 (golang/go#61893), and Rust (std::fs::symlink_metadata) all use IsReparseTagNameSurrogate for this classification.
Related
- [Bug]: RangeError in ignore.dart 582 when running pub publish/dry-run (Dart 3.11.0, Windows) pub#4765 / https://dart-review.googlesource.com/c/sdk/+/560300 —
.and..yielded asLinks byDirectory.listin OneDrive folders. - Windows directory watcher flaky reporting of link type for new link to directory #61797 — Windows directory watcher has a similar
FILE_ATTRIBUTE_REPARSE_POINT== link assumption. - Recursive directory delete traverses into junctions (symlinks) on Windows #4560 — original "do not recurse into reparse points during delete" change.
- python/cpython#83493 — Python's discussion on
IO_REPARSE_TAG_CLOUD*vsIsReparseTagNameSurrogate.
Activity