Add FEATURES=ipc-sandbox to isolate IPC from host.
authorMichał Górny <mgorny@gentoo.org>
Sun, 18 Aug 2013 23:22:59 +0000 (01:22 +0200)
committerZac Medico <zmedico@gentoo.org>
Sun, 18 Aug 2013 23:31:01 +0000 (16:31 -0700)
This way, only privileged phases (pkg_*) can use *nix IPC to communicate
with host applications. src_* use private IPC namespace.

man/make.conf.5
pym/portage/const.py
pym/portage/package/ebuild/doebuild.py
pym/portage/process.py

index 461172c9b1da681d32853fbda19019611b3007c3..91817aec5027b67aab14cf549b0fe02e0769b475 100644 (file)
@@ -385,6 +385,10 @@ would otherwise be useless with prefix configurations. This brings
 compatibility with the prefix branch of portage, which also supports EPREFIX
 for all EAPIs (for obvious reasons).
 .TP
+.B ipc\-sandbox
+Isolate the ebuild phase functions from host IPC namespace. Supported
+only on Linux. Requires network namespace support in kernel.
+.TP
 .B lmirror
 When \fImirror\fR is enabled in \fBFEATURES\fR, fetch files even
 when \fImirror\fR is also in the \fBebuild\fR(5) \fBRESTRICT\fR variable.
index cde0079371d3912b724de12d4b9ece80a986b5e2..88c199b2ce8c9d3097b480e0626dea52e632d545 100644 (file)
@@ -102,7 +102,8 @@ SUPPORTED_FEATURES       = frozenset([
                            "digest", "distcc", "distcc-pump", "distlocks",
                            "downgrade-backup", "ebuild-locks", "fakeroot",
                            "fail-clean", "force-mirror", "force-prefix", "getbinpkg",
-                           "installsources", "keeptemp", "keepwork", "fixlafiles", "lmirror",
+                           "installsources", "ipc-sandbox",
+                           "keeptemp", "keepwork", "fixlafiles", "lmirror",
                            "merge-sync",
                            "metadata-transfer", "mirror", "multilib-strict",
                            "network-sandbox", "news",
index a35e717191c3dee76fae854934bce0287e30b0da..2d26d2cbe4abc6bf433db560f76df32b6c577a05 100644 (file)
@@ -82,14 +82,18 @@ _unsandboxed_phases = frozenset([
        "prerm", "setup"
 ])
 
+# phases in which IPC with host is allowed
+_ipc_phases = frozenset([
+       "setup", "pretend",
+       "preinst", "postinst", "prerm", "postrm",
+])
+
 # phases in which networking access is allowed
 _networked_phases = frozenset([
        # for VCS fetching
        "unpack",
-       # for IPC
-       "setup", "pretend",
-       "preinst", "postinst", "prerm", "postrm",
-])
+       # + for network-bound IPC
+] + list(_ipc_phases))
 
 _phase_func_map = {
        "config": "pkg_config",
@@ -120,6 +124,8 @@ def _doebuild_spawn(phase, settings, actionmap=None, **kwargs):
 
        if phase in _unsandboxed_phases:
                kwargs['free'] = True
+       if phase in _ipc_phases:
+               kwargs['ipc'] = True
        if phase in _networked_phases:
                kwargs['networked'] = True
 
@@ -1399,7 +1405,7 @@ def _validate_deps(mysettings, myroot, mydo, mydbapi):
 
 # XXX This would be to replace getstatusoutput completely.
 # XXX Issue: cannot block execution. Deadlock condition.
-def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakeroot=0, networked=0, **keywords):
+def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakeroot=0, networked=0, ipc=0, **keywords):
        """
        Spawn a subprocess with extra portage-specific options.
        Optiosn include:
@@ -1431,6 +1437,8 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        @type fakeroot: Boolean
        @param networked: Run this command with networking access enabled
        @type networked: Boolean
+       @param ipc: Run this command with host IPC access enabled
+       @type ipc: Boolean
        @param keywords: Extra options encoded as a dict, to be passed to spawn
        @type keywords: Dictionary
        @rtype: Integer
@@ -1459,9 +1467,12 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
 
        features = mysettings.features
 
-       # Unshare network namespace to keep ebuilds sanitized
-       if not networked and uid == 0 and platform.system() == 'Linux' and "network-sandbox" in features:
-               keywords['unshare_net'] = True
+       # Use Linux namespaces if available
+       if uid == 0 and platform.system() == 'Linux':
+               if not networked and "network-sandbox" in features:
+                       keywords['unshare_net'] = True
+               if not ipc and "ipc-sandbox" in features:
+                       keywords['unshare_ipc'] = True
 
        # TODO: Enable fakeroot to be used together with droppriv.  The
        # fake ownership/permissions will have to be converted to real
index 6a60dec553cb09ecd003f1f6d82079c1a9d45df9..22c6a882329c8f0dc08fc9f3f5ffca903a361624 100644 (file)
@@ -184,7 +184,8 @@ def cleanup():
 
 def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
           uid=None, gid=None, groups=None, umask=None, logfile=None,
-          path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False):
+          path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False,
+          unshare_ipc=False):
        """
        Spawns a given command.
        
