]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
fs-util: teach xopenat_full() about XO_SOCKET and XO_TRIGGER_AUTOMOUNT
authorDaan De Meyer <daan@amutable.com>
Wed, 15 Apr 2026 22:55:59 +0000 (22:55 +0000)
committerDaan De Meyer <daan@amutable.com>
Mon, 20 Apr 2026 21:33:02 +0000 (23:33 +0200)
XO_SOCKET asks for the opened inode to be verified as a socket, analogous to
the existing XO_REGULAR. A new fd_verify_socket() helper is added to
stat-util to perform the check on an opened fd.

XO_TRIGGER_AUTOMOUNT asks O_PATH opens to trigger automounts by going via
open_tree() without OPEN_TREE_CLONE, falling back to openat() if the kernel
or sandbox rejects open_tree().

src/basic/fs-util.c
src/basic/fs-util.h
src/basic/stat-util.c
src/basic/stat-util.h
src/test/test-fs-util.c

index f790ca4e136b5c22da3be4bbc919e9b49f034031..d041a9afcff770ab22e799c64797b1d3a30a8f63 100644 (file)
@@ -3,6 +3,7 @@
 #include <linux/falloc.h>
 #include <stdlib.h>
 #include <sys/file.h>
+#include <sys/mount.h>
 #include <unistd.h>
 
 #include "alloc-util.h"
@@ -1130,6 +1131,45 @@ int openat_report_new(int dirfd, const char *pathname, int flags, mode_t mode, b
         }
 }
 
+static int openat_with_automount(int dir_fd, const char *path, int open_flags, mode_t mode) {
+        /* When XO_TRIGGER_AUTOMOUNT is set we want to trigger automounts on the path. open() with O_PATH
+         * does not do that, so we use open_tree() without OPEN_TREE_CLONE which is equivalent to open() with
+         * O_PATH except that it does trigger automounts. Some sandboxes reject open_tree() with EPERM or
+         * ENOSYS, in which case we fall back to plain openat(): autofs wouldn't work inside a restricted
+         * mount namespace anyway. */
+
+        static bool can_open_tree = true;
+        int r;
+
+        assert(dir_fd >= 0 || dir_fd == AT_FDCWD);
+        assert(path);
+
+        if (can_open_tree) {
+                r = RET_NERRNO(open_tree(dir_fd, path,
+                                         OPEN_TREE_CLOEXEC |
+                                         (FLAGS_SET(open_flags, O_NOFOLLOW) ? AT_SYMLINK_NOFOLLOW : 0)));
+                if (r >= 0) {
+                        /* open_tree() doesn't honor O_DIRECTORY, so enforce it ourselves to match
+                         * the openat() fallback's behavior. */
+                        if (FLAGS_SET(open_flags, O_DIRECTORY)) {
+                                int q = fd_verify_directory(r);
+                                if (q < 0) {
+                                        safe_close(r);
+                                        return q;
+                                }
+                        }
+
+                        return r;
+                }
+                if (r != -EPERM && !ERRNO_IS_NEG_NOT_SUPPORTED(r))
+                        return r;
+
+                can_open_tree = false;
+        }
+
+        return RET_NERRNO(openat(dir_fd, path, open_flags, mode));
+}
+
 int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_flags, mode_t mode) {
         _cleanup_close_ int fd = -EBADF;
         bool made_dir = false, made_file = false;
@@ -1137,8 +1177,19 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
 
         assert(dir_fd >= 0 || IN_SET(dir_fd, AT_FDCWD, XAT_FDROOT));
 
-        /* An inode cannot be both a directory and a regular file at the same time. */
+        /* An inode can only be one of a directory, a regular file or a socket at the same time. */
         assert(!(FLAGS_SET(open_flags, O_DIRECTORY) && FLAGS_SET(xopen_flags, XO_REGULAR)));
+        assert(!(FLAGS_SET(xopen_flags, XO_REGULAR) && FLAGS_SET(xopen_flags, XO_SOCKET)));
+        assert(!(FLAGS_SET(open_flags, O_DIRECTORY) && FLAGS_SET(xopen_flags, XO_SOCKET)));
+        /* Sockets cannot be open()ed, only pinned via O_PATH. */
+        assert(!FLAGS_SET(xopen_flags, XO_SOCKET) || FLAGS_SET(open_flags, O_PATH));
+        /* XO_TRIGGER_AUTOMOUNT requires O_PATH and does not support creating inodes. XO_SUBVOLUME
+         * requires O_CREAT, and XO_NOCOW needs a writable fd for its chattr ioctl, so neither is
+         * compatible with XO_TRIGGER_AUTOMOUNT. */
+        assert(!FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) ||
+               (FLAGS_SET(open_flags, O_PATH) && !FLAGS_SET(open_flags, O_CREAT)));
+        assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_SUBVOLUME)));
+        assert(!(FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) && FLAGS_SET(xopen_flags, XO_NOCOW)));
 
         /* This is like openat(), but has a few tricks up its sleeves, extending behaviour:
          *
@@ -1153,6 +1204,10 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
          *
          *   • if XO_REGULAR is specified will return an error if inode is not a regular file.
          *
+         *   • if XO_SOCKET is specified will return an error if inode is not a socket.
+         *
+         *   • if XO_TRIGGER_AUTOMOUNT is specified O_PATH fds will trigger automounts.
+         *
          *   • If mode is specified as MODE_INVALID, we'll use 0755 for dirs, and 0644 for regular files.
          *
          *   • The dir fd can be passed as XAT_FDROOT, in which case any relative paths will be taken relative to the root fs.
@@ -1170,6 +1225,12 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                                 return r;
                 }
 
+                if (FLAGS_SET(xopen_flags, XO_SOCKET)) {
+                        r = fd_verify_socket(dir_fd);
+                        if (r < 0)
+                                return r;
+                }
+
                 return fd_reopen(dir_fd, open_flags & ~O_NOFOLLOW);
         }
 
@@ -1217,9 +1278,11 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                  * first */
 
                 if (FLAGS_SET(open_flags, O_PATH)) {
-                        fd = openat(dir_fd, path, open_flags, mode);
+                        fd = FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT) ?
+                                openat_with_automount(dir_fd, path, open_flags, mode) :
+                                RET_NERRNO(openat(dir_fd, path, open_flags, mode));
                         if (fd < 0) {
-                                r = -errno;
+                                r = fd;
                                 goto error;
                         }
 
