Gentoo Archives: gentoo-portage-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-portage-dev@l.g.o
Cc: "Michał Górny" <mgorny@g.o>
Subject: [gentoo-portage-dev] [PATCH v2 2/3] Support FEATURES=pid-sandbox
Date: Sun, 18 Nov 2018 08:53:58
Message-Id: 20181118085341.3835-2-mgorny@gentoo.org
In Reply to: [gentoo-portage-dev] [PATCH v2 1/3] Add FEATURES=mount-sandbox to take advantage of mount ns by "Michał Górny"
1 Supporting using PID namespace in order to isolate the ebuild processes
2 from host system, and make it possible to kill them all easily
3 (similarly to cgroups but easier to use).
4
5 Bug: https://bugs.gentoo.org/659582
6 Signed-off-by: Michał Górny <mgorny@g.o>
7 ---
8 lib/portage/const.py | 1 +
9 lib/portage/package/ebuild/doebuild.py | 8 +++--
10 lib/portage/process.py | 48 +++++++++++++++++++++++---
11 man/make.conf.5 | 7 ++++
12 4 files changed, 57 insertions(+), 7 deletions(-)
13
14 New in v2: the code was made independent of mount-sandbox. Instead of
15 making all mounts slaved, it just ensures that /proc is slaved for
16 the purpose of remounting. Failure to slave-mount /proc is considered
17 fatal, as the resulting setup will likely break ebuilds.
18
19 diff --git a/lib/portage/const.py b/lib/portage/const.py
20 index e0f93f7cc..ca66bc46e 100644
21 --- a/lib/portage/const.py
22 +++ b/lib/portage/const.py
23 @@ -174,6 +174,7 @@ SUPPORTED_FEATURES = frozenset([
24 "notitles",
25 "parallel-fetch",
26 "parallel-install",
27 + "pid-sandbox",
28 "prelink-checksums",
29 "preserve-libs",
30 "protect-owned",
31 diff --git a/lib/portage/package/ebuild/doebuild.py b/lib/portage/package/ebuild/doebuild.py
32 index e84a618d2..9917ac82c 100644
33 --- a/lib/portage/package/ebuild/doebuild.py
34 +++ b/lib/portage/package/ebuild/doebuild.py
35 @@ -1,4 +1,4 @@
36 -# Copyright 2010-2018 Gentoo Foundation
37 +# Copyright 2010-2018 Gentoo Authors
38 # Distributed under the terms of the GNU General Public License v2
39
40 from __future__ import unicode_literals
41 @@ -152,6 +152,7 @@ def _doebuild_spawn(phase, settings, actionmap=None, **kwargs):
42 kwargs['networked'] = 'network-sandbox' not in settings.features or \
43 phase in _networked_phases or \
44 'network-sandbox' in settings['PORTAGE_RESTRICT'].split()
45 + kwargs['pidns'] = 'pid-sandbox' in settings.features
46
47 if phase == 'depend':
48 kwargs['droppriv'] = 'userpriv' in settings.features
49 @@ -1482,7 +1483,7 @@ def _validate_deps(mysettings, myroot, mydo, mydbapi):
50 # XXX Issue: cannot block execution. Deadlock condition.
51 def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
52 sesandbox=False, fakeroot=False, networked=True, ipc=True,
53 - mountns=False, **keywords):
54 + mountns=False, pidns=False, **keywords):
55 """
56 Spawn a subprocess with extra portage-specific options.
57 Optiosn include:
58 @@ -1518,6 +1519,8 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
59 @type ipc: Boolean
60 @param mountns: Run this command inside mount namespace
61 @type mountns: Boolean
62 + @param pidns: Run this command in isolated PID namespace
63 + @type pidns: Boolean
64 @param keywords: Extra options encoded as a dict, to be passed to spawn
65 @type keywords: Dictionary
66 @rtype: Integer
67 @@ -1551,6 +1554,7 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
68 keywords['unshare_net'] = not networked
69 keywords['unshare_ipc'] = not ipc
70 keywords['unshare_mount'] = mountns
71 + keywords['unshare_pid'] = pidns
72
73 if not networked and mysettings.get("EBUILD_PHASE") != "nofetch" and \
74 ("network-sandbox-proxy" in features or "distcc" in features):
75 diff --git a/lib/portage/process.py b/lib/portage/process.py
76 index 46868f442..dee126c3c 100644
77 --- a/lib/portage/process.py
78 +++ b/lib/portage/process.py
79 @@ -223,7 +223,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
80 uid=None, gid=None, groups=None, umask=None, logfile=None,
81 path_lookup=True, pre_exec=None,
82 close_fds=(sys.version_info < (3, 4)), unshare_net=False,
83 - unshare_ipc=False, unshare_mount=False, cgroup=None):
84 + unshare_ipc=False, unshare_mount=False, unshare_pid=False,
85 + cgroup=None):
86 """
87 Spawns a given command.
88
89 @@ -264,6 +265,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
90 @param unshare_mount: If True, mount namespace will be unshared and mounts will
91 be private to the namespace
92 @type unshare_mount: Boolean
93 + @param unshare_pid: If True, PID ns will be unshared from the spawned process
94 + @type unshare_pid: Boolean
95 @param cgroup: CGroup path to bind the process to
96 @type cgroup: String
97
98 @@ -332,7 +335,7 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
99 # This caches the libc library lookup in the current
100 # process, so that it's only done once rather than
101 # for each child process.
102 - if unshare_net or unshare_ipc or unshare_mount:
103 + if unshare_net or unshare_ipc or unshare_mount or unshare_pid:
104 find_library("c")
105
106 # Force instantiation of portage.data.userpriv_groups before the
107 @@ -348,7 +351,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
108 try:
109 _exec(binary, mycommand, opt_name, fd_pipes,
110 env, gid, groups, uid, umask, pre_exec, close_fds,
111 - unshare_net, unshare_ipc, unshare_mount, cgroup)
112 + unshare_net, unshare_ipc, unshare_mount, unshare_pid,
113 + cgroup)
114 except SystemExit:
115 raise
116 except Exception as e:
117 @@ -418,7 +422,8 @@ def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
118 return 0
119
120 def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
121 - pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, cgroup):
122 + pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, unshare_pid,
123 + cgroup):
124
125 """
126 Execute a given binary with options
127 @@ -450,6 +455,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
128 @param unshare_mount: If True, mount namespace will be unshared and mounts will
129 be private to the namespace
130 @type unshare_mount: Boolean
131 + @param unshare_pid: If True, PID ns will be unshared from the spawned process
132 + @type unshare_pid: Boolean
133 @param cgroup: CGroup path to bind the process to
134 @type cgroup: String
135 @rtype: None
136 @@ -506,7 +513,7 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
137 f.write('%d\n' % os.getpid())
138
139 # Unshare (while still uid==0)
140 - if unshare_net or unshare_ipc or unshare_mount:
141 + if unshare_net or unshare_ipc or unshare_mount or unshare_pid:
142 filename = find_library("c")
143 if filename is not None:
144 libc = LoadLibrary(filename)
145 @@ -514,6 +521,7 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
146 # from /usr/include/bits/sched.h
147 CLONE_NEWNS = 0x00020000
148 CLONE_NEWIPC = 0x08000000
149 + CLONE_NEWPID = 0x20000000
150 CLONE_NEWNET = 0x40000000
151
152 flags = 0
153 @@ -524,6 +532,9 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
154 if unshare_mount:
155 # NEWNS = mount namespace
156 flags |= CLONE_NEWNS
157 + if unshare_pid:
158 + # we also need mount namespace for slave /proc
159 + flags |= CLONE_NEWPID | CLONE_NEWNS
160
161 try:
162 if libc.unshare(flags) != 0:
163 @@ -531,6 +542,15 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
164 errno.errorcode.get(ctypes.get_errno(), '?')),
165 noiselevel=-1)
166 else:
167 + if unshare_pid:
168 + # pid namespace requires us to become init
169 + # TODO: do init-ty stuff
170 + # therefore, fork() ASAP
171 + fork_ret = os.fork()
172 + if fork_ret != 0:
173 + pid, status = os.waitpid(fork_ret, 0)
174 + assert pid == fork_ret
175 + os._exit(status)
176 if unshare_mount:
177 # mark the whole filesystem as slave to avoid
178 # mounts escaping the namespace
179 @@ -541,6 +561,24 @@ def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
180 # TODO: should it be fatal maybe?
181 writemsg("Unable to mark mounts slave: %d\n" % (mount_ret,),
182 noiselevel=-1)
183 + if unshare_pid:
184 + # we need at least /proc being slave
185 + s = subprocess.Popen(['mount',
186 + '--make-slave', '/proc'])
187 + mount_ret = s.wait()
188 + if mount_ret != 0:
189 + # can't proceed with shared /proc
190 + writemsg("Unable to mark /proc slave: %d\n" % (mount_ret,),
191 + noiselevel=-1)
192 + os._exit(1)
193 + # mount new /proc for our namespace
194 + s = subprocess.Popen(['mount',
195 + '-t', 'proc', 'proc', '/proc'])
196 + mount_ret = s.wait()
197 + if mount_ret != 0:
198 + writemsg("Unable to mount new /proc: %d\n" % (mount_ret,),
199 + noiselevel=-1)
200 + os._exit(1)
201 if unshare_net:
202 # 'up' the loopback
203 IFF_UP = 0x1
204 diff --git a/man/make.conf.5 b/man/make.conf.5
205 index 7cb5741ad..de04e5e34 100644
206 --- a/man/make.conf.5
207 +++ b/man/make.conf.5
208 @@ -558,6 +558,13 @@ Use finer\-grained locks when installing packages, allowing for greater
209 parallelization. For additional parallelization, disable
210 \fIebuild\-locks\fR.
211 .TP
212 +.B pid\-sandbox
213 +Isolate the process space for the ebuild processes. This makes it
214 +possible to cleanly kill all processes spawned by the ebuild.
215 +Supported only on Linux. Requires PID and mount namespace support
216 +in kernel. /proc is remounted inside the mount namespace to account
217 +for new PID namespace.
218 +.TP
219 .B prelink\-checksums
220 If \fBprelink\fR(8) is installed then use it to undo any prelinks on files
221 before computing checksums for merge and unmerge. This feature is
222 --
223 2.19.1