#include <linux/falloc.h>
#include <stdlib.h>
#include <sys/file.h>
+#include <sys/mount.h>
#include <unistd.h>
#include "alloc-util.h"
}
}
+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;
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:
*
*
* • 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.
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);
}
* 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;
}
}
}
}
+ } 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;
}
}
+ 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;
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);
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);
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);
#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"
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;