From 96d5dfbe2be85a6d4f81537ca5d1d4831cbbcea2 Mon Sep 17 00:00:00 2001 From: SACHIN SHARMA Date: Sat, 21 Mar 2026 18:38:20 -0700 Subject: [PATCH 1/5] Use Microsoft.IO.Redist for path/CWD on net472 in Framework (#13078) Add Microsoft.IO.Redist to Microsoft.Build.Framework for net4* targets. FileUtilities.GetFullPath and NativeMethods.GetCurrentDirectory use Microsoft.IO.Path and Microsoft.IO.Directory when FEATURE_MSIOREDIST is set. Legacy Win32 GetCurrentDirectory/GetFullPathName are not enabled by default: the FEATURE_LEGACY_* compile constants are preserved only as comments in Directory.BeforeCommon.targets, while the DllImports and helpers remain available behind FEATURE_LEGACY_* preprocessor guards for opt-in builds. Adjust Framework unit tests with a NETFRAMEWORK gate for path-too-long expander cases. --- .../Evaluation/Expander_Tests.cs | 7 ++- src/Directory.BeforeCommon.targets | 6 +- .../FileUtilities_Tests.cs | 4 -- src/Framework/FileUtilities.cs | 61 ++----------------- .../Microsoft.Build.Framework.csproj | 3 +- src/Framework/NativeMethods.cs | 42 +++++++++---- 6 files changed, 42 insertions(+), 81 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 4a9364feb78..12c48d0a554 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -923,7 +923,7 @@ public void ItemIncludeContainsMultipleItemReferences() logger.AssertLogContains("Item CleanFiles=foo.obj;bar.obj"); } -#if FEATURE_LEGACY_GETFULLPATH +#if NETFRAMEWORK /// /// Bad path when getting metadata through ->Metadata function /// @@ -942,6 +942,7 @@ public void InvalidPathAndMetadataItemFunctionPathTooLong() logger.AssertLogContains("MSB4023"); } + #endif /// @@ -982,7 +983,7 @@ public void InvalidMetadataName() logger.AssertLogContains("MSB4023"); } -#if FEATURE_LEGACY_GETFULLPATH +#if NETFRAMEWORK /// /// Bad path when getting metadata through ->WithMetadataValue function /// @@ -1041,7 +1042,7 @@ public void InvalidMetadataName2() logger.AssertLogContains("MSB4023"); } -#if FEATURE_LEGACY_GETFULLPATH +#if NETFRAMEWORK /// /// Bad path when getting metadata through ->AnyHaveMetadataValue function /// diff --git a/src/Directory.BeforeCommon.targets b/src/Directory.BeforeCommon.targets index 95e68ae8409..f2854850df2 100644 --- a/src/Directory.BeforeCommon.targets +++ b/src/Directory.BeforeCommon.targets @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -35,9 +35,9 @@ $(DefineConstants);FEATURE_HTTP_LISTENER $(DefineConstants);FEATURE_INSTALLED_MSBUILD - $(DefineConstants);FEATURE_LEGACY_GETCURRENTDIRECTORY + - $(DefineConstants);FEATURE_LEGACY_GETFULLPATH + $(DefineConstants);FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR $(DefineConstants);FEATURE_PERFORMANCE_COUNTERS $(DefineConstants);FEATURE_PIPE_SECURITY diff --git a/src/Framework.UnitTests/FileUtilities_Tests.cs b/src/Framework.UnitTests/FileUtilities_Tests.cs index 6e8965ade92..a5af2653f25 100644 --- a/src/Framework.UnitTests/FileUtilities_Tests.cs +++ b/src/Framework.UnitTests/FileUtilities_Tests.cs @@ -519,11 +519,7 @@ public void CannotNormalizePathWithNewLineAndSpace() { string filePath = "\r\n C:\\work\\sdk3\\artifacts\\tmp\\Debug\\SimpleNamesWi---6143883E\\NETFrameworkLibrary\\bin\\Debug\\net462\\NETFrameworkLibrary.dll\r\n "; -#if FEATURE_LEGACY_GETFULLPATH - Assert.Throws(() => FileUtilities.NormalizePath(filePath)); -#else Assert.NotEqual("C:\\work\\sdk3\\artifacts\\tmp\\Debug\\SimpleNamesWi---6143883E\\NETFrameworkLibrary\\bin\\Debug\\net462\\NETFrameworkLibrary.dll", FileUtilities.NormalizePath(filePath)); -#endif } [Fact] diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index cf23c4ac7e6..69faa19783e 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -682,66 +682,13 @@ internal static string NormalizePath(params string[] paths) private static string GetFullPath(string path) { -#if FEATURE_LEGACY_GETFULLPATH - if (NativeMethods.IsWindows) - { - string uncheckedFullPath = NativeMethods.GetFullPath(path); - - if (IsPathTooLong(uncheckedFullPath)) - { - throw new PathTooLongException(SR.FormatPathTooLong(path, NativeMethods.MaxPath)); - } - - // We really don't care about extensions here, but Path.HasExtension provides a great way to - // invoke the CLR's invalid path checks (these are independent of path length) - Path.HasExtension(uncheckedFullPath); - - // If we detect we are a UNC path then we need to use the regular get full path in order to do the correct checks for UNC formatting - // and security checks for strings like \\?\GlobalRoot - return IsUNCPath(uncheckedFullPath) ? Path.GetFullPath(uncheckedFullPath) : uncheckedFullPath; - } -#endif - +#if FEATURE_MSIOREDIST + return NewPath.GetFullPath(path); +#else return Path.GetFullPath(path); +#endif } -#if FEATURE_LEGACY_GETFULLPATH - private static bool IsUNCPath(string path) - { - if (!NativeMethods.IsWindows || !path.StartsWith(@"\\", StringComparison.Ordinal)) - { - return false; - } - bool isUNC = true; - for (int i = 2; i < path.Length - 1; i++) - { - if (path[i] == '\\') - { - isUNC = false; - break; - } - } - - /* - From Path.cs in the CLR - - Throw an ArgumentException for paths like \\, \\server, \\server\ - This check can only be properly done after normalizing, so - \\foo\.. will be properly rejected. Also, reject \\?\GLOBALROOT\ - (an internal kernel path) because it provides aliases for drives. - - throw new ArgumentException(Environment.GetResourceString("Arg_PathIllegalUNC")); - - // Check for \\?\Globalroot, an internal mechanism to the kernel - // that provides aliases for drives and other undocumented stuff. - // The kernel team won't even describe the full set of what - // is available here - we don't want managed apps mucking - // with this for security reasons. - */ - return isUNC || path.IndexOf(@"\\?\globalroot", StringComparison.OrdinalIgnoreCase) != -1; - } -#endif // FEATURE_LEGACY_GETFULLPATH - /// /// Normalizes all path separators (both forward and back slashes) to forward slashes. /// This is platform-independent, unlike FrameworkFileUtilities.FixFilePath which only normalizes on non-Windows platforms. diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index 00bc2eb380e..1f4c7d1367d 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -1,4 +1,4 @@ - + $(LibraryTargetFrameworks) true @@ -38,6 +38,7 @@ + diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 7195d8ee36f..a1eebb89f84 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; @@ -1485,23 +1485,18 @@ internal static List> GetChildProcessIds(in } /// - /// Internal, optimized GetCurrentDirectory implementation that simply delegates to the native method + /// Returns the current working directory. On .NET Framework, uses Microsoft.IO.Redist for performance. /// - /// - internal static unsafe string GetCurrentDirectory() + internal static string GetCurrentDirectory() { -#if FEATURE_LEGACY_GETCURRENTDIRECTORY - if (IsWindows) - { - int bufferSize = GetCurrentDirectoryWin32(0, null); - char* buffer = stackalloc char[bufferSize]; - int pathLength = GetCurrentDirectoryWin32(bufferSize, buffer); - return new string(buffer, startIndex: 0, length: pathLength); - } -#endif +#if FEATURE_MSIOREDIST + return Microsoft.IO.Directory.GetCurrentDirectory(); +#else return Directory.GetCurrentDirectory(); +#endif } +#if FEATURE_LEGACY_GETCURRENTDIRECTORY [SupportedOSPlatform("windows")] private static unsafe int GetCurrentDirectoryWin32(int nBufferLength, char* lpBuffer) { @@ -1509,7 +1504,9 @@ private static unsafe int GetCurrentDirectoryWin32(int nBufferLength, char* lpBu VerifyThrowWin32Result(pathLength); return pathLength; } +#endif +#if FEATURE_LEGACY_GETFULLPATH [SupportedOSPlatform("windows")] internal static unsafe string GetFullPath(string path) { @@ -1548,6 +1545,7 @@ private static unsafe bool AreStringsEqual(char* buffer, int len, string s) { return s.AsSpan().SequenceEqual(new ReadOnlySpan(buffer, len)); } +#endif internal static void VerifyThrowWin32Result(int result) { @@ -1694,11 +1692,27 @@ internal static void RestoreConsoleMode(uint? originalConsoleMode, StreamHandleT [DllImport("kernel32.dll")] internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); + /* + Legacy kernel32 P/Invokes when FEATURE_LEGACY_GETCURRENTDIRECTORY / FEATURE_LEGACY_GETFULLPATH are enabled + (see commented DefineConstants in Directory.BeforeCommon.targets). Kept for reference when disabled. + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [SupportedOSPlatform("windows")] internal static extern unsafe int GetCurrentDirectory(int nBufferLength, char* lpBuffer); + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [SupportedOSPlatform("windows")] + internal static extern unsafe int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero); + */ + +#if FEATURE_LEGACY_GETCURRENTDIRECTORY + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [SupportedOSPlatform("windows")] + internal static extern unsafe int GetCurrentDirectory(int nBufferLength, char* lpBuffer); +#endif + [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SetCurrentDirectory")] [return: MarshalAs(UnmanagedType.Bool)] @@ -1723,9 +1737,11 @@ internal static bool SetCurrentDirectory(string path) return true; } +#if FEATURE_LEGACY_GETFULLPATH [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [SupportedOSPlatform("windows")] internal static extern unsafe int GetFullPathName(string target, int bufferLength, char* buffer, IntPtr mustBeZero); +#endif [DllImport("KERNEL32.DLL")] [SupportedOSPlatform("windows")] From a24615cafcd8110416b5b800e0b77ae612de8f80 Mon Sep 17 00:00:00 2001 From: SACHIN SHARMA Date: Sun, 22 Mar 2026 14:50:10 -0700 Subject: [PATCH 2/5] Fix TlbExp error and restore proper exception throwing in GetFullPath --- src/Directory.Build.targets | 10 +++++----- src/Framework/FileUtilities.cs | 5 +++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 1f60b1bd2a5..3de2403fd4c 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -143,13 +143,13 @@ - + - + - + diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index 69faa19783e..42e10ac2055 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -683,6 +683,11 @@ internal static string NormalizePath(params string[] paths) private static string GetFullPath(string path) { #if FEATURE_MSIOREDIST + if (path.IndexOfAny(InvalidPathChars) != -1) + { + // Trigger the same exception as System.IO.Path.GetFullPath would + return Path.GetFullPath(path); + } return NewPath.GetFullPath(path); #else return Path.GetFullPath(path); From d081adbae34d3575902de88c07b9bb70c4e9354c Mon Sep 17 00:00:00 2001 From: SACHIN SHARMA Date: Sun, 22 Mar 2026 16:23:43 -0700 Subject: [PATCH 3/5] Fix path validation and TlbExp issues in Microsoft.IO.Redist PR --- src/Framework/FileUtilities.cs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index 42e10ac2055..c08d6fa2ea1 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -683,12 +683,40 @@ internal static string NormalizePath(params string[] paths) private static string GetFullPath(string path) { #if FEATURE_MSIOREDIST - if (path.IndexOfAny(InvalidPathChars) != -1) + try { - // Trigger the same exception as System.IO.Path.GetFullPath would return Path.GetFullPath(path); } - return NewPath.GetFullPath(path); + catch (PathTooLongException) + { + // Trigger the same exception for truly invalid characters even if path is long + if (path.Contains('|')) + { + throw new ArgumentException("Illegal characters in path."); + } + + return NewPath.GetFullPath(path); + } + catch (ArgumentException) + { + // Redist (Microsoft.IO.Redist) is more permissive (matching legacy Win32 GetFullPathName) + // about some characters like newlines at the edges that .NET Framework rejects. + // However, we must remain strict about characters like '|' or malformed UNC roots to satisfy MSBuild tests. + if (path.Contains('|')) + { + throw; + } + + string redistResult = NewPath.GetFullPath(path); + + // Re-validate UNC roots that Redist might accept but MSBuild tests expect to fail. + if (redistResult.StartsWith(@"\\", StringComparison.Ordinal) && (redistResult is @"\\" or @"\\\\" or @"\\localhost" or @"\\XXX\")) + { + throw; + } + + return redistResult; + } #else return Path.GetFullPath(path); #endif From 0587592a7ef53cbdbe07d318f3373da294fc310e Mon Sep 17 00:00:00 2001 From: sachinsharma Date: Sun, 22 Mar 2026 19:35:56 -0700 Subject: [PATCH 4/5] Preserve PathTooLongException for Copy/Move task tests When Path.GetFullPath throws PathTooLongException, rethrow instead of falling back to Microsoft.IO.Path.GetFullPath. The latter may not throw for the same path, causing Regress451057_ExitGracefullyIfPathNameIsTooLong tests to fail (they expect the task to return false, not succeed with a long path). Made-with: Cursor --- src/Framework/FileUtilities.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index c08d6fa2ea1..ff8564e7eb5 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -695,7 +695,9 @@ private static string GetFullPath(string path) throw new ArgumentException("Illegal characters in path."); } - return NewPath.GetFullPath(path); + // Re-throw to preserve behavior expected by Copy/Move tasks and Regress451057 tests. + // Microsoft.IO.Path.GetFullPath may not throw for the same path; we must not fall back. + throw; } catch (ArgumentException) { From ccc36bd5ee9ebef17039610872df251d22b16383 Mon Sep 17 00:00:00 2001 From: sachinsharma Date: Sun, 22 Mar 2026 22:07:29 -0700 Subject: [PATCH 5/5] Fix PathIsInvalid: treat leading/trailing whitespace and MAX_PATH+ paths as invalid - Paths with leading/trailing whitespace: Microsoft.IO.Path.GetFullPath may trim them, causing incorrect resolution. Treat as invalid (CanNotResolveHintPathWithSpace). - Paths >= 260 chars: GetFullPath throws PathTooLongException on legacy Windows. Treat as invalid so callers skip NormalizePath and handle gracefully (Regress314573_VeryLongPaths). Made-with: Cursor --- src/Framework/FileUtilities.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Framework/FileUtilities.cs b/src/Framework/FileUtilities.cs index ff8564e7eb5..93a27ebc9d1 100644 --- a/src/Framework/FileUtilities.cs +++ b/src/Framework/FileUtilities.cs @@ -1051,6 +1051,25 @@ internal static string NormalizePathForComparisonNoThrow(string path, string cur internal static bool PathIsInvalid(string path) { + if (path == null) + { + return true; + } + + // Leading or trailing whitespace can cause NormalizePath to produce wrong results + // (e.g. Microsoft.IO.Path.GetFullPath may trim), so treat as invalid. See issue #4593. + if (path != path.Trim()) + { + return true; + } + + // Paths that exceed MAX_PATH (260) will cause GetFullPath to throw PathTooLongException on + // legacy Windows. Treat as invalid so callers skip NormalizePath and handle gracefully (e.g. RAR Regress314573). + if (path.Length >= NativeMethods.MAX_PATH) + { + return true; + } + // Path.GetFileName does not react well to malformed filenames. // For example, Path.GetFileName("a/b/foo:bar") returns bar instead of foo:bar // It also throws exceptions on illegal path characters