Gentoo Archives: gentoo-portage-dev

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

Replies