From 6b0a3ef8c791badafc24537c0c13cb7e85575123 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 10:26:36 +0200 Subject: [PATCH 1/9] stat-util: introduce a common (stat|fd)_verify_regular_or_block() helper We already had one in repart.c, let's generalize it, and use it everywhere. --- src/basic/stat-util.c | 22 ++++++++++++++++++++++ src/basic/stat-util.h | 3 +++ src/home/homework-luks.c | 7 +++---- src/import/import-raw.c | 7 ++++--- src/mountfsd/mountwork.c | 14 ++++---------- src/repart/repart.c | 24 ++---------------------- src/shared/loop-util.c | 9 +++------ 7 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index d04da52a78..3e2d191bbb 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -220,6 +220,28 @@ int is_device_node(const char *path) { return verify_stat_at(AT_FDCWD, path, false, stat_verify_device_node, false); } +int stat_verify_regular_or_block(const struct stat *st) { + assert(st); + + if (S_ISDIR(st->st_mode)) + return -EISDIR; + + if (S_ISLNK(st->st_mode)) + return -ELOOP; + + if (!S_ISREG(st->st_mode) && !S_ISBLK(st->st_mode)) + return -EBADFD; + + return 0; +} + +int fd_verify_regular_or_block(int fd) { + if (IN_SET(fd, AT_FDCWD, XAT_FDROOT)) + return -EISDIR; + + return verify_stat_at(fd, /* path= */ NULL, /* follow= */ false, stat_verify_regular_or_block, /* verify= */ true); +} + int dir_is_empty_at(int dir_fd, const char *path, bool ignore_hidden_or_backup) { _cleanup_close_ int fd = -EBADF; struct dirent *buf; diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index 2b50c9c558..6eded0d848 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -31,6 +31,9 @@ int fd_verify_linked(int fd); int stat_verify_device_node(const struct stat *st); int is_device_node(const char *path); +int stat_verify_regular_or_block(const struct stat *st); +int fd_verify_regular_or_block(int fd); + int dir_is_empty_at(int dir_fd, const char *path, bool ignore_hidden_or_backup); static inline int dir_is_empty(const char *path, bool ignore_hidden_or_backup) { return dir_is_empty_at(AT_FDCWD, path, ignore_hidden_or_backup); diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index caa05db26f..5342bea21e 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -1250,10 +1250,9 @@ static int open_image_file( if (fstat(image_fd, &st) < 0) return log_error_errno(errno, "Failed to fstat() image file: %m"); - if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) - return log_error_errno( - S_ISDIR(st.st_mode) ? SYNTHETIC_ERRNO(EISDIR) : SYNTHETIC_ERRNO(EBADFD), - "Image file %s is not a regular file or block device.", ip); + r = stat_verify_regular_or_block(&st); + if (r < 0) + return log_error_errno(r, "Image file '%s' is not a regular file or block device.", ip); /* Locking block devices doesn't really make sense, as this might interfere with * udev's workings, and these locks aren't network propagated anyway, hence not what diff --git a/src/import/import-raw.c b/src/import/import-raw.c index 05d4bc9c9f..2095a0dde5 100644 --- a/src/import/import-raw.c +++ b/src/import/import-raw.c @@ -22,6 +22,7 @@ #include "pretty-print.h" #include "qcow2-util.h" #include "ratelimit.h" +#include "stat-util.h" #include "string-util.h" #include "terminal-util.h" #include "time-util.h" @@ -309,9 +310,9 @@ static int raw_import_open_disk(RawImport *i) { if (fstat(i->output_fd, &i->output_stat) < 0) return log_error_errno(errno, "Failed to stat() output file: %m"); - if (!S_ISREG(i->output_stat.st_mode) && !S_ISBLK(i->output_stat.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(EBADFD), - "Target file is not a regular file or block device"); + r = stat_verify_regular_or_block(&i->output_stat); + if (r < 0) + return log_error_errno(r, "Target file is not a regular file or block device."); if (i->offset != UINT64_MAX) { if (lseek(i->output_fd, i->offset, SEEK_SET) < 0) diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c index 3856e9da94..a5d34d4479 100644 --- a/src/mountfsd/mountwork.c +++ b/src/mountfsd/mountwork.c @@ -158,16 +158,10 @@ static int validate_image_fd(int fd, MountImageParameters *p) { assert(fd >= 0); assert(p); - struct stat st; - if (fstat(fd, &st) < 0) - return -errno; - /* Only support regular files and block devices. Let's use stat_verify_regular() here for the nice - * error numbers it generates. */ - if (!S_ISBLK(st.st_mode)) { - r = stat_verify_regular(&st); - if (r < 0) - return r; - } + /* Only support regular files and block devices. */ + r = fd_verify_regular_or_block(fd); + if (r < 0) + return r; fl = fd_verify_safe_flags_full(fd, O_NONBLOCK); if (fl < 0) diff --git a/src/repart/repart.c b/src/repart/repart.c index e9cd0d8569..308ae3aba5 100644 --- a/src/repart/repart.c +++ b/src/repart/repart.c @@ -3226,26 +3226,6 @@ static int determine_current_padding( return 0; } -static int verify_regular_or_block(int fd) { - struct stat st; - - assert(fd >= 0); - - if (fstat(fd, &st) < 0) - return -errno; - - if (S_ISDIR(st.st_mode)) - return -EISDIR; - - if (S_ISLNK(st.st_mode)) - return -ELOOP; - - if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) - return -EBADFD; - - return 0; -} - static int context_copy_from_one(Context *context, const char *src) { _cleanup_close_ int fd = -EBADF; _cleanup_(fdisk_unref_contextp) struct fdisk_context *c = NULL; @@ -3261,9 +3241,9 @@ static int context_copy_from_one(Context *context, const char *src) { if (r < 0) return r; - r = verify_regular_or_block(fd); + r = fd_verify_regular_or_block(fd); if (r < 0) - return log_error_errno(r, "%s is not a file nor a block device: %m", src); + return log_error_errno(r, "'%s' is not a file nor a block device: %m", src); r = fdisk_new_context_at(fd, /* path= */ NULL, /* read_only= */ true, /* sector_size= */ UINT32_MAX, &c); if (r < 0) diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 9349a98493..1c843661cc 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -820,7 +820,6 @@ int loop_device_make_by_path_memory( _cleanup_close_ int fd = -EBADF, mfd = -EBADF; _cleanup_free_ char *fn = NULL; - struct stat st; int r; assert(path); @@ -837,11 +836,9 @@ int loop_device_make_by_path_memory( if (fd < 0) return -errno; - if (fstat(fd, &st) < 0) - return -errno; - - if (!S_ISREG(st.st_mode) && !S_ISBLK(st.st_mode)) - return -EBADF; + r = fd_verify_regular_or_block(fd); + if (r < 0) + return r; r = path_extract_filename(path, &fn); if (r < 0) From 596c0a259df6ac8f42b90b72a32954e14e754186 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 10:28:54 +0200 Subject: [PATCH 2/9] tree-wide: make more use of stat_verify_device_node() --- src/basic/device-nodes.c | 14 +++++++++----- src/core/bpf-devices.c | 6 ++++-- src/nspawn/nspawn.c | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/basic/device-nodes.c b/src/basic/device-nodes.c index 37af18fce4..15abe31236 100644 --- a/src/basic/device-nodes.c +++ b/src/basic/device-nodes.c @@ -6,6 +6,7 @@ #include "device-nodes.h" #include "path-util.h" +#include "stat-util.h" #include "stdio-util.h" #include "string-util.h" #include "utf8.h" @@ -63,6 +64,7 @@ int encode_devnode_name(const char *str, char *str_enc, size_t len) { int devnode_same(const char *a, const char *b) { struct stat sa, sb; + int r; assert(a); assert(b); @@ -72,13 +74,15 @@ int devnode_same(const char *a, const char *b) { if (stat(a, &sa) < 0) return -errno; + r = stat_verify_device_node(&sa); + if (r < 0) + return r; + if (stat(b, &sb) < 0) return -errno; - - if (!S_ISBLK(sa.st_mode) && !S_ISCHR(sa.st_mode)) - return -ENODEV; - if (!S_ISBLK(sb.st_mode) && !S_ISCHR(sb.st_mode)) - return -ENODEV; + r = stat_verify_device_node(&sb); + if (r < 0) + return r; if (((sa.st_mode ^ sb.st_mode) & S_IFMT) != 0) /* both inode same device node type? */ return false; diff --git a/src/core/bpf-devices.c b/src/core/bpf-devices.c index aeb01e8575..432b3d0162 100644 --- a/src/core/bpf-devices.c +++ b/src/core/bpf-devices.c @@ -16,6 +16,7 @@ #include "nulstr-util.h" #include "parse-util.h" #include "path-util.h" +#include "stat-util.h" #include "string-util.h" #define PASS_JUMP_OFF 4096 @@ -308,8 +309,9 @@ int bpf_devices_allow_list_device( return log_warning_errno(errno, "Couldn't stat device %s: %m", node); } - if (!S_ISCHR(st.st_mode) && !S_ISBLK(st.st_mode)) - return log_warning_errno(SYNTHETIC_ERRNO(ENODEV), "%s is not a device.", node); + r = stat_verify_device_node(&st); + if (r < 0) + return log_warning_errno(r, "'%s' is not a device node.", node); mode = st.st_mode; rdev = (dev_t) st.st_rdev; diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index b6332844db..74364ae1e7 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -2049,8 +2049,9 @@ static int copy_devnode_one(const char *dest, const char *node, bool check) { log_debug_errno(errno, "Device node %s does not exist, ignoring.", from); return 0; } - if (!S_ISCHR(st.st_mode) && !S_ISBLK(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "%s is not a device node.", from); + r = stat_verify_device_node(&st); + if (r < 0) + return log_error_errno(r, "'%s' is not a device node.", from); /* Create the parent directory of the device node. Here, we assume that the path has at most one * subdirectory under /dev/, e.g. /dev/net/tun. */ From 7f2410deb1ae5f093f7c9bb761701e2d36fef388 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 10:41:01 +0200 Subject: [PATCH 3/9] stat-util: add shortcut for fd_verify_symlink() We have a similar shortcut in the other fd_verify_xyz() calls, let's add it here too, just for completion's sake. --- src/basic/stat-util.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 3e2d191bbb..3811bd019e 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -145,6 +145,9 @@ int stat_verify_symlink(const struct stat *st) { } int fd_verify_symlink(int fd) { + if (IN_SET(fd, AT_FDCWD, XAT_FDROOT)) + return -EISDIR; + return verify_stat_at(fd, /* path= */ NULL, /* follow= */ false, stat_verify_symlink, /* verify= */ true); } From bda7e913372bef6002529eacf47d66c9a1473348 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 10:45:29 +0200 Subject: [PATCH 4/9] stat-util: also introduce stat_verify_block() and use it everywhere --- src/basic/stat-util.c | 22 ++++++++++++++++++++++ src/basic/stat-util.h | 3 +++ src/fsck/fsck.c | 8 ++++---- src/hibernate-resume/hibernate-resume.c | 7 ++++--- src/home/homed-home.c | 5 +++-- src/home/homework-luks.c | 15 +++++++-------- src/mount/mount-tool.c | 6 +++--- src/shared/btrfs-util.c | 5 +++-- src/shared/discover-image.c | 8 +++----- src/shared/dissect-image.c | 6 ++++-- src/shared/hibernate-util.c | 5 +++-- src/shared/loop-util.c | 15 +++++++++------ 12 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 3811bd019e..2bdd8af21d 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -203,6 +203,28 @@ int fd_verify_linked(int fd) { return verify_stat_at(fd, NULL, false, stat_verify_linked, true); } +int stat_verify_block(const struct stat *st) { + assert(st); + + if (S_ISDIR(st->st_mode)) + return -EISDIR; + + if (S_ISLNK(st->st_mode)) + return -ELOOP; + + if (!S_ISBLK(st->st_mode)) + return -ENOTBLK; + + return 0; +} + +int fd_verify_block(int fd) { + if (IN_SET(fd, AT_FDCWD, XAT_FDROOT)) + return -EISDIR; + + return verify_stat_at(fd, /* path= */ NULL, /* follow= */ false, stat_verify_block, /* verify= */ true); +} + int stat_verify_device_node(const struct stat *st) { assert(st); diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index 6eded0d848..55bc5b6c7e 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -28,6 +28,9 @@ int is_socket(const char *path); int stat_verify_linked(const struct stat *st); int fd_verify_linked(int fd); +int stat_verify_block(const struct stat *st); +int fd_verify_block(int fd); + int stat_verify_device_node(const struct stat *st); int is_device_node(const char *path); diff --git a/src/fsck/fsck.c b/src/fsck/fsck.c index 43cc208e59..405e9c34fd 100644 --- a/src/fsck/fsck.c +++ b/src/fsck/fsck.c @@ -28,6 +28,7 @@ #include "process-util.h" #include "socket-util.h" #include "special.h" +#include "stat-util.h" #include "stdio-util.h" #include "string-table.h" #include "string-util.h" @@ -299,10 +300,9 @@ static int run(int argc, char *argv[]) { if (stat(device, &st) < 0) return log_error_errno(errno, "Failed to stat %s: %m", device); - if (!S_ISBLK(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "%s is not a block device.", - device); + r = stat_verify_block(&st); + if (r < 0) + return log_error_errno(r, "'%s' is not a block device.", device); r = sd_device_new_from_stat_rdev(&dev, &st); if (r < 0) diff --git a/src/hibernate-resume/hibernate-resume.c b/src/hibernate-resume/hibernate-resume.c index f3cf399e1d..2c6eed3248 100644 --- a/src/hibernate-resume/hibernate-resume.c +++ b/src/hibernate-resume/hibernate-resume.c @@ -13,6 +13,7 @@ #include "main-func.h" #include "parse-util.h" #include "pretty-print.h" +#include "stat-util.h" #include "static-destruct.h" static HibernateInfo arg_info = {}; @@ -165,9 +166,9 @@ static int run(int argc, char *argv[]) { if (stat(arg_info.device, &st) < 0) return log_error_errno(errno, "Failed to stat resume device '%s': %m", arg_info.device); - if (!S_ISBLK(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), - "Resume device '%s' is not a block device.", arg_info.device); + r = stat_verify_block(&st); + if (r < 0) + return log_error_errno(r, "Resume device '%s' is not a block device.", arg_info.device); /* The write shall not return if a resume takes place. */ r = write_resume_config(st.st_rdev, arg_info.offset, arg_info.device); diff --git a/src/home/homed-home.c b/src/home/homed-home.c index 12e0eed2da..f467ef7015 100644 --- a/src/home/homed-home.c +++ b/src/home/homed-home.c @@ -3214,8 +3214,9 @@ static int home_get_image_path_seat(Home *h, char **ret) { if (stat(ip, &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; r = sd_device_new_from_stat_rdev(&d, &st); if (r < 0) diff --git a/src/home/homework-luks.c b/src/home/homework-luks.c index 5342bea21e..f105acfa3b 100644 --- a/src/home/homework-luks.c +++ b/src/home/homework-luks.c @@ -202,16 +202,14 @@ static int probe_file_system_by_path(const char *path, char **ret_fstype, sd_id1 } static int block_get_size_by_fd(int fd, uint64_t *ret) { - struct stat st; + int r; assert(fd >= 0); assert(ret); - if (fstat(fd, &st) < 0) - return -errno; - - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = fd_verify_block(fd); + if (r < 0) + return r; return blockdev_get_device_size(fd, ret); } @@ -2275,8 +2273,9 @@ int home_create_luks( if (setup->image_fd < 0) return setup->image_fd; - if (!S_ISBLK(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTBLK), "Device is not a block device, refusing."); + r = stat_verify_block(&st); + if (r < 0) + return log_error_errno(r, "Device is not a block device, refusing."); if (asprintf(&sysfs, "/sys/dev/block/" DEVNUM_FORMAT_STR "/partition", DEVNUM_FORMAT_VAL(st.st_rdev)) < 0) return log_oom(); diff --git a/src/mount/mount-tool.c b/src/mount/mount-tool.c index 768ff349e2..f1c4c90d76 100644 --- a/src/mount/mount-tool.c +++ b/src/mount/mount-tool.c @@ -1413,9 +1413,9 @@ static int discover_device(void) { if (S_ISREG(st.st_mode)) return discover_loop_backing_file(); - if (!S_ISBLK(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), - "Unsupported mount source type for --discover: %s", arg_mount_what); + r = stat_verify_block(&st); + if (r < 0) + return log_error_errno(r, "Unsupported mount source type for --discover: %s", arg_mount_what); r = sd_device_new_from_stat_rdev(&d, &st); if (r < 0) diff --git a/src/shared/btrfs-util.c b/src/shared/btrfs-util.c index 9bd0a74c5f..8e41f569ba 100644 --- a/src/shared/btrfs-util.c +++ b/src/shared/btrfs-util.c @@ -149,8 +149,9 @@ int btrfs_get_block_device_at(int dir_fd, const char *path, dev_t *ret) { if (stat((char*) di.path, &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; if (major(st.st_rdev) == 0) return -ENODEV; diff --git a/src/shared/discover-image.c b/src/shared/discover-image.c index 6b99549134..1595bb2184 100644 --- a/src/shared/discover-image.c +++ b/src/shared/discover-image.c @@ -1895,17 +1895,15 @@ int image_read_only(Image *i, bool b, RuntimeScope scope) { case IMAGE_BLOCK: { _cleanup_close_ int fd = -EBADF; - struct stat st; int state = b; fd = open(i->path, O_CLOEXEC|O_RDONLY|O_NONBLOCK|O_NOCTTY); if (fd < 0) return -errno; - if (fstat(fd, &st) < 0) - return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTTY; + r = fd_verify_block(fd); + if (r < 0) + return r; if (ioctl(fd, BLKROSET, &state) < 0) return -errno; diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 9817ed87ec..c5911beb71 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -2141,14 +2141,16 @@ DissectedImage* dissected_image_unref(DissectedImage *m) { static int is_loop_device(const char *path) { char s[SYS_BLOCK_PATH_MAX("/../loop/")]; struct stat st; + int r; assert(path); if (stat(path, &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; xsprintf_sys_block_path(s, "/loop/", st.st_dev); if (access(s, F_OK) < 0) { diff --git a/src/shared/hibernate-util.c b/src/shared/hibernate-util.c index 001eaa920c..c141cdc8c6 100644 --- a/src/shared/hibernate-util.c +++ b/src/shared/hibernate-util.c @@ -209,8 +209,9 @@ static int swap_entry_get_resume_config(SwapEntry *swap) { return -errno; if (!swap->swapfile) { - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; swap->devno = st.st_rdev; swap->offset = 0; diff --git a/src/shared/loop-util.c b/src/shared/loop-util.c index 1c843661cc..e6fe4dbb49 100644 --- a/src/shared/loop-util.c +++ b/src/shared/loop-util.c @@ -378,9 +378,9 @@ static int loop_configure( } static int fd_get_max_discard(int fd, uint64_t *ret) { - struct stat st; char sysfs_path[STRLEN("/sys/dev/block/" ":" "/queue/discard_max_bytes") + DECIMAL_STR_MAX(dev_t) * 2 + 1]; _cleanup_free_ char *buffer = NULL; + struct stat st; int r; assert(ret); @@ -388,8 +388,9 @@ static int fd_get_max_discard(int fd, uint64_t *ret) { if (fstat(ASSERT_FD(fd), &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; xsprintf(sysfs_path, "/sys/dev/block/" DEVNUM_FORMAT_STR "/queue/discard_max_bytes", DEVNUM_FORMAT_VAL(st.st_rdev)); @@ -401,14 +402,16 @@ static int fd_get_max_discard(int fd, uint64_t *ret) { } static int fd_set_max_discard(int fd, uint64_t max_discard) { - struct stat st; char sysfs_path[STRLEN("/sys/dev/block/" ":" "/queue/discard_max_bytes") + DECIMAL_STR_MAX(dev_t) * 2 + 1]; + struct stat st; + int r; if (fstat(ASSERT_FD(fd), &st) < 0) return -errno; - if (!S_ISBLK(st.st_mode)) - return -ENOTBLK; + r = stat_verify_block(&st); + if (r < 0) + return r; xsprintf(sysfs_path, "/sys/dev/block/" DEVNUM_FORMAT_STR "/queue/discard_max_bytes", DEVNUM_FORMAT_VAL(st.st_rdev)); From 8e0756c0b5e020dc8f8b53c3e69fffcb0892f599 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 11:09:25 +0200 Subject: [PATCH 5/9] tree-wide: use stat_verify_regular() more --- src/basic/locale-util.c | 6 ++++-- src/shared/hibernate-util.c | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/basic/locale-util.c b/src/basic/locale-util.c index 11e91ab834..3b869d70f9 100644 --- a/src/basic/locale-util.c +++ b/src/basic/locale-util.c @@ -16,6 +16,7 @@ #include "path-util.h" #include "process-util.h" #include "set.h" +#include "stat-util.h" #include "string-table.h" #include "string-util.h" #include "strv.h" @@ -115,8 +116,9 @@ static int add_locales_from_archive(Set *locales) { if (fstat(fd, &st) < 0) return -errno; - if (!S_ISREG(st.st_mode)) - return -EBADMSG; + r = stat_verify_regular(&st); + if (r < 0) + return r; if (st.st_size < (off_t) sizeof(struct locarhead)) return -EBADMSG; diff --git a/src/shared/hibernate-util.c b/src/shared/hibernate-util.c index c141cdc8c6..f9e095f273 100644 --- a/src/shared/hibernate-util.c +++ b/src/shared/hibernate-util.c @@ -37,14 +37,16 @@ int read_fiemap(int fd, struct fiemap **ret) { uint32_t result_extents = 0; uint64_t fiemap_start = 0, fiemap_length; const size_t n_extra = DIV_ROUND_UP(sizeof(struct fiemap), sizeof(struct fiemap_extent)); + int r; assert(fd >= 0); assert(ret); if (fstat(fd, &statinfo) < 0) return log_debug_errno(errno, "Cannot determine file size: %m"); - if (!S_ISREG(statinfo.st_mode)) - return -ENOTTY; + r = stat_verify_regular(&statinfo); + if (r < 0) + return r; fiemap_length = statinfo.st_size; /* Zero this out in case we run on a file with no extents */ From 4031a73a4b44e6da040c232ae548340dcb46c5c4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 11:10:03 +0200 Subject: [PATCH 6/9] tree-wide: use stat_verify_directory() more --- src/libsystemd/sd-journal/sd-journal.c | 9 +++------ src/shared/btrfs-util.c | 9 +++------ src/shared/chown-recursive.c | 7 +++++-- src/shared/find-esp.c | 15 +++++++++------ src/shared/rm-rf.c | 5 +++-- 5 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/libsystemd/sd-journal/sd-journal.c b/src/libsystemd/sd-journal/sd-journal.c index 3e5185b23f..52b3e616a1 100644 --- a/src/libsystemd/sd-journal/sd-journal.c +++ b/src/libsystemd/sd-journal/sd-journal.c @@ -2456,7 +2456,6 @@ _public_ int sd_journal_open_files(sd_journal **ret, const char **paths, int fla _public_ int sd_journal_open_directory_fd(sd_journal **ret, int fd, int flags) { _cleanup_(sd_journal_closep) sd_journal *j = NULL; - struct stat st; bool take_fd; int r; @@ -2464,11 +2463,9 @@ _public_ int sd_journal_open_directory_fd(sd_journal **ret, int fd, int flags) { assert_return(fd >= 0, -EBADF); assert_return((flags & ~OPEN_DIRECTORY_FD_ALLOWED_FLAGS) == 0, -EINVAL); - if (fstat(fd, &st) < 0) - return -errno; - - if (!S_ISDIR(st.st_mode)) - return -EBADFD; + r = fd_verify_directory(fd); + if (r < 0) + return r; take_fd = FLAGS_SET(flags, SD_JOURNAL_TAKE_DIRECTORY_FD); j = journal_new(flags & ~SD_JOURNAL_TAKE_DIRECTORY_FD, NULL, NULL); diff --git a/src/shared/btrfs-util.c b/src/shared/btrfs-util.c index 8e41f569ba..cde21bd602 100644 --- a/src/shared/btrfs-util.c +++ b/src/shared/btrfs-util.c @@ -879,18 +879,15 @@ static int subvol_remove_children(int fd, const char *subvolume, uint64_t subvol struct btrfs_ioctl_vol_args vol_args = {}; _cleanup_close_ int subvol_fd = -EBADF; - struct stat st; bool made_writable = false; int r; assert(fd >= 0); assert(subvolume); - if (fstat(fd, &st) < 0) - return -errno; - - if (!S_ISDIR(st.st_mode)) - return -EINVAL; + r = fd_verify_directory(fd); + if (r < 0) + return r; subvol_fd = openat(fd, subvolume, O_RDONLY|O_NOCTTY|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW); if (subvol_fd < 0) diff --git a/src/shared/chown-recursive.c b/src/shared/chown-recursive.c index 850e495836..a0e885ece9 100644 --- a/src/shared/chown-recursive.c +++ b/src/shared/chown-recursive.c @@ -9,6 +9,7 @@ #include "fd-util.h" #include "fs-util.h" #include "path-util.h" +#include "stat-util.h" #include "strv.h" #include "user-util.h" #include "xattr-util.h" @@ -144,6 +145,7 @@ int fd_chown_recursive( int duplicated_fd = -EBADF; struct stat st; + int r; /* Note that the slightly different order of fstat() and the checks here and in * path_chown_recursive(). That's because when we open the directory ourselves we can specify @@ -153,8 +155,9 @@ int fd_chown_recursive( if (fstat(fd, &st) < 0) return -errno; - if (!S_ISDIR(st.st_mode)) - return -ENOTDIR; + r = stat_verify_directory(&st); + if (r < 0) + return r; if (!uid_is_valid(uid) && !gid_is_valid(gid) && FLAGS_SET(mask, 07777)) return 0; /* nothing to do */ diff --git a/src/shared/find-esp.c b/src/shared/find-esp.c index 3f490ced71..1d13683f12 100644 --- a/src/shared/find-esp.c +++ b/src/shared/find-esp.c @@ -297,8 +297,9 @@ static int verify_fsroot_dir( (unprivileged_mode && ERRNO_IS_NEG_PRIVILEGE(r)) ? LOG_DEBUG : LOG_ERR, r, "Failed to determine block device node of \"%s\": %m", path); - if (!S_ISDIR(sx.stx_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "Path \"%s\" is not a directory", path); + r = statx_verify_directory(&sx); + if (r < 0) + return log_error_errno(r, "Path \"%s\" is not a directory", path); if (!FLAGS_SET(sx.stx_attributes, STATX_ATTR_MOUNT_ROOT)) return log_full_errno(searching ? LOG_DEBUG : LOG_ERR, @@ -477,8 +478,9 @@ int find_esp_and_warn_at_full( if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to stat '%s': %m", p); - if (!S_ISDIR(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "ESP path '%s' is not a directory.", p); + r = stat_verify_directory(&st); + if (r < 0) + return log_error_errno(r, "ESP path '%s' is not a directory.", p); if (ret_path) *ret_path = TAKE_PTR(p); @@ -829,8 +831,9 @@ int find_xbootldr_and_warn_at_full( if (fstat(fd, &st) < 0) return log_error_errno(errno, "Failed to stat '%s': %m", p); - if (!S_ISDIR(st.st_mode)) - return log_error_errno(SYNTHETIC_ERRNO(ENOTDIR), "XBOOTLDR path '%s' is not a directory.", p); + r = stat_verify_directory(&st); + if (r < 0) + return log_error_errno(r, "XBOOTLDR path '%s' is not a directory.", p); if (ret_path) *ret_path = TAKE_PTR(p); diff --git a/src/shared/rm-rf.c b/src/shared/rm-rf.c index 8c179bbaca..ede18318ab 100644 --- a/src/shared/rm-rf.c +++ b/src/shared/rm-rf.c @@ -37,8 +37,9 @@ static int patch_dirfd_mode( if (fstat(dfd, &st) < 0) return -errno; - if (!S_ISDIR(st.st_mode)) - return -ENOTDIR; + r = stat_verify_directory(&st); + if (r < 0) + return r; if (FLAGS_SET(st.st_mode, 0700)) { /* Already set? */ if (refuse_already_set) From 4eb60e3160a5d00587042514362eb8fb25c6ef5e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 11:10:36 +0200 Subject: [PATCH 7/9] socket-netlink: use stat_verify_socket() more --- src/shared/socket-netlink.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shared/socket-netlink.c b/src/shared/socket-netlink.c index 1d369353b9..438b2d172b 100644 --- a/src/shared/socket-netlink.c +++ b/src/shared/socket-netlink.c @@ -18,6 +18,7 @@ #include "socket-label.h" #include "socket-netlink.h" #include "socket-util.h" +#include "stat-util.h" #include "string-util.h" int socket_address_parse(SocketAddress *a, const char *s) { @@ -497,8 +498,9 @@ int af_unix_get_qlen(int fd, uint32_t *ret) { struct stat st; if (fstat(fd, &st) < 0) return -errno; - if (!S_ISSOCK(st.st_mode)) - return -ENOTSOCK; + r = stat_verify_socket(&st); + if (r < 0) + return r; _cleanup_(sd_netlink_unrefp) sd_netlink *nl = NULL; r = sd_sock_diag_socket_open(&nl); From aa0da09127f4c5c4a2063cf0bc4e629b878a7e27 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 11:20:57 +0200 Subject: [PATCH 8/9] stat-util: also add stat_verify_char() and use it everywhere --- src/basic/stat-util.c | 15 +++++++++++++++ src/basic/stat-util.h | 2 ++ src/basic/terminal-util.c | 12 ++++++++---- src/core/manager.c | 5 ++--- src/shared/watchdog.c | 7 +++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 2bdd8af21d..116b36a346 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -225,6 +225,21 @@ int fd_verify_block(int fd) { return verify_stat_at(fd, /* path= */ NULL, /* follow= */ false, stat_verify_block, /* verify= */ true); } +int stat_verify_char(const struct stat *st) { + assert(st); + + if (S_ISDIR(st->st_mode)) + return -EISDIR; + + if (S_ISLNK(st->st_mode)) + return -ELOOP; + + if (!S_ISCHR(st->st_mode)) + return -EBADFD; + + return 0; +} + int stat_verify_device_node(const struct stat *st) { assert(st); diff --git a/src/basic/stat-util.h b/src/basic/stat-util.h index 55bc5b6c7e..ec04a2b80c 100644 --- a/src/basic/stat-util.h +++ b/src/basic/stat-util.h @@ -31,6 +31,8 @@ int fd_verify_linked(int fd); int stat_verify_block(const struct stat *st); int fd_verify_block(int fd); +int stat_verify_char(const struct stat *st); + int stat_verify_device_node(const struct stat *st); int is_device_node(const char *path); diff --git a/src/basic/terminal-util.c b/src/basic/terminal-util.c index ecdc241247..d7ff92ae89 100644 --- a/src/basic/terminal-util.c +++ b/src/basic/terminal-util.c @@ -1861,6 +1861,8 @@ int terminal_set_cursor_position(int fd, unsigned row, unsigned column) { } static int terminal_verify_same(int input_fd, int output_fd) { + int r; + assert(input_fd >= 0); assert(output_fd >= 0); @@ -1871,15 +1873,17 @@ static int terminal_verify_same(int input_fd, int output_fd) { if (fstat(input_fd, &sti) < 0) return -errno; - if (!S_ISCHR(sti.st_mode)) /* TTYs are character devices */ - return -ENOTTY; + r = stat_verify_char(&sti); /* TTYs are character devices */ + if (r < 0) + return r; struct stat sto; if (fstat(output_fd, &sto) < 0) return -errno; - if (!S_ISCHR(sto.st_mode)) - return -ENOTTY; + r = stat_verify_char(&sto); + if (r < 0) + return r; if (sti.st_rdev != sto.st_rdev) return -ENOLINK; diff --git a/src/core/manager.c b/src/core/manager.c index 73368ec18a..8db6c471a1 100644 --- a/src/core/manager.c +++ b/src/core/manager.c @@ -4519,10 +4519,9 @@ const char* manager_get_confirm_spawn(Manager *m) { goto fail; } - if (!S_ISCHR(st.st_mode)) { - r = -ENOTTY; + r = stat_verify_char(&st); + if (r < 0) goto fail; - } last_errno = 0; return m->confirm_spawn; diff --git a/src/shared/watchdog.c b/src/shared/watchdog.c index 5b11301395..8b74bb4bbd 100644 --- a/src/shared/watchdog.c +++ b/src/shared/watchdog.c @@ -17,6 +17,7 @@ #include "log.h" #include "path-util.h" #include "ratelimit.h" +#include "stat-util.h" #include "string-util.h" #include "strv.h" #include "time-util.h" @@ -55,6 +56,7 @@ static int saturated_usec_to_sec(usec_t val) { static int watchdog_get_sysfs_path(const char *filename, char **ret_path) { struct stat st; + int r; if (watchdog_fd < 0) return -EBADF; @@ -62,8 +64,9 @@ static int watchdog_get_sysfs_path(const char *filename, char **ret_path) { if (fstat(watchdog_fd, &st)) return -errno; - if (!S_ISCHR(st.st_mode)) - return -EBADF; + r = stat_verify_char(&st); + if (r < 0) + return r; if (asprintf(ret_path, "/sys/dev/char/"DEVNUM_FORMAT_STR"/%s", DEVNUM_FORMAT_VAL(st.st_rdev), filename) < 0) return -ENOMEM; From 7977c1427382b0a3b340e74d198c2dad5b69b2ec Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Mon, 13 Apr 2026 13:14:46 +0200 Subject: [PATCH 9/9] stat-util: always check S_ISDIR() before S_ISLNK() Check S_ISDIR() before S_ISLINK() for all stat_verify_xyz() helpers first, where we check them. Just to ensure we systematically return the same errors. --- src/basic/stat-util.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/basic/stat-util.c b/src/basic/stat-util.c index 116b36a346..06634b7df6 100644 --- a/src/basic/stat-util.c +++ b/src/basic/stat-util.c @@ -157,12 +157,12 @@ int is_symlink(const char *path) { } static int mode_verify_socket(mode_t mode) { - if (S_ISLNK(mode)) - return -ELOOP; - if (S_ISDIR(mode)) return -EISDIR; + if (S_ISLNK(mode)) + return -ELOOP; + if (!S_ISSOCK(mode)) return -ENOTSOCK; @@ -243,12 +243,12 @@ int stat_verify_char(const struct stat *st) { int stat_verify_device_node(const struct stat *st) { assert(st); - if (S_ISLNK(st->st_mode)) - return -ELOOP; - if (S_ISDIR(st->st_mode)) return -EISDIR; + if (S_ISLNK(st->st_mode)) + return -ELOOP; + if (!S_ISBLK(st->st_mode) && !S_ISCHR(st->st_mode)) return -ENOTTY;