From 7bd79cee2b5fd8555e0aa256f8ca5531bd52ada7 Mon Sep 17 00:00:00 2001 From: Yaniv Bronhaim Date: Mon, 21 Jan 2013 15:28:02 +0200 Subject: Initial commit Signed-off-by: Yaniv Bronhaim --- cpopen.py | 73 +++++++++++++ cpopen.spec | 35 ++++++ createprocess.c | 329 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 13 +++ 4 files changed, 450 insertions(+) create mode 100644 cpopen.py create mode 100644 cpopen.spec create mode 100644 createprocess.c create mode 100644 setup.py diff --git a/cpopen.py b/cpopen.py new file mode 100644 index 0000000..2e5fbd6 --- /dev/null +++ b/cpopen.py @@ -0,0 +1,73 @@ +# +# Copyright 2012 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Refer to the README and COPYING files for full details of the license +# + +""" +Python's implementation of Popen forks back to python before execing. +Forking a python proc is a very complex and volatile process. + +This is a simpler method of execing that doesn't go back to python after +forking. This allows for faster safer exec. +""" + +import os +from subprocess import Popen, PIPE + +from createprocess import createProcess + + +class cpopen(Popen): + def __init__(self, args, close_fds=False, cwd=None, env=None): + if not isinstance(args, list): + args = list(args) + + if env is not None and not isinstance(env, list): + env = list(("=".join(item) for item in env.iteritems())) + + Popen.__init__(self, args, + close_fds=close_fds, cwd=cwd, env=env, + stdin=PIPE, stdout=PIPE, + stderr=PIPE) + + def _execute_child(self, args, executable, preexec_fn, close_fds, + cwd, env, universal_newlines, + startupinfo, creationflags, shell, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite): + + try: + pid, stdin, stdout, stderr = createProcess(args, close_fds, + p2cread, p2cwrite, + c2pread, c2pwrite, + errread, errwrite, + cwd, env) + + self.pid = pid + self._closed = False + self._returncode = None + except: + os.close(p2cwrite) + os.close(errread) + os.close(c2pread) + raise + finally: + os.close(p2cread) + os.close(errwrite) + os.close(c2pwrite) diff --git a/cpopen.spec b/cpopen.spec new file mode 100644 index 0000000..83fe09d --- /dev/null +++ b/cpopen.spec @@ -0,0 +1,35 @@ +%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} +%{!?python_ver: %global python_ver %(%{__python} -c "import sys ; print sys.version[:3]")} + +Name: python-cpopen +Version: 1.0 +Release: 1%{?dist} +Summary: Creates a subprocess in simpler safer manner + +License: GPLv2+ +Group: System Environment/Libraries +URL: http://pypi.python.org/pypi/cpopen +Source0: http://pypi.python.org/packages/source/c/cpopen/cpopen-%{version}.tar.gz + +BuildRequires: python2 + +%description +Python package for creating subprocess in simpler and safer manner by using C code. + +%prep +%setup -q -n cpopen-%{version} + +%build + + +%install +%{__python} setup.py install --root $RPM_BUILD_ROOT --install-lib %{python_sitelib} + +%files +%{python_sitelib}/createprocess.so* +%{python_sitelib}/cpopen.py* +%{python_sitelib}/cpopen-%{version}-py*.egg-info + +%changelog +* Sun Jan 20 2013 Yaniv Bronhaim 1.0 +- Initial take diff --git a/createprocess.c b/createprocess.c new file mode 100644 index 0000000..499bed6 --- /dev/null +++ b/createprocess.c @@ -0,0 +1,329 @@ +/* +* Copyright 2012 Red Hat, Inc. +* +* This program is free software; you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation; either version 2 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +* +* Refer to the README and COPYING files for full details of the license +*/ + +#include + +#include +#include +#include +#include + +static PyObject *createProcess(PyObject *self, PyObject *args); +static PyMethodDef CreateProcessMethods[]; +static void closeFDs(int errnofd); + +/* Python boilerplate */ +static PyMethodDef +CreateProcessMethods[] = { + {"createProcess", createProcess, METH_VARARGS, + "Execute a command."}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + +PyMODINIT_FUNC +initcreateprocess(void) +{ + PyObject *m; + + m = Py_InitModule("createprocess", CreateProcessMethods); + + // In the future put other init code after this condition. + if (m == NULL) + return; +} + +/* Just like close() but retries on interrupt */ +static int +safeClose(int fd) { + int rv; + +retry: + rv = close(fd); + if ((rv < 0) && (errno == EINTR)) { + goto retry; + } + + return rv; +} + +static int +setCloseOnExec(int fd) { + int flags; + + flags = fcntl(fd, F_GETFD); + if (flags == -1) { + return -1; + } + + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + return -1; + } + + return 0; +} + +/* Closes all open FDs except for stdin, stdout and stderr */ +static void +closeFDs(int errnofd) { + DIR *dp; + int dfd; + struct dirent *ep; + int fdNum = -1; + + dfd = open("/proc/self/fd/", O_RDONLY); + dp = fdopendir(dfd); + while ((ep = readdir(dp))) { + if(sscanf(ep->d_name, "%d", &fdNum) < 1) { + continue; + } + + if (fdNum < 3) { + continue; + } + + if (fdNum == dfd) { + continue; + } + + if (fdNum == errnofd) { + continue; + } + + safeClose(fdNum); + } + + closedir(dp); + safeClose(dfd); +} + +static void +freeStringArray(char** arr) { + char** item; + for (item = arr; *item != NULL; item++) { + PyMem_Free(*item); + } + + free(arr); +} + +/* Copies the strings from a python list to a null terminated array. + * The strings are shallow copied and are owned by python. + * Don't keep this array after the call. + * + * Returns a NULL terminated array of null strings. On error returns NULL and + * sets the python error accordingly + */ +static char** +pyListToArray(PyObject* list, int checkIfEmpty) { + int argn; + int i; + char** argv; + + if (!PyList_Check(list)) { + PyErr_SetString(PyExc_TypeError, "Argument must be a python list"); + return NULL; + } + + argn = PyList_Size(list); + if ((checkIfEmpty) && (argn < 1)) { + PyErr_SetString(PyExc_ValueError, "List must not be empty"); + return NULL; + } + + argv = calloc(argn + 1, sizeof(char*)); + if (!argv) { + PyErr_SetFromErrno(PyExc_OSError); + return NULL; + } + + for (i = 0; i < argn; i++) { + if (!PyArg_Parse(PyList_GetItem(list, i), + "et;", + Py_FileSystemDefaultEncoding, + &argv[i])) { + PyErr_SetString(PyExc_TypeError, + "createProcess() arg 2 must contain only strings"); + goto fail; + } + } + + return argv; + +fail: + freeStringArray(argv); + return NULL; +} + +/* Python's implementation of Popen forks back to python before execing. + * Forking a python proc is a very complex and volatile process. + * + * This is a simpler method of execing that doesn't go back to python after + * forking. This allows for faster safer exec. + * + * return NULL on error and sets the python error accordingly. + */ +static PyObject * +createProcess(PyObject *self, PyObject *args) +{ + int cpid; + + int outfd[2] = {-1, -1}; + int in1fd[2] = {-1, -1}; + int in2fd[2] = {-1, -1}; + + int errnofd[2] = {-1, -1}; + int childErrno = 0; + + PyObject* pyArgList; + PyObject* pyEnvList; + const char* cwd; + int close_fds = 0; + + char** argv = NULL; + char** envp = NULL; + + if (!PyArg_ParseTuple(args, "O!iiiiiiizO:createProcess;", + &PyList_Type, &pyArgList, &close_fds, + &outfd[0], &outfd[1], + &in1fd[0], &in1fd[1], + &in2fd[0], &in2fd[1], + &cwd, &pyEnvList)) { + return NULL; + } + + argv = pyListToArray(pyArgList, 1); + if (!argv) { + goto fail; + } + + if (PyList_Check(pyEnvList)) { + envp = pyListToArray(pyEnvList, 0); + if (!envp) { + goto fail; + } + } + + if(pipe(errnofd) < 0) { + PyErr_SetFromErrno(PyExc_OSError); + goto fail; + } + +try_fork: + cpid = fork(); + if (cpid < 0) { + if (errno == EAGAIN || + errno == EINTR ) { + goto try_fork; + } + + PyErr_SetFromErrno(PyExc_OSError); + goto fail; + } + + if (!cpid) { + safeClose(0); + safeClose(1); + safeClose(2); + + dup2(outfd[0], 0); + dup2(in1fd[1], 1); + dup2(in2fd[1], 2); + + safeClose(outfd[0]); + safeClose(outfd[1]); + safeClose(in1fd[0]); + safeClose(in1fd[1]); + safeClose(in2fd[0]); + safeClose(in2fd[1]); + safeClose(errnofd[0]); + if (setCloseOnExec(errnofd[1]) < 0) { + goto sendErrno; + } + + if (close_fds) { + closeFDs(errnofd[1]); + } + + if (cwd) { + if (chdir(cwd) < 0) { + goto sendErrno; + } + setenv("PWD", cwd, 1); + } +exec: + if (envp) { + execvpe(argv[0], argv, envp); + } else { + execvp(argv[0], argv); + } + + if (errno == EINTR || + errno == EAGAIN ) + { + goto exec; + } +sendErrno: + if (write(errnofd[1], &errno, sizeof(int)) < 0) { + exit(errno); + } + exit(-1); + } + + safeClose(errnofd[1]); + errnofd[1] = -1; + if (read(errnofd[0], &childErrno, sizeof(int)) == sizeof(int)) { + errno = childErrno; + PyErr_SetFromErrno(PyExc_OSError); + goto fail; + } + + safeClose(errnofd[0]); + errnofd[0] = -1; + + /* From this point errors shouldn't occur, if they do something is very + * very very wrong */ + + freeStringArray(argv); + + if (envp) { + freeStringArray(envp); + } + + return Py_BuildValue("(iiii)", cpid, outfd[1], in1fd[0], in2fd[0]); + +fail: + if (argv) { + freeStringArray(argv); + } + + if (envp) { + freeStringArray(envp); + } + + if (errnofd[0] >= 0) { + safeClose(errnofd[0]); + } + + if (errnofd[1] >= 0) { + safeClose(errnofd[1]); + } + + return NULL; +} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ee8c094 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup, Extension + +module1 = Extension('createprocess', + sources=['createprocess.c']) + +setup(name='cpopen', + version='1.0', + description='Creates a subprocess in simpler safer manner', + py_modules=['cpopen'], + author='Yaniv Bronhaim', + author_email='ybronhei@redhat.com', + url='redhat.com', + ext_modules=[module1]) -- cgit