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().
This commit is contained in:
Daan De Meyer
2026-04-20 23:33:02 +02:00
parent c99e93ce0a
commit 038aaba475
5 changed files with 154 additions and 7 deletions
+80 -3
View 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;
+6 -4
View 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);
+7
View 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);
+1
View 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);
+60
View 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;