Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 708a318

Browse files
committed
Fixes issue #15798: subprocess.Popen() no longer fails if file
descriptor 0, 1 or 2 is closed. The errpipe_write fd will always be >= 3.
1 parent 5c1c3b4 commit 708a318

3 files changed

Lines changed: 66 additions & 6 deletions

File tree

Lib/test/test_subprocess.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,27 @@ def test_close_fds_0_1_2(self):
15591559
# all standard fds closed.
15601560
self.check_close_std_fds([0, 1, 2])
15611561

1562+
def test_small_errpipe_write_fd(self):
1563+
"""Issue #15798: Popen should work when stdio fds are available."""
1564+
new_stdin = os.dup(0)
1565+
new_stdout = os.dup(1)
1566+
try:
1567+
os.close(0)
1568+
os.close(1)
1569+
1570+
# Side test: if errpipe_write fails to have its CLOEXEC
1571+
# flag set this should cause the parent to think the exec
1572+
# failed. Extremely unlikely: everyone supports CLOEXEC.
1573+
subprocess.Popen([
1574+
sys.executable, "-c",
1575+
"print('AssertionError:0:CLOEXEC failure.')"]).wait()
1576+
finally:
1577+
# Restore original stdin and stdout
1578+
os.dup2(new_stdin, 0)
1579+
os.dup2(new_stdout, 1)
1580+
os.close(new_stdin)
1581+
os.close(new_stdout)
1582+
15621583
def test_remapping_std_fds(self):
15631584
# open up some temporary files
15641585
temps = [mkstemp() for i in range(3)]

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Core and Builtins
1818
Library
1919
-------
2020

21+
- Issue #15798: Fixed subprocess.Popen() to no longer fail if file
22+
descriptor 0, 1 or 2 is closed.
23+
2124
- Issue #19088: Fixed incorrect caching of the copyreg module in
2225
object.__reduce__() and object.__reduce_ex__().
2326

Modules/_posixsubprocess.c

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -723,26 +723,24 @@ Raises: Only on an error in the parent process.\n\
723723

724724
PyDoc_STRVAR(subprocess_cloexec_pipe_doc,
725725
"cloexec_pipe() -> (read_end, write_end)\n\n\
726-
Create a pipe whose ends have the cloexec flag set.");
726+
Create a pipe whose ends have the cloexec flag set; write_end will be >= 3.");
727727

728728
static PyObject *
729729
subprocess_cloexec_pipe(PyObject *self, PyObject *noargs)
730730
{
731731
int fds[2];
732-
int res;
732+
int res, saved_errno;
733+
long oldflags;
733734
#ifdef HAVE_PIPE2
734735
Py_BEGIN_ALLOW_THREADS
735736
res = pipe2(fds, O_CLOEXEC);
736737
Py_END_ALLOW_THREADS
737738
if (res != 0 && errno == ENOSYS)
738739
{
739-
{
740740
#endif
741741
/* We hold the GIL which offers some protection from other code calling
742742
* fork() before the CLOEXEC flags have been set but we can't guarantee
743743
* anything without pipe2(). */
744-
long oldflags;
745-
746744
res = pipe(fds);
747745

748746
if (res == 0) {
@@ -759,9 +757,47 @@ subprocess_cloexec_pipe(PyObject *self, PyObject *noargs)
759757
if (res == 0)
760758
res = fcntl(fds[1], F_SETFD, oldflags | FD_CLOEXEC);
761759
#ifdef HAVE_PIPE2
762-
}
763760
}
764761
#endif
762+
if (res == 0 && fds[1] < 3) {
763+
/* We always want the write end of the pipe to avoid fds 0, 1 and 2
764+
* as our child may claim those for stdio connections. */
765+
int write_fd = fds[1];
766+
int fds_to_close[3] = {-1, -1, -1};
767+
int fds_to_close_idx = 0;
768+
#ifdef F_DUPFD_CLOEXEC
769+
fds_to_close[fds_to_close_idx++] = write_fd;
770+
write_fd = fcntl(write_fd, F_DUPFD_CLOEXEC, 3);
771+
if (write_fd < 0) /* We don't support F_DUPFD_CLOEXEC / other error */
772+
#endif
773+
{
774+
/* Use dup a few times until we get a desirable fd. */
775+
for (; fds_to_close_idx < 3; ++fds_to_close_idx) {
776+
fds_to_close[fds_to_close_idx] = write_fd;
777+
write_fd = dup(write_fd);
778+
if (write_fd >= 3)
779+
break;
780+
/* We may dup a few extra times if it returns an error but
781+
* that is okay. Repeat calls should return the same error. */
782+
}
783+
if (write_fd < 0) res = write_fd;
784+
if (res == 0) {
785+
oldflags = fcntl(write_fd, F_GETFD, 0);
786+
if (oldflags < 0) res = oldflags;
787+
if (res == 0)
788+
res = fcntl(write_fd, F_SETFD, oldflags | FD_CLOEXEC);
789+
}
790+
}
791+
saved_errno = errno;
792+
/* Close fds we tried for the write end that were too low. */
793+
for (fds_to_close_idx=0; fds_to_close_idx < 3; ++fds_to_close_idx) {
794+
int temp_fd = fds_to_close[fds_to_close_idx];
795+
while (temp_fd >= 0 && close(temp_fd) < 0 && errno == EINTR);
796+
}
797+
errno = saved_errno; /* report dup or fcntl errors, not close. */
798+
fds[1] = write_fd;
799+
} /* end if write fd was too small */
800+
765801
if (res != 0)
766802
return PyErr_SetFromErrno(PyExc_OSError);
767803
return Py_BuildValue("(ii)", fds[0], fds[1]);

0 commit comments

Comments
 (0)