Skip to content

Commit 280fb74

Browse files
danmoseleyCopilot
andauthored
Fix deterministic MountVolume test failures on ARM64 Helix machines (#126660)
> [!NOTE] > This PR was created with Copilot assistance. ## Fix deterministic MountVolume test failures on ARM64 Helix machines Fixes #125295, fixes #125624, fixes #126627 ### Problem `Directory_Delete_MountVolume.RunTest` and `Directory_ReparsePoints_MountVolume.runTest` fail deterministically (~100% of the time, ~750ms duration) on the `Windows.11.Arm64.Open` Helix machine pool. This is **not timing-related** and was not addressed by the delay/polling fixes in #125914 or the Unmount resilience fix in #125625 (those PRs fixed real timing issues -- pre-fix failures on other configurations have since expired from AzDO retention, so we can't verify directly, but there is no evidence they were ineffective for their intended purpose). **Root cause**: The ARM64 Helix machines have an E:\ drive (likely an Azure resource/temp disk) that passes all `DriveInfo` checks -- `DriveType=Fixed`, `DriveFormat=NTFS`, `IsReady=True` -- but `GetVolumeNameForVolumeMountPoint` fails with `ERROR_INVALID_PARAMETER` (87). The drive has no volume GUID and doesn't support volume mount point operations. `IOServices.GetNtfsDriveOtherThanCurrent()` returns this drive, and the test crashes trying to use it. Some ARM64 Helix machines have only C:\ and a CD-ROM (no second drive at all). On those machines, the cross-drive scenarios already skip gracefully and only same-drive scenarios 3.x run. ### Evidence Analyzed Helix console logs from 5 post-fix builds (all `arm64-NativeAOT-Win11`, same C:\ volume GUID). Every failure shows the identical pattern: - Scenario 1: `GetVolumeNameForVolumeMountPoint("E:\")` -> error 87 - Scenario 2: `SetVolumeMountPoint` onto E:\ succeeds but path traversal through the mount point fails with `DirectoryNotFoundException` - Scenarios 3.x (same-drive): Always pass Reproduced locally by removing the real E: drive letter and creating `SUBST E:` which exhibits identical error 87 behavior. ### Changes 1. **`IOServices.GetNtfsDriveOtherThan()`**: After the existing Fixed/Ready/NTFS checks, also verify the drive has a volume GUID via `GetVolumeNameForVolumeMountPoint`. Drives without one (SUBST drives, Azure resource disks) are skipped. 2. **`DumpDriveInformation` diagnostic test**: New Helix-only test (following the `DescriptionNameTests.DumpRuntimeInformationToConsole` pattern) that dumps all drives with their volume GUIDs to the console log. Makes future drive-related CI issues immediately diagnosable from the same Helix work item log. 3. **`GetVolumeNameForVolumeMountPoint` P/Invoke in DllImports.cs**: Uses `char[]` (not `StringBuilder`) because this file uses `LibraryImport` which does not support `StringBuilder`. ### Local validation | Scenario | Before fix | After fix | |---|---|---| | SUBST E: (no volume GUID) | Error 87 / DirectoryNotFoundException | Pass (SUBST filtered, scenarios 3.x run) | | Real NTFS E: | Pass (all scenarios) | Pass (all scenarios) | | Single-drive machine | Scenarios 1/2 skip, 3.x pass | Same -- no change | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d38d8f7 commit 280fb74

File tree

4 files changed

+103
-3
lines changed

4 files changed

+103
-3
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Runtime.InteropServices;
7+
using Microsoft.DotNet.XUnitExtensions;
8+
using Xunit;
9+
10+
namespace System.IO.Tests
11+
{
12+
public class DumpDriveInformation
13+
{
14+
// When running both inner and outer loop together, dump only once
15+
private static bool s_dumped = false;
16+
17+
[Fact]
18+
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "Not applicable")]
19+
public void DumpDriveInformationToConsole()
20+
{
21+
if (s_dumped || !PlatformDetection.IsInHelix)
22+
return;
23+
24+
s_dumped = true;
25+
26+
// Not really a test, but useful to dump drive/volume information to the test log
27+
// to help debug environmental issues with mount volume tests in CI.
28+
// Follows the pattern of DescriptionNameTests.DumpRuntimeInformationToConsole.
29+
30+
Console.WriteLine("### DRIVE INFORMATION");
31+
Console.WriteLine($"### Machine: {Environment.MachineName}");
32+
Console.WriteLine($"### OS: {RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})");
33+
Console.WriteLine($"### Current directory: {Directory.GetCurrentDirectory()}");
34+
35+
foreach (DriveInfo drive in DriveInfo.GetDrives())
36+
{
37+
try
38+
{
39+
string info = $"### Drive {drive.Name}: Type={drive.DriveType}";
40+
if (drive.IsReady)
41+
{
42+
info += $" Format={drive.DriveFormat} Size={drive.TotalSize / (1024 * 1024)}MB";
43+
44+
if (OperatingSystem.IsWindows())
45+
{
46+
char[] volName = new char[260];
47+
bool hasGuid = DllImports.GetVolumeNameForVolumeMountPoint(drive.Name, volName, volName.Length);
48+
info += hasGuid
49+
? $" VolumeGUID={new string(volName).TrimEnd('\0')}"
50+
: $" VolumeGUID=NONE({Marshal.GetLastPInvokeErrorMessage()})";
51+
}
52+
}
53+
else
54+
{
55+
info += " NotReady";
56+
}
57+
Console.WriteLine(info);
58+
}
59+
catch (Exception ex)
60+
{
61+
Console.WriteLine($"### Drive {drive.Name}: error probing: {ex.GetType().Name}: {ex.Message}");
62+
}
63+
}
64+
65+
if (OperatingSystem.IsWindows())
66+
{
67+
string otherNtfs = IOServices.GetNtfsDriveOtherThanCurrent();
68+
Console.WriteLine($"### GetNtfsDriveOtherThanCurrent() = {otherNtfs ?? "(null)"}");
69+
}
70+
}
71+
72+
[Fact]
73+
[OuterLoop]
74+
[SkipOnPlatform(TestPlatforms.Browser | TestPlatforms.Wasi, "Not applicable")]
75+
public void DumpDriveInformationToConsoleOuter()
76+
{
77+
// Outer loop runs don't run inner loop tests.
78+
// But we want to log this data for any Helix run.
79+
DumpDriveInformationToConsole();
80+
}
81+
}
82+
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/PortedCommon/DllImports.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Runtime.InteropServices;
6-
using System.Text;
76

