Merge lp:~kyrofa/snap-confine/create_user_data_directory into lp:~snappy-dev/snap-confine/trunk

Proposed by Kyle Fazzari
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
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.

To post a comment you must log in.
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.

Revision history for this message
Seth Arnold (seth-arnold) wrote :

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

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
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

Revision history for this message
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_DATA=/create/path/to/anywhere ubuntu-core-launcher ...`, where ... are valid arguments to ubuntu-core-launcher.

Does setup_user_data() have to be called before dropping user privileges?

review: Needs Fixing
92. By Kyle Fazzari

Make the user data directory creation run after privileges are dropped.

Revision history for this message
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?

Revision history for this message
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.

Revision history for this message
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_USER_DATA_PATH, as well as SNAP_USER_DATA/../ and SNAP_APP_USER_DATA_PATH/../, because of the bug that you're trying to fix here. This change would happily follow symlinks that snaps had created in those locations since there's nothing to prevent the following of symlinks.

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-core-security, at the same time or after this change lands in the image, to remove access to those directories.

Yes, those white space changes sure make it more difficult to review! :)

review: Needs Fixing
93. By Kyle Fazzari

Refactor to use open/openat/mkdirat instead of just mkdir.

Revision history for this message
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.

Revision history for this message
Tyler Hicks (tyhicks) wrote :

Thanks for making those changes, Kyle. It all looks good to me now.

review: Approve
95. By Kyle Fazzari

Include O_CLOEXEC in the open flags.

Revision history for this message
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.

review: Needs Fixing
Revision history for this message
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.

review: Approve
Revision history for this message
Jamie Strandboge (jdstrand) wrote :

And for completeness, I've queued the ubuntu-core-security changes in http://bazaar.launchpad.net/~ubuntu-security/ubuntu-core-security/trunk/revision/193

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'
288Binary 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

Subscribers

People subscribed via source and target branches