Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 83 additions & 18 deletions src/libraries/System.Private.CoreLib/src/System/IO/FileSystem.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -697,26 +697,15 @@ internal static void CreateHardLink(string path, string pathToTarget)
}
else
{
string? current = linkTarget;
int visitCount = 1;

while (current != null)
{
if (visitCount > MaxFollowedLinks)
{
sb.Dispose();
// We went over the limit and couldn't reach the final target
throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath));
}

GetLinkTargetFullPath(ref sb, current);
current = Interop.Sys.ReadLink(sb.AsSpan());
visitCount++;
}
sb.Dispose();
linkTarget = ResolveFinalTarget(linkPath, linkTarget);
}

Debug.Assert(sb.Length > 0);
linkTarget = sb.ToString(); // ToString disposes
if (!returnFinalTarget)
{
Debug.Assert(sb.Length > 0);
linkTarget = sb.ToString(); // ToString disposes
}

return isDirectory ?
new DirectoryInfo(linkTarget) :
Expand All @@ -738,6 +727,82 @@ static void GetLinkTargetFullPath(ref ValueStringBuilder sb, ReadOnlySpan<char>
}
sb.Append(linkTarget);
}

static string ResolveFinalTarget(string linkPath, string linkTarget)
{
int visitCount = 1;
string currentPath = Path.GetFullPath(linkPath);
string[] pendingComponents = new string[8];
int pendingComponentCount = 0;

// Resolve relative targets one component at a time because an intermediate component may itself be a link.
FollowLinkTarget(linkTarget);

while (pendingComponentCount > 0)
{
string component = pendingComponents[--pendingComponentCount];
pendingComponents[pendingComponentCount] = null!;

if (component == ".")
{
continue;
}

if (component == "..")
{
currentPath = Path.GetDirectoryName(currentPath) ?? currentPath;
continue;
}

currentPath = Path.Join(currentPath, component);
string? nestedTarget = Interop.Sys.ReadLink(currentPath);
if (nestedTarget != null)
{
ThrowIfTooManyLinks();
FollowLinkTarget(nestedTarget);
}
}

return currentPath;

void FollowLinkTarget(string target)
{
// Preserve the lexical path of absolute targets while following a link at that exact path.
while (!PathInternal.IsPartiallyQualified(target))
{
currentPath = target;
string? nextTarget = Interop.Sys.ReadLink(currentPath);
if (nextTarget == null)
{
return;
}

ThrowIfTooManyLinks();
target = nextTarget;
}

currentPath = Path.GetDirectoryName(currentPath)!;
string[] components = target.Split(PathInternal.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries);
if (pendingComponentCount + components.Length > pendingComponents.Length)
{
Array.Resize(ref pendingComponents, Math.Max(pendingComponentCount + components.Length, pendingComponents.Length * 2));
}

for (int i = components.Length - 1; i >= 0; i--)
{
pendingComponents[pendingComponentCount++] = components[i];
}
}

void ThrowIfTooManyLinks()
{
visitCount++;
if (visitCount > MaxFollowedLinks)
{
throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, linkPath));
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,51 @@ public void ResolveLinkTarget_ReturnFinalTarget_Relative_WithRedundantSegments()
filePath: filePath);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
[PlatformSpecific(TestPlatforms.AnyUnix)]
public void ResolveLinkTarget_ReturnFinalTarget_RelativeIntermediateDirectoryLink(bool targetExists)
{
string rootPath = GetRandomDirPath();
Directory.CreateDirectory(rootPath);

const string versionDirectoryName = "..2025_08_14_07_17_19.1395403829";
string versionDirectoryPath = Path.Join(rootPath, versionDirectoryName);
Directory.CreateDirectory(versionDirectoryPath);

string targetName = IsDirectoryTest ? "target-directory" : "target-file";
string targetPath = Path.Join(versionDirectoryPath, targetName);
if (targetExists)
{
CreateFileOrDirectory(targetPath);
}

Directory.CreateSymbolicLink(Path.Join(rootPath, "..data"), versionDirectoryName);

string linkPath = Path.Join(rootPath, "link");
CreateSymbolicLink(linkPath, Path.Join("..data", targetName));

FileSystemInfo targetInfo = ResolveLinkTarget(linkPath, returnFinalTarget: true);
Assert.Equal(targetPath, targetInfo.FullName);
Assert.Equal(targetExists, targetInfo.Exists);
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)]
public void ResolveLinkTarget_ReturnFinalTarget_IntermediateDirectoryLinkCycle_Throws()
{
string rootPath = GetRandomDirPath();
Directory.CreateDirectory(rootPath);

Directory.CreateSymbolicLink(Path.Join(rootPath, "cycle"), "cycle");

string linkPath = Path.Join(rootPath, "link");
CreateSymbolicLink(linkPath, Path.Join("cycle", "target"));

Assert.Throws<IOException>(() => ResolveLinkTarget(linkPath, returnFinalTarget: true));
}

[Theory]
[InlineData(1, false)]
[InlineData(10, false)]
Expand Down
Loading