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 461172c..91817ae 100644
--- a/man/make.conf.5
+++ b/man/make.conf.5
@@ -385,6 +385,10 @@
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 cde0079..88c199b 100644
--- a/pym/portage/const.py
+++ b/pym/portage/const.py
@@ -102,7 +102,8 @@
"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 a35e717..2d26d2c 100644
--- a/pym/portage/package/ebuild/doebuild.py
+++ b/pym/portage/package/ebuild/doebuild.py
@@ -82,14 +82,18 @@
"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 @@
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 @@
# 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 @@
@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 @@
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 6a60dec..22c6a88 100644
--- a/pym/portage/process.py
+++ b/pym/portage/process.py
@@ -184,7 +184,8 @@
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 @@
@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 @@
# 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 @@
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 @@
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 @@
@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 @@
_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
+ 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()
+ 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