From: Michał Górny Date: Sat, 17 Aug 2013 10:28:05 +0000 (+0200) Subject: Add FEATURES=network-sandbox support, bug #481450 X-Git-Tag: v2.2.1~15 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=39db5201e087156ed46f6cac4dc9a69a2f3cc81c;p=portage.git Add FEATURES=network-sandbox support, bug #481450 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. --- diff --git a/man/make.conf.5 b/man/make.conf.5 index 63e2097ea..461172c9b 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -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. diff --git a/pym/portage/const.py b/pym/portage/const.py index bd55cb1a8..cde007937 100644 --- a/pym/portage/const.py +++ b/pym/portage/const.py @@ -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", diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index 1cf5dc6ae..a35e71719 100644 --- a/pym/portage/package/ebuild/doebuild.py +++ b/pym/portage/package/ebuild/doebuild.py @@ -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. diff --git a/pym/portage/process.py b/pym/portage/process.py index 5f6a172e0..a7464c71d 100644 --- a/pym/portage/process.py +++ b/pym/portage/process.py @@ -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.