Skip to content

bpo-37412: Fix os.getcwd() for long path on Windows #14424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 28, 2019
Merged

bpo-37412: Fix os.getcwd() for long path on Windows #14424

merged 2 commits into from
Jun 28, 2019

Conversation

vstinner
Copy link
Member

@vstinner vstinner commented Jun 27, 2019

  • Fix test for integer overflow.
  • Add an unit test.

https://bugs.python.org/issue37412

@vstinner
Copy link
Member Author

Oh. I first used min_len = 1000 to avoid ENAMETOOLONG, but then I realized that min_len = 1000 is not enough to test the regression fixed by this change. So I modified my change to use min_len = 2000.

@vstinner
Copy link
Member Author

@eryksun: Would you mind to review my PR?

@vstinner
Copy link
Member Author

Oh, test_getcwd_long_path() failed on AppVeyor with:

FileNotFoundError: [WinError 206] The filename or extension is too long: 'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\tmpqlshuxlz\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

mkdir() fails on a path of 251 characters. On my Windows 10 VM, the test works as expected.

pythoninfo says:

platform.platform: Windows-10-10.0.14393-SP0
sys.windowsversion: sys.getwindowsversion(major=10, minor=0, build=14393, platform=2, service_pack='')

try:
os.mkdir(path)
except OSError as exc:
if exc.errno == errno.ENAMETOOLONG:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CRT in Windows doesn't use ENAMETOOLONG. In the Windows API, depending on the context, a path that's too long fails with either ERROR_PATH_NOT_FOUND (3) or ERROR_FILENAME_EXCED_RANGE (206). Both get mapped to the errno value ENOENT.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modified my PR to catch also FileNotFoundError. The test stops on such error: we reached the maximum path length.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CRT in Windows doesn't use ENAMETOOLONG.

"if exc.errno == errno.ENAMETOOLONG:" is for Linux ;-)

@eryksun
Copy link
Contributor

eryksun commented Jun 27, 2019

Oh, test_getcwd_long_path() failed on AppVeyor with:

FileNotFoundError: [WinError 206] The filename or extension is too long: 'C:\\Users\\appveyor\\AppData\\Local\\Temp\\1\\tmpqlshuxlz\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

mkdir() fails on a path of 251 characters. On my Windows 10 VM, the test works as expected.

The classic DOS path length limit is MAX_PATH (260) characters. In practice it's 259 characters when opening or creating a file, not counting the null terminator; or 258 characters when setting the current directory, not counting the trailing path separator and null terminator; or 246 or 247 characters when creating a directory, leaving space for a path separator, 12-character short filename (DOS 8.3), and the null terminator.

UNC paths (e.g. "\\localhost\C$\Temp") have the same length limit as DOS drive-letter paths. Normalized device paths that start with "\\.\" also have the same limit. Unless we're in Windows 10 with long paths enabled, only non-normalized "\\?\" device paths are extended to the native limit of 32,767 characters. Since "\\?\" paths aren't normalized, they can only use backslash as the path separator. Also, for Windows 7 and older versions, "\\?\" paths must be Unicode because the routine that decodes from ANSI to Unicode is limited to MAX_PATH in older versions of Windows.

That said, "\\?\" paths are not supported for the current directory. The only way the current directory can exceed MAX_PATH - 2 is to enable long-path support in Windows 10, which removes the MAX_PATH limit in most cases. This is configured by a registry DWORD value named "LongPathsEnabled" in the key "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem". The application manifest also has to enable the "longPathAware" setting. Python 3.6+ enables this in the python[w].exe embedded manifest.

There's no documented way to check whether long paths are enabled in the current process. Using public APIs, we can only check indirectly, e.g. by trying to create a long path in the temp directory. We can check this directly if we're willing to use the undocumented function RtlAreLongPathsEnabled. The following code is from an answer I wrote on Stack Overflow:

import ctypes
ntdll = ctypes.WinDLL('ntdll')

if hasattr(ntdll, 'RtlAreLongPathsEnabled'):

    ntdll.RtlAreLongPathsEnabled.restype = ctypes.c_ubyte
    ntdll.RtlAreLongPathsEnabled.argtypes = ()

    def are_long_paths_enabled():
        return bool(ntdll.RtlAreLongPathsEnabled())

else:

    def are_long_paths_enabled():
        return False

(FYI, a notable exception with long-path support is that CreateProcessW does not allow a long path as the current directory of a child process, neither implicitly inherited nor explicitly set. This causes subprocess.Popen to mysteriously fail with an invalid-parameter error if a long path is set as the current directory.)

@vstinner
Copy link
Member Author

We can check this directly if we're willing to use the undocumented function RtlAreLongPathsEnabled.

I'm not sure that it's worth it to attempt to get the exact maximum path. My design of my test is more "try to build a long path, if we cannot, stop the test". We have enough buildbots to hope that one has long paths enabled ;-)

@eryksun
Copy link
Contributor

eryksun commented Jun 27, 2019

