Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 5428c98

Browse files
committed
Fix FileSystem enumeration with broken symlinks
Today when enumerating directories, we look only for entries with DT_DIR (directory) or DT_REG (regular file); if we encounter something unknown or a symlink, we stat to resolve it and determine whether it's actually an S_IFDIR (directory) or S_IFREG (regular file). There are (at least) two problems with this: 1) The stat to a symlink will fail if the symlink points to a non-existent entry. 2) We end up skipping lots of special files, e.g. FIFOs. The commit changes our logic to simply special case directories: we treat everything as a file unless we're sure it's a directory. This directly fixes (2), and indirectly fixes (1): our logic for DirectoryExists already ignores any stat errors, returning false if the symlink couldn't be resolved, such that this naturally maps to our desired behavior of broken symlinks being categorized as a file.
1 parent 595a468 commit 5428c98

File tree

3 files changed

+90
-29
lines changed

3 files changed

+90
-29
lines changed

src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public override bool DirectoryExists(string fullPath)
318318
return DirectoryExists(fullPath, out errno);
319319
}
320320

321-
private bool DirectoryExists(string fullPath, out int errno)
321+
private static bool DirectoryExists(string fullPath, out int errno)
322322
{
323323
return FileExists(fullPath, Interop.libcoreclr.FileTypes.S_IFDIR, out errno);
324324
}
@@ -329,7 +329,7 @@ public override bool FileExists(string fullPath)
329329
return FileExists(fullPath, Interop.libcoreclr.FileTypes.S_IFREG, out errno);
330330
}
331331

332-
private bool FileExists(string fullPath, int fileType, out int errno)
332+
private static bool FileExists(string fullPath, int fileType, out int errno)
333333
{
334334
Interop.libcoreclr.fileinfo fileinfo;
335335
while (true)
@@ -475,48 +475,53 @@ private IEnumerator<T> Enumerate(Interop.libc.SafeDirHandle dirHandle)
475475
string name = Interop.libc.GetDirEntName(curEntry);
476476

477477
// Get from the dir entry whether the entry is a file or directory.
478-
// If we're not sure from the dir entry itself, stat to the entry.
479-
bool isDir = false, isFile = false;
478+
// We classify everything as a file unless we know it to be a directory,
479+
// e.g. a FIFO will be classified as a file.
480+
bool isDir;
480481
switch (Interop.libc.GetDirEntType(curEntry))
481482
{
482483
case Interop.libc.DType.DT_DIR:
484+
// We know it's a directory.
483485
isDir = true;
484486
break;
485-
case Interop.libc.DType.DT_REG:
486-
isFile = true;
487-
break;
488487
case Interop.libc.DType.DT_LNK:
489488
case Interop.libc.DType.DT_UNKNOWN:
490-
string fullPath = Path.Combine(dirPath.FullPath, name);
491-
Interop.libcoreclr.fileinfo fileinfo;
492-
while (Interop.CheckIo(Interop.libcoreclr.GetFileInformationFromPath(fullPath, out fileinfo), fullPath)) ;
493-
isDir = (fileinfo.mode & Interop.libcoreclr.FileTypes.S_IFMT) == Interop.libcoreclr.FileTypes.S_IFDIR;
494-
isFile = (fileinfo.mode & Interop.libcoreclr.FileTypes.S_IFMT) == Interop.libcoreclr.FileTypes.S_IFREG;
489+
// It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
490+
// If we can't (e.g.symlink to a file, broken symlink, etc.), we'll just treat it as a file.
491+
int errnoIgnored;
492+
isDir = DirectoryExists(Path.Combine(dirPath.FullPath, name), out errnoIgnored);
493+
break;
494+
default:
495+
// Otherwise, treat it as a file. This includes regular files,
496+
// FIFOs, etc.
497+
isDir = false;
495498
break;
496499
}
497-
bool matchesSearchPattern =
498-
(isFile || isDir) &&
499-
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0;
500500

501501
// Yield the result if the user has asked for it. In the case of directories,
502502
// always explore it by pushing it onto the stack, regardless of whether
503503
// we're returning directories.
504-
if (isDir && !ShouldIgnoreDirectory(name))
504+
if (isDir)
505505
{
506-
if (_includeDirectories && matchesSearchPattern)
507-
{
508-
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/true);
509-
}
510-
if (_searchOption == SearchOption.AllDirectories)
506+
if (!ShouldIgnoreDirectory(name))
511507
{
512-
if (toExplore == null)
508+
if (_includeDirectories &&
509+
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0)
510+
{
511+
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/true);
512+
}
513+
if (_searchOption == SearchOption.AllDirectories)
513514
{
514-
toExplore = new Stack<PathPair>();
515+
if (toExplore == null)
516+
{
517+
toExplore = new Stack<PathPair>();
518+
}
519+
toExplore.Push(new PathPair(Path.Combine(dirPath.UserPath, name), Path.Combine(dirPath.FullPath, name)));
515520
}
516-
toExplore.Push(new PathPair(Path.Combine(dirPath.UserPath, name), Path.Combine(dirPath.FullPath, name)));
517521
}
518522
}
519-
else if (isFile && _includeFiles && matchesSearchPattern)
523+
else if (_includeFiles &&
524+
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0)
520525
{
521526
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/false);
522527
}

