Skip to content

DiskUtils: read the reparse buffer the driver returned - #248

Open
bfs2021 wants to merge 1 commit into
ForNeVeR:mainfrom
bfs2021:fix/reparse-buffer-length
Open

bfs2021 wants to merge 1 commit into
ForNeVeR:mainfrom
bfs2021:fix/reparse-buffer-length

Conversation

@bfs2021

@bfs2021 bfs2021 commented Oct 21, 2026

Copy link
Copy Markdown

Problem

Fixes #242. DiskUtils.TryGetReparseDataBuffer rents a 16 KiB buffer from ArrayPool<byte>.Shared, calls DeviceIoControl(FSCTL_GET_REPARSE_POINT), then reads a SymbolicLinkReparseBuffer out of the whole array. The driver-reported byte count is discarded, and pooled arrays are not zeroed, so MemoryMarshal.TryRead always succeeds. When the driver returns fewer bytes than the struct size, the remaining fields are leftovers from whatever previously used the pooled array. The method also interprets the symlink-specific layout for every reparse tag, although fields such as Flags only exist for IO_REPARSE_TAG_SYMLINK.

Root cause

  • lpBytesReturned was passed as out _ and ignored.
  • The span covered the entire rented array rather than the bytes the driver actually wrote.
  • The tag-specific union was unconditionally reinterpreted as SymbolicLinkReparseBuffer.
  • The rented buffer was returned to the pool only on the success path.

Fix

  • Capture lpBytesReturned, validate it against the rented array size, and parse only buffer[0..bytesReturned]. A response shorter than the common header now returns null.
  • Clear the fixed header region of the rented array before the call, so stale pooled data cannot leak into the result.
  • Replace SymbolicLinkReparseBuffer with ReparseDataBufferHeader (ReparseTag, ReparseDataLength, Reserved), the part common to all reparse tags. Tag-specific data is never interpreted here.
  • Return the rented array from a finally block on every path.
  • IsJunction is unchanged and still only compares ReparseTag.

Testing

  • New tests in TruePath.Tests/DiskUtilsTests.cs: empty and too-short buffers return null; header fields are parsed correctly; tag-specific bytes after the header are ignored; a real non-reparse directory returns null. No elevated privileges needed.
  • dotnet build TruePath.sln and dotnet build TruePath.sln -p:CheckXmlDoc=true --no-incremental: clean.
  • dotnet test TruePath.Tests: 477 passed, 1 pre-existing failure (AbsolutePathTests.ReadKind_IsSymlink, which needs symlink-creation privilege; verified identical without this change).
  • dotnet test ... -- --filter-class TruePath.Tests.DiskUtilsTests: 10/10 passed.

TryGetReparseDataBuffer ignored the byte count returned by DeviceIoControl and read a SymbolicLinkReparseBuffer from the whole rented ArrayPool buffer. Pooled arrays are not zeroed, and the span was larger than the actual response, so MemoryMarshal.TryRead always succeeded: when the driver returned fewer bytes than the struct size, the remaining fields contained stale data from whatever previously used the array.

The method also interpreted SymbolicLinkReparseBuffer for every reparse tag, although its fields (including Flags) are only valid for IO_REPARSE_TAG_SYMLINK; for mount points, the same bytes belong to a different layout.

Capture lpBytesReturned and parse only the returned prefix of the buffer, so a response shorter than the common header is rejected instead of reading stale memory. Clear the header area of the pooled array before the call as well. Replace the symlink-specific struct with a ReparseDataBufferHeader containing only the fields common to all reparse tags; tag-specific data must be parsed per tag after checking ReparseTag. Return the rented buffer to the pool from a finally block on every path.

IsJunction still reads only ReparseTag, so its behavior is unchanged. Added tests for empty and too-short buffers, header parsing, ignoring tag-specific bytes, and a non-reparse-point path.

Signed-off-by: bfs2021 <101915083+bfs2021@users.noreply.github.com>
@ForNeVeR
ForNeVeR self-requested a review October 21, 2026 09:11
@ForNeVeR ForNeVeR self-assigned this Oct 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DiskUtils.TryGetReparseDataBuffer reads a struct from unzeroed pooled memory and ignores the number of bytes returned

2 participants