]> granicus.if.org Git - python/commitdiff
issue7213: Open the pipes used by subprocesses with the FD_CLOEXEC flag from
authorGregory P. Smith <greg@mad-scientist.com>
Mon, 13 Dec 2010 07:59:39 +0000 (07:59 +0000)
committerGregory P. Smith <greg@mad-scientist.com>
Mon, 13 Dec 2010 07:59:39 +0000 (07:59 +0000)
the C code, using pipe2() when available.  Adds unittests for close_fds and
cloexec behaviors.

Lib/subprocess.py
Lib/test/subprocessdata/fd_status.py [new file with mode: 0644]
Lib/test/subprocessdata/input_reader.py [new file with mode: 0644]
Lib/test/subprocessdata/qcat.py [new file with mode: 0644]
Lib/test/subprocessdata/qgrep.py [new file with mode: 0644]
Lib/test/test_subprocess.py
Makefile.pre.in
Modules/_posixsubprocess.c
Tools/msi/msi.py
configure.in

index 62dfd36c4c5e298e81c9d72797fd3d079c371daa..729a53b622ba9fc90cafab776d306d7ce826fbeb 100644 (file)
@@ -390,6 +390,23 @@ else:
     # POSIX defines PIPE_BUF as >= 512.
     _PIPE_BUF = getattr(select, 'PIPE_BUF', 512)
 
+    if _posixsubprocess:
+        _create_pipe = _posixsubprocess.cloexec_pipe
+    else:
+        def _create_pipe():
+            try:
+                cloexec_flag = fcntl.FD_CLOEXEC
+            except AttributeError:
+                cloexec_flag = 1
+
+            fds = os.pipe()
+
+            old = fcntl.fcntl(fds[0], fcntl.F_GETFD)
+            fcntl.fcntl(fds[0], fcntl.F_SETFD, old | cloexec_flag)
+            old = fcntl.fcntl(fds[1], fcntl.F_GETFD)
+            fcntl.fcntl(fds[1], fcntl.F_SETFD, old | cloexec_flag)
+
+            return fds
 
 __all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "getstatusoutput",
            "getoutput", "check_output", "CalledProcessError"]
@@ -1031,7 +1048,7 @@ class Popen(object):
             if stdin is None:
                 pass
             elif stdin == PIPE:
-                p2cread, p2cwrite = os.pipe()
+                p2cread, p2cwrite = _create_pipe()
             elif isinstance(stdin, int):
                 p2cread = stdin
             else:
@@ -1041,7 +1058,7 @@ class Popen(object):
             if stdout is None:
                 pass
             elif stdout == PIPE:
-                c2pread, c2pwrite = os.pipe()
+                c2pread, c2pwrite = _create_pipe()
             elif isinstance(stdout, int):
                 c2pwrite = stdout
             else:
@@ -1051,7 +1068,7 @@ class Popen(object):
             if stderr is None:
                 pass
             elif stderr == PIPE:
-                errread, errwrite = os.pipe()
+                errread, errwrite = _create_pipe()
             elif stderr == STDOUT:
                 errwrite = c2pwrite
             elif isinstance(stderr, int):
@@ -1065,16 +1082,6 @@ class Popen(object):
                     errread, errwrite)
 
 
-        def _set_cloexec_flag(self, fd):
-            try:
-                cloexec_flag = fcntl.FD_CLOEXEC
-            except AttributeError:
-                cloexec_flag = 1
-
-            old = fcntl.fcntl(fd, fcntl.F_GETFD)
-            fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag)
-
-
         def _close_fds(self, but):
             os.closerange(3, but)
             os.closerange(but + 1, MAXFD)
@@ -1116,10 +1123,9 @@ class Popen(object):
             # For transferring possible exec failure from child to parent.
             # Data format: "exception name:hex errno:description"
             # Pickle is not used; it is complex and involves memory allocation.
-            errpipe_read, errpipe_write = os.pipe()
+            errpipe_read, errpipe_write = _create_pipe()
             try:
                 try:
-                    self._set_cloexec_flag(errpipe_write)
 
                     if _posixsubprocess:
                         # We must avoid complex work that could involve
