6

I'm trying to make my Addin's (using MAF) AddInProcess.exe (created by the AddInProcess class) "long path aware" for Windows 10.

The difficulty stems from the fact that I don't own the AddInProcess.exe. It is part of the .NET Framework, and lives in C:\Windows\Microsoft.NET\Framework64\v4.0.30319\AddInProcess.exe

Here's what I've done (based on following the guidance of Jeremy Kuhne's Blog entry .NET 4.6.2 and long paths on Windows 10):

  • Set the AppContext switches Switch.System.IO.UseLegacyPathHandling and Switch.System.IO.BlockLongPaths to false. This made some .NET APIs start accepting long paths. However, calls that under the hood called into native win32 still failed with System.IO.DirectoryNotFoundException or other exceptions.

  • The next thing I'm supposed to do, but can't is edit the application's manifest and add the settings below. However, there isn't a (correct) way for me to do this. If I could do this it would work. To prove that it would work, I did the following:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
  </windowsSettings>
</application>

While the above hack works, it's obviously not reasonable to do this on an end-user's machine and moreover, it would change the behavior for all AddInProcess.exe, not just the ones I create.

Question: Is there a legitimate way to make a process you don't have control over (AddInProcess.exe) be longPathAware?

Is there a way to change this manifest setting at runtime?

Notes:

  • I enabled Win32 Long Path Support in the Group Policy editor (see .NET 4.6.2 and long paths on Windows 10)
  • I had some difficulty getting the AppContextSwitchOverrides for UseLegacyPathHandling and BlockLongPaths to actually be respected. I turned out to be this problem and I was able to workaround it.
  • I can get the methods that don't work to start working by using extended paths (i.e. paths prepended with \\?\ (since this causes path length checks to be skipped). See Path Normalization blog entry. However, this approach would be a much larger/riskier endeavor (trying to find all current code occurrences that would throw and modify the path and prevent future developers from adding new usages)

Additional References:

Matt Smith
  • 17,026
  • 7
  • 53
  • 103
  • 2
    begin from 1709 version you can set [`IsLongPathAwareProcess`](https://github.com/processhacker/processhacker/blob/master/phnt/include/ntpebteb.h#L75) to 1 in *PEB* if such solution ok for you - `RtlGetCurrentPeb()->IsLongPathAwareProcess = 1;` – RbMm Oct 29 '19 at 00:31
  • @RbMm I've tried out that approach, and my initial tests indicate it works! Thank you. The unfortunate thing about this approach is it is undocumented/unsupported. I'm hoping for a supported approach. But this is a good fallback, if there is nothing else. – Matt Smith Oct 30 '19 at 14:57
  • i sure that just now not exist documented way do this. way with `IsLongPathAwareProcess` can be changed. most reliable way - use *\\\\?\\* win32 paths or nt functions – RbMm Oct 30 '19 at 15:37
  • I am not sure if this could help or not as I have no experience in Addins, but it may worth to check. Please see [this link](https://learn.microsoft.com/en-us/dotnet/api/system.addin.hosting.addinprocess?view=netframework-4.8). I wonder if the created external process (AddInProcess.exe) can take configuration from your code instead of its default. – Farhad Rahmanifard Nov 02 '19 at 20:36

1 Answers1

0

The undocumented approach suggested by @RbMm in the comments (setting the PEB's IsLongPathAwareProcess bit to 1) worked (at least on the version of Windows 10 I was on: 1903). Since that approach is a native C/C++ approach, I just needed to translate that into something consumable by C#.

There are multiple approaches that can be used (C++/CLI, pinvoke). Since I was just doing "proof of concept", I decided to just use pinvoke and be a bit lazy in the quality of the code.

Notes:

  • This code does no version checking to see if it is running on a version of Windows 10 that supports IsLongPathAwareProcess.
  • Limited error checking
  • I only included the beginning part of the PEB structure, since that is all that is necessary to access the bit I want to modify.
  • There's very likely a more concise way to set the IsLongPathAwareProcess bit than the approach I used (Marshal.StructureToPtr)
  • Includes a test that works when the call to SetIsLongPathAwareProcess is executed and fails when it is not called
  • Notice in the test code that I delete C:\Test (so that I can re-run the test multiple times without the directory already existing)

Test Code:

    class Program
    {
        static void Main(string[] args)
        {
            SetIsLongPathAwareProcess();

            string reallyLongDirectory = @"C:\Test\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
            reallyLongDirectory = reallyLongDirectory + @"\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
            reallyLongDirectory = reallyLongDirectory + @"\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
            CreateDirTest(reallyLongDirectory);
            Directory.Delete(@"C:\Test", recursive: true);
        }

        private static void CreateDirTest(string name)
        {
            Console.WriteLine($"Creating a directory that is {name.Length} characters long");

            // Test managed CreateDirectory
            Directory.CreateDirectory(name);

            // Test Native CreateDirectory.  Note this method will only create the "last part" of the directory (not the full path).
            // Also note that it fails if the directory already exists.
            // https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createdirectorya
            string nativeName = Path.Combine(name, "TestEnding");
            var r = Native.CreateDirectory(nativeName, null);
            if (!r)
            {
                int currentError = Marshal.GetLastWin32Error();
                Debug.Fail($"Native.CreateDirectory failed: {currentError}");
            }
        }


        // Adapted from https://www.pinvoke.net/default.aspx/ntdll.ntqueryinformationprocess
        private static void SetIsLongPathAwareProcess()
        {
            var currentProcess = Process.GetCurrentProcess();
            IntPtr hProc = currentProcess.Handle;

            IntPtr pPbi = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Native.PROCESS_BASIC_INFORMATION)));
            IntPtr outLong = Marshal.AllocHGlobal(sizeof(long));

            int status = Native.NtQueryInformationProcess(hProc, 0, pPbi, (uint)Marshal.SizeOf(typeof(Native.PROCESS_BASIC_INFORMATION)), outLong);

            Marshal.FreeHGlobal(outLong);

            //STATUS_SUCCESS = 0
            if (status == 0)
            {
                var pbi = Marshal.PtrToStructure<Native.PROCESS_BASIC_INFORMATION>(pPbi);
                var pPeb = pbi.PebBaseAddress;

                var peb = Marshal.PtrToStructure<Native.PEB_Beginning>(pPeb);

                var bitField1 = peb.BitField1;
                peb.BitField1 = bitField1 | Native.PEBBitField1.IsLongPathAwareProcess;
                Marshal.StructureToPtr<Native.PEB_Beginning>(peb, pPeb, false);
            }

            //Free allocated space
            Marshal.FreeHGlobal(pPbi);
        }

        static class Native
        {
            // http://pinvoke.net/default.aspx/Structures/PROCESS_BASIC_INFORMATION.html
            [StructLayout(LayoutKind.Sequential, Pack = 1)]
            internal struct PROCESS_BASIC_INFORMATION
            {
                public IntPtr ExitStatus;
                public IntPtr PebBaseAddress;
                public IntPtr AffinityMask;
                public IntPtr BasePriority;
                public UIntPtr UniqueProcessId;
                public IntPtr InheritedFromUniqueProcessId;

                public int Size
                {
                    get { return (int)Marshal.SizeOf(typeof(PROCESS_BASIC_INFORMATION)); }
                }
            }

            [Flags]
            internal enum PEBBitField1 : byte
            {
                None = 0,
                ImageUsesLargePages = 1 << 0,
                IsProtectedProcess = 1 << 1,
                IsImageDynamicallyRelocated = 1 << 2,
                SkipPatchingUser32Forwarders = 1 << 3,
                IsPackagedProcess = 1 << 4,
                IsAppContainer = 1 << 5,
                IsProtectedProcessLight = 1 << 6,
                IsLongPathAwareProcess = 1 << 7
            }

            // Note: this only contains the "beginning" of the PEB structure
            // but that is all we need for access to the IsLongPathAwareProcess bit
            // Used as a guide: https://github.com/processhacker/processhacker/blob/master/phnt/include/ntpebteb.h#L75
            // Related: https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
            [StructLayout(LayoutKind.Sequential, Pack = 1)]
            internal struct PEB_Beginning
            {
                Byte InheritedAddressSpace;
                Byte ReadImageFileExecOptions;
                Byte BeingDebugged;
                public PEBBitField1 BitField1;
            };

            // https://www.pinvoke.net/default.aspx/ntdll.ntqueryinformationprocess
            [DllImport("ntdll.dll", SetLastError = true)]
            internal static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, IntPtr processInformation, uint processInformationLength, IntPtr returnLength);

            // The below is for test purposes only:
            // Include CreateDirectory for testing if the long path aware changes work or not.
            // Requires SECURITY_ATTRIBUTES
            // http://pinvoke.net/default.aspx/kernel32/CreateDirectory.html?diff=y
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)]
            internal static extern bool CreateDirectory(String path, SECURITY_ATTRIBUTES lpSecurityAttributes);


            // https://www.pinvoke.net/default.aspx/Structures/SECURITY_ATTRIBUTES.html
            [StructLayout(LayoutKind.Sequential)]
            internal class SECURITY_ATTRIBUTES
            {
                internal int nLength = 0;
                // don't remove null, or this field will disappear in bcl.small
                internal unsafe byte* pSecurityDescriptor = null;
                internal int bInheritHandle = 0;
            }
        }
    }

If you use this code in a Console App that is naturally longPathAware, you can still test that it works correctly, by manually setting longPathAware to false in the app.manifest:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">false</longPathAware>
  </windowsSettings>
</application>
Matt Smith
  • 17,026
  • 7
  • 53
  • 103