]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
fs-util: add XO_AUTO_RW_RO
authorLennart Poettering <lennart@amutable.com>
Wed, 22 Apr 2026 08:17:19 +0000 (10:17 +0200)
committerLennart Poettering <lennart@amutable.com>
Sun, 26 Apr 2026 19:21:49 +0000 (21:21 +0200)
src/basic/fs-util.c
src/basic/fs-util.h
src/test/test-fs-util.c

index d041a9afcff770ab22e799c64797b1d3a30a8f63..3960938309fcd952bc47fc53a13004ab0be02821 100644 (file)
@@ -1191,6 +1191,9 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
         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
@@ -1211,13 +1214,26 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
          *   • 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);
@@ -1231,7 +1247,16 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                                 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;
@@ -1292,10 +1317,23 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
 
                 } 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;
@@ -1309,10 +1347,24 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                                 }
 
                                 /* 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;
@@ -1322,10 +1374,24 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
                                 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;
+                                        }
                                 }
                         }
                 }
@@ -1338,10 +1404,22 @@ int xopenat_full(int dir_fd, const char *path, int open_flags, XOpenFlags xopen_
         } 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;
+                        }
                 }
         }
 
index c33a084d3fdbffd91974c6d0ed27efa77849447a..32283d4c1fbc59a93cd64622bc915524f6b34ffe 100644 (file)
@@ -115,6 +115,7 @@ typedef enum XOpenFlags {
         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. */
+        XO_AUTO_RW_RO        = 1 << 6, /* Open in O_RDWR mode if possible, O_RDONLY if not */
 } XOpenFlags;
 
 int open_mkdir_at_full(int dirfd, const char *path, int flags, XOpenFlags xopen_flags, mode_t mode);
index d04fbc6768b20cd800989e0ca4bf4792a377f0d9..23aa5a5815fd18266b14a7334d3e35def82344c6 100644 (file)
@@ -800,6 +800,90 @@ TEST(xopenat_trigger_automount) {
         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;