src/System.IO.FileSystem/tests/Directory/GetDirectories_str.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33

44
using System;
55
using System.IO;
6-
using System.Collections;
7-
using System.Globalization;
8-
using System.Text;
9-
using System.Threading;
6+
using System.Linq;
107
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
119
using Xunit;
1210

1311
public class Directory_GetDirectories_str
@@ -347,6 +345,33 @@ public static void printerr(String err, [CallerMemberName] string memberName = "
347345
{
348346
Console.WriteLine("ERROR: ({0}, {1}, {2}) {3}", memberName, filePath, lineNumber, err);
349347
}
348+
349+
[Fact]
350+
[PlatformSpecific(PlatformID.AnyUnix)]
351+
public void EnumerateWithSymLinkToDirectory()
352+
{
353+
using (var containingFolder = new TemporaryDirectory())
354+
{
355+
// Test a symlink to a directory that does and then doesn't exist
356+
using (var targetDir = new TemporaryDirectory())
357+
{
358+
// Create a symlink to a folder that exists
359+
string linkPath = Path.Combine(containingFolder.Path, Path.GetRandomFileName());
360+
Assert.Equal(0, symlink(targetDir.Path, linkPath));
361+
Assert.True(Directory.Exists(linkPath));
362+
Assert.Equal(1, Directory.GetDirectories(containingFolder.Path).Count());
363+
}
364+
365+
// The target file is gone and the symlink still exists; since it can't be resolved,
366+
// it's treated as a file rather than as a directory.
367+
Assert.Equal(0, Directory.GetDirectories(containingFolder.Path).Count());
368+
Assert.Equal(1, Directory.GetFiles(containingFolder.Path).Count());
369+
}
370+
}
371+
372+
[DllImport("libc", SetLastError = true)]
373+
private static extern int symlink(string path1, string path2);
374+
350375
}
351376

352377

src/System.IO.FileSystem/tests/Directory/GetFiles_str.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.IO;
66
using System.IO.FileSystem.Tests;
7+
using System.Linq;
8+
using System.Runtime.InteropServices;
79
using Xunit;
810

911
public class Directory_GetFiles_str : FileSystemTest
@@ -156,5 +158,34 @@ public void UnixFilePathWithSpaces()
156158
}
157159
}
158160

161+
[Fact]
162+
[PlatformSpecific(PlatformID.AnyUnix)]
163+
public void EnumerateWithSymLinkToFile()
164+
{
165+
using (var containingFolder = new TemporaryDirectory())
166+
{
167+
string linkPath;
168+
169+
// Test a symlink to a file that does and then doesn't exist
170+
using (var targetFile = new TemporaryFile())
171+
{
172+
linkPath = Path.Combine(containingFolder.Path, Path.GetRandomFileName());
173+
Assert.Equal(0, symlink(targetFile.Path, linkPath));
174+
Assert.True(File.Exists(linkPath));
175+
Assert.Equal(1, GetFiles(containingFolder.Path).Count());
176+
}
177+
178+
// The symlink still exists even though the target file is gone.
179+
Assert.Equal(1, GetFiles(containingFolder.Path).Count());
180+
181+
// The symlink is gone
182+
File.Delete(linkPath);
183+
Assert.Equal(0, GetFiles(containingFolder.Path).Count());
184+
}
185+
}
186+
187+
[DllImport("libc", SetLastError = true)]
188+
private static extern int symlink(string path1, string path2);
189+
159190
#endregion
160191
}

0 commit comments

Comments
 (0)