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 <zmed...@gentoo.org>
---
 lib/portage/process.py | 130 +++++++++++++++++++++++++++++++++--------
 1 file changed, 106 insertions(+), 24 deletions(-)

diff --git a/lib/portage/process.py b/lib/portage/process.py
index ce3e42a8f..4bd6a4192 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
+       # This caches the libc library lookup and _unshare_validator results
+       # in the current process, so that it's only done once rather than
        # for each child process.
+       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_validator.instance.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,26 +548,14 @@ 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:
+                                       errno_value = 
_unshare_validator.instance.validate(unshare_flags)
+                                       if errno_value != 0:
+                                               writemsg("Unable to unshare: 
%s\n" % (
+                                                       
errno.errorcode.get(errno_value, '?')),
+                                                       noiselevel=-1)
+                                               raise AttributeError('unshare')
+                                       if libc.unshare(unshare_flags) != 0:
                                                writemsg("Unable to unshare: 
%s\n" % (
                                                        
errno.errorcode.get(ctypes.get_errno(), '?')),
                                                        noiselevel=-1)
@@ -626,6 +635,79 @@ 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.
+       """
+
+       instance = None
+
+       def __init__(self):
+               self._results = {}
+
+       def validate(self, flags):
+               """
+               Validate unshare with the given flags. Results are cached.
+
+               @rtype: int
+               @returns: errno value, or 0 if no error occurred.
+               """
+
+               if flags == 0:
+                       return 0
+
+               result = self._results.get(flags)
+               if result is not None:
+                       return result
+
+               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=self._validator_subproc,
+                       args=(libc.unshare, flags, subproc_pipe))
+               proc.start()
+               subproc_pipe.close()
+
+               result = parent_pipe.recv()
+               parent_pipe.close()
+               proc.join()
+
+               self._results[flags] = result
+               return result
+
+       def _validator_subproc(self, unshare, flags, subproc_pipe):
+               """
+               Perform validation, and send results to parent process.
+
+               @param unshare: unshare function
+               @type unshare: callable
+               @param flags: unshare flags
+               @type flags: int
+               @param subproc_pipe: connection to parent process
+               @type subproc_pipe: multiprocessing.Connection
+               """
+               if unshare(flags) != 0:
+                       subproc_pipe.send(ctypes.get_errno())
+               else:
+                       subproc_pipe.send(0)
+               subproc_pipe.close()
+
+
+_unshare_validator.instance = _unshare_validator()
+
+
 def _setup_pipes(fd_pipes, close_fds=True, inheritable=None):
        """Setup pipes for a forked process.
 
-- 
2.18.1


Reply via email to