aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZac Medico <zmedico@gentoo.org>2018-12-28 22:56:40 -0800
committerZac Medico <zmedico@gentoo.org>2019-01-03 19:04:49 -0800
commitc2a9850a25b2f32a25b43ef30189cd6657f397ad (patch)
tree08346ae914b3d9df4f0e196478d88ff5118c9c6d
parentebuild.sh: sandbox write to ${PORTAGE_TMPDIR}/portage (bug 673738) (diff)
downloadportage-c2a9850a25b2f32a25b43ef30189cd6657f397ad.tar.gz
portage-c2a9850a25b2f32a25b43ef30189cd6657f397ad.tar.bz2
portage-c2a9850a25b2f32a25b43ef30189cd6657f397ad.zip
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@gentoo.org>
-rw-r--r--lib/portage/process.py159
1 files 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.