Skip to content
Merged
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
92 changes: 62 additions & 30 deletions src/System.Management.Automation/namespaces/FileSystemProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1845,11 +1845,10 @@ private void Dir(
// a) the user has asked to with the -FollowSymLinks switch parameter and
// b) the directory pointed to by the symlink has not already been visited,
// preventing symlink loops.
// c) it is not a name surrogate making it not a symlink
// c) it is not a reparse point with a target
if (tracker == null)
{
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(recursiveDirectory) &&
InternalSymbolicLinkLinkCodeMethods.IsNameSurrogateReparsePoint(recursiveDirectory.FullName))
if (InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(recursiveDirectory))
{
continue;
}
Expand Down Expand Up @@ -2001,8 +2000,7 @@ string ToModeString(FileSystemInfo fileSystemInfo)
public static string NameString(PSObject instance)
{
return instance?.BaseObject is FileSystemInfo fileInfo
? (InternalSymbolicLinkLinkCodeMethods.IsReparsePoint(fileInfo) &&
InternalSymbolicLinkLinkCodeMethods.IsNameSurrogateReparsePoint(fileInfo.FullName))
? InternalSymbolicLinkLinkCodeMethods.IsReparsePointWithTarget(fileInfo)
? $"{fileInfo.Name} -> {InternalSymbolicLinkLinkCodeMethods.GetTarget(instance)}"
: fileInfo.Name
: string.Empty;
Expand Down Expand Up @@ -7704,6 +7702,8 @@ public static class InternalSymbolicLinkLinkCodeMethods

private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003;

private const uint IO_REPARSE_TAG_APPEXECLINK = 0x8000001B;

private const string NonInterpretedPathPrefix = @"\??\";

private const int MAX_PATH = 260;
Expand Down Expand Up @@ -7790,6 +7790,17 @@ private struct REPARSE_DATA_BUFFER_MOUNTPOINT
public byte[] PathBuffer;
}

[StructLayout(LayoutKind.Sequential)]
private struct REPARSE_DATA_BUFFER_APPEXECLINK
{
public uint ReparseTag;
public ushort ReparseDataLength;
public ushort Reserved;
public uint StringCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3FF0)]
public byte[] StringList;
}

[StructLayout(LayoutKind.Sequential)]
private struct BY_HANDLE_FILE_INFORMATION
{
Expand Down Expand Up @@ -7989,13 +8000,23 @@ private static string WinInternalGetLinkType(string filePath)

REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_SYMBOLICLINK>(outBuffer);

if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
linkType = "SymbolicLink";
else if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
linkType = "Junction";
else
switch (reparseDataBuffer.ReparseTag)
{
linkType = IsHardLink(ref dangerousHandle) ? "HardLink" : null;
case IO_REPARSE_TAG_SYMLINK:
linkType = "SymbolicLink";
break;

case IO_REPARSE_TAG_MOUNT_POINT:
linkType = "Junction";
break;

case IO_REPARSE_TAG_APPEXECLINK:
linkType = "AppExeCLink";
break;

default:
linkType = IsHardLink(ref dangerousHandle) ? "HardLink" : null;
break;
}

return linkType;
Expand Down Expand Up @@ -8026,22 +8047,25 @@ internal static bool IsReparsePoint(FileSystemInfo fileInfo)
return fileInfo.Attributes.HasFlag(System.IO.FileAttributes.ReparsePoint);
}

internal static bool IsNameSurrogateReparsePoint(string filePath)
internal static bool IsReparsePointWithTarget(FileSystemInfo fileInfo)
{
if (!IsReparsePoint(fileInfo))
{
return false;
}
#if !UNIX
var data = new WIN32_FIND_DATA();
using (SafeFileHandle handle = FindFirstFileEx(filePath, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
using (SafeFileHandle handle = FindFirstFileEx(fileInfo.FullName, FINDEX_INFO_LEVELS.FindExInfoBasic, ref data, FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, 0))
{
// Name surrogates are reparse points that point to other named entities local to the filesystem (like symlinks)
// Name surrogates (0x20000000) are reparse points that point to other named entities local to the filesystem (like symlinks)
// In the case of OneDrive, they are not surrogates and would be safe to recurse into.
// This code is equivalent to the IsReparseTagNameSurrogate macro: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/content/ntifs/nf-ntifs-isreparsetagnamesurrogate
if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0)
if (!handle.IsInvalid && (data.dwReserved0 & 0x20000000) == 0 && (data.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK) != IO_REPARSE_TAG_APPEXECLINK)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have the two checks of dwReserved0 a different form?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first half is the equivalent of the IsReparseTagNameSurrogate macro which explicitly covers the OneDrive scenario. The addition is specific to this new reparse point type.

Copy link
Collaborator

@iSazonov iSazonov Aug 13, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant why we do not use
data.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK) == 0

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IO_REPARSE_TAGs aren't bitflags, so in the case of OneDrive, dwReserved0 has a value of 0x9000601A. We want this check to succeed so we don't try to access the file and end up downloading it, so the first half passes, the second half resolves to 0x8000001A which also passes (as IO_REPARSE_TAG_APPEXECLINK is 0x8000001B). If we change it to & IO_REPARSE_TAG_APPEXECLINK) == 0, then the second half fails as it's not zero so we end up downloading files from OneDrive.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be just data.dwReserved0 != IO_REPARSE_TAG_APPEXECLINK without any bit masking, then?

The current comparison (data.dwReserved0 & IO_REPARSE_TAG_APPEXECLINK) != IO_REPARSE_TAG_APPEXECLINK will return false for e.g. IO_REPARSE_TAG_WCI_TOMBSTONE = 0xA000001F, or for 0x8000003B if Microsoft later starts using that for something. Because WinInternalGetLinkType does not treat those values as AppX reparse tags, I expect IsReparsePointWithTarget shouldn't either.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not an expert on reparse tags, either.

Microsoft's sample code compares to IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 without bit masking. https://github.com/microsoft/Windows-driver-samples/blob/82bd522bb3b893e145d6d73f930edfc04977e4de/filesys/miniFilter/minispy/user/mspyLog.c#L70

The Reparse Point Tag Request instructions ask the driver developer to specify how the flags in the highest bits of the 32-bit reparse tag should be set. This seems to imply that the developer is not allowed to flip those flags on their own later. https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/reparse-point-tag-request

Some information about the 0x10000000 bit and the IsReparseTagDirectory macro, which don't seem to be documented at docs.microsoft.com yet, was posted to the NTFSD list in September 2016. https://community.osr.com/discussion/comment/279158#Comment_279158

Copy link
Collaborator

@iSazonov iSazonov Aug 22, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed all code I found. The tags is absolute values and I think a right way is to do simple comparison.

For reference https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/c8e77b37-3909-4fe6-a4ea-2b9d423b1ee4

It seems we use 0x20000000 as a trick to catch both IO_REPARSE_TAG_MOUNT_POINT and IO_REPARSE_TAG_SYMLINK

I'd use explicit values otherwise we can fall in tricky bug.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the 0x20000000 flag (IsReparseTagNameSurrogate) can be OK, if you only care about the coarse meaning of the reparse tag and not about the format of the reparse data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iSazonov do you want to submit a new PR to clean this up?

Copy link
Collaborator

@iSazonov iSazonov Aug 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done #10431

{
return false;
}
}
#endif
// true means the reparse point is a symlink
return true;
}

