Support FEATURES=mount-sandbox that unshares the ebuild processes
into a new mount namespace and makes all the mounts private by default.

Signed-off-by: Michał Górny <mgo...@gentoo.org>
---
 lib/portage/const.py                   |  1 +
 lib/portage/package/ebuild/doebuild.py |  7 +++++-
 lib/portage/process.py                 | 34 +++++++++++++++++++++-----
 man/make.conf.5                        |  5 ++++
 4 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/lib/portage/const.py b/lib/portage/const.py
index 602caeb34..e0f93f7cc 100644
--- a/lib/portage/const.py
+++ b/lib/portage/const.py
@@ -160,6 +160,7 @@ SUPPORTED_FEATURES       = frozenset([
        "merge-sync",
        "metadata-transfer",
        "mirror",
+       "mount-sandbox",
        "multilib-strict",
        "network-sandbox",
        "network-sandbox-proxy",
diff --git a/lib/portage/package/ebuild/doebuild.py 
b/lib/portage/package/ebuild/doebuild.py
index d0e96f34c..e84a618d2 100644
--- a/lib/portage/package/ebuild/doebuild.py
+++ b/lib/portage/package/ebuild/doebuild.py
@@ -148,6 +148,7 @@ def _doebuild_spawn(phase, settings, actionmap=None, 
**kwargs):
 
        kwargs['ipc'] = 'ipc-sandbox' not in settings.features or \
                phase in _ipc_phases
+       kwargs['mountns'] = 'mount-sandbox' in settings.features
        kwargs['networked'] = 'network-sandbox' not in settings.features or \
                phase in _networked_phases or \
                'network-sandbox' in settings['PORTAGE_RESTRICT'].split()
@@ -1480,7 +1481,8 @@ 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=False, free=False, droppriv=False,
-       sesandbox=False, fakeroot=False, networked=True, ipc=True, **keywords):
+       sesandbox=False, fakeroot=False, networked=True, ipc=True,
+       mountns=False, **keywords):
        """
        Spawn a subprocess with extra portage-specific options.
        Optiosn include:
@@ -1514,6 +1516,8 @@ def spawn(mystring, mysettings, debug=False, free=False, 
droppriv=False,
        @type networked: Boolean
        @param ipc: Run this command with host IPC access enabled
        @type ipc: Boolean
+       @param mountns: Run this command inside mount namespace
+       @type mountns: Boolean
        @param keywords: Extra options encoded as a dict, to be passed to spawn
        @type keywords: Dictionary
        @rtype: Integer
@@ -1546,6 +1550,7 @@ def spawn(mystring, mysettings, debug=False, free=False, 
droppriv=False,
        if uid == 0 and platform.system() == 'Linux':
                keywords['unshare_net'] = not networked
                keywords['unshare_ipc'] = not ipc
+               keywords['unshare_mount'] = mountns
 
                if not networked and mysettings.get("EBUILD_PHASE") != 
"nofetch" and \
                        ("network-sandbox-proxy" in features or "distcc" in 
features):
diff --git a/lib/portage/process.py b/lib/portage/process.py
index fd326731a..e2ad89b43 100644
--- a/lib/portage/process.py
+++ b/lib/portage/process.py
@@ -1,5 +1,5 @@
 # portage.py -- core Portage functionality
-# Copyright 1998-2014 Gentoo Foundation
+# Copyright 1998-2018 Gentoo Authors
 # Distributed under the terms of the GNU General Public License v2
 
 
@@ -10,6 +10,7 @@ import platform
 import signal
 import socket
 import struct
+import subprocess
 import sys
 import traceback
 import os as _os
@@ -222,7 +223,7 @@ 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=(sys.version_info < (3, 4)), unshare_net=False,
-          unshare_ipc=False, cgroup=None):
+          unshare_ipc=False, unshare_mount=False, cgroup=None):
        """
        Spawns a given command.
        
@@ -260,6 +261,9 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, 
returnpid=False,
        @type unshare_net: Boolean
        @param unshare_ipc: If True, IPC will be unshared from the spawned 
