Blob Blame History Raw
From e5ac7ba7a16445f3ad23d9931979c20214eae913 Mon Sep 17 00:00:00 2001
From: Jan Synacek <jsynacek@redhat.com>
Date: Thu, 14 Sep 2017 16:27:08 +0200
Subject: [PATCH] path-util: make use of "mnt_id" field exported in
 /proc/self/fdinfo/<fd>

This commit is not a backport of a specific commit. It includes parts of
several upstream commits (3f72b427b44f39a1aec6806dad6f6b57103ae9ed,
5d409034017e9f9f8c4392157d95511fc2e05d87 and others).

The main goal was to bring path_is_mount_point() up to date, which meant
introducing fd_fdinfo_mnt_id() and fd_is_mount_point(). These were
needed mainly because we need to determine mount points based on
/proc/self/fdinfo/<fd> in containers. Also, there are more places in the
code where checks for mount points are performed, which would benefit from
this fix as well. Additionally, corresponding tests has been added.

Resolves: #1472439
---
 src/nspawn/nspawn.c       |   2 +-
 src/shared/path-util.c    | 219 +++++++++++++++++++++++++++++---------
 src/shared/path-util.h    |   1 +
 src/test/test-path-util.c |  62 +++++++++++
 4 files changed, 235 insertions(+), 49 deletions(-)

diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index ea365b3f9..ee2e1832f 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -990,7 +990,7 @@ static int mount_cgroup_hierarchy(const char *dest, const char *controller, cons
         to = strjoina(dest, "/sys/fs/cgroup/", hierarchy);
 
         r = path_is_mount_point(to, false);
-        if (r < 0)
+        if (r < 0 && r != -ENOENT)
                 return log_error_errno(r, "Failed to determine if %s is mounted already: %m", to);
         if (r > 0)
                 return 0;
diff --git a/src/shared/path-util.c b/src/shared/path-util.c
index 1181ffb9d..5d4de9ec4 100644
--- a/src/shared/path-util.c
+++ b/src/shared/path-util.c
@@ -36,6 +36,7 @@
 #include "strv.h"
 #include "path-util.h"
 #include "missing.h"
+#include "fileio.h"
 
 bool path_is_absolute(const char *p) {
         return p[0] == '/';
@@ -473,87 +474,209 @@ char* path_join(const char *root, const char *path, const char *rest) {
                                NULL);
 }
 
-int path_is_mount_point(const char *t, bool allow_symlink) {
+static int fd_fdinfo_mnt_id(int fd, const char *filename, int flags, int *mnt_id) {
+        char path[strlen("/proc/self/fdinfo/") + DECIMAL_STR_MAX(int)];
+        _cleanup_free_ char *fdinfo = NULL;
+        _cleanup_close_ int subfd = -1;
+        char *p;
+        int r;
+
+        if ((flags & AT_EMPTY_PATH) && isempty(filename))
+                xsprintf(path, "/proc/self/fdinfo/%i", fd);
+        else {
+                subfd = openat(fd, filename, O_CLOEXEC|O_PATH);
+                if (subfd < 0)
+                        return -errno;
 
-        union file_handle_union h = FILE_HANDLE_INIT;
+                xsprintf(path, "/proc/self/fdinfo/%i", subfd);
+        }
+
+        r = read_full_file(path, &fdinfo, NULL);
+        if (r == -ENOENT) /* The fdinfo directory is a relatively new addition */
+                return -EOPNOTSUPP;
+        if (r < 0)
+                return -errno;
+
+        p = startswith(fdinfo, "mnt_id:");
+        if (!p) {
+                p = strstr(fdinfo, "\nmnt_id:");
+                if (!p) /* The mnt_id field is a relatively new addition */
+                        return -EOPNOTSUPP;
+
+                p += 8;
+        }
+
+        p += strspn(p, WHITESPACE);
+        p[strcspn(p, WHITESPACE)] = 0;
+
+        return safe_atoi(p, mnt_id);
+}
+
+int fd_is_mount_point(int fd, const char *filename, int flags) {
+        union file_handle_union h = FILE_HANDLE_INIT, h_parent = FILE_HANDLE_INIT;
         int mount_id = -1, mount_id_parent = -1;
-        _cleanup_free_ char *parent = NULL;
+        bool nosupp = false, check_st_dev = true;
         struct stat a, b;
         int r;
-        bool nosupp = false;
 
-        /* We are not actually interested in the file handles, but
-         * name_to_handle_at() also passes us the mount ID, hence use
-         * it but throw the handle away */
+        assert(fd >= 0);
+        assert(filename);
 
-        if (path_equal(t, "/"))
-                return 1;
-
-        r = name_to_handle_at(AT_FDCWD, t, &h.handle, &mount_id, allow_symlink ? AT_SYMLINK_FOLLOW : 0);
+        /* First we will try the name_to_handle_at() syscall, which
+         * tells us the mount id and an opaque file "handle". It is
+         * not supported everywhere though (kernel compile-time
+         * option, not all file systems are hooked up). If it works
+         * the mount id is usually good enough to tell us whether
+         * something is a mount point.
+         *
+         * If that didn't work we will try to read the mount id from
+         * /proc/self/fdinfo/<fd>. This is almost as good as
+         * name_to_handle_at(), however, does not return the
+         * opaque file handle. The opaque file handle is pretty useful
+         * to detect the root directory, which we should always
+         * consider a mount point. Hence we use this only as
+         * fallback. Exporting the mnt_id in fdinfo is a pretty recent
+         * kernel addition.
+         *
+         * As last fallback we do traditional fstat() based st_dev
+         * comparisons. This is how things were traditionally done,
+         * but unionfs breaks this since it exposes file
+         * systems with a variety of st_dev reported. Also, btrfs
+         * subvolumes have different st_dev, even though they aren't
+         * real mounts of their own. */
+
+        r = name_to_handle_at(fd, filename, &h.handle, &mount_id, flags);
         if (r < 0) {
-                if (errno == ENOSYS)
-                        /* This kernel does not support name_to_handle_at()
-                         * fall back to the traditional stat() logic. */
-                        goto fallback;
+                if (IN_SET(errno, ENOSYS, EACCES, EPERM))
+                        /* This kernel does not support name_to_handle_at() at all, or the syscall was blocked (maybe
+                         * through seccomp, because we are running inside of a container?): fall back to simpler
+                         * logic. */
+                        goto fallback_fdinfo;
                 else if (errno == EOPNOTSUPP)
                         /* This kernel or file system does not support
-                         * name_to_handle_at(), hence fallback to the
+                         * name_to_handle_at(), hence let's see if the
+                         * upper fs supports it (in which case it is a
+                         * mount point), otherwise fallback to the
                          * traditional stat() logic */
                         nosupp = true;
-                else if (errno == ENOENT)
-                        return 0;
                 else
                         return -errno;
         }
 
-        r = path_get_parent(t, &parent);
-        if (r < 0)
-                return r;
-
-        h.handle.handle_bytes = MAX_HANDLE_SZ;
-        r = name_to_handle_at(AT_FDCWD, parent, &h.handle, &mount_id_parent, AT_SYMLINK_FOLLOW);
-        if (r < 0)
-                if (errno == EOPNOTSUPP)
+        r = name_to_handle_at(fd, "", &h_parent.handle, &mount_id_parent, AT_EMPTY_PATH);
+        if (r < 0) {
+                if (errno == EOPNOTSUPP) {
                         if (nosupp)
                                 /* Neither parent nor child do name_to_handle_at()?
                                    We have no choice but to fall back. */
-                                goto fallback;
+                                goto fallback_fdinfo;
                         else
-                                /* The parent can't do name_to_handle_at() but
-                                 * the directory we are interested in can?
-                                 * Or the other way around?
+                                /* The parent can't do name_to_handle_at() but the
+                                 * directory we are interested in can?
                                  * If so, it must be a mount point. */
                                 return 1;
-                else
+                } else
                         return -errno;
-        else
-                return mount_id != mount_id_parent;
+        }
 
-fallback:
-        if (allow_symlink)
-                r = stat(t, &a);
-        else
-                r = lstat(t, &a);
+        /* The parent can do name_to_handle_at() but the
+         * directory we are interested in can't? If so, it
+         * must be a mount point. */
+        if (nosupp)
+                return 1;
 
-        if (r < 0) {
-                if (errno == ENOENT)
-                        return 0;
+        /* If the file handle for the directory we are
+         * interested in and its parent are identical, we
+         * assume this is the root directory, which is a mount
+         * point. */
 
-                return -errno;
-        }
+        if (h.handle.handle_bytes == h_parent.handle.handle_bytes &&
+            h.handle.handle_type == h_parent.handle.handle_type &&
+            memcmp(h.handle.f_handle, h_parent.handle.f_handle, h.handle.handle_bytes) == 0)
+                return 1;
 
-        free(parent);
-        parent = NULL;
+        return mount_id != mount_id_parent;
 
-        r = path_get_parent(t, &parent);
+fallback_fdinfo:
+        r = fd_fdinfo_mnt_id(fd, filename, flags, &mount_id);
+        if (IN_SET(r, -EOPNOTSUPP, -EACCES, -EPERM))
+                goto fallback_fstat;
         if (r < 0)
                 return r;
 
-        r = stat(parent, &b);
+        r = fd_fdinfo_mnt_id(fd, "", AT_EMPTY_PATH, &mount_id_parent);
         if (r < 0)
+                return r;
+
+        if (mount_id != mount_id_parent)
+                return 1;
+
+        /* Hmm, so, the mount ids are the same. This leaves one
+         * special case though for the root file system. For that,
+         * let's see if the parent directory has the same inode as we
+         * are interested in. Hence, let's also do fstat() checks now,
+         * too, but avoid the st_dev comparisons, since they aren't
+         * that useful on unionfs mounts. */
+        check_st_dev = false;
+
+fallback_fstat:
+        /* yay for fstatat() taking a different set of flags than the other
+         * _at() above */
+        if (flags & AT_SYMLINK_FOLLOW)
+                flags &= ~AT_SYMLINK_FOLLOW;
+        else
+                flags |= AT_SYMLINK_NOFOLLOW;
+        if (fstatat(fd, filename, &a, flags) < 0)
+                return -errno;
+
+        if (fstatat(fd, "", &b, AT_EMPTY_PATH) < 0)
+                return -errno;
+
+        /* A directory with same device and inode as its parent? Must
+         * be the root directory */
+        if (a.st_dev == b.st_dev &&
+            a.st_ino == b.st_ino)
+                return 1;
+
+        return check_st_dev && (a.st_dev != b.st_dev);
+}
+
+
+
+int path_is_mount_point(const char *t, bool allow_symlink) {
+        _cleanup_free_ char *canonical = NULL, *parent = NULL;
+        _cleanup_close_ int fd = -1;
+        int flags = allow_symlink ? AT_SYMLINK_FOLLOW : 0;
+
+        assert(t);
+
+        if (path_equal(t, "/"))
+                return 1;
+
+        /* we need to resolve symlinks manually, we can't just rely on
+         * fd_is_mount_point() to do that for us; if we have a structure like
+         * /bin -> /usr/bin/ and /usr is a mount point, then the parent that we
+         * look at needs to be /usr, not /. */
+        if (flags & AT_SYMLINK_FOLLOW) {
+                canonical = canonicalize_file_name(t);
+                if (!canonical) {
+                        if (errno == ENOENT)
+                                return 0;
+                        else
+                                return -errno;
+                }
+                t = canonical;
+        }
+
+        parent = dirname_malloc(t);
+        if (!parent)
+                return -ENOMEM;
+
+        fd = openat(AT_FDCWD, parent, O_DIRECTORY|O_CLOEXEC|O_PATH);
+        if (fd < 0)
                 return -errno;
 
-        return a.st_dev != b.st_dev;
+        return fd_is_mount_point(fd, basename(t), flags);
 }
 
 int path_is_read_only_fs(const char *path) {
diff --git a/src/shared/path-util.h b/src/shared/path-util.h
index 71bb740e9..34c016229 100644
--- a/src/shared/path-util.h
+++ b/src/shared/path-util.h
@@ -53,6 +53,7 @@ char** path_strv_make_absolute_cwd(char **l);
 char** path_strv_resolve(char **l, const char *prefix);
 char** path_strv_resolve_uniq(char **l, const char *prefix);
 
+int fd_is_mount_point(int fd, const char *filename, int flags);
 int path_is_mount_point(const char *path, bool allow_symlink);
 int path_is_read_only_fs(const char *path);
 int path_is_os_tree(const char *path);
diff --git a/src/test/test-path-util.c b/src/test/test-path-util.c
index 6396fcb39..a4fec07e7 100644
--- a/src/test/test-path-util.c
+++ b/src/test/test-path-util.c
@@ -21,6 +21,7 @@
 
 #include <stdio.h>
 #include <unistd.h>
+#include <sys/mount.h>
 
 #include "path-util.h"
 #include "util.h"
@@ -99,6 +100,66 @@ static void test_path(void) {
         }
 }
 
+static void test_path_is_mount_point(void) {
+        int fd, rt, rf, rlt, rlf;
+        char tmp_dir[] = "/tmp/test-path-is-mount-point-XXXXXX";
+        _cleanup_free_ char *file1 = NULL, *file2 = NULL, *link1 = NULL, *link2 = NULL;
+
+        assert_se(path_is_mount_point("/", true) > 0);
+        assert_se(path_is_mount_point("/", false) > 0);
+
+        assert_se(path_is_mount_point("/proc", true) > 0);
+        assert_se(path_is_mount_point("/proc", false) > 0);
+
+        assert_se(path_is_mount_point("/proc/1", true) == 0);
+        assert_se(path_is_mount_point("/proc/1", false) == 0);
+
+        assert_se(path_is_mount_point("/sys", true) > 0);
+        assert_se(path_is_mount_point("/sys", false) > 0);
+
+        /* file mountpoints */
+        assert_se(mkdtemp(tmp_dir) != NULL);
+        file1 = path_join(NULL, tmp_dir, "file1");
+        assert_se(file1);
+        file2 = path_join(NULL, tmp_dir, "file2");
+        assert_se(file2);
+        fd = open(file1, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+        assert_se(fd > 0);
+        close(fd);
+        fd = open(file2, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, 0664);
+        assert_se(fd > 0);
+        close(fd);
+        link1 = path_join(NULL, tmp_dir, "link1");
+        assert_se(link1);
+        assert_se(symlink("file1", link1) == 0);
+        link2 = path_join(NULL, tmp_dir, "link2");
+        assert_se(link1);
+        assert_se(symlink("file2", link2) == 0);
+
+        assert_se(path_is_mount_point(file1, true) == 0);
+        assert_se(path_is_mount_point(file1, false) == 0);
+        assert_se(path_is_mount_point(link1, true) == 0);
+        assert_se(path_is_mount_point(link1, false) == 0);
+
+        /* this test will only work as root */
+        if (mount(file1, file2, NULL, MS_BIND, NULL) >= 0) {
+                rf = path_is_mount_point(file2, false);
+                rt = path_is_mount_point(file2, true);
+                rlf = path_is_mount_point(link2, false);
+                rlt = path_is_mount_point(link2, true);
+
+                assert_se(umount(file2) == 0);
+
+                assert_se(rf == 1);
+                assert_se(rt == 1);
+                assert_se(rlf == 0);
+                assert_se(rlt == 1);
+        } else
+                printf("Skipping bind mount file test: %m\n");
+
+        assert_se(rm_rf(tmp_dir, false, true, false) == 0);
+}
+
 static void test_find_binary(const char *self, bool local) {
         char *p;
 
@@ -288,6 +349,7 @@ int main(int argc, char **argv) {
         test_make_relative();
         test_strv_resolve();
         test_path_startswith();
+        test_path_is_mount_point();
 
         return 0;
 }