4

In a C++ console application on windows, i'm trying to break the MAX_PATH restriction for the SetCurrentDirectoryW function.

There are many similar questions already asked but none got a usable answer:

Doc Research

Apparently this might be possible by using application manifest files. The docs for SetCurrentDirectoryW state:

Tip Starting with Windows 10, version 1607, for the unicode version of this function (SetCurrentDirectoryW), you can opt-in to remove the MAX_PATH limitation. See the "Maximum Path Length Limitation" section of Naming Files, Paths, and Namespaces for details.

And from the general docs about Manifests:

Manifests are XML files that accompany and describe side-by-side assemblies or isolated applications. ... Application Manifests describe isolated applications. They are used to manage the names and versions of shared side-by-side assemblies that the application should bind to at run time. Application manifests are copied into the same folder as the application executable file or included as a resource in the application's executable file.

The docs about Assembly Manifests point out the difference to Application Manifests once more:

As a resource in a DLL, the assembly is available for the private use of the DLL. An assembly manifest cannot be included as a resource in an EXE. An EXE file may include an Application Manifests as a resource.

The docs about Application Manifests list the assembly and assemblyIdentity elements as required:

  • The assembly element requires exactly one attribute:

    • manifestVersion
      • The manifestVersion attribute must be set to 1.0.
  • The assemblyIdentity element requires the following attributes:

    • type
      • The value must be Win32 and all in lower case
    • name
      • Use the following format for the name: Organization.Division.Name. For example Microsoft.Windows.mysampleApp.
    • version
      • Specifies the application or assembly version. Use the four-part version format: mmmmm.nnnnn.ooooo.ppppp. Each of the parts separated by periods can be 0-65535 inclusive. For more information, see Assembly Versions.

All other elements and attributes seem to be optional.

Additional requirements for the assembly element are:

Its first subelement must be a noInherit or assemblyIdentity element. The assembly element must be in the namespace "urn:schemas-microsoft-com:asm.v1". Child elements of the assembly must also be in this namespace, by inheritance or by tagging.

Finally, there's the longPathAware element which is optional but which should hopefully allow SetCurrentDirectoryW to use long paths:

Enables long paths that exceed MAX_PATH in length. This element is supported in Windows 10, version 1607, and later. For more information, see this article.

The section in the docs shows this example xml manifest:

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
 ...
  <asmv3:application>
    <asmv3:windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
      <ws2:longPathAware>true</ws2:longPathAware>
    </asmv3:windowsSettings>
  </asmv3:application>
 ...
</assembly>

It doesn't seem to exactly follow the rules from the assembly element:

The assembly element must be in the namespace "urn:schemas-microsoft-com:asm.v1". Child elements of the assembly must also be in this namespace, by inheritance or by tagging.

Tests

The test environment is:

  • Windows 10 21H2 x64 19044.1586
  • VS2022 17.1.1
  • Windows SDK Version 10.0.20348.0

The test application is a new c++ console application where i made the following change to the Additional Manifest Files: Additional Manifest Files

The source code is very simple; it's also on godbolt but without the manifest:

#include <iostream>
#include <string>
#include <windows.h> 

int main() {
    std::wstring const path = LR"(H:\test\longPaths\manySmallLongPaths\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\12345678\)";
    std::wstring const path2 = LR"(\\?\)" + path;
    if (!SetCurrentDirectoryW(path.c_str())) {
        printf("Exe SetCurrentDirectory failed 1 - (%d)\n", GetLastError());
        if (!SetCurrentDirectoryW(path2.c_str()))
            printf("Exe SetCurrentDirectory failed 2 - (%d)\n", GetLastError());
    }
}

Trying to put this all together, i think the following file might be a valid Application Manifest:

<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns:asmv1='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
    <asmv1:assemblyIdentity type='win32' name='my.test.app' version='1.0.0.0' />
    <asmv1:application>
        <asmv1:windowsSettings>
            <asmv1:longPathAware>true</asmv1:longPathAware>
        </asmv1:windowsSettings>
    </asmv1:application>
</assembly>

But compiling and starting the application results in the following error:

The application has failed to start because its side-by-side configuration is incorrect. Please see the application event log or use the command-line sxstrace.exe tool for more detail.

Using sxstrace.exe reveals:

INFO: Parsing Manifest File C:\test\longPaths.exe.
        INFO: Manifest Definition Identity is my.test.app,type="win32",version="1.0.0.0".
        ERROR: Line 2: The element ws1:longPathAware appears as a child of element urn:schemas-microsoft-com:asm.v1^windowsSettings which is not supported by this version of Windows.
ERROR: Activation Context generation failed.

Maybe

Child elements of the assembly must also be in this namespace

