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
Non-file file descriptors may be mishandled in ntpath #103924
A couple of bugs slipped by when the new accelerator functions were added.
If _Py_get_osfhandle_noraise() fails (e.g. a bad file descriptor), the result is INVALID_HANDLE_VALUE, but the thread's last error isn't set and is thus a random error code. It could be one of the errors that's handled by calling STAT(), but in this case there is no _path.wide value to check. It happens that CreateFileW() doesn't raise an OS exception that crashes the process when passed a null pointer for lpFileName, but that's not documented and shouldn't be relied on.
If GetFileType(hfile) isn't FILE_TYPE_DISK, we have to return a false result. Since we didn't open the file, we don't know whether or not it has a pending synchronous operation that's blocked indefinitely. For example, a pipe or character file could have a pending synchronous read that may never complete. In this case, GetFileInformationByHandleEx() would block.
Also, a pipe file shouldn't be identified as a regular file, whether the test is by file descriptor or by name. Thus when checking isfile(), GetFileType(hfile) has to be checked regardless of whether we opened the file. A regular file must be a FILE_TYPE_DISK file, and its FileAttributes must be non-zero (e.g. FILE_ATTRIBUTE_NORMAL must be set if no other attributes are set).
On a related note, os.Direntry.is_file() is also wrong for pipe files in case of os.scandir('//./pipe/') or os.scandir('//?/pipe/'). Unfortunately all we have are the file attributes in this case, which don't help us because the named-pipe filesystem properly returns FILE_ATTRIBUTE_NORMAL. However, it's unlikely that anyone would call os.scandir() on "//./pipe/" and care about the result of is_file().
From the issue description, it looks like the affected path is the Windows ntpath accelerator handling for file-descriptor inputs, especially where _Py_get_osfhandle_noraise() can return INVALID_HANDLE_VALUE without a reliable last-error value, and where non-disk handles should be rejected before calling file-information APIs.
I’ll first try to reproduce the behavior with invalid descriptors and non-file handles such as pipes/character devices, then check whether the specialized exists/isfile/isdir/islink paths can be fixed with a small targeted change.
From what I can see, the original non-file descriptor behavior appears mostly addressed now:
non-disk handles are rejected for isfile() / isdir() / islink()
invalid handles no longer appear to fall through to pathname fallback logic
pipes and character devices return exists=True but isfile=False, which matches the existing genericpath.exists(fd) style behavior
I tested:
invalid positive fd
negative fd
closed fd
anonymous pipe fd
NUL fd
regular file fd
socket fileno integer
Release build behavior looked correct.
One remaining issue I could still reproduce is narrower: on a Windows debug build, ntpath.exists(123456) triggers a CRT debug assertion inside _get_osfhandle() before CPython can return False.
That path goes through _Py_get_osfhandle_noraise() in Python/fileutils.c, so I’m hesitant to patch it only in ntpath without guidance because the helper is shared.
Would you prefer this remaining debug assertion case to be handled under this issue, or should it be treated as a separate/narrower bug?
We can't handle the debug assertion, since it's more important that we pass the FD to the CRT to validate than for us to try and do it ourselves (since we can't). The CRT is designed assuming you're writing an application using it, not a library/runtime, and so an assertion makes sense. We already suppress the fast-exit for both debug and release builds, and the test suite disables assertion popups.
It sounds like there's nothing more to fix here. Thanks for confirming.
Activity
[-]Non-file files may be mishandled in ntpath[/-][+]Non-file file descriptors may be mishandled in ntpath[/+]eryksun commentedon Apr 27, 2023
Also, a pipe file shouldn't be identified as a regular file, whether the test is by file descriptor or by name. Thus when checking
isfile(),GetFileType(hfile)has to be checked regardless of whether we opened the file. A regular file must be aFILE_TYPE_DISKfile, and itsFileAttributesmust be non-zero (e.g.FILE_ATTRIBUTE_NORMALmust be set if no other attributes are set).On a related note,
os.Direntry.is_file()is also wrong for pipe files in case ofos.scandir('//./pipe/')oros.scandir('//?/pipe/'). Unfortunately all we have are the file attributes in this case, which don't help us because the named-pipe filesystem properly returnsFILE_ATTRIBUTE_NORMAL. However, it's unlikely that anyone would callos.scandir()on "//./pipe/" and care about the result ofis_file().zainnadeem786 commentedon Jul 9, 2026
Hi @zooba,
I’d be interested in investigating this.
From the issue description, it looks like the affected path is the Windows ntpath accelerator handling for file-descriptor inputs, especially where
_Py_get_osfhandle_noraise()can returnINVALID_HANDLE_VALUEwithout a reliable last-error value, and where non-disk handles should be rejected before calling file-information APIs.I’ll first try to reproduce the behavior with invalid descriptors and non-file handles such as pipes/character devices, then check whether the specialized
exists/isfile/isdir/islinkpaths can be fixed with a small targeted change.I’ll report findings before opening a PR.
zainnadeem786 commentedon Jul 9, 2026
Hi @zooba,
I investigated this on current
upstream/main.From what I can see, the original non-file descriptor behavior appears mostly addressed now:
isfile()/isdir()/islink()exists=Truebutisfile=False, which matches the existinggenericpath.exists(fd)style behaviorI tested:
NULfdRelease build behavior looked correct.
One remaining issue I could still reproduce is narrower: on a Windows debug build,
ntpath.exists(123456)triggers a CRT debug assertion inside_get_osfhandle()before CPython can returnFalse.That path goes through
_Py_get_osfhandle_noraise()inPython/fileutils.c, so I’m hesitant to patch it only inntpathwithout guidance because the helper is shared.Would you prefer this remaining debug assertion case to be handled under this issue, or should it be treated as a separate/narrower bug?
zooba commentedon Jul 9, 2026
We can't handle the debug assertion, since it's more important that we pass the FD to the CRT to validate than for us to try and do it ourselves (since we can't). The CRT is designed assuming you're writing an application using it, not a library/runtime, and so an assertion makes sense. We already suppress the fast-exit for both debug and release builds, and the test suite disables assertion popups.
It sounds like there's nothing more to fix here. Thanks for confirming.
zainnadeem786 commentedon Jul 9, 2026
Thanks for the clarification, @zooba.
That makes sense. I’ll leave this as resolved and won’t pursue a patch for the debug CRT assertion case.
Thanks!