process
        @type unshare_ipc: Boolean
+       @param unshare_mount: If True, mount namespace will be unshared and 
mounts will
+               be private to the namespace
+       @type unshare_mount: Boolean
        @param cgroup: CGroup path to bind the process to
        @type cgroup: String
 
@@ -328,7 +332,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 or unshare_ipc:
+       if unshare_net or unshare_ipc or unshare_mount:
                find_library("c")
 
        # Force instantiation of portage.data.userpriv_groups before the
@@ -344,7 +348,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_ipc, cgroup)
+                                       unshare_net, unshare_ipc, 
unshare_mount, cgroup)
                        except SystemExit:
                                raise
                        except Exception as e:
@@ -414,7 +418,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, unshare_ipc, cgroup):
+       pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, cgroup):
 
        """
        Execute a given binary with options
@@ -443,6 +447,9 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, 
groups, uid, umask,
        @type unshare_net: Boolean
        @param unshare_ipc: If True, IPC will be unshared from the spawned 
process
        @type unshare_ipc: Boolean
+       @param unshare_mount: If True, mount namespace will be unshared and 
mounts will
+               be private to the namespace
+       @type unshare_mount: Boolean
        @param cgroup: CGroup path to bind the process to
        @type cgroup: String
        @rtype: None
@@ -499,11 +506,13 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, 
gid, groups, uid, umask,
                        f.write('%d\n' % os.getpid())
 
        # Unshare (while still uid==0)
-       if unshare_net or unshare_ipc:
+       if unshare_net or unshare_ipc or unshare_mount:
                filename = find_library("c")
                if filename is not None:
                        libc = LoadLibrary(filename)
                        if libc is not None:
+                               # from /usr/include/bits/sched.h
+                               CLONE_NEWNS = 0x00020000
                                CLONE_NEWIPC = 0x08000000
                                CLONE_NEWNET = 0x40000000
 
@@ -512,6 +521,9 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, 
groups, uid, umask,
                                        flags |= CLONE_NEWNET
                                if unshare_ipc:
                                        flags |= CLONE_NEWIPC
+                               if unshare_mount:
+                                       # NEWNS = mount namespace
+                                       flags |= CLONE_NEWNS
 
                                try:
                                        if libc.unshare(flags) != 0:
@@ -519,6 +531,16 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, 
groups, uid, umask,
                                                        
errno.errorcode.get(ctypes.get_errno(), '?')),
                                                        noiselevel=-1)
                                        else:
+                                               if unshare_mount:
+                                                       # mark the whole 
filesystem as private to avoid
+                                                       # mounts escaping the 
namespace
+                                                       s = 
subprocess.Popen(['mount',
+                                                               
'--make-rprivate', '/'])
+                                                       mount_ret = s.wait()
+                                                       if mount_ret != 0:
+                                                               # TODO: should 
it be fatal maybe?
+                                                               
writemsg("Unable to mark mounts private: %d\n" % (mount_ret,),
+                                                                       
noiselevel=-1)
                                                if unshare_net:
                                                        # 'up' the loopback
                                                        IFF_UP = 0x1
diff --git a/man/make.conf.5 b/man/make.conf.5
index f69afd015..7cb5741ad 100644
--- a/man/make.conf.5
+++ b/man/make.conf.5
@@ -494,6 +494,11 @@ ${repository_location}/metadata/md5\-cache/ directory will 
be used directly
 Fetch everything in \fBSRC_URI\fR regardless of \fBUSE\fR settings,
 except do not fetch anything when \fImirror\fR is in \fBRESTRICT\fR.
 .TP
+.B mount\-sandbox
+Isolate the ebuild phase functions from host mount namespace. This makes
+it possible for ebuild to alter mountpoints without affecting the host
+system. Supported only on Linux. Requires mount namespace support in kernel.
+.TP
 .B multilib\-strict
 Many Makefiles assume that their libraries should go to /usr/lib, or
 $(prefix)/lib. This assumption can cause a serious mess if /usr/lib
-- 
2.19.1


Reply via email to