Merge lp:~kyrofa/snap-confine/create_user_data_directory into lp:~snappy-dev/snap-confine/trunk
- create_user_data_directory
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 90 |
Proposed branch: | lp:~kyrofa/snap-confine/create_user_data_directory |
Merge into: | lp:~snappy-dev/snap-confine/trunk |
Diff against target: |
454 lines (+328/-76) 2 files modified
src/main.c (+166/-76) tests/test_create_user_data (+162/-0) |
To merge this branch: | bzr merge lp:~kyrofa/snap-confine/create_user_data_directory |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jamie Strandboge (community) | Approve | ||
Tyler Hicks (community) | Approve | ||
Review via email: mp+285360@code.launchpad.net |
Commit message
Add creation of user data directory.
Description of the change
Both Snappy binaries and services are provided the $SNAP_USER_DATA environment variable. However, the job of actually creating that directory has until now been left up to the Snappy binary wrapper. Which means that the $SNAP_USER_DATA directory is created for binaries, but actually points to a non-existing directory for services, since nothing creates it (bug #1527612).
This MP attempts to bring the creation of the user data directory into the one thing that both binaries and services have in common: the launcher.
- 90. By Kyle Fazzari
-
Add creation of user data directory.
Previously this was only handled within Snappy's binary wrappers, which
meant that it wasn't created for services. This is an attempt to bring
that functionality into the thing binaries and services have in common:
Ubuntu Core Launcher.
Seth Arnold (seth-arnold) wrote : | # |
Kyle Fazzari (kyrofa) wrote : | # |
Thanks for taking a look, Seth.
> Does this code run with more privileges than the services that it is working for?
Yes, the launcher runs with setuid root.
> If so, does something prevent the services from running at the same time?
No, nothing prevents services (or binaries) from running at the same time, but from what I've read mkdir() is atomic. Perhaps I misunderstood your concern here?
> If so, does something fix the directories' owner and group later?
Ah, great catch, I need to do that. Services would have been fine, but as-is this will create directories owned by root for binaries, which isn't what we want here. There are two ways I see to do that: the chown() syscall after the fact, or fork and drop privs before creating the directories. Normally I'd say the latter would be more secure so as to mitigate race condition attacks, but since we'd only ever be dropping privilege levels with the chown() (from root to user, never the other way around), I wonder if it's really a concern. Thoughts?
- 91. By Kyle Fazzari
-
Make sure the directory is owned by the runner, not root.
Kyle Fazzari (kyrofa) wrote : | # |
I've fixed that with a call to chown() as you can see in the most recent push. Let me know if there's a concern with it.
Seth Arnold (seth-arnold) wrote : | # |
The trouble is that while one mkdir() call is atomic, a series of them is not; and mkdir() will happily follow symbolic links in the path.
I think there's a few approaches that would work:
- perform the chown() operations in reverse order, so the snap owner never owns a directory that's being operated on
- perform all the mkdir operations with the effective uid of the snap owner, so they only ever have their permissions anyway
- use mkdirat(2) instead of mkdir(2) to ensure that the directories are being created in the proper parent directory regardless of rename() or symlink() or mount(.., MS_BIND) tricks
There's pros and cons to each of these; the mkdirat() version may still require the reverse-order chown() calls too.
Thanks
Tyler Hicks (tyhicks) wrote : | # |
Hi Kyle - The current MP isn't acceptable because it would allow an unprivileged user to create a path, owned by that user, anywhere in the filesystem by doing something like `SNAP_USER_
Does setup_user_data() have to be called before dropping user privileges?
- 92. By Kyle Fazzari
-
Make the user data directory creation run after privileges are dropped.
Kyle Fazzari (kyrofa) wrote : | # |
Oh my, what a glaring error that was!
Of course you're right Tyler-- the directory creation needs to happen before the apparmor/seccomp confinement, but should definitely happen after the privileges are dropped. My latest push reflects that.
Seth, I believe that actually addresses your concern as well, yes? Since now all mkdir operations are performed with the effective uid of whoever launched it?
Kyle Fazzari (kyrofa) wrote : | # |
Note on the previous push I also updated the indentation to be consistent in the main() function. I'm sorry it makes for more to review, but it made it really difficult to conform when there was no conformity.
Tyler Hicks (tyhicks) wrote : | # |
Thanks Kyle - This looks pretty good now but there's still one issue. Jamie tells me that snaps were granted control of the paths specified in SNAP_USER_DATA and SNAP_APP_
I'd suggest modifying the mkdir() loop to use openat() w/ O_NOFOLLOW and mkdirat(), as Seth originally suggested.
We'll need to update ubuntu-
Yes, those white space changes sure make it more difficult to review! :)
- 93. By Kyle Fazzari
-
Refactor to use open/openat/mkdirat instead of just mkdir.
Kyle Fazzari (kyrofa) wrote : | # |
Alright Tyler, I've refactored the mkpath function to use open/openat/mkdirat with O_NOFOLLOW instead of mkdir. It complicated it a bit, but I think it's still readable. I also refactored the tests to be more readable.
- 94. By Kyle Fazzari
-
Use strtok_r for mkpath tokenization.
Tyler Hicks (tyhicks) wrote : | # |
Thanks for making those changes, Kyle. It all looks good to me now.
- 95. By Kyle Fazzari
-
Include O_CLOEXEC in the open flags.
Jamie Strandboge (jdstrand) wrote : | # |
This MP contains whitespace changes that make it difficult to review and is inconsistent with the current codebase. For example, it seems you went from 4 spaces to 3 spaces in main(). Can you update the MP to remove these whitespace changes?
Once that is done, I will sponsor the upload.
Jamie Strandboge (jdstrand) wrote : | # |
Ok, on closer inspection I see that these whitespace changes are sorely needed. Looking at diff -Naur -wb, I see the non-whitespace changes and they are clear and fine. Thank you for the patch and for adding O_CLOEXEC.
Jamie Strandboge (jdstrand) wrote : | # |
And for completeness, I've queued the ubuntu-
Preview Diff
1 | === modified file 'src/main.c' |
2 | --- src/main.c 2016-01-26 15:11:20 +0000 |
3 | +++ src/main.c 2016-02-10 15:44:24 +0000 |
4 | @@ -212,7 +212,7 @@ |
5 | |
6 | // Create a 0700 base directory, this is the base dir that is |
7 | // protected from other users. |
8 | - // |
9 | + // |
10 | // Under that basedir, we put a 1777 /tmp dir that is then bind |
11 | // mounted for the applications to use |
12 | must_snprintf(tmpdir, sizeof(tmpdir), "/tmp/snap.%d_%s_XXXXXX", uid, appname); |
13 | @@ -233,7 +233,7 @@ |
14 | die("unable to create /tmp inside private dir"); |
15 | } |
16 | umask(old_mask); |
17 | - |
18 | + |
19 | // MS_BIND is there from linux 2.4 |
20 | if (mount(tmpdir, "/tmp", NULL, MS_BIND, NULL) != 0) { |
21 | die("unable to bind private /tmp"); |
22 | @@ -304,20 +304,108 @@ |
23 | if (unshare(CLONE_NEWNS) < 0) { |
24 | die("unable to set up mount namespace"); |
25 | } |
26 | - |
27 | + |
28 | // make our "/" a rslave of the real "/". this means that |
29 | // mounts from the host "/" get propagated to our namespace |
30 | // (i.e. we see new media mounts) |
31 | if (mount("none", "/", NULL, MS_REC|MS_SLAVE, NULL) != 0) { |
32 | die("can not make make / rslave"); |
33 | } |
34 | -} |
35 | +} |
36 | + |
37 | +void mkpath(const char *const path) { |
38 | + // If asked to create an empty path, return immediately. |
39 | + if (strlen(path) == 0) { |
40 | + return; |
41 | + } |
42 | + |
43 | + // We're going to use strtok_r, which needs to modify the path, so we'll make |
44 | + // a copy of it. |
45 | + char *path_copy = strdup(path); |
46 | + if (path_copy == NULL) { |
47 | + die("failed to create user data directory"); |
48 | + } |
49 | + |
50 | + // Open flags to use while we walk the user data path: |
51 | + // - Don't follow symlinks |
52 | + // - Don't allow child access to file descriptor |
53 | + // - Only open a directory (fail otherwise) |
54 | + int open_flags = O_NOFOLLOW | O_CLOEXEC | O_DIRECTORY; |
55 | + |
56 | + // We're going to create each path segment via openat/mkdirat calls instead |
57 | + // of mkdir calls, to avoid following symlinks and placing the user data |
58 | + // directory somewhere we never intended for it to go. The first step is to |
59 | + // get an initial file descriptor. |
60 | + int fd = AT_FDCWD; |
61 | + if (path_copy[0] == '/') { |
62 | + fd = open("/", open_flags); |
63 | + if (fd < 0) { |
64 | + free(path_copy); |
65 | + die("failed to create user data directory"); |
66 | + } |
67 | + } |
68 | + |
69 | + // strtok_r needs a pointer to keep track of where it is in the string. |
70 | + char *path_walker; |
71 | + |
72 | + // Initialize tokenizer and obtain first path segment. |
73 | + char *path_segment = strtok_r(path_copy, "/", &path_walker); |
74 | + while (path_segment) { |
75 | + // Try to create the directory. It's okay if it already existed, but any |
76 | + // other error is fatal. |
77 | + if (mkdirat(fd, path_segment, 0755) < 0 && errno != EEXIST) { |
78 | + close(fd); |
79 | + free(path_copy); |
80 | + die("failed to create user data directory"); |
81 | + } |
82 | + |
83 | + // Open the parent directory we just made (and close the previous one) so |
84 | + // we can continue down the path. |
85 | + int previous_fd = fd; |
86 | + fd = openat(fd, path_segment, open_flags); |
87 | + close(previous_fd); |
88 | + if (fd < 0) { |
89 | + free(path_copy); |
90 | + die("failed to create user data directory"); |
91 | + } |
92 | + |
93 | + // Obtain the next path segment. |
94 | + path_segment = strtok_r(NULL, "/", &path_walker); |
95 | + } |
96 | + |
97 | + // Close the descriptor for the final directory in the path. |
98 | + close(fd); |
99 | + |
100 | + free(path_copy); |
101 | +} |
102 | + |
103 | +void setup_user_data() { |
104 | + const char *user_data = getenv("SNAP_USER_DATA"); |
105 | + |
106 | + // If $SNAP_USER_DATA wasn't defined, check the deprecated |
107 | + // $SNAP_APP_USER_DATA_PATH. |
108 | + if (user_data == NULL) { |
109 | + user_data = getenv("SNAP_APP_USER_DATA_PATH"); |
110 | + // If it's still not defined, there's nothing to do. No need to die, |
111 | + // there's simply no directory to create. |
112 | + if (user_data == NULL) { |
113 | + return; |
114 | + } |
115 | + } |
116 | + |
117 | + // Only support absolute paths. |
118 | + if (user_data[0] != '/') { |
119 | + die("user data directory must be an absolute path"); |
120 | + } |
121 | + |
122 | + mkpath(user_data); |
123 | +} |
124 | |
125 | int main(int argc, char **argv) |
126 | { |
127 | const int NR_ARGS = 3; |
128 | if(argc < NR_ARGS+1) |
129 | - die("Usage: %s <appname> <apparmor> <binary>", argv[0]); |
130 | + die("Usage: %s <appname> <apparmor> <binary>", argv[0]); |
131 | |
132 | const char *appname = argv[1]; |
133 | const char *aa_profile = argv[2]; |
134 | @@ -329,78 +417,80 @@ |
135 | // this code always needs to run as root for the cgroup/udev setup, |
136 | // however for the tests we allow it to run as non-root |
137 | if(geteuid() != 0 && getenv("UBUNTU_CORE_LAUNCHER_NO_ROOT") == NULL) { |
138 | - die("need to run as root or suid"); |
139 | + die("need to run as root or suid"); |
140 | } |
141 | |
142 | if(geteuid() == 0) { |
143 | - // verify binary path |
144 | - char snaps_prefix[128]; |
145 | - must_snprintf(snaps_prefix, sizeof(snaps_prefix), "/snaps/%s/", appname); |
146 | - if (strstr(binary, snaps_prefix) != binary) |
147 | - die("binary must be inside /snaps/%s/", appname); |
148 | - |
149 | - // ensure we run in our own slave mount namespace, this will |
150 | - // create a new mount namespace and make it a slave of "/" |
151 | - // |
152 | - // Note that this means that no mount actions inside our |
153 | - // namespace are propagated to the main "/". We need this |
154 | - // both for the private /tmp we create and for the bind |
155 | - // mounts we do on a classic ubuntu system |
156 | - // |
157 | - // This also means you can't run an automount daemon unter |
158 | - // this launcher |
159 | - setup_slave_mount_namespace(); |
160 | - |
161 | - // do the mounting if run on a non-native snappy system |
162 | - if(is_running_on_classic_ubuntu()) { |
163 | - setup_snappy_os_mounts(); |
164 | - } |
165 | - |
166 | - // set up private mounts |
167 | - setup_private_mount(appname); |
168 | - |
169 | - // this needs to happen as root |
170 | - if(snappy_udev_setup_required(appname)) { |
171 | - setup_devices_cgroup(appname); |
172 | - setup_udev_snappy_assign(appname); |
173 | - } |
174 | - |
175 | - // the rest does not so drop privs back to calling user |
176 | - unsigned real_uid = getuid(); |
177 | - unsigned real_gid = getgid(); |
178 | - |
179 | - // Note that we do not call setgroups() here because its ok |
180 | - // that the user keeps the groups he already belongs to |
181 | - if (setgid(real_gid) != 0) |
182 | - die("setgid failed"); |
183 | - if (setuid(real_uid) != 0) |
184 | - die("setuid failed"); |
185 | - |
186 | - if(real_gid != 0 && (getuid() == 0 || geteuid() == 0)) |
187 | - die("dropping privs did not work"); |
188 | - if(real_uid != 0 && (getgid() == 0 || getegid() == 0)) |
189 | - die("dropping privs did not work"); |
190 | - } |
191 | - |
192 | - //https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement#ubuntu-snapp-launch |
193 | - |
194 | - int rc = 0; |
195 | - // set apparmor rules |
196 | - rc = aa_change_onexec(aa_profile); |
197 | - if (rc != 0) { |
198 | - if (getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) |
199 | - die("aa_change_onexec failed with %i", rc); |
200 | - } |
201 | - |
202 | - // set seccomp |
203 | - rc = seccomp_load_filters(aa_profile); |
204 | - if (rc != 0) |
205 | - die("seccomp_load_filters failed with %i", rc); |
206 | - |
207 | - // and exec the new binary |
208 | - argv[NR_ARGS] = (char*)binary, |
209 | - execv(binary, (char *const*)&argv[NR_ARGS]); |
210 | - perror("execv failed"); |
211 | - return 1; |
212 | + // verify binary path |
213 | + char snaps_prefix[128]; |
214 | + must_snprintf(snaps_prefix, sizeof(snaps_prefix), "/snaps/%s/", appname); |
215 | + if (strstr(binary, snaps_prefix) != binary) |
216 | + die("binary must be inside /snaps/%s/", appname); |
217 | + |
218 | + // ensure we run in our own slave mount namespace, this will |
219 | + // create a new mount namespace and make it a slave of "/" |
220 | + // |
221 | + // Note that this means that no mount actions inside our |
222 | + // namespace are propagated to the main "/". We need this |
223 | + // both for the private /tmp we create and for the bind |
224 | + // mounts we do on a classic ubuntu system |
225 | + // |
226 | + // This also means you can't run an automount daemon unter |
227 | + // this launcher |
228 | + setup_slave_mount_namespace(); |
229 | + |
230 | + // do the mounting if run on a non-native snappy system |
231 | + if(is_running_on_classic_ubuntu()) { |
232 | + setup_snappy_os_mounts(); |
233 | + } |
234 | + |
235 | + // set up private mounts |
236 | + setup_private_mount(appname); |
237 | + |
238 | + // this needs to happen as root |
239 | + if(snappy_udev_setup_required(appname)) { |
240 | + setup_devices_cgroup(appname); |
241 | + setup_udev_snappy_assign(appname); |
242 | + } |
243 | + |
244 | + // the rest does not so drop privs back to calling user |
245 | + unsigned real_uid = getuid(); |
246 | + unsigned real_gid = getgid(); |
247 | + |
248 | + // Note that we do not call setgroups() here because its ok |
249 | + // that the user keeps the groups he already belongs to |
250 | + if (setgid(real_gid) != 0) |
251 | + die("setgid failed"); |
252 | + if (setuid(real_uid) != 0) |
253 | + die("setuid failed"); |
254 | + |
255 | + if(real_gid != 0 && (getuid() == 0 || geteuid() == 0)) |
256 | + die("dropping privs did not work"); |
257 | + if(real_uid != 0 && (getgid() == 0 || getegid() == 0)) |
258 | + die("dropping privs did not work"); |
259 | + } |
260 | + |
261 | + // Ensure that the user data path exists. |
262 | + setup_user_data(); |
263 | + |
264 | + //https://wiki.ubuntu.com/SecurityTeam/Specifications/SnappyConfinement#ubuntu-snapp-launch |
265 | + |
266 | + int rc = 0; |
267 | + // set apparmor rules |
268 | + rc = aa_change_onexec(aa_profile); |
269 | + if (rc != 0) { |
270 | + if (getenv("SNAPPY_LAUNCHER_INSIDE_TESTS") == NULL) |
271 | + die("aa_change_onexec failed with %i", rc); |
272 | + } |
273 | + |
274 | + // set seccomp |
275 | + rc = seccomp_load_filters(aa_profile); |
276 | + if (rc != 0) |
277 | + die("seccomp_load_filters failed with %i", rc); |
278 | + |
279 | + // and exec the new binary |
280 | + argv[NR_ARGS] = (char*)binary, |
281 | + execv(binary, (char *const*)&argv[NR_ARGS]); |
282 | + perror("execv failed"); |
283 | + return 1; |
284 | } |
285 | - |
286 | |
287 | === added file 'src/ubuntu-core-launcher' |
288 | Binary files src/ubuntu-core-launcher 1970-01-01 00:00:00 +0000 and src/ubuntu-core-launcher 2016-02-10 15:44:24 +0000 differ |
289 | === added file 'tests/test_create_user_data' |
290 | --- tests/test_create_user_data 1970-01-01 00:00:00 +0000 |
291 | +++ tests/test_create_user_data 2016-02-10 15:44:24 +0000 |
292 | @@ -0,0 +1,162 @@ |
293 | +#!/bin/sh |
294 | + |
295 | +set -e |
296 | + |
297 | +. $(pwd)/common.sh |
298 | + |
299 | +cat >$TMP/unrestricted <<EOF |
300 | +# some comment |
301 | +@unrestricted |
302 | +EOF |
303 | + |
304 | +cat >$TMP/restricted <<EOF |
305 | +# filter that works ok for true |
306 | +open |
307 | +close |
308 | + |
309 | +mmap |
310 | +munmap |
311 | +mprotect |
312 | + |
313 | +fstat |
314 | +access |
315 | +read |
316 | + |
317 | +brk |
318 | +execve |
319 | + |
320 | +arch_prctl |
321 | +exit_group |
322 | + |
323 | +# unknown syscalls are ignore |
324 | +i-dont-exit |
325 | +EOF |
326 | + |
327 | +# $1: Path to check existence for and potentially remove at the end |
328 | +# $2: Profile name |
329 | +# $3: True if success is expected, false otherwise |
330 | +run_launcher() { |
331 | + pass=false |
332 | + if $L appid $2 /bin/true 2>/dev/null; then |
333 | + if $3; then |
334 | + if [ ! -d "$1" ]; then |
335 | + pass=false |
336 | + fi |
337 | + |
338 | + pass=true |
339 | + else |
340 | + pass=false |
341 | + fi |
342 | + else |
343 | + if $3; then |
344 | + pass=false |
345 | + else |
346 | + pass=true |
347 | + fi |
348 | + fi |
349 | + |
350 | + if [ -d "$1" ]; then |
351 | + rmdir $1 |
352 | + fi |
353 | + |
354 | + if $pass; then |
355 | + return 0 |
356 | + else |
357 | + return 1 |
358 | + fi |
359 | +} |
360 | + |
361 | +# $1 = $SNAP_USER_DATA definition |
362 | +# $2 = Profile name |
363 | +# $3 = True if success is expected, false otherwise |
364 | +run_current() { |
365 | + export SNAP_USER_DATA=$1 |
366 | + run_launcher $1 $2 $3 |
367 | + pass=$? |
368 | + unset SNAP_USER_DATA |
369 | + |
370 | + return $pass |
371 | +} |
372 | + |
373 | +# $1 = $SNAP_APP_USER_DATA_PATH definition |
374 | +# $2 = Profile name |
375 | +# $3 = True if success is expected, false otherwise |
376 | +run_deprecated() { |
377 | + export SNAP_APP_USER_DATA_PATH=$1 |
378 | + run_launcher $1 $2 $3 |
379 | + pass=$? |
380 | + unset SNAP_APP_USER_DATA_PATH |
381 | + |
382 | + return $pass |
383 | +} |
384 | + |
385 | +# $1 = User data path |
386 | +# $2 = Profile name |
387 | +# $3 = True if success is expected, false otherwise |
388 | +run_both() { |
389 | + run_current $1 $2 $3 |
390 | + current_pass=$? |
391 | + run_deprecated $1 $2 $3 |
392 | + deprecated_pass=$? |
393 | + |
394 | + if [ $current_pass -a $deprecated_pass ]; then |
395 | + PASS |
396 | + else |
397 | + FAIL |
398 | + fi |
399 | +} |
400 | + |
401 | +printf "Test that an unrestricted launcher creates user data" |
402 | +run_both $TMP/user_data unrestricted true |
403 | + |
404 | +printf "Test that a restricted launcher creates user data" |
405 | +run_both $TMP/user_data restricted true |
406 | + |
407 | +printf "Test that an unrestricted launcher creates user data with parent directory" |
408 | +run_both $TMP/parent/user_data unrestricted true |
409 | + |
410 | +printf "Test that a restricted launcher creates user data with parent directory" |
411 | +run_both $TMP/parent/user_data restricted true |
412 | + |
413 | +printf "Test that user data can contain multiple path separators" |
414 | +run_both $TMP//user_data unrestricted true |
415 | + |
416 | +printf "Test that user data must be absolute" |
417 | +run_both "../foo" unrestricted false |
418 | + |
419 | +printf "Testing that user data cannot be contained within a symlink" |
420 | +mkdir $TMP/nefarious_parent |
421 | +ln -s $TMP/nefarious_parent $TMP/parent |
422 | +run_both $TMP/parent/user_data unrestricted, false |
423 | + |
424 | +printf "Test that an unrestricted launcher works when user data exists (current)" |
425 | +mkdir $TMP/user_data |
426 | +if run_current $TMP/user_data unrestricted true; then |
427 | + PASS |
428 | +else |
429 | + FAIL |
430 | +fi |
431 | + |
432 | +printf "Test that an restricted launcher works when user data exists (current)" |
433 | +mkdir $TMP/user_data |
434 | +if run_current $TMP/user_data restricted true; then |
435 | + PASS |
436 | +else |
437 | + FAIL |
438 | +fi |
439 | + |
440 | +printf "Test that an unrestricted launcher works when user data exists (deprecated)" |
441 | +mkdir $TMP/user_data |
442 | +if run_deprecated $TMP/user_data unrestricted true; then |
443 | + PASS |
444 | +else |
445 | + FAIL |
446 | +fi |
447 | + |
448 | +printf "Test that an restricted launcher works when user data exists (deprecated)" |
449 | +mkdir $TMP/user_data |
450 | +if run_deprecated $TMP/user_data restricted true; then |
451 | + PASS |
452 | +else |
453 | + FAIL |
454 | +fi |
Does this code run with more privileges than the services that it is working for?
If so, does something prevent the services from running at the same time?
If so, does something fix the directories' owner and group later?
Thanks