1 |
commit: c2a9850a25b2f32a25b43ef30189cd6657f397ad |
2 |
Author: Zac Medico <zmedico <AT> gentoo <DOT> org> |
3 |
AuthorDate: Sat Dec 29 06:56:40 2018 +0000 |
4 |
Commit: Zac Medico <zmedico <AT> gentoo <DOT> org> |
5 |
CommitDate: Fri Jan 4 03:04:49 2019 +0000 |
6 |
URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=c2a9850a |
7 |
|
8 |
process.spawn: validate unshare calls (bug 673900) |
9 |
|
10 |
In order to prevent failed unshare calls from corrupting the state |
11 |
of an essential process, validate the relevant unshare call in a |
12 |
short-lived subprocess. An unshare call is considered valid if it |
13 |
successfully executes in a short-lived subprocess. |
14 |
|
15 |
Bug: https://bugs.gentoo.org/673900 |
16 |
Signed-off-by: Zac Medico <zmedico <AT> gentoo.org> |
17 |
|
18 |
lib/portage/process.py | 159 +++++++++++++++++++++++++++++++++++++++++-------- |
19 |
1 file changed, 133 insertions(+), 26 deletions(-) |
20 |
|
21 |
diff --git a/lib/portage/process.py b/lib/portage/process.py |
22 |
index ce3e42a8f..7103b6b31 100644 |
23 |
--- a/lib/portage/process.py |
24 |
+++ b/lib/portage/process.py |
25 |
@@ -6,6 +6,7 @@ |
26 |
import atexit |
27 |
import errno |
28 |
import fcntl |
29 |
+import multiprocessing |
30 |
import platform |
31 |
import signal |
32 |
import socket |
33 |
@@ -338,11 +339,29 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
34 |
fd_pipes[1] = pw |
35 |
fd_pipes[2] = pw |
36 |
|
37 |
- # This caches the libc library lookup in the current |
38 |
- # process, so that it's only done once rather than |
39 |
- # for each child process. |
40 |
+ # This caches the libc library lookup and _unshare_validator results |
41 |
+ # in the current process, so that results are cached for use in |
42 |
+ # child processes. |
43 |
+ unshare_flags = 0 |
44 |
if unshare_net or unshare_ipc or unshare_mount or unshare_pid: |
45 |
- find_library("c") |
46 |
+ # from /usr/include/bits/sched.h |
47 |
+ CLONE_NEWNS = 0x00020000 |
48 |
+ CLONE_NEWIPC = 0x08000000 |
49 |
+ CLONE_NEWPID = 0x20000000 |
50 |
+ CLONE_NEWNET = 0x40000000 |
51 |
+ |
52 |
+ if unshare_net: |
53 |
+ unshare_flags |= CLONE_NEWNET |
54 |
+ if unshare_ipc: |
55 |
+ unshare_flags |= CLONE_NEWIPC |
56 |
+ if unshare_mount: |
57 |
+ # NEWNS = mount namespace |
58 |
+ unshare_flags |= CLONE_NEWNS |
59 |
+ if unshare_pid: |
60 |
+ # we also need mount namespace for slave /proc |
61 |
+ unshare_flags |= CLONE_NEWPID | CLONE_NEWNS |
62 |
+ |
63 |
+ _unshare_validate(unshare_flags) |
64 |
|
65 |
# Force instantiation of portage.data.userpriv_groups before the |
66 |
# fork, so that the result is cached in the main process. |
67 |
@@ -358,7 +377,7 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
68 |
_exec(binary, mycommand, opt_name, fd_pipes, |
69 |
env, gid, groups, uid, umask, cwd, pre_exec, close_fds, |
70 |
unshare_net, unshare_ipc, unshare_mount, unshare_pid, |
71 |
- cgroup) |
72 |
+ unshare_flags, cgroup) |
73 |
except SystemExit: |
74 |
raise |
75 |
except Exception as e: |
76 |
@@ -430,7 +449,7 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
77 |
def _exec(binary, mycommand, opt_name, fd_pipes, |
78 |
env, gid, groups, uid, umask, cwd, |
79 |
pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, unshare_pid, |
80 |
- cgroup): |
81 |
+ unshare_flags, cgroup): |
82 |
|
83 |
""" |
84 |
Execute a given binary with options |
85 |
@@ -466,6 +485,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
86 |
@type unshare_mount: Boolean |
87 |
@param unshare_pid: If True, PID ns will be unshared from the spawned process |
88 |
@type unshare_pid: Boolean |
89 |
+ @param unshare_flags: Flags for the unshare(2) function |
90 |
+ @type unshare_flags: Integer |
91 |
@param cgroup: CGroup path to bind the process to |
92 |
@type cgroup: String |
93 |
@rtype: None |
94 |
@@ -527,28 +548,19 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
95 |
if filename is not None: |
96 |
libc = LoadLibrary(filename) |
97 |
if libc is not None: |
98 |
- # from /usr/include/bits/sched.h |
99 |
- CLONE_NEWNS = 0x00020000 |
100 |
- CLONE_NEWIPC = 0x08000000 |
101 |
- CLONE_NEWPID = 0x20000000 |
102 |
- CLONE_NEWNET = 0x40000000 |
103 |
- |
104 |
- flags = 0 |
105 |
- if unshare_net: |
106 |
- flags |= CLONE_NEWNET |
107 |
- if unshare_ipc: |
108 |
- flags |= CLONE_NEWIPC |
109 |
- if unshare_mount: |
110 |
- # NEWNS = mount namespace |
111 |
- flags |= CLONE_NEWNS |
112 |
- if unshare_pid: |
113 |
- # we also need mount namespace for slave /proc |
114 |
- flags |= CLONE_NEWPID | CLONE_NEWNS |
115 |
- |
116 |
try: |
117 |
- if libc.unshare(flags) != 0: |
118 |
+ # Since a failed unshare call could corrupt process |
119 |
+ # state, first validate that the call can succeed. |
120 |
+ # The parent process should call _unshare_validate |
121 |
+ # before it forks, so that all child processes can |
122 |
+ # reuse _unshare_validate results that have been |
123 |
+ # cached by the parent process. |
124 |
+ errno_value = _unshare_validate(unshare_flags) |
125 |
+ if errno_value == 0 and libc.unshare(unshare_flags) != 0: |
126 |
+ errno_value = ctypes.get_errno() |
127 |
+ if errno_value != 0: |
128 |
writemsg("Unable to unshare: %s\n" % ( |
129 |
- errno.errorcode.get(ctypes.get_errno(), '?')), |
130 |
+ errno.errorcode.get(errno_value, '?')), |
131 |
noiselevel=-1) |
132 |
else: |
133 |
if unshare_pid: |
134 |
@@ -626,6 +638,101 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
135 |
# And switch to the new process. |
136 |
os.execve(binary, myargs, env) |
137 |
|
138 |
+ |
139 |
+class _unshare_validator(object): |
140 |
+ """ |
141 |
+ In order to prevent failed unshare calls from corrupting the state |
142 |
+ of an essential process, validate the relevant unshare call in a |
143 |
+ short-lived subprocess. An unshare call is considered valid if it |
144 |
+ successfully executes in a short-lived subprocess. |
145 |
+ """ |
146 |
+ |
147 |
+ def __init__(self): |
148 |
+ self._results = {} |
149 |
+ |
150 |
+ def __call__(self, flags): |
151 |
+ """ |
152 |
+ Validate unshare with the given flags. Results are cached. |
153 |
+ |
154 |
+ @rtype: int |
155 |
+ @returns: errno value, or 0 if no error occurred. |
156 |
+ """ |
157 |
+ |
158 |
+ try: |
159 |
+ return self._results[flags] |
160 |
+ except KeyError: |
161 |
+ result = self._results[flags] = self._validate(flags) |
162 |
+ return result |
163 |
+ |
164 |
+ @classmethod |
165 |
+ def _validate(cls, flags): |
166 |
+ """ |
167 |
+ Perform validation. |
168 |
+ |
169 |
+ @param flags: unshare flags |
170 |
+ @type flags: int |
171 |
+ @rtype: int |
172 |
+ @returns: errno value, or 0 if no error occurred. |
173 |
+ """ |
174 |
+ filename = find_library("c") |
175 |
+ if filename is None: |
176 |
+ return errno.ENOTSUP |
177 |
+ |
178 |
+ libc = LoadLibrary(filename) |
179 |
+ if libc is None: |
180 |
+ return errno.ENOTSUP |
181 |
+ |
182 |
+ parent_pipe, subproc_pipe = multiprocessing.Pipe(duplex=False) |
183 |
+ |
184 |
+ proc = multiprocessing.Process( |
185 |
+ target=cls._run_subproc, |
186 |
+ args=(subproc_pipe, cls._validate_subproc, (libc.unshare, flags))) |
187 |
+ proc.start() |
188 |
+ subproc_pipe.close() |
189 |
+ |
190 |
+ result = parent_pipe.recv() |
191 |
+ parent_pipe.close() |
192 |
+ proc.join() |
193 |
+ |
194 |
+ return result |
195 |
+ |
196 |
+ @staticmethod |
197 |
+ def _run_subproc(subproc_pipe, target, args=(), kwargs={}): |
198 |
+ """ |
199 |
+ Call function and send return value to parent process. |
200 |
+ |
201 |
+ @param subproc_pipe: connection to parent process |
202 |
+ @type subproc_pipe: multiprocessing.Connection |
203 |
+ @param target: target is the callable object to be invoked |
204 |
+ @type target: callable |
205 |
+ @param args: the argument tuple for the target invocation |
206 |
+ @type args: tuple |
207 |
+ @param kwargs: dictionary of keyword arguments for the target invocation |
208 |
+ @type kwargs: dict |
209 |
+ """ |
210 |
+ subproc_pipe.send(target(*args, **kwargs)) |
211 |
+ subproc_pipe.close() |
212 |
+ |
213 |
+ @staticmethod |
214 |
+ def _validate_subproc(unshare, flags): |
215 |
+ """ |
216 |
+ Perform validation. Calls to this method must be isolated in a |
217 |
+ subprocess, since the unshare function is called for purposes of |
218 |
+ validation. |
219 |
+ |
220 |
+ @param unshare: unshare function |
221 |
+ @type unshare: callable |
222 |
+ @param flags: unshare flags |
223 |
+ @type flags: int |
224 |
+ @rtype: int |
225 |
+ @returns: errno value, or 0 if no error occurred. |
226 |
+ """ |
227 |
+ return 0 if unshare(flags) == 0 else ctypes.get_errno() |
228 |
+ |
229 |
+ |
230 |
+_unshare_validate = _unshare_validator() |
231 |
+ |
232 |
+ |
233 |
def _setup_pipes(fd_pipes, close_fds=True, inheritable=None): |
234 |
"""Setup pipes for a forked process. |