From 45ceb97f6a446fbd7308a99803b4b777f2beb031 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Apr 2026 15:44:52 +0200 Subject: [PATCH 1/5] path-lookup: add state_directory_generic() helper inspired by runtime_directory_generic() --- src/libsystemd/sd-path/path-lookup.c | 24 ++++++++++++++++++++++++ src/libsystemd/sd-path/path-lookup.h | 4 ++++ 2 files changed, 28 insertions(+) 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); From f0abcd5e0fd5ba309e91a2fe589727b814e2967b Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 22 Apr 2026 23:43:17 +0200 Subject: [PATCH 2/5] format-table: add tristate field --- src/shared/format-table.c | 33 ++++++++++++++++++ src/shared/format-table.h | 1 + src/test/test-format-table.c | 66 ++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) 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; From 5827ff2b9dd0b022d6d39119f9c6e1a7b7f57a69 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 22 Apr 2026 10:17:19 +0200 Subject: [PATCH 3/5] fs-util: add XO_AUTO_RW_RO --- src/basic/fs-util.c | 104 +++++++++++++++++++++++++++++++++++----- src/basic/fs-util.h | 1 + src/test/test-fs-util.c | 84 ++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 13 deletions(-) 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/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; From 41a8cc281eb9d76e15b553209d0b7cda4861175e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Apr 2026 08:58:48 +0200 Subject: [PATCH 4/5] blockdev-list: pick up read_only property --- src/shared/blockdev-list.c | 8 ++++++++ src/shared/blockdev-list.h | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c index d856b1cb48..19aa2b48e4 100644 --- a/src/shared/blockdev-list.c +++ b/src/shared/blockdev-list.c @@ -189,10 +189,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 +223,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); From 71d82cc638363e5293930a883d81e8838c468d12 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 23 Apr 2026 15:46:10 +0200 Subject: [PATCH 5/5] blockdev-list: make BLOCKDEV_LIST_IGNORE_ROOT suppress all definitions of the root disk There are various definitions of the root disk, let's suppress them all if the flag is set. So far only the outermost is suppressed, which is a bit weird, given it's "further away" from the rootfs. --- src/shared/blockdev-list.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c index 19aa2b48e4..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; }