diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index d041a9afcf..3960938309 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -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; + } } } diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index c33a084d3f..32283d4c1f 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -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); diff --git a/src/libsystemd/sd-path/path-lookup.c b/src/libsystemd/sd-path/path-lookup.c index 4e4abaebf8..b0f10e4b5a 100644 --- a/src/libsystemd/sd-path/path-lookup.c +++ b/src/libsystemd/sd-path/path-lookup.c @@ -140,6 +140,30 @@ int runtime_directory_make(RuntimeScope scope, const char *subdir, char **ret_di return 0; } +int state_directory_generic(RuntimeScope scope, const char *suffix, char **ret) { + assert(ret); + + /* This does not bother with $STATE_DIRECTORY, and hence can be applied to get other service's state + * dir */ + + switch (scope) { + + case RUNTIME_SCOPE_USER: + return xdg_user_state_dir(suffix, ret); + + case RUNTIME_SCOPE_SYSTEM: { + char *d = path_join("/var/lib", suffix); + if (!d) + return -ENOMEM; + *ret = d; + return 0; + } + + default: + return -EINVAL; + } +} + static const char* const user_data_unit_paths[] = { "/usr/local/lib/systemd/user", "/usr/local/share/systemd/user", diff --git a/src/libsystemd/sd-path/path-lookup.h b/src/libsystemd/sd-path/path-lookup.h index 8dcbf766e6..80e37a571c 100644 --- a/src/libsystemd/sd-path/path-lookup.h +++ b/src/libsystemd/sd-path/path-lookup.h @@ -61,6 +61,7 @@ int config_directory_generic(RuntimeScope scope, const char *suffix, char **ret) int runtime_directory_generic(RuntimeScope scope, const char *suffix, char **ret); int runtime_directory(RuntimeScope scope, const char *fallback_suffix, char **ret); int runtime_directory_make(RuntimeScope scope, const char *subdir, char **ret_dir, char **ret_dir_destroy); +int state_directory_generic(RuntimeScope scope, const char *suffix, char **ret); /* We don't treat /etc/xdg/systemd/ in these functions as the xdg base dir spec suggests because we assume * that is a link to /etc/systemd/ anyway. */ @@ -75,6 +76,9 @@ static inline int xdg_user_config_dir(const char *suffix, char **ret) { static inline int xdg_user_data_dir(const char *suffix, char **ret) { return sd_path_lookup(SD_PATH_USER_SHARED, suffix, ret); } +static inline int xdg_user_state_dir(const char *suffix, char **ret) { + return sd_path_lookup(SD_PATH_USER_STATE_PRIVATE, suffix, ret); +} bool path_is_user_data_dir(const char *path); bool path_is_user_config_dir(const char *path); diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c index d856b1cb48..5b11c81694 100644 --- a/src/shared/blockdev-list.c +++ b/src/shared/blockdev-list.c @@ -8,6 +8,7 @@ #include "blockdev-util.h" #include "device-private.h" #include "device-util.h" +#include "devnum-util.h" #include "errno-util.h" #include "string-util.h" #include "strv.h" @@ -99,14 +100,21 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re size_t n = 0; CLEANUP_ARRAY(l, n, block_device_array_free); - dev_t root_devno = 0; - if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ROOT)) - if (blockdev_get_root(LOG_DEBUG, &root_devno) > 0) { - r = block_get_whole_disk(root_devno, &root_devno); + dev_t root_devno = 0, whole_root_devno = 0; + if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_ROOT)) { + r = blockdev_get_root(LOG_DEBUG, &root_devno); + if (r < 0) + log_debug_errno(r, "Failed to get block device of root device, ignoring: %m"); + else if (r > 0) { + r = block_get_whole_disk(root_devno, &whole_root_devno); if (r < 0) - log_debug_errno(r, "Failed to get whole block device of root device: %m"); + log_debug_errno(r, "Failed to get whole block device of root device, ignoring: %m"); } + /* It's fine if root_devno/whole_root_devno are zero here as devnum_set_and_equal() will + * happily take that into account – it is in fact its primary raison d'etre. */ + } + if (sd_device_enumerator_new(&e) < 0) return log_oom(); @@ -138,7 +146,8 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re continue; } - if (devno == root_devno) + if (devnum_set_and_equal(devno, root_devno) || + devnum_set_and_equal(devno, whole_root_devno)) continue; } @@ -189,10 +198,17 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re } _cleanup_free_ char *model = NULL, *vendor = NULL, *subsystem = NULL; + int ro = -1; if (FLAGS_SET(flags, BLOCKDEV_LIST_METADATA)) { (void) blockdev_get_prop(dev, "ID_MODEL_FROM_DATABASE", "ID_MODEL", &model); (void) blockdev_get_prop(dev, "ID_VENDOR_FROM_DATABASE", "ID_VENDOR", &vendor); (void) blockdev_get_subsystem(dev, &subsystem); + + r = device_get_sysattr_bool(dev, "ro"); + if (r < 0) + log_device_debug_errno(dev, r, "Failed to acquire read-only flag of device '%s', ignoring: %m", node); + else + ro = r; } if (ret_devices) { @@ -216,6 +232,7 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re .model = TAKE_PTR(model), .vendor = TAKE_PTR(vendor), .subsystem = TAKE_PTR(subsystem), + .read_only = ro, }; } else { diff --git a/src/shared/blockdev-list.h b/src/shared/blockdev-list.h index 845f336be5..d82345435f 100644 --- a/src/shared/blockdev-list.h +++ b/src/shared/blockdev-list.h @@ -10,7 +10,7 @@ typedef enum BlockDevListFlags { BLOCKDEV_LIST_REQUIRE_LUKS = 1 << 3, /* Only consider block devices with LUKS superblocks */ BLOCKDEV_LIST_IGNORE_ROOT = 1 << 4, /* Ignore the block device we are currently booted from */ BLOCKDEV_LIST_IGNORE_EMPTY = 1 << 5, /* Ignore disks of zero size (usually drives without a medium) */ - BLOCKDEV_LIST_METADATA = 1 << 6, /* Fill in model, vendor, subsystem */ + BLOCKDEV_LIST_METADATA = 1 << 6, /* Fill in model, vendor, subsystem, read_only */ } BlockDevListFlags; typedef struct BlockDevice { @@ -21,11 +21,13 @@ typedef struct BlockDevice { char *subsystem; uint64_t diskseq; uint64_t size; /* in bytes */ + int read_only; } BlockDevice; #define BLOCK_DEVICE_NULL (BlockDevice) { \ .diskseq = UINT64_MAX, \ .size = UINT64_MAX, \ + .read_only = -1, \ } void block_device_done(BlockDevice *d); diff --git a/src/shared/format-table.c b/src/shared/format-table.c index 200890d3cd..432e054d4a 100644 --- a/src/shared/format-table.c +++ b/src/shared/format-table.c @@ -87,6 +87,7 @@ typedef struct TableData { union { uint8_t data[0]; /* data is generic array */ bool boolean; + int tristate; usec_t timestamp; usec_t timespan; uint64_t size; @@ -342,6 +343,7 @@ static size_t table_data_size(TableDataType type, const void *data) { case TABLE_PERCENT: case TABLE_IFINDEX: case TABLE_SIGNAL: + case TABLE_TRISTATE: return sizeof(int); case TABLE_IN_ADDR: @@ -935,6 +937,7 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { uint64_t uint64; int percent; int ifindex; + int tristate; bool b; union in_addr_union address; sd_id128_t id128; @@ -972,6 +975,11 @@ int table_add_many_internal(Table *t, TableDataType first_type, ...) { data = &buffer.b; break; + case TABLE_TRISTATE: + buffer.tristate = va_arg(ap, int); + data = &buffer.tristate; + break; + case TABLE_TIMESTAMP: case TABLE_TIMESTAMP_UTC: case TABLE_TIMESTAMP_RELATIVE: @@ -1434,12 +1442,25 @@ static int cell_data_compare(TableData *a, size_t index_a, TableData *b, size_t return strv_compare(a->strv, b->strv); case TABLE_BOOLEAN: + case TABLE_BOOLEAN_CHECKMARK: if (!a->boolean && b->boolean) return -1; if (a->boolean && !b->boolean) return 1; return 0; + case TABLE_TRISTATE: + /* NB: we do not use CMP() here, since we want to collapse all negative and all + * positive into one bucket each. */ + if ((a->tristate < 0 && b->tristate >= 0) || + (a->tristate == 0 && b->tristate > 0)) + return -1; + + if ((b->tristate < 0 && a->tristate >= 0) || + (b->tristate == 0 && a->tristate > 0)) + return 1; + return 0; + case TABLE_TIMESTAMP: case TABLE_TIMESTAMP_UTC: case TABLE_TIMESTAMP_RELATIVE: @@ -1691,6 +1712,12 @@ static const char* table_data_format( case TABLE_BOOLEAN_CHECKMARK: return glyph(d->boolean ? GLYPH_CHECK_MARK : GLYPH_CROSS_MARK); + case TABLE_TRISTATE: + if (d->tristate < 0) + return table_ersatz_string(t); + + return yes_no(d->tristate); + case TABLE_TIMESTAMP: case TABLE_TIMESTAMP_UTC: case TABLE_TIMESTAMP_RELATIVE: @@ -2776,6 +2803,12 @@ static int table_data_to_json(TableData *d, sd_json_variant **ret) { case TABLE_BOOLEAN: return sd_json_variant_new_boolean(ret, d->boolean); + case TABLE_TRISTATE: + if (d->tristate < 0) + return sd_json_variant_new_null(ret); + + return sd_json_variant_new_boolean(ret, d->tristate); + case TABLE_TIMESTAMP: case TABLE_TIMESTAMP_UTC: case TABLE_TIMESTAMP_RELATIVE: diff --git a/src/shared/format-table.h b/src/shared/format-table.h index 7665c93e59..5b98d49017 100644 --- a/src/shared/format-table.h +++ b/src/shared/format-table.h @@ -20,6 +20,7 @@ typedef enum TableDataType { TABLE_VERSION, /* just like TABLE_STRING, but uses version comparison when sorting */ TABLE_BOOLEAN, TABLE_BOOLEAN_CHECKMARK, + TABLE_TRISTATE, TABLE_TIMESTAMP, TABLE_TIMESTAMP_UTC, TABLE_TIMESTAMP_RELATIVE, diff --git a/src/test/test-format-table.c b/src/test/test-format-table.c index 677adaa9c9..9aae79e223 100644 --- a/src/test/test-format-table.c +++ b/src/test/test-format-table.c @@ -581,6 +581,72 @@ TEST(table) { "5min 5min \n"); } +TEST(tristate) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *w = NULL; + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_free_ char *formatted = NULL; + + ASSERT_NOT_NULL((t = table_new("name", "flag"))); + + ASSERT_OK(table_add_many(t, + TABLE_STRING, "neg", + TABLE_TRISTATE, -1)); + ASSERT_OK(table_add_many(t, + TABLE_STRING, "zero", + TABLE_TRISTATE, 0)); + ASSERT_OK(table_add_many(t, + TABLE_STRING, "pos", + TABLE_TRISTATE, 1)); + + ASSERT_OK(table_format(t, &formatted)); + printf("%s\n", formatted); + ASSERT_STREQ(formatted, + "NAME FLAG\n" + "neg \n" + "zero no\n" + "pos yes\n"); + formatted = mfree(formatted); + + /* Try a non-default ersatz string. */ + table_set_ersatz_string(t, TABLE_ERSATZ_DASH); + ASSERT_OK(table_format(t, &formatted)); + printf("%s\n", formatted); + ASSERT_STREQ(formatted, + "NAME FLAG\n" + "neg -\n" + "zero no\n" + "pos yes\n"); + formatted = mfree(formatted); + + /* Sorting: -1 < 0 < 1 */ + ASSERT_OK(table_set_sort(t, (size_t) 1, SIZE_MAX)); + ASSERT_OK(table_format(t, &formatted)); + printf("%s\n", formatted); + ASSERT_STREQ(formatted, + "NAME FLAG\n" + "neg -\n" + "zero no\n" + "pos yes\n"); + formatted = mfree(formatted); + + /* JSON: -1 → null, 0 → false, positive → true */ + ASSERT_OK(table_to_json(t, &v)); + + ASSERT_OK(sd_json_build(&w, + SD_JSON_BUILD_ARRAY( + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR("name", JSON_BUILD_CONST_STRING("neg")), + SD_JSON_BUILD_PAIR("flag", SD_JSON_BUILD_NULL)), + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR("name", JSON_BUILD_CONST_STRING("zero")), + SD_JSON_BUILD_PAIR_BOOLEAN("flag", false)), + SD_JSON_BUILD_OBJECT( + SD_JSON_BUILD_PAIR("name", JSON_BUILD_CONST_STRING("pos")), + SD_JSON_BUILD_PAIR_BOOLEAN("flag", true))))); + + ASSERT_TRUE(sd_json_variant_equal(v, w)); +} + TEST(signed_integers) { _cleanup_(table_unrefp) Table *t = NULL; _cleanup_free_ char *formatted = NULL; diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index d04fbc6768..23aa5a5815 100644 --- a/src/test/test-fs-util.c +++ b/src/test/test-fs-util.c @@ -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;