87
internal static partial class DllImports
98
{
@@ -16,4 +15,8 @@ internal static partial class DllImports
1615

1716
[LibraryImport("kernel32.dll", EntryPoint = "GetDriveTypeW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
1817
internal static partial int GetDriveType(string drive);
18+
19+
[LibraryImport("kernel32.dll", EntryPoint = "GetVolumeNameForVolumeMountPointW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
20+
[return: MarshalAs(UnmanagedType.Bool)]
21+
internal static partial bool GetVolumeNameForVolumeMountPoint(string volumeMountPoint, [Out] char[] volumeName, int bufferLength);
1922
}

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/PortedCommon/IOServices.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,27 @@ public static string GetNtfsDriveOtherThan(string drive)
6464
if (!IsReady(otherDrive))
6565
continue;
6666

67-
if (IsDriveNTFS(otherDrive))
68-
return otherDrive;
67+
if (!IsDriveNTFS(otherDrive))
68+
continue;
69+
70+
// Filter out drives that don't support volume mount point operations
71+
// (e.g., SUBST drives, Azure resource disks without volume GUIDs).
72+
// These report as Fixed/NTFS/Ready but GetVolumeNameForVolumeMountPoint fails.
73+
if (!HasVolumeGuid(otherDrive))
74+
continue;
75+
76+
return otherDrive;
6977
}
7078

7179
return null;
7280
}
7381

82+
private static bool HasVolumeGuid(string drive)
83+
{
84+
char[] volumeName = new char[260];
85+
return DllImports.GetVolumeNameForVolumeMountPoint(drive, volumeName, volumeName.Length);
86+
}
87+
7488
public static string GetNonNtfsDriveOtherThanCurrent()
7589
{
7690
return GetNonNtfsDriveOtherThan(GetCurrentDrive());

src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<Compile Include="FileStream\ctor_options.cs" />
4343
<Compile Include="FileStream\Handle.cs" />
4444
<Compile Include="Directory\GetLogicalDrives.cs" />
45+
<Compile Include="DumpDriveInformation.cs" />
4546
<Compile Include="FileStream\LockUnlock.cs" />
4647
<Compile Include="FileSystemTest.cs" />
4748
<Compile Include="File\AppendAllBytesAsync.cs" />

0 commit comments

Comments
 (0)