You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wrong paths from nt._getfinalpathname with DOS device selectors in the CWD #112563
In Windows if the CWD has a device selector \\.\ or \\?\ any function dependent on nt._getfullpathname may change the meaning of the path.
importosimportntpathos.chdir('\\\\?\\C:\\') # any path with a device selectorprint(ntpath.abspath('\\')) # NOTE: single slash character
That code will print \\
and I mean 2 slashes, not an escaped one.
So if you use:
print(ntpath.abspath('\\Some\\Path')) # single slash before each component
The result is \\Some\Path.
That is an UNC network share, not a file named Path a couple of levels down from the root of the volume.
Perhaps there are security concerns there.
And if the CWD is a volume root, or drive device root, then:
os.chdir('\\\\.\\C:\\') # or \\?\Volume{GUID}\print(ntpath.abspath('.')) # current directory
That will show \\.\C:
which is a device block, which means os.scandir would raise an error with that path.
Looking at the definition of _getfullpathname it doesn't look like python is doing much outside of calling GetFullPathNameW so I'm wondering if this may be an issue with the Windows api's handling of the prefixes being used here. Something to note is that in the Windows api namespace documentation under the win32 file namespace section it states that some apis do not support using "\\?\" and under the win32 device namespace section it states that most apis will not support using "\\.\". However, looking at the GetFullPathNameW documentation it looks as though it does at least support using "\\?\". I wonder if the problem may be coming from changing the current directory, as from looking at the SetCurrentDirectory documentation it does not appear to support "\\.\" or "\\?\" (at least from my read of it) and that may be causing the unexpected behavior we are seeing here.
It does look as though os.getcwd() still returns the correct path to the current directory so one potential solution for this specific case (now that #113829 has been merged) could be to call ntpath._abspath_fallback() (as this is just calling join(getcwd(),path)) instead of _getfullpathname as this will return the correct results for these cases. i.e.
However, just blanketly always running ntpath._abspath_fallback() does seem to change some additional functionality. If I change abspath to always run _abspath_fallback instead of trying _getfullpathname first then I see the following tests fail:
======================================================================
ERROR: test_abspath (test.test_ntpath.TestNtpath.test_abspath)
----------------------------------------------------------------------
Traceback (most recent call last):
File "cpython\Lib\test\test_ntpath.py", line 800, in test_abspath
tester('ntpath.abspath("\\\\?\\C:////spam////eggs. . .")', "\\\\?\\C:\\spam\\eggs")
~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "cpython\Lib\test\test_ntpath.py", line 60, in tester
raise TestFailed("%s should return: %s but returned: %s" \
%(str(fn), str(wantResult), str(gotResult)))
test.support.TestFailed: ntpath.abspath("\\\\?\\C:////spam////eggs. . .") should return: \\?\C:\spam\eggs but returned: \\?\C:\spam\eggs. . .
Lastly, _abspath_fallback seems to be slower than abspath. To test the difference for a few different path lengths I ran the following:
import timeit
paths = ["foo" + (x>0)*10**(x)*"/o" for x in range(5)]
results = []
for path in paths:
a = timeit.timeit(f"import ntpath;ntpath.abspath('{path}')", number=10000)
b = timeit.timeit(f"import ntpath;ntpath._abspath_fallback('{path}')", number=10000)
results.append((a,b,b/a))
I get these results:
subdirs
0
10
100
1000
10000
path length
3
23
203
2003
20003
abspath
0.051
0.055
0.086
0.377
3.158
fallback
0.205
0.217
0.251
0.523
3.076
multiples slower (fallback/abspath)
3.980
3.910
2.922
1.387
0.973
It does look as though _abspath_fallback is slower with smaller path lengths/fewer subdirectories but faster with a longer path length/more subdirectories. Though it looks as though the crossover point is slightly less than a path length of 20000 which is much larger than the default MAX_PATH limit of the windows api but could potentially be relevant if using the expanded MAX_PATH limit of 32,767 characters. Even in that case though it's pretty close to the performance of the current abspath implementation.
It does look as though os.getcwd() still returns the correct path to the current directory
That is correct, but I should point out that other os module functions behave like GetFullPathNameW.
importosos.chdir('C:\\')
os.stat('.') # worksos.stat('\\') # worksos.chdir('\\\\?\\C:\\')
os.stat('.') # worksos.stat('\\') # fails; cannot determine the correct root with a device selector in the CWD
The thing with the UNC \\?\ and \\.\ schemes is that only the selector itself is immune to . and .. components.
Used to be that you could not use file or device selectors in the CWD, and still can't on the Command Prompt and other apps, but PowerShell and Python do allow it, which is nice for unmounted volumes.
Something to note is that in the Windows api namespace documentation under the win32 file namespace section it states that some apis do not support using "\\?\"
I wonder if the problem may be coming from changing the current directory, as from looking at the SetCurrentDirectory documentation it does not appear to support "\\.\" or "\\?\" (at least from my read of it) and that may be causing the unexpected behavior we are seeing here.
Since CPython does support the file and device selectors in the CWD, it seems that in Windows built-in functions like os.stat
the single-leading-slash (root-relative) paths are just kinda broken.
importosos.chdir(os.listvolumes()[0]) # first volume with GUIDos.scandir('\\Some\\Path') # fails; correct root not found, but tries to connect to a network share
For argument's sake, if you were to deal with it at the C level, it seems to me that something would need to be sacrificed, like unmounted volumes. You could just use GetFinalPathNameByHandle to format as DOS path when changing the CWD by removing/replacing the selector. Of course, that would bring another set of problems, but DOS path arguments would behave as documented, whereas the Win32 path documentation does have caveats. That would deal with (ugh) the root of the problem.
That would also be more consistent with POSIX; because, I think, it resolves the symlinks when setting the CWD.
...And there's more:
importosimportntpathos.chdir('C:\\')
ntpath.join(ntpath.abspath('.'), '..') # "C:\.." <-- this is OKos.chdir('\\\\?\\C:\\')
ntpath.join(ntpath.abspath('.'), '..') # "\\?\C:.." <-- this is very wrongos.chdir('\\\\?\\C:\\Path')
ntpath.join(ntpath.abspath('.'), 'BAD_FILENAME.') # "\\?\C:\Path\BAD_FILENAME." <-- this is hell on Earth if you use this path to create a file, because the dot will not be trimmed and Windows Explorer is going to show the file to you but then when you try to open or delete the file you'll get an error saying that it ain't know wacha talking 'bout because there's no file there even though you're seeing the file with your own two eyes... so...# relative segments must not indirectly inherit# a file selector, which prevents normalization
Since CPython does support the file and device selectors in the CWD
This isn't strictly true - CPython doesn't care about your CWD. It just passes the string through to the operating system.
It seems that the only sensible thing we can do here is to raise in chdir if a path with a prefix is passed. It would be preferable if the operating system would reject the path itself, which we would convert into an error, since the places where the path fails are due to the OS not handling the CWD from its own internal uses (we're not passing it in).
I don't particularly want to add new errors like this, as I think you should be able to set the CWD to weird values if you need to. But we don't have to guarantee that the OS can handle it.
It's worth noting that os.path.join(os.getcwd(), PATH) is a suitable alternative to os.path.abspath(PATH) if you need to handle this case, and you can use it anywhere you expect to pass a special path to an API that knows how to use it.
If we had an easy way to detect that the fast abspath implementation had joined incorrectly, we could automatically fall back to the slow join, but considering you've already put the OS into an unsupported state, I don't think we have to be responsible for making everything else work around that.
I'm inclined to close this issue, as I don't think there's anything we can do. If someone comes up with a good proposal to document the oddity I'd consider it, but we don't want to make Python's behaviour dependant on something this low level.
This isn't strictly true - CPython doesn't care about your CWD. It just passes the string through to the operating system.
I think I get what you mean, please forgive if the term "support" was incorrect, but this is how I look at it: the currently supported Windows platforms and the Win32 APIs used do seem to all accept such paths, and CPython already has os.listvolumes which specifically uses the file selector, and os.readlink may also use it for absolute paths, and perhaps more importantly, os.scandir accepts and returns them, which is great.
It seems that the only sensible thing we can do here is to raise in chdir if a path with a prefix is passed.
Please keep the ability to use \\?\ in current working directories, or at least, try to resolve the DOS path before raising errors.
We don't offer this ability - Windows does. You'll need to ask Microsoft to support it. All we offer is access to the OS APIs, and the OS APIs are the ones that are failing to handle the path correctly.
Just curious. Would that affect os.getcwd anyway?
Yes, 100%. getcwd simply calls GetCurrentDirectoryW, and chdir simply calls SetCurrentDirectoryW. CPython doesn't do any handling of it whatsoever, it just passes straight through to the OS.
Python already has os.listvolumes which specifically uses the file selector, and os.readlink may also use it for absolute paths, and perhaps more importantly, os.scandir accepts and returns them, which is great.
As I mentioned, any OS API that supports the path will work with the path, and our join() function works with them. The API that seems to not work correctly is SetCurrentDirectoryW, which considering that its documentation does not say it supports them unlike the other APIs that do say it, would seem to be by (Microsoft's) design.1 There seems to be other APIs that handle device paths when provided as absolute paths, but not when inferred from the current working directory, which also suggests that SetCurrentDirectoryW is at fault.
For your chdir(listvolumes()[0]) example, you ought to be using listdrives (or possibly listmounts(listvolumes()[0])[0]). Clarifying the documentation on this point would be fine if that's helpful (e.g. "a volume identifier is not necessarily a file system path and may not work in all the places a regular path would - consider using listdrives() instead").
Footnotes
More likely it's just impossible to change for compatibility reasons. The "current directory" concept has been known to be a bad idea for decades, and the fact that it's still around just shows how hard it is to change existing code. ↩
Activity
finnagin commentedon Jan 17, 2024
Looking at the definition of
_getfullpathnameit doesn't look like python is doing much outside of callingGetFullPathNameWso I'm wondering if this may be an issue with the Windows api's handling of the prefixes being used here. Something to note is that in the Windows api namespace documentation under the win32 file namespace section it states that some apis do not support using"\\?\"and under the win32 device namespace section it states that most apis will not support using"\\.\". However, looking at the GetFullPathNameW documentation it looks as though it does at least support using"\\?\". I wonder if the problem may be coming from changing the current directory, as from looking at the SetCurrentDirectory documentation it does not appear to support"\\.\"or"\\?\"(at least from my read of it) and that may be causing the unexpected behavior we are seeing here.It does look as though
os.getcwd()still returns the correct path to the current directory so one potential solution for this specific case (now that #113829 has been merged) could be to callntpath._abspath_fallback()(as this is just callingjoin(getcwd(),path)) instead of_getfullpathnameas this will return the correct results for these cases. i.e.However, just blanketly always running
ntpath._abspath_fallback()does seem to change some additional functionality. If I changeabspathto always run_abspath_fallbackinstead of trying_getfullpathnamefirst then I see the following tests fail:Lastly,
_abspath_fallbackseems to be slower thanabspath. To test the difference for a few different path lengths I ran the following:I get these results:
It does look as though
_abspath_fallbackis slower with smaller path lengths/fewer subdirectories but faster with a longer path length/more subdirectories. Though it looks as though the crossover point is slightly less than a path length of 20000 which is much larger than the default MAX_PATH limit of the windows api but could potentially be relevant if using the expanded MAX_PATH limit of 32,767 characters. Even in that case though it's pretty close to the performance of the current abspath implementation.CarlitoGil commentedon Jan 20, 2024
Hi, @finnagin
That is correct, but I should point out that other
osmodule functions behave likeGetFullPathNameW.The thing with the UNC
\\?\and\\.\schemes is that only the selector itself is immune to.and..components.Used to be that you could not use file or device selectors in the CWD, and still can't on the Command Prompt and other apps, but PowerShell and Python do allow it, which is nice for unmounted volumes.
Since CPython does support the file and device selectors in the CWD, it seems that in Windows built-in functions like
os.statthe single-leading-slash (root-relative) paths are just kinda broken.
For argument's sake, if you were to deal with it at the
Clevel, it seems to me that something would need to be sacrificed, like unmounted volumes. You could just useGetFinalPathNameByHandleto format asDOSpath when changing the CWD by removing/replacing the selector. Of course, that would bring another set of problems, butDOSpath arguments would behave as documented, whereas theWin32path documentation does have caveats. That would deal with (ugh) the root of the problem.That would also be more consistent with POSIX; because, I think, it resolves the symlinks when setting the CWD.
...And there's more:
zooba commentedon Jan 24, 2024
This isn't strictly true - CPython doesn't care about your CWD. It just passes the string through to the operating system.
It seems that the only sensible thing we can do here is to raise in
chdirif a path with a prefix is passed. It would be preferable if the operating system would reject the path itself, which we would convert into an error, since the places where the path fails are due to the OS not handling the CWD from its own internal uses (we're not passing it in).I don't particularly want to add new errors like this, as I think you should be able to set the CWD to weird values if you need to. But we don't have to guarantee that the OS can handle it.
It's worth noting that
os.path.join(os.getcwd(), PATH)is a suitable alternative toos.path.abspath(PATH)if you need to handle this case, and you can use it anywhere you expect to pass a special path to an API that knows how to use it.If we had an easy way to detect that the fast
abspathimplementation had joined incorrectly, we could automatically fall back to the slowjoin, but considering you've already put the OS into an unsupported state, I don't think we have to be responsible for making everything else work around that.I'm inclined to close this issue, as I don't think there's anything we can do. If someone comes up with a good proposal to document the oddity I'd consider it, but we don't want to make Python's behaviour dependant on something this low level.
CarlitoGil commentedon Jan 25, 2024
Hi, @zooba
I think I get what you mean, please forgive if the term "support" was incorrect, but this is how I look at it: the currently supported Windows platforms and the Win32 APIs used do seem to all accept such paths, and CPython already has
os.listvolumeswhich specifically uses the file selector, andos.readlinkmay also use it for absolute paths, and perhaps more importantly,os.scandiraccepts and returns them, which is great.From the SetCurrentDirectory documentation:
Just curious. Would that affect
os.getcwdanyway?Please keep the ability to use
\\?\in current working directories, or at least, try to resolve theDOSpath before raising errors.zooba commentedon Jan 25, 2024
We don't offer this ability - Windows does. You'll need to ask Microsoft to support it. All we offer is access to the OS APIs, and the OS APIs are the ones that are failing to handle the path correctly.
Yes, 100%.
getcwdsimply callsGetCurrentDirectoryW, andchdirsimply callsSetCurrentDirectoryW. CPython doesn't do any handling of it whatsoever, it just passes straight through to the OS.As I mentioned, any OS API that supports the path will work with the path, and our
join()function works with them. The API that seems to not work correctly isSetCurrentDirectoryW, which considering that its documentation does not say it supports them unlike the other APIs that do say it, would seem to be by (Microsoft's) design.1 There seems to be other APIs that handle device paths when provided as absolute paths, but not when inferred from the current working directory, which also suggests thatSetCurrentDirectoryWis at fault.For your
chdir(listvolumes()[0])example, you ought to be usinglistdrives(or possiblylistmounts(listvolumes()[0])[0]). Clarifying the documentation on this point would be fine if that's helpful (e.g. "a volume identifier is not necessarily a file system path and may not work in all the places a regular path would - consider using listdrives() instead").Footnotes
More likely it's just impossible to change for compatibility reasons. The "current directory" concept has been known to be a bad idea for decades, and the fact that it's still around just shows how hard it is to change existing code. ↩