From: Daan De Meyer Date: Wed, 15 Apr 2026 22:55:59 +0000 (+0000) Subject: fs-util: teach xopenat_full() about XO_SOCKET and XO_TRIGGER_AUTOMOUNT X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=038aaba475719cbb1ba63b78a16ca87e6df51598;p=thirdparty%2Fsystemd.git fs-util: teach xopenat_full() about XO_SOCKET and XO_TRIGGER_AUTOMOUNT 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(). --- diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index f790ca4e136..d041a9afcff 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #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; diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index d75c253dbb4..c33a084d3fd 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -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); diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index e0dc59a863b..bbfb8a9d187 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -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); diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index de9ee03f440..267d6ed7b41 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -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); diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index 7cb720938cc..d04fbc6768b 100644 --- a/src/test/test-fs-util.c +++ b/src/test/test-fs-util.c @@ -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;