diff --git a/Lib/test/subprocessdata/fd_status.py b/Lib/test/subprocessdata/fd_status.py
new file mode 100644 (file)
index 0000000..083b2f9
--- /dev/null
@@ -0,0 +1,24 @@
+"""When called as a script, print a comma-separated list of the open
+file descriptors on stdout."""
+
+import errno
+import os
+import fcntl
+
+try:
+    _MAXFD = os.sysconf("SC_OPEN_MAX")
+except:
+    _MAXFD = 256
+
+def isopen(fd):
+    """Return True if the fd is open, and False otherwise"""
+    try:
+        fcntl.fcntl(fd, fcntl.F_GETFD, 0)
+    except IOError as e:
+        if e.errno == errno.EBADF:
+            return False
+        raise
+    return True
+
+if __name__ == "__main__":
+    print(','.join(str(fd) for fd in range(0, _MAXFD) if isopen(fd)))
diff --git a/Lib/test/subprocessdata/input_reader.py b/Lib/test/subprocessdata/input_reader.py
new file mode 100644 (file)
index 0000000..ccae5f3
--- /dev/null
@@ -0,0 +1,7 @@
+"""When called as a script, consumes the input"""
+
+import sys
+
+if __name__ = "__main__":
+    for line in sys.stdin:
+        pass
diff --git a/Lib/test/subprocessdata/qcat.py b/Lib/test/subprocessdata/qcat.py
new file mode 100644 (file)
index 0000000..fe6f9db
--- /dev/null
@@ -0,0 +1,7 @@
+"""When ran as a script, simulates cat with no arguments."""
+
+import sys
+
+if __name__ == "__main__":
+    for line in sys.stdin:
+        sys.stdout.write(line)
diff --git a/Lib/test/subprocessdata/qgrep.py b/Lib/test/subprocessdata/qgrep.py
new file mode 100644 (file)
index 0000000..6990637
--- /dev/null
@@ -0,0 +1,10 @@
+"""When called with a single argument, simulated fgrep with a single
+argument and no options."""
+
+import sys
+
+if __name__ == "__main__":
+    pattern = sys.argv[1]
+    for line in sys.stdin:
+        if pattern in line:
+            sys.stdout.write(line)
index eaa26d2ba67ac7af5ccc71245bd2ed023294786d..74e740438957af444f6ac45d9e066a906cac941a 100644 (file)
@@ -10,6 +10,7 @@ import time
 import re
 import sysconfig
 import warnings
+import select
 try:
     import gc
 except ImportError:
@@ -964,6 +965,83 @@ class POSIXProcessTestCase(BaseTestCase):
         exitcode = subprocess.call([program, "-c", "pass"], env=envb)
         self.assertEqual(exitcode, 0)
 
+    def test_pipe_cloexec(self):
+        sleeper = support.findfile("input_reader.py", subdir="subprocessdata")
+        fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
+
+        p1 = subprocess.Popen([sys.executable, sleeper],
+                              stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                              stderr=subprocess.PIPE, close_fds=False)
+
+        self.addCleanup(p1.communicate, b'')
+
+        p2 = subprocess.Popen([sys.executable, fd_status],
+                              stdout=subprocess.PIPE, close_fds=False)
+
+        output, error = p2.communicate()
+        result_fds = set(map(int, output.split(b',')))
+        unwanted_fds = set([p1.stdin.fileno(), p1.stdout.fileno(),
+                            p1.stderr.fileno()])
+
+        self.assertFalse(result_fds & unwanted_fds,
+                         "Expected no fds from %r to be open in child, "
+                         "found %r" %
+                              (unwanted_fds, result_fds & unwanted_fds))
+
+    def test_pipe_cloexec_real_tools(self):
+        qcat = support.findfile("qcat.py", subdir="subprocessdata")
+        qgrep = support.findfile("qgrep.py", subdir="subprocessdata")
+
+        subdata = b'zxcvbn'
+        data = subdata * 4 + b'\n'
+
+        p1 = subprocess.Popen([sys.executable, qcat],
+                              stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+                              close_fds=False)
+
+        p2 = subprocess.Popen([sys.executable, qgrep, subdata],
+                              stdin=p1.stdout, stdout=subprocess.PIPE,
+                              close_fds=False)
+
+        self.addCleanup(p1.wait)
+        self.addCleanup(p2.wait)
+        self.addCleanup(p1.terminate)
+        self.addCleanup(p2.terminate)
+
+        p1.stdin.write(data)
+        p1.stdin.close()
+
+        readfiles, ignored1, ignored2 = select.select([p2.stdout], [], [], 10)
+
+        self.assertTrue(readfiles, "The child hung")
+        self.assertEqual(p2.stdout.read(), data)
+
+    def test_close_fds(self):
+        fd_status = support.findfile("fd_status.py", subdir="subprocessdata")
+
+        fds = os.pipe()
+        self.addCleanup(os.close, fds[0])
+        self.addCleanup(os.close, fds[1])
+
+        open_fds = set(fds)
+
+        p = subprocess.Popen([sys.executable, fd_status],
+                             stdout=subprocess.PIPE, close_fds=False)
+        output, ignored = p.communicate()
+        remaining_fds = set(map(int, output.split(b',')))
+
+        self.assertEqual(remaining_fds & open_fds, open_fds,
+                         "Some fds were closed")
+
+        p = subprocess.Popen([sys.executable, fd_status],
+                             stdout=subprocess.PIPE, close_fds=True)
+        output, ignored = p.communicate()
+        remaining_fds = set(map(int, output.split(b',')))
+
+        self.assertFalse(remaining_fds & open_fds,
+                         "Some fds were left open")
+        self.assertIn(1, remaining_fds, "Subprocess failed")
+
 
 @unittest.skipUnless(mswindows, "Windows specific tests")
 class Win32ProcessTestCase(BaseTestCase):