Expand Down Expand Up @@ -8205,25 +8229,33 @@ private static string WinInternalGetTarget(SafeFileHandle handle)
throw new Win32Exception(lastError);
}

// Unmarshal to symbolic link to look for tags.
REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_SYMBOLICLINK>(outBuffer);

if (reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_SYMLINK && reparseDataBuffer.ReparseTag != IO_REPARSE_TAG_MOUNT_POINT)
return null;

string targetDir = null;

if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_SYMLINK)
{
targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);
}
REPARSE_DATA_BUFFER_SYMBOLICLINK reparseDataBuffer = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_SYMBOLICLINK>(outBuffer);

if (reparseDataBuffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT)
switch (reparseDataBuffer.ReparseTag)
{
// Since this is a junction we need to unmarshal to the correct structure.
REPARSE_DATA_BUFFER_MOUNTPOINT reparseDataBufferMountPoint = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_MOUNTPOINT>(outBuffer);
case IO_REPARSE_TAG_SYMLINK:
targetDir = Encoding.Unicode.GetString(reparseDataBuffer.PathBuffer, reparseDataBuffer.SubstituteNameOffset, reparseDataBuffer.SubstituteNameLength);
break;

case IO_REPARSE_TAG_MOUNT_POINT:
REPARSE_DATA_BUFFER_MOUNTPOINT reparseMountPointDataBuffer = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_MOUNTPOINT>(outBuffer);
targetDir = Encoding.Unicode.GetString(reparseMountPointDataBuffer.PathBuffer, reparseMountPointDataBuffer.SubstituteNameOffset, reparseMountPointDataBuffer.SubstituteNameLength);
break;

case IO_REPARSE_TAG_APPEXECLINK:
REPARSE_DATA_BUFFER_APPEXECLINK reparseAppExeDataBuffer = Marshal.PtrToStructure<REPARSE_DATA_BUFFER_APPEXECLINK>(outBuffer);
// The target file is at index 2
if (reparseAppExeDataBuffer.StringCount >= 3)
{
string temp = Encoding.Unicode.GetString(reparseAppExeDataBuffer.StringList);
targetDir = temp.Split('\0')[2];
}
break;

targetDir = Encoding.Unicode.GetString(reparseDataBufferMountPoint.PathBuffer, reparseDataBufferMountPoint.SubstituteNameOffset, reparseDataBufferMountPoint.SubstituteNameLength);
default:
return null;
}

if (targetDir != null && targetDir.StartsWith(NonInterpretedPathPrefix, StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Describe "Get-ChildItem" -Tags "CI" {
@{Parameters = @{Path = (Join-Path $searchRoot '*'); Recurse = $true; File = $true }; ExpectedCount = 1; Title = "file with wildcard"},
@{Parameters = @{Path = (Join-Path $searchRoot 'F*.txt'); Recurse = $true; File = $true }; ExpectedCount = 1; Title = "file with wildcard filename"}
)

$SkipAppExeCLinks = $true
if ($IsWindows -and (Get-ChildItem -Path ~\AppData\Local\Microsoft\WindowsApps\*.exe -ErrorAction Ignore) -ne $null)
{
$SkipAppExeCLinks = $false
}
}

It "Should list the contents of the current folder" {
Expand Down Expand Up @@ -174,6 +180,12 @@ Describe "Get-ChildItem" -Tags "CI" {
$null = New-Item -Path TestDrive:/noextension -ItemType File
(Get-ChildItem -File -LiteralPath TestDrive:/ -Filter noext*.*).Name | Should -BeExactly 'noextension'
}

It "Understand APPEXECLINKs" -Skip:($SkipAppExeCLinks) {
$app = Get-ChildItem -Path ~\appdata\local\microsoft\windowsapps\*.exe | Select-Object -First 1
$app.Target | Should -Not -Be $app.FullName
$app.LinkType | Should -BeExactly 'AppExeCLink'
}
}

Context 'Env: Provider' {
Expand Down