Skip to content

Commit abf76c3

Browse files
serhiy-storchakaPurityLake
authored andcommitted
pythongh-104522: Fix OSError raised when run a subprocess (python#114195)
Only set filename to cwd if it was caused by failed chdir(cwd). _fork_exec() now returns "noexec:chdir" for failed chdir(cwd). Co-authored-by: Robert O'Shea <[email protected]>
1 parent 5d3cd00 commit abf76c3

File tree

4 files changed

+29
-18
lines changed

4 files changed

+29
-18
lines changed

Lib/subprocess.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1944,16 +1944,21 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
19441944
SubprocessError)
19451945
if issubclass(child_exception_type, OSError) and hex_errno:
19461946
errno_num = int(hex_errno, 16)
1947-
child_exec_never_called = (err_msg == "noexec")
1948-
if child_exec_never_called:
1947+
if err_msg == "noexec:chdir":
19491948
err_msg = ""
19501949
# The error must be from chdir(cwd).
19511950
err_filename = cwd
1951+
elif err_msg == "noexec":
1952+
err_msg = ""
1953+
err_filename = None
19521954
else:
19531955
err_filename = orig_executable
19541956
if errno_num != 0:
19551957
err_msg = os.strerror(errno_num)
1956-
raise child_exception_type(errno_num, err_msg, err_filename)
1958+
if err_filename is not None:
1959+
raise child_exception_type(errno_num, err_msg, err_filename)
1960+
else:
1961+
raise child_exception_type(errno_num, err_msg)
19571962
raise child_exception_type(err_msg)
19581963

19591964

Lib/test/test_subprocess.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,11 +2017,12 @@ def test_user(self):
20172017
"import os; print(os.getuid())"],
20182018
user=user,
20192019
close_fds=close_fds)
2020-
except PermissionError: # (EACCES, EPERM)
2021-
pass
2020+
except PermissionError as e: # (EACCES, EPERM)
2021+
self.assertIsNone(e.filename)
20222022
except OSError as e:
20232023
if e.errno not in (errno.EACCES, errno.EPERM):
20242024
raise
2025+
self.assertIsNone(e.filename)
20252026
else:
20262027
if isinstance(user, str):
20272028
user_uid = pwd.getpwnam(user).pw_uid
@@ -2065,8 +2066,8 @@ def test_group(self):
20652066
"import os; print(os.getgid())"],
20662067
group=group,
20672068
close_fds=close_fds)
2068-
except PermissionError: # (EACCES, EPERM)
2069-
pass
2069+
except PermissionError as e: # (EACCES, EPERM)
2070+
self.assertIsNone(e.filename)
20702071
else:
20712072
if isinstance(group, str):
20722073
group_gid = grp.getgrnam(group).gr_gid
@@ -2114,7 +2115,8 @@ def _test_extra_groups_impl(self, *, gid, group_list):
21142115
[sys.executable, "-c",
21152116
"import os, sys, json; json.dump(os.getgroups(), sys.stdout)"],
21162117
extra_groups=group_list)
2117-
except PermissionError:
2118+
except PermissionError as e:
2119+
self.assertIsNone(e.filename)
21182120
self.skipTest("setgroup() EPERM; this test may require root.")
21192121
else:
21202122
parent_groups = os.getgroups()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:exc:`OSError` raised when run a subprocess now only has *filename*
2+
attribute set to *cwd* if the error was caused by a failed attempt to change
3+
the current directory.

Modules/_posixsubprocess.c

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -673,9 +673,10 @@ child_exec(char *const exec_array[],
673673
PyObject *preexec_fn,
674674
PyObject *preexec_fn_args_tuple)
675675
{
676-
int i, saved_errno, reached_preexec = 0;
676+
int i, saved_errno;
677677
PyObject *result;
678-
const char* err_msg = "";
678+
/* Indicate to the parent that the error happened before exec(). */
679+
const char *err_msg = "noexec";
679680
/* Buffer large enough to hold a hex integer. We can't malloc. */
680681
char hex_errno[sizeof(saved_errno)*2+1];
681682

@@ -735,8 +736,12 @@ child_exec(char *const exec_array[],
735736
/* We no longer manually close p2cread, c2pwrite, and errwrite here as
736737
* _close_open_fds takes care when it is not already non-inheritable. */
737738

738-
if (cwd)
739-
POSIX_CALL(chdir(cwd));
739+
if (cwd) {
740+
if (chdir(cwd) == -1) {
741+
err_msg = "noexec:chdir";
742+
goto error;
743+
}
744+
}
740745

741746
if (child_umask >= 0)
742747
umask(child_umask); /* umask() always succeeds. */
@@ -784,7 +789,7 @@ child_exec(char *const exec_array[],
784789
#endif /* HAVE_SETREUID */
785790

786791

787-
reached_preexec = 1;
792+
err_msg = "";
788793
if (preexec_fn != Py_None && preexec_fn_args_tuple) {
789794
/* This is where the user has asked us to deadlock their program. */
790795
result = PyObject_Call(preexec_fn, preexec_fn_args_tuple, NULL);
@@ -842,16 +847,12 @@ child_exec(char *const exec_array[],
842847
}
843848
_Py_write_noraise(errpipe_write, cur, hex_errno + sizeof(hex_errno) - cur);
844849
_Py_write_noraise(errpipe_write, ":", 1);
845-
if (!reached_preexec) {
846-
/* Indicate to the parent that the error happened before exec(). */
847-
_Py_write_noraise(errpipe_write, "noexec", 6);
848-
}
849850
/* We can't call strerror(saved_errno). It is not async signal safe.
850851
* The parent process will look the error message up. */
851852
} else {
852853
_Py_write_noraise(errpipe_write, "SubprocessError:0:", 18);
853-
_Py_write_noraise(errpipe_write, err_msg, strlen(err_msg));
854854
}
855+
_Py_write_noraise(errpipe_write, err_msg, strlen(err_msg));
855856
}
856857

857858

0 commit comments

Comments
 (0)