Gentoo Archives: gentoo-portage-dev

From: Zac Medico <zmedico@g.o>
To: "Michał Górny" <mgorny@g.o>, gentoo-portage-dev@l.g.o
Cc: Zac Medico <zmedico@g.o>
Subject: Re: [gentoo-portage-dev] [PATCH] process.spawn: validate unshare calls (bug 673900)
Date: Sun, 30 Dec 2018 09:07:01
Message-Id: 4f97cf4e-0da5-f6f8-5a90-5008fae06725@gentoo.org
In Reply to: Re: [gentoo-portage-dev] [PATCH] process.spawn: validate unshare calls (bug 673900) by "Michał Górny"
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

Attachments

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

Replies