commit:     c2a9850a25b2f32a25b43ef30189cd6657f397ad
Author:     Zac Medico <zmedico <AT> gentoo <DOT> org>
AuthorDate: Sat Dec 29 06:56:40 2018 +0000
Commit:     Zac Medico <zmedico <AT> gentoo <DOT> org>
CommitDate: Fri Jan  4 03:04:49 2019 +0000
URL:        https://gitweb.gentoo.org/proj/portage.git/commit/?id=c2a9850a

process.spawn: validate unshare calls (bug 673900)

In order to prevent failed unshare calls from corrupting the state
of an essential process, validate the relevant unshare call in a
short-lived subprocess. An unshare call is considered valid if it
successfully executes in a short-lived subprocess.

Bug: https://bugs.gentoo.org/673900
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>

 lib/portage/process.py | 159 +++++++++++++++++++++++++++++++++++++++++--------
 1 file changed, 133 insertions(+), 26 deletions(-)

diff --git a/lib/portage/process.py b/lib/portage/process.py
index ce3e42a8f..7103b6b31 100644
--- a/lib/portage/process.py
+++ b/lib/portage/process.py
@@ -6,6 +6,7 @@
 import atexit
 import errno
 import fcntl
+import multiprocessing
 import platform
 import signal
 import socket
