1 |
On 12/30/18 12:44 AM, Michał Górny wrote: |
2 |
> On Sat, 2018-12-29 at 04:49 -0800, Zac Medico wrote: |
3 |
>> In order to prevent failed unshare calls from corrupting the state |
4 |
>> of an essential process, validate the relevant unshare call in a |
5 |
>> short-lived subprocess. An unshare call is considered valid if it |
6 |
>> successfully executes in a short-lived subprocess. |
7 |
>> |
8 |
>> Bug: https://bugs.gentoo.org/673900 |
9 |
>> Signed-off-by: Zac Medico <zmedico@g.o> |
10 |
>> --- |
11 |
>> lib/portage/process.py | 130 +++++++++++++++++++++++++++++++++-------- |
12 |
>> 1 file changed, 106 insertions(+), 24 deletions(-) |
13 |
>> |
14 |
>> diff --git a/lib/portage/process.py b/lib/portage/process.py |
15 |
>> index ce3e42a8f..4bd6a4192 100644 |
16 |
>> --- a/lib/portage/process.py |
17 |
>> +++ b/lib/portage/process.py |
18 |
>> @@ -6,6 +6,7 @@ |
19 |
>> import atexit |
20 |
>> import errno |
21 |
>> import fcntl |
22 |
>> +import multiprocessing |
23 |
>> import platform |
24 |
>> import signal |
25 |
>> import socket |
26 |
>> @@ -338,11 +339,29 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
27 |
>> fd_pipes[1] = pw |
28 |
>> fd_pipes[2] = pw |
29 |
>> |
30 |
>> - # This caches the libc library lookup in the current |
31 |
>> - # process, so that it's only done once rather than |
32 |
>> + # This caches the libc library lookup and _unshare_validator results |
33 |
>> + # in the current process, so that it's only done once rather than |
34 |
>> # for each child process. |
35 |
>> + unshare_flags = 0 |
36 |
>> if unshare_net or unshare_ipc or unshare_mount or unshare_pid: |
37 |
>> - find_library("c") |
38 |
>> + # from /usr/include/bits/sched.h |
39 |
>> + CLONE_NEWNS = 0x00020000 |
40 |
>> + CLONE_NEWIPC = 0x08000000 |
41 |
>> + CLONE_NEWPID = 0x20000000 |
42 |
>> + CLONE_NEWNET = 0x40000000 |
43 |
>> + |
44 |
>> + if unshare_net: |
45 |
>> + unshare_flags |= CLONE_NEWNET |
46 |
>> + if unshare_ipc: |
47 |
>> + unshare_flags |= CLONE_NEWIPC |
48 |
>> + if unshare_mount: |
49 |
>> + # NEWNS = mount namespace |
50 |
>> + unshare_flags |= CLONE_NEWNS |
51 |
>> + if unshare_pid: |
52 |
>> + # we also need mount namespace for slave /proc |
53 |
>> + unshare_flags |= CLONE_NEWPID | CLONE_NEWNS |
54 |
>> + |
55 |
>> + _unshare_validator.instance.validate(unshare_flags) |
56 |
>> |
57 |
>> # Force instantiation of portage.data.userpriv_groups before the |
58 |
>> # fork, so that the result is cached in the main process. |
59 |
>> @@ -358,7 +377,7 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
60 |
>> _exec(binary, mycommand, opt_name, fd_pipes, |
61 |
>> env, gid, groups, uid, umask, cwd, pre_exec, close_fds, |
62 |
>> unshare_net, unshare_ipc, unshare_mount, unshare_pid, |
63 |
>> - cgroup) |
64 |
>> + unshare_flags, cgroup) |
65 |
>> except SystemExit: |
66 |
>> raise |
67 |
>> except Exception as e: |
68 |
>> @@ -430,7 +449,7 @@ def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False, |
69 |
>> def _exec(binary, mycommand, opt_name, fd_pipes, |
70 |
>> env, gid, groups, uid, umask, cwd, |
71 |
>> pre_exec, close_fds, unshare_net, unshare_ipc, unshare_mount, unshare_pid, |
72 |
>> - cgroup): |
73 |
>> + unshare_flags, cgroup): |
74 |
>> |
75 |
>> """ |
76 |
>> Execute a given binary with options |
77 |
>> @@ -466,6 +485,8 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
78 |
>> @type unshare_mount: Boolean |
79 |
>> @param unshare_pid: If True, PID ns will be unshared from the spawned process |
80 |
>> @type unshare_pid: Boolean |
81 |
>> + @param unshare_flags: Flags for the unshare(2) function |
82 |
>> + @type unshare_flags: Integer |
83 |
>> @param cgroup: CGroup path to bind the process to |
84 |
>> @type cgroup: String |
85 |
>> @rtype: None |
86 |
>> @@ -527,26 +548,14 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
87 |
>> if filename is not None: |
88 |
>> libc = LoadLibrary(filename) |
89 |
>> if libc is not None: |
90 |
>> - # from /usr/include/bits/sched.h |
91 |
>> - CLONE_NEWNS = 0x00020000 |
92 |
>> - CLONE_NEWIPC = 0x08000000 |
93 |
>> - CLONE_NEWPID = 0x20000000 |
94 |
>> - CLONE_NEWNET = 0x40000000 |
95 |
>> - |
96 |
>> - flags = 0 |
97 |
>> - if unshare_net: |
98 |
>> - flags |= CLONE_NEWNET |
99 |
>> - if unshare_ipc: |
100 |
>> - flags |= CLONE_NEWIPC |
101 |
>> - if unshare_mount: |
102 |
>> - # NEWNS = mount namespace |
103 |
>> - flags |= CLONE_NEWNS |
104 |
>> - if unshare_pid: |
105 |
>> - # we also need mount namespace for slave /proc |
106 |
>> - flags |= CLONE_NEWPID | CLONE_NEWNS |
107 |
>> - |
108 |
>> try: |
109 |
>> - if libc.unshare(flags) != 0: |
110 |
>> + errno_value = _unshare_validator.instance.validate(unshare_flags) |
111 |
>> + if errno_value != 0: |
112 |
>> + writemsg("Unable to unshare: %s\n" % ( |
113 |
>> + errno.errorcode.get(errno_value, '?')), |
114 |
>> + noiselevel=-1) |
115 |
>> + raise AttributeError('unshare') |
116 |
>> + if libc.unshare(unshare_flags) != 0: |
117 |
>> writemsg("Unable to unshare: %s\n" % ( |
118 |
>> errno.errorcode.get(ctypes.get_errno(), '?')), |
119 |
>> noiselevel=-1) |
120 |
>> @@ -626,6 +635,79 @@ def _exec(binary, mycommand, opt_name, fd_pipes, |
121 |
>> # And switch to the new process. |
122 |
>> os.execve(binary, myargs, env) |
123 |
>> |
124 |
>> + |
125 |
>> +class _unshare_validator(object): |
126 |
>> + """ |
127 |
>> + In order to prevent failed unshare calls from corrupting the state |
128 |
>> + of an essential process, validate the relevant unshare call in a |
129 |
>> + short-lived subprocess. An unshare call is considered valid if it |
130 |
>> + successfully executes in a short-lived subprocess. |
131 |
>> + """ |
132 |
>> + |
133 |
>> + instance = None |
134 |
>> + |
135 |
>> + def __init__(self): |
136 |
>> + self._results = {} |
137 |
>> + |
138 |
>> + def validate(self, flags): |
139 |
>> + """ |
140 |
>> + Validate unshare with the given flags. Results are cached. |
141 |
>> + |
142 |
>> + @rtype: int |
143 |
>> + @returns: errno value, or 0 if no error occurred. |
144 |
>> + """ |
145 |
>> + |
146 |
>> + if flags == 0: |
147 |
>> + return 0 |
148 |
>> + |
149 |
>> + result = self._results.get(flags) |
150 |
>> + if result is not None: |
151 |
>> + return result |
152 |
>> + |
153 |
>> + filename = find_library("c") |
154 |
>> + if filename is None: |
155 |
>> + return errno.ENOTSUP |
156 |
>> + |
157 |
>> + libc = LoadLibrary(filename) |
158 |
>> + if libc is None: |
159 |
>> + return errno.ENOTSUP |
160 |
>> + |
161 |
>> + parent_pipe, subproc_pipe = multiprocessing.Pipe(duplex=False) |
162 |
>> + |
163 |
>> + proc = multiprocessing.Process( |
164 |
>> + target=self._validator_subproc, |
165 |
>> + args=(libc.unshare, flags, subproc_pipe)) |
166 |
>> + proc.start() |
167 |
>> + subproc_pipe.close() |
168 |
>> + |
169 |
>> + result = parent_pipe.recv() |
170 |
>> + parent_pipe.close() |
171 |
>> + proc.join() |
172 |
>> + |
173 |
>> + self._results[flags] = result |
174 |
>> + return result |
175 |
>> + |
176 |
>> + def _validator_subproc(self, unshare, flags, subproc_pipe): |
177 |
>> + """ |
178 |
>> + Perform validation, and send results to parent process. |
179 |
>> + |
180 |
>> + @param unshare: unshare function |
181 |
>> + @type unshare: callable |
182 |
>> + @param flags: unshare flags |
183 |
>> + @type flags: int |
184 |
>> + @param subproc_pipe: connection to parent process |
185 |
>> + @type subproc_pipe: multiprocessing.Connection |
186 |
>> + """ |
187 |
>> + if unshare(flags) != 0: |
188 |
>> + subproc_pipe.send(ctypes.get_errno()) |
189 |
>> + else: |
190 |
>> + subproc_pipe.send(0) |
191 |
>> + subproc_pipe.close() |
192 |
>> + |
193 |
>> + |
194 |
>> +_unshare_validator.instance = _unshare_validator() |
195 |
>> + |
196 |
>> + |
197 |
>> def _setup_pipes(fd_pipes, close_fds=True, inheritable=None): |
198 |
>> """Setup pipes for a forked process. |
199 |
>> |
200 |
> |
201 |
> Can't we detect the failure in normally spawned ebuild process, |
202 |
> and restart it with unsharing disabled? Possibly cache the result to |
203 |
> avoid having to restart everything afterwards. |
204 |
|
205 |
We'd need an IPC mechanism to communicate the failure to the parent |
206 |
process so that it could retry. However, _exec is intended to exec an |
207 |
arbitrary child process, so it's not an appropriate place for IPC. |
208 |
-- |
209 |
Thanks, |
210 |
Zac |