Add FEATURES=network-sandbox support, bug #481450
authorMichał Górny <mgorny@gentoo.org>
Sat, 17 Aug 2013 10:28:05 +0000 (12:28 +0200)
committerZac Medico <zmedico@gentoo.org>
Sat, 17 Aug 2013 20:38:53 +0000 (13:38 -0700)
This way, only privileged phases (pkg_* and src_unpack) have network
access during the ebuild run. All of the src_* phases are completely
detached from host's network interfaces.

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

index 63e2097ea35fd05bc14a8b78f57fb9c8f678481f..461172c9b1da681d32853fbda19019611b3007c3 100644 (file)
@@ -415,6 +415,10 @@ isn't a symlink to /usr/lib64. To find the bad packages, we have a
 portage feature called \fImultilib\-strict\fR. It will prevent emerge
 from putting 64bit libraries into anything other than (/usr)/lib64.
 .TP
+.B network\-sandbox
+Isolate the ebuild phase functions from host network interfaces.
+Supported only on Linux. Requires network namespace support in kernel.
+.TP
 .B news
 Enable GLEP 42 news support. See
 \fIhttp://www.gentoo.org/proj/en/glep/glep-0042.html\fR.
index bd55cb1a86183bfe616e7b2878001e228f072116..cde0079371d3912b724de12d4b9ece80a986b5e2 100644 (file)
@@ -104,7 +104,8 @@ SUPPORTED_FEATURES       = frozenset([
                            "fail-clean", "force-mirror", "force-prefix", "getbinpkg",
                            "installsources", "keeptemp", "keepwork", "fixlafiles", "lmirror",
                            "merge-sync",
-                           "metadata-transfer", "mirror", "multilib-strict", "news",
+                           "metadata-transfer", "mirror", "multilib-strict",
+                           "network-sandbox", "news",
                            "noauto", "noclean", "nodoc", "noinfo", "noman",
                            "nostrip", "notitles", "parallel-fetch", "parallel-install",
                            "prelink-checksums", "preserve-libs",
index 1cf5dc6aec553b949358f9ee5fd8919688a29a5c..a35e717191c3dee76fae854934bce0287e30b0da 100644 (file)
@@ -12,6 +12,7 @@ import io
 from itertools import chain
 import logging
 import os as _os
+import platform
 import pwd
 import re
 import signal
@@ -81,6 +82,15 @@ _unsandboxed_phases = frozenset([
        "prerm", "setup"
 ])
 
+# phases in which networking access is allowed
+_networked_phases = frozenset([
+       # for VCS fetching
+       "unpack",
+       # for IPC
+       "setup", "pretend",
+       "preinst", "postinst", "prerm", "postrm",
+])
+
 _phase_func_map = {
        "config": "pkg_config",
        "setup": "pkg_setup",
@@ -110,6 +120,8 @@ def _doebuild_spawn(phase, settings, actionmap=None, **kwargs):
 
        if phase in _unsandboxed_phases:
                kwargs['free'] = True
+       if phase in _networked_phases:
+               kwargs['networked'] = True
 
        if phase == 'depend':
                kwargs['droppriv'] = 'userpriv' in settings.features
@@ -1387,7 +1399,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, **keywords):
+def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakeroot=0, networked=0, **keywords):
        """
        Spawn a subprocess with extra portage-specific options.
        Optiosn include:
@@ -1417,6 +1429,8 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        @type sesandbox: Boolean
        @param fakeroot: Run this command with faked root privileges
        @type fakeroot: Boolean
+       @param networked: Run this command with networking access enabled
+       @type networked: Boolean
        @param keywords: Extra options encoded as a dict, to be passed to spawn
        @type keywords: Dictionary
        @rtype: Integer
@@ -1444,6 +1458,11 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
                        break
 
        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
+
        # TODO: Enable fakeroot to be used together with droppriv.  The
        # fake ownership/permissions will have to be converted to real
        # permissions in the merge phase.
index 5f6a172e0008b153e0c68749289c9af55401bb59..a7464c71d74ec40bf61af127f859659edc673538 100644 (file)
@@ -5,8 +5,11 @@
 
 import atexit
 import errno
+import fcntl
 import platform
 import signal
+import socket
+import struct
 import sys
 import traceback
 import os as _os
@@ -21,6 +24,7 @@ portage.proxy.lazyimport.lazyimport(globals(),
 
 from portage.const import BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY
 from portage.exception import CommandNotFound
+from portage.util._ctypes import find_library, LoadLibrary, ctypes
 
 try:
        import resource
@@ -180,7 +184,7 @@ 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):
+          path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False):
        """
        Spawns a given command.
        
@@ -213,7 +217,9 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
        @param close_fds: If True, then close all file descriptors except those
                referenced by fd_pipes (default is True).
        @type close_fds: Boolean
-       
+       @param unshare_net: If True, networking will be unshared from the spawned process
+       @type unshare_net: Boolean
+
        logfile requires stdout and stderr to be assigned to this process (ie not pointed
           somewhere else.)
        
@@ -276,6 +282,12 @@ def spawn(mycommand, env={}, 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.
+       if unshare_net:
+               find_library("c")
+
        parent_pid = os.getpid()
        pid = None
        try:
@@ -284,7 +296,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
                if pid == 0:
                        try:
                                _exec(binary, mycommand, opt_name, fd_pipes,
-                                       env, gid, groups, uid, umask, pre_exec, close_fds)
+                                       env, gid, groups, uid, umask, pre_exec, close_fds,
+                                       unshare_net)
                        except SystemExit:
                                raise
                        except Exception as e:
@@ -354,7 +367,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):
+       pre_exec, close_fds, unshare_net):
 
        """
        Execute a given binary with options
@@ -379,10 +392,12 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
        @type umask: Integer
        @param pre_exec: A function to be called with no arguments just prior to the exec call.
        @type pre_exec: callable
+       @param unshare_net: If True, networking will be unshared from the spawned process
+       @type unshare_net: Boolean
        @rtype: None
        @return: Never returns (calls os.execve)
        """
-       
+
        # If the process we're creating hasn't been given a name
        # assign it the name of the executable.
        if not opt_name:
@@ -415,6 +430,35 @@ 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:
+               filename = find_library("c")
+               if filename is not None:
+                       libc = LoadLibrary(filename)
+                       if libc is not None:
+                               CLONE_NEWNET = 0x40000000
+                               try:
+                                       if libc.unshare(CLONE_NEWNET) != 0:
+                                               writemsg("Unable to unshare network: %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)
+                               except AttributeError:
+                                       # unshare() not supported by libc
+                                       pass
+
        # Set requested process permissions.
        if gid:
                # Cast proxies to int, in case it matters.