@@ -338,11 +339,29 @@ def spawn(mycommand, env=None, opt_name=None, 
fd_pipes=None, returnpid=False,
                fd_pipes[1] = pw
                fd_pipes[2] = pw
 
-       # This caches the libc library lookup in the current
-       # process, so that it's only done once rather than
-       # for each child process.
+       # This caches the libc library lookup and _unshare_validator results
+       # in the current process, so that results are cached for use in
+       # child processes.
+       unshare_flags = 0
        if unshare_net or unshare_ipc or unshare_mount or unshare_pid:
-               find_library("c")
+               # from /usr/include/bits/sched.h
+               CLONE_NEWNS = 0x00020000
+               CLONE_NEWIPC = 0x08000000
+               CLONE_NEWPID = 0x20000000
+               CLONE_NEWNET = 0x40000000
+
+               if unshare_net:
+                       unshare_flags |= CLONE_NEWNET
+               if unshare_ipc:
+                       unshare_flags |= CLONE_NEWIPC
+               if unshare_mount:
+                       # NEWNS = mount namespace
+                       unshare_flags |= CLONE_NEWNS
+               if unshare_pid:
+                       # we also need mount namespace for slave /proc
+                       unshare_flags |= CLONE_NEWPID | CLONE_NEWNS
+
+               _unshare_validate(unshare_flags)
 
        # Force instantiation of portage.data.userpriv_groups before the
        # fork, so that the result is cached in the main process.
@@ -358,7 +377,7 @@ def spawn(mycommand, env=None, opt_name=None, 
fd_pipes=None, returnpid=False,
                                _exec(binary, mycommand, opt_name, fd_pipes,
                                        env, gid, groups, uid, umask, cwd, 
pre_exec, close_fds,
                                        unshare_net, unshare_ipc, 
unshare_mount, unshare_pid,
-                                       cgroup)
+                                       unshare_flags, cgroup)
                        except SystemExit:
                                raise
                        except Exception as e:
@@ -430,7 +449,7 @@ def spawn(mycommand, env=None, opt_name=None, 
fd_pipes=None, returnpid=False,
 def _exec(binary, mycommand, opt_name, fd_pipes,
        env, gid, groups, uid, umask, cwd,
        pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, 
unshare_pid,
-       cgroup):
+       unshare_flags, cgroup):
 
        """
        Execute a given binary with options
@@ -466,6 +485,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes,
        @type unshare_mount: Boolean
        @param unshare_pid: If True, PID ns will be unshared from the spawned 
process
        @type unshare_pid: Boolean
+       @param unshare_flags: Flags for the unshare(2) function
+       @type unshare_flags: Integer
        @param cgroup: CGroup path to bind the process to
        @type cgroup: String
        @rtype: None
@@ -527,28 +548,19 @@ def _exec(binary, mycommand, opt_name, fd_pipes,
                if filename is not None:
                        libc = LoadLibrary(filename)
                        if libc is not None:
-                               # from /usr/include/bits/sched.h
-                               CLONE_NEWNS = 0x00020000
-                               CLONE_NEWIPC = 0x08000000
-                               CLONE_NEWPID = 0x20000000
-                               CLONE_NEWNET = 0x40000000
-
-                               flags = 0
-                               if unshare_net:
-                                       flags |= CLONE_NEWNET
-                               if unshare_ipc:
-                                       flags |= CLONE_NEWIPC
-                               if unshare_mount:
-                                       # NEWNS = mount namespace
-                                       flags |= CLONE_NEWNS
-                               if unshare_pid:
-                                       # we also need mount namespace for 
slave /proc
-                                       flags |= CLONE_NEWPID | CLONE_NEWNS
-
                                try:
-                                       if libc.unshare(flags) != 0:
+                                       # Since a failed unshare call could 
corrupt process
+                                       # state, first validate that the call 
can succeed.
+                                       # The parent process should call 
_unshare_validate
+                                       # before it forks, so that all child 
processes can
+                                       # reuse _unshare_validate results that 
have been
+                                       # cached by the parent process.
+                                       errno_value = 
_unshare_validate(unshare_flags)
+                                       if errno_value == 0 and 
libc.unshare(unshare_flags) != 0:
+                                               errno_value = ctypes.get_errno()
+                                       if errno_value != 0:
                                                writemsg("Unable to unshare: 
%s\n" % (
-                                                       
errno.errorcode.get(ctypes.get_errno(), '?')),
+                                                       
errno.errorcode.get(errno_value, '?')),
                                                        noiselevel=-1)
                                        else:
                                                if unshare_pid:
@@ -626,6 +638,101 @@ def _exec(binary, mycommand, opt_name, fd_pipes,
        # And switch to the new process.
        os.execve(binary, myargs, env)
 
+
+class _unshare_validator(object):
+       """
+       In order to prevent failed unshare calls from corrupting the state
+       of an essential process, validate the relevant unshare call in a
+       short-lived subprocess. An unshare call is considered valid if it
+       successfully executes in a short-lived subprocess.
+       """
+
+       def __init__(self):
+               self._results = {}
+
+       def __call__(self, flags):
+               """
+               Validate unshare with the given flags. Results are cached.
+
+               @rtype: int
+               @returns: errno value, or 0 if no error occurred.
+               """
+
+               try:
+                       return self._results[flags]
+               except KeyError:
+                       result = self._results[flags] = self._validate(flags)
+                       return result
+
+       @classmethod
+       def _validate(cls, flags):
+               """
+               Perform validation.
+
+               @param flags: unshare flags
+               @type flags: int
+               @rtype: int
+               @returns: errno value, or 0 if no error occurred.
+               """
+               filename = find_library("c")
+               if filename is None:
+                       return errno.ENOTSUP
+
+               libc = LoadLibrary(filename)
+               if libc is None:
+                       return errno.ENOTSUP
+
+               parent_pipe, subproc_pipe = multiprocessing.Pipe(duplex=False)
+
+               proc = multiprocessing.Process(
+                       target=cls._run_subproc,
+                       args=(subproc_pipe, cls._validate_subproc, 
(libc.unshare, flags)))
+               proc.start()
+               subproc_pipe.close()
+
+               result = parent_pipe.recv()
+               parent_pipe.close()
+               proc.join()
+
+               return result
+
+       @staticmethod
+       def _run_subproc(subproc_pipe, target, args=(), kwargs={}):
+               """
+               Call function and send return value to parent process.
+
+               @param subproc_pipe: connection to parent process
+               @type subproc_pipe: multiprocessing.Connection
+               @param target: target is the callable object to be invoked
+               @type target: callable
+               @param args: the argument tuple for the target invocation
+               @type args: tuple
+               @param kwargs: dictionary of keyword arguments for the target 
invocation
+               @type kwargs: dict
+               """
+               subproc_pipe.send(target(*args, **kwargs))
+               subproc_pipe.close()
+
+       @staticmethod
+       def _validate_subproc(unshare, flags):
+               """
+               Perform validation. Calls to this method must be isolated in a
+               subprocess, since the unshare function is called for purposes of
+               validation.
+
+               @param unshare: unshare function
+               @type unshare: callable
+               @param flags: unshare flags
+               @type flags: int
+               @rtype: int
+               @returns: errno value, or 0 if no error occurred.
+               """
+               return 0 if unshare(flags) == 0 else ctypes.get_errno()
+
+
+_unshare_validate = _unshare_validator()
+
+
 def _setup_pipes(fd_pipes, close_fds=True, inheritable=None):
        """Setup pipes for a forked process.
 

Reply via email to