From 5fdba4ed2842acaeb2488d30f7c83bb17556eeaa Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Wed, 17 Jan 2018 15:39:48 -0500 Subject: create parent paths of target mounts as needed Currently if you want to bind mount a single subdir, you have to make sure to create the full parent directory chain. For example, if you want /var/lib/timezone/ but not the rest of /var, you have to do: -k none,/var,tmpfs -k none,/var/lib,tmpfs -b /var/lib/timezone/ For every additional subdir, you need to add another -k option just to do an [effective] mkdir with a tmpfs mount. The current -k/-b behavior is to run mkdir if the target doesn't already exist, but only for the final target. Lets extend it to also create any missing parent paths, so now only the base path needs to be writable: -k none,/var,tmpfs -b /var/lib/timezone/ Bug: None Test: `minijail0 --profile minimalistic-mountns -k none,/var,tmpfs -b /var/lib/timezone /bin/date` works Change-Id: I7f36bcb445ce40ed66a9403a4ee1c1fe3f9e5ea8 --- minijail0.1 | 5 +++-- system.c | 48 +++++++++++++++++++++++++++++++++++++++++++----- system.h | 2 ++ system_unittest.cc | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 7 deletions(-) diff --git a/minijail0.1 b/minijail0.1 index e713fed..7c535e0 100644 --- a/minijail0.1 +++ b/minijail0.1 @@ -17,7 +17,7 @@ Bind-mount \fIsrc\fR into the chroot directory at \fIdest\fR, optionally writeab The \fIsrc\fR path must be an absolute path. If \fIdest\fR is not specified, it will default to \fIsrc\fR. If the destination does not exist, it will be created as a file or directory -based on the \fIsrc\fR type. +based on the \fIsrc\fR type (including missing parent directories). .TP \fB-c \fR Restrict capabilities to \fIcaps\fR. When used in conjunction with \fB-u\fR and @@ -81,7 +81,8 @@ fields here are filesystem specific. If the mount is not a pseudo filesystem (e.g. proc or sysfs), \fIsrc\fR path must be an absolute path (e.g. \fI/dev/sda1\fR and not \fIsda1\fR). -If the destination does not exist, it will be created as a directory. +If the destination does not exist, it will be created as a directory (including +missing parent directories). .TP \fB-K\fR Don't mark all existing mounts as MS_PRIVATE. diff --git a/system.c b/system.c index 74e97c2..bb1abf9 100644 --- a/system.c +++ b/system.c @@ -214,6 +214,39 @@ int write_pid_to_path(pid_t pid, const char *path) return 0; } +/* + * Create the |path| directory and its parents (if need be) with |mode|. + * If not |isdir|, then |path| is actually a file, so the last component + * will not be created. + */ +int mkdir_p(const char *path, mode_t mode, bool isdir) +{ + char *dir = strdup(path); + if (!dir) + return -errno; + + /* Starting from the root, work our way out to the end. */ + char *p = strchr(dir + 1, '/'); + while (p) { + *p = '\0'; + if (mkdir(dir, mode) && errno != EEXIST) { + free(dir); + return -errno; + } + *p = '/'; + p = strchr(p + 1, '/'); + } + + /* + * Create the last directory. We still check EEXIST here in case + * of trailing slashes. + */ + free(dir); + if (isdir && mkdir(path, mode) && errno != EEXIST) + return -errno; + return 0; +} + /* * setup_mount_destination: Ensures the mount target exists. * Creates it if needed and possible. @@ -267,11 +300,16 @@ int setup_mount_destination(const char *source, const char *dest, uid_t uid, domkdir = true; } - /* Now that we know what we want to do, do it! */ - if (domkdir) { - if (mkdir(dest, 0700)) - return -errno; - } else { + /* + * Now that we know what we want to do, do it! + * We always create the intermediate dirs and the final path with 0755 + * perms and root/root ownership. This shouldn't be a problem because + * the actual mount will set those perms/ownership on the mount point + * which is all people should need to access it. + */ + if (mkdir_p(dest, 0755, domkdir)) + return -errno; + if (!domkdir) { int fd = open(dest, O_RDWR | O_CREAT | O_CLOEXEC, 0700); if (fd < 0) return -errno; diff --git a/system.h b/system.h index 7f36ad2..b816f5f 100644 --- a/system.h +++ b/system.h @@ -51,6 +51,8 @@ int setup_and_dupe_pipe_end(int fds[2], size_t index, int fd); int write_pid_to_path(pid_t pid, const char *path); int write_proc_file(pid_t pid, const char *content, const char *basename); +int mkdir_p(const char *path, mode_t mode, bool isdir); + int setup_mount_destination(const char *source, const char *dest, uid_t uid, uid_t gid, bool bind); diff --git a/system_unittest.cc b/system_unittest.cc index db5fe98..c584808 100644 --- a/system_unittest.cc +++ b/system_unittest.cc @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -124,6 +125,57 @@ TEST(write_pid_to_path, basic) { free(path); } +// If the destination exists, there's nothing to do. +// Also check trailing slash handling. +TEST(mkdir_p, dest_exists) { + EXPECT_EQ(0, mkdir_p("/", 0, true)); + EXPECT_EQ(0, mkdir_p("///", 0, true)); + EXPECT_EQ(0, mkdir_p("/proc", 0, true)); + EXPECT_EQ(0, mkdir_p("/proc/", 0, true)); + EXPECT_EQ(0, mkdir_p("/dev", 0, true)); + EXPECT_EQ(0, mkdir_p("/dev/", 0, true)); +} + +// Create a directory tree that doesn't exist. +TEST(mkdir_p, create_tree) { + char *path = get_temp_path(); + ASSERT_NE(nullptr, path); + unlink(path); + + // Run `mkdir -p /a/b/c`. + char *path_a, *path_a_b, *path_a_b_c; + ASSERT_NE(-1, asprintf(&path_a, "%s/a", path)); + ASSERT_NE(-1, asprintf(&path_a_b, "%s/b", path_a)); + ASSERT_NE(-1, asprintf(&path_a_b_c, "%s/c", path_a_b)); + + // First try creating it as a file. + EXPECT_EQ(0, mkdir_p(path_a_b_c, 0700, false)); + + // Make sure the final path doesn't exist yet. + struct stat st; + EXPECT_EQ(0, stat(path_a_b, &st)); + EXPECT_EQ(true, S_ISDIR(st.st_mode)); + EXPECT_EQ(-1, stat(path_a_b_c, &st)); + + // Then create it as a complete dir. + EXPECT_EQ(0, mkdir_p(path_a_b_c, 0700, true)); + + // Make sure the final dir actually exists. + EXPECT_EQ(0, stat(path_a_b_c, &st)); + EXPECT_EQ(true, S_ISDIR(st.st_mode)); + + // Clean up. + ASSERT_EQ(0, rmdir(path_a_b_c)); + ASSERT_EQ(0, rmdir(path_a_b)); + ASSERT_EQ(0, rmdir(path_a)); + ASSERT_EQ(0, rmdir(path)); + + free(path_a_b_c); + free(path_a_b); + free(path_a); + free(path); +} + // If the destination exists, there's nothing to do. TEST(setup_mount_destination, dest_exists) { // Pick some paths that should always exist. We pass in invalid pointers -- cgit v1.2.3