diff --git a/Doc/library/os.rst b/Doc/library/os.rst index 7547967c6b32f0..d6646b9906aa93 100644 --- a/Doc/library/os.rst +++ b/Doc/library/os.rst @@ -5161,11 +5161,18 @@ written in Python, such as a mail server's external command delivery program. Performs ``os.closerange(fd, INF)``. + .. data:: POSIX_SPAWN_CHDIR + + (``os.POSIX_SPAWN_CHDIR``, *path*) + + Performs ``os.chdir(path)``. + These tuples correspond to the C library :c:func:`!posix_spawn_file_actions_addopen`, :c:func:`!posix_spawn_file_actions_addclose`, - :c:func:`!posix_spawn_file_actions_adddup2`, and - :c:func:`!posix_spawn_file_actions_addclosefrom_np` API calls used to prepare + :c:func:`!posix_spawn_file_actions_adddup2`, + :c:func:`!posix_spawn_file_actions_addclosefrom_np`, and + :c:func:`!posix_spawn_file_actions_addchdir_np` API calls used to prepare for the :c:func:`!posix_spawn` call itself. The *setpgroup* argument will set the process group of the child to the value @@ -5212,6 +5219,10 @@ written in Python, such as a mail server's external command delivery program. ``os.POSIX_SPAWN_CLOSEFROM`` is available on platforms where :c:func:`!posix_spawn_file_actions_addclosefrom_np` exists. + .. versionchanged:: 3.15 + ``os.POSIX_SPAWN_CHDIR`` is available on platforms where + :c:func:`!posix_spawn_file_actions_addchdir_np` exist. + .. availability:: Unix, not WASI, not Android, not iOS. .. function:: posix_spawnp(path, argv, env, *, file_actions=None, \ diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 7ac2289f535b6d..9a3a2a220c0103 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -806,6 +806,7 @@ def _can_use_kqueue(): # These are primarily fail-safe knobs for negatives. A True value does not # guarantee the given libc/syscall API will be used. _USE_POSIX_SPAWN = _use_posix_spawn() +_HAVE_POSIX_SPAWN_CHDIR = hasattr(os, 'POSIX_SPAWN_CHDIR') _HAVE_POSIX_SPAWN_CLOSEFROM = hasattr(os, 'POSIX_SPAWN_CLOSEFROM') @@ -1836,7 +1837,7 @@ def _get_handles(self, stdin, stdout, stderr): errread, errwrite) - def _posix_spawn(self, args, executable, env, restore_signals, close_fds, + def _posix_spawn(self, args, executable, env, restore_signals, close_fds, cwd, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite): @@ -1863,6 +1864,9 @@ def _posix_spawn(self, args, executable, env, restore_signals, close_fds, if fd != -1: file_actions.append((os.POSIX_SPAWN_DUP2, fd, fd2)) + if cwd is not None: + file_actions.append((os.POSIX_SPAWN_CHDIR, cwd)) + if close_fds: file_actions.append((os.POSIX_SPAWN_CLOSEFROM, 3)) @@ -1915,7 +1919,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, and preexec_fn is None and (not close_fds or _HAVE_POSIX_SPAWN_CLOSEFROM) and not pass_fds - and cwd is None + and (cwd is None or _HAVE_POSIX_SPAWN_CHDIR) and (p2cread == -1 or p2cread > 2) and (c2pwrite == -1 or c2pwrite > 2) and (errwrite == -1 or errwrite > 2) @@ -1925,7 +1929,8 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, and gids is None and uid is None and umask < 0): - self._posix_spawn(args, executable, env, restore_signals, close_fds, + self._posix_spawn(args, executable, env, restore_signals, + close_fds, cwd, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py index 0c5679611848ea..691807c325d7c4 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -2018,6 +2018,7 @@ def _get_chdir_exception(self): self._nonexistent_dir) return desired_exception + @mock.patch("subprocess._HAVE_POSIX_SPAWN_CHDIR", new=False) def test_exception_cwd(self): """Test error in the child raised in the parent for a bad cwd.""" desired_exception = self._get_chdir_exception() diff --git a/Misc/NEWS.d/next/Library/2026-04-01-15-16-28.gh-issue-114467.g-8r4E.rst b/Misc/NEWS.d/next/Library/2026-04-01-15-16-28.gh-issue-114467.g-8r4E.rst new file mode 100644 index 00000000000000..ba1a04f02c3c07 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-01-15-16-28.gh-issue-114467.g-8r4E.rst @@ -0,0 +1,3 @@ +The :mod:`subprocess` module can now use the :func:`os.posix_spawn` function +with ``cwd`` set on platforms where ``posix_spawn_file_actions_addchdir_np`` +is available. Patch by Jakub Kulik. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 07c2b73575f14e..9070b4092baae6 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -7606,6 +7606,9 @@ enum posix_spawn_file_actions_identifier { #ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP ,POSIX_SPAWN_CLOSEFROM #endif +#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + ,POSIX_SPAWN_CHDIR +#endif }; #if defined(HAVE_SCHED_SETPARAM) || defined(HAVE_SCHED_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDULER) || defined(POSIX_SPAWN_SETSCHEDPARAM) @@ -7757,7 +7760,7 @@ parse_posix_spawn_flags(PyObject *module, const char *func_name, PyObject *setpg static int parse_file_actions(PyObject *file_actions, posix_spawn_file_actions_t *file_actionsp, - PyObject *temp_buffer) + PyObject *temp_buffer, PyObject* cwd_buffer) { PyObject *seq; PyObject *file_action = NULL; @@ -7864,6 +7867,29 @@ parse_file_actions(PyObject *file_actions, } break; } +#endif +#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + case POSIX_SPAWN_CHDIR: { + PyObject *path; + if (!PyArg_ParseTuple(file_action, "OO&" + ";A chdir file_action tuple must have 2 elements", + &tag_obj, PyUnicode_FSConverter, &path)) + { + goto fail; + } + errno = posix_spawn_file_actions_addchdir_np(file_actionsp, + PyBytes_AS_STRING(path)); + if (errno) { + posix_error(); + Py_DECREF(path); + goto fail; + } + if (PyList_Append(cwd_buffer, path)) { + Py_DECREF(path); + goto fail; + } + break; + } #endif default: { PyErr_SetString(PyExc_TypeError, @@ -7901,6 +7927,7 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a Py_ssize_t argc, envc; PyObject *result = NULL; PyObject *temp_buffer = NULL; + PyObject *cwd_buffer = NULL; pid_t pid; int err_code; @@ -7972,7 +7999,13 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a if (!temp_buffer) { goto exit; } - if (parse_file_actions(file_actions, &file_actions_buf, temp_buffer)) { + /* Use a list to capture all directories passed via POSIX_SPAWN_CHDIR + * action for potential exception creation below. */ + cwd_buffer = PyList_New(0); + if (!cwd_buffer) { + goto exit; + } + if (parse_file_actions(file_actions, &file_actions_buf, temp_buffer, cwd_buffer)) { goto exit; } file_actionsp = &file_actions_buf; @@ -8004,6 +8037,41 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a if (err_code) { errno = err_code; +#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + Py_ssize_t cwd_size = PyList_GET_SIZE(cwd_buffer); + if (errno == ENOENT && cwd_size > 0) { + /* ENOENT can occur when either the path of the executable or any of + * the cwds given via file_actions doesn't exist. Since it's not + * possible to determine which of those paths caused the problem, + * we return an exception with all of those. */ + + if (cwd_size == 1) { + PyObject *cwd = PyList_GET_ITEM(cwd_buffer, 0); + PyErr_Format(PyExc_FileNotFoundError, "Either '%S' or '%s' doesn't exist.", + path->object, PyBytes_AS_STRING(cwd)); + } else { + /* Multiple POSIX_SPAWN_CHDIR actions were used in a single + * spawn. In this case, we have to build the expection message + * from all possibly missing paths. */ + PyObject *separator = PyBytes_FromString(", "); + if (!separator) { + goto exit; + } + + PyObject *joined = PyBytes_Join(separator, cwd_buffer); + Py_DECREF(separator); + if (!joined) { + goto exit; + } + PyErr_Format(PyExc_FileNotFoundError, + "Either '%S' or one of (%s) doesn't exist.", + path->object, PyBytes_AS_STRING(joined)); + + Py_DECREF(joined); + } + goto exit; + } +#endif PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path->object); goto exit; } @@ -8025,6 +8093,7 @@ py_posix_spawn(int use_posix_spawnp, PyObject *module, path_t *path, PyObject *a if (argvlist) { free_string_array(argvlist, argc); } + Py_XDECREF(cwd_buffer); Py_XDECREF(temp_buffer); return result; } @@ -18150,6 +18219,9 @@ all_ins(PyObject *m) #ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP if (PyModule_AddIntMacro(m, POSIX_SPAWN_CLOSEFROM)) return -1; #endif +#ifdef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + if (PyModule_AddIntMacro(m, POSIX_SPAWN_CHDIR)) return -1; +#endif #endif #if defined(HAVE_SPAWNV) || defined (HAVE_RTPSPAWN) diff --git a/configure b/configure index 4726b4fe3102ac..785d776d2c4c55 100755 --- a/configure +++ b/configure @@ -20109,6 +20109,12 @@ if test "x$ac_cv_func_posix_spawnp" = xyes then : printf "%s\n" "#define HAVE_POSIX_SPAWNP 1" >>confdefs.h +fi +ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addchdir_np" "ac_cv_func_posix_spawn_file_actions_addchdir_np" +if test "x$ac_cv_func_posix_spawn_file_actions_addchdir_np" = xyes +then : + printf "%s\n" "#define HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP 1" >>confdefs.h + fi ac_fn_c_check_func "$LINENO" "posix_spawn_file_actions_addclosefrom_np" "ac_cv_func_posix_spawn_file_actions_addclosefrom_np" if test "x$ac_cv_func_posix_spawn_file_actions_addclosefrom_np" = xyes diff --git a/configure.ac b/configure.ac index dd860292cc2058..92325ff19e8596 100644 --- a/configure.ac +++ b/configure.ac @@ -5335,7 +5335,7 @@ AC_CHECK_FUNCS([ \ lockf lstat lutimes madvise mbrtowc memrchr mkdirat mkfifo mkfifoat \ mknod mknodat mktime mmap mremap nice openat opendir pathconf pause pipe \ pipe2 plock poll ppoll posix_fadvise posix_fallocate posix_openpt posix_spawn posix_spawnp \ - posix_spawn_file_actions_addclosefrom_np \ + posix_spawn_file_actions_addchdir_np posix_spawn_file_actions_addclosefrom_np \ pread preadv preadv2 process_vm_readv \ pthread_cond_timedwait_relative_np pthread_condattr_setclock pthread_init \ pthread_kill pthread_get_name_np pthread_getname_np pthread_set_name_np \ diff --git a/pyconfig.h.in b/pyconfig.h.in index 9da33c954a52f8..73f65ad070973a 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -975,6 +975,10 @@ /* Define to 1 if you have the 'posix_spawnp' function. */ #undef HAVE_POSIX_SPAWNP +/* Define to 1 if you have the 'posix_spawn_file_actions_addchdir_np' + function. */ +#undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCHDIR_NP + /* Define to 1 if you have the 'posix_spawn_file_actions_addclosefrom_np' function. */ #undef HAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP