diff --git a/TruePath.Tests/DiskUtilsTests.cs b/TruePath.Tests/DiskUtilsTests.cs index acd735d..19f79f9 100644 --- a/TruePath.Tests/DiskUtilsTests.cs +++ b/TruePath.Tests/DiskUtilsTests.cs @@ -72,6 +72,83 @@ public void DiskUtils_PassNonExistentPath_KeepFileDescriptorZeroOpen() } } + [Fact] + public void ParseReparseDataBuffer_EmptyBuffer_ReturnsNull() + { + Assert.Null(DiskUtils.ParseReparseDataBuffer(ReadOnlySpan.Empty)); + } + + [Theory] + [InlineData(1)] + [InlineData(4)] + [InlineData(7)] + public void ParseReparseDataBuffer_BufferShorterThanHeader_ReturnsNull(int length) + { + Assert.Null(DiskUtils.ParseReparseDataBuffer(new byte[length])); + } + + [Fact] + public void ParseReparseDataBuffer_ReturnsCommonHeaderFields() + { + // Arrange + var expected = new Kernel32.ReparseDataBufferHeader + { + ReparseTag = Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT, + ReparseDataLength = 0x18, + Reserved = 0 + }; + var buffer = new byte[Marshal.SizeOf()]; + MemoryMarshal.Write(buffer, in expected); + + // Act + var actual = DiskUtils.ParseReparseDataBuffer(buffer); + + // Assert + Assert.NotNull(actual); + Assert.Equal(expected.ReparseTag, actual.Value.ReparseTag); + Assert.Equal(expected.ReparseDataLength, actual.Value.ReparseDataLength); + Assert.Equal(expected.Reserved, actual.Value.Reserved); + } + + [Fact] + public void ParseReparseDataBuffer_IgnoresTagSpecificDataAfterHeader() + { + // Arrange: a mount point response has no symbolic-link fields, only the common header is valid. + var headerSize = Marshal.SizeOf(); + var expectedTag = Kernel32.IOReparseOptions.IO_REPARSE_TAG_MOUNT_POINT; + var buffer = new byte[headerSize + 12]; + MemoryMarshal.Write(buffer.AsSpan(0, sizeof(uint)), in expectedTag); + buffer.AsSpan(headerSize).Fill(0xFF); + + // Act + var actual = DiskUtils.ParseReparseDataBuffer(buffer); + + // Assert + Assert.NotNull(actual); + Assert.Equal(expectedTag, actual.Value.ReparseTag); + } + + [Fact] + public void TryGetReparseDataBuffer_OnNonReparsePoint_ReturnsNull() + { + // Arrange + if (!OperatingSystem.IsWindows()) return; + + var directory = Temporary.CreateTempFolder(); + try + { + // Act + var actual = DiskUtils.TryGetReparseDataBuffer(directory.Value); + + // Assert + Assert.Null(actual); + } + finally + { + directory.DeleteEmptyDirectory(); + } + } + // ReSharper disable once InconsistentNaming private const int F_GETFD = 1; // The same on Linux and macOS. diff --git a/TruePath/DiskUtils.cs b/TruePath/DiskUtils.cs index a605a1e..19f9f29 100644 --- a/TruePath/DiskUtils.cs +++ b/TruePath/DiskUtils.cs @@ -38,13 +38,18 @@ public static bool IsJunction(string path) } /// - /// Tries to get the reparse data buffer for the specified path. + /// Tries to get the reparse data buffer header for the specified path. /// /// The path to check. /// - /// A containing the reparse data if successful; otherwise, . + /// A containing the common reparse data header if successful; otherwise, + /// . /// - internal static unsafe SymbolicLinkReparseBuffer? TryGetReparseDataBuffer(string path) + /// + /// Only the header common to all reparse tags is returned. The layout of the data following it depends on the tag, + /// so it must not be interpreted without checking first. + /// + internal static unsafe ReparseDataBufferHeader? TryGetReparseDataBuffer(string path) { using SafeFileHandle output = CreateFile( path, @@ -61,37 +66,59 @@ public static bool IsJunction(string path) } byte[] buffer = ArrayPool.Shared.Rent(MAXIMUM_REPARSE_DATA_BUFFER_SIZE); - - bool success; - fixed (byte* pBuffer = buffer) + try { - success = DeviceIoControl( - output, - dwIoControlCode: FSCTL_GET_REPARSE_POINT, - lpInBuffer: null, - nInBufferSize: 0, - lpOutBuffer: pBuffer, - nOutBufferSize: MAXIMUM_REPARSE_DATA_BUFFER_SIZE, - out _, - IntPtr.Zero); + // Pooled arrays are not zeroed, so clear the part of the buffer we may read. The driver may return no data + // at all (e.g. when the path is not a reparse point), and the response may also be shorter than the header. + buffer.AsSpan(0, sizeof(ReparseDataBufferHeader)).Clear(); + + bool success; + uint bytesReturned; + fixed (byte* pBuffer = buffer) + { + success = DeviceIoControl( + output, + dwIoControlCode: FSCTL_GET_REPARSE_POINT, + lpInBuffer: null, + nInBufferSize: 0, + lpOutBuffer: pBuffer, + nOutBufferSize: MAXIMUM_REPARSE_DATA_BUFFER_SIZE, + out bytesReturned, + IntPtr.Zero); + } if (!success) { return null; } - } - - Span bufferSpan = new(buffer); - success = MemoryMarshal.TryRead(bufferSpan, out SymbolicLinkReparseBuffer reparseDataBuffer); - ArrayPool.Shared.Return(buffer); + // The rented array may be larger than the requested buffer, and the driver may return less data than the + // buffer can hold: never inspect bytes the driver did not write. + if (bytesReturned > (uint)buffer.Length) + { + return null; + } - if (success) + return ParseReparseDataBuffer(new ReadOnlySpan(buffer, 0, (int)bytesReturned)); + } + finally { - return reparseDataBuffer; + ArrayPool.Shared.Return(buffer); } + } - return null; + /// + /// Parses the common header of a reparse data buffer. + /// + /// The bytes returned by the driver. + /// + /// The parsed header, or if is shorter than the header. + /// + internal static ReparseDataBufferHeader? ParseReparseDataBuffer(ReadOnlySpan buffer) + { + return MemoryMarshal.TryRead(buffer, out ReparseDataBufferHeader reparseDataBuffer) + ? reparseDataBuffer + : null; } /// diff --git a/TruePath/Kernel32.cs b/TruePath/Kernel32.cs index dcab9c6..ea5d26b 100644 --- a/TruePath/Kernel32.cs +++ b/TruePath/Kernel32.cs @@ -290,13 +290,22 @@ internal static class IOReparseOptions } /// - /// Represents a symbolic link reparse buffer. + /// The header of a reparse data buffer, common to all reparse tags. /// /// + /// + /// The data following this header depends on . For example, symbolic links + /// () are followed by the symbolic link data, which includes a + /// Flags field, while mount points () are followed by + /// the mount point data, which has no Flags field. Do not interpret the tag-specific part without checking the + /// tag, and do not read past the number of bytes returned by the driver. + /// + /// /// For more information, see . + /// /// [StructLayout(LayoutKind.Sequential)] - internal struct SymbolicLinkReparseBuffer + internal struct ReparseDataBufferHeader { /// /// The reparse tag. @@ -312,31 +321,6 @@ internal struct SymbolicLinkReparseBuffer /// Reserved; do not use. /// internal ushort Reserved; - - /// - /// The offset, in bytes, to the substitute name string in the PathBuffer array. - /// - internal ushort SubstituteNameOffset; - - /// - /// The length, in bytes, of the substitute name string. If this string is null-terminated, SubstituteNameLength does not include space for the null character. - /// - internal ushort SubstituteNameLength; - - /// - /// The offset, in bytes, to the print name string in the PathBuffer array. - /// - internal ushort PrintNameOffset; - - /// - /// The length, in bytes, of the print name string. If this string is null-terminated, PrintNameLength does not include space for the null character. - /// - internal ushort PrintNameLength; - - /// - /// Flags that control the behavior of the symbolic link. - /// - internal uint Flags; } }