is not entirely true (anymore) or i interpreted this wrong. Trying with a completed example from the longPathAware element:

<assembly xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
    <asmv1:assemblyIdentity type='win32' name='my.test.app' version='1.0.0.0' />
    <asmv3:application>
        <asmv3:windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
            <ws2:longPathAware>true</ws2:longPathAware>
        </asmv3:windowsSettings>
    </asmv3:application>
</assembly>

runs the application successfully, but without long path awareness (windows error code 206 = ERROR_FILENAME_EXCED_RANGE):

Exe SetCurrentDirectory failed 1 - 206
Exe SetCurrentDirectory failed 2 - 206

I checked the final embedded resource but it's definitively there: enter image description here

To finish

i can only say that i don't know what else to test or if adding longPathAware element to the manifest is even possible with the type of application i'm trying to achieve that.

Maybe there's another api to change the current working folder of my application to a long path, and i would be fine with it, but at least _chdir and std::filesystem::current_path have the same limitations.

Workarounds

Using short names aka. 8.3 aliases might provide a limited work around.

For my cases this is often not feasible because short paths don't need to exists; they can be controlled system wide or per volume:

  • The general state can be queried with fsutil 8dot3name query
  • The per volume setting can be queried with fsutil behavior query disable8dot3 c:

Sidenotes

  • The manifest can be embedded in the executable or a dll.
    • It will be ignored when the dll containing it is delay loaded.
    • It will not be ignored by the delay loaded dll when the executable is containing it.
  • Because the Manifest is "additional" in the project settings, it doesn't need the assemblyIdentity element.
ridilculous
  • 624
  • 3
  • 16
  • In the past when I needed long paths I used UNC paths `"\\?\C:\...` but have not spent a long time investigating without or the OS changes in Win10. I expect I did this back in Windows XP. – drescherjm Mar 24 '22 at 13:06
  • Have you set the registry policy to enable long paths? – Anders Mar 24 '22 at 13:09
  • @drescherjm they work for many api's very well but not for SetCurrentDirectory :( – ridilculous Mar 24 '22 at 13:09
  • @Anders no, i haven't. I was under the impression that with the manifest i can opt in per application and can avoid changing the whole system. Is the setting in the manifest bound to the registry setting? – ridilculous Mar 24 '22 at 13:10

1 Answers1

3

The manifest applies to your application, it allows you to opt in to long path support.

However, long path support must also be enabled system wide. This is the group policy "Computer Configuration > Administrative Templates > System > Filesystem > Enable Win32 long paths".

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem]
"LongPathsEnabled"=dword:00000001

This design makes no sense but it is what it is. You can't argue that it is in the name of compatibility because one could create long paths with \\?\ since at least Windows 2000.

Anders
  • 97,548
  • 12
  • 110
  • 164
  • Ah, i see, it's described [here](https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation). And i agree, it doesn't seem to make sense. Unfortunately i cannot force customers to set that key. I'm using the magic prefixes \\?\ and \\?\UNC\ for other api's successfully but they don't seem to work for SetCurrentDirectory and this functionality is very important for a application i'm working on. – ridilculous Mar 24 '22 at 13:27
  • I just tested and SetCurrentDirectory really seems to accept long paths only with the combination of registry key and manifest element; the magic prefixes never change anything for it. This is the point which is bugging me because for all other applications, there's at least one api which can be used with the magic prefixes which does the job, even when sometimes this means that i have to convert strings to wchar; the current directory seems to be the exception and i don't understand why except that they didn't want to touch its process global memory layout for some reason. – ridilculous Mar 24 '22 at 14:00
  • 1
    My guess as to why SetCurrentDirectory does not accept '\\?\' is that in the old world, something in the PEB had a fixed-size buffer or something along those lines (maybe related to A to W conversion of relative paths?) while other path APIs just passed the string to the kernel. – Anders Mar 24 '22 at 14:14
  • That's what i've thought as well but since it's almost certain now that there's no way around the registry i searched and it looks like it's dynamic: [RTL_USER_PROCESS_PARAMETERS](https://www.nirsoft.net/kernel_struct/vista/RTL_USER_PROCESS_PARAMETERS.html). Found that ref in [this nice blog post](https://ofekshilon.com/2017/07/01/tracking-the-current-directory-from-the-debugger-rtlpcurdirref/). – ridilculous Mar 24 '22 at 14:15
  • 1
    Yes but I suspect it was tied to the A functions use of WCHAR StaticUnicodeBuffer[261] in the TEB. This is just speculation on my part though but clearly there was a fixed buffer somewhere in the current directory handling that prevented this 25 years ago. – Anders Mar 24 '22 at 14:19