From 587e4e0ef74575e9ed830abd6429c1325df9c151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 17 Aug 2013 12:28:05 +0200 Subject: [PATCH] Support unsharing network namespaces in ebuild. 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. --- pym/portage/package/ebuild/doebuild.py | 21 ++++++++++++++++++++- pym/portage/process.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index 1cf5dc6..59a0474 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': + 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 5f6a172..512c324 100644 --- a/pym/portage/process.py +++ b/pym/portage/process.py @@ -21,6 +21,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 +181,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 +214,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.) @@ -284,7 +287,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 +358,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 +383,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 +421,22 @@ 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) + except AttributeError: + # unshare() not supported by libc + pass + # Set requested process permissions. if gid: # Cast proxies to int, in case it matters. -- 1.8.3.2