On 12/30/18 12:44 AM, Michał Górny wrote:
> On Sat, 2018-12-29 at 04:49 -0800, Zac Medico wrote:
>> 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.
>>  
> 
> Can't we detect the failure in normally spawned ebuild process,
> and restart it with unsharing disabled?  Possibly cache the result to
> avoid having to restart everything afterwards.

We'd need an IPC mechanism to communicate the failure to the parent
process so that it could retry. However, _exec is intended to exec an
arbitrary child process, so it's not an appropriate place for IPC.
-- 
Thanks,
Zac

Attachment: signature.asc
Description: OpenPGP digital signature

Reply via email to