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. -- Best regards, Michał Górny
signature.asc
Description: This is a digitally signed message part