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)));
+ /* Don't specify an access mode if you want auto mode. */
+ assert(!FLAGS_SET(xopen_flags, XO_AUTO_RW_RO) || (open_flags & O_ACCMODE_STRICT) == 0);
+
/* This is like openat(), but has a few tricks up its sleeves, extending behaviour:
*
* • O_DIRECTORY|O_CREAT is supported, which causes a directory to be created, and immediately
* • 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.
+ *
+ * • If XO_AUTO_RW_RO is specified and the file cannot be opened in O_RDWR mode due to EACCES/EROFS or similar, retry in O_RDONLY mode.
*/
if (mode == MODE_INVALID)
mode = (open_flags & O_DIRECTORY) ? 0755 : 0644;
+ if (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ if (open_flags & O_DIRECTORY) {
+ /* Directories can only be opened in read-only mode */
+ xopen_flags &= ~XO_AUTO_RW_RO;
+ open_flags |= O_RDONLY;
+ } else if (open_flags & O_PATH)
+ /* O_PATH is incompatible with O_RDONLY/O_RDWR → fail */
+ return -EINVAL;
+ }
+
if (isempty(path)) {
assert(!FLAGS_SET(open_flags, O_CREAT|O_EXCL));
+ open_flags &= ~O_NOFOLLOW;
if (FLAGS_SET(xopen_flags, XO_REGULAR)) {
r = fd_verify_regular(dir_fd);
return r;
}
- return fd_reopen(dir_fd, open_flags & ~O_NOFOLLOW);
+ if (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ /* First try: in r/w mode */
+ fd = fd_reopen(dir_fd, open_flags|O_RDWR);
+ if (!ERRNO_IS_NEG_FS_WRITE_REFUSED(fd) && fd != -EISDIR)
+ return TAKE_FD(fd);
+
+ open_flags |= O_RDONLY;
+ }
+
+ return fd_reopen(dir_fd, open_flags);
}
_cleanup_close_ int _dir_fd = -EBADF;
} else if (FLAGS_SET(open_flags, O_CREAT|O_EXCL)) {
/* In O_EXCL mode we can just create the thing, everything is dealt with for us */
- fd = openat(dir_fd, path, open_flags, mode);
+
+ if (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ fd = RET_NERRNO(openat(dir_fd, path, open_flags|O_RDWR, mode));
+ if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
+ open_flags |= O_RDONLY;
+ else if (fd < 0) {
+ r = fd;
+ goto error;
+ }
+ }
+
if (fd < 0) {
- r = -errno;
- goto error;
+ fd = openat(dir_fd, path, open_flags, mode);
+ if (fd < 0) {
+ r = -errno;
+ goto error;
+ }
}
made_file = true;
}
/* Doesn't exist yet, then try to create it */
- fd = openat(dir_fd, path, open_flags|O_CREAT|O_EXCL, mode);
+ open_flags |= O_EXCL;
+
+ if (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ fd = RET_NERRNO(openat(dir_fd, path, open_flags|O_RDWR, mode));
+ if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
+ open_flags |= O_RDONLY;
+ else if (fd < 0) {
+ r = fd;
+ goto error;
+ }
+ }
+
if (fd < 0) {
- r = -errno;
- goto error;
+ fd = openat(dir_fd, path, open_flags, mode);
+ if (fd < 0) {
+ r = -errno;
+ goto error;
+ }
}
made_file = true;
if (r < 0)
goto error;
- fd = fd_reopen(inode_fd, open_flags & ~(O_NOFOLLOW|O_CREAT));
+ open_flags &= ~(O_NOFOLLOW|O_CREAT);
+
+ if (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ fd = fd_reopen(inode_fd, open_flags|O_RDWR);
+ if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
+ open_flags |= O_RDONLY;
+ else if (fd < 0) {
+ r = fd;
+ goto error;
+ }
+ }
+
if (fd < 0) {
- r = fd;
- goto error;
+ fd = fd_reopen(inode_fd, open_flags);
+ 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 (FLAGS_SET(xopen_flags, XO_AUTO_RW_RO)) {
+ fd = openat_report_new(dir_fd, path, O_RDWR|open_flags, mode, &made_file);
+ if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd) || fd == -EISDIR)
+ open_flags |= O_RDONLY;
+ else if (fd < 0) {
+ r = fd;
+ goto error;
+ }
+ }
+
if (fd < 0) {
- r = fd;
- goto error;
+ fd = openat_report_new(dir_fd, path, open_flags, mode, &made_file);
+ if (fd < 0) {
+ r = fd;
+ goto error;
+ }
}
}
ASSERT_OK_POSITIVE(fd_inode_same(fd, fd2));
}
+TEST(xopenat_auto_rw_ro) {
+ _cleanup_(rm_rf_physical_and_freep) char *t = NULL;
+ _cleanup_close_ int tfd = -EBADF, fd = -EBADF;
+ int fl;
+
+ assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0);
+
+ /* Regular writable file: XO_AUTO_RW_RO should end up in O_RDWR. */
+
+ fd = xopenat_full(tfd, "rw", O_CREAT|O_EXCL|O_CLOEXEC, XO_AUTO_RW_RO, 0644);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDWR);
+ fd = safe_close(fd);
+
+ /* Same thing, but with XO_REGULAR set too. */
+
+ fd = xopenat_full(tfd, "rw2", O_CREAT|O_EXCL|O_CLOEXEC, XO_AUTO_RW_RO|XO_REGULAR, 0644);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDWR);
+ fd = safe_close(fd);
+
+ /* Reopen via empty path on an O_PATH fd must also end up in O_RDWR. */
+
+ _cleanup_close_ int path_fd = xopenat_full(tfd, "rw", O_PATH|O_CLOEXEC, 0, 0);
+ assert_se(path_fd >= 0);
+ fd = xopenat_full(path_fd, "", O_CLOEXEC, XO_AUTO_RW_RO, 0);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDWR);
+ fd = safe_close(fd);
+
+ /* Directories can only be opened read-only: XO_AUTO_RW_RO with O_DIRECTORY must fall back to O_RDONLY. */
+
+ fd = xopenat_full(tfd, "subdir", O_DIRECTORY|O_CREAT|O_CLOEXEC, XO_AUTO_RW_RO, 0755);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDONLY);
+ fd = safe_close(fd);
+
+ /* Same for opening an existing directory. */
+
+ fd = xopenat_full(tfd, "subdir", O_DIRECTORY|O_CLOEXEC, XO_AUTO_RW_RO, 0);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDONLY);
+ fd = safe_close(fd);
+
+ /* Fallback when the inode is not writable: create a file as read-only mode and verify that
+ * XO_AUTO_RW_RO falls back to O_RDONLY. Root bypasses mode bits via CAP_DAC_OVERRIDE, so skip
+ * this when running as root. */
+
+ if (geteuid() != 0) {
+ fd = openat(tfd, "ro", O_CREAT|O_EXCL|O_WRONLY|O_CLOEXEC, 0444);
+ assert_se(fd >= 0);
+ fd = safe_close(fd);
+ assert_se(fchmodat(tfd, "ro", 0444, 0) >= 0);
+
+ /* Plain case: no XO_REGULAR. */
+ fd = xopenat_full(tfd, "ro", O_CLOEXEC, XO_AUTO_RW_RO, 0);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDONLY);
+ fd = safe_close(fd);
+
+ /* With XO_REGULAR (exercises the pin-via-O_PATH + reopen path). */
+ fd = xopenat_full(tfd, "ro", O_CLOEXEC, XO_AUTO_RW_RO|XO_REGULAR, 0);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDONLY);
+ fd = safe_close(fd);
+
+ /* Also exercise the empty-path/fd-reopen branch. */
+ _cleanup_close_ int ro_path_fd = xopenat_full(tfd, "ro", O_PATH|O_CLOEXEC, 0, 0);
+ assert_se(ro_path_fd >= 0);
+ fd = xopenat_full(ro_path_fd, "", O_CLOEXEC, XO_AUTO_RW_RO, 0);
+ assert_se(fd >= 0);
+ ASSERT_OK_ERRNO(fl = fcntl(fd, F_GETFL));
+ assert_se((fl & O_ACCMODE) == O_RDONLY);
+ fd = safe_close(fd);
+ }
+}
+
TEST(xopenat_lock_full) {
_cleanup_(rm_rf_physical_and_freep) char *t = NULL;
_cleanup_close_ int tfd = -EBADF, fd = -EBADF;