summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGregory P. Smith <greg@krypto.org>2013-12-02 01:27:40 (GMT)
committerGregory P. Smith <greg@krypto.org>2013-12-02 01:27:40 (GMT)
commit708a3182c931ff1b6e8a420f87929d143822de28 (patch)
tree7788ae8571bf4b061d425fc41988e958d34276d5
parent5c1c3b4f197c57952760be37d77d73669284a607 (diff)
downloadcpython-708a3182c931ff1b6e8a420f87929d143822de28.zip
cpython-708a3182c931ff1b6e8a420f87929d143822de28.tar.gz
cpython-708a3182c931ff1b6e8a420f87929d143822de28.tar.bz2
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.
-rw-r--r--Lib/test/test_subprocess.py21
-rw-r--r--Misc/NEWS3
-rw-r--r--Modules/_posixsubprocess.c48
3 files changed, 66 insertions, 6 deletions
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index 77f1ba3..f5a70a3 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -1559,6 +1559,27 @@ class POSIXProcessTestCase(BaseTestCase):
# all standard fds closed.
self.check_close_std_fds([0, 1, 2])
+ def test_small_errpipe_write_fd(self):
+ """Issue #15798: Popen should work when stdio fds are available."""
+ new_stdin = os.dup(0)
+ new_stdout = os.dup(1)
+ try:
+ os.close(0)
+ os.close(1)
+
+ # Side test: if errpipe_write fails to have its CLOEXEC
+ # flag set this should cause the parent to think the exec
+ # failed. Extremely unlikely: everyone supports CLOEXEC.
+ subprocess.Popen([
+ sys.executable, "-c",
+ "print('AssertionError:0:CLOEXEC failure.')"]).wait()
+ finally:
+ # Restore original stdin and stdout
+ os.dup2(new_stdin, 0)
+ os.dup2(new_stdout, 1)
+ os.close(new_stdin)
+ os.close(new_stdout)
+
def test_remapping_std_fds(self):
# open up some temporary files
temps = [mkstemp() for i in range(3)]
diff --git a/Misc/NEWS b/Misc/NEWS
index 3aa4ad6..845a4b0 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -18,6 +18,9 @@ Core and Builtins
Library
-------
+- Issue #15798: Fixed subprocess.Popen() to no longer fail if file
+ descriptor 0, 1 or 2 is closed.
+
- Issue #19088: Fixed incorrect caching of the copyreg module in
object.__reduce__() and object.__reduce_ex__().
diff --git a/Modules/_posixsubprocess.c b/Modules/_posixsubprocess.c
index 8d65530..0196ca2 100644
--- a/Modules/_posixsubprocess.c
+++ b/Modules/_posixsubprocess.c
@@ -723,26 +723,24 @@ Raises: Only on an error in the parent process.\n\
PyDoc_STRVAR(subprocess_cloexec_pipe_doc,
"cloexec_pipe() -> (read_end, write_end)\n\n\
-Create a pipe whose ends have the cloexec flag set.");
+Create a pipe whose ends have the cloexec flag set; write_end will be >= 3.");
static PyObject *
subprocess_cloexec_pipe(PyObject *self, PyObject *noargs)
{
int fds[2];
- int res;
+ int res, saved_errno;
+ long oldflags;
#ifdef HAVE_PIPE2
Py_BEGIN_ALLOW_THREADS
res = pipe2(fds, O_CLOEXEC);
Py_END_ALLOW_THREADS
if (res != 0 && errno == ENOSYS)
{
- {
#endif
/* We hold the GIL which offers some protection from other code calling
* fork() before the CLOEXEC flags have been set but we can't guarantee
* anything without pipe2(). */
- long oldflags;
-
res = pipe(fds);
if (res == 0) {
@@ -759,9 +757,47 @@ subprocess_cloexec_pipe(PyObject *self, PyObject *noargs)
if (res == 0)
res = fcntl(fds[1], F_SETFD, oldflags | FD_CLOEXEC);
#ifdef HAVE_PIPE2
- }
}
#endif
+ if (res == 0 && fds[1] < 3) {
+ /* We always want the write end of the pipe to avoid fds 0, 1 and 2
+ * as our child may claim those for stdio connections. */
+ int write_fd = fds[1];
+ int fds_to_close[3] = {-1, -1, -1};
+ int fds_to_close_idx = 0;
+#ifdef F_DUPFD_CLOEXEC
+ fds_to_close[fds_to_close_idx++] = write_fd;
+ write_fd = fcntl(write_fd, F_DUPFD_CLOEXEC, 3);
+ if (write_fd < 0) /* We don't support F_DUPFD_CLOEXEC / other error */
+#endif
+ {
+ /* Use dup a few times until we get a desirable fd. */
+ for (; fds_to_close_idx < 3; ++fds_to_close_idx) {
+ fds_to_close[fds_to_close_idx] = write_fd;
+ write_fd = dup(write_fd);
+ if (write_fd >= 3)
+ break;
+ /* We may dup a few extra times if it returns an error but
+ * that is okay. Repeat calls should return the same error. */
+ }
+ if (write_fd < 0) res = write_fd;
+ if (res == 0) {
+ oldflags = fcntl(write_fd, F_GETFD, 0);
+ if (oldflags < 0) res = oldflags;
+ if (res == 0)
+ res = fcntl(write_fd, F_SETFD, oldflags | FD_CLOEXEC);
+ }
+ }
+ saved_errno = errno;
+ /* Close fds we tried for the write end that were too low. */
+ for (fds_to_close_idx=0; fds_to_close_idx < 3; ++fds_to_close_idx) {
+ int temp_fd = fds_to_close[fds_to_close_idx];
+ while (temp_fd >= 0 && close(temp_fd) < 0 && errno == EINTR);
+ }
+ errno = saved_errno; /* report dup or fcntl errors, not close. */
+ fds[1] = write_fd;
+ } /* end if write fd was too small */
+
if (res != 0)
return PyErr_SetFromErrno(PyExc_OSError);
return Py_BuildValue("(ii)", fds[0], fds[1]);