From a221a65ad0563d1bfe8770e928b221efc6ba8c88 Mon Sep 17 00:00:00 2001 From: David Tardon 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; }