preparatory work for the storage provider work in #41776 (#41814)

nothing earth shattering, but generally useful. without #41776 it has no
immediate users though
This commit is contained in:
Lennart Poettering
2026-04-26 23:44:02 +02:00
committed by GitHub
10 changed files with 330 additions and 20 deletions
+91 -13
View 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;
}
}
}
+1
View 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);
+24
View File
@@ -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",
+4
View File
@@ -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);
+23 -6
View File
@@ -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 {
+3 -1
View File
@@ -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);
+33
View File
@@ -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:
+1
View File
@@ -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,
+66
View File
@@ -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;
+84
View 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;