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.
This commit is contained in:
Victor Stinner 2026-03-03 13:57:08 +01:00 committed by GitHub
parent db41717cd5
commit 52c8efa87d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 103 additions and 32 deletions

View file

@ -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)

View file

@ -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)

View file

@ -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.

View file

@ -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);