From: Michał Górny Date: Sun, 18 Aug 2013 23:22:59 +0000 (+0200) Subject: Add FEATURES=ipc-sandbox to isolate IPC from host. X-Git-Tag: v2.2.1~12 X-Git-Url: http://git.tremily.us/?a=commitdiff_plain;h=f0711200ce35920552962190c9a1f7b98d107070;p=portage.git Add FEATURES=ipc-sandbox to isolate IPC from host. This way, only privileged phases (pkg_*) can use *nix IPC to communicate with host applications. src_* use private IPC namespace. --- diff --git a/man/make.conf.5 b/man/make.conf.5 index 461172c9b..91817aec5 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -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. diff --git a/pym/portage/const.py b/pym/portage/const.py index cde007937..88c199b2c 100644 --- a/pym/portage/const.py +++ b/pym/portage/const.py @@ -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", diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index a35e71719..2d26d2cbe 100644 --- a/pym/portage/package/ebuild/doebuild.py +++ b/pym/portage/package/ebuild/doebuild.py @@ -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 diff --git a/pym/portage/process.py b/pym/portage/process.py index 6a60dec55..22c6a8823 100644 --- a/pym/portage/process.py +++ b/pym/portage/process.py @@ -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