@@ -1266,7 +1329,15 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                                 }
                         }
                 }
+        } else if (FLAGS_SET(xopen_flags, XO_TRIGGER_AUTOMOUNT)) {
+                fd = openat_with_automount(dir_fd, path, open_flags, mode);
+                if (fd < 0) {
+                        r = fd;
+                        goto error;
+                }
         } else {
+                /* XO_SOCKET also lands here: it requires O_PATH (see asserts above) so openat() pins
+                 * the inode without connecting, and fd_verify_socket() below enforces the type. */
                 fd = openat_report_new(dir_fd, path, open_flags, mode, &made_file);
                 if (fd < 0) {
                         r = fd;
@@ -1274,6 +1345,12 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                 }
         }
 
+        if (FLAGS_SET(xopen_flags, XO_SOCKET)) {
+                r = fd_verify_socket(fd);
+                if (r < 0)
+                        goto error;
+        }
+
         if (call_label_ops_post) {
                 call_label_ops_post = false;
 
index d75c253dbb46aa7008bb974b4376a32f6fc4d850..c33a084d3fdbffd91974c6d0ed27efa77849447a 100644 (file)
@@ -109,10 +109,12 @@ int posix_fallocate_loop(int fd, uint64_t offset, uint64_t size);
 int parse_cifs_service(const char *s, char **ret_host, char **ret_service, char **ret_path);
 
 typedef enum XOpenFlags {
-        XO_LABEL     = 1 << 0, /* When creating: relabel */
-        XO_SUBVOLUME = 1 << 1, /* When creating as directory: make it a subvolume */
-        XO_NOCOW     = 1 << 2, /* Enable NOCOW mode after opening */
-        XO_REGULAR   = 1 << 3, /* Fail if the inode is not a regular file */
+        XO_LABEL             = 1 << 0, /* When creating: relabel */
+        XO_SUBVOLUME         = 1 << 1, /* When creating as directory: make it a subvolume */
+        XO_NOCOW             = 1 << 2, /* Enable NOCOW mode after opening */
+        XO_REGULAR           = 1 << 3, /* Fail if the inode is not a regular file */
+        XO_SOCKET            = 1 << 4, /* Fail if the inode is not a socket */
+        XO_TRIGGER_AUTOMOUNT = 1 << 5, /* Trigger automounts via open_tree(). Requires O_PATH. */
 } XOpenFlags;
 
 int open_mkdir_at_full(int dirfd, const char *path, int flags, XOpenFlags xopen_flags, mode_t mode);
index e0dc59a863bad53bc6c5595bf168027002a06ffc..bbfb8a9d1879b6de2e924d2e2b1d80304a819be7 100644 (file)
@@ -181,6 +181,13 @@ int statx_verify_socket(const struct statx *stx) {
         return mode_verify_socket(stx->stx_mode);
 }
 
+int fd_verify_socket(int fd) {
+        if (IN_SET(fd, AT_FDCWD, XAT_FDROOT))
+                return -EISDIR;
+
+        return verify_stat_at(fd, /* path= */ NULL, /* follow= */ false, stat_verify_socket, /* verify= */ true);
+}
+
 int is_socket(const char *path) {
         assert(!isempty(path));
         return verify_stat_at(AT_FDCWD, path, /* follow= */ true, stat_verify_socket, /* verify= */ false);
index de9ee03f44034321935a073173a42ba6569c6ced..267d6ed7b410c4e08c0e1159c8767945a15742eb 100644 (file)
@@ -23,6 +23,7 @@ int is_symlink(const char *path);
 
 int stat_verify_socket(const struct stat *st);
 int statx_verify_socket(const struct statx *stx);
+int fd_verify_socket(int fd);
 int is_socket(const char *path);
 
 int stat_verify_linked(const struct stat *st);
index 7cb720938ccefac820d2b7c0cba28d3cc7305acd..d04fbc6768b20cd800989e0ca4bf4792a377f0d9 100644 (file)
@@ -16,6 +16,7 @@
 #include "process-util.h"
 #include "random-util.h"
 #include "rm-rf.h"
+#include "socket-util.h"
 #include "stat-util.h"
 #include "string-util.h"
 #include "strv.h"
@@ -740,6 +741,65 @@ TEST(xopenat_regular) {
         assert_se(unlink("/tmp/xopenat-regular-test") >= 0);
 }
 
+TEST(xopenat_socket) {
+        _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+        _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+
+        ASSERT_OK(tfd = mkdtemp_open(NULL, 0, &t));
+
+        /* Create a Unix domain socket via bind(). */
+        fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0);
+        ASSERT_OK(fd);
+
+        const char *sockpath = strjoina(t, "/test.sock");
+        union sockaddr_union sa = { .un.sun_family = AF_UNIX };
+        strncpy(sa.un.sun_path, sockpath, sizeof(sa.un.sun_path) - 1);
+        ASSERT_OK_ERRNO(bind(fd, &sa.sa, offsetof(struct sockaddr_un, sun_path) + strlen(sockpath) + 1));
+        fd = safe_close(fd);
+
+        /* XO_SOCKET requires O_PATH. */
+        fd = xopenat_full(tfd, "test.sock", O_PATH|O_CLOEXEC, XO_SOCKET, 0);
+        ASSERT_OK(fd);
+        fd = safe_close(fd);
+
+        /* Reopen via empty path should also work. */
+        fd = ASSERT_OK(xopenat_full(tfd, "test.sock", O_PATH|O_CLOEXEC, 0, 0));
+        _cleanup_close_ int fd2 = xopenat_full(fd, NULL, O_PATH|O_CLOEXEC, XO_SOCKET, 0);
+        ASSERT_OK(fd2);
+        fd = safe_close(fd);
+
+        /* Non-socket inodes must be rejected. */
+        ASSERT_OK_ERRNO(mkdirat(tfd, "dir", 0755));
+        ASSERT_ERROR(xopenat_full(tfd, "dir", O_PATH|O_CLOEXEC, XO_SOCKET, 0), EISDIR);
+
+        fd = ASSERT_OK_ERRNO(openat(tfd, "reg", O_CREAT|O_CLOEXEC, 0600));
+        fd = safe_close(fd);
+        ASSERT_ERROR(xopenat_full(tfd, "reg", O_PATH|O_CLOEXEC, XO_SOCKET, 0), ENOTSOCK);
+
+        /* Reopen via empty path of a non-socket fd must also be rejected. */
+        fd = ASSERT_OK(xopenat_full(tfd, "reg", O_PATH|O_CLOEXEC, 0, 0));
+        ASSERT_ERROR(xopenat_full(fd, NULL, O_PATH|O_CLOEXEC, XO_SOCKET, 0), ENOTSOCK);
+        fd = safe_close(fd);
+
+        fd = ASSERT_OK(xopenat_full(tfd, "dir", O_PATH|O_CLOEXEC, 0, 0));
+        ASSERT_ERROR(xopenat_full(fd, NULL, O_PATH|O_CLOEXEC, XO_SOCKET, 0), EISDIR);
+        fd = safe_close(fd);
+}
+
+TEST(xopenat_trigger_automount) {
+        _cleanup_close_ int fd = -EBADF;
+
+        /* We can't easily set up an autofs mount in a test, but we can verify that
+         * XO_TRIGGER_AUTOMOUNT works on a regular path and produces the same inode as a
+         * plain O_PATH open. */
+        fd = xopenat_full(AT_FDCWD, "/usr", O_PATH|O_CLOEXEC|O_DIRECTORY, XO_TRIGGER_AUTOMOUNT, 0);
+        ASSERT_OK(fd);
+
+        _cleanup_close_ int fd2 = xopenat_full(AT_FDCWD, "/usr", O_PATH|O_CLOEXEC|O_DIRECTORY, 0, 0);
+        ASSERT_OK(fd2);
+        ASSERT_OK_POSITIVE(fd_inode_same(fd, fd2));
+}
+
 TEST(xopenat_lock_full) {
         _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
         _cleanup_close_ int tfd = -EBADF, fd = -EBADF;