You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
502 lines
19 KiB
502 lines
19 KiB
From a221a65ad0563d1bfe8770e928b221efc6ba8c88 Mon Sep 17 00:00:00 2001 |
|
From: David Tardon <dtardon@redhat.com> |
|
Date: Thu, 3 Jan 2019 13:09:43 +0100 |
|
Subject: [PATCH] backport chase_symlinks |
|
|
|
Related: #1663143 |
|
--- |
|
src/shared/util.c | 233 +++++++++++++++++++++++++++++++++++++++++++ |
|
src/shared/util.h | 8 ++ |
|
src/test/test-util.c | 208 ++++++++++++++++++++++++++++++++++++++ |
|
3 files changed, 449 insertions(+) |
|
|
|
diff --git a/src/shared/util.c b/src/shared/util.c |
|
index 2838d50f6f..385551f2b3 100644 |
|
--- a/src/shared/util.c |
|
+++ b/src/shared/util.c |
|
@@ -9202,3 +9202,236 @@ int fd_is_fs_type(int fd, statfs_f_type_t magic_value) { |
|
|
|
return is_fs_type(&s, magic_value); |
|
} |
|
+ |
|
+int chase_symlinks(const char *path, const char *original_root, unsigned flags, char **ret) { |
|
+ _cleanup_free_ char *buffer = NULL, *done = NULL, *root = NULL; |
|
+ _cleanup_close_ int fd = -1; |
|
+ unsigned max_follow = 32; /* how many symlinks to follow before giving up and returning ELOOP */ |
|
+ bool exists = true; |
|
+ char *todo; |
|
+ int r; |
|
+ |
|
+ assert(path); |
|
+ |
|
+ /* This is a lot like canonicalize_file_name(), but takes an additional "root" parameter, that allows following |
|
+ * symlinks relative to a root directory, instead of the root of the host. |
|
+ * |
|
+ * Note that "root" primarily matters if we encounter an absolute symlink. It is also used when following |
|
+ * relative symlinks to ensure they cannot be used to "escape" the root directory. The path parameter passed is |
|
+ * assumed to be already prefixed by it, except if the CHASE_PREFIX_ROOT flag is set, in which case it is first |
|
+ * prefixed accordingly. |
|
+ * |
|
+ * Algorithmically this operates on two path buffers: "done" are the components of the path we already |
|
+ * processed and resolved symlinks, "." and ".." of. "todo" are the components of the path we still need to |
|
+ * process. On each iteration, we move one component from "todo" to "done", processing it's special meaning |
|
+ * each time. The "todo" path always starts with at least one slash, the "done" path always ends in no |
|
+ * slash. We always keep an O_PATH fd to the component we are currently processing, thus keeping lookup races |
|
+ * at a minimum. |
|
+ * |
|
+ * Suggested usage: whenever you want to canonicalize a path, use this function. Pass the absolute path you got |
|
+ * as-is: fully qualified and relative to your host's root. Optionally, specify the root parameter to tell this |
|
+ * function what to do when encountering a symlink with an absolute path as directory: prefix it by the |
|
+ * specified path. */ |
|
+ |
|
+ if (original_root) { |
|
+ root = path_make_absolute_cwd(original_root); |
|
+ if (root == NULL) |
|
+ return -ENOENT; |
|
+ |
|
+ if (flags & CHASE_PREFIX_ROOT) |
|
+ path = prefix_roota(root, path); |
|
+ } |
|
+ |
|
+ buffer = path_make_absolute_cwd(path); |
|
+ if (buffer == NULL) |
|
+ return -ENOENT; |
|
+ |
|
+ fd = open("/", O_CLOEXEC|O_NOFOLLOW|O_PATH); |
|
+ if (fd < 0) |
|
+ return -errno; |
|
+ |
|
+ todo = buffer; |
|
+ for (;;) { |
|
+ _cleanup_free_ char *first = NULL; |
|
+ _cleanup_close_ int child = -1; |
|
+ struct stat st; |
|
+ size_t n, m; |
|
+ |
|
+ /* Determine length of first component in the path */ |
|
+ n = strspn(todo, "/"); /* The slashes */ |
|
+ m = n + strcspn(todo + n, "/"); /* The entire length of the component */ |
|
+ |
|
+ /* Extract the first component. */ |
|
+ first = strndup(todo, m); |
|
+ if (!first) |
|
+ return -ENOMEM; |
|
+ |
|
+ todo += m; |
|
+ |
|
+ /* Empty? Then we reached the end. */ |
|
+ if (isempty(first)) |
|
+ break; |
|
+ |
|
+ /* Just a single slash? Then we reached the end. */ |
|
+ if (path_equal(first, "/")) { |
|
+ /* Preserve the trailing slash */ |
|
+ if (!strextend(&done, "/", NULL)) |
|
+ return -ENOMEM; |
|
+ |
|
+ break; |
|
+ } |
|
+ |
|
+ /* Just a dot? Then let's eat this up. */ |
|
+ if (path_equal(first, "/.")) |
|
+ continue; |
|
+ |
|
+ /* Two dots? Then chop off the last bit of what we already found out. */ |
|
+ if (path_equal(first, "/..")) { |
|
+ _cleanup_free_ char *parent = NULL; |
|
+ int fd_parent = -1; |
|
+ |
|
+ /* If we already are at the top, then going up will not change anything. This is in-line with |
|
+ * how the kernel handles this. */ |
|
+ if (isempty(done) || path_equal(done, "/")) |
|
+ continue; |
|
+ |
|
+ parent = dirname_malloc(done); |
|
+ if (!parent) |
|
+ return -ENOMEM; |
|
+ |
|
+ /* Don't allow this to leave the root dir. */ |
|
+ if (root && |
|
+ path_startswith(done, root) && |
|
+ !path_startswith(parent, root)) |
|
+ continue; |
|
+ |
|
+ free(done); |
|
+ done = parent; |
|
+ parent = NULL; |
|
+ |
|
+ fd_parent = openat(fd, "..", O_CLOEXEC|O_NOFOLLOW|O_PATH); |
|
+ if (fd_parent < 0) |
|
+ return -errno; |
|
+ |
|
+ safe_close(fd); |
|
+ fd = fd_parent; |
|
+ |
|
+ continue; |
|
+ } |
|
+ |
|
+ /* Otherwise let's see what this is. */ |
|
+ child = openat(fd, first + n, O_CLOEXEC|O_NOFOLLOW|O_PATH); |
|
+ if (child < 0) { |
|
+ |
|
+ if (errno == ENOENT && |
|
+ (flags & CHASE_NONEXISTENT) && |
|
+ (isempty(todo) || path_is_safe(todo))) { |
|
+ |
|
+ /* If CHASE_NONEXISTENT is set, and the path does not exist, then that's OK, return |
|
+ * what we got so far. But don't allow this if the remaining path contains "../ or "./" |
|
+ * or something else weird. */ |
|
+ |
|
+ /* If done is "/", as first also contains slash at the head, then remove this redundant slash. */ |
|
+ if (streq_ptr(done, "/")) |
|
+ *done = '\0'; |
|
+ |
|
+ if (!strextend(&done, first, todo, NULL)) |
|
+ return -ENOMEM; |
|
+ |
|
+ exists = false; |
|
+ break; |
|
+ } |
|
+ |
|
+ return -errno; |
|
+ } |
|
+ |
|
+ if (fstat(child, &st) < 0) |
|
+ return -errno; |
|
+ if ((flags & CHASE_NO_AUTOFS) && |
|
+ fd_is_fs_type(child, AUTOFS_SUPER_MAGIC) > 0) |
|
+ return -EREMOTE; |
|
+ |
|
+ if (S_ISLNK(st.st_mode)) { |
|
+ char *joined; |
|
+ |
|
+ _cleanup_free_ char *destination = NULL; |
|
+ |
|
+ /* This is a symlink, in this case read the destination. But let's make sure we don't follow |
|
+ * symlinks without bounds. */ |
|
+ if (--max_follow <= 0) |
|
+ return -ELOOP; |
|
+ |
|
+ r = readlinkat_malloc(fd, first + n, &destination); |
|
+ if (r < 0) |
|
+ return r; |
|
+ if (isempty(destination)) |
|
+ return -EINVAL; |
|
+ |
|
+ if (path_is_absolute(destination)) { |
|
+ |
|
+ /* An absolute destination. Start the loop from the beginning, but use the root |
|
+ * directory as base. */ |
|
+ |
|
+ safe_close(fd); |
|
+ fd = open(root ?: "/", O_CLOEXEC|O_NOFOLLOW|O_PATH); |
|
+ if (fd < 0) |
|
+ return -errno; |
|
+ |
|
+ free(done); |
|
+ |
|
+ /* Note that we do not revalidate the root, we take it as is. */ |
|
+ if (isempty(root)) |
|
+ done = NULL; |
|
+ else { |
|
+ done = strdup(root); |
|
+ if (!done) |
|
+ return -ENOMEM; |
|
+ } |
|
+ |
|
+ /* Prefix what's left to do with what we just read, and start the loop again, but |
|
+ * remain in the current directory. */ |
|
+ joined = strjoin(destination, todo, NULL); |
|
+ } else |
|
+ joined = strjoin("/", destination, todo, NULL); |
|
+ if (!joined) |
|
+ return -ENOMEM; |
|
+ |
|
+ free(buffer); |
|
+ todo = buffer = joined; |
|
+ |
|
+ continue; |
|
+ } |
|
+ |
|
+ /* If this is not a symlink, then let's just add the name we read to what we already verified. */ |
|
+ if (!done) { |
|
+ done = first; |
|
+ first = NULL; |
|
+ } else { |
|
+ /* If done is "/", as first also contains slash at the head, then remove this redundant slash. */ |
|
+ if (streq(done, "/")) |
|
+ *done = '\0'; |
|
+ |
|
+ if (!strextend(&done, first, NULL)) |
|
+ return -ENOMEM; |
|
+ } |
|
+ |
|
+ /* And iterate again, but go one directory further down. */ |
|
+ safe_close(fd); |
|
+ fd = child; |
|
+ child = -1; |
|
+ } |
|
+ |
|
+ if (!done) { |
|
+ /* Special case, turn the empty string into "/", to indicate the root directory. */ |
|
+ done = strdup("/"); |
|
+ if (!done) |
|
+ return -ENOMEM; |
|
+ } |
|
+ |
|
+ if (ret) { |
|
+ *ret = done; |
|
+ done = NULL; |
|
+ } |
|
+ |
|
+ return exists; |
|
+} |
|
diff --git a/src/shared/util.h b/src/shared/util.h |
|
index f768936ab1..915c7439e8 100644 |
|
--- a/src/shared/util.h |
|
+++ b/src/shared/util.h |
|
@@ -1155,3 +1155,11 @@ typedef typeof(((struct statfs*)NULL)->f_type) statfs_f_type_t; |
|
|
|
bool is_fs_type(const struct statfs *s, statfs_f_type_t magic_value) _pure_; |
|
int fd_is_fs_type(int fd, statfs_f_type_t magic_value); |
|
+ |
|
+enum { |
|
+ CHASE_PREFIX_ROOT = 1, /* If set, the specified path will be prefixed by the specified root before beginning the iteration */ |
|
+ CHASE_NONEXISTENT = 2, /* If set, it's OK if the path doesn't actually exist. */ |
|
+ CHASE_NO_AUTOFS = 4, /* If set, return -EREMOTE if autofs mount point found */ |
|
+}; |
|
+ |
|
+int chase_symlinks(const char *path_with_prefix, const char *root, unsigned flags, char **ret); |
|
diff --git a/src/test/test-util.c b/src/test/test-util.c |
|
index efb02ff530..397c45a9f4 100644 |
|
--- a/src/test/test-util.c |
|
+++ b/src/test/test-util.c |
|
@@ -36,6 +36,7 @@ |
|
#include "fileio.h" |
|
#include "conf-parser.h" |
|
#include "virt.h" |
|
+#include "path-util.h" |
|
|
|
static void test_streq_ptr(void) { |
|
assert_se(streq_ptr(NULL, NULL)); |
|
@@ -1909,6 +1910,212 @@ static void test_acquire_data_fd(void) { |
|
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE|ACQUIRE_NO_TMPFILE); |
|
} |
|
|
|
+static void test_chase_symlinks(void) { |
|
+ _cleanup_free_ char *result = NULL; |
|
+ char temp[] = "/tmp/test-chase.XXXXXX"; |
|
+ const char *top, *p, *pslash, *q, *qslash; |
|
+ int r; |
|
+ |
|
+ assert_se(mkdtemp(temp)); |
|
+ |
|
+ top = strjoina(temp, "/top"); |
|
+ assert_se(mkdir(top, 0700) >= 0); |
|
+ |
|
+ p = strjoina(top, "/dot"); |
|
+ assert_se(symlink(".", p) >= 0); |
|
+ |
|
+ p = strjoina(top, "/dotdot"); |
|
+ assert_se(symlink("..", p) >= 0); |
|
+ |
|
+ p = strjoina(top, "/dotdota"); |
|
+ assert_se(symlink("../a", p) >= 0); |
|
+ |
|
+ p = strjoina(temp, "/a"); |
|
+ assert_se(symlink("b", p) >= 0); |
|
+ |
|
+ p = strjoina(temp, "/b"); |
|
+ assert_se(symlink("/usr", p) >= 0); |
|
+ |
|
+ p = strjoina(temp, "/start"); |
|
+ assert_se(symlink("top/dot/dotdota", p) >= 0); |
|
+ |
|
+ /* Paths that use symlinks underneath the "root" */ |
|
+ |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, "/usr")); |
|
+ result = mfree(result); |
|
+ |
|
+ pslash = strjoina(p, "/"); |
|
+ r = chase_symlinks(pslash, NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, "/usr/")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ r = chase_symlinks(pslash, temp, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ q = strjoina(temp, "/usr"); |
|
+ |
|
+ r = chase_symlinks(p, temp, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(path_equal(result, q)); |
|
+ result = mfree(result); |
|
+ |
|
+ qslash = strjoina(q, "/"); |
|
+ |
|
+ r = chase_symlinks(pslash, temp, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(path_equal(result, qslash)); |
|
+ result = mfree(result); |
|
+ |
|
+ assert_se(mkdir(q, 0700) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, q)); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks(pslash, temp, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, qslash)); |
|
+ result = mfree(result); |
|
+ |
|
+ p = strjoina(temp, "/slash"); |
|
+ assert_se(symlink("/", p) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, "/")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, temp)); |
|
+ result = mfree(result); |
|
+ |
|
+ /* Paths that would "escape" outside of the "root" */ |
|
+ |
|
+ p = strjoina(temp, "/6dots"); |
|
+ assert_se(symlink("../../..", p) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0 && path_equal(result, temp)); |
|
+ result = mfree(result); |
|
+ |
|
+ p = strjoina(temp, "/6dotsusr"); |
|
+ assert_se(symlink("../../../usr", p) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0 && path_equal(result, q)); |
|
+ result = mfree(result); |
|
+ |
|
+ p = strjoina(temp, "/top/8dotsusr"); |
|
+ assert_se(symlink("../../../../usr", p) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0 && path_equal(result, q)); |
|
+ result = mfree(result); |
|
+ |
|
+ /* Paths that contain repeated slashes */ |
|
+ |
|
+ p = strjoina(temp, "/slashslash"); |
|
+ assert_se(symlink("///usr///", p) >= 0); |
|
+ |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, "/usr")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks(p, temp, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, q)); |
|
+ result = mfree(result); |
|
+ |
|
+ /* Paths using . */ |
|
+ |
|
+ r = chase_symlinks("/etc/./.././", NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(path_equal(result, "/")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/etc/./.././", "/etc", 0, &result); |
|
+ assert_se(r > 0 && path_equal(result, "/etc")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/../.././//../../etc", NULL, 0, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(streq(result, "/etc")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/../.././//../../test-chase.fsldajfl", NULL, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(streq(result, "/test-chase.fsldajfl")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/../.././//../../etc", "/", CHASE_PREFIX_ROOT, &result); |
|
+ assert_se(r > 0); |
|
+ assert_se(streq(result, "/etc")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/../.././//../../test-chase.fsldajfl", "/", CHASE_PREFIX_ROOT|CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(streq(result, "/test-chase.fsldajfl")); |
|
+ result = mfree(result); |
|
+ |
|
+ r = chase_symlinks("/etc/machine-id/foo", NULL, 0, &result); |
|
+ assert_se(r == -ENOTDIR); |
|
+ result = mfree(result); |
|
+ |
|
+ /* Path that loops back to self */ |
|
+ |
|
+ p = strjoina(temp, "/recursive-symlink"); |
|
+ assert_se(symlink("recursive-symlink", p) >= 0); |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r == -ELOOP); |
|
+ |
|
+ /* Path which doesn't exist */ |
|
+ |
|
+ p = strjoina(temp, "/idontexist"); |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ r = chase_symlinks(p, NULL, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(path_equal(result, p)); |
|
+ result = mfree(result); |
|
+ |
|
+ p = strjoina(temp, "/idontexist/meneither"); |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ r = chase_symlinks(p, NULL, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == 0); |
|
+ assert_se(path_equal(result, p)); |
|
+ result = mfree(result); |
|
+ |
|
+ /* Path which doesn't exist, but contains weird stuff */ |
|
+ |
|
+ p = strjoina(temp, "/idontexist/.."); |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ r = chase_symlinks(p, NULL, CHASE_NONEXISTENT, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ p = strjoina(temp, "/target"); |
|
+ q = strjoina(temp, "/top"); |
|
+ assert_se(symlink(q, p) >= 0); |
|
+ p = strjoina(temp, "/target/idontexist"); |
|
+ r = chase_symlinks(p, NULL, 0, &result); |
|
+ assert_se(r == -ENOENT); |
|
+ |
|
+ assert_se(rm_rf_dangerous(temp, false, true, false) >= 0); |
|
+} |
|
+ |
|
int main(int argc, char *argv[]) { |
|
log_parse_environment(); |
|
log_open(); |
|
@@ -1992,6 +2199,7 @@ int main(int argc, char *argv[]) { |
|
test_system_tasks_max(); |
|
test_system_tasks_max_scale(); |
|
test_acquire_data_fd(); |
|
+ test_chase_symlinks(); |
|
|
|
return 0; |
|
}
|
|
|