Gentoo Archives: gentoo-portage-dev

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

Attachments

File name MIME type
signature.asc application/pgp-signature