1 |
commit: f3e51a930312422cc78b693a247b7c5704ac90a2 |
2 |
Author: Sergei Trofimovich <slyfox <AT> gentoo <DOT> org> |
3 |
AuthorDate: Sun Jan 6 09:32:55 2019 +0000 |
4 |
Commit: Sergei Trofimovich <slyfox <AT> gentoo <DOT> org> |
5 |
CommitDate: Tue Jan 8 22:48:44 2019 +0000 |
6 |
URL: https://gitweb.gentoo.org/proj/sandbox.git/commit/?id=f3e51a93 |
7 |
|
8 |
exec*() wrappers: never mutate 'environ' of host process |
9 |
|
10 |
In bug #669702 gcc exposed sandbox bug where execv() |
11 |
wrapper changed 'environ' global variable underneath. |
12 |
|
13 |
A few GNU projects (pex_unix_exec_child in gcc and gdb) |
14 |
use the following idiom: |
15 |
|
16 |
for (;;) { |
17 |
vfork(); |
18 |
char ** save_environ = environ; // [1] |
19 |
if (child) { |
20 |
environ = child_environ; // [2] |
21 |
execv(payload); // [3] |
22 |
} |
23 |
if (parent) { |
24 |
environ = save_environ; // [4] |
25 |
... |
26 |
waitpid(child, ...); |
27 |
} |
28 |
} |
29 |
|
30 |
Code above assumes that execv() does not mutate 'environ'. |
31 |
|
32 |
In case of #669702 sandbox's execv() wrapper at '[3]' mutated |
33 |
'environ' and relocated it (via maloc()/free() internally). |
34 |
This caused '[4]' to point 'environ' fo freed location. |
35 |
|
36 |
The change fixes it in a following way: |
37 |
- execv() call now works more like execve() call by mutating |
38 |
external array and substitutes 'environ' only for a period |
39 |
of 'execv()' execution. |
40 |
- add basic execv()/'environ' corruption test |
41 |
|
42 |
Tested on: |
43 |
- linux/glibc-2.28 |
44 |
- linux/uclibc-ng-1.0.31 |
45 |
|
46 |
Reported-and-tested-by: Walther |
47 |
Reported-by: 0x6d6174 <AT> posteo.de |
48 |
Reported-by: Andrey Korolyov |
49 |
Bug: https://bugs.gentoo.org/669702 |
50 |
Signed-off-by: Sergei Trofimovich <slyfox <AT> gentoo.org> |
51 |
|
52 |
libsandbox/libsandbox.c | 92 +++++++++++++++---------------- |
53 |
libsandbox/libsandbox.h | 17 +++++- |
54 |
libsandbox/wrapper-funcs/__wrapper_exec.c | 15 +++-- |
55 |
tests/Makefile.am | 1 + |
56 |
tests/execv-0.c | 21 +++++++ |
57 |
tests/execv-1.sh | 4 ++ |
58 |
tests/execv.at | 1 + |
59 |
7 files changed, 96 insertions(+), 55 deletions(-) |
60 |
|
61 |
diff --git a/libsandbox/libsandbox.c b/libsandbox/libsandbox.c |
62 |
index e0c9d1a..77e9959 100644 |
63 |
--- a/libsandbox/libsandbox.c |
64 |
+++ b/libsandbox/libsandbox.c |
65 |
@@ -1122,10 +1122,18 @@ typedef struct { |
66 |
* |
67 |
* XXX: Might be much nicer if we could serialize these vars behind the back of |
68 |
* the program. Might be hard to handle LD_PRELOAD though ... |
69 |
+ * |
70 |
+ * execv*() must never modify environment inplace with |
71 |
+ * setenv/putenv/unsetenv as it can relocate 'environ' and break |
72 |
+ * vfork()/execv() users: https://bugs.gentoo.org/669702 |
73 |
*/ |
74 |
-char **sb_check_envp(char **envp, size_t *mod_cnt, bool insert) |
75 |
+struct sb_envp_ctx sb_new_envp(char **envp, bool insert) |
76 |
{ |
77 |
- char **my_env; |
78 |
+ struct sb_envp_ctx r = { |
79 |
+ .sb_envp = envp, |
80 |
+ .orig_envp = envp, |
81 |
+ .__mod_cnt = 0, |
82 |
+ }; |
83 |
char *entry; |
84 |
size_t count, i; |
85 |
env_pair vars[] = { |
86 |
@@ -1152,7 +1160,7 @@ char **sb_check_envp(char **envp, size_t *mod_cnt, bool insert) |
87 |
/* If sandbox is explicitly disabled, do not propagate the vars |
88 |
* and just return user's envp */ |
89 |
if (!sbcontext.on) |
90 |
- return envp; |
91 |
+ return r; |
92 |
|
93 |
/* First figure out how many vars are already in the env */ |
94 |
found_var_cnt = 0; |
95 |
@@ -1188,7 +1196,7 @@ char **sb_check_envp(char **envp, size_t *mod_cnt, bool insert) |
96 |
if ((insert && num_vars == found_var_cnt) || |
97 |
(!insert && found_var_cnt == 0)) |
98 |
/* Use the user's envp */ |
99 |
- return envp; |
100 |
+ return r; |
101 |
|
102 |
/* Ok, we need to create our own envp, as we need to restore stuff |
103 |
* and we should not touch the user's envp. First we add our vars, |
104 |
@@ -1208,61 +1216,50 @@ char **sb_check_envp(char **envp, size_t *mod_cnt, bool insert) |
105 |
vars[13].value = "1"; |
106 |
} |
107 |
|
108 |
- my_env = NULL; |
109 |
+ char ** my_env = NULL; |
110 |
if (!insert) { |
111 |
- if (mod_cnt) { |
112 |
- str_list_for_each_item(envp, entry, count) { |
113 |
- for (i = 0; i < num_vars; ++i) |
114 |
- if (i != 12 && is_env_var(entry, vars[i].name, vars[i].len)) { |
115 |
- (*mod_cnt)++; |
116 |
- goto skip; |
117 |
- } |
118 |
- str_list_add_item(my_env, entry, error); |
119 |
- skip: ; |
120 |
- } |
121 |
- } else { |
122 |
+ str_list_for_each_item(envp, entry, count) { |
123 |
for (i = 0; i < num_vars; ++i) |
124 |
- if (i != 12) unsetenv(vars[i].name); |
125 |
+ if (i != 12 /* LD_LIBRARY_PATH index */ |
126 |
+ && is_env_var(entry, vars[i].name, vars[i].len)) { |
127 |
+ r.__mod_cnt++; |
128 |
+ goto skip; |
129 |
+ } |
130 |
+ str_list_add_item(my_env, entry, error); |
131 |
+ skip: ; |
132 |
} |
133 |
} else { |
134 |
- if (mod_cnt) { |
135 |
- /* Count directly due to variability with LD_PRELOAD and the value |
136 |
- * logic below. Getting out of sync can mean memory corruption. */ |
137 |
- *mod_cnt = 0; |
138 |
- if (unlikely(merge_ld_preload)) { |
139 |
- str_list_add_item(my_env, ld_preload, error); |
140 |
- (*mod_cnt)++; |
141 |
- } |
142 |
- for (i = 0; i < num_vars; ++i) { |
143 |
- if (found_vars[i] || !vars[i].value) |
144 |
- continue; |
145 |
- str_list_add_item_env(my_env, vars[i].name, vars[i].value, error); |
146 |
- (*mod_cnt)++; |
147 |
- } |
148 |
+ /* Count directly due to variability with LD_PRELOAD and the value |
149 |
+ * logic below. Getting out of sync can mean memory corruption. */ |
150 |
+ r.__mod_cnt = 0; |
151 |
+ if (unlikely(merge_ld_preload)) { |
152 |
+ str_list_add_item(my_env, ld_preload, error); |
153 |
+ r.__mod_cnt++; |
154 |
+ } |
155 |
+ for (i = 0; i < num_vars; ++i) { |
156 |
+ if (found_vars[i] || !vars[i].value) |
157 |
+ continue; |
158 |
+ str_list_add_item_env(my_env, vars[i].name, vars[i].value, error); |
159 |
+ r.__mod_cnt++; |
160 |
+ } |
161 |
|
162 |
- str_list_for_each_item(envp, entry, count) { |
163 |
- if (unlikely(merge_ld_preload && is_env_var(entry, vars[0].name, vars[0].len))) |
164 |
- continue; |
165 |
- str_list_add_item(my_env, entry, error); |
166 |
- } |
167 |
- } else { |
168 |
- if (unlikely(merge_ld_preload)) |
169 |
- putenv(ld_preload); |
170 |
- for (i = 0; i < num_vars; ++i) { |
171 |
- if (found_vars[i] || !vars[i].value) |
172 |
- continue; |
173 |
- setenv(vars[i].name, vars[i].value, 1); |
174 |
- } |
175 |
+ str_list_for_each_item(envp, entry, count) { |
176 |
+ if (unlikely(merge_ld_preload && is_env_var(entry, vars[0].name, vars[0].len))) |
177 |
+ continue; |
178 |
+ str_list_add_item(my_env, entry, error); |
179 |
} |
180 |
} |
181 |
|
182 |
error: |
183 |
- return my_env; |
184 |
+ r.sb_envp = my_env; |
185 |
+ return r; |
186 |
} |
187 |
|
188 |
-void sb_cleanup_envp(char **envp, size_t mod_cnt) |
189 |
+void sb_free_envp(struct sb_envp_ctx * envp_ctx) |
190 |
{ |
191 |
/* We assume all the stuffed vars are at the start */ |
192 |
+ size_t mod_cnt = envp_ctx->__mod_cnt; |
193 |
+ char ** envp = envp_ctx->sb_envp; |
194 |
size_t i; |
195 |
for (i = 0; i < mod_cnt; ++i) |
196 |
free(envp[i]); |
197 |
@@ -1271,5 +1268,6 @@ void sb_cleanup_envp(char **envp, size_t mod_cnt) |
198 |
* entries except for LD_PRELOAD. All the other entries are |
199 |
* pointers to existing envp memory. |
200 |
*/ |
201 |
- free(envp); |
202 |
+ if (envp != envp_ctx->orig_envp) |
203 |
+ free(envp); |
204 |
} |
205 |
|
206 |
diff --git a/libsandbox/libsandbox.h b/libsandbox/libsandbox.h |
207 |
index 63882e7..70fc422 100644 |
208 |
--- a/libsandbox/libsandbox.h |
209 |
+++ b/libsandbox/libsandbox.h |
210 |
@@ -56,8 +56,21 @@ void *get_dlsym(const char *symname, const char *symver); |
211 |
|
212 |
extern char sandbox_lib[SB_PATH_MAX]; |
213 |
extern bool sandbox_on; |
214 |
-char **sb_check_envp(char **envp, size_t *mod_cnt, bool insert); |
215 |
-void sb_cleanup_envp(char **envp, size_t mod_cnt); |
216 |
+ |
217 |
+struct sb_envp_ctx { |
218 |
+ /* Sandboxified environment with sandbox variables injected. |
219 |
+ * Allocated by 'sb_new_envp', freed by 'sb_free_envp'. */ |
220 |
+ char ** sb_envp; |
221 |
+ /* Original environment. Passed from outside. */ |
222 |
+ char ** orig_envp; |
223 |
+ |
224 |
+ /* Internal counter to free. |
225 |
+ * Not to be used outside sb_{new,free}_envp. */ |
226 |
+ size_t __mod_cnt; |
227 |
+}; |
228 |
+struct sb_envp_ctx sb_new_envp(char **envp, bool insert); |
229 |
+void sb_free_envp(struct sb_envp_ctx * envp_ctx); |
230 |
+ |
231 |
extern pid_t trace_pid; |
232 |
|
233 |
extern void sb_lock(void); |
234 |
|
235 |
diff --git a/libsandbox/wrapper-funcs/__wrapper_exec.c b/libsandbox/wrapper-funcs/__wrapper_exec.c |
236 |
index 226c0c0..974156e 100644 |
237 |
--- a/libsandbox/wrapper-funcs/__wrapper_exec.c |
238 |
+++ b/libsandbox/wrapper-funcs/__wrapper_exec.c |
239 |
@@ -275,10 +275,11 @@ WRAPPER_RET_TYPE WRAPPER_NAME(WRAPPER_ARGS_PROTO) |
240 |
#endif |
241 |
|
242 |
#ifdef EXEC_MY_ENV |
243 |
- size_t mod_cnt; |
244 |
- char **my_env = sb_check_envp((char **)envp, &mod_cnt, run_in_process); |
245 |
+ struct sb_envp_ctx ec = sb_new_envp((char**)envp, run_in_process); |
246 |
+ char **my_env = ec.sb_envp; |
247 |
#else |
248 |
- sb_check_envp(environ, NULL, run_in_process); |
249 |
+ struct sb_envp_ctx ec = sb_new_envp(environ, run_in_process); |
250 |
+ environ = ec.sb_envp; |
251 |
#endif |
252 |
|
253 |
restore_errno(); |
254 |
@@ -287,10 +288,12 @@ WRAPPER_RET_TYPE WRAPPER_NAME(WRAPPER_ARGS_PROTO) |
255 |
#endif |
256 |
result = SB_HIDDEN_FUNC(WRAPPER_NAME)(EXEC_ARGS); |
257 |
|
258 |
-#ifdef EXEC_MY_ENV |
259 |
- if (my_env != envp) |
260 |
- sb_cleanup_envp(my_env, mod_cnt); |
261 |
+#ifndef EXEC_MY_ENV |
262 |
+ /* https://bugs.gentoo.org/669702: maintain illusion |
263 |
+ or unmodified 'environ'. */ |
264 |
+ environ = ec.orig_envp; |
265 |
#endif |
266 |
+ sb_free_envp(&ec); |
267 |
|
268 |
#ifdef EXEC_RECUR_CHECK |
269 |
--recursive; |
270 |
|
271 |
diff --git a/tests/Makefile.am b/tests/Makefile.am |
272 |
index d98898f..3baf5b1 100644 |
273 |
--- a/tests/Makefile.am |
274 |
+++ b/tests/Makefile.am |
275 |
@@ -18,6 +18,7 @@ check_PROGRAMS = \ |
276 |
chown-0 \ |
277 |
creat-0 \ |
278 |
creat64-0 \ |
279 |
+ execv-0 \ |
280 |
execvp-0 \ |
281 |
faccessat-0 \ |
282 |
fchmodat-0 \ |
283 |
|
284 |
diff --git a/tests/execv-0.c b/tests/execv-0.c |
285 |
new file mode 100644 |
286 |
index 0000000..bea0aaa |
287 |
--- /dev/null |
288 |
+++ b/tests/execv-0.c |
289 |
@@ -0,0 +1,21 @@ |
290 |
+/* |
291 |
+ * A simple wrapper for execv that validates environment |
292 |
+ * is not corrupted by wrapper: https://bugs.gentoo.org/669702 |
293 |
+ */ |
294 |
+ |
295 |
+#define _GNU_SOURCE /* for environ */ |
296 |
+#include <stdio.h> |
297 |
+#include "tests.h" |
298 |
+ |
299 |
+int main(int argc, char *argv[]) |
300 |
+{ |
301 |
+ char* execv_argv[] = {"nope", (char*)NULL,}; |
302 |
+ char* execv_environ[] = {"FOO=1", (char*)NULL,}; |
303 |
+ environ = execv_environ; |
304 |
+ execv("./does/not/exist", execv_argv); |
305 |
+ if (environ != execv_environ) { |
306 |
+ fprintf(stderr, "environ was changed unexpectedly by execv wrapper\n"); |
307 |
+ return 1; |
308 |
+ } |
309 |
+ return 0; |
310 |
+} |
311 |
|
312 |
diff --git a/tests/execv-1.sh b/tests/execv-1.sh |
313 |
new file mode 100755 |
314 |
index 0000000..ab816b0 |
315 |
--- /dev/null |
316 |
+++ b/tests/execv-1.sh |
317 |
@@ -0,0 +1,4 @@ |
318 |
+#!/bin/sh |
319 |
+# Make sure execv run does not corrup 'environ' of caller process: |
320 |
+# https://bugs.gentoo.org/669702 |
321 |
+timeout -s KILL 10 execv-0 |
322 |
|
323 |
diff --git a/tests/execv.at b/tests/execv.at |
324 |
new file mode 100644 |
325 |
index 0000000..081d7d2 |
326 |
--- /dev/null |
327 |
+++ b/tests/execv.at |
328 |
@@ -0,0 +1 @@ |
329 |
+SB_CHECK(1) |