index 4c7c28bc9cce57f9ec19b438a79f8aaa5daa1155..4ce952f45da5bbec56e36d356882818f01e5ff29 100644 (file)
@@ -888,7 +888,7 @@ MACHDEPS=   $(PLATDIR) $(EXTRAPLATDIR)
 XMLLIBSUBDIRS=  xml xml/dom xml/etree xml/parsers xml/sax
 LIBSUBDIRS=    tkinter tkinter/test tkinter/test/test_tkinter \
                 tkinter/test/test_ttk site-packages test \
-               test/decimaltestdata test/xmltestdata \
+               test/decimaltestdata test/xmltestdata test/subprocessdata \
                test/tracedmodules test/encoded_modules \
                concurrent concurrent/futures encodings \
                email email/mime email/test email/test/data \
index 36da2e5fa954697bdcabad84ec55526283e45583..0b5e54470fa8d50deb33f961d67ad51927e93fe2 100644 (file)
@@ -1,6 +1,10 @@
 /* Authors: Gregory P. Smith & Jeffrey Yasskin */
 #include "Python.h"
+#ifdef HAVE_PIPE2
+#define _GNU_SOURCE
+#endif
 #include <unistd.h>
+#include <fcntl.h>
 
 
 #define POSIX_CALL(call)   if ((call) == -1) goto error
@@ -398,6 +402,45 @@ Returns: the child process's PID.\n\
 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.");
+
+static PyObject *
+subprocess_cloexec_pipe(PyObject *self, PyObject *noargs)
+{
+    int fds[2];
+    int res;
+#ifdef HAVE_PIPE2
+    Py_BEGIN_ALLOW_THREADS
+    res = pipe2(fds, O_CLOEXEC);
+    Py_END_ALLOW_THREADS
+#else
+    /* 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) {
+        oldflags = fcntl(fds[0], F_GETFD, 0);
+        if (oldflags < 0) res = oldflags;
+    }
+    if (res == 0)
+        res = fcntl(fds[0], F_SETFD, oldflags | FD_CLOEXEC);
+
+    if (res == 0) {
+        oldflags = fcntl(fds[1], F_GETFD, 0);
+        if (oldflags < 0) res = oldflags;
+    }
+    if (res == 0)
+        res = fcntl(fds[1], F_SETFD, oldflags | FD_CLOEXEC);
+#endif
+    if (res != 0)
+        return PyErr_SetFromErrno(PyExc_OSError);
+    return Py_BuildValue("(ii)", fds[0], fds[1]);
+}
 
 /* module level code ********************************************************/
 
@@ -407,6 +450,7 @@ PyDoc_STRVAR(module_doc,
 
 static PyMethodDef module_methods[] = {
     {"fork_exec", subprocess_fork_exec, METH_VARARGS, subprocess_fork_exec_doc},
+    {"cloexec_pipe", subprocess_cloexec_pipe, METH_NOARGS, subprocess_cloexec_pipe_doc},
     {NULL, NULL}  /* sentinel */
 };
 
index 2288cf351989c1a0576b007a58e5cb2dee479e04..4cebad210b6452ab44ae993d62c41d7340757277 100644 (file)
@@ -1035,6 +1035,8 @@ def add_files(db):
         if dir=='xmltestdata':
             lib.glob("*.xml")
             lib.add_file("test.xml.out")
+        if dir=='subprocessdata':
+            lib.glob("*.py")
         if dir=='output':
             lib.glob("test_*")
         if dir=='sndhdrdata':
index 03181332d7967f9aea3106425920ec054b8070cd..fe030b37a7f380166275fc3d7ce745a9d33b676b 100644 (file)
@@ -4235,7 +4235,7 @@ case $ac_sys_system in
   OSF*) AC_MSG_ERROR(OSF* systems are deprecated unless somebody volunteers. Check http://bugs.python.org/issue8606) ;;
 esac
 
-
+AC_CHECK_FUNC(pipe2, AC_DEFINE(HAVE_PIPE2, 1, [Define if the OS supports pipe2()]), )
 
 AC_SUBST(THREADHEADERS)