I'm not sure that it's worth it to attempt to get the exact maximum path. My design of my test is more "try to build a long path, if we cannot, stop the test". We have enough buildbots to hope that one has long paths enabled ;-)

My comment was intended to provide a general background in order to understand why a Python 3.6+ process doesn't always support long paths in Windows 10 (depends on the system LongPathsEnabled policy at startup) and why, with long paths not enabled, the system failed to create a directory when the fully-qualified path was only 251 characters (the path length of a new directory is limited to 247 characters, unless long paths are enabled or it's a "\\?\" path).

@vstinner
Copy link
Member Author

@eryksun: So what do you think of my test, is it good enough to check for the regression?

I'm not sure about using "\?", "\localhost\C$\Temp" or "\.". They might be used to create a new sub-directory (mkdir), but I don't see how they could be used to get the current directory: os.getcwd() (GetCurrentDirectoryW) takes no argument. So I understand that it's fine to stop the test when we fail to build a subdirectory even if we didn't reach min_len (2000 characters).

We have enough buildbots to hope that one has long paths enabled ;-)

I wrote PR #14434 to check if it's the case :-)

@vstinner
Copy link
Member Author

Oh, the test failed on Windows jobs of Azure Pipelines:

ERROR: test_getcwd_long_path (test.test_os.MiscTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\a\1\s\lib\test\test_os.py", line 130, in test_getcwd_long_path
    os.chdir(path)
FileNotFoundError: [WinError 206] The filename or extension is too long: 'C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\tmp5tfbc984\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

chdir() failed on a path of 452 characters:

>>> len("'C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\tmp5tfbc984\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'")
452

whereas the previous iteration with 250 characters was fine:

>>> len("'C:\\Users\\VSSADM~1\\AppData\\Local\\Temp\\tmp5tfbc984\\python_test_dir_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
250

@vstinner
Copy link
Member Author

Oh, the test failed on Windows jobs of Azure Pipelines (...)
chdir() failed on a path of 452 characters: ...

According to PR #14434 (RtlAreLongPathsEnabled), long paths are enabled on this CI.

else:
raise

os.chdir(path)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the os.chdir call needs to be in the try block. Though it's weird that CreateDirectoryW succeeds but SetCurrentDirectoryW fails. If long paths are not enabled, then CreateDirectoryW is limited to 247 characters unless we use a "\\?\" path, and SetCurrentDirectoryW is limited to 258 characters regardless of the path type. Maybe it's a bug in Windows 10 1607 (build 14393). Long-path support was added in this version.

@vstinner
Copy link
Member Author

Ok, the test now pass on AppVeyor and Azure Pipelines (win32, win64).

The macOS job of Azure Pipelines failed, but it's unrelated: https://bugs.python.org/issue37245

* Fix test for integer overflow.
* Add an unit test.
@vstinner
Copy link
Member Author

I rebased my PR, squashed commits, I fixed a typo in test comment, and I elaborated the test comment.

def test_getcwd_long_path(self):
# bpo-37412: On Linux, PATH_MAX is usually around 4096 bytes. On
# Windows, MAX_PATH is defined as 260 characters, but the universal
# naming convention (UNC) allows longer paths. Internally, the os
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, UNC paths have the same length limit as DOS drive paths. "\\?\" paths remove the limit because they bypass normalization, but these are not UNC paths.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hum. I rephased the comment in a more generic way.

"On Windows, MAX_PATH is defined as 260 characters, but Windows supports longer path if longer paths support is enabled."

@vstinner vstinner merged commit ec3e20a into python:master Jun 28, 2019
@miss-islington
Copy link
Contributor

Thanks @vstinner for the PR 🌮🎉.. I'm working now to backport this PR to: 3.8.
🐍🍒⛏🤖

@vstinner vstinner deleted the getcwd_long_path branch June 28, 2019 16:02
@bedevere-bot
Copy link

GH-14451 is a backport of this pull request to the 3.8 branch.

miss-islington pushed a commit to miss-islington/cpython that referenced this pull request Jun 28, 2019
* Fix test for integer overflow.
* Add an unit test.
(cherry picked from commit ec3e20a)

Co-authored-by: Victor Stinner <[email protected]>
@vstinner
Copy link
Member Author

The test pass on all CI, I fixed the test comment.

I decided to not wait for @eryksun just to make sure that the fix is merged before the weekend, otherwise I would forget and there is an incoming 3.8beta2 release ;-)

vstinner added a commit that referenced this pull request Jun 28, 2019
)

* Fix test for integer overflow.
* Add an unit test.
(cherry picked from commit ec3e20a)

Co-authored-by: Victor Stinner <[email protected]>
lisroach pushed a commit to lisroach/cpython that referenced this pull request Sep 10, 2019
* Fix test for integer overflow.
* Add an unit test.
DinoV pushed a commit to DinoV/cpython that referenced this pull request Jan 14, 2020
* Fix test for integer overflow.
* Add an unit test.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants