https://github.com/python/cpython/commit/52c8efa87d028e57895d6a44f22caeb99a589711
commit: 52c8efa87d028e57895d6a44f22caeb99a589711
branch: main
author: Victor Stinner <[email protected]>
committer: vstinner <[email protected]>
date: 2026-03-03T13:57:08+01:00
summary:

gh-145335: Fix os functions when passing fd -1 as path (#145439)

os.listdir(-1) and os.scandir(-1) now fail with OSError(errno.EBADF)
rather than listing the current directory.

os.listxattr(-1) now fails with OSError(errno.EBADF) rather than
listing extended attributes of the current directory.

files:
A Misc/NEWS.d/next/Library/2026-03-02-20-08-09.gh-issue-145335.lVTBvd.rst
M Doc/library/os.rst
M Lib/test/test_os/test_os.py
M Modules/posixmodule.c

diff --git a/Doc/library/os.rst b/Doc/library/os.rst
index 7418f3a8bacb0f..a22afdec516bb4 100644
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -2409,6 +2409,10 @@ features:
    .. versionchanged:: 3.6
       Accepts a :term:`path-like object`.
 
+   .. versionchanged:: next
+      ``os.listdir(-1)`` now fails with ``OSError(errno.EBADF)`` rather than
+      listing the current directory.
+
 
 .. function:: listdrives()
 
@@ -2939,6 +2943,10 @@ features:
    .. versionchanged:: 3.7
       Added support for :ref:`file descriptors <path_fd>` on Unix.
 
+   .. versionchanged:: next
+      ``os.scandir(-1)`` now fails with ``OSError(errno.EBADF)`` rather than
+      listing the current directory.
+
 
 .. class:: DirEntry
 
@@ -4574,6 +4582,10 @@ These functions are all available on Linux only.
    .. versionchanged:: 3.6
       Accepts a :term:`path-like object`.
 
+   .. versionchanged:: next
+      ``os.listxattr(-1)`` now fails with ``OSError(errno.EBADF)`` rather than
+      listing extended attributes of the current directory.
+
 
 .. function:: removexattr(path, attribute, *, follow_symlinks=True)
 
diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py
index 1f241609da80cd..3cab8ff9536d23 100644
--- a/Lib/test/test_os/test_os.py
+++ b/Lib/test/test_os/test_os.py
@@ -2784,10 +2784,61 @@ def test_fpathconf_bad_fd(self):
         'musl fpathconf ignores the file descriptor and returns a constant',
         )
     def test_pathconf_negative_fd_uses_fd_semantics(self):
+        if os.pathconf not in os.supports_fd:
+            self.skipTest('needs fpathconf()')
+
         with self.assertRaises(OSError) as ctx:
             os.pathconf(-1, 1)
         self.assertEqual(ctx.exception.errno, errno.EBADF)
 
+    @support.subTests("fd", [-1, -5])
+    def test_negative_fd_ebadf(self, fd):
+        tests = [(os.stat, fd)]
+        if hasattr(os, "statx"):
+            tests.append((os.statx, fd, 0))
+        if os.chdir in os.supports_fd:
+            tests.append((os.chdir, fd))
+        if os.chmod in os.supports_fd:
+            tests.append((os.chmod, fd, 0o777))
+        if hasattr(os, "chown") and os.chown in os.supports_fd:
+            tests.append((os.chown, fd, 0, 0))
+        if os.listdir in os.supports_fd:
+            tests.append((os.listdir, fd))
+        if os.utime in os.supports_fd:
+            tests.append((os.utime, fd, (0, 0)))
+        if hasattr(os, "truncate") and os.truncate in os.supports_fd:
+            tests.append((os.truncate, fd, 0))
+        if hasattr(os, 'statvfs') and os.statvfs in os.supports_fd:
+            tests.append((os.statvfs, fd))
+        if hasattr(os, "setxattr"):
+            tests.append((os.getxattr, fd, b"user.test"))
+            tests.append((os.setxattr, fd, b"user.test", b"1"))
+            tests.append((os.removexattr, fd, b"user.test"))
+            tests.append((os.listxattr, fd))
+        if os.scandir in os.supports_fd:
+            tests.append((os.scandir, fd))
+
+        for func, *args in tests:
+            with self.subTest(func=func, args=args):
+                with self.assertRaises(OSError) as ctx:
+                    func(*args)
+                self.assertEqual(ctx.exception.errno, errno.EBADF)
+
+        if hasattr(os, "execve") and os.execve in os.supports_fd:
+            # glibc fails with EINVAL, musl fails with EBADF
+            with self.assertRaises(OSError) as ctx:
+                os.execve(fd, [sys.executable, "-c", "pass"], os.environ)
+            self.assertIn(ctx.exception.errno, (errno.EBADF, errno.EINVAL))
+
+        if support.MS_WINDOWS:
+            import nt
+            self.assertFalse(nt._path_exists(fd))
+            self.assertFalse(nt._path_lexists(fd))
+            self.assertFalse(nt._path_isdir(fd))
+            self.assertFalse(nt._path_isfile(fd))
+            self.assertFalse(nt._path_islink(fd))
+            self.assertFalse(nt._path_isjunction(fd))
+
     @unittest.skipUnless(hasattr(os, 'ftruncate'), 'test needs os.ftruncate()')
     def test_ftruncate(self):
         self.check(os.truncate, 0)
