1 |
commit: 8aa33981d83913b6ae92e6ea8e44c1a37fb59872 |
2 |
Author: Michał Górny <mgorny <AT> gentoo <DOT> org> |
3 |
AuthorDate: Mon Nov 12 16:32:35 2018 +0000 |
4 |
Commit: Michał Górny <mgorny <AT> gentoo <DOT> org> |
5 |
CommitDate: Sun Nov 18 12:24:34 2018 +0000 |
6 |
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=8aa33981 |
7 |
|
8 |
Add FEATURES=mount-sandbox to take advantage of mount ns |
9 |
|
10 |
Support FEATURES=mount-sandbox that unshares the ebuild processes |
11 |
into a new mount namespace and makes all the mounts private by default. |
12 |
|
13 |
Reviewed-by: Zac Medico <zmedico <AT> gentoo.org> |
14 |
Signed-off-by: Michał Górny <mgorny <AT> gentoo.org> |
15 |
|
16 |
lib/portage/const.py | 1 + |
17 |
lib/portage/package/ebuild/doebuild.py | 7 ++++++- |
18 |
lib/portage/process.py | 34 ++++++++++++++++++++++++++++------ |
19 |
man/make.conf.5 | 5 +++++ |
20 |
4 files changed, 40 insertions(+), 7 deletions(-) |
21 |
|
22 |
diff --git a/lib/portage/const.py b/lib/portage/const.py |
23 |
index 602caeb34..e0f93f7cc 100644 |
24 |
--- a/lib/portage/const.py |
25 |
+++ b/lib/portage/const.py |
26 |
@@ -160,6 +160,7 @@ SUPPORTED_FEATURES = frozenset([ |
27 |
"merge-sync", |
28 |
"metadata-transfer", |
29 |
"mirror", |
30 |
+ "mount-sandbox", |
31 |
"multilib-strict", |
32 |
"network-sandbox", |
33 |
"network-sandbox-proxy", |
34 |
|
35 |
diff --git a/lib/portage/package/ebuild/doebuild.py b/lib/portage/package/ebuild/doebuild.py |
36 |
index d0e96f34c..e84a618d2 100644 |
37 |
--- a/lib/portage/package/ebuild/doebuild.py |
38 |
+++ b/lib/portage/package/ebuild/doebuild.py |
39 |
@@ -148,6 +148,7 @@ def _doebuild_spawn(phase, settings, actionmap=None, **kwargs): |
40 |
|
41 |
kwargs['ipc'] = 'ipc-sandbox' not in settings.features or \ |
42 |
phase in _ipc_phases |
43 |
+ kwargs['mountns'] = 'mount-sandbox' in settings.features |
44 |
kwargs['networked'] = 'network-sandbox' not in settings.features or \ |
45 |
phase in _networked_phases or \ |
46 |
'network-sandbox' in settings['PORTAGE_RESTRICT'].split() |
47 |
@@ -1480,7 +1481,8 @@ def _validate_deps(mysettings, myroot, mydo, mydbapi): |
48 |
# XXX This would be to replace getstatusoutput completely. |
49 |
# XXX Issue: cannot block execution. Deadlock condition. |
50 |
def spawn(mystring, mysettings, debug=False, free=False, droppriv=False, |
51 |
- sesandbox=False, fakeroot=False, networked=True, ipc=True, **keywords): |
52 |
+ sesandbox=False, fakeroot=False, networked=True, ipc=True, |
53 |
+ mountns=False, **keywords): |
54 |
""" |
55 |
Spawn a subprocess with extra portage-specific options. |
56 |
Optiosn include: |
57 |
@@ -1514,6 +1516,8 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False, |
58 |
@type networked: Boolean |
59 |
@param ipc: Run this command with host IPC access enabled |
60 |
@type ipc: Boolean |
61 |
+ @param mountns: Run this command inside mount namespace |
62 |
+ @type mountns: Boolean |
63 |
@param keywords: Extra options encoded as a dict, to be passed to spawn |
64 |
@type keywords: Dictionary |
65 |
@rtype: Integer |
66 |
@@ -1546,6 +1550,7 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False, |
67 |
if uid == 0 and platform.system() == 'Linux': |
68 |
keywords['unshare_net'] = not networked |
69 |
keywords['unshare_ipc'] = not ipc |
70 |
+ keywords['unshare_mount'] = mountns |
71 |
|
72 |
if not networked and mysettings.get("EBUILD_PHASE") != "nofetch" and \ |
73 |
("network-sandbox-proxy" in features or "distcc" in features): |
74 |
|
75 |
diff --git a/lib/portage/process.py b/lib/portage/process.py |
76 |
index fd326731a..46868f442 100644 |
77 |
--- a/lib/portage/process.py |
78 |
+++ b/lib/portage/process.py |
79 |
@@ -1,5 +1,5 @@ |
80 |
# portage.py -- core Portage functionality |
81 |
-# Copyright 1998-2014 Gentoo Foundation |
82 |
+# Copyright 1998-2018 Gentoo Authors |
83 |
# Distributed under the terms of the GNU General Public License v2 |
84 |
|
85 |
|
86 |
@@ -10,6 +10,7 @@ import platform |
87 |
import signal |
88 |
import socket |
89 |
import struct |
90 |
+import subprocess |
91 |
import sys |
92 |
import traceback |
93 |
import os as _os |
94 |
@@ -222,7 +223,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
95 |
uid=None, gid=None, groups=None, umask=None, logfile=None, |
96 |
path_lookup=True, pre_exec=None, |
97 |
close_fds=(sys.version_info < (3, 4)), unshare_net=False, |
98 |
- unshare_ipc=False, cgroup=None): |
99 |
+ unshare_ipc=False, unshare_mount=False, cgroup=None): |
100 |
""" |
101 |
Spawns a given command. |
102 |
|
103 |
@@ -260,6 +261,9 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
104 |
@type unshare_net: Boolean |
105 |
@param unshare_ipc: If True, IPC will be unshared from the spawned process |
106 |
@type unshare_ipc: Boolean |
107 |
+ @param unshare_mount: If True, mount namespace will be unshared and mounts will |
108 |
+ be private to the namespace |
109 |
+ @type unshare_mount: Boolean |
110 |
@param cgroup: CGroup path to bind the process to |
111 |
@type cgroup: String |
112 |
|
113 |
@@ -328,7 +332,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
114 |
# This caches the libc library lookup in the current |
115 |
# process, so that it's only done once rather than |
116 |
# for each child process. |
117 |
- if unshare_net or unshare_ipc: |
118 |
+ if unshare_net or unshare_ipc or unshare_mount: |
119 |
find_library("c") |
120 |
|
121 |
# Force instantiation of portage.data.userpriv_groups before the |
122 |
@@ -344,7 +348,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
123 |
try: |
124 |
_exec(binary, mycommand, opt_name, fd_pipes, |
125 |
env, gid, groups, uid, umask, pre_exec, close_fds, |
126 |
- unshare_net, unshare_ipc, cgroup) |
127 |
+ unshare_net, unshare_ipc, unshare_mount, cgroup) |
128 |
except SystemExit: |
129 |
raise |
130 |
except Exception as e: |
131 |
@@ -414,7 +418,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
132 |
return 0 |
133 |
|
134 |
def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
135 |
- pre_exec, close_fds, unshare_net, unshare_ipc, cgroup): |
136 |
+ pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, cgroup): |
137 |
|
138 |
""" |
139 |
Execute a given binary with options |
140 |
@@ -443,6 +447,9 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
141 |
@type unshare_net: Boolean |
142 |
@param unshare_ipc: If True, IPC will be unshared from the spawned process |
143 |
@type unshare_ipc: Boolean |
144 |
+ @param unshare_mount: If True, mount namespace will be unshared and mounts will |
145 |
+ be private to the namespace |
146 |
+ @type unshare_mount: Boolean |
147 |
@param cgroup: CGroup path to bind the process to |
148 |
@type cgroup: String |
149 |
@rtype: None |
150 |
@@ -499,11 +506,13 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
151 |
f.write('%d\n' % os.getpid()) |
152 |
|
153 |
# Unshare (while still uid==0) |
154 |
- if unshare_net or unshare_ipc: |
155 |
+ if unshare_net or unshare_ipc or unshare_mount: |
156 |
filename = find_library("c") |
157 |
if filename is not None: |
158 |
libc = LoadLibrary(filename) |
159 |
if libc is not None: |
160 |
+ # from /usr/include/bits/sched.h |
161 |
+ CLONE_NEWNS = 0x00020000 |
162 |
CLONE_NEWIPC = 0x08000000 |
163 |
CLONE_NEWNET = 0x40000000 |
164 |
|
165 |
@@ -512,6 +521,9 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
166 |
flags |= CLONE_NEWNET |
167 |
if unshare_ipc: |
168 |
flags |= CLONE_NEWIPC |
169 |
+ if unshare_mount: |
170 |
+ # NEWNS = mount namespace |
171 |
+ flags |= CLONE_NEWNS |
172 |
|
173 |
try: |
174 |
if libc.unshare(flags) != 0: |
175 |
@@ -519,6 +531,16 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
176 |
errno.errorcode.get(ctypes.get_errno(), '?')), |
177 |
noiselevel=-1) |
178 |
else: |
179 |
+ if unshare_mount: |
180 |
+ # mark the whole filesystem as slave to avoid |
181 |
+ # mounts escaping the namespace |
182 |
+ s = subprocess.Popen(['mount', |
183 |
+ '--make-rslave', '/']) |
184 |
+ mount_ret = s.wait() |
185 |
+ if mount_ret != 0: |
186 |
+ # TODO: should it be fatal maybe? |
187 |
+ writemsg("Unable to mark mounts slave: %d\n" % (mount_ret,), |
188 |
+ noiselevel=-1) |
189 |
if unshare_net: |
190 |
# 'up' the loopback |
191 |
IFF_UP = 0x1 |
192 |
|
193 |
diff --git a/man/make.conf.5 b/man/make.conf.5 |
194 |
index f69afd015..7cb5741ad 100644 |
195 |
--- a/man/make.conf.5 |
196 |
+++ b/man/make.conf.5 |
197 |
@@ -494,6 +494,11 @@ ${repository_location}/metadata/md5\-cache/ directory will be used directly |
198 |
Fetch everything in \fBSRC_URI\fR regardless of \fBUSE\fR settings, |
199 |
except do not fetch anything when \fImirror\fR is in \fBRESTRICT\fR. |
200 |
.TP |
201 |
+.B mount\-sandbox |
202 |
+Isolate the ebuild phase functions from host mount namespace. This makes |
203 |
+it possible for ebuild to alter mountpoints without affecting the host |
204 |
+system. Supported only on Linux. Requires mount namespace support in kernel. |
205 |
+.TP |
206 |
.B multilib\-strict |
207 |
Many Makefiles assume that their libraries should go to /usr/lib, or |
208 |
$(prefix)/lib. This assumption can cause a serious mess if /usr/lib |