@@ -219,6 +220,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
        @type close_fds: Boolean
        @param unshare_net: If True, networking will be unshared from the spawned process
        @type unshare_net: Boolean
+       @param unshare_ipc: If True, IPC will be unshared from the spawned process
+       @type unshare_ipc: Boolean
 
        logfile requires stdout and stderr to be assigned to this process (ie not pointed
           somewhere else.)
@@ -285,7 +288,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
        # This caches the libc library lookup in the current
        # process, so that it's only done once rather than
        # for each child process.
-       if unshare_net:
+       if unshare_net or unshare_ipc:
                find_library("c")
 
        parent_pid = os.getpid()
@@ -297,7 +300,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
                        try:
                                _exec(binary, mycommand, opt_name, fd_pipes,
                                        env, gid, groups, uid, umask, pre_exec, close_fds,
-                                       unshare_net)
+                                       unshare_net, unshare_ipc)
                        except SystemExit:
                                raise
                        except Exception as e:
@@ -367,7 +370,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
        return 0
 
 def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
-       pre_exec, close_fds, unshare_net):
+       pre_exec, close_fds, unshare_net, unshare_ipc):
 
        """
        Execute a given binary with options
@@ -394,6 +397,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
        @type pre_exec: callable
        @param unshare_net: If True, networking will be unshared from the spawned process
        @type unshare_net: Boolean
+       @param unshare_ipc: If True, IPC will be unshared from the spawned process
+       @type unshare_ipc: Boolean
        @rtype: None
        @return: Never returns (calls os.execve)
        """
@@ -430,32 +435,41 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
 
        _setup_pipes(fd_pipes, close_fds=close_fds)
 
-       # Unshare network (while still uid==0)
-       if unshare_net:
+       # Unshare (while still uid==0)
+       if unshare_net or unshare_ipc:
                filename = find_library("c")
                if filename is not None:
                        libc = LoadLibrary(filename)
                        if libc is not None:
+                               CLONE_NEWIPC = 0x08000000
                                CLONE_NEWNET = 0x40000000
+
+                               flags = 0
+                               if unshare_net:
+                                       flags |= CLONE_NEWNET
+                               if unshare_ipc:
+                                       flags |= CLONE_NEWIPC
+
                                try:
-                                       if libc.unshare(CLONE_NEWNET) != 0:
-                                               writemsg("Unable to unshare network: %s\n" % (
+                                       if libc.unshare(flags) != 0:
+                                               writemsg("Unable to unshare: %s\n" % (
                                                        errno.errorcode.get(ctypes.get_errno(), '?')),
                                                        noiselevel=-1)
                                        else:
-                                               # 'up' the loopback
-                                               IFF_UP = 0x1
-                                               ifreq = struct.pack('16sh', b'lo', IFF_UP)
-                                               SIOCSIFFLAGS = 0x8914
-
-                                               sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
-                                               try:
-                                                       fcntl.ioctl(sock, SIOCSIFFLAGS, ifreq)
-                                               except IOError as e:
-                                                       writemsg("Unable to enable loopback interface: %s\n" % (
-                                                               errno.errorcode.get(e.errno, '?')),
-                                                               noiselevel=-1)
-                                               sock.close()
+                                               if unshare_net:
+                                                       # 'up' the loopback
+                                                       IFF_UP = 0x1
+                                                       ifreq = struct.pack('16sh', b'lo', IFF_UP)
+                                                       SIOCSIFFLAGS = 0x8914
+
+                                                       sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
+                                                       try:
+                                                               fcntl.ioctl(sock, SIOCSIFFLAGS, ifreq)
+                                                       except IOError as e:
+                                                               writemsg("Unable to enable loopback interface: %s\n" % (
+                                                                       errno.errorcode.get(e.errno, '?')),
+                                                                       noiselevel=-1)
+                                                       sock.close()
                                except AttributeError:
                                        # unshare() not supported by libc
                                        pass