diff --git 
a/Misc/NEWS.d/next/Library/2026-03-02-20-08-09.gh-issue-145335.lVTBvd.rst 
b/Misc/NEWS.d/next/Library/2026-03-02-20-08-09.gh-issue-145335.lVTBvd.rst
new file mode 100644
index 00000000000000..53033b06c2fae0
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-02-20-08-09.gh-issue-145335.lVTBvd.rst
@@ -0,0 +1,5 @@
+``os.listdir(-1)`` and ``os.scandir(-1)`` now fail with
+``OSError(errno.EBADF)`` rather than listing the current directory.
+``os.listxattr(-1)`` now fails with ``OSError(errno.EBADF)`` rather than
+listing extended attributes of the current directory. Patch by Victor
+Stinner.
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index b82f08e7dc4291..aa3d682a19bc9c 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -1638,10 +1638,10 @@ dir_fd_and_fd_invalid(const char *function_name, int 
dir_fd, int fd)
 }
 
 static int
-fd_and_follow_symlinks_invalid(const char *function_name, int fd,
+fd_and_follow_symlinks_invalid(const char *function_name, int is_fd,
                                int follow_symlinks)
 {
-    if ((fd >= 0) && (!follow_symlinks)) {
+    if (is_fd && (!follow_symlinks)) {
         PyErr_Format(PyExc_ValueError,
                      "%s: cannot use fd and follow_symlinks together",
                      function_name);
@@ -2880,12 +2880,13 @@ posix_do_stat(PyObject *module, const char 
*function_name, path_t *path,
 
     if (path_and_dir_fd_invalid("stat", path, dir_fd) ||
         dir_fd_and_fd_invalid("stat", dir_fd, path->fd) ||
-        fd_and_follow_symlinks_invalid("stat", path->fd, follow_symlinks))
+        fd_and_follow_symlinks_invalid("stat", path->is_fd, follow_symlinks))
         return NULL;
 
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1)
+    if (path->is_fd) {
         result = FSTAT(path->fd, &st);
+    }
 #ifdef MS_WINDOWS
     else if (follow_symlinks)
         result = win32_stat(path->wide, &st);
@@ -3647,7 +3648,7 @@ os_statx_impl(PyObject *module, path_t *path, unsigned 
int mask, int flags,
 {
     if (path_and_dir_fd_invalid("statx", path, dir_fd) ||
         dir_fd_and_fd_invalid("statx", dir_fd, path->fd) ||
-        fd_and_follow_symlinks_invalid("statx", path->fd, follow_symlinks)) {
+        fd_and_follow_symlinks_invalid("statx", path->is_fd, follow_symlinks)) 
{
         return NULL;
     }
 
@@ -3677,7 +3678,7 @@ os_statx_impl(PyObject *module, path_t *path, unsigned 
int mask, int flags,
 
     int result;
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         result = statx(path->fd, "", flags | AT_EMPTY_PATH, mask, &v->stx);
     }
     else {
@@ -3934,7 +3935,7 @@ os_chdir_impl(PyObject *module, path_t *path)
     result = !win32_wchdir(path->wide);
 #else
 #ifdef HAVE_FCHDIR
-    if (path->fd != -1)
+    if (path->is_fd)
         result = fchdir(path->fd);
     else
 #endif
@@ -4090,7 +4091,7 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, 
int dir_fd,
 #ifdef MS_WINDOWS
     result = 0;
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         result = win32_fchmod(path->fd, mode);
     }
     else if (follow_symlinks) {
@@ -4113,8 +4114,9 @@ os_chmod_impl(PyObject *module, path_t *path, int mode, 
int dir_fd,
 #else /* MS_WINDOWS */
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_FCHMOD
-    if (path->fd != -1)
+    if (path->is_fd) {
         result = fchmod(path->fd, mode);
+    }
     else
 #endif /* HAVE_CHMOD */
 #ifdef HAVE_LCHMOD
@@ -4511,7 +4513,7 @@ os_chown_impl(PyObject *module, path_t *path, uid_t uid, 
gid_t gid,
         return NULL;
 #endif
     if (dir_fd_and_fd_invalid("chown", dir_fd, path->fd) ||
-        fd_and_follow_symlinks_invalid("chown", path->fd, follow_symlinks))
+        fd_and_follow_symlinks_invalid("chown", path->is_fd, follow_symlinks))
         return NULL;
 
     if (PySys_Audit("os.chown", "OIIi", path->object, uid, gid,
@@ -4521,7 +4523,7 @@ os_chown_impl(PyObject *module, path_t *path, uid_t uid, 
gid_t gid,
 
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_FCHOWN
-    if (path->fd != -1)
+    if (path->is_fd)
         result = fchown(path->fd, uid, gid);
     else
 #endif
@@ -4999,7 +5001,7 @@ _posix_listdir(path_t *path, PyObject *list)
 
     errno = 0;
 #ifdef HAVE_FDOPENDIR
-    if (path->fd != -1) {
+    if (path->is_fd) {
       if (HAVE_FDOPENDIR_RUNTIME) {
         /* closedir() closes the FD, so we duplicate it */
         fd = _Py_dup(path->fd);
@@ -5898,7 +5900,7 @@ _testFileExists(path_t *path, BOOL followLinks)
     }
 
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         HANDLE hfile = _Py_get_osfhandle_noraise(path->fd);
         if (hfile != INVALID_HANDLE_VALUE) {
             if (GetFileType(hfile) != FILE_TYPE_UNKNOWN || !GetLastError()) {
@@ -5924,7 +5926,7 @@ _testFileType(path_t *path, int testedType)
     }
 
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         HANDLE hfile = _Py_get_osfhandle_noraise(path->fd);
         if (hfile != INVALID_HANDLE_VALUE) {
             result = _testFileTypeByHandle(hfile, testedType, TRUE);
@@ -7141,7 +7143,7 @@ os_utime_impl(PyObject *module, path_t *path, PyObject 
*times, PyObject *ns,
 
     if (path_and_dir_fd_invalid("utime", path, dir_fd) ||
         dir_fd_and_fd_invalid("utime", dir_fd, path->fd) ||
-        fd_and_follow_symlinks_invalid("utime", path->fd, follow_symlinks))
+        fd_and_follow_symlinks_invalid("utime", path->is_fd, follow_symlinks))
         return NULL;
 
 #if !defined(HAVE_UTIMENSAT)
@@ -7200,7 +7202,7 @@ os_utime_impl(PyObject *module, path_t *path, PyObject 
*times, PyObject *ns,
 #endif
 
 #if defined(HAVE_FUTIMES) || defined(HAVE_FUTIMENS)
-    if (path->fd != -1)
+    if (path->is_fd)
         result = utime_fd(&utime, path->fd);
     else
 #endif
@@ -7569,7 +7571,7 @@ os_execve_impl(PyObject *module, path_t *path, PyObject 
*argv, PyObject *env)
 
     _Py_BEGIN_SUPPRESS_IPH
 #ifdef HAVE_FEXECVE
-    if (path->fd > -1)
+    if (path->is_fd)
         fexecve(path->fd, argvlist, envlist);
     else
 #endif
@@ -13355,7 +13357,7 @@ os_truncate_impl(PyObject *module, path_t *path, 
Py_off_t length)
     int fd;
 #endif
 
-    if (path->fd != -1)
+    if (path->is_fd)
         return os_ftruncate_impl(module, path->fd, length);
 
     if (PySys_Audit("os.truncate", "On", path->object, length) < 0) {
@@ -14052,7 +14054,7 @@ os_statvfs_impl(PyObject *module, path_t *path)
     struct statfs st;
 
     Py_BEGIN_ALLOW_THREADS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         result = fstatfs(path->fd, &st);
     }
     else
@@ -14070,7 +14072,7 @@ os_statvfs_impl(PyObject *module, path_t *path)
 
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_FSTATVFS
-    if (path->fd != -1) {
+    if (path->is_fd) {
         result = fstatvfs(path->fd, &st);
     }
     else
@@ -15410,7 +15412,7 @@ os_getxattr_impl(PyObject *module, path_t *path, path_t 
*attribute,
                  int follow_symlinks)
 /*[clinic end generated code: output=5f2f44200a43cff2 input=025789491708f7eb]*/
 {
-    if (fd_and_follow_symlinks_invalid("getxattr", path->fd, follow_symlinks))
+    if (fd_and_follow_symlinks_invalid("getxattr", path->is_fd, 
follow_symlinks))
         return NULL;
 
     if (PySys_Audit("os.getxattr", "OO", path->object, attribute->object) < 0) 
{
@@ -15432,7 +15434,7 @@ os_getxattr_impl(PyObject *module, path_t *path, path_t 
*attribute,
         void *ptr = PyBytesWriter_GetData(writer);
 
         Py_BEGIN_ALLOW_THREADS;
-        if (path->fd >= 0)
+        if (path->is_fd)
             result = fgetxattr(path->fd, attribute->narrow, ptr, buffer_size);
         else if (follow_symlinks)
             result = getxattr(path->narrow, attribute->narrow, ptr, 
buffer_size);
@@ -15481,7 +15483,7 @@ os_setxattr_impl(PyObject *module, path_t *path, path_t 
*attribute,
 {
     ssize_t result;
 
-    if (fd_and_follow_symlinks_invalid("setxattr", path->fd, follow_symlinks))
+    if (fd_and_follow_symlinks_invalid("setxattr", path->is_fd, 
follow_symlinks))
         return NULL;
 
     if (PySys_Audit("os.setxattr", "OOy#i", path->object, attribute->object,
@@ -15490,7 +15492,7 @@ os_setxattr_impl(PyObject *module, path_t *path, path_t 
*attribute,
     }
 
     Py_BEGIN_ALLOW_THREADS;
-    if (path->fd > -1)
+    if (path->is_fd)
         result = fsetxattr(path->fd, attribute->narrow,
                            value->buf, value->len, flags);
     else if (follow_symlinks)
@@ -15534,7 +15536,7 @@ os_removexattr_impl(PyObject *module, path_t *path, 
path_t *attribute,
 {
     ssize_t result;
 
-    if (fd_and_follow_symlinks_invalid("removexattr", path->fd, 
follow_symlinks))
+    if (fd_and_follow_symlinks_invalid("removexattr", path->is_fd, 
follow_symlinks))
         return NULL;
 
     if (PySys_Audit("os.removexattr", "OO", path->object, attribute->object) < 
0) {
@@ -15542,7 +15544,7 @@ os_removexattr_impl(PyObject *module, path_t *path, 
path_t *attribute,
     }
 
     Py_BEGIN_ALLOW_THREADS;
-    if (path->fd > -1)
+    if (path->is_fd)
         result = fremovexattr(path->fd, attribute->narrow);
     else if (follow_symlinks)
         result = removexattr(path->narrow, attribute->narrow);
@@ -15584,7 +15586,7 @@ os_listxattr_impl(PyObject *module, path_t *path, int 
follow_symlinks)
     const char *name;
     char *buffer = NULL;
 
-    if (fd_and_follow_symlinks_invalid("listxattr", path->fd, follow_symlinks))
+    if (fd_and_follow_symlinks_invalid("listxattr", path->is_fd, 
follow_symlinks))
         goto exit;
 
     if (PySys_Audit("os.listxattr", "(O)",
@@ -15611,7 +15613,7 @@ os_listxattr_impl(PyObject *module, path_t *path, int 
follow_symlinks)
         }
 
         Py_BEGIN_ALLOW_THREADS;
-        if (path->fd > -1)
+        if (path->is_fd)
             length = flistxattr(path->fd, buffer, buffer_size);
         else if (follow_symlinks)
             length = listxattr(name, buffer, buffer_size);
@@ -16664,7 +16666,7 @@ DirEntry_from_posix_info(PyObject *module, path_t 
*path, const char *name,
     entry->stat = NULL;
     entry->lstat = NULL;
 
-    if (path->fd != -1) {
+    if (path->is_fd) {
         entry->dir_fd = path->fd;
         joined_path = NULL;
     }
@@ -16689,7 +16691,7 @@ DirEntry_from_posix_info(PyObject *module, path_t 
*path, const char *name,
     if (!entry->name)
         goto error;
 
-    if (path->fd != -1) {
+    if (path->is_fd) {
         entry->path = Py_NewRef(entry->name);
     }
     else if (!entry->path)
@@ -16813,8 +16815,9 @@ ScandirIterator_closedir(ScandirIterator *iterator)
     iterator->dirp = NULL;
     Py_BEGIN_ALLOW_THREADS
 #ifdef HAVE_FDOPENDIR
-    if (iterator->path.fd != -1)
+    if (iterator->path.is_fd) {
         rewinddir(dirp);
+    }
 #endif
     closedir(dirp);
     Py_END_ALLOW_THREADS
@@ -17034,7 +17037,7 @@ os_scandir_impl(PyObject *module, path_t *path)
 #else /* POSIX */
     errno = 0;
 #ifdef HAVE_FDOPENDIR
-    if (iterator->path.fd != -1) {
+    if (iterator->path.is_fd) {
       if (HAVE_FDOPENDIR_RUNTIME) {
         /* closedir() closes the FD, so we duplicate it */
         fd = _Py_dup(iterator->path.fd);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to