diff --git a/.github/workflows/unit-tests-musl.yml b/.github/workflows/unit-tests-musl.yml
index a5b619796f2b6..614482df0cff6 100644
--- a/.github/workflows/unit-tests-musl.yml
+++ b/.github/workflows/unit-tests-musl.yml
@@ -68,6 +68,7 @@ jobs:
libqrencode-dev
libseccomp-dev
libselinux-dev
+ libucontext-dev
libxkbcommon-dev
linux-pam-dev
lz4-dev
diff --git a/TODO.md b/TODO.md
index e51f6734f065e..60a11dcee3ad1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -128,6 +128,21 @@ SPDX-License-Identifier: LGPL-2.1-or-later
## Features
+- StorageProvider interface + storagectl
+ - hook-up in systemd-nspawn
+ - hook-up in systemd-vmspawn
+ - hook-up in service manager (BindVolume=)
+ - introduce a locking concept: right now all access to volumes is fully
+ shared. Let's add a basic locking concept: supporting backends can take an
+ additional locking flag (which has to be combined with Varlink's "more"),
+ in which case access would only be handed out to one client at a time, with
+ the lock's lifetime synced up with the Varlink connection lifetime.
+ - introduce a volume lifecycle concept: optionally support volumes whose
+ whole lifecycle is associated with the varlink connections they are tied
+ to: when the last varlink connection that acquired them goes away, the
+ volume is auto-destroyed. Would be exposed via a new flag on the Acquire
+ call, similar to the locking logic above.
+
- a small tool that can do basic btrfs raid policy mgmt. i.e. gets started as
part of the initial transaction for some btrfs raid fs, waits for some time,
then puts message on screen (plymouth, console) that some devices apparently
@@ -2545,8 +2560,9 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- systemd-tpm2-support: add a some logic that detects if system is in DA
lockout mode, and queries the user for TPM recovery PIN then.
-- systemd: add storage API via varlink, where everyone can drop a socket in a
- dir, similar, do the same thing for networking
+- add a networking provider API, inspired by the StorageProvider. Make networkd
+ a provider that exposes interfaces for adding tap, tun, veth via the api,
+ base this on .netdev logic somehow.
- $SYSTEMD_EXECPID that the service manager sets should
be augmented with $SYSTEMD_EXECPIDFD (and similar for
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 4aae561512991..719838064c02f 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -972,6 +972,7 @@ manpages = [
['sd_watchdog_enabled', '3', [], ''],
['shutdown', '8', [], ''],
['smbios-type-11', '7', [], ''],
+ ['storagectl', '1', ['mount.storage'], ''],
['sysctl.d', '5', [], ''],
['sysext.conf',
'5',
@@ -1186,6 +1187,14 @@ manpages = [
['systemd-ssh-issue', '1', [], ''],
['systemd-ssh-proxy', '1', [], ''],
['systemd-stdio-bridge', '1', [], ''],
+ ['systemd-storage-block@.service',
+ '8',
+ ['systemd-storage-block', 'systemd-storage-block.socket'],
+ ''],
+ ['systemd-storage-fs@.service',
+ '8',
+ ['systemd-storage-fs', 'systemd-storage-fs.socket'],
+ ''],
['systemd-storagetm.service', '8', ['systemd-storagetm'], 'ENABLE_STORAGETM'],
['systemd-stub',
'7',
diff --git a/man/storagectl.xml b/man/storagectl.xml
new file mode 100644
index 0000000000000..5fddf3ca08db5
--- /dev/null
+++ b/man/storagectl.xml
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+ storagectl
+ systemd
+
+
+
+ storagectl
+ 1
+
+
+
+ storagectl
+ mount.storage
+ Enumerate and mount storage volumes provided by storage providers
+
+
+
+
+ storagectl
+ OPTIONS
+ COMMAND
+ NAME
+
+
+
+ mount
+ -t
+ storage
+ PROVIDER:VOLUME
+ DIRECTORY
+
+
+
+ mount
+ -t
+ storage.FSTYPE
+ PROVIDER:VOLUME
+ DIRECTORY
+
+
+
+
+ Description
+
+ storagectl may be used to inspect storage providers and the storage
+ volumes they expose. A storage provider is a service implementing the
+ io.systemd.StorageProvider Varlink
+ interface, registered as an AF_UNIX socket below the well-known socket directory
+ /run/systemd/io.systemd.StorageProvider/ (in system mode) or
+ $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/ (in user mode). The two
+ storage providers shipped with systemd are
+ systemd-storage-block@.service8,
+ which exposes the system's block devices, and
+ systemd-storage-fs@.service8,
+ which exposes regular files and directories from a backing file system.
+
+ The tool also provides a mount8 helper
+ for the file system type storage, which permits mounting storage volumes to arbitrary
+ places. See "Use as a mount helper" below for details.
+
+
+
+ Commands
+
+ The following commands are understood:
+
+
+
+
+ volumes GLOB
+
+ List storage volumes provided by all storage providers running on the
+ system (or, with , in the user runtime). The optional
+ GLOB argument is a shell-style pattern (see
+ fnmatch3)
+ that filters the result by volume name. The output is a table containing the providing
+ service, the volume name, its type (blk, reg or
+ dir), whether it is read-only, and — if known — its size and the number
+ of bytes used.
+
+ This is the default command if none is specified.
+
+
+
+
+
+ templates GLOB
+
+ List volume templates supported by the running storage providers. Templates
+ encapsulate a configuration to use when creating volumes on-the-fly, when they are acquired. Template
+ support is an optional feature for providers, and only applies to providers that allow creation
+ of volumes on-the-fly. See the respective provider documentation for details, for example
+ systemd-storage-fs@.service8. The
+ optional GLOB argument filters by template name. Storage providers that do
+ not implement template-based volume creation (such as the block-device provider) do not contribute to
+ this output.
+
+
+
+
+
+ providers
+
+ List the storage providers known to the system. This is determined by scanning the
+ well-known socket directory for AF_UNIX sockets that look like
+ io.systemd.StorageProvider endpoints. For each provider it is also reported
+ whether the socket can currently be connected to.
+
+
+
+
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+
+ Operate on system-wide storage providers. Sockets are looked for in
+ /run/systemd/io.systemd.StorageProvider/. This is the default.
+
+
+
+
+
+
+
+ Operate on per-user storage providers. Sockets are looked for in
+ $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Use as a mount helper
+
+ The tool provides the /sbin/mount.storage alias, implementing the
+ mount8
+ "external helper" interface, allowing storage volumes to be mounted with the regular
+ mount command. The volume to mount is encoded as the source of the mount,
+ in the form
+ PROVIDER:VOLUME, where
+ PROVIDER is the name of a storage provider (as listed by
+ storagectl providers) and VOLUME is the volume
+ name. Two file system type spellings are recognized:
+
+
+
+ storage
+
+ Acquires a directory volume and bind-mounts its directory tree onto the
+ target.
+
+
+
+
+
+ storage.FSTYPE
+
+ Acquires a regular file or block device volume and mounts it as a file system of type
+ FSTYPE (for example storage.ext4,
+ storage.btrfs, …).
+
+
+
+
+
+ The standard mount options are forwarded to
+ mount. In addition, the following storage.-prefixed
+ options are interpreted by mount.storage itself and stripped from the
+ forwarded list:
+
+
+
+ MODE
+
+ Takes one of any (open if it exists, otherwise create — the
+ default), open (fail if the volume does not yet exist) or new
+ (fail if the volume already exists).
+
+
+
+
+
+ NAME
+
+ The template to use when creating a new volume, if it is missing and the provider
+ supports on-the-fly creation of volumes.
+
+
+
+
+
+ BYTES
+
+ When creating a new volume on-the-fly, the size in bytes to allocate. Accepts the
+ usual K/M/G/T suffixes
+ (base 1024). Required when creating a regular file volume.
+
+
+
+
+
+
+
+
+ Examples
+
+
+ Enumerate available storage providers, volumes and templates
+
+ $ storagectl providers
+$ storagectl volumes
+$ storagectl volumes '*foo*'
+$ storagectl templates
+
+
+
+ Mount a directory volume from the file system provider
+
+ # mount -t storage fs:myvol /mnt/myvol
+
+ If the volume myvol does not yet exist, it will be created using
+ the default subvolume template.
+
+
+
+ Create and mount an ext4 file system from a regular file.
+
+ # mount -t storage.ext4 fs:scratch /mnt/scratch -o loop
+
+
+
+ Mount a block device volume read-only
+
+ # mount -t storage.ext4 -o ro block:/dev/disk/by-id/usb-foo /mnt/foo
+
+
+
+
+ Exit status
+
+ On success, 0 is returned, a non-zero failure code otherwise.
+
+
+
+
+
+ See Also
+
+ systemd1
+ systemd-storage-block@.service8
+ systemd-storage-fs@.service8
+ varlinkctl1
+ mount8
+
+
+
+
diff --git a/man/systemd-boot.xml b/man/systemd-boot.xml
index dab10ed8ef12a..1acf5d083e580 100644
--- a/man/systemd-boot.xml
+++ b/man/systemd-boot.xml
@@ -406,6 +406,66 @@
loader.conf5.
+
+ Companion Files
+
+ For Type #1 boot loader entries (as defined in the UAPI.1 Boot Loader
+ Specification) systemd-boot will collect additional companion resources
+ declared via the extra key in the entry, dynamically generate
+ cpio initrd archives from them, and register those archives via the Linux initrd EFI
+ protocol so that they are passed to the kernel together with the entry's own initrd. This is supported
+ for entries referencing a Unified Kernel Image (UKI) via the uki or
+ uki-url keys. Each extra key references a single regular file
+ (relative to the root of the file system containing the entry snippet) and the key may be specified
+ multiple times. Companion resources are recognized by file name suffix:
+
+
+ Files with the .cred suffix are packed into a
+ cpio archive placed in the /.extra/credentials/ directory of
+ the initrd file hierarchy. This is intended to convey auxiliary, encrypted, authenticated credentials
+ for use with LoadCredentialEncrypted=. See
+ systemd.exec5 and
+ systemd-creds1 for
+ details on encrypted credentials. The generated cpio archive is measured into TPM
+ PCR 12 (if a TPM is present).
+
+ Files with the .sysext.raw suffix are packed into a
+ cpio archive placed in the /.extra/sysext/ directory of the
+ initrd file hierarchy. This is intended to pass additional entry-specific system extension images to
+ the initrd. See
+ systemd-sysext8 for
+ details on system extension images. The generated cpio archive is measured into TPM
+ PCR 13 (if a TPM is present).
+
+ Files with the .confext.raw suffix are packed into a
+ cpio archive placed in the /.extra/confext/ directory of the
+ initrd file hierarchy. This is intended to pass additional entry-specific configuration extension
+ images to the initrd. See
+ systemd-confext8
+ for details on configuration extension images. The generated cpio archive is
+ measured into TPM PCR 12 (if a TPM is present).
+
+
+ When the booted kernel is a UKI, the systemd-stub UEFI stub embedded in it will
+ combine the companion resources injected here with any companion files it itself collects from the UKI's
+ .extra.d/ drop-in directory and from /loader/credentials/ and
+ /loader/extensions/, so that all sources are merged uniformly into
+ /.extra/ in the initrd. See
+ systemd-stub7 for
+ details.
+
+ Example Type #1 entry making use of the extra key:
+
+ title My OS
+version 1.2.3
+machine-id 6a9857a393724b7a981ebb5b8495b9ea
+uki /6a9857a393724b7a981ebb5b8495b9ea/1.2.3/img.efi
+extra /6a9857a393724b7a981ebb5b8495b9ea/1.2.3/foo.cred
+extra /6a9857a393724b7a981ebb5b8495b9ea/1.2.3/bar.sysext.raw
+extra /6a9857a393724b7a981ebb5b8495b9ea/1.2.3/baz.confext.raw
+
+
EFI Variables
diff --git a/man/systemd-storage-block@.service.xml b/man/systemd-storage-block@.service.xml
new file mode 100644
index 0000000000000..ee6022af053bb
--- /dev/null
+++ b/man/systemd-storage-block@.service.xml
@@ -0,0 +1,97 @@
+
+
+
+
+
+
+
+ systemd-storage-block@.service
+ systemd
+
+
+
+ systemd-storage-block@.service
+ 8
+
+
+
+ systemd-storage-block@.service
+ systemd-storage-block.socket
+ systemd-storage-block
+ Storage provider exposing local block devices as storage volumes
+
+
+
+ systemd-storage-block@.service
+ systemd-storage-block.socket
+
+
+
+ Description
+
+ systemd-storage-block@.service is a system service that implements the
+ io.systemd.StorageProvider Varlink
+ interface, exposing the system's block devices (such as disks, partitions, and device-mapper
+ nodes) as storage volumes that may be acquired by other programs as file descriptors.
+
+ The service is socket-activated via systemd-storage-block.socket, which
+ listens on the AF_UNIX socket /run/systemd/io.systemd.StorageProvider/block. The
+ socket directory /run/systemd/io.systemd.StorageProvider/ is the well-known location
+ where storage providers register, see
+ storagectl1 for an
+ enumeration tool.
+
+ See also
+ systemd-storage-fs@.service8
+ for a complementary implementation that exposes regular files and directories from a backing file
+ system.
+
+
+
+ Volumes
+
+ The volumes exposed via the provider are identified by an absolute path (which must begin with
+ /dev/), i.e. as a kernel block device node such as /dev/sda or
+ /dev/disk/by-id/…. Volume names that are not normalized or that do not begin with
+ /dev/ are not accepted.
+
+
+
+ Options
+
+ The following options are understood:
+
+
+
+
+
+
+
+
+ Files
+
+
+
+ /run/systemd/io.systemd.StorageProvider/block
+
+ AF_UNIX socket the service listens on. This is the canonical location
+ for the block storage provider, and is enumerated by
+ storagectl providers.
+
+
+
+
+
+
+
+ See Also
+
+ systemd1
+ storagectl1
+ systemd-storage-fs@.service8
+
+
+
+
diff --git a/man/systemd-storage-fs@.service.xml b/man/systemd-storage-fs@.service.xml
new file mode 100644
index 0000000000000..4fe0734398c98
--- /dev/null
+++ b/man/systemd-storage-fs@.service.xml
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+ systemd-storage-fs@.service
+ systemd
+
+
+
+ systemd-storage-fs@.service
+ 8
+
+
+
+ systemd-storage-fs@.service
+ systemd-storage-fs.socket
+ systemd-storage-fs
+ Storage provider exposing regular files and directories as storage volumes
+
+
+
+ systemd-storage-fs@.service
+ systemd-storage-fs.socket
+
+
+
+ Description
+
+ systemd-storage-fs@.service is a service that implements the
+ io.systemd.StorageProvider Varlink
+ interface, exposing regular files and directories in /var/lib/storage/*.volume (if
+ used in system mode) or $XDG_STATE_HOME/storage (when used in user mode) as storage
+ volumes. Acquired volumes are returned to the caller as file descriptors. Unlike
+ systemd-storage-block@.service8,
+ this implementation also supports creating new volumes on demand from a small set of built-in
+ templates.
+
+ The service is socket-activated via systemd-storage-fs.socket. In system mode
+ it listens on the AF_UNIX socket /run/systemd/io.systemd.StorageProvider/fs, in user
+ mode on $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/fs. See
+ storagectl1 for an
+ enumeration tool.
+
+ See also
+ systemd-storage-block@.service8
+ for a complementary implementation that exposes local block devices as storage volumes.
+
+
+
+ Volumes
+
+ Volumes are stored below the storage directory:
+
+
+ /var/lib/storage/ when run in system mode.
+
+ $XDG_STATE_HOME/storage/ (typically
+ ~/.local/state/storage/) when run in user mode.
+
+
+ Each volume on disk is stored as a directory entry with a .volume suffix in
+ the storage directory. Entries which are regular files are exposed as volumes of type
+ reg; entries which are directories are exposed as volumes of type
+ dir. Moreover, block device nodes may be symlinked (or bind mounted) into the
+ directory, which are then exposed as volumes of type blk.
+
+ For directory volumes, the root of the file system passed to clients is placed in a subdirectory
+ root/ of the NAME.volume directory. The former (and all inodes
+ below it) must be owned by the foreign UID range, the latter by the host's root.
+
+ When acquiring a volume, symlinks are followed.
+
+ An administrator is permitted to freely manipulate the volume hierarchy directly as long as the
+ rules described above are followed. In particular, it's permitted to copy, mount or symlink arbitrary
+ external resources (regardless if directory, regular file or block) into the volume directory, so that
+ they are exposed as additional volumes.
+
+
+
+ Templates
+
+ The provider supports creating new volumes automatically when they are acquired. The caller may
+ select a template that determines configuration details of the volume to create. The
+ following built-in templates are available:
+
+
+
+ sparse-file
+
+ Creates a volume backed by a sparsely populated regular file. This is the default
+ template when creating a regular file volume. (Volume type is reg.)
+
+
+
+
+
+ allocated-file
+
+ Creates a volume backed by a fully allocated regular file. (Volume type is
+ reg.)
+
+
+
+
+
+ directory
+
+ Creates a volume backed by a regular directory. (Volume type is
+ dir.)
+
+
+
+
+
+ subvolume
+
+ Creates a btrfs subvolume as backing inode (falling back to a regular directory if
+ the storage directory is not on btrfs). This is the default template when creating a directory
+ volume. (Volume type is dir.)
+
+
+
+
+
+
+
+ Options
+
+ The following command-line options are understood:
+
+
+
+
+
+ Operate in system mode. Volumes are stored below
+ /var/lib/storage/. This is the default when invoked from
+ systemd-storage-fs@.service in the system manager.
+
+
+
+
+
+
+
+ Operate in user mode. Volumes are stored below
+ $XDG_STATE_HOME/storage/. This is the default when invoked from
+ systemd-storage-fs@.service in the user manager.
+
+
+
+
+
+
+
+
+
+
+ Files
+
+
+
+ /var/lib/storage/
+ $XDG_STATE_HOME/storage/
+
+ The storage directory used to back the system mode and user mode service
+ instances respectively. Each volume is stored as an entry with a
+ .volume suffix below this directory.
+
+
+
+
+
+ /run/systemd/io.systemd.StorageProvider/fs
+ $XDG_RUNTIME_DIR/systemd/io.systemd.StorageProvider/fs
+
+ AF_UNIX sockets the service listens on, in system and user mode
+ respectively. These are the canonical locations for the fs storage
+ provider, and are enumerated by storagectl providers.
+
+
+
+
+
+
+
+ See Also
+
+ systemd1
+ storagectl1
+ systemd-storage-block@.service8
+
+
+
+
diff --git a/man/systemd-stub.xml b/man/systemd-stub.xml
index bf23c900d026c..95f62ca66b56a 100644
--- a/man/systemd-stub.xml
+++ b/man/systemd-stub.xml
@@ -291,6 +291,14 @@
by systemd-creds encrypt -T (see
systemd-creds1 for
details); in case of the system extension images by using signed Verity images.
+
+ Note that earlier components of the boot process might register additional initrds, and thus
+ additional "companion" resources such as system extensions, configuration extensions and credentials for
+ consumption by the kernel and OS eventually booted. For example,
+ systemd-boot7 does
+ this for resources configured in UAPI.1 Type #1 extra
+ lines. systemd-stub will combine any resources provided that way with the companion
+ file resources it acquires itself.
diff --git a/meson.build b/meson.build
index 4f1a791bc7651..8c979038dc749 100644
--- a/meson.build
+++ b/meson.build
@@ -1019,6 +1019,13 @@ librt = cc.find_library('rt')
libm = cc.find_library('m')
libdl = cc.find_library('dl')
+if get_option('libc') == 'musl'
+ libucontext = dependency('libucontext', required : true)
+else
+ libucontext = dependency('libucontext', required : get_option('libucontext'))
+endif
+conf.set10('HAVE_LIBUCONTEXT', libucontext.found())
+
# On some distributions that use musl (e.g. Alpine), libintl.h may be provided by gettext rather than musl.
# In that case, we need to explicitly link with libintl.so.
if cc.has_function('dgettext',
@@ -1797,6 +1804,7 @@ libsystemd_includes = [basic_includes, include_directories(
'src/libsystemd/sd-common',
'src/libsystemd/sd-device',
'src/libsystemd/sd-event',
+ 'src/libsystemd/sd-future',
'src/libsystemd/sd-hwdb',
'src/libsystemd/sd-id128',
'src/libsystemd/sd-journal',
@@ -1838,6 +1846,7 @@ libsystemd = shared_library(
libbasic_static],
link_whole : [libsystemd_static],
dependencies : [librt,
+ libucontext,
threads,
userspace],
link_depends : libsystemd_sym,
@@ -1863,6 +1872,7 @@ if static_libsystemd != 'false'
liblz4_cflags,
libm,
librt,
+ libucontext,
libxz_cflags,
libzstd_cflags,
threads,
@@ -2139,6 +2149,7 @@ subdir('src/socket-activate')
subdir('src/socket-proxy')
subdir('src/ssh-generator')
subdir('src/stdio-bridge')
+subdir('src/storage')
subdir('src/storagetm')
subdir('src/sulogin-shell')
subdir('src/sysctl')
@@ -2873,6 +2884,7 @@ foreach tuple : [
['libfdisk'],
['libfido2'],
['libidn2'],
+ ['libucontext'],
['microhttpd'],
['openssl'],
['p11kit'],
diff --git a/meson_options.txt b/meson_options.txt
index d61afac519d84..a5ea35031814b 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -493,6 +493,8 @@ option('libarchive', type : 'feature',
description : 'libarchive support')
option('libmount', type : 'feature',
description : 'libmount support')
+option('libucontext', type : 'feature', value : 'disabled',
+ description : 'libucontext support')
option('bootloader', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' },
description : 'sd-boot/stub and userspace tools')
diff --git a/shell-completion/bash/meson.build b/shell-completion/bash/meson.build
index 154910979ea56..b0e56608e8f37 100644
--- a/shell-completion/bash/meson.build
+++ b/shell-completion/bash/meson.build
@@ -36,6 +36,7 @@ foreach item : [
['portablectl', 'ENABLE_PORTABLED'],
['resolvectl', 'ENABLE_RESOLVE'],
['run0', ''],
+ ['storagectl', ''],
['systemd-analyze', ''],
['systemd-cat', ''],
['systemd-cgls', ''],
diff --git a/shell-completion/bash/storagectl b/shell-completion/bash/storagectl
new file mode 100644
index 0000000000000..5aefc30ed162d
--- /dev/null
+++ b/shell-completion/bash/storagectl
@@ -0,0 +1,74 @@
+# shellcheck shell=bash
+# storagectl(1) completion -*- shell-script -*-
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# systemd is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with systemd; If not, see .
+
+__contains_word () {
+ local w word=$1; shift
+ for w in "$@"; do
+ [[ $w = "$word" ]] && return
+ done
+}
+
+_storagectl() {
+ local i verb comps
+ local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
+
+ local -A OPTS=(
+ [STANDALONE]='-h --help --version --no-pager --no-legend --no-ask-password
+ --system --user'
+ [ARG]='--json'
+ )
+
+ if __contains_word "$prev" ${OPTS[ARG]}; then
+ case $prev in
+ --json)
+ comps=$( storagectl --json=help 2>/dev/null )
+ ;;
+ esac
+ COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+ return 0
+ fi
+
+ if [[ "$cur" = -* ]]; then
+ COMPREPLY=( $(compgen -W '${OPTS[*]}' -- "$cur") )
+ return 0
+ fi
+
+ local -A VERBS=(
+ [STANDALONE]='volumes templates providers help'
+ )
+
+ for ((i=0; i < COMP_CWORD; i++)); do
+ if __contains_word "${COMP_WORDS[i]}" ${VERBS[*]} &&
+ ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[ARG]}; then
+ verb=${COMP_WORDS[i]}
+ break
+ fi
+ done
+
+ if [[ -z ${verb-} ]]; then
+ comps=${VERBS[*]}
+ elif __contains_word "$verb" ${VERBS[STANDALONE]}; then
+ comps=''
+ fi
+
+ COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
+ return 0
+}
+
+complete -F _storagectl storagectl
diff --git a/shell-completion/zsh/_storagectl b/shell-completion/zsh/_storagectl
new file mode 100644
index 0000000000000..b2fdf595a1076
--- /dev/null
+++ b/shell-completion/zsh/_storagectl
@@ -0,0 +1,35 @@
+#compdef storagectl
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+(( $+functions[_storagectl_commands] )) || _storagectl_commands()
+{
+ local -a _storagectl_cmds
+ _storagectl_cmds=(
+ "volumes:List storage volumes"
+ "templates:List storage volume templates"
+ "providers:List storage providers"
+ "help:Prints a short help text and exits"
+ )
+ if (( CURRENT == 1 )); then
+ _describe -t commands 'storagectl command' _storagectl_cmds
+ else
+ local curcontext="$curcontext"
+ cmd="${${_storagectl_cmds[(r)$words[1]:*]%%:*}}"
+ if (( $+functions[_storagectl_$cmd] )); then
+ _storagectl_$cmd
+ else
+ _message "no more options"
+ fi
+ fi
+}
+
+_arguments \
+ '(- *)'{-h,--help}'[Prints a short help text and exits.]' \
+ '(- *)--version[Prints a short version string and exits.]' \
+ '--no-pager[Do not pipe output into a pager]' \
+ '--no-legend[Do not show the headers and footers]' \
+ '--no-ask-password[Do not query the user for authentication]' \
+ '--json=[Show output as JSON]:mode:(pretty short off help)' \
+ '--system[Operate in system mode]' \
+ '--user[Operate in user mode]' \
+ '*::storagectl command:_storagectl_commands'
diff --git a/shell-completion/zsh/meson.build b/shell-completion/zsh/meson.build
index b1bff151e41a3..6cc8a2d57f83e 100644
--- a/shell-completion/zsh/meson.build
+++ b/shell-completion/zsh/meson.build
@@ -33,6 +33,7 @@ foreach item : [
['_sd_machines', 'ENABLE_MACHINED'],
['_sd_outputmodes', ''],
['_sd_unit_files', ''],
+ ['_storagectl', ''],
['_systemd', ''],
['_systemd-analyze', ''],
['_systemd-delta', ''],
diff --git a/src/ac-power/ac-power.c b/src/ac-power/ac-power.c
index 530ee82ff0665..2a9c517329321 100644
--- a/src/ac-power/ac-power.c
+++ b/src/ac-power/ac-power.c
@@ -43,7 +43,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/ask-password/ask-password.c b/src/ask-password/ask-password.c
index 129fbf4d7e753..6a1abf5f999a1 100644
--- a/src/ask-password/ask-password.c
+++ b/src/ask-password/ask-password.c
@@ -75,7 +75,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/basic/basic-forward.h b/src/basic/basic-forward.h
index 396056a8e55eb..17fa50fa19ae8 100644
--- a/src/basic/basic-forward.h
+++ b/src/basic/basic-forward.h
@@ -110,10 +110,13 @@ typedef enum UnitNameMangle UnitNameMangle;
typedef enum UnitType UnitType;
typedef enum WaitFlags WaitFlags;
+typedef struct Fiber Fiber;
+typedef struct FiberOps FiberOps;
typedef struct Hashmap Hashmap;
typedef struct HashmapBase HashmapBase;
typedef struct IteratedCache IteratedCache;
typedef struct Iterator Iterator;
+typedef struct LogContext LogContext;
typedef struct OrderedHashmap OrderedHashmap;
typedef struct OrderedSet OrderedSet;
typedef struct Set Set;
diff --git a/src/basic/fiber-def.c b/src/basic/fiber-def.c
new file mode 100644
index 0000000000000..257b57c50b410
--- /dev/null
+++ b/src/basic/fiber-def.c
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "fiber-def.h"
+#include "string-table.h"
+
+static thread_local Fiber *current_fiber = NULL;
+
+static const char* const fiber_state_table[_FIBER_STATE_MAX] = {
+ [FIBER_STATE_INITIAL] = "initial",
+ [FIBER_STATE_READY] = "ready",
+ [FIBER_STATE_SUSPENDED] = "suspended",
+ [FIBER_STATE_COMPLETED] = "completed",
+ [FIBER_STATE_CANCELLED] = "cancelled",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(fiber_state, FiberState);
+
+Fiber* fiber_get_current(void) {
+ return current_fiber;
+}
+
+void fiber_set_current(Fiber *f) {
+ current_fiber = f;
+}
diff --git a/src/basic/fiber-def.h b/src/basic/fiber-def.h
new file mode 100644
index 0000000000000..5257cd2543f24
--- /dev/null
+++ b/src/basic/fiber-def.h
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include
+
+#include "basic-forward.h"
+#include "list.h"
+#include "macro-fundamental.h"
+
+/* We need to be able to get the current fiber and access its log context and log prefix from log.c and
+ * log-context.c, so the definition of Fiber lives here instead of in libsystemd. */
+
+typedef struct sd_event sd_event;
+typedef struct sd_event_source sd_event_source;
+typedef struct sd_future sd_future;
+typedef struct sd_promise sd_promise;
+typedef int (*sd_fiber_func_t)(void *userdata);
+typedef void (*sd_fiber_destroy_t)(void *userdata);
+
+typedef enum FiberState {
+ FIBER_STATE_INITIAL,
+ FIBER_STATE_READY,
+ FIBER_STATE_SUSPENDED,
+ FIBER_STATE_CANCELLED,
+ FIBER_STATE_COMPLETED,
+ _FIBER_STATE_MAX,
+ _FIBER_STATE_INVALID = -EINVAL,
+} FiberState;
+
+/* Hooks installed on a fiber so that functions in src/basic can transparently defer to the suspending
+ * variants in sd-future when invoked from a running fiber. Populated by sd_fiber_new() with pointers to the
+ * implementations in fiber-ops.c. */
+typedef struct FiberOps {
+ int (*ppoll)(struct pollfd *fds, size_t n_fds, usec_t timeout);
+ ssize_t (*read)(int fd, void *buf, size_t count);
+ ssize_t (*write)(int fd, const void *buf, size_t count);
+ sd_future* (*timeout)(uint64_t timeout);
+ sd_future* (*timeout_done)(sd_future *timer);
+} FiberOps;
+
+typedef struct Fiber {
+ sd_promise *promise; /* must be first: set by sd_future_new() via the impl convention */
+
+ void *stack;
+ size_t stack_size;
+ ucontext_t context;
+ ucontext_t resume_context; /* Where to jump back to when the fiber yields or completes. */
+
+ FiberState state;
+ int result; /* Either resume error code or final return value */
+
+ sd_future *floating; /* Self-ref held while the fiber is floating; dropped on resolve. */
+
+ sd_event *event;
+ sd_event_source *defer_event_source;
+ sd_event_source *exit_event_source;
+
+ char *name;
+ int64_t priority;
+ sd_fiber_func_t func;
+ void *userdata;
+ sd_fiber_destroy_t destroy;
+
+ const FiberOps *ops;
+
+ LIST_HEAD(LogContext, log_context);
+ size_t log_context_num_fields;
+ const char *log_prefix;
+
+#if HAVE_VALGRIND_VALGRIND_H
+ unsigned stack_id;
+#endif
+} Fiber;
+
+DECLARE_STRING_TABLE_LOOKUP(fiber_state, FiberState);
+
+Fiber* fiber_get_current(void);
+void fiber_set_current(Fiber *f);
+
+typedef struct FiberOpsRestore {
+ Fiber *fiber;
+ const FiberOps *ops;
+} FiberOpsRestore;
+
+static inline void fiber_ops_restore(FiberOpsRestore *s) {
+ if (s->fiber)
+ s->fiber->ops = s->ops;
+}
+
+/* Forward the call to the fiber op (if we're on a fiber with ops installed), otherwise fall through to the
+ * caller's fallback body. Clears and restores ops around the call so the op's implementation can call back
+ * into the non-redirected basic functions without infinite recursion. The restore runs via cleanup, so ops
+ * is reinstated regardless of how the scope exits. */
+#define FIBER_OPS_FORWARD(func, ...) \
+ do { \
+ Fiber *_f = fiber_get_current(); \
+ if (_f && _f->ops && _f->ops->func) { \
+ _unused_ _cleanup_(fiber_ops_restore) FiberOpsRestore _r = { .fiber = _f, .ops = _f->ops }; \
+ const FiberOps *_o = _f->ops; \
+ _f->ops = NULL; \
+ return _o->func(__VA_ARGS__); \
+ } \
+ } while (0)
+
+/* Mirror of SD_FIBER_TIMEOUT() for code under src/basic that doesn't include sd-future.h: dispatches
+ * through FiberOps so the actual sd_fiber_timeout() implementation lives in libsystemd. */
+static inline sd_future* fiber_ops_timeout(uint64_t timeout) {
+ Fiber *f = fiber_get_current();
+ if (f && f->ops)
+ return f->ops->timeout(timeout);
+ return NULL;
+}
+
+static inline void fiber_ops_timeout_done(sd_future **timer) {
+ if (!*timer)
+ return;
+
+ Fiber *f = ASSERT_PTR(fiber_get_current());
+ *timer = f->ops->timeout_done(*timer);
+}
+
+#define FIBER_OPS_TIMEOUT(timeout) _FIBER_OPS_TIMEOUT(UNIQ, (timeout))
+#define _FIBER_OPS_TIMEOUT(uniq, timeout) \
+ _unused_ _cleanup_(fiber_ops_timeout_done) sd_future *UNIQ_T(_fot_, uniq) = fiber_ops_timeout(timeout)
diff --git a/src/basic/io-util.c b/src/basic/io-util.c
index 103aa2a7cde03..0a73924431e98 100644
--- a/src/basic/io-util.c
+++ b/src/basic/io-util.c
@@ -1,5 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#include
#include
#include
#include
@@ -7,6 +8,7 @@
#include
#include "errno-util.h"
+#include "fiber-def.h"
#include "io-util.h"
#include "time-util.h"
@@ -51,6 +53,7 @@ int flush_fd(int fd) {
ssize_t loop_read(int fd, void *buf, size_t nbytes, bool do_poll) {
uint8_t *p = ASSERT_PTR(buf);
+ Fiber *f = fiber_get_current();
ssize_t n = 0;
assert(fd >= 0);
@@ -60,25 +63,44 @@ ssize_t loop_read(int fd, void *buf, size_t nbytes, bool do_poll) {
if (nbytes > (size_t) SSIZE_MAX)
return -EINVAL;
+ /* do_poll == false means "don't wait, return what we have if EAGAIN". If the fd is already
+ * non-blocking, read() can't block the thread, so the non-fiber path satisfies that semantic
+ * correctly even from a fiber. Only use the fiber path when the fd is blocking (where read()
+ * would otherwise block the entire event loop). */
+ int flags = 0;
+ if (f && f->ops && !do_poll) {
+ flags = fcntl(fd, F_GETFL);
+ if (flags < 0)
+ return -errno;
+ }
+
do {
ssize_t k;
- k = read(fd, p, nbytes);
- if (k < 0) {
- if (errno == EINTR)
- continue;
-
- if (errno == EAGAIN && do_poll) {
-
- /* We knowingly ignore any return value here,
- * and expect that any error/EOF is reported
- * via read() */
-
- (void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
- continue;
+ if (f && f->ops && !FLAGS_SET(flags, O_NONBLOCK)) {
+ /* On a fiber the read op suspends on EAGAIN until data is available, so we don't
+ * need a separate poll step or the do_poll knob. */
+ k = f->ops->read(fd, p, nbytes);
+ if (k < 0)
+ return n > 0 ? n : k;
+ } else {
+ k = read(fd, p, nbytes);
+ if (k < 0) {
+ if (errno == EINTR)
+ continue;
+
+ if (errno == EAGAIN && do_poll) {
+
+ /* We knowingly ignore any return value here,
+ * and expect that any error/EOF is reported
+ * via read() */
+
+ (void) fd_wait_for_event(fd, POLLIN, USEC_INFINITY);
+ continue;
+ }
+
+ return n > 0 ? n : -errno;
}
-
- return n > 0 ? n : -errno;
}
if (k == 0)
@@ -128,6 +150,38 @@ int loop_write_full(int fd, const void *buf, size_t nbytes, usec_t timeout) {
p = buf;
}
+ Fiber *f = fiber_get_current();
+
+ /* timeout == 0 means "don't wait, return -EAGAIN if not ready". If the fd is already
+ * non-blocking, write() can't block the thread, so the non-fiber path satisfies that
+ * semantic correctly even from a fiber. Only use the fiber path when the fd is blocking
+ * (where write() would otherwise block the entire event loop). */
+ int flags = 0;
+ if (f && f->ops && timeout == 0) {
+ flags = fcntl(fd, F_GETFL);
+ if (flags < 0)
+ return -errno;
+ }
+
+ if (f && f->ops && !FLAGS_SET(flags, O_NONBLOCK)) {
+ /* On a fiber the write op suspends on EAGAIN until the fd is writable; honor the
+ * caller's timeout via a deadline scope. */
+ FIBER_OPS_TIMEOUT(timestamp_is_set(timeout) ? timeout : USEC_INFINITY);
+
+ while (nbytes > 0) {
+ ssize_t k = f->ops->write(fd, p, nbytes);
+ if (k < 0)
+ return (int) k;
+ if (_unlikely_(nbytes > 0 && k == 0)) /* Can't really happen */
+ return -EIO;
+
+ p += k;
+ nbytes -= k;
+ }
+
+ return 0;
+ }
+
/* When timeout is 0 or USEC_INFINITY this is not used. But we initialize it to a sensible value. */
end = timestamp_is_set(timeout) ? usec_add(now(CLOCK_MONOTONIC), timeout) : USEC_INFINITY;
@@ -192,6 +246,9 @@ int ppoll_usec_full(struct pollfd *fds, size_t n_fds, usec_t timeout, const sigs
int r;
assert(fds || n_fds == 0);
+ assert(!fiber_get_current() || !ss);
+
+ FIBER_OPS_FORWARD(ppoll, fds, n_fds, timeout);
/* This is a wrapper around ppoll() that does primarily two things:
*
diff --git a/src/basic/log-context.c b/src/basic/log-context.c
index a05b4b1980e6b..633500d3894e9 100644
--- a/src/basic/log-context.c
+++ b/src/basic/log-context.c
@@ -4,6 +4,7 @@
#include "alloc-util.h"
#include "env-util.h"
+#include "fiber-def.h"
#include "iovec-util.h"
#include "log.h"
#include "log-context.h"
@@ -35,10 +36,17 @@ bool log_context_enabled(void) {
static LogContext* log_context_attach(LogContext *c) {
assert(c);
- _log_context_num_fields += strv_length(c->fields);
- _log_context_num_fields += c->n_input_iovec;
- _log_context_num_fields += !!c->key;
+ size_t add = strv_length(c->fields);
+ add += c->n_input_iovec;
+ add += !!c->key;
+ Fiber *f = fiber_get_current();
+ if (f) {
+ f->log_context_num_fields += add;
+ return LIST_PREPEND(ll, f->log_context, c);
+ }
+
+ _log_context_num_fields += add;
return LIST_PREPEND(ll, _log_context, c);
}
@@ -46,11 +54,20 @@ static LogContext* log_context_detach(LogContext *c) {
if (!c)
return NULL;
- assert(_log_context_num_fields >= strv_length(c->fields) + c->n_input_iovec +!!c->key);
- _log_context_num_fields -= strv_length(c->fields);
- _log_context_num_fields -= c->n_input_iovec;
- _log_context_num_fields -= !!c->key;
+ size_t sub = strv_length(c->fields);
+ sub += c->n_input_iovec;
+ sub += !!c->key;
+
+ Fiber *f = fiber_get_current();
+ if (f) {
+ assert(f->log_context_num_fields >= sub);
+ f->log_context_num_fields -= sub;
+ LIST_REMOVE(ll, f->log_context, c);
+ return NULL;
+ }
+ assert(_log_context_num_fields >= sub);
+ _log_context_num_fields -= sub;
LIST_REMOVE(ll, _log_context, c);
return NULL;
}
@@ -62,7 +79,7 @@ LogContext* log_context_new(const char *key, const char *value) {
if (!value)
return NULL;
- LIST_FOREACH(ll, i, _log_context)
+ LIST_FOREACH(ll, i, log_context_head())
if (i->key == key && i->value == value)
return log_context_ref(i);
@@ -83,7 +100,7 @@ LogContext* log_context_new_strv(char **fields, bool owned) {
if (!fields)
return NULL;
- LIST_FOREACH(ll, i, _log_context)
+ LIST_FOREACH(ll, i, log_context_head())
if (i->fields == fields) {
assert(!owned);
return log_context_ref(i);
@@ -106,7 +123,7 @@ LogContext* log_context_new_iov(struct iovec *input_iovec, size_t n_input_iovec,
if (!input_iovec || n_input_iovec == 0)
return NULL;
- LIST_FOREACH(ll, i, _log_context)
+ LIST_FOREACH(ll, i, log_context_head())
if (i->input_iovec == input_iovec && i->n_input_iovec == n_input_iovec) {
assert(!owned);
return log_context_ref(i);
@@ -161,20 +178,22 @@ LogContext* log_context_new_iov_consume(struct iovec *input_iovec, size_t n_inpu
}
LogContext* log_context_head(void) {
- return _log_context;
+ Fiber *f = fiber_get_current();
+ return f ? f->log_context : _log_context;
}
size_t log_context_num_contexts(void) {
size_t n = 0;
- LIST_FOREACH(ll, c, _log_context)
+ LIST_FOREACH(ll, c, log_context_head())
n++;
return n;
}
size_t log_context_num_fields(void) {
- return _log_context_num_fields;
+ Fiber *f = fiber_get_current();
+ return f ? f->log_context_num_fields : _log_context_num_fields;
}
void _reset_log_level(int *saved_log_level) {
diff --git a/src/basic/log.c b/src/basic/log.c
index d8b441bfadf21..2e926bf373bc3 100644
--- a/src/basic/log.c
+++ b/src/basic/log.c
@@ -16,6 +16,7 @@
#include "errno-util.h"
#include "extract-word.h"
#include "fd-util.h"
+#include "fiber-def.h"
#include "format-util.h"
#include "iovec-util.h"
#include "list.h"
@@ -71,7 +72,7 @@ static bool always_reopen_console = false;
static bool open_when_needed = false;
static bool prohibit_ipc = false;
-static thread_local const char *log_prefix = NULL;
+static thread_local const char *_log_prefix = NULL;
#if LOG_MESSAGE_VERIFICATION || defined(__COVERITY__)
bool _log_message_dummy = false; /* Always false */
@@ -87,6 +88,25 @@ bool _log_message_dummy = false; /* Always false */
} \
} while (false)
+static const char* log_prefix(void) {
+ Fiber *f = fiber_get_current();
+ return f ? f->log_prefix : _log_prefix;
+}
+
+const char* _log_set_prefix(const char *prefix, bool force) {
+ const char *old = log_prefix();
+
+ if (prefix || force) {
+ Fiber *f = fiber_get_current();
+ if (f)
+ f->log_prefix = prefix;
+ else
+ _log_prefix = prefix;
+ }
+
+ return old;
+}
+
static void log_close_console(void) {
/* See comment in log_close_journal() */
(void) safe_close_above_stdio(TAKE_FD(console_fd));
@@ -462,8 +482,8 @@ static int write_to_console(
if (on)
iovec[n++] = IOVEC_MAKE_STRING(on);
- if (log_prefix) {
- iovec[n++] = IOVEC_MAKE_STRING(log_prefix);
+ if (log_prefix()) {
+ iovec[n++] = IOVEC_MAKE_STRING(log_prefix());
iovec[n++] = IOVEC_MAKE_STRING(": ");
}
iovec[n++] = IOVEC_MAKE_STRING(buffer);
@@ -534,8 +554,8 @@ static int write_to_syslog(
IOVEC_MAKE_STRING(header_time),
IOVEC_MAKE_STRING(program_invocation_short_name),
IOVEC_MAKE_STRING(header_pid),
- IOVEC_MAKE_STRING(strempty(log_prefix)),
- IOVEC_MAKE_STRING(log_prefix ? ": " : ""),
+ IOVEC_MAKE_STRING(strempty(log_prefix())),
+ IOVEC_MAKE_STRING(log_prefix() ? ": " : ""),
IOVEC_MAKE_STRING(buffer),
};
const struct msghdr msghdr = {
@@ -604,8 +624,8 @@ static int write_to_kmsg(
IOVEC_MAKE_STRING(header_priority),
IOVEC_MAKE_STRING(program_invocation_short_name),
IOVEC_MAKE_STRING(header_pid),
- IOVEC_MAKE_STRING(strempty(log_prefix)),
- IOVEC_MAKE_STRING(log_prefix ? ": " : ""),
+ IOVEC_MAKE_STRING(strempty(log_prefix())),
+ IOVEC_MAKE_STRING(log_prefix() ? ": " : ""),
IOVEC_MAKE_STRING(buffer),
IOVEC_MAKE_STRING("\n"),
};
@@ -727,8 +747,8 @@ static int write_to_journal(
iovec[n++] = IOVEC_MAKE_STRING(header);
iovec[n++] = IOVEC_MAKE_STRING("MESSAGE=");
- if (log_prefix) {
- iovec[n++] = IOVEC_MAKE_STRING(log_prefix);
+ if (log_prefix()) {
+ iovec[n++] = IOVEC_MAKE_STRING(log_prefix());
iovec[n++] = IOVEC_MAKE_STRING(": ");
}
iovec[n++] = IOVEC_MAKE_STRING(buffer);
@@ -1719,12 +1739,3 @@ void log_setup(void) {
if (log_on_console() && show_color < 0)
log_show_color(true);
}
-
-const char* _log_set_prefix(const char *prefix, bool force) {
- const char *old = log_prefix;
-
- if (prefix || force)
- log_prefix = prefix;
-
- return old;
-}
diff --git a/src/basic/meson.build b/src/basic/meson.build
index f847b175b61f0..7aaf7f6d481f3 100644
--- a/src/basic/meson.build
+++ b/src/basic/meson.build
@@ -36,6 +36,7 @@ basic_sources = files(
'ether-addr-util.c',
'extract-word.c',
'fd-util.c',
+ 'fiber-def.c',
'fileio.c',
'filesystems.c',
'format-ifname.c',
diff --git a/src/basic/pidref.c b/src/basic/pidref.c
index 10ff9a63b12bc..b5af6ffd65a1c 100644
--- a/src/basic/pidref.c
+++ b/src/basic/pidref.c
@@ -7,6 +7,7 @@
#include "alloc-util.h"
#include "errno-util.h"
#include "fd-util.h"
+#include "fiber-def.h"
#include "format-util.h"
#include "hash-funcs.h"
#include "io-util.h"
@@ -466,16 +467,28 @@ int pidref_wait_for_terminate_full(PidRef *pidref, usec_t timeout, siginfo_t *re
if (pidref->pid == 1 || pidref_is_self(pidref))
return -ECHILD;
- if (timeout != USEC_INFINITY && pidref->fd < 0)
+ if (pidref->fd < 0 && (timeout != USEC_INFINITY || fiber_get_current()))
return -ENOMEDIUM;
usec_t ts = timeout == USEC_INFINITY ? USEC_INFINITY : usec_add(now(CLOCK_MONOTONIC), timeout);
+ /* Poll the pidfd before waitid() if either there's a finite timeout (so we can honor it) or
+ * we're on a fiber (so fd_wait_for_event() can suspend us instead of blocking the event loop
+ * inside waitid()). Otherwise let waitid() block directly. The precondition above guarantees
+ * pidref->fd >= 0 in both cases. */
+ bool poll_first = ts != USEC_INFINITY || fiber_get_current();
+
for (;;) {
- if (ts != USEC_INFINITY) {
- usec_t left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
- if (left == 0)
- return -ETIMEDOUT;
+ if (poll_first) {
+ usec_t left;
+
+ if (ts == USEC_INFINITY)
+ left = USEC_INFINITY;
+ else {
+ left = usec_sub_unsigned(ts, now(CLOCK_MONOTONIC));
+ if (left == 0)
+ return -ETIMEDOUT;
+ }
r = fd_wait_for_event(pidref->fd, POLLIN, left);
if (r == 0)
diff --git a/src/battery-check/battery-check.c b/src/battery-check/battery-check.c
index 3e957d9fa71df..13dc8960f2efb 100644
--- a/src/battery-check/battery-check.c
+++ b/src/battery-check/battery-check.c
@@ -83,7 +83,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/binfmt/binfmt.c b/src/binfmt/binfmt.c
index ed37fba276afb..4e24b35db744b 100644
--- a/src/binfmt/binfmt.c
+++ b/src/binfmt/binfmt.c
@@ -141,7 +141,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/bless-boot/bless-boot.c b/src/bless-boot/bless-boot.c
index 8d2fe21a11f66..e0afb3611c278 100644
--- a/src/bless-boot/bless-boot.c
+++ b/src/bless-boot/bless-boot.c
@@ -81,7 +81,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/bless-boot/boot-check-no-failures.c b/src/bless-boot/boot-check-no-failures.c
index 37b0f7fd6d2b2..9fa42a7ed6620 100644
--- a/src/bless-boot/boot-check-no-failures.c
+++ b/src/bless-boot/boot-check-no-failures.c
@@ -46,7 +46,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/boot/boot.c b/src/boot/boot.c
index df5ce31fa3a3f..a2a1becc9aaa0 100644
--- a/src/boot/boot.c
+++ b/src/boot/boot.c
@@ -3,6 +3,7 @@
#include "bcd.h"
#include "bootspec-fundamental.h"
#include "console.h"
+#include "cpio.h"
#include "device-path-util.h"
#include "devicetree.h"
#include "drivers.h"
@@ -38,6 +39,9 @@
#include "version.h"
#include "vmm.h"
+/* Safety margin, refuse larger extra files (this is not load bearing, only a safety net for robustness reasons). */
+#define EXTRA_SIZE_MAX (1024U * 1024U * 1536U)
+
/* Magic string for recognizing our own binaries */
#define SD_MAGIC "#### LoaderInfo: systemd-boot " GIT_VERSION " ####"
DECLARE_NOALLOC_SECTION(".sdmagic", SD_MAGIC);
@@ -120,6 +124,7 @@ typedef struct BootEntry {
char16_t *options;
bool options_implied; /* If true, these options are implied if we invoke the PE binary without any parameters (as in: UKI). If false we must specify these options explicitly. */
char16_t **initrd;
+ char16_t **extras;
char16_t key;
EFI_STATUS (*call)(const struct BootEntry *entry, EFI_FILE *root_dir, EFI_HANDLE parent_image);
int tries_done;
@@ -424,6 +429,8 @@ static void print_status(Config *config, char16_t *loaded_image_path) {
printf(" url: %ls\n", entry->url);
STRV_FOREACH(initrd, entry->initrd)
printf(" initrd: %ls\n", *initrd);
+ STRV_FOREACH(extra, entry->extras)
+ printf(" extra: %ls\n", *extra);
if (entry->devicetree)
printf(" devicetree: %ls\n", entry->devicetree);
if (entry->options)
@@ -1047,6 +1054,7 @@ static BootEntry* boot_entry_free(BootEntry *entry) {
free(entry->devicetree);
free(entry->options);
strv_free(entry->initrd);
+ strv_free(entry->extras);
free(entry->directory);
free(entry->current_name);
free(entry->next_name);
@@ -1363,7 +1371,7 @@ static void boot_entry_add_type1(
_cleanup_(boot_entry_freep) BootEntry *entry = NULL;
char *line;
- size_t pos = 0, n_initrd = 0;
+ size_t pos = 0, n_initrd = 0, n_extras = 0;
char *key, *value;
EFI_STATUS err;
@@ -1492,6 +1500,14 @@ static void boot_entry_add_type1(
entry->initrd[n_initrd++] = xstr8_to_path(value);
entry->initrd[n_initrd] = NULL;
+ } else if (streq8(key, "extra")) {
+ entry->extras = xrealloc(
+ entry->extras,
+ n_extras == 0 ? 0 : (n_extras + 1) * sizeof(uint16_t *),
+ (n_extras + 2) * sizeof(uint16_t *));
+ entry->extras[n_extras++] = xstr8_to_path(value);
+ entry->extras[n_extras] = NULL;
+
} else if (streq8(key, "options")) {
_cleanup_free_ char16_t *new = NULL;
@@ -2569,7 +2585,9 @@ static EFI_STATUS initrd_prepare(
assert(ret_initrd_pages);
assert(ret_initrd_size);
- if (entry->type != LOADER_LINUX || strv_isempty(entry->initrd)) {
+ assert(entry->type == LOADER_LINUX);
+
+ if (strv_isempty(entry->initrd)) {
*ret_options = NULL;
*ret_initrd_pages = (Pages) {};
*ret_initrd_size = 0;
@@ -2673,6 +2691,173 @@ static EFI_STATUS initrd_prepare(
return EFI_SUCCESS;
}
+static EFI_STATUS load_extras(
+ EFI_FILE *root,
+ const BootEntry *entry,
+ Pages *ret_initrd_pages,
+ size_t *ret_initrd_size) {
+
+ EFI_STATUS err;
+
+ assert(root);
+ assert(entry);
+ assert(ret_initrd_pages);
+ assert(ret_initrd_size);
+
+ assert(IN_SET(entry->type, LOADER_UKI, LOADER_UKI_URL));
+
+ _cleanup_(iovec_done) struct iovec previous_initrd = {}, confext_initrd = {}, sysext_initrd = {}, credential_initrd = {};
+
+ const struct ExtraResourceInfo {
+ const char16_t *suffix;
+ const CpioTarget *target;
+ struct iovec *iovec;
+ const char16_t *tpm_description;
+ } table[] = {
+ { u".cred", &cpio_target_credentials, &credential_initrd, u"Entry credentials initrd" },
+ { u".sysext.raw", &cpio_target_sysext, &sysext_initrd, u"Entry system extension initrd" },
+ { u".confext.raw", &cpio_target_confext, &confext_initrd, u"Entry configuration extension initrd" },
+ };
+
+ if (strv_isempty(entry->extras))
+ goto nothing;
+
+ uint32_t inode = 1; /* inode counter, so that each item gets a new inode */
+ unsigned n = 0;
+
+ STRV_FOREACH(i, entry->extras) {
+ _cleanup_file_close_ EFI_FILE *handle = NULL;
+ err = root->Open(root, &handle, *i, EFI_FILE_MODE_READ, /* Attributes= */ 0);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to open extra file '%ls', ignoring: %m", *i);
+ continue;
+ }
+
+ _cleanup_free_ EFI_FILE_INFO *info = NULL;
+ err = get_file_info(handle, &info, /* ret_size= */ NULL);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to get information about file '%ls', ignoring: %m", *i);
+ continue;
+ }
+
+ if (FLAGS_SET(info->Attribute, EFI_FILE_DIRECTORY)) {
+ log_warning("Extra file '%ls' is a directory, ignoring.", *i);
+ continue;
+ }
+
+ if (info->FileSize == 0) {
+ log_warning("Extra file '%ls' is empty, ignoring.", *i);
+ continue;
+ }
+ if (info->FileSize > EXTRA_SIZE_MAX) {
+ log_warning("Extra file '%ls' is larger than allowed extra file size, ignoring.", *i);
+ continue;
+ }
+
+ if (!is_ascii(info->FileName)) {
+ log_warning("Extra file name '%ls' is not valid ASCII, ignoring.", *i);
+ continue;
+ }
+ if (strlen16(info->FileName) > 255) { /* Max filename size on Linux */
+ log_warning("Filename '%ls' too long, ignoring.", *i);
+ continue;
+ }
+
+ const struct ExtraResourceInfo *x = NULL;
+ FOREACH_ELEMENT(j, table)
+ if (endswith_no_case(info->FileName, j->suffix)) {
+ x = j;
+ break;
+ }
+ if (!x) {
+ log_warning("Unrecognized type of extra file '%ls', ignoring.", info->FileName);
+ continue;
+ }
+
+ _cleanup_free_ char *content = NULL;
+ size_t contentsize = 0; /* avoid false maybe-uninitialized warning */
+ err = file_handle_read(handle, /* offset= */ 0, info->FileSize, &content, &contentsize);
+ if (err != EFI_SUCCESS) {
+ log_warning_status(err, "Failed to read '%ls', ignoring: %m", *i);
+ continue;
+ }
+
+ /* Generate the leading directory inodes right before adding the first files to the
+ * archive. Otherwise the cpio archive cannot be unpacked, since the leading dirs won't
+ * exist. Note that we potentially do redundant work here: a prior iteration might already
+ * have created the prefix for us, but to simplify this we regenerate it anyway. It's very
+ * little data, and simplifies the implementation here a lot. */
+ err = pack_cpio_prefix(x->target, &inode, &x->iovec->iov_base, &x->iovec->iov_len);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to pack cpio prefix '%s': %m", x->target->directory);
+
+ err = pack_cpio_one(
+ info->FileName,
+ content, contentsize,
+ x->target,
+ &inode,
+ &x->iovec->iov_base, &x->iovec->iov_len);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to pack cpio file '%ls': %m", info->FileName);
+
+ n++;
+ }
+
+ if (n == 0) /* Nothing actually loaded */
+ goto nothing;
+
+ FOREACH_ELEMENT(x, table) {
+ if (x->iovec->iov_len <= 0)
+ continue;
+
+ err = pack_cpio_trailer(&x->iovec->iov_base, &x->iovec->iov_len);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to pack cpio trailer: %m");
+
+ err = tpm_log_ipl_event(
+ x->target->tpm_pcr,
+ POINTER_TO_PHYSICAL_ADDRESS(x->iovec->iov_base),
+ x->iovec->iov_len,
+ x->tpm_description,
+ /* ret_measured= */ NULL);
+ if (err != EFI_SUCCESS)
+ return log_error_status(
+ err,
+ "Unable to add cpio TPM measurement for PCR %u (%ls): %m",
+ x->target->tpm_pcr,
+ x->tpm_description);
+ }
+
+ /* Be nice: pick up any previously registered initrds and prepend them to what we are generating here */
+ err = initrd_read_previous(&previous_initrd);
+ if (err == EFI_NOT_FOUND)
+ log_debug_status(err, "No previous initrd installed.");
+ else if (err != EFI_SUCCESS)
+ log_warning_status(err, "Failed to read previously registered initrd, ignoring.");
+ else
+ log_debug("Successfully loaded previously installed initrd (%zu bytes).", previous_initrd.iov_len);
+
+ err = combine_initrds(
+ (const struct iovec[]) {
+ previous_initrd,
+ credential_initrd,
+ sysext_initrd,
+ confext_initrd,
+ },
+ /* n_initrds= */ 4,
+ ret_initrd_pages,
+ ret_initrd_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Failed to combine previous with extra initrds: %m");
+
+ return EFI_SUCCESS;
+
+nothing:
+ *ret_initrd_pages = (Pages) {};
+ *ret_initrd_size = 0;
+ return EFI_SUCCESS;
+}
+
static EFI_STATUS expand_path(
EFI_HANDLE parent_image,
EFI_DEVICE_PATH *path,
@@ -2821,15 +3006,11 @@ static EFI_STATUS call_image_start(
return log_error_status(err, "Error loading EFI binary %ls: %m", entry->loader);
}
- _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
_cleanup_free_ char16_t *options_initrd = NULL;
- _cleanup_pages_ Pages initrd_pages = {};
+ _cleanup_pages_ Pages initrd_pages = {}; /* Note: please keep order intact: these pages should be released after the initrd handle is released */
+ _cleanup_(cleanup_initrd) EFI_HANDLE initrd_handle = NULL;
size_t initrd_size = 0;
if (image_root) {
- err = initrd_prepare(image_root, entry, &options_initrd, &initrd_pages, &initrd_size);
- if (err != EFI_SUCCESS)
- return log_error_status(err, "Error preparing initrd: %m");
-
/* DTBs are loaded by the kernel before ExitBootServices(), and they can be used to map and
* assign arbitrary memory ranges, so skip them when secure boot is enabled as the DTB here
* is unverified. */
@@ -2839,9 +3020,35 @@ static EFI_STATUS call_image_start(
return log_error_status(err, "Error loading %ls: %m", entry->devicetree);
}
+ switch (entry->type) {
+
+ case LOADER_LINUX:
+ /* For traditional Linux we follow 'initrd' links, because that's how things worked in the good old days */
+ err = initrd_prepare(image_root, entry, &options_initrd, &initrd_pages, &initrd_size);
+ if (err != EFI_SUCCESS)
+ return log_error_status(err, "Error preparing initrd: %m");
+
+ break;
+
+ case LOADER_UKI:
+ case LOADER_UKI_URL:
+ /* For modern UKIs we'll not bother with 'initrd', but we'll instead support 'extra'
+ * for loading credentials, sysexts, and confexts. */
+
+ err = load_extras(image_root, entry, &initrd_pages, &initrd_size);
+ if (err != EFI_SUCCESS)
+ return err; /* load_extras() logs on its own */
+ break;
+
+ default:
+ ;
+ }
+
err = initrd_register(&IOVEC_MAKE(PHYSICAL_ADDRESS_TO_POINTER(initrd_pages.addr), initrd_size), &initrd_handle);
if (err != EFI_SUCCESS)
return log_error_status(err, "Error registering initrd: %m");
+
+ /* NB: the initrd pages remain in our possession, we will free them if executing the image fails below */
}
EFI_LOADED_IMAGE_PROTOCOL *loaded_image;
diff --git a/src/boot/cpio.c b/src/boot/cpio.c
index 81792b00a89f4..36c536681cc12 100644
--- a/src/boot/cpio.c
+++ b/src/boot/cpio.c
@@ -5,6 +5,7 @@
#include "iovec-util-fundamental.h"
#include "measure.h"
#include "string-util-fundamental.h"
+#include "tpm2-pcr.h"
#include "util.h"
static char *write_cpio_word(char *p, uint32_t v) {
@@ -306,7 +307,6 @@ EFI_STATUS pack_cpio(
const char16_t *match_suffix,
const char16_t *exclude_suffix,
const CpioTarget *target,
- uint32_t tpm_pcr,
const char16_t *tpm_description,
struct iovec *ret_buffer,
bool *ret_measured) {
@@ -406,7 +406,7 @@ EFI_STATUS pack_cpio(
err = file_read(extra_dir, items[i], 0, 0, &content, &contentsize);
if (err != EFI_SUCCESS) {
- log_error_status(err, "Failed to read %ls, ignoring: %m", items[i]);
+ log_warning_status(err, "Failed to read %ls, ignoring: %m", items[i]);
continue;
}
@@ -425,12 +425,16 @@ EFI_STATUS pack_cpio(
return log_error_status(err, "Failed to pack cpio trailer: %m");
err = tpm_log_ipl_event(
- tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured);
+ target->tpm_pcr,
+ POINTER_TO_PHYSICAL_ADDRESS(buffer),
+ buffer_size,
+ tpm_description,
+ ret_measured);
if (err != EFI_SUCCESS)
return log_error_status(
err,
- "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m",
- tpm_pcr,
+ "Unable to add cpio TPM measurement for PCR %u (%ls): %m",
+ target->tpm_pcr,
tpm_description);
*ret_buffer = IOVEC_MAKE(TAKE_PTR(buffer), buffer_size);
@@ -450,7 +454,6 @@ EFI_STATUS pack_cpio_literal(
size_t data_size,
const CpioTarget *target,
const char16_t *target_filename,
- uint32_t tpm_pcr,
const char16_t *tpm_description,
struct iovec *ret_buffer,
bool *ret_measured) {
@@ -486,12 +489,16 @@ EFI_STATUS pack_cpio_literal(
return log_error_status(err, "Failed to pack cpio trailer: %m");
err = tpm_log_ipl_event(
- tpm_pcr, POINTER_TO_PHYSICAL_ADDRESS(buffer), buffer_size, tpm_description, ret_measured);
+ target->tpm_pcr,
+ POINTER_TO_PHYSICAL_ADDRESS(buffer),
+ buffer_size,
+ tpm_description,
+ ret_measured);
if (err != EFI_SUCCESS)
return log_error_status(
err,
- "Unable to add cpio TPM measurement for PCR %u (%ls), ignoring: %m",
- tpm_pcr,
+ "Unable to add cpio TPM measurement for PCR %u (%ls): %m",
+ target->tpm_pcr,
tpm_description);
*ret_buffer = IOVEC_MAKE(TAKE_PTR(buffer), buffer_size);
@@ -506,46 +513,54 @@ const CpioTarget cpio_target_credentials = {
.directory = ".extra/credentials",
.dir_mode = 0500,
.access_mode = 0400,
+ .tpm_pcr = TPM2_PCR_KERNEL_CONFIG,
};
const CpioTarget cpio_target_global_credentials = {
.directory = ".extra/global_credentials",
.dir_mode = 0500,
.access_mode = 0400,
+ .tpm_pcr = TPM2_PCR_KERNEL_CONFIG,
};
const CpioTarget cpio_target_sysext = {
.directory = ".extra/sysext",
.dir_mode = 0555,
.access_mode = 0444,
+ .tpm_pcr = TPM2_PCR_SYSEXTS,
};
const CpioTarget cpio_target_global_sysext = {
.directory = ".extra/global_sysext",
.dir_mode = 0555,
.access_mode = 0444,
+ .tpm_pcr = TPM2_PCR_SYSEXTS,
};
const CpioTarget cpio_target_confext = {
.directory = ".extra/confext",
.dir_mode = 0555,
.access_mode = 0444,
+ .tpm_pcr = TPM2_PCR_KERNEL_CONFIG,
};
const CpioTarget cpio_target_global_confext = {
.directory = ".extra/global_confext",
.dir_mode = 0555,
.access_mode = 0444,
+ .tpm_pcr = TPM2_PCR_KERNEL_CONFIG,
};
const CpioTarget cpio_target_meta = {
.directory = ".extra",
.dir_mode = 0555,
.access_mode = 0444,
+ .tpm_pcr = UINT32_MAX,
};
const CpioTarget cpio_target_meta_secret = {
.directory = ".extra",
.dir_mode = 0555,
.access_mode = 0400,
+ .tpm_pcr = UINT32_MAX,
};
diff --git a/src/boot/cpio.h b/src/boot/cpio.h
index 3c311bc714d28..3aa525779344f 100644
--- a/src/boot/cpio.h
+++ b/src/boot/cpio.h
@@ -8,6 +8,7 @@ typedef struct CpioTarget {
const char *directory; /* Path to directory where to place resources */
uint32_t dir_mode; /* Access mode for the directory */
uint32_t access_mode; /* Access mode for the files in the directory */
+ uint32_t tpm_pcr; /* Where to measure this data into */
} CpioTarget;
EFI_STATUS pack_cpio_one(
@@ -35,7 +36,6 @@ EFI_STATUS pack_cpio(
const char16_t *match_suffix,
const char16_t *exclude_suffix,
const CpioTarget *target,
- uint32_t tpm_pcr,
const char16_t *tpm_description,
struct iovec *ret_buffer,
bool *ret_measured);
@@ -45,7 +45,6 @@ EFI_STATUS pack_cpio_literal(
size_t data_size,
const CpioTarget *target,
const char16_t *target_filename,
- uint32_t tpm_pcr,
const char16_t *tpm_description,
struct iovec *ret_buffer,
bool *ret_measured);
diff --git a/src/boot/meson.build b/src/boot/meson.build
index dfac98f034a6d..29fb64efbee1b 100644
--- a/src/boot/meson.build
+++ b/src/boot/meson.build
@@ -309,6 +309,7 @@ endif
libefi_sources = files(
'chid.c',
'console.c',
+ 'cpio.c',
'device-path-util.c',
'devicetree.c',
'drivers.c',
@@ -341,7 +342,6 @@ systemd_boot_sources = files(
stub_sources = files(
'boot-secret.c',
- 'cpio.c',
'linux.c',
'splash.c',
'stub.c',
diff --git a/src/boot/stub.c b/src/boot/stub.c
index 8632a603a21de..52927e91ff077 100644
--- a/src/boot/stub.c
+++ b/src/boot/stub.c
@@ -819,7 +819,6 @@ static void generate_sidecar_initrds(
u".cred",
/* exclude_suffix= */ NULL,
&cpio_target_credentials,
- /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG,
u"Credentials initrd",
initrds + INITRD_CREDENTIAL,
&m) == EFI_SUCCESS)
@@ -830,7 +829,6 @@ static void generate_sidecar_initrds(
u".cred",
/* exclude_suffix= */ NULL,
&cpio_target_global_credentials,
- /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG,
u"Global credentials initrd",
initrds + INITRD_GLOBAL_CREDENTIAL,
&m) == EFI_SUCCESS)
@@ -841,7 +839,6 @@ static void generate_sidecar_initrds(
u".raw", /* ideally we'd pick up only *.sysext.raw here, but for compat we pick up *.raw instead … */
u".confext.raw", /* … but then exclude *.confext.raw again */
&cpio_target_sysext,
- /* tpm_pcr= */ TPM2_PCR_SYSEXTS,
u"System extension initrd",
initrds + INITRD_SYSEXT,
&m) == EFI_SUCCESS)
@@ -852,7 +849,6 @@ static void generate_sidecar_initrds(
u".raw", /* as above */
u".confext.raw",
&cpio_target_global_sysext,
- /* tpm_pcr= */ TPM2_PCR_SYSEXTS,
u"Global system extension initrd",
initrds + INITRD_GLOBAL_SYSEXT,
&m) == EFI_SUCCESS)
@@ -863,7 +859,6 @@ static void generate_sidecar_initrds(
u".confext.raw",
/* exclude_suffix= */ NULL,
&cpio_target_confext,
- /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG,
u"Configuration extension initrd",
initrds + INITRD_CONFEXT,
&m) == EFI_SUCCESS)
@@ -874,7 +869,6 @@ static void generate_sidecar_initrds(
u".confext.raw",
/* exclude_suffix= */ NULL,
&cpio_target_global_confext,
- /* tpm_pcr= */ TPM2_PCR_KERNEL_CONFIG,
u"Global configuration extension initrd",
initrds + INITRD_GLOBAL_CONFEXT,
&m) == EFI_SUCCESS)
@@ -926,7 +920,6 @@ static void generate_embedded_initrds(
sections[t->section].memory_size,
&cpio_target_meta,
t->filename,
- /* tpm_pcr= */ UINT32_MAX,
/* tpm_description= */ NULL,
initrds + t->initrd_index,
/* ret_measured= */ NULL);
@@ -948,7 +941,6 @@ static void generate_boot_secret_initrd(
BOOT_SECRET_SIZE,
&cpio_target_meta_secret,
u"boot-secret",
- /* tpm_pcr= */ UINT32_MAX,
/* tpm_description= */ NULL,
initrds + INITRD_BOOT_SECRET,
/* ret_measured= */ NULL);
diff --git a/src/bootctl/bootctl.c b/src/bootctl/bootctl.c
index 04213dc8e17aa..6869e838cfc4e 100644
--- a/src/bootctl/bootctl.c
+++ b/src/bootctl/bootctl.c
@@ -298,8 +298,10 @@ static int help(void) {
"Options",
};
- _cleanup_(table_unref_many) Table *verb_tables[ELEMENTSOF(verb_groups) + 1] = {};
- _cleanup_(table_unref_many) Table *option_tables[ELEMENTSOF(option_groups) + 1] = {};
+ Table *verb_tables[ELEMENTSOF(verb_groups)] = {};
+ CLEANUP_ELEMENTS(verb_tables, table_unref_array_clear);
+ Table *option_tables[ELEMENTSOF(option_groups)] = {};
+ CLEANUP_ELEMENTS(option_tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(verb_groups); i++) {
r = verbs_get_help_table_group(verb_groups[i], &verb_tables[i]);
@@ -418,7 +420,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_GROUP("Block Device Discovery Commands"): {}
diff --git a/src/cgls/cgls.c b/src/cgls/cgls.c
index 9ed57c35cdf4f..cdb47ba8bdc57 100644
--- a/src/cgls/cgls.c
+++ b/src/cgls/cgls.c
@@ -72,7 +72,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_RETURN_POSITIONAL_ARGS };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/cgtop/cgtop.c b/src/cgtop/cgtop.c
index d1d20992bd159..dfee990a0f831 100644
--- a/src/cgtop/cgtop.c
+++ b/src/cgtop/cgtop.c
@@ -722,7 +722,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/core/dbus-manager.c b/src/core/dbus-manager.c
index f48d04a4cb483..076a26c6fd171 100644
--- a/src/core/dbus-manager.c
+++ b/src/core/dbus-manager.c
@@ -971,6 +971,10 @@ static int method_list_units_by_names(sd_bus_message *message, void *userdata, s
if (r < 0)
return r;
+ if (strv_length(units) > MAX(hashmap_size(m->units), (unsigned) MANAGER_MAX_NAMES / 2))
+ return sd_bus_error_set(reterr_error, SD_BUS_ERROR_LIMITS_EXCEEDED,
+ "Too many unit names requested.");
+
r = sd_bus_message_new_method_return(message, &reply);
if (r < 0)
return r;
diff --git a/src/core/executor.c b/src/core/executor.c
index 20bc65b63e6de..00761c6e3f7a6 100644
--- a/src/core/executor.c
+++ b/src/core/executor.c
@@ -64,7 +64,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/creds/creds.c b/src/creds/creds.c
index e14a9a921cda7..95af91c120db7 100644
--- a/src/creds/creds.c
+++ b/src/creds/creds.c
@@ -826,7 +826,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/cryptenroll/cryptenroll.c b/src/cryptenroll/cryptenroll.c
index 5d0c782689392..bcc4b6cca66cd 100644
--- a/src/cryptenroll/cryptenroll.c
+++ b/src/cryptenroll/cryptenroll.c
@@ -31,7 +31,6 @@
#include "process-util.h"
#include "string-table.h"
#include "string-util.h"
-#include "strv.h"
#include "tpm2-pcr.h"
#include "tpm2-util.h"
@@ -242,7 +241,8 @@ static int help(void) {
"TPM2 Enrollment",
};
- _cleanup_(table_unref_many) Table *tables[ELEMENTSOF(groups) + 1] = {};
+ Table *tables[ELEMENTSOF(groups)] = {};
+ CLEANUP_ELEMENTS(tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(groups); i++) {
r = option_parser_get_help_table_group(groups[i], &tables[i]);
@@ -279,7 +279,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
@@ -579,14 +579,12 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
- char **args = option_parser_get_args(&opts);
+ if (option_parser_get_n_args(&opts) > 1)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too many arguments, refusing.");
- if (strv_length(args) > 1)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Too many arguments, refusing.");
-
- if (args[0])
- r = parse_path_argument(args[0], false, &arg_node);
+ const char *arg = option_parser_get_arg(&opts, 0);
+ if (arg)
+ r = parse_path_argument(arg, false, &arg_node);
else if (!wipe_requested())
r = determine_default_node();
else
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c
index 43f2873da262c..2130c54047c04 100644
--- a/src/cryptsetup/cryptsetup.c
+++ b/src/cryptsetup/cryptsetup.c
@@ -2507,7 +2507,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/delta/delta.c b/src/delta/delta.c
index 92b77f9ddce5b..27dfc105ee7d6 100644
--- a/src/delta/delta.c
+++ b/src/delta/delta.c
@@ -520,7 +520,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/detect-virt/detect-virt.c b/src/detect-virt/detect-virt.c
index f88528fccf992..be39634583f2c 100644
--- a/src/detect-virt/detect-virt.c
+++ b/src/detect-virt/detect-virt.c
@@ -55,7 +55,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/dissect/dissect.c b/src/dissect/dissect.c
index 338aed8391d97..280d1ada5fdbf 100644
--- a/src/dissect/dissect.c
+++ b/src/dissect/dissect.c
@@ -232,7 +232,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_NO_PAGER:
diff --git a/src/escape/escape-tool.c b/src/escape/escape-tool.c
index 98f0b9a0146a0..09e0338c348fd 100644
--- a/src/escape/escape-tool.c
+++ b/src/escape/escape-tool.c
@@ -58,7 +58,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/factory-reset/factory-reset-tool.c b/src/factory-reset/factory-reset-tool.c
index 975c391fc8fae..e26e948e93416 100644
--- a/src/factory-reset/factory-reset-tool.c
+++ b/src/factory-reset/factory-reset-tool.c
@@ -72,7 +72,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/firstboot/firstboot.c b/src/firstboot/firstboot.c
index 721fbba21e102..3d768b491f83a 100644
--- a/src/firstboot/firstboot.c
+++ b/src/firstboot/firstboot.c
@@ -1270,7 +1270,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/fundamental/cleanup-fundamental.h b/src/fundamental/cleanup-fundamental.h
index 9094cff2331e0..8d499e5c3498b 100644
--- a/src/fundamental/cleanup-fundamental.h
+++ b/src/fundamental/cleanup-fundamental.h
@@ -64,6 +64,15 @@
free(array); \
}
+/* Like DEFINE_POINTER_ARRAY_FREE_FUNC() but does not deallocate the array itself, useful for
+ * arrays with automatic storage duration (e.g. on the stack). */
+#define DEFINE_POINTER_ARRAY_CLEAR_FUNC(type, helper) \
+ void helper ## _array_clear(type *array, size_t n) { \
+ assert(array || n == 0); \
+ FOREACH_ARRAY(item, array, n) \
+ *item = helper(*item); \
+ }
+
/* Clean up an array of objects of known size by dropping all the items in it.
* Then free the array itself. */
#define DEFINE_ARRAY_FREE_FUNC(name, type, helper) \
@@ -108,3 +117,33 @@ static inline void array_cleanup(const ArrayCleanup *c) {
_f; \
}), \
}
+
+/* An automatic _cleanup_-like logic for fixed-size arrays where the bound is known via
+ * ELEMENTSOF(). Unlike CLEANUP_ARRAY() this neither frees the storage nor zeroes it: it just
+ * invokes func() across the elements when leaving scope. */
+typedef struct ElementsCleanup {
+ void *array;
+ size_t n;
+ free_array_func_t pfunc;
+} ElementsCleanup;
+
+static inline void elements_cleanup(const ElementsCleanup *c) {
+ assert(c);
+
+ if (c->n == 0)
+ return;
+
+ assert(c->array);
+ assert(c->pfunc);
+ c->pfunc(c->array, c->n);
+}
+
+#define CLEANUP_ELEMENTS(_array, _func) \
+ _cleanup_(elements_cleanup) _unused_ const ElementsCleanup CONCATENATE(_cleanup_elements_, UNIQ) = { \
+ .array = (_array), \
+ .n = ELEMENTSOF(_array), \
+ .pfunc = (free_array_func_t) ({ \
+ void (*_f)(typeof((_array)[0]) *a, size_t b) = _func; \
+ _f; \
+ }), \
+ }
diff --git a/src/growfs/growfs.c b/src/growfs/growfs.c
index 3e9eb678bf038..30d371200d47f 100644
--- a/src/growfs/growfs.c
+++ b/src/growfs/growfs.c
@@ -162,7 +162,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
@@ -181,8 +181,7 @@ static int parse_argv(int argc, char *argv[]) {
"%s expects exactly one argument (the mount point).",
program_invocation_short_name);
- arg_target = option_parser_get_args(&opts)[0];
-
+ arg_target = option_parser_get_arg(&opts, 0);
return 1;
}
diff --git a/src/hibernate-resume/hibernate-resume.c b/src/hibernate-resume/hibernate-resume.c
index 5f42097194e18..d2dccd59bda8c 100644
--- a/src/hibernate-resume/hibernate-resume.c
+++ b/src/hibernate-resume/hibernate-resume.c
@@ -57,7 +57,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c
index 9ad2a0b4ec05e..2989840b364d7 100644
--- a/src/hostname/hostnamectl.c
+++ b/src/hostname/hostnamectl.c
@@ -767,7 +767,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/hwdb/hwdb.c b/src/hwdb/hwdb.c
index 286ea000dbeec..5ad3bac3211ee 100644
--- a/src/hwdb/hwdb.c
+++ b/src/hwdb/hwdb.c
@@ -83,7 +83,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/id128/id128.c b/src/id128/id128.c
index fbcacdbe4608b..ceac8a832e5c1 100644
--- a/src/id128/id128.c
+++ b/src/id128/id128.c
@@ -239,7 +239,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/imds/imds-tool.c b/src/imds/imds-tool.c
index 7752e1f769cfe..06bc6c4487d73 100644
--- a/src/imds/imds-tool.c
+++ b/src/imds/imds-tool.c
@@ -84,7 +84,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/imds/imdsd.c b/src/imds/imdsd.c
index c803b27829aba..a0c54ad84d7af 100644
--- a/src/imds/imdsd.c
+++ b/src/imds/imdsd.c
@@ -2251,7 +2251,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/import/export.c b/src/import/export.c
index 5b233e71a5bfd..a77333643c6bc 100644
--- a/src/import/export.c
+++ b/src/import/export.c
@@ -240,7 +240,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/import/import-fs.c b/src/import/import-fs.c
index 878d0c5b8f14a..513a2c62d3960 100644
--- a/src/import/import-fs.c
+++ b/src/import/import-fs.c
@@ -316,7 +316,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/import/import.c b/src/import/import.c
index 43740aeac7d46..798b6b743a21c 100644
--- a/src/import/import.c
+++ b/src/import/import.c
@@ -319,7 +319,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/import/importctl.c b/src/import/importctl.c
index 65fff5f5f3a43..d4a6483f36d7c 100644
--- a/src/import/importctl.c
+++ b/src/import/importctl.c
@@ -1115,7 +1115,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/import/pull.c b/src/import/pull.c
index 0cc23dc6ed4b2..6a1f913ff8a5c 100644
--- a/src/import/pull.c
+++ b/src/import/pull.c
@@ -366,7 +366,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/include/override/sys/ucontext.h b/src/include/override/sys/ucontext.h
new file mode 100644
index 0000000000000..eea1205279c4d
--- /dev/null
+++ b/src/include/override/sys/ucontext.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+/* libucontext.h in freestanding mode and sys/ucontext.h both define REG_R8 which ends up conflicting if both
+ * are included in the same translation unit. Unfortunately, we cannot simply not include sys/ucontext.h when
+ * building with libucontext as sys/ucontext.h is unconditionally included by signal.h, so we override
+ * sys/ucontext.h to avoid conflicts. */
+
+/* Since we don't have a way to check whether libucontext was built in freestanding mode or not, we check for
+ * glibc as a proxy since glibc-based distributions shipping libucontext should be building it in
+ * freestanding mode. */
+
+#if HAVE_LIBUCONTEXT && defined(__GLIBC__)
+#include /* IWYU pragma: export */
+
+typedef libucontext_ucontext_t ucontext_t;
+#else
+#include_next /* IWYU pragma: export */
+#endif
diff --git a/src/include/override/ucontext.h b/src/include/override/ucontext.h
new file mode 100644
index 0000000000000..726df75a1ec35
--- /dev/null
+++ b/src/include/override/ucontext.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#if HAVE_LIBUCONTEXT && defined(__GLIBC__)
+#include /* IWYU pragma: export */
+
+typedef libucontext_ucontext_t ucontext_t;
+#define getcontext libucontext_getcontext
+#define setcontext libucontext_setcontext
+#define makecontext libucontext_makecontext
+#define swapcontext libucontext_swapcontext
+#else
+#include_next /* IWYU pragma: export */
+#endif
diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c
index ffef7edea5ec8..e70fc4f6dbf37 100644
--- a/src/journal-remote/journal-gatewayd.c
+++ b/src/journal-remote/journal-gatewayd.c
@@ -1121,7 +1121,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/journal-remote/journal-remote-main.c b/src/journal-remote/journal-remote-main.c
index 5709f87f74617..614ec61be907d 100644
--- a/src/journal-remote/journal-remote-main.c
+++ b/src/journal-remote/journal-remote-main.c
@@ -907,7 +907,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/journal/bsod.c b/src/journal/bsod.c
index 1701605590209..e380e08b1c20c 100644
--- a/src/journal/bsod.c
+++ b/src/journal/bsod.c
@@ -250,7 +250,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/journal/cat.c b/src/journal/cat.c
index f8b5e0df31727..b2b1689ff26d5 100644
--- a/src/journal/cat.c
+++ b/src/journal/cat.c
@@ -62,7 +62,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/keyutil/keyutil.c b/src/keyutil/keyutil.c
index 474f42fec72ec..2a66fabb19542 100644
--- a/src/keyutil/keyutil.c
+++ b/src/keyutil/keyutil.c
@@ -90,7 +90,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/libsystemd-network/test-ndisc-send.c b/src/libsystemd-network/test-ndisc-send.c
index de04198d370c0..87b8abefd58f0 100644
--- a/src/libsystemd-network/test-ndisc-send.c
+++ b/src/libsystemd-network/test-ndisc-send.c
@@ -80,7 +80,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_VERSION:
diff --git a/src/libsystemd/meson.build b/src/libsystemd/meson.build
index 08d8d7c5c39e7..f2a85513558b0 100644
--- a/src/libsystemd/meson.build
+++ b/src/libsystemd/meson.build
@@ -33,6 +33,7 @@ sd_daemon_sources = files('sd-daemon/sd-daemon.c')
############################################################
sd_event_sources = files(
+ 'sd-event/event-future.c',
'sd-event/event-util.c',
'sd-event/sd-event.c',
)
@@ -48,6 +49,7 @@ sd_bus_sources = files(
'sd-bus/bus-dump.c',
'sd-bus/bus-dump-json.c',
'sd-bus/bus-error.c',
+ 'sd-bus/bus-future.c',
'sd-bus/bus-internal.c',
'sd-bus/bus-introspect.c',
'sd-bus/bus-kernel.c',
@@ -75,6 +77,14 @@ sd_device_sources = files(
############################################################
+sd_future_sources = files(
+ 'sd-future/fiber-io.c',
+ 'sd-future/fiber.c',
+ 'sd-future/sd-future.c',
+)
+
+############################################################
+
sd_login_sources = files('sd-login/sd-login.c')
############################################################
@@ -135,8 +145,9 @@ libsystemd_sources = files(
'sd-resolve/sd-resolve.c',
) + sd_journal_sources + sd_id128_sources + sd_daemon_sources \
+ sd_event_sources + sd_bus_sources + sd_device_sources \
- + sd_login_sources + sd_json_sources + sd_varlink_sources \
- + sd_path_sources + sd_netlink_sources + sd_network_sources
+ + sd_future_sources + sd_login_sources + sd_json_sources \
+ + sd_varlink_sources + sd_path_sources + sd_netlink_sources \
+ + sd_network_sources
sources += libsystemd_sources
@@ -153,6 +164,7 @@ libsystemd_static = static_library(
dependencies : [threads,
libm,
librt,
+ libucontext,
userspace],
build_by_default : false)
@@ -174,50 +186,38 @@ libsystemd_pc = custom_target(
############################################################
-simple_tests += files(
- 'sd-journal/test-audit-type.c',
- 'sd-journal/test-catalog.c',
- 'sd-journal/test-journal-file.c',
- 'sd-journal/test-journal-init.c',
- 'sd-journal/test-journal-match.c',
- 'sd-journal/test-journal-send.c',
- 'sd-journal/test-mmap-cache.c',
-)
-
-libsystemd_tests += [
- {
- 'sources' : files('sd-journal/test-journal-enum.c'),
- 'timeout' : 360,
- },
- {
- 'sources' : files('sd-event/test-event.c'),
- 'timeout' : 120,
- }
-]
-
-############################################################
-
simple_tests += files(
'sd-bus/test-bus-creds.c',
+ 'sd-bus/test-bus-fiber.c',
'sd-bus/test-bus-introspect.c',
'sd-bus/test-bus-match.c',
'sd-bus/test-bus-vtable.c',
'sd-device/test-device-util.c',
'sd-device/test-sd-device-monitor.c',
+ 'sd-event/test-event-future.c',
+ 'sd-future/test-fiber.c',
+ 'sd-future/test-fiber-io.c',
+ 'sd-future/test-fiber-ops.c',
+ 'sd-hwdb/test-sd-hwdb.c',
+ 'sd-id128/test-id128.c',
+ 'sd-journal/test-audit-type.c',
+ 'sd-journal/test-catalog.c',
+ 'sd-journal/test-journal-file.c',
'sd-journal/test-journal-flush.c',
+ 'sd-journal/test-journal-init.c',
'sd-journal/test-journal-interleaving.c',
+ 'sd-journal/test-journal-match.c',
+ 'sd-journal/test-journal-send.c',
'sd-journal/test-journal-stream.c',
'sd-journal/test-journal.c',
+ 'sd-journal/test-mmap-cache.c',
'sd-login/test-login.c',
'sd-login/test-sd-login.c',
'sd-netlink/test-netlink.c',
+ 'sd-path/test-sd-path.c',
)
libsystemd_tests += [
- {
- 'sources' : files('sd-device/test-sd-device.c'),
- 'dependencies' : [ threads, libmount_cflags ],
- },
{
'sources' : files('sd-bus/test-bus-address.c'),
'dependencies' : threads
@@ -275,6 +275,18 @@ libsystemd_tests += [
'dependencies' : threads,
'timeout' : 120,
},
+ {
+ 'sources' : files('sd-device/test-sd-device.c'),
+ 'dependencies' : [threads, libmount_cflags],
+ },
+ {
+ 'sources' : files('sd-event/test-event.c'),
+ 'timeout' : 120,
+ },
+ {
+ 'sources' : files('sd-journal/test-journal-enum.c'),
+ 'timeout' : 360,
+ },
{
'sources' : files('sd-journal/test-journal-append.c'),
'type' : 'manual',
@@ -287,11 +299,23 @@ libsystemd_tests += [
'sources' : files('sd-journal/test-journal-verify.c'),
'timeout' : 90,
},
+ {
+ 'sources' : files('sd-json/test-json.c'),
+ 'dependencies' : libm,
+ },
{
'sources' : files('sd-resolve/test-resolve.c'),
'dependencies' : threads,
'timeout' : 120,
},
+ {
+ 'sources' : files('sd-varlink/test-varlink.c'),
+ 'dependencies' : threads,
+ },
+ {
+ 'sources' : files('sd-varlink/test-varlink-idl.c'),
+ 'dependencies' : threads,
+ },
]
if cxx_cmd != ''
diff --git a/src/libsystemd/sd-bus/bus-future.c b/src/libsystemd/sd-bus/bus-future.c
new file mode 100644
index 0000000000000..16b2707f47367
--- /dev/null
+++ b/src/libsystemd/sd-bus/bus-future.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-bus.h"
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "bus-future.h"
+#include "bus-internal.h"
+#include "bus-message.h"
+
+typedef struct BusFuture {
+ sd_promise *promise;
+ sd_bus_slot *slot;
+ sd_bus_message *reply;
+} BusFuture;
+
+static void* bus_future_free(void *impl) {
+ BusFuture *f = impl;
+ if (!f)
+ return NULL;
+
+ sd_bus_slot_unref(f->slot);
+ sd_bus_message_unref(f->reply);
+ return mfree(f);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(BusFuture*, bus_future_free);
+
+static int bus_future_cancel(void *impl) {
+ BusFuture *f = ASSERT_PTR(impl);
+
+ f->slot = sd_bus_slot_unref(f->slot);
+ return sd_promise_resolve(f->promise, -ECANCELED);
+}
+
+static const sd_future_ops bus_future_ops = {
+ .free = bus_future_free,
+ .cancel = bus_future_cancel,
+};
+
+static int bus_future_handler(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
+ BusFuture *f = ASSERT_PTR(userdata);
+
+ /* Resolve with 0 on any reply (including error replies). The reply itself carries
+ * success/error information via future_get_bus_reply(); the future's resolution result is
+ * reserved for cancellation (-ECANCELED), so callers can distinguish "got a reply" from
+ * "no reply will arrive". */
+ f->slot = sd_bus_slot_unref(f->slot);
+ f->reply = sd_bus_message_ref(m);
+ return sd_promise_resolve(f->promise, 0);
+}
+
+int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **ret) {
+ int r;
+
+ assert(bus);
+ assert(m);
+ assert(ret);
+
+ _cleanup_(bus_future_freep) BusFuture *impl = new0(BusFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ r = sd_bus_call_async(bus, &impl->slot, m, bus_future_handler, impl, usec);
+ if (r < 0)
+ return r;
+
+ sd_future *f;
+ r = sd_future_new(&bus_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+ *ret = TAKE_PTR(f);
+ return 0;
+}
+
+int future_get_bus_reply(sd_future *f, sd_bus_error *reterr_error, sd_bus_message **ret_reply) {
+ assert(f);
+ assert(sd_future_get_ops(f) == &bus_future_ops);
+ assert(sd_future_state(f) == SD_FUTURE_RESOLVED);
+
+ BusFuture *impl = ASSERT_PTR(sd_future_get_impl(f));
+ sd_bus_message *reply = ASSERT_PTR(impl->reply);
+
+ if (sd_bus_message_is_method_error(reply, NULL)) {
+ if (reterr_error)
+ return sd_bus_error_copy(reterr_error, sd_bus_message_get_error(reply));
+ return -sd_bus_message_get_errno(reply);
+ }
+
+ if (reply->n_fds > 0 && !sd_bus_message_get_bus(reply)->accept_fd)
+ return sd_bus_error_set(reterr_error, SD_BUS_ERROR_INCONSISTENT_MESSAGE,
+ "Reply message contained file descriptors which I couldn't accept. Sorry.");
+
+ if (reterr_error)
+ *reterr_error = SD_BUS_ERROR_NULL;
+ if (ret_reply)
+ *ret_reply = sd_bus_message_ref(reply);
+
+ return 1;
+}
+
+int bus_call_suspend(
+ sd_bus *bus,
+ sd_bus_message *m,
+ uint64_t usec,
+ sd_bus_error *reterr_error,
+ sd_bus_message **ret_reply) {
+
+ int r;
+
+ assert(bus);
+ assert(m);
+ assert(sd_fiber_is_running());
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *call = NULL;
+ r = bus_call_future(bus, m, usec, &call);
+ if (r < 0)
+ return r;
+
+ r = sd_fiber_suspend();
+ if (r < 0)
+ return r;
+
+ return future_get_bus_reply(call, reterr_error, ret_reply);
+}
diff --git a/src/libsystemd/sd-bus/bus-future.h b/src/libsystemd/sd-bus/bus-future.h
new file mode 100644
index 0000000000000..ec9bd80b1598a
--- /dev/null
+++ b/src/libsystemd/sd-bus/bus-future.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-forward.h"
+
+int bus_call_future(sd_bus *bus, sd_bus_message *m, uint64_t usec, sd_future **ret);
+int future_get_bus_reply(sd_future *f, sd_bus_error *reterr_error, sd_bus_message **ret_reply);
+
+int bus_call_suspend(
+ sd_bus *bus,
+ sd_bus_message *m,
+ uint64_t usec,
+ sd_bus_error *reterr_error,
+ sd_bus_message **ret_reply);
diff --git a/src/libsystemd/sd-bus/bus-internal.h b/src/libsystemd/sd-bus/bus-internal.h
index 19a3b67d12f6a..3a52f738d6bd7 100644
--- a/src/libsystemd/sd-bus/bus-internal.h
+++ b/src/libsystemd/sd-bus/bus-internal.h
@@ -17,6 +17,13 @@
#define DEFAULT_SYSTEM_BUS_ADDRESS "unix:path=/run/dbus/system_bus_socket"
#define DEFAULT_USER_BUS_ADDRESS_FMT "unix:path=%s/bus"
+/* Private vtable flag: dispatch the method handler on its own fiber, so it can use suspending
+ * primitives (sd_bus_call() on a fiber, sd_fiber_sleep(), loop_read_suspend(), ...) without
+ * blocking the event loop for other connections or method calls. Kept out of the public
+ * sd-bus-vtable.h so the fiber runtime stays an implementation detail of systemd. The bit value is
+ * reserved in sd-bus-vtable.h to make sure it never collides with a future public flag. */
+#define SD_BUS_VTABLE_METHOD_FIBER (UINT64_C(1) << 10)
+
typedef struct BusReplyCallback {
sd_bus_message_handler_t callback;
usec_t timeout_usec; /* this is a relative timeout until we reach the BUS_HELLO state, and an absolute one right after */
@@ -222,6 +229,12 @@ typedef struct sd_bus {
Set *vtable_methods;
Set *vtable_properties;
+ /* Futures for outstanding SD_BUS_VTABLE_METHOD_FIBER dispatches. Entries are added as the
+ * dispatcher spawns each fiber and removed when the fiber resolves. On bus_enter_closing()
+ * we cancel everything in here and then wait in process_closing() until the set drains,
+ * before tearing down the rest of the bus. */
+ Set *fiber_futures;
+
union sockaddr_union sockaddr;
socklen_t sockaddr_size;
diff --git a/src/libsystemd/sd-bus/bus-message.c b/src/libsystemd/sd-bus/bus-message.c
index 94be969f7f420..017ffb7a6127a 100644
--- a/src/libsystemd/sd-bus/bus-message.c
+++ b/src/libsystemd/sd-bus/bus-message.c
@@ -4331,6 +4331,7 @@ int bus_message_get_blob(sd_bus_message *m, void **buffer, size_t *sz) {
_public_ int sd_bus_message_read_strv_extend(sd_bus_message *m, char ***l) {
char type;
const char *contents, *s;
+ size_t n;
int r;
assert(m);
@@ -4347,9 +4348,10 @@ _public_ int sd_bus_message_read_strv_extend(sd_bus_message *m, char ***l) {
if (r <= 0)
return r;
+ n = strv_length(*l);
/* sd_bus_message_read_basic() does content validation for us. */
while ((r = sd_bus_message_read_basic(m, *contents, &s)) > 0) {
- r = strv_extend(l, s);
+ r = strv_extend_with_size(l, &n, s);
if (r < 0)
return r;
}
diff --git a/src/libsystemd/sd-bus/bus-objects.c b/src/libsystemd/sd-bus/bus-objects.c
index 83ba3a523992b..c39a51a6d4380 100644
--- a/src/libsystemd/sd-bus/bus-objects.c
+++ b/src/libsystemd/sd-bus/bus-objects.c
@@ -3,6 +3,7 @@
#include
#include "sd-bus.h"
+#include "sd-future.h"
#include "alloc-util.h"
#include "bus-internal.h"
@@ -337,6 +338,69 @@ static int check_access(sd_bus *bus, sd_bus_message *m, BusVTableMember *c, sd_b
return sd_bus_error_setf(reterr_error, SD_BUS_ERROR_ACCESS_DENIED, "Access to %s.%s() not permitted.", c->interface, c->member);
}
+typedef struct BusFiberData {
+ sd_bus *bus;
+ sd_bus_message *message;
+ sd_bus_slot *slot;
+ sd_bus_message_handler_t handler;
+ void *userdata;
+} BusFiberData;
+
+static BusFiberData* bus_fiber_data_free(BusFiberData *d) {
+ if (!d)
+ return NULL;
+
+ sd_bus_slot_unref(d->slot);
+ sd_bus_message_unref(d->message);
+ sd_bus_unref(d->bus);
+ return mfree(d);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(BusFiberData*, bus_fiber_data_free);
+
+static void bus_fiber_data_destroy(void *userdata) {
+ bus_fiber_data_free(userdata);
+}
+
+static void bus_fiber_future_unref(void *p) {
+ sd_future_unref(p);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+ bus_fiber_future_hash_ops,
+ void,
+ trivial_hash_func,
+ trivial_compare_func,
+ bus_fiber_future_unref);
+
+static int bus_fiber_resolved(sd_future *f) {
+ sd_bus *bus = ASSERT_PTR(sd_future_get_userdata(f));
+
+ /* Remove the future from the bus' tracking set. set_remove() calls sd_future_unref() via the
+ * hash_ops destructor; fiber_run() holds an extra ref across the resolve path so the future
+ * itself isn't freed mid-resolution even if our ref was the last one. */
+ assert_se(set_remove(bus->fiber_futures, f) == f);
+ return 0;
+}
+
+static int bus_fiber_entry(void *userdata) {
+ BusFiberData *d = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ int r;
+
+ /* Note: unlike the synchronous dispatch path, we deliberately do NOT set
+ * bus->current_slot/handler/userdata around the callback. Those fields track the slot of the
+ * message currently being dispatched inline and must be NULL at each entry into
+ * bus_process_internal(). Because a fiber handler can yield and let the event loop dispatch
+ * other messages before it resumes, leaving current_slot non-NULL across yields would trip
+ * that invariant. Fiber handlers receive their slot's userdata via the handler argument, so
+ * sd_bus_get_current_slot()/handler()/userdata() simply aren't meaningful inside them — the
+ * handler should use the message/userdata parameters directly instead. */
+ r = d->handler(d->message, d->userdata, &error);
+
+ return bus_maybe_reply_error(d->message, r, &error);
+}
+
static int method_callbacks_run(
sd_bus *bus,
sd_bus_message *m,
@@ -407,6 +471,50 @@ static int method_callbacks_run(
slot = container_of(c->parent, sd_bus_slot, node_vtable);
+ if (FLAGS_SET(c->vtable->flags, SD_BUS_VTABLE_METHOD_FIBER)) {
+ /* A fiber-dispatched method requires an event loop to spawn the fiber on.
+ * By the time a method call actually arrives the bus is running, so the
+ * event loop should already be attached — if not, the caller set up the bus
+ * wrong and there's no meaningful recovery. */
+ assert(bus->event);
+
+ _cleanup_(bus_fiber_data_freep) BusFiberData *d = new(BusFiberData, 1);
+ if (!d)
+ return -ENOMEM;
+
+ *d = (BusFiberData) {
+ .bus = sd_bus_ref(bus),
+ .message = sd_bus_message_ref(m),
+ .slot = sd_bus_slot_ref(slot),
+ .handler = c->vtable->x.method.handler,
+ .userdata = u,
+ };
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ r = sd_fiber_new(bus->event, c->member, bus_fiber_entry, d, bus_fiber_data_destroy, &f);
+ if (r < 0)
+ return bus_maybe_reply_error(m, r, NULL);
+
+ /* The fiber now owns d via bus_fiber_data_destroy. Drop our cleanup before any
+ * further fallible calls, so a later failure unwinding f doesn't double-free d. */
+ TAKE_PTR(d);
+
+ r = set_ensure_put(&bus->fiber_futures, &bus_fiber_future_hash_ops, f);
+ if (r < 0)
+ return bus_maybe_reply_error(m, r, NULL);
+ assert(r > 0);
+
+ /* Track the future on the bus so shutdown can cancel it and wait for it. */
+ r = sd_future_set_callback(f, bus_fiber_resolved, bus);
+ if (r < 0) {
+ assert_se(set_remove(bus->fiber_futures, f) == f);
+ return bus_maybe_reply_error(m, r, NULL);
+ }
+
+ TAKE_PTR(f);
+ return 1;
+ }
+
bus->current_slot = sd_bus_slot_ref(slot);
bus->current_handler = c->vtable->x.method.handler;
bus->current_userdata = u;
diff --git a/src/libsystemd/sd-bus/sd-bus.c b/src/libsystemd/sd-bus/sd-bus.c
index 27f788d995576..67bf395f10444 100644
--- a/src/libsystemd/sd-bus/sd-bus.c
+++ b/src/libsystemd/sd-bus/sd-bus.c
@@ -10,12 +10,14 @@
#include "sd-bus.h"
#include "sd-event.h"
+#include "sd-future.h"
#include "af-list.h"
#include "alloc-util.h"
#include "bus-container.h"
#include "bus-control.h"
#include "bus-error.h"
+#include "bus-future.h"
#include "bus-internal.h"
#include "bus-kernel.h"
#include "bus-label.h"
@@ -33,7 +35,6 @@
#include "glyph-util.h"
#include "hexdecoct.h"
#include "hostname-util.h"
-#include "io-util.h"
#include "log.h"
#include "log-context.h"
#include "memory-util.h"
@@ -222,6 +223,12 @@ static sd_bus* bus_free(sd_bus *b) {
ordered_hashmap_free(b->reply_callbacks);
prioq_free(b->reply_callbacks_prioq);
+ /* Outstanding fiber handlers pin the bus via their BusFiberData ref, so by the time refcount
+ * reaches zero and bus_free() runs, every fiber has already resolved and removed itself from
+ * this set. */
+ assert(set_isempty(b->fiber_futures));
+ set_free(b->fiber_futures);
+
assert(b->match_callbacks.type == BUS_MATCH_ROOT);
bus_match_free(&b->match_callbacks);
@@ -1809,6 +1816,8 @@ _public_ sd_bus* sd_bus_flush_close_unref(sd_bus *bus) {
}
void bus_enter_closing(sd_bus *bus, int exit_code) {
+ sd_future *f;
+
assert(bus);
if (!IN_SET(bus->state, BUS_WATCH_BIND, BUS_OPENING, BUS_AUTHENTICATING, BUS_HELLO, BUS_RUNNING))
@@ -1816,6 +1825,16 @@ void bus_enter_closing(sd_bus *bus, int exit_code) {
bus_set_state(bus, BUS_CLOSING);
bus->exit_code = exit_code;
+
+ /* Cancel all outstanding fiber-dispatched method handlers. Each cancellation is scheduled
+ * asynchronously (fibers resolve with -ECANCELED the next time they run), so this doesn't
+ * block here — process_closing() waits for the fiber_futures set to drain before it
+ * continues tearing down the rest of the bus. */
+ SET_FOREACH(f, bus->fiber_futures) {
+ int r = sd_future_cancel(f);
+ if (r < 0)
+ log_debug_errno(r, "Failed to cancel outstanding fiber method handler, ignoring: %m");
+ }
}
/* Define manually so we can add the PID check */
@@ -2388,23 +2407,30 @@ _public_ int sd_bus_call(
sd_bus_error *reterr_error,
sd_bus_message **ret_reply) {
- _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = sd_bus_message_ref(_m);
usec_t timeout;
uint64_t cookie;
size_t i;
int r;
- bus_assert_return(m, -EINVAL, reterr_error);
- bus_assert_return(m->header->type == SD_BUS_MESSAGE_METHOD_CALL, -EINVAL, reterr_error);
- bus_assert_return(!(m->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED), -EINVAL, reterr_error);
+ bus_assert_return(_m, -EINVAL, reterr_error);
+ bus_assert_return(_m->header->type == SD_BUS_MESSAGE_METHOD_CALL, -EINVAL, reterr_error);
+ bus_assert_return(!(_m->header->flags & BUS_MESSAGE_NO_REPLY_EXPECTED), -EINVAL, reterr_error);
bus_assert_return(!bus_error_is_dirty(reterr_error), -EINVAL, reterr_error);
if (bus)
assert_return(bus = bus_resolve(bus), -ENOPKG);
else
- assert_return(bus = m->bus, -ENOTCONN);
+ assert_return(bus = _m->bus, -ENOTCONN);
bus_assert_return(!bus_origin_changed(bus), -ECHILD, reterr_error);
+ /* If the current fiber and the bus share their event loop, we can use sd_bus_call_suspend()
+ * instead which does an async method call. This allows multiple invocations of sd_bus_call() to
+ * happen across multiple fibers at once. */
+ if (sd_fiber_is_running() && bus->event == sd_fiber_get_event())
+ return bus_call_suspend(bus, _m, usec, reterr_error, ret_reply);
+
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = sd_bus_message_ref(_m);
+
if (!BUS_IS_OPEN(bus->state)) {
r = -ENOTCONN;
goto fail;
@@ -3177,7 +3203,14 @@ static int process_closing(sd_bus *bus, sd_bus_message **ret) {
assert(bus);
assert(bus->state == BUS_CLOSING);
- /* First, fail all outstanding method calls */
+ /* Wait for any still-running fiber method handlers to finish unwinding their cancellation
+ * before tearing down the rest of the bus. bus_enter_closing() scheduled the cancel; each
+ * fiber resolves asynchronously and bus_fiber_resolved() removes it from the set. Returning
+ * 1 here keeps the bus in CLOSING state so the event loop drives the fibers to completion. */
+ if (!set_isempty(bus->fiber_futures))
+ return 1;
+
+ /* Then, fail all outstanding method calls */
c = ordered_hashmap_first(bus->reply_callbacks);
if (c)
return process_closing_reply_callback(bus, c);
@@ -3369,7 +3402,7 @@ static int bus_poll(sd_bus *bus, bool need_more, uint64_t timeout_usec) {
if (timeout_usec != UINT64_MAX && (m == USEC_INFINITY || timeout_usec < m))
m = timeout_usec;
- r = ppoll_usec(p, n, m);
+ r = sd_fiber_poll(p, n, m);
if (r <= 0)
return r;
diff --git a/src/libsystemd/sd-bus/test-bus-chat.c b/src/libsystemd/sd-bus/test-bus-chat.c
index 1f358ccd3396e..25daf4081acc1 100644
--- a/src/libsystemd/sd-bus/test-bus-chat.c
+++ b/src/libsystemd/sd-bus/test-bus-chat.c
@@ -1,11 +1,11 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include
-#include
#include
#include
#include "sd-bus.h"
+#include "sd-future.h"
#include "alloc-util.h"
#include "bus-error.h"
@@ -102,7 +102,8 @@ static int server_init(sd_bus **ret) {
return 0;
}
-static int server(sd_bus *bus) {
+static int server(void *userdata) {
+ sd_bus *bus = ASSERT_PTR(userdata);
bool client1_gone = false, client2_gone = false;
int r;
@@ -178,7 +179,7 @@ static int server(sd_bus *bus) {
client2_gone = true;
} else if (sd_bus_message_is_method_call(m, "org.freedesktop.systemd.test", "Slow")) {
- sleep(1);
+ sd_fiber_sleep(1 * USEC_PER_SEC);
r = sd_bus_reply_method_return(m, NULL);
if (r < 0)
@@ -194,10 +195,10 @@ static int server(sd_bus *bus) {
log_info("Received fd=%d", fd);
- if (write(fd, &x, 1) < 0) {
- r = log_error_errno(errno, "Failed to write to fd: %m");
+ ssize_t n = sd_fiber_write(fd, &x, 1);
+ if (n < 0) {
safe_close(fd);
- return r;
+ return log_error_errno(n, "Failed to write to fd: %m");
}
r = sd_bus_reply_method_return(m, NULL);
@@ -217,7 +218,7 @@ static int server(sd_bus *bus) {
return 0;
}
-static void* client1(void *p) {
+static int client1(void *userdata) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -277,9 +278,9 @@ static void* client1(void *p) {
goto finish;
}
- errno = 0;
- if (read(pp[0], &x, 1) <= 0) {
- log_error("Failed to read from pipe: %s", STRERROR_OR_EOF(errno));
+ ssize_t n = sd_fiber_read(pp[0], &x, 1);
+ if (n <= 0) {
+ log_error("Failed to read from pipe: %s", STRERROR_OR_EOF(n));
goto finish;
}
@@ -303,7 +304,7 @@ static void* client1(void *p) {
}
- return INT_TO_PTR(r);
+ return r;
}
static int quit_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
@@ -315,7 +316,7 @@ static int quit_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_er
return 1;
}
-static void* client2(void *p) {
+static int client2(void *userdata) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
@@ -494,7 +495,7 @@ static void* client2(void *p) {
(void) sd_bus_send(bus, q, NULL);
}
- return INT_TO_PTR(r);
+ return r;
}
static ino_t get_inode(int fd) {
@@ -626,9 +627,9 @@ TEST(ctrunc) {
}
TEST(chat) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client1 = NULL, *f_client2 = NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- pthread_t c1, c2;
- void *p;
int r;
test_setup_logging(LOG_INFO);
@@ -639,16 +640,18 @@ TEST(chat) {
log_info("Initialized...");
- ASSERT_OK(-pthread_create(&c1, NULL, client1, NULL));
- ASSERT_OK(-pthread_create(&c2, NULL, client2, NULL));
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
- r = server(bus);
+ ASSERT_OK(sd_fiber_new(e, "client-1", client1, NULL, /* destroy= */ NULL, &f_client1));
+ ASSERT_OK(sd_fiber_new(e, "client-2", client2, NULL, /* destroy= */ NULL, &f_client2));
+ ASSERT_OK(sd_fiber_new(e, "server", server, bus, /* destroy= */ NULL, &f_server));
- ASSERT_OK(-pthread_join(c1, &p));
- ASSERT_OK(PTR_TO_INT(p));
- ASSERT_OK(-pthread_join(c2, &p));
- ASSERT_OK(PTR_TO_INT(p));
- ASSERT_OK(r);
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(f_client1));
+ ASSERT_OK(sd_future_result(f_client2));
+ ASSERT_OK(sd_future_result(f_server));
}
DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/libsystemd/sd-bus/test-bus-fiber.c b/src/libsystemd/sd-bus/test-bus-fiber.c
new file mode 100644
index 0000000000000..3c3509f2ca2d4
--- /dev/null
+++ b/src/libsystemd/sd-bus/test-bus-fiber.c
@@ -0,0 +1,194 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-bus.h"
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "bus-internal.h"
+#include "tests.h"
+#include "time-util.h"
+
+typedef struct Context {
+ /* Counters for the concurrency check: every Concurrent invocation bumps in_flight on entry
+ * and drops it on exit, and tracks the maximum observed concurrency. If fiber dispatch
+ * works, two overlapping client calls must both be inside the handler at the same time,
+ * giving a max of at least 2. */
+ int in_flight;
+ int max_in_flight;
+} Context;
+
+static int method_concurrent(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
+ Context *c = ASSERT_PTR(userdata);
+
+ ASSERT_OK_POSITIVE(sd_fiber_is_running());
+
+ c->in_flight++;
+ if (c->in_flight > c->max_in_flight)
+ c->max_in_flight = c->in_flight;
+
+ ASSERT_OK(sd_fiber_sleep(10 * USEC_PER_MSEC));
+
+ c->in_flight--;
+
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+static int method_fail_errno(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
+ ASSERT_OK_POSITIVE(sd_fiber_is_running());
+
+ /* Yielding first exercises the deferred-error path in the fiber entry: the handler returns
+ * a negative errno after suspending, and bus_maybe_reply_error() must still turn that into
+ * a matching sd_bus error reply. */
+ ASSERT_OK(sd_fiber_sleep(1 * USEC_PER_MSEC));
+
+ return -EACCES;
+}
+
+static int method_fail_error(sd_bus_message *m, void *userdata, sd_bus_error *reterr_error) {
+ ASSERT_OK_POSITIVE(sd_fiber_is_running());
+
+ ASSERT_OK(sd_fiber_sleep(1 * USEC_PER_MSEC));
+
+ return sd_bus_error_set(reterr_error, SD_BUS_ERROR_INVALID_ARGS, "bad arguments from fiber");
+}
+
+static const sd_bus_vtable vtable[] = {
+ SD_BUS_VTABLE_START(0),
+ SD_BUS_METHOD("Concurrent", NULL, NULL, method_concurrent,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
+ SD_BUS_METHOD("FailErrno", NULL, NULL, method_fail_errno,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
+ SD_BUS_METHOD("FailError", NULL, NULL, method_fail_error,
+ SD_BUS_VTABLE_UNPRIVILEGED|SD_BUS_VTABLE_METHOD_FIBER),
+ SD_BUS_VTABLE_END,
+};
+
+typedef struct Setup {
+ int fds[2];
+ Context *c;
+} Setup;
+
+static int attach_pair(Setup *s, sd_bus **ret_server, sd_bus **ret_client) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
+ sd_id128_t id;
+
+ assert(ret_server);
+ assert(ret_client);
+
+ ASSERT_OK(sd_id128_randomize(&id));
+ ASSERT_OK(sd_bus_new(&server));
+ ASSERT_OK(sd_bus_set_description(server, "server"));
+ ASSERT_OK(sd_bus_set_fd(server, s->fds[0], s->fds[0]));
+ ASSERT_OK(sd_bus_set_server(server, true, id));
+ ASSERT_OK(sd_bus_attach_event(server, sd_fiber_get_event(), 0));
+ ASSERT_OK(sd_bus_add_object_vtable(server, NULL, "/test", "test.Fiber", vtable, s->c));
+ ASSERT_OK(sd_bus_start(server));
+
+ ASSERT_OK(sd_bus_new(&client));
+ ASSERT_OK(sd_bus_set_description(client, "client"));
+ ASSERT_OK(sd_bus_set_fd(client, s->fds[1], s->fds[1]));
+ ASSERT_OK(sd_bus_attach_event(client, sd_fiber_get_event(), 0));
+ ASSERT_OK(sd_bus_start(client));
+
+ *ret_server = TAKE_PTR(server);
+ *ret_client = TAKE_PTR(client);
+ return 0;
+}
+
+static int call_concurrent_fiber(void *userdata) {
+ sd_bus *client = ASSERT_PTR(userdata);
+
+ /* A plain suspending sd_bus_call() — on a fiber this goes through sd_bus_call_suspend()
+ * which multiplexes onto the single client connection, so multiple caller fibers can have
+ * calls in flight at the same time. */
+ return sd_bus_call_method(client, NULL, "/test", "test.Fiber", "Concurrent",
+ NULL, NULL, NULL);
+}
+
+static int concurrency_fiber(void *userdata) {
+ Setup *s = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *f_a = NULL, *f_b = NULL;
+
+ ASSERT_OK(attach_pair(s, &server, &client));
+
+ /* Two concurrent calls on the shared client bus. Each lands in method_concurrent which
+ * sleeps 10ms; if fiber dispatch works the second is entered while the first is suspended,
+ * so max_in_flight on the context reaches 2. */
+ ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "call-a", call_concurrent_fiber, client,
+ /* destroy= */ NULL, &f_a));
+ ASSERT_OK(sd_fiber_new(sd_fiber_get_event(), "call-b", call_concurrent_fiber, client,
+ /* destroy= */ NULL, &f_b));
+
+ ASSERT_OK(sd_fiber_await(f_a));
+ ASSERT_OK(sd_fiber_await(f_b));
+
+ ASSERT_OK(sd_future_result(f_a));
+ ASSERT_OK(sd_future_result(f_b));
+ return 0;
+}
+
+TEST(fiber_method_concurrency) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ Context c = {};
+ Setup s = { .c = &c };
+
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, s.fds));
+
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ ASSERT_OK(sd_fiber_new(e, "concurrency", concurrency_fiber, &s, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(f));
+ ASSERT_GE(c.max_in_flight, 2);
+}
+
+static int errors_fiber(void *userdata) {
+ Setup *s = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *server = NULL, *client = NULL;
+
+ ASSERT_OK(attach_pair(s, &server, &client));
+
+ /* A fiber handler that returns a negative errno gets turned into a matching sd_bus error
+ * reply (bus_maybe_reply_error → sd_bus_reply_method_errno). */
+ _cleanup_(sd_bus_error_free) sd_bus_error e1 = SD_BUS_ERROR_NULL;
+ ASSERT_ERROR(sd_bus_call_method(client, NULL, "/test", "test.Fiber", "FailErrno",
+ &e1, NULL, NULL),
+ EACCES);
+ ASSERT_TRUE(sd_bus_error_has_name(&e1, SD_BUS_ERROR_ACCESS_DENIED));
+
+ /* A fiber handler that populates sd_bus_error directly propagates both name and message. */
+ _cleanup_(sd_bus_error_free) sd_bus_error e2 = SD_BUS_ERROR_NULL;
+ ASSERT_FAIL(sd_bus_call_method(client, NULL, "/test", "test.Fiber", "FailError",
+ &e2, NULL, NULL));
+ ASSERT_TRUE(sd_bus_error_has_name(&e2, SD_BUS_ERROR_INVALID_ARGS));
+ ASSERT_STREQ(e2.message, "bad arguments from fiber");
+
+ return 0;
+}
+
+TEST(fiber_method_errors) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ Context c = {};
+ Setup s = { .c = &c };
+
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, s.fds));
+
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ ASSERT_OK(sd_fiber_new(e, "errors", errors_fiber, &s, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(f));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd/sd-bus/test-bus-objects.c b/src/libsystemd/sd-bus/test-bus-objects.c
index 4ad60f0d58225..ac33086a6f374 100644
--- a/src/libsystemd/sd-bus/test-bus-objects.c
+++ b/src/libsystemd/sd-bus/test-bus-objects.c
@@ -1,8 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#include
-
#include "sd-bus.h"
+#include "sd-future.h"
#include "alloc-util.h"
#include "bus-internal.h"
@@ -211,9 +210,9 @@ static int enumerator3_callback(sd_bus *bus, const char *path, void *userdata, c
return 1;
}
-static void* server(void *p) {
- struct context *c = p;
- sd_bus *bus = NULL;
+static int server(void *userdata) {
+ struct context *c = ASSERT_PTR(userdata);
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
sd_id128_t id;
int r;
@@ -242,36 +241,25 @@ static void* server(void *p) {
log_error("Loop!");
r = sd_bus_process(bus, NULL);
- if (r < 0) {
- log_error_errno(r, "Failed to process requests: %m");
- goto fail;
- }
+ if (r < 0)
+ return log_error_errno(r, "Failed to process requests: %m");
if (r == 0) {
r = sd_bus_wait(bus, UINT64_MAX);
- if (r < 0) {
- log_error_errno(r, "Failed to wait: %m");
- goto fail;
- }
+ if (r < 0)
+ return log_error_errno(r, "Failed to wait: %m");
continue;
}
}
- r = 0;
-
-fail:
- if (bus) {
- sd_bus_flush(bus);
- sd_bus_unref(bus);
- }
-
- return INT_TO_PTR(r);
+ return 0;
}
-static int client(struct context *c) {
+static int client(void *p) {
+ struct context *c = ASSERT_PTR(p);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
- _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_strv_free_ char **lines = NULL;
const char *s;
@@ -575,16 +563,13 @@ static int client(struct context *c) {
ASSERT_OK(sd_bus_call_method(bus, "org.freedesktop.systemd.test", "/foo", "org.freedesktop.systemd.test", "Exit", &error, NULL, NULL));
- sd_bus_flush(bus);
-
return 0;
}
int main(int argc, char *argv[]) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
struct context c = {};
- pthread_t s;
- void *p;
- int r, q;
test_setup_logging(LOG_DEBUG);
@@ -593,21 +578,16 @@ int main(int argc, char *argv[]) {
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM, 0, c.fds));
- r = pthread_create(&s, NULL, server, &c);
- if (r != 0)
- return -r;
-
- r = client(&c);
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
- q = pthread_join(s, &p);
- if (q != 0)
- return -q;
+ ASSERT_OK(sd_fiber_new(e, "server", server, &c, /* destroy= */ NULL, &f_server));
+ ASSERT_OK(sd_fiber_new(e, "client", client, &c, /* destroy= */ NULL, &f_client));
- if (r < 0)
- return r;
+ ASSERT_OK(sd_event_loop(e));
- if (PTR_TO_INT(p) < 0)
- return PTR_TO_INT(p);
+ ASSERT_OK(sd_future_result(f_server));
+ ASSERT_OK(sd_future_result(f_client));
free(c.something);
free(c.automatic_string_property);
diff --git a/src/libsystemd/sd-bus/test-bus-peersockaddr.c b/src/libsystemd/sd-bus/test-bus-peersockaddr.c
index 2cac35dde4033..bee76c9b10ca7 100644
--- a/src/libsystemd/sd-bus/test-bus-peersockaddr.c
+++ b/src/libsystemd/sd-bus/test-bus-peersockaddr.c
@@ -1,9 +1,9 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#include
#include
#include "sd-bus.h"
+#include "sd-future.h"
#include "bus-dump.h"
#include "fd-util.h"
@@ -38,9 +38,9 @@ static bool gid_list_same(const gid_t *a, size_t n, const gid_t *b, size_t m) {
gid_list_contained(b, m, a, n);
}
-static void* server(void *p) {
+static int server(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_close_ int listen_fd = PTR_TO_INT(p), fd = -EBADF;
+ _cleanup_close_ int listen_fd = PTR_TO_INT(userdata), fd = -EBADF;
_cleanup_(sd_bus_creds_unrefp) sd_bus_creds *c = NULL;
_cleanup_free_ char *our_comm = NULL;
sd_id128_t id;
@@ -48,7 +48,7 @@ static void* server(void *p) {
ASSERT_OK(sd_id128_randomize(&id));
- ASSERT_OK_ERRNO(fd = accept4(listen_fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK));
+ ASSERT_OK(fd = sd_fiber_accept(listen_fd, NULL, NULL, SOCK_CLOEXEC|SOCK_NONBLOCK));
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_fd(bus, fd, fd));
@@ -114,17 +114,18 @@ static void* server(void *p) {
}
}
- return NULL;
+ return 0;
}
-static void* client(void *p) {
+static int client(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
const char *z;
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "wuffwuff"));
- ASSERT_OK(sd_bus_set_address(bus, p));
+ ASSERT_OK(sd_bus_set_address(bus, userdata));
+ ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_start(bus));
ASSERT_OK(sd_bus_call_method(bus, "foo.foo", "/foo", "foo.foo", "Foo", NULL, &reply, "s", "foo"));
@@ -132,17 +133,18 @@ static void* client(void *p) {
ASSERT_OK(sd_bus_message_read(reply, "s", &z));
ASSERT_STREQ(z, "bar");
- return NULL;
+ return 0;
}
TEST(description) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
_cleanup_free_ char *a = NULL;
_cleanup_close_ int fd = -EBADF;
union sockaddr_union sa = {
.un.sun_family = AF_UNIX,
};
socklen_t salen;
- pthread_t s, c;
ASSERT_OK_ERRNO(fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0));
ASSERT_OK_ERRNO(bind(fd, &sa.sa, offsetof(struct sockaddr_un, sun_path))); /* force auto-bind */
@@ -155,13 +157,18 @@ TEST(description) {
ASSERT_OK(asprintf(&a, "unix:abstract=%s", sa.un.sun_path + 1));
- ASSERT_OK(-pthread_create(&s, NULL, server, INT_TO_PTR(fd)));
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ ASSERT_OK(sd_fiber_new(e, "server", server, INT_TO_PTR(fd), /* destroy= */ NULL, &f_server));
TAKE_FD(fd);
- ASSERT_OK(-pthread_create(&c, NULL, client, a));
+ ASSERT_OK(sd_fiber_new(e, "client", client, a, /* destroy= */ NULL, &f_client));
+
+ ASSERT_OK(sd_event_loop(e));
- ASSERT_OK(-pthread_join(s, NULL));
- ASSERT_OK(-pthread_join(c, NULL));
+ ASSERT_OK(sd_future_result(f_server));
+ ASSERT_OK(sd_future_result(f_client));
}
DEFINE_TEST_MAIN(LOG_INFO);
diff --git a/src/libsystemd/sd-bus/test-bus-server.c b/src/libsystemd/sd-bus/test-bus-server.c
index 989d2bf10dcaa..1edcec858f2ac 100644
--- a/src/libsystemd/sd-bus/test-bus-server.c
+++ b/src/libsystemd/sd-bus/test-bus-server.c
@@ -1,10 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#include
#include
#include "sd-bus.h"
+#include "sd-event.h"
+#include "sd-future.h"
+#include "errno-util.h"
#include "log.h"
#include "memory-util.h"
#include "string-util.h"
@@ -20,7 +22,8 @@ struct context {
bool server_anonymous_auth;
};
-static int _server(struct context *c) {
+static int server(void *userdata) {
+ struct context *c = ASSERT_PTR(userdata);
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
sd_id128_t id;
bool quit = false;
@@ -29,6 +32,7 @@ static int _server(struct context *c) {
ASSERT_OK(sd_id128_randomize(&id));
ASSERT_OK(sd_bus_new(&bus));
+ ASSERT_OK(sd_bus_set_description(bus, "server"));
ASSERT_OK(sd_bus_set_fd(bus, c->fds[0], c->fds[0]));
ASSERT_OK(sd_bus_set_server(bus, 1, id));
ASSERT_OK(sd_bus_set_anonymous(bus, c->server_anonymous_auth));
@@ -74,17 +78,16 @@ static int _server(struct context *c) {
return 0;
}
-static void* server(void *p) {
- return INT_TO_PTR(_server(p));
-}
-
-static int client(struct context *c) {
+static int client(void *userdata) {
+ struct context *c = ASSERT_PTR(userdata);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL, *reply = NULL;
- _cleanup_(sd_bus_unrefp) sd_bus *bus = NULL;
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
ASSERT_OK(sd_bus_new(&bus));
+ ASSERT_OK(sd_bus_set_description(bus, "client"));
ASSERT_OK(sd_bus_set_fd(bus, c->fds[1], c->fds[1]));
+ ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_negotiate_fds(bus, c->client_negotiate_unix_fds));
ASSERT_OK(sd_bus_set_anonymous(bus, c->client_anonymous_auth));
ASSERT_OK(sd_bus_start(bus));
@@ -103,10 +106,10 @@ static int client(struct context *c) {
static int test_one(bool client_negotiate_unix_fds, bool server_negotiate_unix_fds,
bool client_anonymous_auth, bool server_anonymous_auth) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client = NULL;
struct context c;
- pthread_t s;
- void *p;
- int r, q;
+ int r = 0;
zero(c);
@@ -117,23 +120,18 @@ static int test_one(bool client_negotiate_unix_fds, bool server_negotiate_unix_f
c.client_anonymous_auth = client_anonymous_auth;
c.server_anonymous_auth = server_anonymous_auth;
- r = pthread_create(&s, NULL, server, &c);
- if (r != 0)
- return -r;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
- r = client(&c);
+ ASSERT_OK(sd_fiber_new(e, "server", server, &c, /* destroy= */ NULL, &f_server));
+ ASSERT_OK(sd_fiber_new(e, "client", client, &c, /* destroy= */ NULL, &f_client));
- q = pthread_join(s, &p);
- if (q != 0)
- return -q;
+ ASSERT_OK(sd_event_loop(e));
- if (r < 0)
- return r;
+ RET_GATHER(r, sd_future_result(f_client));
+ RET_GATHER(r, sd_future_result(f_server));
- if (PTR_TO_INT(p) < 0)
- return PTR_TO_INT(p);
-
- return 0;
+ return r;
}
int main(int argc, char *argv[]) {
@@ -145,7 +143,7 @@ int main(int argc, char *argv[]) {
ASSERT_OK(test_one(false, false, false, false));
ASSERT_OK(test_one(true, true, true, true));
ASSERT_OK(test_one(true, true, false, true));
- ASSERT_ERROR(test_one(true, true, true, false), EPERM);
+ ASSERT_ERROR(test_one(true, true, true, false), EACCES);
return EXIT_SUCCESS;
}
diff --git a/src/libsystemd/sd-bus/test-bus-watch-bind.c b/src/libsystemd/sd-bus/test-bus-watch-bind.c
index 1bf4ee7017119..6561633b8a823 100644
--- a/src/libsystemd/sd-bus/test-bus-watch-bind.c
+++ b/src/libsystemd/sd-bus/test-bus-watch-bind.c
@@ -1,10 +1,8 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#include
-#include
-
#include "sd-bus.h"
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-id128.h"
#include "alloc-util.h"
@@ -44,33 +42,33 @@ static const sd_bus_vtable vtable[] = {
SD_BUS_VTABLE_END,
};
-static void* thread_server(void *p) {
+static int server(void *userdata) {
_cleanup_free_ char *suffixed = NULL, *suffixed_basename = NULL, *suffixed2 = NULL, *d = NULL;
_cleanup_close_ int fd = -EBADF;
union sockaddr_union u;
- const char *path = p;
+ const char *path = ASSERT_PTR(userdata);
int r;
log_debug("Initializing server");
/* Let's play some games, by slowly creating the socket directory, and renaming it in the middle */
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(mkdir_parents(path, 0755));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(path_extract_directory(path, &d));
ASSERT_OK(asprintf(&suffixed, "%s.%" PRIx64, d, random_u64()));
ASSERT_OK_ERRNO(rename(d, suffixed));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(asprintf(&suffixed2, "%s.%" PRIx64, d, random_u64()));
ASSERT_OK_ERRNO(symlink(suffixed2, d));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(path_extract_filename(suffixed, &suffixed_basename));
ASSERT_OK_ERRNO(symlink(suffixed_basename, suffixed2));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
socklen_t sa_len;
r = sockaddr_un_set_path(&u.un, path);
@@ -81,13 +79,13 @@ static void* thread_server(void *p) {
ASSERT_OK_ERRNO(fd);
ASSERT_OK_ERRNO(bind(fd, &u.sa, sa_len));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK_ERRNO(listen(fd, SOMAXCONN_DELUXE));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
ASSERT_OK(touch(path));
- usleep_safe(100 * USEC_PER_MSEC);
+ ASSERT_OK(sd_fiber_sleep(100 * USEC_PER_MSEC));
log_debug("Initialized server");
@@ -101,8 +99,7 @@ static void* thread_server(void *p) {
ASSERT_OK(sd_event_new(&event));
- bus_fd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC);
- ASSERT_OK_ERRNO(bus_fd);
+ ASSERT_OK(bus_fd = sd_fiber_accept(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC));
log_debug("Accepted server connection");
@@ -129,13 +126,13 @@ static void* thread_server(void *p) {
log_debug("Server done");
- return NULL;
+ return 0;
}
-static void* thread_client1(void *p) {
+static int client1(void *userdata) {
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- const char *path = p, *t;
+ const char *path = ASSERT_PTR(userdata), *t;
log_debug("Initializing client1");
@@ -151,59 +148,65 @@ static void* thread_client1(void *p) {
log_debug("Client1 done");
- return NULL;
-}
-
-static int client2_callback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) {
- ASSERT_OK_ZERO(sd_bus_message_is_method_error(m, NULL));
- ASSERT_OK(sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), 0));
return 0;
}
-static void* thread_client2(void *p) {
+static int client2(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
- _cleanup_(sd_event_unrefp) sd_event *event = NULL;
- const char *path = p, *t;
+ const char *path = ASSERT_PTR(userdata), *t;
log_debug("Initializing client2");
- ASSERT_OK(sd_event_new(&event));
ASSERT_OK(sd_bus_new(&bus));
ASSERT_OK(sd_bus_set_description(bus, "client2"));
t = strjoina("unix:path=", path);
ASSERT_OK(sd_bus_set_address(bus, t));
ASSERT_OK(sd_bus_set_watch_bind(bus, true));
- ASSERT_OK(sd_bus_attach_event(bus, event, 0));
+ ASSERT_OK(sd_bus_attach_event(bus, sd_fiber_get_event(), 0));
ASSERT_OK(sd_bus_start(bus));
- ASSERT_OK(sd_bus_call_method_async(bus, NULL, "foo.bar", "/foo", "foo.TestInterface", "Foobar", client2_callback, NULL, NULL));
-
- ASSERT_OK(sd_event_loop(event));
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
+ ASSERT_OK(sd_bus_call_method(bus, "foo.bar", "/foo", "foo.TestInterface", "Foobar", NULL, &m, NULL));
+ ASSERT_OK_ZERO(sd_bus_message_is_method_error(m, NULL));
log_debug("Client2 done");
- return NULL;
+ return 0;
}
-static void request_exit(const char *path) {
+typedef struct RequestExitArgs {
+ const char *path;
+ sd_future *client1;
+ sd_future *client2;
+} RequestExitArgs;
+
+static int request_exit(void *userdata) {
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ RequestExitArgs *args = ASSERT_PTR(userdata);
const char *t;
+ /* Wait for all client fibers to complete before requesting exit */
+ ASSERT_OK(sd_fiber_await(args->client1));
+ ASSERT_OK(sd_fiber_await(args->client2));
+
ASSERT_OK(sd_bus_new(&bus));
- t = strjoina("unix:path=", path);
+ t = strjoina("unix:path=", args->path);
ASSERT_OK(sd_bus_set_address(bus, t));
ASSERT_OK(sd_bus_set_watch_bind(bus, true));
ASSERT_OK(sd_bus_set_description(bus, "request-exit"));
ASSERT_OK(sd_bus_start(bus));
ASSERT_OK(sd_bus_call_method(bus, "foo.bar", "/foo", "foo.TestInterface", "Exit", NULL, NULL, NULL));
+
+ return 0;
}
int main(int argc, char *argv[]) {
_cleanup_(rm_rf_physical_and_freep) char *d = NULL;
- pthread_t server, client1, client2;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f_server = NULL, *f_client1 = NULL, *f_client2 = NULL, *f_exit = NULL;
char *path;
test_setup_logging(LOG_DEBUG);
@@ -214,16 +217,27 @@ int main(int argc, char *argv[]) {
path = strjoina(d, "/this/is/a/socket");
- ASSERT_OK(-pthread_create(&server, NULL, thread_server, path));
- ASSERT_OK(-pthread_create(&client1, NULL, thread_client1, path));
- ASSERT_OK(-pthread_create(&client2, NULL, thread_client2, path));
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
- ASSERT_OK(-pthread_join(client1, NULL));
- ASSERT_OK(-pthread_join(client2, NULL));
+ ASSERT_OK(sd_fiber_new(e, "server", server, path, /* destroy= */ NULL, &f_server));
- request_exit(path);
+ ASSERT_OK(sd_fiber_new(e, "client-1", client1, path, /* destroy= */ NULL, &f_client1));
+ ASSERT_OK(sd_fiber_new(e, "client-2", client2, path, /* destroy= */ NULL, &f_client2));
- ASSERT_OK(-pthread_join(server, NULL));
+ RequestExitArgs args = {
+ .path = path,
+ .client1 = f_client1,
+ .client2 = f_client2,
+ };
+ ASSERT_OK(sd_fiber_new(e, "request-exit", request_exit, &args, /* destroy= */ NULL, &f_exit));
- return 0;
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(f_client1));
+ ASSERT_OK(sd_future_result(f_client2));
+ ASSERT_OK(sd_future_result(f_exit));
+ ASSERT_OK(sd_future_result(f_server));
+
+ return EXIT_SUCCESS;
}
diff --git a/src/libsystemd/sd-common/sd-forward.h b/src/libsystemd/sd-common/sd-forward.h
index 8abe655209dec..2d8b7646da928 100644
--- a/src/libsystemd/sd-common/sd-forward.h
+++ b/src/libsystemd/sd-common/sd-forward.h
@@ -127,3 +127,10 @@ typedef struct sd_resolve sd_resolve;
typedef struct sd_resolve_query sd_resolve_query;
typedef struct sd_hwdb sd_hwdb;
+
+typedef struct sd_future sd_future;
+typedef struct sd_promise sd_promise;
+
+typedef int (*sd_future_func_t)(sd_future *f);
+typedef int (*sd_fiber_func_t)(void *userdata);
+typedef void (*sd_fiber_destroy_t)(void *userdata);
diff --git a/src/libsystemd/sd-device/device-enumerator.c b/src/libsystemd/sd-device/device-enumerator.c
index b3fe85a976167..d1a48defe906c 100644
--- a/src/libsystemd/sd-device/device-enumerator.c
+++ b/src/libsystemd/sd-device/device-enumerator.c
@@ -82,18 +82,13 @@ _public_ int sd_device_enumerator_new(sd_device_enumerator **ret) {
return 0;
}
-static void device_unref_many(sd_device **devices, size_t n) {
- assert(devices || n == 0);
-
- for (size_t i = 0; i < n; i++)
- sd_device_unref(devices[i]);
-}
+static DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_device*, sd_device_unref);
static void device_enumerator_unref_devices(sd_device_enumerator *enumerator) {
assert(enumerator);
hashmap_clear(enumerator->devices_by_syspath);
- device_unref_many(enumerator->devices, enumerator->n_devices);
+ sd_device_unref_array_clear(enumerator->devices, enumerator->n_devices);
enumerator->devices = mfree(enumerator->devices);
enumerator->n_devices = 0;
}
@@ -461,7 +456,7 @@ static int enumerator_sort_devices(sd_device_enumerator *enumerator) {
typesafe_qsort(devices + n_sorted, n - n_sorted, device_compare);
- device_unref_many(enumerator->devices, enumerator->n_devices);
+ sd_device_unref_array_clear(enumerator->devices, enumerator->n_devices);
enumerator->n_devices = n;
free_and_replace(enumerator->devices, devices);
@@ -470,7 +465,7 @@ static int enumerator_sort_devices(sd_device_enumerator *enumerator) {
return 0;
failed:
- device_unref_many(devices, n);
+ sd_device_unref_array_clear(devices, n);
free(devices);
return r;
}
diff --git a/src/libsystemd/sd-event/event-future.c b/src/libsystemd/sd-event/event-future.c
new file mode 100644
index 0000000000000..2c4b899a1f45b
--- /dev/null
+++ b/src/libsystemd/sd-event/event-future.c
@@ -0,0 +1,304 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "event-future.h"
+#include "event-util.h"
+#include "fd-util.h"
+
+typedef struct IoFuture {
+ sd_promise *promise;
+ sd_event_source *source;
+ uint32_t revents;
+} IoFuture;
+
+static void* io_future_free(void *impl) {
+ IoFuture *f = impl;
+ if (!f)
+ return NULL;
+
+ sd_event_source_unref(f->source);
+ return mfree(f);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(IoFuture*, io_future_free);
+
+static int io_future_cancel(void *impl) {
+ IoFuture *f = ASSERT_PTR(impl);
+ int r = sd_event_source_set_enabled(f->source, SD_EVENT_OFF);
+ RET_GATHER(r, sd_promise_resolve(f->promise, -ECANCELED));
+ return r;
+}
+
+static int io_future_set_priority(void *impl, int64_t priority) {
+ IoFuture *f = ASSERT_PTR(impl);
+ return sd_event_source_set_priority(f->source, priority);
+}
+
+static const sd_future_ops io_future_ops = {
+ .free = io_future_free,
+ .cancel = io_future_cancel,
+ .set_priority = io_future_set_priority,
+};
+
+static int io_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ IoFuture *f = ASSERT_PTR(userdata);
+ int r = 0;
+
+ f->revents = revents;
+
+ if (FLAGS_SET(revents, EPOLLERR)) {
+ int error = 0;
+ socklen_t len = sizeof(error);
+
+ r = RET_NERRNO(getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len));
+ if (r == -ENOTSOCK)
+ r = 0;
+ else if (r >= 0)
+ r = -error;
+ /* On any other getsockopt() error fall through and resolve the promise with that
+ * error so the waiting fiber wakes up rather than hanging forever. */
+ }
+
+ return sd_promise_resolve(f->promise, r);
+}
+
+int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret) {
+ int r;
+
+ assert(e);
+ assert(fd >= 0);
+ assert(ret);
+
+ if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
+ return -ECANCELED;
+
+ _cleanup_(io_future_freep) IoFuture *impl = new0(IoFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ /* Duplicate fd to avoid EEXIST from epoll when adding the same fd multiple times */
+ _cleanup_close_ int fd_copy = fcntl(fd, F_DUPFD_CLOEXEC, 3);
+ if (fd_copy < 0)
+ return -errno;
+
+ r = sd_event_add_io(e, &impl->source, fd_copy, events, io_handler, impl);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(impl->source, SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_io_fd_own(impl->source, true);
+ if (r < 0)
+ return r;
+
+ TAKE_FD(fd_copy);
+
+ if (sd_fiber_is_running()) {
+ r = sd_event_source_set_priority(impl->source, sd_fiber_get_priority());
+ if (r < 0)
+ return r;
+ }
+
+ sd_future *f = NULL;
+ r = sd_future_new(&io_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+ *ret = TAKE_PTR(f);
+ return 0;
+}
+
+typedef struct TimeFuture {
+ sd_promise *promise;
+ sd_event_source *source;
+ uint64_t usec;
+
+ /* Result the future resolves with on natural expiry (vs. cancellation). 0 for normal sleep,
+ * non-zero (e.g. -ETIMEDOUT) lets a fiber waiting on this future resume with that error. */
+ int result;
+} TimeFuture;
+
+static void* time_future_free(void *impl) {
+ TimeFuture *f = impl;
+ if (!f)
+ return NULL;
+
+ sd_event_source_unref(f->source);
+ return mfree(f);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(TimeFuture*, time_future_free);
+
+static int time_future_cancel(void *impl) {
+ TimeFuture *f = ASSERT_PTR(impl);
+ int r = sd_event_source_set_enabled(f->source, SD_EVENT_OFF);
+ RET_GATHER(r, sd_promise_resolve(f->promise, -ECANCELED));
+ return r;
+}
+
+static int time_future_set_priority(void *impl, int64_t priority) {
+ TimeFuture *f = ASSERT_PTR(impl);
+ return sd_event_source_set_priority(f->source, priority);
+}
+
+static const sd_future_ops time_future_ops = {
+ .free = time_future_free,
+ .cancel = time_future_cancel,
+ .set_priority = time_future_set_priority,
+};
+
+static int time_handler(sd_event_source *s, usec_t usec, void *userdata) {
+ TimeFuture *f = ASSERT_PTR(userdata);
+
+ f->usec = usec;
+ return sd_promise_resolve(f->promise, f->result);
+}
+
+int future_new_time(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret) {
+ int r;
+
+ assert(e);
+ assert(ret);
+
+ if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
+ return -ECANCELED;
+
+ _cleanup_(time_future_freep) TimeFuture *impl = new(TimeFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ *impl = (TimeFuture) {
+ .result = result,
+ };
+
+ r = sd_event_add_time(e, &impl->source, clock, usec, accuracy, time_handler, impl);
+ if (r < 0)
+ return r;
+
+ if (sd_fiber_is_running()) {
+ r = sd_event_source_set_priority(impl->source, sd_fiber_get_priority());
+ if (r < 0)
+ return r;
+ }
+
+ sd_future *f;
+ r = sd_future_new(&time_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+ *ret = TAKE_PTR(f);
+ return 0;
+}
+
+int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret) {
+ int r;
+
+ assert(e);
+ assert(ret);
+
+ if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
+ return -ECANCELED;
+
+ _cleanup_(time_future_freep) TimeFuture *impl = new(TimeFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ *impl = (TimeFuture) {
+ .result = result,
+ };
+
+ r = sd_event_add_time_relative(e, &impl->source, clock, usec, accuracy, time_handler, impl);
+ if (r < 0)
+ return r;
+
+ if (sd_fiber_is_running()) {
+ r = sd_event_source_set_priority(impl->source, sd_fiber_get_priority());
+ if (r < 0)
+ return r;
+ }
+
+ sd_future *f;
+ r = sd_future_new(&time_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+ *ret = TAKE_PTR(f);
+ return 0;
+}
+
+int event_run_suspend(sd_event *e, uint64_t timeout) {
+ int r;
+
+ assert(e);
+ assert(sd_fiber_is_running());
+ assert(sd_fiber_get_event());
+ assert(e != sd_fiber_get_event());
+
+ /* Make sure that none of the preparation callbacks ends up freeing the event source under our feet */
+ PROTECT_EVENT(e);
+
+ r = sd_event_prepare(e);
+ if (r < 0)
+ return r;
+ if (r == 0) {
+ r = sd_event_wait(e, 0);
+ if (r < 0)
+ return r;
+ }
+ if (r > 0) {
+ r = sd_event_dispatch(e);
+ if (r < 0)
+ return r;
+
+ return 1;
+ }
+
+ if (timeout == 0)
+ return 0;
+
+ int fd = sd_event_get_fd(e);
+ if (fd < 0)
+ return fd;
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
+ r = future_new_io(sd_fiber_get_event(), fd, EPOLLIN, &io);
+ if (r < 0)
+ return r;
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
+ if (timeout != USEC_INFINITY) {
+ r = future_new_time_relative(
+ sd_fiber_get_event(),
+ CLOCK_MONOTONIC,
+ timeout,
+ /* accuracy= */ 1,
+ /* result= */ 0,
+ &timer);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_fiber_suspend();
+ if (r < 0)
+ return r;
+
+ r = sd_event_prepare(e);
+ if (r == 0)
+ r = sd_event_wait(e, 0);
+ if (r > 0) {
+ r = sd_event_dispatch(e);
+ if (r < 0)
+ return r;
+
+ return 1;
+ }
+
+ return r;
+}
diff --git a/src/libsystemd/sd-event/event-future.h b/src/libsystemd/sd-event/event-future.h
new file mode 100644
index 0000000000000..83d5939d6b02d
--- /dev/null
+++ b/src/libsystemd/sd-event/event-future.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-forward.h"
+
+int future_new_io(sd_event *e, int fd, uint32_t events, sd_future **ret);
+int future_new_time(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret);
+int future_new_time_relative(sd_event *e, clockid_t clock, uint64_t usec, uint64_t accuracy, int result, sd_future **ret);
+
+int event_run_suspend(sd_event *e, uint64_t timeout);
diff --git a/src/libsystemd/sd-event/event-util.h b/src/libsystemd/sd-event/event-util.h
index dc3b3ed70ff12..ce213b9c9e4d9 100644
--- a/src/libsystemd/sd-event/event-util.h
+++ b/src/libsystemd/sd-event/event-util.h
@@ -5,6 +5,9 @@
#include "sd-forward.h"
+#define PROTECT_EVENT(e) \
+ _unused_ _cleanup_(sd_event_unrefp) sd_event *_ref = sd_event_ref(e);
+
extern const struct hash_ops event_source_hash_ops;
int event_reset_time(
diff --git a/src/libsystemd/sd-event/sd-event.c b/src/libsystemd/sd-event/sd-event.c
index 9e7ba7813cde9..95ecf8fe144b1 100644
--- a/src/libsystemd/sd-event/sd-event.c
+++ b/src/libsystemd/sd-event/sd-event.c
@@ -10,12 +10,15 @@
#include "sd-daemon.h"
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-id128.h"
#include "sd-messages.h"
#include "alloc-util.h"
#include "errno-util.h"
+#include "event-future.h"
#include "event-source.h"
+#include "event-util.h"
#include "fd-util.h"
#include "format-util.h"
#include "glyph-util.h"
@@ -474,9 +477,6 @@ _public_ sd_event* sd_event_unref(sd_event *e) {
return event_free(e);
}
-#define PROTECT_EVENT(e) \
- _unused_ _cleanup_(sd_event_unrefp) sd_event *_ref = sd_event_ref(e);
-
_public_ sd_event_source* sd_event_source_disable_unref(sd_event_source *s) {
int r;
@@ -4943,6 +4943,13 @@ _public_ int sd_event_run(sd_event *e, uint64_t timeout) {
assert_return(e->state != SD_EVENT_FINISHED, -ESTALE);
assert_return(e->state == SD_EVENT_INITIAL, -EBUSY);
+ /* When running on a fiber, delegate to the suspending implementation. Note that the
+ * profile_delays accounting below is intentionally skipped on that path: the suspending variant
+ * drives the event loop via sd_event_prepare()/sd_event_wait()/sd_event_dispatch() itself, which
+ * are the same primitives profile_delays tracks when called directly. */
+ if (sd_fiber_is_running())
+ return event_run_suspend(e, timeout);
+
if (e->profile_delays && e->last_run_usec != 0) {
usec_t this_run;
unsigned l;
diff --git a/src/libsystemd/sd-event/test-event-future.c b/src/libsystemd/sd-event/test-event-future.c
new file mode 100644
index 0000000000000..754daf0df2fc4
--- /dev/null
+++ b/src/libsystemd/sd-event/test-event-future.c
@@ -0,0 +1,358 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "fd-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+static int timer_callback(sd_event_source *s, uint64_t usec, void *userdata) {
+ int *count = ASSERT_PTR(userdata);
+ int r;
+
+ (*count)++;
+
+ r = sd_event_source_set_time_relative(s, 5 * USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+
+ if (sd_fiber_is_running() && *count >= 3)
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+
+ return 0;
+}
+
+static int event_run_fiber_func(void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *inner_timer = NULL;
+ int r;
+
+ /* Create inner event loop from within the fiber */
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ /* Add a timer to the inner event loop that fires every 5ms */
+ r = sd_event_add_time_relative(inner, &inner_timer, CLOCK_MONOTONIC,
+ 5 * USEC_PER_MSEC, 0, timer_callback,
+ userdata);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_enabled(inner_timer, SD_EVENT_ON);
+ if (r < 0)
+ return r;
+
+ return sd_event_loop(inner);
+}
+
+TEST(sd_event_loop_fiber) {
+ /* Create outer event loop for the fiber scheduler */
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ /* Add a timer to the outer event loop that fires every 5ms */
+ _cleanup_(sd_event_source_unrefp) sd_event_source *outer_timer = NULL;
+ int outer_timer_count = 0;
+ ASSERT_OK(sd_event_add_time_relative(outer, &outer_timer, CLOCK_MONOTONIC,
+ 5 * USEC_PER_MSEC, 0, timer_callback,
+ &outer_timer_count));
+
+ /* Create a fiber that will create and run the inner event loop */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int inner_timer_count = 0;
+ ASSERT_OK(sd_fiber_new(outer, "event-runner", event_run_fiber_func, &inner_timer_count, /* destroy= */ NULL, &f));
+
+ /* Run the outer event loop */
+ ASSERT_OK(sd_event_loop(outer));
+
+ /* Fiber should have completed successfully */
+ ASSERT_OK(sd_future_result(f));
+
+ /* Both timers should have fired at least once */
+ ASSERT_EQ(inner_timer_count, 3);
+ ASSERT_GT(outer_timer_count, 0);
+}
+
+static int event_run_fiber_timeout_func(void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ int r;
+
+ /* Create inner event loop from within the fiber */
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ /* Run with a short timeout - should timeout since there are no events */
+ return sd_event_run(inner, 10 * USEC_PER_MSEC);
+}
+
+TEST(sd_event_run_fiber_timeout) {
+ /* Create outer event loop for the fiber scheduler */
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ /* Create a fiber that will run sd_event_run() with timeout */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "event-timeout", event_run_fiber_timeout_func, NULL, /* destroy= */ NULL, &f));
+
+ /* Run the outer event loop */
+ ASSERT_OK(sd_event_loop(outer));
+
+ /* Fiber should have completed successfully (timeout returns 0) */
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: sd_event_run() with zero timeout returns immediately */
+static int sd_event_run_zero_timeout_fiber(void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ int r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ /* With zero timeout on an empty event loop, should return 0 immediately */
+ r = sd_event_run(inner, 0);
+ if (r != 0)
+ return r < 0 ? r : -EIO;
+
+ return 0;
+}
+
+TEST(sd_event_run_zero_timeout) {
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "run-suspend-zero", sd_event_run_zero_timeout_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(outer));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: sd_event_run() dispatches immediately pending IO */
+static int io_callback(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+ char buf[64];
+
+ (*counter)++;
+
+ /* Drain the fd */
+ (void) read(fd, buf, sizeof(buf));
+
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+}
+
+static int sd_event_run_immediate_fiber(void *userdata) {
+ int *pipefd = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
+ int counter = 0, r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ /* Add IO source watching the read end of the pipe */
+ r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
+ if (r < 0)
+ return r;
+
+ /* Data is already available on the pipe (written before fiber started), so
+ * sd_event_run() should dispatch immediately without suspending */
+ r = sd_event_run(inner, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ /* The IO callback should have fired */
+ if (counter != 1)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(sd_event_run_immediate) {
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ /* Write data before starting the fiber so it's immediately available */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1);
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "run-suspend-immediate", sd_event_run_immediate_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(outer));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: sd_event_run() with IO arriving during suspension */
+static int sd_event_run_io_fiber(void *userdata) {
+ int *pipefd = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
+ int counter = 0, r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
+ if (r < 0)
+ return r;
+
+ /* No data available yet, so this will suspend the fiber until IO arrives */
+ r = sd_event_run(inner, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ if (counter != 1)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(sd_event_run_io) {
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "run-suspend-io", sd_event_run_io_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ /* First iteration: fiber runs, adds IO source, suspends because no data */
+ ASSERT_OK_POSITIVE(sd_event_run(outer, 0));
+
+ /* Write data to the pipe to wake the inner event loop */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "Y", 1), 1);
+
+ /* Complete: fiber resumes, dispatches IO, finishes */
+ ASSERT_OK(sd_event_loop(outer));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: event_run called in a loop keeps event loop state consistent.
+ * This is a regression test for a bug where error paths after sd_event_prepare()
+ * could leave the inner event loop stuck in SD_EVENT_ARMED state. */
+static int sd_event_run_loop_fiber(void *userdata) {
+ int *pipefd = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
+ int counter = 0, r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_io(inner, &source, pipefd[0], EPOLLIN, io_callback, &counter);
+ if (r < 0)
+ return r;
+
+ /* Call sd_event_run() multiple times with short timeouts.
+ * Each call should leave the inner event loop in a clean state for the next call. */
+ for (int i = 0; i < 5; i++) {
+ r = sd_event_run(inner, 10 * USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ break;
+ }
+
+ /* After multiple timeouts, the event loop should still be usable.
+ * Write data and do one more run to verify. */
+ if (counter == 0) {
+ /* Data wasn't written yet, do a final run with longer timeout */
+ r = sd_event_run(inner, USEC_INFINITY);
+ if (r < 0)
+ return r;
+ }
+
+ if (counter != 1)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(sd_event_run_loop) {
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "run-suspend-loop", sd_event_run_loop_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ /* Let the fiber run through a few timeout iterations */
+ for (int i = 0; i < 10; i++)
+ ASSERT_OK(sd_event_run(outer, 50 * USEC_PER_MSEC));
+
+ /* Write data to unblock the fiber */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "Z", 1), 1);
+
+ ASSERT_OK(sd_event_loop(outer));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: sd_event_run() with an inner timer that fires during suspension */
+static int inner_timer_handler(sd_event_source *s, uint64_t usec, void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+ (*counter)++;
+ return sd_event_exit(sd_event_source_get_event(s), 0);
+}
+
+static int sd_event_run_timer_fiber(void *userdata) {
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_event_source_unrefp) sd_event_source *source = NULL;
+ int counter = 0, r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ /* Add a timer that fires after 10ms */
+ r = sd_event_add_time_relative(inner, &source, CLOCK_MONOTONIC,
+ 10 * USEC_PER_MSEC, 0, inner_timer_handler,
+ &counter);
+ if (r < 0)
+ return r;
+
+ /* Should suspend, then resume when the timer fires */
+ r = sd_event_run(inner, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ if (counter != 1)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(sd_event_run_timer) {
+ _cleanup_(sd_event_unrefp) sd_event *outer = NULL;
+ ASSERT_OK(sd_event_new(&outer));
+ ASSERT_OK(sd_event_set_exit_on_idle(outer, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(outer, "run-suspend-timer", sd_event_run_timer_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(outer));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd/sd-future/fiber-io.c b/src/libsystemd/sd-future/fiber-io.c
new file mode 100644
index 0000000000000..ebd4ccbfebb98
--- /dev/null
+++ b/src/libsystemd/sd-future/fiber-io.c
@@ -0,0 +1,422 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include /* IWYU pragma: keep */
+#include
+#include
+#include
+#include
+
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "event-future.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "socket-util.h"
+
+typedef ssize_t (*FiberIOFunc)(int fd, void *args);
+
+static ssize_t fiber_io_operation(int fd, uint32_t events, FiberIOFunc func, void *args) {
+ _cleanup_(nonblock_resetp) int reset_fd = -EBADF;
+ int r;
+
+ assert(fd >= 0);
+ assert(func);
+
+ if (!sd_fiber_is_running())
+ return func(fd, args);
+
+ assert(sd_fiber_get_event());
+
+ r = fd_nonblock(fd, true);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ reset_fd = fd;
+
+ ssize_t n = func(fd, args);
+ if (n >= 0 || !ERRNO_IS_NEG_TRANSIENT(n))
+ return n;
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
+ r = future_new_io(sd_fiber_get_event(), fd, events, &io);
+ if (r < 0)
+ return r;
+
+ r = sd_fiber_suspend();
+ if (r < 0)
+ return r;
+
+ return func(fd, args);
+}
+
+typedef struct ReadArgs {
+ void *buf;
+ size_t count;
+} ReadArgs;
+
+static ssize_t read_callback(int fd, void *args) {
+ ReadArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = read(fd, a->buf, a->count);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_read(int fd, void *buf, size_t count) {
+ assert(fd >= 0);
+ assert(buf || count == 0);
+
+ return fiber_io_operation(fd, EPOLLIN, read_callback, &(ReadArgs) {
+ .buf = buf,
+ .count = count,
+ });
+}
+
+typedef struct WriteArgs {
+ const void *buf;
+ size_t count;
+} WriteArgs;
+
+static ssize_t write_callback(int fd, void *args) {
+ WriteArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = write(fd, a->buf, a->count);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_write(int fd, const void *buf, size_t count) {
+ assert(fd >= 0);
+ assert(buf || count == 0);
+
+ return fiber_io_operation(fd, EPOLLOUT, write_callback, &(WriteArgs) {
+ .buf = buf,
+ .count = count,
+ });
+}
+
+typedef struct ReadvArgs {
+ const struct iovec *iov;
+ int iovcnt;
+} ReadvArgs;
+
+static ssize_t readv_callback(int fd, void *args) {
+ ReadvArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = readv(fd, a->iov, a->iovcnt);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_readv(int fd, const struct iovec *iov, int iovcnt) {
+ assert(fd >= 0);
+ assert(iov || iovcnt == 0);
+
+ return fiber_io_operation(fd, EPOLLIN, readv_callback, &(ReadvArgs) {
+ .iov = iov,
+ .iovcnt = iovcnt,
+ });
+}
+
+typedef struct WritevArgs {
+ const struct iovec *iov;
+ int iovcnt;
+} WritevArgs;
+
+static ssize_t writev_callback(int fd, void *args) {
+ WritevArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = writev(fd, a->iov, a->iovcnt);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_writev(int fd, const struct iovec *iov, int iovcnt) {
+ assert(fd >= 0);
+ assert(iov || iovcnt == 0);
+
+ return fiber_io_operation(fd, EPOLLOUT, writev_callback, &(WritevArgs) {
+ .iov = iov,
+ .iovcnt = iovcnt,
+ });
+}
+
+typedef struct RecvArgs {
+ void *buf;
+ size_t len;
+ int flags;
+} RecvArgs;
+
+static ssize_t recv_callback(int fd, void *args) {
+ RecvArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = recv(fd, a->buf, a->len, a->flags);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_recv(int sockfd, void *buf, size_t len, int flags) {
+ assert(sockfd >= 0);
+ assert(buf || len == 0);
+
+ return fiber_io_operation(sockfd, EPOLLIN, recv_callback, &(RecvArgs) {
+ .buf = buf,
+ .len = len,
+ .flags = flags,
+ });
+}
+
+typedef struct SendArgs {
+ const void *buf;
+ size_t len;
+ int flags;
+} SendArgs;
+
+static ssize_t send_callback(int fd, void *args) {
+ SendArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = send(fd, a->buf, a->len, a->flags);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_send(int sockfd, const void *buf, size_t len, int flags) {
+ assert(sockfd >= 0);
+ assert(buf || len == 0);
+
+ return fiber_io_operation(sockfd, EPOLLOUT, send_callback, &(SendArgs) {
+ .buf = buf,
+ .len = len,
+ .flags = flags,
+ });
+}
+
+int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
+ _cleanup_(nonblock_resetp) int reset_fd = -EBADF;
+ int r;
+
+ assert(sockfd >= 0);
+ assert(addr);
+
+ if (!sd_fiber_is_running())
+ return RET_NERRNO(connect(sockfd, addr, addrlen));
+
+ assert(sd_fiber_get_event());
+
+ r = fd_nonblock(sockfd, true);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ reset_fd = sockfd;
+
+ r = RET_NERRNO(connect(sockfd, addr, addrlen));
+ if (r != -EINPROGRESS)
+ return r;
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *io = NULL;
+ r = future_new_io(sd_fiber_get_event(), sockfd, EPOLLOUT, &io);
+ if (r < 0)
+ return r;
+
+ return sd_fiber_suspend();
+}
+
+typedef struct RecvmsgArgs {
+ struct msghdr *msg;
+ int flags;
+} RecvmsgArgs;
+
+static ssize_t recvmsg_callback(int fd, void *args) {
+ RecvmsgArgs *a = ASSERT_PTR(args);
+
+ return recvmsg_safe(fd, a->msg, a->flags);
+}
+
+ssize_t sd_fiber_recvmsg(int sockfd, struct msghdr *msg, int flags) {
+ assert(sockfd >= 0);
+ assert(msg);
+
+ return fiber_io_operation(sockfd, EPOLLIN, recvmsg_callback, &(RecvmsgArgs) {
+ .msg = msg,
+ .flags = flags,
+ });
+}
+
+typedef struct SendmsgArgs {
+ const struct msghdr *msg;
+ int flags;
+} SendmsgArgs;
+
+static ssize_t sendmsg_callback(int fd, void *args) {
+ SendmsgArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = sendmsg(fd, a->msg, a->flags);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_sendmsg(int sockfd, const struct msghdr *msg, int flags) {
+ assert(sockfd >= 0);
+ assert(msg);
+
+ return fiber_io_operation(sockfd, EPOLLOUT, sendmsg_callback, &(SendmsgArgs) {
+ .msg = msg,
+ .flags = flags,
+ });
+}
+
+typedef struct RecvfromArgs {
+ void *buf;
+ size_t len;
+ int flags;
+ struct sockaddr *src_addr;
+ socklen_t *addrlen;
+} RecvfromArgs;
+
+static ssize_t recvfrom_callback(int fd, void *args) {
+ RecvfromArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = recvfrom(fd, a->buf, a->len, a->flags, a->src_addr, a->addrlen);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) {
+ assert(sockfd >= 0);
+ assert(buf || len == 0);
+
+ return fiber_io_operation(sockfd, EPOLLIN, recvfrom_callback, &(RecvfromArgs) {
+ .buf = buf,
+ .len = len,
+ .flags = flags,
+ .src_addr = src_addr,
+ .addrlen = addrlen,
+ });
+}
+
+typedef struct SendtoArgs {
+ const void *buf;
+ size_t len;
+ int flags;
+ const struct sockaddr *dest_addr;
+ socklen_t addrlen;
+} SendtoArgs;
+
+static ssize_t sendto_callback(int fd, void *args) {
+ SendtoArgs *a = ASSERT_PTR(args);
+ ssize_t n;
+
+ n = sendto(fd, a->buf, a->len, a->flags, a->dest_addr, a->addrlen);
+ return n >= 0 ? n : -errno;
+}
+
+ssize_t sd_fiber_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) {
+ assert(sockfd >= 0);
+ assert(buf || len == 0);
+
+ return fiber_io_operation(sockfd, EPOLLOUT, sendto_callback, &(SendtoArgs) {
+ .buf = buf,
+ .len = len,
+ .flags = flags,
+ .dest_addr = dest_addr,
+ .addrlen = addrlen,
+ });
+}
+
+typedef struct AcceptArgs {
+ struct sockaddr *addr;
+ socklen_t *addrlen;
+ int flags;
+} AcceptArgs;
+
+static ssize_t accept_callback(int fd, void *args) {
+ AcceptArgs *a = ASSERT_PTR(args);
+
+ return RET_NERRNO(accept4(fd, a->addr, a->addrlen, a->flags));
+}
+
+int sd_fiber_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags) {
+ assert(sockfd >= 0);
+
+ return fiber_io_operation(sockfd, EPOLLIN, accept_callback, &(AcceptArgs) {
+ .addr = addr,
+ .addrlen = addrlen,
+ .flags = flags,
+ });
+}
+
+int sd_fiber_poll(struct pollfd *fds, size_t n_fds, uint64_t timeout) {
+ int r;
+
+ assert(fds || n_fds == 0);
+
+ if (!sd_fiber_is_running())
+ return ppoll_usec(fds, n_fds, timeout);
+
+ assert(sd_fiber_get_event());
+
+ /* No fds to wait on and no timeout means there's nothing that could ever wake the fiber up,
+ * since unlike raw ppoll() we cannot use signal delivery as a wakeup. Signals received while
+ * the fiber is suspended are handled by sd-event via signalfd, in which case the signal handler
+ * is expected to cancel the fiber via sd_future_cancel() if a wakeup is desired. */
+ if (n_fds == 0 && timeout == USEC_INFINITY)
+ return -EINVAL;
+
+ /* Try polling with zero timeout first to see if any are immediately ready. This also handles
+ * the n_fds == 0 && timeout == 0 case (returns 0 via ppoll_usec()'s own fast path). */
+ r = ppoll_usec(fds, n_fds, /* timeout= */ 0);
+ if (timeout == 0 || r != 0) /* Either error or some fds are ready */
+ return r;
+
+ sd_future **futures = NULL;
+ CLEANUP_ARRAY(futures, n_fds, sd_future_cancel_wait_unref_array);
+
+ futures = new0(sd_future*, n_fds);
+ if (!futures)
+ return -ENOMEM;
+
+ /* Set up I/O event sources for all valid fds */
+ for (size_t i = 0; i < n_fds; i++) {
+ if (fds[i].fd < 0)
+ continue;
+
+ uint32_t events = 0;
+ if (fds[i].events & POLLIN)
+ events |= EPOLLIN;
+ if (fds[i].events & POLLOUT)
+ events |= EPOLLOUT;
+ if (fds[i].events & POLLPRI)
+ events |= EPOLLPRI;
+ if (fds[i].events & POLLRDHUP)
+ events |= EPOLLRDHUP;
+
+ if (events == 0)
+ continue;
+
+ r = future_new_io(sd_fiber_get_event(), fds[i].fd, events, &futures[i]);
+ if (r < 0)
+ return r;
+ }
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
+ if (timeout != USEC_INFINITY) {
+ r = future_new_time_relative(
+ sd_fiber_get_event(),
+ CLOCK_MONOTONIC,
+ timeout,
+ /* accuracy= */ 1,
+ /* result= */ 0,
+ &timer);
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_fiber_suspend();
+ if (r < 0)
+ return r;
+
+ return ppoll_usec(fds, n_fds, /* timeout= */ 0);
+}
diff --git a/src/libsystemd/sd-future/fiber.c b/src/libsystemd/sd-future/fiber.c
new file mode 100644
index 0000000000000..993032a993817
--- /dev/null
+++ b/src/libsystemd/sd-future/fiber.c
@@ -0,0 +1,625 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+#include
+#include
+
+#include
+
+#if HAVE_VALGRIND_VALGRIND_H
+#include
+#endif
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "event-future.h"
+#include "fiber-def.h"
+#include "log-context.h"
+#include "log.h"
+#include "memory-util.h"
+#include "time-util.h"
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+#include
+#endif
+
+#define FIBER_DEFAULT_STACK_SIZE UINT64_C(8388608)
+
+static int fiber_allocate_stack(size_t size, void **ret) {
+ void *stack = NULL;
+ int r;
+
+ assert(size > 0 && size % page_size() == 0);
+ assert(ret);
+
+ stack = mmap(/* addr= */ NULL, size,
+ PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
+ /* fd= */ -1, /* offset= */ 0);
+ if (stack == MAP_FAILED)
+ return -errno;
+
+ /* Set up guard page at the bottom of the stack (grows downward) */
+ r = RET_NERRNO(mprotect(stack, page_size(), PROT_NONE));
+ if (r < 0) {
+ (void) munmap(stack, size);
+ return r;
+ }
+
+ *ret = TAKE_PTR(stack);
+ return 0;
+}
+
+static void fiber_entry_point(void) {
+ Fiber *f = fiber_get_current();
+
+ assert(f);
+ assert(f->func);
+ assert(IN_SET(f->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED));
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we've switched from the main stack to a fiber. */
+ __sanitizer_finish_switch_fiber(NULL, NULL, NULL);
+#endif
+
+ LOG_SET_PREFIX(f->name);
+ LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", f->name);
+
+ f->result = f->state == FIBER_STATE_CANCELLED ? -ECANCELED : f->func(f->userdata);
+ f->state = FIBER_STATE_COMPLETED;
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we're switching back to the caller's stack from the completed fiber. When a
+ * fiber finishes we have to pass NULL as the first argument to destroy the fake stack. */
+ __sanitizer_start_switch_fiber(NULL, f->resume_context.uc_stack.ss_sp, f->resume_context.uc_stack.ss_size);
+#endif
+}
+
+static int fiber_makecontext(Fiber *f, const void *stack, size_t size) {
+ assert(f);
+
+ if (getcontext(&f->context) < 0)
+ return -errno;
+
+ f->context.uc_stack.ss_sp = (uint8_t*) stack + page_size();
+ f->context.uc_stack.ss_size = size - page_size();
+ /* When the fiber's entry function returns without explicitly yielding, ucontext resumes
+ * execution at uc_link. We populate resume_context inside fiber_run() each time we enter the
+ * fiber, so this reference is live whenever the fiber is running. Keeping the resume context
+ * per-fiber (rather than a thread-global) is what makes nested fiber_run() calls — e.g. a bus
+ * method dispatched as a fiber handler while sd_event_loop() itself runs in a fiber — safe. */
+ f->context.uc_link = &f->resume_context;
+ makecontext(&f->context, fiber_entry_point, 0);
+
+ return 0;
+}
+
+static void reset_current_fiber(void) {
+ fiber_set_current(NULL);
+}
+
+static sd_event_source* fiber_current_event_source(Fiber *f) {
+ assert(f);
+ assert(f->state != FIBER_STATE_COMPLETED);
+ assert(f->event);
+
+ return sd_event_get_state(f->event) == SD_EVENT_EXITING ? f->exit_event_source : f->defer_event_source;
+}
+
+static int atfork_ret = 0;
+
+static void install_atfork(void) {
+ /* __register_atfork() either returns 0 or -ENOMEM, in its glibc implementation. Since it's
+ * only half-documented (glibc doesn't document it but LSB does — though only superficially)
+ * we'll check for errors only in the most generic fashion possible. */
+ atfork_ret = pthread_atfork(NULL, NULL, reset_current_fiber);
+}
+
+static void fiber_resolve(Fiber *f) {
+ assert(f);
+
+ f->defer_event_source = sd_event_source_disable_unref(f->defer_event_source);
+ f->exit_event_source = sd_event_source_disable_unref(f->exit_event_source);
+ /* The floating self-ref (if any) is potentially the last ref keeping the fiber alive — moving it
+ * into a local _cleanup_ slot ensures sd_promise_resolve() runs callbacks and waiters while f is
+ * still valid; the local's cleanup drops the ref afterwards, at which point no further f->...
+ * access can happen. */
+ _unused_ _cleanup_(sd_future_unrefp) sd_future *floating = TAKE_PTR(f->floating);
+ sd_promise_resolve(f->promise, f->result);
+}
+
+static int fiber_run(Fiber *f) {
+ int r;
+
+ assert(f);
+
+ if (f->state == FIBER_STATE_COMPLETED)
+ return -ESTALE;
+
+ assert(IN_SET(f->state, FIBER_STATE_INITIAL, FIBER_STATE_READY, FIBER_STATE_CANCELLED));
+
+ static pthread_once_t atfork_once = PTHREAD_ONCE_INIT;
+ r = pthread_once(&atfork_once, install_atfork);
+ if (r != 0)
+ return -r;
+ if (atfork_ret != 0)
+ return -atfork_ret;
+
+ LOG_SET_PREFIX(f->name);
+ LOG_CONTEXT_PUSH_KEY_VALUE("FIBER=", f->name);
+
+ log_debug("Scheduling fiber");
+
+ /* Save the previously-current fiber (if any) so we can restore it when this fiber yields or
+ * completes. This matters when fiber_run() is invoked from within another fiber (e.g. an
+ * sd-event dispatch that happens to be running inside a fiber context itself): the
+ * LOG_SET_PREFIX/LOG_CONTEXT_PUSH above attached to whichever fiber was current at that moment,
+ * and their scope-level cleanup must see the same fiber_get_current() when it runs to detach
+ * them from the correct list. */
+ Fiber *prev = fiber_get_current();
+ fiber_set_current(f);
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we're switching to the fiber's stack. */
+ void *fake_stack;
+ __sanitizer_start_switch_fiber(&fake_stack, f->context.uc_stack.ss_sp, f->context.uc_stack.ss_size);
+#endif
+
+ /* This looks innocent but this is where we start executing the fiber. Once it yields, we continue
+ * here as if nothing happened. */
+ r = RET_NERRNO(swapcontext(&f->resume_context, &f->context));
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we've switched back to the caller's stack. */
+ __sanitizer_finish_switch_fiber(fake_stack, NULL, NULL);
+#endif
+
+ fiber_set_current(prev);
+
+ if (r < 0)
+ return r;
+
+ switch (f->state) {
+
+ case FIBER_STATE_COMPLETED:
+ if (f->result < 0 && f->result != -ECANCELED)
+ log_debug_errno(f->result, "Fiber failed with error: %m");
+ else
+ log_debug("Fiber finished executing");
+
+ fiber_resolve(f);
+ break;
+
+ case FIBER_STATE_CANCELLED:
+ case FIBER_STATE_READY:
+ log_debug("Fiber yielded execution");
+
+ r = sd_event_source_set_enabled(fiber_current_event_source(f), SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+ break;
+
+ case FIBER_STATE_SUSPENDED:
+ log_debug("Fiber suspended execution");
+ /* Fiber is waiting for something - don't re-queue it */
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+static int fiber_cancel(void *userdata) {
+ Fiber *f = userdata;
+ int r;
+
+ assert(f);
+ assert(f != fiber_get_current());
+
+ if (IN_SET(f->state, FIBER_STATE_COMPLETED, FIBER_STATE_CANCELLED))
+ return 0;
+
+ if (f->state == FIBER_STATE_INITIAL) {
+ /* The fiber's stack was allocated but never entered, so there are no scope-level cleanups
+ * waiting to run. Skip the dispatch round-trip that would just have fiber_entry_point()
+ * fall straight through with -ECANCELED, and settle the future right here — mirroring the
+ * FIBER_STATE_COMPLETED branch of fiber_run(). */
+ f->result = -ECANCELED;
+ f->state = FIBER_STATE_COMPLETED;
+ fiber_resolve(f);
+ return 1;
+ }
+
+ /* Once we cancel a fiber, we want to immediately resume it with -ECANCELED. */
+ r = sd_event_source_set_enabled(fiber_current_event_source(f), SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+
+ f->state = FIBER_STATE_CANCELLED;
+
+ return 1;
+}
+
+static int fiber_on_defer(sd_event_source *s, void *userdata) {
+ Fiber *f = ASSERT_PTR(userdata);
+ return fiber_run(f);
+}
+
+static int fiber_on_exit(sd_event_source *s, void *userdata) {
+ Fiber *f = ASSERT_PTR(userdata);
+ int r;
+
+ /* The fiber may already have completed via the regular defer path before sd_event_exit()
+ * fires the exit source; in that case there's nothing left to drive and we'd otherwise
+ * trip fiber_run()'s -ESTALE return, which sd_event would log spuriously and disable the
+ * source for. */
+ if (f->state == FIBER_STATE_COMPLETED)
+ return 0;
+
+ /* If fiber_cancel() returned 1 the fiber was just marked cancelled and its deferred/exit event
+ * source was re-armed; we let the event loop dispatch that source on the next iteration so it goes
+ * through the normal fiber_on_defer/fiber_on_exit path rather than running it recursively here. */
+ r = fiber_cancel(f);
+ if (r != 0)
+ return r;
+
+ return fiber_run(f);
+}
+
+static void* fiber_free(void *userdata) {
+ Fiber *f = userdata;
+ if (!f)
+ return NULL;
+
+ /* To make sure all memory is deallocated, the fiber has to have completed by the time we free it to
+ * make sure its stack has finished unwinding (which will invoke the registered cleanup functions).
+ * As this function may get called when not running on a fiber ourselves, we can't guarantee here
+ * that we can run the fiber to completion ourselves, so we insist that this happens before we get
+ * here. To ensure fibers are cleaned up before exiting the event loop, exit handlers are added for
+ * fibers created outside of existing fibers. For fibers created within running fibers, unwinding the
+ * outer fiber should take care of cleaning up any created child fibers (for example using
+ * sd_future_cancel_wait_unref()).
+ *
+ * FIBER_STATE_INITIAL is also accepted: the stack was allocated but never entered, so there are no
+ * registered cleanups to run. This covers the partial-construction failure path in sd_fiber_new()
+ * as well as fibers that are unrefed before the event loop ever dispatches them. */
+ assert(IN_SET(f->state, FIBER_STATE_INITIAL, FIBER_STATE_COMPLETED));
+
+ if (f->destroy)
+ f->destroy(f->userdata);
+
+#if HAVE_VALGRIND_VALGRIND_H
+ VALGRIND_STACK_DEREGISTER(f->stack_id);
+#endif
+
+ (void) munmap(f->stack, f->stack_size);
+
+ sd_event_source_disable_unref(f->defer_event_source);
+ sd_event_source_disable_unref(f->exit_event_source);
+ sd_event_unref(f->event);
+
+ free(f->name);
+ return mfree(f);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Fiber*, fiber_free);
+
+int sd_fiber_is_running(void) {
+ return !!fiber_get_current();
+}
+
+sd_event* sd_fiber_get_event(void) {
+ return ASSERT_PTR(fiber_get_current())->event;
+}
+
+int64_t sd_fiber_get_priority(void) {
+ return ASSERT_PTR(fiber_get_current())->priority;
+}
+
+static int fiber_swap(FiberState state) {
+ Fiber *f = ASSERT_PTR(fiber_get_current());
+ int r;
+
+ f->state = state;
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we're switching back to the caller's stack. */
+ void *fake_stack;
+ __sanitizer_start_switch_fiber(&fake_stack, f->resume_context.uc_stack.ss_sp, f->resume_context.uc_stack.ss_size);
+#endif
+
+ r = RET_NERRNO(swapcontext(&f->context, &f->resume_context));
+
+#if HAS_FEATURE_ADDRESS_SANITIZER
+ /* Inform ASan that we've switched back to the fiber from the caller's stack. */
+ __sanitizer_finish_switch_fiber(fake_stack, NULL, NULL);
+#endif
+
+ if (r < 0)
+ return r;
+
+ /* When we get here, we've been resumed. */
+
+ if (f->state == FIBER_STATE_CANCELLED)
+ return -ECANCELED;
+
+ /* If something asynchronous (e.g. a deadline timer) stashed a wakeup error in f->result,
+ * propagate it to the caller and clear it so it doesn't pollute subsequent suspends or the
+ * fiber's eventual return value. */
+ return f->result < 0 ? TAKE_GENERIC(f->result, int, 0) : 0;
+}
+
+int sd_fiber_yield(void) {
+ return fiber_swap(FIBER_STATE_READY);
+}
+
+int sd_fiber_suspend(void) {
+ return fiber_swap(FIBER_STATE_SUSPENDED);
+}
+
+static int fiber_set_priority(void *userdata, int64_t priority) {
+ Fiber *f = userdata;
+ int r = 0;
+
+ assert(f);
+
+ if (f->defer_event_source)
+ RET_GATHER(r, sd_event_source_set_priority(f->defer_event_source, priority));
+
+ if (f->exit_event_source)
+ RET_GATHER(r, sd_event_source_set_priority(f->exit_event_source, priority));
+
+ if (r >= 0)
+ f->priority = priority;
+
+ return r;
+}
+
+int sd_fiber_resume(sd_future *fiber_future, int result) {
+ Fiber *fiber = ASSERT_PTR(sd_future_get_impl(fiber_future));
+
+ if (fiber->state != FIBER_STATE_SUSPENDED)
+ return 0;
+
+ /* Stash the result so fiber_swap() returns it from sd_fiber_suspend(). */
+ fiber->result = result;
+ fiber->state = FIBER_STATE_READY;
+ return sd_event_source_set_enabled(fiber_current_event_source(fiber), SD_EVENT_ONESHOT);
+}
+
+/* The fiber_future ops pass the Fiber pointer through as opaque userdata — Fiber is already shaped like a
+ * future-impl struct (first field is `sd_promise *promise`, stamped by sd_future_new()). The fiber
+ * resolves its own future once it finishes running, so fiber_cancel() intentionally does not resolve. */
+static const sd_future_ops fiber_future_ops = {
+ .free = fiber_free,
+ .cancel = fiber_cancel,
+ .set_priority = fiber_set_priority,
+};
+
+static const FiberOps fiber_ops = {
+ .ppoll = sd_fiber_poll,
+ .read = sd_fiber_read,
+ .write = sd_fiber_write,
+ .timeout = sd_fiber_timeout,
+ .timeout_done = sd_future_unref,
+};
+
+int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *userdata, sd_fiber_destroy_t destroy, sd_future **ret) {
+ int r;
+
+ assert(e);
+ assert(name);
+ assert(func);
+
+ if (IN_SET(sd_event_get_state(e), SD_EVENT_EXITING, SD_EVENT_FINISHED))
+ return -ECANCELED;
+
+ _cleanup_(fiber_freep) Fiber *fiber = new(Fiber, 1);
+ if (!fiber)
+ return -ENOMEM;
+
+ struct rlimit rl = { .rlim_cur = FIBER_DEFAULT_STACK_SIZE };
+ if (getrlimit(RLIMIT_STACK, &rl) < 0)
+ log_debug_errno(errno, "Reading RLIMIT_STACK failed, ignoring: %m");
+ if (rl.rlim_cur == RLIM_INFINITY)
+ rl.rlim_cur = FIBER_DEFAULT_STACK_SIZE;
+
+ *fiber = (Fiber) {
+ .stack_size = ROUND_UP(rl.rlim_cur, page_size()),
+ .state = FIBER_STATE_INITIAL,
+ .name = strdup(name),
+ .func = func,
+ .userdata = userdata,
+ .event = sd_event_ref(e),
+ .ops = &fiber_ops,
+ };
+ if (!fiber->name)
+ return -ENOMEM;
+
+ r = fiber_allocate_stack(fiber->stack_size, &fiber->stack);
+ if (r < 0)
+ return r;
+
+ r = fiber_makecontext(fiber, fiber->stack, fiber->stack_size);
+ if (r < 0)
+ return r;
+
+#if HAVE_VALGRIND_VALGRIND_H
+ fiber->stack_id = VALGRIND_STACK_REGISTER(fiber->context.uc_stack.ss_sp,
+ (uint8_t*) fiber->context.uc_stack.ss_sp + fiber->context.uc_stack.ss_size);
+#endif
+
+ /* Execution of the fiber is driven by two event sources, one deferred, one exit. The exit event
+ * source kicks in when sd_event_exit() is called, as from that point onwards only exit event
+ * sources will be dispatched. */
+
+ r = sd_event_add_defer(e, &fiber->defer_event_source, fiber_on_defer, fiber);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_description(fiber->defer_event_source, fiber->name);
+ if (r < 0)
+ return r;
+
+ r = sd_event_add_exit(e, &fiber->exit_event_source, fiber_on_exit, fiber);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_description(fiber->exit_event_source, fiber->name);
+ if (r < 0)
+ return r;
+
+ /* If we're on a fiber, we'll rely on the parent fiber to cancel this fiber if the event loop is
+ * exiting. Otherwise, we'll trigger cancellation of this fiber via the exit event source. Why cancel
+ * via the exit event source? We can only run the fiber while the event loop is active, so we need to
+ * make sure all fibers finish running before the event loop is finished, which an exit event source
+ * allows us to do. */
+ r = sd_event_source_set_enabled(fiber->exit_event_source, sd_fiber_is_running() ? SD_EVENT_OFF : SD_EVENT_ONESHOT);
+ if (r < 0)
+ return r;
+
+ fiber->destroy = destroy;
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ r = sd_future_new(&fiber_future_ops, fiber, &f);
+ if (r < 0)
+ return r;
+
+ /* Stays in FIBER_STATE_INITIAL until the event loop first dispatches it via fiber_run(). */
+ TAKE_PTR(fiber);
+
+ if (ret)
+ *ret = TAKE_PTR(f);
+ else {
+ /* Fire-and-forget: the fiber is guaranteed to resolve (via completion, cancellation, or
+ * the event loop exit handler), so making the future floating cleans it up. */
+ r = sd_fiber_set_floating(f, true);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+const char* sd_fiber_get_name(sd_future *f) {
+ Fiber *fiber;
+
+ if (!f)
+ fiber = fiber_get_current();
+ else {
+ assert(sd_future_get_ops(f) == &fiber_future_ops);
+ fiber = sd_future_get_impl(f);
+ }
+
+ return ASSERT_PTR(fiber)->name;
+}
+
+int sd_fiber_set_floating(sd_future *f, int b) {
+ assert(f);
+ assert(sd_future_get_ops(f) == &fiber_future_ops);
+
+ Fiber *fiber = sd_future_get_impl(f);
+
+ if (!!fiber->floating == !!b)
+ return 0;
+
+ /* The floating self-ref keeps the future alive until the fiber resolves; fiber_run() drops it
+ * in the COMPLETED branch. Only valid for fiber futures because fibers uniquely guarantee
+ * resolution (via completion, cancellation, or the event loop exit handler). */
+ if (b)
+ fiber->floating = sd_future_ref(f);
+ else
+ fiber->floating = sd_future_unref(fiber->floating);
+
+ return 0;
+}
+
+int sd_fiber_get_floating(sd_future *f) {
+ assert(f);
+ assert(sd_future_get_ops(f) == &fiber_future_ops);
+
+ Fiber *fiber = sd_future_get_impl(f);
+ return !!fiber->floating;
+}
+
+int sd_fiber_sleep(uint64_t usec) {
+ Fiber *f = fiber_get_current();
+ int r;
+
+ if (!f)
+ return usleep_safe(usec);
+
+ if (usec == 0)
+ return 0;
+
+ /* Match usleep_safe(USEC_INFINITY): suspend indefinitely. Passing USEC_INFINITY to
+ * sd_event_add_time_relative() would overflow into -EOVERFLOW. */
+ if (usec == USEC_INFINITY)
+ return sd_fiber_suspend();
+
+ assert(f->event);
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *timer = NULL;
+ r = future_new_time_relative(
+ f->event,
+ CLOCK_MONOTONIC,
+ usec,
+ /* accuracy= */ 1,
+ /* result= */ 0,
+ &timer);
+ if (r < 0)
+ return r;
+
+ return sd_fiber_suspend();
+}
+
+int sd_fiber_await(sd_future *target) {
+ Fiber *f = ASSERT_PTR(fiber_get_current());
+ int r;
+
+ assert(target);
+ assert(target != sd_fiber_get_current());
+
+ if (sd_future_state(target) == SD_FUTURE_RESOLVED)
+ return 0;
+
+ /* Note that we do allow waiting for other fibers when the event loop is exiting, since waiting for
+ * other fibers does not require adding new event sources to the event loop. */
+ if (sd_event_get_state(f->event) == SD_EVENT_FINISHED)
+ return -ECANCELED;
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *wait = NULL;
+ r = sd_future_new_wait(target, &wait);
+ if (r < 0)
+ return r;
+
+ return sd_fiber_suspend();
+}
+
+sd_future* sd_fiber_timeout(uint64_t timeout) {
+ Fiber *f = ASSERT_PTR(fiber_get_current());
+ int r;
+
+ if (timeout == USEC_INFINITY)
+ return NULL;
+
+ sd_future *timer;
+ r = future_new_time_relative(
+ f->event,
+ CLOCK_MONOTONIC,
+ timeout,
+ /* accuracy= */ 1,
+ /* result= */ -ETIME,
+ &timer);
+ if (r < 0)
+ return NULL; /* On allocation failure no timer is armed and the scope becomes a no-op.
+ * Errors here are rare; if the caller cares they can compare to NULL. */
+
+ return timer;
+}
diff --git a/src/libsystemd/sd-future/sd-future.c b/src/libsystemd/sd-future/sd-future.c
new file mode 100644
index 0000000000000..01bdc6ecdbfac
--- /dev/null
+++ b/src/libsystemd/sd-future/sd-future.c
@@ -0,0 +1,268 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "errno-util.h"
+#include "fiber-def.h"
+#include "log.h"
+#include "macro.h"
+#include "set.h"
+
+/* sd_promise is the write side of a future — the only handle that can resolve it. It's embedded
+ * inside sd_future (opaque from the outside; just a marker type), and we recover the containing
+ * sd_future via container_of() when the promise is resolved. */
+struct sd_promise {};
+
+struct sd_future {
+ sd_promise promise;
+
+ unsigned n_ref;
+
+ int state;
+ int result;
+
+ Set *waiters;
+
+ sd_future_func_t callback;
+ void *userdata;
+
+ const sd_future_ops *ops;
+ void *impl;
+};
+
+static int fiber_resume_trampoline(sd_future *f) {
+ /* The future's result is what the fiber should resume with. Impls choose the value at
+ * resolution time — e.g. a deadline timer resolves with -ETIME, a wait future resolves
+ * with the target's result, a normal IO/sleep future resolves with 0 on success. */
+ return sd_fiber_resume(sd_future_get_userdata(f), sd_future_result(f));
+}
+
+int sd_promise_resolve(sd_promise *p, int result) {
+ sd_future *f = container_of(p, sd_future, promise);
+ int r = 0;
+
+ assert(f);
+
+ if (f->state != SD_FUTURE_PENDING)
+ return 0;
+
+ f->state = SD_FUTURE_RESOLVED;
+ f->result = result;
+
+ if (f->callback)
+ RET_GATHER(r, f->callback(f));
+
+ sd_promise *w;
+ SET_FOREACH(w, f->waiters)
+ RET_GATHER(r, sd_promise_resolve(w, result));
+
+ f->waiters = set_free(f->waiters);
+
+ return r;
+}
+
+static sd_future* sd_future_free(sd_future *f) {
+ if (!f)
+ return NULL;
+
+ if (f->state == SD_FUTURE_PENDING)
+ sd_promise_resolve(&f->promise, -ECANCELED);
+
+ set_free(f->waiters);
+
+ if (f->ops->free)
+ f->ops->free(f->impl);
+
+ return mfree(f);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_future, sd_future, sd_future_free);
+DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_unref);
+DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_unref);
+
+sd_future* sd_future_cancel_wait_unref(sd_future *f) {
+ int r;
+
+ if (!f)
+ return NULL;
+
+ /* We have to be able to suspend until the fiber we're waiting for finishes, and that's only
+ * possible if we're running on a fiber ourselves. */
+ assert(sd_fiber_is_running());
+
+ r = sd_future_cancel(f);
+ if (r < 0)
+ log_debug_errno(r, "Failed to cancel future, ignoring: %m");
+
+ if (f->state == SD_FUTURE_PENDING) {
+ /* Fast path: when f's resolve callback already targets the current fiber (the default for
+ * futures created on this fiber), we can suspend directly and let the existing trampoline
+ * wake us up — no need to allocate a wait future just to learn about the resolution.
+ * Otherwise fall back to sd_fiber_await() which sets up an explicit waiter. */
+ if (f->callback == fiber_resume_trampoline && f->userdata == sd_fiber_get_current())
+ r = sd_fiber_suspend();
+ else
+ r = sd_fiber_await(f);
+ if (r < 0 && r != -ECANCELED)
+ log_debug_errno(r, "Failed to wait for future to finish, ignoring: %m");
+ }
+
+ return sd_future_unref(f);
+}
+
+DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_future*, sd_future_cancel_wait_unref);
+DEFINE_POINTER_ARRAY_FREE_FUNC(sd_future*, sd_future_cancel_wait_unref);
+
+int sd_future_new(const sd_future_ops *ops, void *impl, sd_future **ret) {
+ assert(ops);
+ assert(impl);
+ assert(ret);
+
+ sd_future *f = new(sd_future, 1);
+ if (!f)
+ return -ENOMEM;
+
+ *f = (sd_future) {
+ .n_ref = 1,
+ .state = SD_FUTURE_PENDING,
+ .ops = ops,
+ .impl = impl,
+ };
+
+ /* By convention the first field of any impl struct is `sd_promise *promise` so handlers that
+ * receive the impl pointer can resolve the future without a separate lookup. Stamp the
+ * back-pointer here so the caller doesn't have to. */
+ *(sd_promise **) impl = &f->promise;
+
+ /* If we're being created on a fiber, default the callback to resuming that fiber on resolve —
+ * this is almost always what you want, and it saves the usual set_callback boilerplate before
+ * sd_fiber_suspend(). Callers that want different behavior can override with
+ * sd_future_set_callback(). */
+ sd_future *fiber = sd_fiber_get_current();
+ if (fiber)
+ (void) sd_future_set_callback(f, fiber_resume_trampoline, fiber);
+
+ *ret = f;
+ return 0;
+}
+
+int sd_future_state(sd_future *f) {
+ assert(f);
+ return f->state;
+}
+
+int sd_future_result(sd_future *f) {
+ assert(f);
+ assert(f->state == SD_FUTURE_RESOLVED);
+ return f->result;
+}
+
+void* sd_future_get_userdata(sd_future *f) {
+ assert(f);
+ return f->userdata;
+}
+
+void* sd_future_get_impl(sd_future *f) {
+ assert(f);
+ return f->impl;
+}
+
+const sd_future_ops* sd_future_get_ops(sd_future *f) {
+ assert(f);
+ return f->ops;
+}
+
+int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata) {
+ assert(f);
+
+ f->callback = callback;
+ f->userdata = userdata;
+ return 0;
+}
+
+int sd_future_set_priority(sd_future *f, int64_t priority) {
+ assert(f);
+ assert(f->state == SD_FUTURE_PENDING);
+ assert(f->ops->set_priority);
+
+ return f->ops->set_priority(f->impl, priority);
+}
+
+int sd_future_cancel(sd_future *f) {
+ assert(f);
+ assert(f->ops->cancel);
+
+ if (f->state == SD_FUTURE_RESOLVED)
+ return 0;
+
+ return f->ops->cancel(f->impl);
+}
+
+sd_future* sd_fiber_get_current(void) {
+ Fiber *f = fiber_get_current();
+ return f ? container_of(f->promise, sd_future, promise) : NULL;
+}
+
+typedef struct WaitFuture {
+ sd_promise *promise;
+ sd_future *target;
+} WaitFuture;
+
+static void* wait_future_free(void *impl) {
+ WaitFuture *f = impl;
+ if (!f)
+ return NULL;
+
+ if (f->target) {
+ set_remove(f->target->waiters, f->promise);
+ sd_future_unref(f->target);
+ }
+
+ return mfree(f);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(WaitFuture*, wait_future_free);
+
+static int wait_future_cancel(void *impl) {
+ WaitFuture *f = ASSERT_PTR(impl);
+
+ set_remove(f->target->waiters, f->promise);
+ return sd_promise_resolve(f->promise, -ECANCELED);
+}
+
+static const sd_future_ops wait_future_ops = {
+ .free = wait_future_free,
+ .cancel = wait_future_cancel,
+};
+
+int sd_future_new_wait(sd_future *target, sd_future **ret) {
+ int r;
+
+ assert(target);
+ assert(ret);
+
+ _cleanup_(wait_future_freep) WaitFuture *impl = new(WaitFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ *impl = (WaitFuture) {
+ .target = sd_future_ref(target),
+ };
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ r = sd_future_new(&wait_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+
+ if (target->state == SD_FUTURE_RESOLVED)
+ r = sd_promise_resolve(&f->promise, target->result);
+ else
+ r = set_ensure_put(&target->waiters, &trivial_hash_ops, &f->promise);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(f);
+ return 0;
+}
diff --git a/src/libsystemd/sd-future/test-fiber-io.c b/src/libsystemd/sd-future/test-fiber-io.c
new file mode 100644
index 0000000000000..a08e500d6f445
--- /dev/null
+++ b/src/libsystemd/sd-future/test-fiber-io.c
@@ -0,0 +1,1394 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+#include
+#include
+#include
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "fd-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+/* Test: Basic pipe I/O with sd-event */
+
+typedef struct PipeIOContext {
+ int *pipefd;
+ int order;
+} PipeIOContext;
+
+static int pipe_read_fiber(void *userdata) {
+ PipeIOContext *ctx = ASSERT_PTR(userdata);
+ char buf[64];
+ ssize_t n;
+
+ n = sd_fiber_read(ctx->pipefd[0], buf, sizeof(buf));
+ if (n < 0)
+ return (int) n;
+
+ /* Verify we read "hello" */
+ if (n != 5 || memcmp(buf, "hello", 5) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+TEST(fiber_io_basic) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ PipeIOContext ctx = { .pipefd = pipefd };
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "pipe-read", pipe_read_fiber, &ctx, /* destroy= */ NULL, &f));
+
+ /* Write data to the pipe */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "hello", 5), 5);
+
+ /* Run the scheduler - should process the I/O */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Verify fiber read the data */
+ ASSERT_OK_EQ(sd_future_result(f), 5);
+}
+
+static int pipe_read_order_fiber(void *userdata) {
+ PipeIOContext *ctx = ASSERT_PTR(userdata);
+ char buf[64];
+ ssize_t n;
+
+ /* Record that the read fiber started before attempting the blocking read */
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ n = sd_fiber_read(ctx->pipefd[0], buf, sizeof(buf));
+ if (n < 0)
+ return (int) n;
+
+ /* After resuming, verify the write fiber ran while we were suspended */
+ ASSERT_EQ(ctx->order, 2);
+
+ /* Verify we read "hello" */
+ if (n != 5 || memcmp(buf, "hello", 5) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+static int pipe_write_order_fiber(void *userdata) {
+ PipeIOContext *ctx = ASSERT_PTR(userdata);
+
+ /* Verify the read fiber already ran and suspended before we started */
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ return sd_fiber_write(ctx->pipefd[1], "hello", STRLEN("hello"));
+}
+
+TEST(fiber_io_read_write) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ PipeIOContext ctx = { .pipefd = pipefd };
+
+ /* Higher priority for the read fiber, which will run first and then suspend because no data is
+ * available. The write fiber will run second, write data to the pipe, causing the read fiber to get
+ * resumed. */
+ _cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
+ ASSERT_OK(sd_fiber_new(e, "pipe-read", pipe_read_order_fiber, &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "pipe-write", pipe_write_order_fiber, &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 1));
+
+ /* Run the scheduler - should process the I/O */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Verify both fibers completed and the full read->suspend->write->resume sequence occurred */
+ ASSERT_OK_EQ(sd_future_result(fr), 5);
+ ASSERT_OK_EQ(sd_future_result(fw), 5);
+}
+
+/* Test: Multiple concurrent reads */
+static int concurrent_read_fiber(void *userdata) {
+ int *args = userdata;
+ int fd = args[0];
+ int expected = args[1];
+ char buf[64];
+ ssize_t n;
+
+ n = sd_fiber_read(fd, buf, sizeof buf);
+ if (n < 0)
+ return (int) n;
+
+ if (n != 1 || buf[0] != (char) expected)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_io_concurrent) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future **fibers = NULL;
+ size_t n_fibers = 3;
+ CLEANUP_ARRAY(fibers, n_fibers, sd_future_unref_array);
+
+ /* Create 3 pipes and 3 fibers */
+ ASSERT_NOT_NULL(fibers = new0(sd_future*, n_fibers));
+ int pipes[3][2];
+ int args[3][2];
+ for (size_t i = 0; i < n_fibers; i++) {
+ ASSERT_OK_ERRNO(pipe2(pipes[i], O_CLOEXEC | O_NONBLOCK));
+ args[i][0] = pipes[i][0];
+ args[i][1] = 'A' + i;
+ ASSERT_OK(sd_fiber_new(e, "concurrent-read", concurrent_read_fiber, args[i], /* destroy= */ NULL, &fibers[i]));
+ }
+
+ /* Write data in reverse order */
+ ASSERT_EQ(write(pipes[2][1], "C", 1), 1);
+ ASSERT_EQ(write(pipes[1][1], "B", 1), 1);
+ ASSERT_EQ(write(pipes[0][1], "A", 1), 1);
+
+ /* Run until all complete */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* All should complete successfully */
+ for (size_t i = 0; i < n_fibers; i++) {
+ ASSERT_OK(sd_future_result(fibers[i]));
+ safe_close_pair(pipes[i]);
+ }
+}
+
+/* Test: Cancel fiber during I/O */
+static int blocking_read_fiber(void *userdata) {
+ int fd = PTR_TO_INT(userdata);
+ char buf[64];
+ ssize_t n;
+
+ n = sd_fiber_read(fd, buf, sizeof(buf));
+ return (int) n;
+}
+
+TEST(fiber_io_cancel) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "blocking-read", blocking_read_fiber, INT_TO_PTR(pipefd[0]), /* destroy= */ NULL, &f));
+
+ /* Run once - fiber will suspend on read */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ /* Fiber should be suspended now - add explicit check via state tracking */
+
+ /* Cancel the fiber */
+ ASSERT_OK(sd_future_cancel(f));
+
+ /* Run to completion */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Should be cancelled */
+ ASSERT_ERROR(sd_future_result(f), ECANCELED);
+}
+
+TEST(fiber_io_fallback) {
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC)); /* Note: blocking pipe */
+
+ char buf[STRLEN("fallback")] = {};
+ ASSERT_OK_EQ(sd_fiber_write(pipefd[1], "fallback", sizeof(buf)), (ssize_t) sizeof(buf));
+ ASSERT_OK_EQ(sd_fiber_read(pipefd[0], buf, sizeof(buf)), (ssize_t) sizeof(buf));
+}
+
+static int pipe_readv_order_fiber(void *userdata) {
+ PipeIOContext *ctx = ASSERT_PTR(userdata);
+ char buf1[5], buf2[5];
+ struct iovec iov[] = {
+ { .iov_base = buf1, .iov_len = sizeof(buf1) },
+ { .iov_base = buf2, .iov_len = sizeof(buf2) },
+ };
+ ssize_t n;
+
+ /* Record that the read fiber started before attempting the blocking read */
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ /* This will initially block since no data is available */
+ n = sd_fiber_readv(ctx->pipefd[0], iov, ELEMENTSOF(iov));
+ if (n < 0)
+ return (int) n;
+
+ /* After resuming, verify the write fiber ran while we were suspended */
+ ASSERT_EQ(ctx->order, 2);
+
+ if (n != 10 || memcmp(buf1, "fiber", 5) != 0 || memcmp(buf2, "readv", 5) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+static int pipe_writev_order_fiber(void *userdata) {
+ PipeIOContext *ctx = ASSERT_PTR(userdata);
+ const char *part1 = "fiber";
+ const char *part2 = "readv";
+ struct iovec iov[] = {
+ { .iov_base = (void*) part1, .iov_len = 5 },
+ { .iov_base = (void*) part2, .iov_len = 5 },
+ };
+
+ /* Verify the read fiber already ran and suspended before we started */
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ return sd_fiber_writev(ctx->pipefd[1], iov, ELEMENTSOF(iov));
+}
+
+TEST(fiber_io_readv_writev) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ PipeIOContext ctx = { .pipefd = pipefd };
+
+ /* Higher priority for the read fiber, which will run first and then suspend because no data is
+ * available. The write fiber will run second, write data to the pipe, causing the read fiber to get
+ * resumed. */
+ _cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
+ ASSERT_OK(sd_fiber_new(e, "pipe-readv", pipe_readv_order_fiber, &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "pipe-writev", pipe_writev_order_fiber, &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 1));
+
+ /* Run the scheduler - should process the I/O */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Verify both fibers completed and the full read->suspend->write->resume sequence occurred */
+ ASSERT_OK_EQ(sd_future_result(fr), 10);
+ ASSERT_OK_EQ(sd_future_result(fw), 10);
+}
+
+static int concurrent_readv_fiber(void *userdata) {
+ int *args = userdata;
+ int fd = args[0];
+ int expected1 = args[1];
+ int expected2 = args[2];
+ char buf1[1], buf2[1];
+ struct iovec iov[] = {
+ { .iov_base = buf1, .iov_len = sizeof(buf1) },
+ { .iov_base = buf2, .iov_len = sizeof(buf2) },
+ };
+ ssize_t n;
+
+ n = sd_fiber_readv(fd, iov, ELEMENTSOF(iov));
+ if (n < 0)
+ return (int) n;
+
+ if (n != 2 || buf1[0] != (char) expected1 || buf2[0] != (char) expected2)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_io_readv_concurrent) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future **fibers = NULL;
+ size_t n_fibers = 3;
+ CLEANUP_ARRAY(fibers, n_fibers, sd_future_unref_array);
+
+ /* Create 3 pipes and 3 fibers */
+ ASSERT_NOT_NULL(fibers = new0(sd_future*, 3));
+ int pipes[3][2];
+ int args[3][3];
+ for (size_t i = 0; i < n_fibers; i++) {
+ ASSERT_OK_ERRNO(pipe2(pipes[i], O_CLOEXEC | O_NONBLOCK));
+ args[i][0] = pipes[i][0];
+ args[i][1] = 'A' + i;
+ args[i][2] = 'a' + i;
+ ASSERT_OK(sd_fiber_new(e, "concurrent-readv", concurrent_readv_fiber, args[i], /* destroy= */ NULL, &fibers[i]));
+ }
+
+ /* Write data in reverse order */
+ ASSERT_EQ(write(pipes[2][1], "Cc", 2), 2);
+ ASSERT_EQ(write(pipes[1][1], "Bb", 2), 2);
+ ASSERT_EQ(write(pipes[0][1], "Aa", 2), 2);
+
+ /* Run until all complete */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* All should complete successfully */
+ for (size_t i = 0; i < n_fibers; i++) {
+ ASSERT_OK(sd_future_result(fibers[i]));
+ safe_close_pair(pipes[i]);
+ }
+}
+
+typedef struct SocketIOContext {
+ int *sockfd;
+ int order;
+} SocketIOContext;
+
+static int socket_send_order_fiber(void *userdata) {
+ SocketIOContext *ctx = ASSERT_PTR(userdata);
+
+ /* Verify the recv fiber already ran and suspended before we started */
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ return sd_fiber_send(ctx->sockfd[0], "socket", STRLEN("socket"), 0);
+}
+
+static int socket_recv_order_fiber(void *userdata) {
+ SocketIOContext *ctx = ASSERT_PTR(userdata);
+ char buf[64];
+ ssize_t n;
+
+ /* Record that the recv fiber started before attempting the blocking recv */
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ n = sd_fiber_recv(ctx->sockfd[1], buf, sizeof(buf), 0);
+ if (n < 0)
+ return (int) n;
+
+ /* After resuming, verify the send fiber ran while we were suspended */
+ ASSERT_EQ(ctx->order, 2);
+
+ /* Verify we received "socket" */
+ if (n != 6 || memcmp(buf, "socket", 6) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+TEST(fiber_io_recv_send) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ SocketIOContext ctx = { .sockfd = sockfd };
+
+ /* Higher priority for the recv fiber, which will run first and suspend */
+ _cleanup_(sd_future_unrefp) sd_future *fs = NULL, *fr = NULL;
+ ASSERT_OK(sd_fiber_new(e, "socket-recv", socket_recv_order_fiber, &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "socket-send", socket_send_order_fiber, &ctx, /* destroy= */ NULL, &fs));
+ ASSERT_OK(sd_future_set_priority(fs, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Verify both fibers completed and the full recv->suspend->send->resume sequence occurred */
+ ASSERT_OK_EQ(sd_future_result(fr), 6);
+ ASSERT_OK_EQ(sd_future_result(fs), 6);
+}
+
+static int socket_recv_peek_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ char buf1[64], buf2[64];
+ ssize_t n1, n2;
+
+ /* First peek at the data */
+ n1 = sd_fiber_recv(sockfd, buf1, sizeof(buf1), MSG_PEEK);
+ if (n1 < 0)
+ return (int) n1;
+
+ /* Then actually read it */
+ n2 = sd_fiber_recv(sockfd, buf2, sizeof(buf2), 0);
+ if (n2 < 0)
+ return (int) n2;
+
+ /* Both should have read the same data */
+ if (n1 != n2 || memcmp(buf1, buf2, n1) != 0)
+ return -EIO;
+
+ if (n1 != 4 || memcmp(buf1, "peek", 4) != 0)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_io_recv_peek) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "socket-recv-peek", socket_recv_peek_fiber, INT_TO_PTR(sockfd[1]), /* destroy= */ NULL, &f));
+
+ /* Write data to the socket */
+ ASSERT_OK_EQ_ERRNO(write(sockfd[0], "peek", 4), 4);
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+static int socket_connect_fiber(void *userdata) {
+ struct sockaddr_un *addr = userdata;
+ _cleanup_close_ int sockfd = -EBADF;
+
+ sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if (sockfd < 0)
+ return -errno;
+
+ return sd_fiber_connect(sockfd, (struct sockaddr*) addr, sizeof(*addr));
+}
+
+TEST(fiber_io_connect) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create listening socket with abstract namespace */
+ _cleanup_close_ int listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ ASSERT_OK(listen_fd);
+
+ /* Use abstract socket (starts with null byte) */
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "test-fiber-connect-%d", getpid());
+
+ ASSERT_OK(bind(listen_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK(listen(listen_fd, 1));
+
+ /* Create fiber to connect */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "socket-connect", socket_connect_fiber, &addr, /* destroy= */ NULL, &f));
+
+ /* Run the event loop - connection should complete */
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+static int socket_sendmsg_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ struct iovec iov = {
+ .iov_base = (void*) "message",
+ .iov_len = STRLEN("message"),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+
+ return sd_fiber_sendmsg(sockfd, &msg, 0);
+}
+
+static int socket_recvmsg_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ char buf[64];
+ struct iovec iov = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+ ssize_t n;
+
+ n = sd_fiber_recvmsg(sockfd, &msg, 0);
+ if (n < 0)
+ return (int) n;
+
+ if (n != 7 || memcmp(buf, "message", 7) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+TEST(fiber_io_recvmsg_sendmsg) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ _cleanup_(sd_future_unrefp) sd_future *fs = NULL, *fr = NULL;
+ ASSERT_OK(sd_fiber_new(e, "socket-recvmsg", socket_recvmsg_fiber, INT_TO_PTR(sockfd[1]), /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 1));
+ ASSERT_OK(sd_fiber_new(e, "socket-sendmsg", socket_sendmsg_fiber, INT_TO_PTR(sockfd[0]), /* destroy= */ NULL, &fs));
+ ASSERT_OK(sd_future_set_priority(fs, 0));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK_EQ(sd_future_result(fr), 7);
+ ASSERT_OK_EQ(sd_future_result(fs), 7);
+}
+
+static int socket_sendto_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+
+ /* For socketpair dgram sockets, we can use NULL address since they're connected */
+ return sd_fiber_sendto(sockfd, "datagram", STRLEN("datagram"), 0, NULL, 0);
+}
+
+static int socket_recvfrom_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ char buf[64];
+ struct sockaddr_un addr;
+ socklen_t addr_len = sizeof(addr);
+ ssize_t n;
+
+ n = sd_fiber_recvfrom(sockfd, buf, sizeof(buf), 0,
+ (struct sockaddr*) &addr, &addr_len);
+ if (n < 0)
+ return (int) n;
+
+ if (n != 8 || memcmp(buf, "datagram", 8) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+TEST(fiber_io_recvfrom_sendto) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ _cleanup_(sd_future_unrefp) sd_future *fs = NULL, *fr = NULL;
+ ASSERT_OK(sd_fiber_new(e, "socket-recvfrom", socket_recvfrom_fiber, INT_TO_PTR(sockfd[1]), /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 1));
+ ASSERT_OK(sd_fiber_new(e, "socket-sendto", socket_sendto_fiber, INT_TO_PTR(sockfd[0]), /* destroy= */ NULL, &fs));
+ ASSERT_OK(sd_future_set_priority(fs, 0));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK_EQ(sd_future_result(fr), 8);
+ ASSERT_OK_EQ(sd_future_result(fs), 8);
+}
+
+static int socket_sendmsg_fd_fiber(void *userdata) {
+ int *args = userdata;
+ int sockfd = args[0];
+ int fd_to_send = args[1];
+ struct iovec iov = {
+ .iov_base = (void*) "X",
+ .iov_len = 1,
+ };
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(int))];
+ } control = {};
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ struct cmsghdr *cmsg;
+
+ cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(int));
+ memcpy(CMSG_DATA(cmsg), &fd_to_send, sizeof(int));
+
+ return sd_fiber_sendmsg(sockfd, &msg, 0);
+}
+
+static int socket_recvmsg_fd_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ char buf[1];
+ struct iovec iov = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf),
+ };
+ union {
+ struct cmsghdr cmsghdr;
+ uint8_t buf[CMSG_SPACE(sizeof(int))];
+ } control = {};
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+ struct cmsghdr *cmsg;
+ int received_fd;
+ ssize_t n;
+
+ n = sd_fiber_recvmsg(sockfd, &msg, 0);
+ if (n < 0)
+ return (int) n;
+
+ if (n != 1 || buf[0] != 'X')
+ return -EIO;
+
+ /* Extract the file descriptor */
+ cmsg = CMSG_FIRSTHDR(&msg);
+ if (!cmsg || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS)
+ return -EIO;
+
+ memcpy(&received_fd, CMSG_DATA(cmsg), sizeof(int));
+
+ /* Verify we can use the fd */
+ if (fcntl(received_fd, F_GETFD) < 0)
+ return -errno;
+
+ close(received_fd);
+ return 0;
+}
+
+TEST(fiber_io_sendmsg_recvmsg_fd) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ /* Create a test file descriptor to send */
+ _cleanup_close_ int test_fd = open("/dev/null", O_RDONLY | O_CLOEXEC);
+ ASSERT_OK_ERRNO(test_fd);
+
+ _cleanup_(sd_future_unrefp) sd_future *fs = NULL, *fr = NULL;
+ int args[2] = { sockfd[0], test_fd };
+ ASSERT_OK(sd_fiber_new(e, "socket-recvmsg-fd", socket_recvmsg_fd_fiber, INT_TO_PTR(sockfd[1]), /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 1));
+ ASSERT_OK(sd_fiber_new(e, "socket-sendmsg-fd", socket_sendmsg_fd_fiber, args, /* destroy= */ NULL, &fs));
+ ASSERT_OK(sd_future_set_priority(fs, 0));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(fr));
+ ASSERT_OK_EQ(sd_future_result(fs), 1);
+}
+
+TEST(fiber_io_socket_fallback) {
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ char buf[STRLEN("fallback")] = {};
+
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockfd));
+
+ /* Test send/recv without fiber context */
+ ASSERT_OK_EQ(sd_fiber_send(sockfd[0], "fallback", sizeof(buf), 0), (ssize_t) sizeof(buf));
+ ASSERT_OK_EQ(sd_fiber_recv(sockfd[1], buf, sizeof(buf), 0), (ssize_t) sizeof(buf));
+
+ /* Test sendto/recvfrom without fiber context */
+ ASSERT_OK_EQ(sd_fiber_sendto(sockfd[0], "fallback", sizeof(buf), 0, NULL, 0), (ssize_t) sizeof(buf));
+ ASSERT_OK_EQ(sd_fiber_recvfrom(sockfd[1], buf, sizeof(buf), 0, NULL, NULL), (ssize_t) sizeof(buf));
+}
+
+static int blocking_recv_fiber(void *userdata) {
+ int sockfd = PTR_TO_INT(userdata);
+ char buf[64];
+
+ return sd_fiber_recv(sockfd, buf, sizeof(buf), 0);
+}
+
+TEST(fiber_io_socket_cancel) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int sockfd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sockfd));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "blocking-recv", blocking_recv_fiber, INT_TO_PTR(sockfd[0]), /* destroy= */ NULL, &f));
+
+ /* Run once - fiber will suspend on recv */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ /* Cancel the fiber */
+ ASSERT_OK(sd_future_cancel(f));
+
+ /* Run to completion */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Should be cancelled */
+ ASSERT_ERROR(sd_future_result(f), ECANCELED);
+}
+
+/* Test: Basic accept operation */
+static int accept_fiber(void *userdata) {
+ int listen_fd = PTR_TO_INT(userdata);
+ struct sockaddr_un addr;
+ socklen_t addr_len = sizeof(addr);
+ int client_fd;
+
+ client_fd = sd_fiber_accept(listen_fd, (struct sockaddr*) &addr, &addr_len, SOCK_CLOEXEC);
+ if (client_fd < 0)
+ return client_fd;
+
+ close(client_fd);
+ return 0;
+}
+
+TEST(fiber_io_accept_basic) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create listening socket with abstract namespace */
+ _cleanup_close_ int listen_fd = -EBADF;
+ ASSERT_OK_ERRNO(listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "test-fiber-accept-%d", getpid());
+
+ ASSERT_OK_ERRNO(bind(listen_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK_ERRNO(listen(listen_fd, 1));
+
+ /* Create fiber to accept connection */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "accept", accept_fiber, INT_TO_PTR(listen_fd), /* destroy= */ NULL, &f));
+
+ /* Connect from outside fiber context */
+ _cleanup_close_ int connect_fd = -EBADF;
+ ASSERT_OK(connect_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+ ASSERT_OK(connect(connect_fd, (struct sockaddr*) &addr, sizeof(addr)));
+
+ /* Run the event loop - accept should complete */
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: Multiple sequential accepts */
+static int accept_multiple_fiber(void *userdata) {
+ int listen_fd = PTR_TO_INT(userdata);
+ struct sockaddr_un addr;
+ socklen_t addr_len;
+ int count = 0;
+
+ for (int i = 0; i < 3; i++) {
+ _cleanup_close_ int client_fd = -EBADF;
+
+ addr_len = sizeof(addr);
+ client_fd = sd_fiber_accept(listen_fd, (struct sockaddr*) &addr, &addr_len, SOCK_CLOEXEC);
+ if (client_fd < 0)
+ return client_fd;
+
+ count++;
+ }
+
+ return count;
+}
+
+TEST(fiber_io_accept_multiple) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create listening socket */
+ _cleanup_close_ int listen_fd = -EBADF;
+ ASSERT_OK(listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "test-fiber-accept-multi-%d", getpid());
+
+ ASSERT_OK(bind(listen_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK(listen(listen_fd, 5));
+
+ /* Create fiber to accept multiple connections */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "accept-multiple", accept_multiple_fiber, INT_TO_PTR(listen_fd), /* destroy= */ NULL, &f));
+
+ /* Connect multiple times */
+ int connect_fds[3] = { -EBADF, -EBADF, -EBADF };
+ for (size_t i = 0; i < 3; i++) {
+ connect_fds[i] = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ ASSERT_OK(connect_fds[i]);
+ ASSERT_OK(connect(connect_fds[i], (struct sockaddr*) &addr, sizeof(addr)));
+ }
+
+ /* Run the event loop */
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_EQ(sd_future_result(f), 3);
+
+ /* Clean up connection fds */
+ for (size_t i = 0; i < 3; i++)
+ safe_close(connect_fds[i]);
+}
+
+/* Test: Accept and exchange data */
+static int accept_and_read_fiber(void *userdata) {
+ int listen_fd = PTR_TO_INT(userdata);
+ _cleanup_close_ int client_fd = -EBADF;
+ char buf[64];
+ ssize_t n;
+
+ client_fd = sd_fiber_accept(listen_fd, NULL, NULL, SOCK_CLOEXEC);
+ if (client_fd < 0)
+ return client_fd;
+
+ n = sd_fiber_read(client_fd, buf, sizeof(buf));
+ if (n < 0)
+ return (int) n;
+
+ if (n != 5 || memcmp(buf, "hello", 5) != 0)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_io_accept_and_read) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create listening socket */
+ _cleanup_close_ int listen_fd = -EBADF;
+ ASSERT_OK(listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "test-fiber-accept-read-%d", getpid());
+
+ ASSERT_OK(bind(listen_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK(listen(listen_fd, 1));
+
+ /* Create fiber to accept and read */
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "accept-and-read", accept_and_read_fiber, INT_TO_PTR(listen_fd), /* destroy= */ NULL, &f));
+
+ /* Connect and send data */
+ _cleanup_close_ int connect_fd = -EBADF;
+ ASSERT_OK(connect_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+ ASSERT_OK(connect(connect_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK_EQ_ERRNO(write(connect_fd, "hello", 5), 5);
+
+ /* Run the event loop */
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: poll with single fd ready immediately */
+static int poll_immediate_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipefd[0], .events = POLLIN },
+ };
+ int r;
+
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ /* Should have one fd ready */
+ if (r != 1)
+ return -EIO;
+
+ if (!(fds[0].revents & POLLIN))
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_poll_immediate) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ /* Write data before creating fiber */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "X", 1), 1);
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-immediate", poll_immediate_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: poll with fd that becomes ready after suspension */
+static int poll_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipefd[0], .events = POLLIN },
+ };
+ int r;
+
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ if (r != 1 || !(fds[0].revents & POLLIN))
+ return -EIO;
+
+ /* Read the data */
+ char buf[1];
+ if (read(pipefd[0], buf, 1) != 1 || buf[0] != 'Y')
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_poll) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-suspend", poll_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ /* Run once - fiber will suspend on poll */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ /* Write data to wake it up */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "Y", 1), 1);
+
+ /* Complete execution */
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: poll with multiple fds */
+static int poll_multiple_fiber(void *userdata) {
+ int (*pipes)[2] = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipes[0][0], .events = POLLIN },
+ { .fd = pipes[1][0], .events = POLLIN },
+ { .fd = pipes[2][0], .events = POLLIN },
+ };
+ int r;
+
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ /* Should have all three ready */
+ if (r != 3)
+ return -EIO;
+
+ for (size_t i = 0; i < 3; i++) {
+ if (!(fds[i].revents & POLLIN))
+ return -EIO;
+
+ char buf[1];
+ if (read(fds[i].fd, buf, 1) != 1 || buf[0] != (char) ('A' + i))
+ return -EIO;
+ }
+
+ return 0;
+}
+
+TEST(fiber_poll_multiple) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create three pipes */
+ int pipes[3][2];
+ for (size_t i = 0; i < 3; i++)
+ ASSERT_OK_ERRNO(pipe2(pipes[i], O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-multiple", poll_multiple_fiber, pipes, /* destroy= */ NULL, &f));
+
+ /* Run once - fiber will suspend waiting for data */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ /* Write to all three pipes in different order */
+ ASSERT_OK_EQ_ERRNO(write(pipes[2][1], "C", 1), 1);
+ ASSERT_OK_EQ_ERRNO(write(pipes[0][1], "A", 1), 1);
+ ASSERT_OK_EQ_ERRNO(write(pipes[1][1], "B", 1), 1);
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+
+ for (size_t i = 0; i < 3; i++)
+ safe_close_pair(pipes[i]);
+}
+
+/* Test: poll with POLLOUT (write readiness) */
+static int poll_pollout_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipefd[1], .events = POLLOUT },
+ };
+ int r;
+
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ if (r != 1 || !(fds[0].revents & POLLOUT))
+ return -EIO;
+
+ /* Pipe should be writable */
+ if (write(pipefd[1], "Z", 1) != 1)
+ return -errno;
+
+ return 0;
+}
+
+TEST(fiber_poll_pollout) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-pollout", poll_pollout_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+
+ /* Verify data was written */
+ char buf[1];
+ ASSERT_OK_EQ_ERRNO(read(pipefd[0], buf, 1), 1);
+ ASSERT_EQ(buf[0], 'Z');
+}
+
+/* Test: poll with timeout that expires */
+static int poll_timeout_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipefd[0], .events = POLLIN },
+ };
+ int r;
+
+ /* Poll with 100ms timeout - no data will arrive */
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), 100 * USEC_PER_MSEC);
+ if (r < 0)
+ return r;
+
+ /* Should timeout with no fds ready */
+ if (r != 0)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_poll_timeout) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-timeout", poll_timeout_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: poll with zero timeout (should not block) */
+static int poll_zero_timeout_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = pipefd[0], .events = POLLIN },
+ };
+ int r;
+
+ /* Poll with zero timeout - should return immediately */
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), 0);
+ if (r < 0)
+ return r;
+
+ /* No data available, so should return 0 */
+ if (r != 0)
+ return -EIO;
+
+ /* Now write data */
+ if (write(pipefd[1], "Q", 1) != 1)
+ return -errno;
+
+ /* Poll again with zero timeout - should see data */
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), 0);
+ if (r < 0)
+ return r;
+
+ if (r != 1 || !(fds[0].revents & POLLIN))
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_poll_zero_timeout) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-zero-timeout", poll_zero_timeout_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: poll with zero fds and zero timeout (should return immediately) */
+static int poll_zero_fds_fiber(void *userdata) {
+ return sd_fiber_poll(NULL, 0, 0);
+}
+
+TEST(fiber_poll_zero_fds) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-zero-fds", poll_zero_fds_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_EQ(sd_future_result(f), 0);
+}
+
+/* Test: poll with zero fds and no timeout has no possible wakeup, must reject with -EINVAL */
+static int poll_zero_fds_no_timeout_fiber(void *userdata) {
+ return sd_fiber_poll(NULL, 0, USEC_INFINITY);
+}
+
+TEST(fiber_poll_zero_fds_no_timeout) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-zero-fds-no-timeout", poll_zero_fds_no_timeout_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), EINVAL);
+}
+
+/* Test: poll with negative fd (should be ignored) */
+static int poll_negative_fd_fiber(void *userdata) {
+ int *pipefd = userdata;
+ struct pollfd fds[] = {
+ { .fd = -1, .events = POLLIN },
+ { .fd = pipefd[0], .events = POLLIN },
+ };
+ int r;
+
+ r = sd_fiber_poll(fds, ELEMENTSOF(fds), USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ /* Only the second fd should be ready */
+ if (r != 1 || !(fds[1].revents & POLLIN))
+ return -EIO;
+
+ /* First fd should have no events */
+ if (fds[0].revents != 0)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_poll_negative_fd) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ /* Write data before creating fiber */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "N", 1), 1);
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "poll-negative-fd", poll_negative_fd_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: Multiple fibers waiting on the same fd */
+typedef struct SharedFdArgs {
+ int pipefd;
+ int *counter;
+} SharedFdArgs;
+
+static int shared_fd_read_fiber(void *userdata) {
+ SharedFdArgs *args = ASSERT_PTR(userdata);
+ char buf[1];
+ ssize_t n;
+
+ n = sd_fiber_read(args->pipefd, buf, sizeof(buf));
+ if (n < 0)
+ return (int) n;
+
+ if (n != 1)
+ return -EIO;
+
+ /* Increment counter to track successful reads */
+ (*args->counter)++;
+
+ return 0;
+}
+
+TEST(fiber_io_same_fd_multiple_fibers) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ /* Create 3 fibers all waiting on the same pipe read end */
+ sd_future **fibers = NULL;
+ size_t n_fibers = 3;
+ CLEANUP_ARRAY(fibers, n_fibers, sd_future_unref_array);
+ SharedFdArgs args[3];
+ int counter = 0;
+
+ ASSERT_NOT_NULL(fibers = new0(sd_future*, n_fibers));
+ for (size_t i = 0; i < 3; i++) {
+ args[i].pipefd = pipefd[0];
+ args[i].counter = &counter;
+ ASSERT_OK(sd_fiber_new(e, "shared-fd-read", shared_fd_read_fiber, &args[i], /* destroy= */ NULL, &fibers[i]));
+ }
+
+ /* All fibers should suspend waiting for data */
+ for (size_t i = 0; i < n_fibers; i++)
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ /* Write 3 bytes - each byte will wake one fiber */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "ABC", 3), 3);
+
+ /* Run until all fibers complete */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* All should complete successfully and each should have read one byte */
+ for (size_t i = 0; i < n_fibers; i++)
+ ASSERT_OK(sd_future_result(fibers[i]));
+
+ ASSERT_EQ(counter, 3);
+}
+
+static int blocking_fd_preserve_fiber(void *userdata) {
+ int *pipefd = ASSERT_PTR(userdata);
+ char buf[8] = {};
+ ssize_t n;
+
+ /* The pipe has data pre-filled, so this should succeed immediately on the fast path.
+ * This exercises the fd blocking state restore: fiber_io_operation() temporarily sets the fd
+ * to nonblocking, and must restore it to blocking on the success path. */
+ n = sd_fiber_read(pipefd[0], buf, sizeof(buf));
+ if (n < 0)
+ return (int) n;
+
+ if ((size_t) n != sizeof(buf) || memcmp(buf, "blocking", sizeof(buf)) != 0)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(fiber_io_blocking_fd_preserved) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create a blocking pipe (no O_NONBLOCK) */
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ /* Pre-fill the pipe so the read will succeed immediately */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "blocking", 8), 8);
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "blocking-fd-preserve", blocking_fd_preserve_fiber, pipefd, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+
+ /* Verify the read end is still in blocking mode after the fiber completed */
+ ASSERT_OK_ZERO(fd_nonblock(pipefd[0], false));
+}
+
+static int socket_connect_blocking_fiber(void *userdata) {
+ struct sockaddr_un *addr = userdata;
+ _cleanup_close_ int sockfd = -EBADF;
+
+ /* Use a blocking socket (no SOCK_NONBLOCK). sd_fiber_connect() should temporarily set it
+ * to nonblocking, handle the EINPROGRESS path with getsockopt(SO_ERROR), and restore
+ * the blocking state. */
+ sockfd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (sockfd < 0)
+ return -errno;
+
+ int r = sd_fiber_connect(sockfd, (struct sockaddr*) addr, sizeof(*addr));
+ if (r < 0)
+ return r;
+
+ /* Verify the socket is back in blocking mode */
+ r = fd_nonblock(sockfd, false);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return -EBUSY; /* fd was nonblocking, but should have been restored to blocking */
+
+ return 0;
+}
+
+TEST(fiber_io_connect_blocking) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create listening socket */
+ _cleanup_close_ int listen_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ ASSERT_OK(listen_fd);
+
+ struct sockaddr_un addr = {
+ .sun_family = AF_UNIX,
+ };
+ addr.sun_path[0] = '\0';
+ snprintf(addr.sun_path + 1, sizeof(addr.sun_path) - 1, "test-fiber-connect-blocking-%d", getpid());
+
+ ASSERT_OK(bind(listen_fd, (struct sockaddr*) &addr, sizeof(addr)));
+ ASSERT_OK(listen(listen_fd, 1));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "connect-blocking", socket_connect_blocking_fiber, &addr, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd/sd-future/test-fiber-ops.c b/src/libsystemd/sd-future/test-fiber-ops.c
new file mode 100644
index 0000000000000..c6ef31ee6e572
--- /dev/null
+++ b/src/libsystemd/sd-future/test-fiber-ops.c
@@ -0,0 +1,574 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+#include
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "alloc-util.h"
+#include "cleanup-util.h"
+#include "fd-util.h"
+#include "io-util.h"
+#include "pidref.h"
+#include "process-util.h"
+#include "tests.h"
+#include "time-util.h"
+
+/* Test: wait_for_terminate basic functionality */
+static int wait_simple_fiber(void *userdata) {
+ _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
+ siginfo_t si;
+ int r;
+
+ /* Fork a child that exits immediately */
+ r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidref);
+ if (r < 0)
+ return r;
+
+ if (r == 0)
+ _exit(42);
+
+ /* Parent - wait for child */
+ r = pidref_wait_for_terminate(&pidref, &si);
+ if (r < 0)
+ return r;
+
+ pidref_done(&pidref);
+
+ /* Verify child exited with status 42 */
+ if (si.si_code != CLD_EXITED || si.si_status != 42)
+ return -EIO;
+
+ return 0;
+}
+
+TEST(wait_for_terminate_fiber_basic) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "wait-simple", wait_simple_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+/* Test: wait_for_terminate with multiple children */
+static int wait_multiple_fiber(void *userdata) {
+ PidRef pidrefs[3] = { PIDREF_NULL, PIDREF_NULL, PIDREF_NULL };
+ siginfo_t si;
+ int r;
+
+ /* Fork three children with different exit codes */
+ for (size_t i = 0; i < 3; i++) {
+ r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidrefs[i]);
+ if (r < 0)
+ goto cleanup;
+
+ if (r == 0)
+ /* Child process */
+ _exit(10 + i);
+ }
+
+ /* Wait for all three in order */
+ for (size_t i = 0; i < 3; i++) {
+ r = pidref_wait_for_terminate(&pidrefs[i], &si);
+ if (r < 0)
+ goto cleanup;
+
+ pidref_done(&pidrefs[i]);
+
+ if (si.si_code != CLD_EXITED || si.si_status != (int) (10 + i)) {
+ r = -EIO;
+ goto cleanup;
+ }
+ }
+
+ return 0;
+
+cleanup:
+ for (size_t i = 0; i < 3; i++)
+ pidref_done(&pidrefs[i]);
+
+ return r;
+}
+
+TEST(wait_for_terminate_fiber_multiple) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "wait-multiple", wait_multiple_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+}
+
+static int concurrent_wait_fiber(void *userdata) {
+ _cleanup_(pidref_done_sigkill_wait) PidRef pidref = PIDREF_NULL;
+ siginfo_t si;
+ int r;
+
+ r = pidref_safe_fork("(test-child)", FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_LOG, &pidref);
+ if (r < 0)
+ return r;
+
+ if (r == 0)
+ /* Child exits with specified status */
+ _exit(PTR_TO_INT(userdata));
+
+ r = pidref_wait_for_terminate(&pidref, &si);
+ if (r < 0)
+ return r;
+
+ pidref_done(&pidref);
+
+ if (si.si_code != CLD_EXITED || si.si_status != PTR_TO_INT(userdata))
+ return -EIO;
+
+ return 0;
+}
+
+TEST(wait_for_terminate_fiber_concurrent) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[3] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+
+ /* Create 3 fibers, each waiting for a different child */
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK(sd_fiber_new(e, "concurrent-wait", concurrent_wait_fiber, INT_TO_PTR(20 + i), /* destroy= */ NULL, &fibers[i]));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* All fibers should complete successfully */
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK(sd_future_result(fibers[i]));
+}
+
+typedef struct LoopIOContext {
+ int *pipefd;
+ const char *data;
+ size_t len;
+ int order;
+} LoopIOContext;
+
+static int loop_read_suspend_fiber(void *userdata) {
+ LoopIOContext *ctx = ASSERT_PTR(userdata);
+ char buf[64];
+
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ ssize_t n = loop_read(ctx->pipefd[0], buf, sizeof(buf), /* do_poll= */ true);
+
+ /* While we were suspended, the writer fiber should have run. */
+ ASSERT_EQ(ctx->order, 2);
+
+ if (n < 0)
+ return (int) n;
+ if ((size_t) n != ctx->len || memcmp(buf, ctx->data, ctx->len) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+static int loop_write_suspend_fiber(void *userdata) {
+ LoopIOContext *ctx = ASSERT_PTR(userdata);
+
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ int r = loop_write(ctx->pipefd[1], ctx->data, ctx->len);
+ if (r < 0)
+ return r;
+
+ /* Close the write end so the reader sees EOF after reading the data. */
+ ctx->pipefd[1] = safe_close(ctx->pipefd[1]);
+ return 0;
+}
+
+/* Test: two fibers cooperatively pass a small payload through a blocking pipe using the suspending
+ * loop helpers. Exercises the non-blocking flip, event-loop yielding, and the blocking-mode restore. */
+TEST(loop_read_write_suspend) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ static const char payload[] = "loop-suspend";
+ LoopIOContext ctx = {
+ .pipefd = pipefd,
+ .data = payload,
+ .len = sizeof(payload) - 1,
+ };
+
+ _cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-read", loop_read_suspend_fiber, &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "loop-write", loop_write_suspend_fiber, &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.len);
+ ASSERT_OK_ZERO(sd_future_result(fw));
+
+ /* The read fd started out blocking and loop_read() must have restored it before returning. */
+ ASSERT_OK_ZERO(fcntl(pipefd[0], F_GETFL) & O_NONBLOCK);
+}
+
+static int loop_read_exact_short_fiber(void *userdata) {
+ int fd = PTR_TO_INT(userdata);
+ char buf[16];
+
+ /* Requesting more bytes than the peer writes should return -EIO once EOF is hit. */
+ return loop_read_exact(fd, buf, sizeof(buf), /* do_poll= */ true);
+}
+
+/* Test: loop_read_exact() returns -EIO when the peer closes early. */
+TEST(loop_read_exact_short) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-read-exact", loop_read_exact_short_fiber,
+ INT_TO_PTR(pipefd[0]), /* destroy= */ NULL, &f));
+
+ /* Write a few bytes and close the write end — less than the fiber asked for. */
+ ASSERT_OK_EQ_ERRNO(write(pipefd[1], "abc", 3), (ssize_t) 3);
+ pipefd[1] = safe_close(pipefd[1]);
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_ERROR(sd_future_result(f), EIO);
+}
+
+typedef struct LoopWriteTimeoutContext {
+ int fd;
+ int result;
+} LoopWriteTimeoutContext;
+
+static int loop_write_timeout_fiber(void *userdata) {
+ LoopWriteTimeoutContext *ctx = ASSERT_PTR(userdata);
+
+ /* Try to write much more than the pipe buffer can hold with a short timeout. The write will
+ * succeed partially and then hit -ETIME after exhausting the timeout while blocked. */
+ static const char big_buf[128 * 1024] = { 0 };
+ ctx->result = loop_write_full(ctx->fd, big_buf, sizeof(big_buf), 100 * USEC_PER_MSEC);
+ return 0;
+}
+
+/* Test: loop_write_full() returns -ETIME when the peer never drains. */
+TEST(loop_write_full_timeout) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ /* Shrink the pipe buffer to its minimum (one page) so the 128K write below is guaranteed to block
+ * regardless of the architecture's page size. The default pipe buffer is 16 pages, which on
+ * 64K-page architectures (e.g. ppc64le) is 1 MiB — enough to absorb the entire write without ever
+ * blocking, defeating the purpose of the timeout. */
+ ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
+
+ LoopWriteTimeoutContext ctx = { .fd = pipefd[1], .result = 0 };
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-write-timeout", loop_write_timeout_fiber, &ctx, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK_ZERO(sd_future_result(f));
+ ASSERT_ERROR(ctx.result, ETIME);
+}
+
+typedef struct PpollDispatchContext {
+ int *pipefd;
+ int order;
+} PpollDispatchContext;
+
+static int ppoll_dispatch_read_fiber(void *userdata) {
+ PpollDispatchContext *ctx = ASSERT_PTR(userdata);
+ struct pollfd pfd = {
+ .fd = ctx->pipefd[0],
+ .events = POLLIN,
+ };
+
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ /* Direct ppoll_usec() call from a fiber must dispatch through sd_fiber_poll(), suspending the
+ * fiber instead of blocking the entire thread. If dispatch fails, the writer fiber never gets a
+ * chance to run and the test deadlocks. */
+ int r = ppoll_usec(&pfd, 1, USEC_INFINITY);
+ if (r < 0)
+ return r;
+
+ ASSERT_EQ(ctx->order, 2);
+
+ if (r != 1 || !FLAGS_SET(pfd.revents, POLLIN))
+ return -EIO;
+
+ return 0;
+}
+
+static int ppoll_dispatch_write_fiber(void *userdata) {
+ PpollDispatchContext *ctx = ASSERT_PTR(userdata);
+
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ if (write(ctx->pipefd[1], "x", 1) != 1)
+ return -errno;
+
+ return 0;
+}
+
+/* Test: ppoll_usec() called from a fiber dispatches through the FiberOps hook to sd_fiber_poll(),
+ * yielding to the event loop instead of blocking. */
+TEST(ppoll_usec_dispatch) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ PpollDispatchContext ctx = { .pipefd = pipefd };
+
+ _cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
+ ASSERT_OK(sd_fiber_new(e, "ppoll-read", ppoll_dispatch_read_fiber, &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "ppoll-write", ppoll_dispatch_write_fiber, &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(fr));
+ ASSERT_OK(sd_future_result(fw));
+}
+
+static int loop_write_zero_timeout_nonblock_fiber(void *userdata) {
+ int fd = PTR_TO_INT(userdata);
+
+ /* Fill the pipe so the next write would block. The fd is non-blocking, so on a fiber
+ * loop_write_full(timeout=0) must take the non-fiber path and return -EAGAIN immediately
+ * rather than suspending. */
+ static const char big_buf[128 * 1024] = { 0 };
+ return loop_write_full(fd, big_buf, sizeof(big_buf), /* timeout= */ 0);
+}
+
+/* Test: timeout == 0 on a non-blocking fd from a fiber preserves the "don't wait" semantic and
+ * returns -EAGAIN when the pipe buffer is full, instead of suspending the fiber. */
+TEST(loop_write_zero_timeout_nonblock) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+ ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-write-zt-nb", loop_write_zero_timeout_nonblock_fiber,
+ INT_TO_PTR(pipefd[1]), /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), EAGAIN);
+}
+
+typedef struct LoopWriteZeroBlockingContext {
+ int *pipefd;
+ size_t total;
+ int order;
+} LoopWriteZeroBlockingContext;
+
+static int loop_write_zero_blocking_writer_fiber(void *userdata) {
+ LoopWriteZeroBlockingContext *ctx = ASSERT_PTR(userdata);
+
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ /* timeout == 0 on a *blocking* fd from a fiber: the fast EAGAIN return isn't possible, so
+ * loop_write_full() takes the fiber path. The reader fiber drains the pipe, letting our
+ * write complete via fiber suspension/resume. */
+ _cleanup_free_ char *big_buf = malloc0(ctx->total);
+ ASSERT_NOT_NULL(big_buf);
+ int r = loop_write_full(ctx->pipefd[1], big_buf, ctx->total, /* timeout= */ 0);
+
+ ASSERT_EQ(ctx->order, 2);
+ return r;
+}
+
+static int loop_write_zero_blocking_reader_fiber(void *userdata) {
+ LoopWriteZeroBlockingContext *ctx = ASSERT_PTR(userdata);
+
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ _cleanup_free_ char *buf = malloc(ctx->total);
+ ASSERT_NOT_NULL(buf);
+ ssize_t n = loop_read(ctx->pipefd[0], buf, ctx->total, /* do_poll= */ true);
+ if (n < 0)
+ return (int) n;
+ return (int) n;
+}
+
+/* Test: timeout == 0 on a blocking fd from a fiber takes the fiber path (suspends until the peer
+ * drains) instead of blocking the entire thread. */
+TEST(loop_write_zero_timeout_blocking) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+ ASSERT_OK_ERRNO(fcntl(pipefd[1], F_SETPIPE_SZ, 1));
+
+ /* F_SETPIPE_SZ rounds up to the kernel's pipe minimum (typically a page); query the actual
+ * size and write more than that, so the write must wait on the reader regardless of page size. */
+ int pipe_sz = fcntl(pipefd[1], F_GETPIPE_SZ);
+ ASSERT_OK_ERRNO(pipe_sz);
+
+ LoopWriteZeroBlockingContext ctx = { .pipefd = pipefd, .total = (size_t) pipe_sz * 2 };
+
+ _cleanup_(sd_future_unrefp) sd_future *fw = NULL, *fr = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-write-zt-blk", loop_write_zero_blocking_writer_fiber,
+ &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 0));
+ ASSERT_OK(sd_fiber_new(e, "loop-read-zt-blk", loop_write_zero_blocking_reader_fiber,
+ &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(fw));
+ ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.total);
+}
+
+static int loop_read_no_poll_nonblock_fiber(void *userdata) {
+ int fd = PTR_TO_INT(userdata);
+ char buf[64];
+
+ /* Empty non-blocking pipe + do_poll=false: on a fiber loop_read() must take the non-fiber
+ * path and return -EAGAIN immediately rather than suspending. */
+ return (int) loop_read(fd, buf, sizeof(buf), /* do_poll= */ false);
+}
+
+/* Test: do_poll == false on a non-blocking fd from a fiber preserves the "don't wait" semantic
+ * and returns -EAGAIN when no data is available, instead of suspending the fiber. */
+TEST(loop_read_no_poll_nonblock) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC | O_NONBLOCK));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-read-np-nb", loop_read_no_poll_nonblock_fiber,
+ INT_TO_PTR(pipefd[0]), /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), EAGAIN);
+}
+
+typedef struct LoopReadNoPollBlockingContext {
+ int *pipefd;
+ const char *data;
+ size_t len;
+ int order;
+} LoopReadNoPollBlockingContext;
+
+static int loop_read_no_poll_blocking_reader_fiber(void *userdata) {
+ LoopReadNoPollBlockingContext *ctx = ASSERT_PTR(userdata);
+ char buf[64];
+
+ ASSERT_EQ(ctx->order, 0);
+ ctx->order = 1;
+
+ /* do_poll == false on a *blocking* fd from a fiber: the fast EAGAIN return isn't possible,
+ * so loop_read() takes the fiber path and suspends until the writer fiber feeds data. */
+ ssize_t n = loop_read(ctx->pipefd[0], buf, sizeof(buf), /* do_poll= */ false);
+
+ ASSERT_EQ(ctx->order, 2);
+
+ if (n < 0)
+ return (int) n;
+ if ((size_t) n != ctx->len || memcmp(buf, ctx->data, ctx->len) != 0)
+ return -EIO;
+
+ return (int) n;
+}
+
+static int loop_read_no_poll_blocking_writer_fiber(void *userdata) {
+ LoopReadNoPollBlockingContext *ctx = ASSERT_PTR(userdata);
+
+ ASSERT_EQ(ctx->order, 1);
+ ctx->order = 2;
+
+ int r = loop_write(ctx->pipefd[1], ctx->data, ctx->len);
+ if (r < 0)
+ return r;
+
+ ctx->pipefd[1] = safe_close(ctx->pipefd[1]);
+ return 0;
+}
+
+/* Test: do_poll == false on a blocking fd from a fiber takes the fiber path (suspends until the
+ * peer feeds data) instead of blocking the entire thread. */
+TEST(loop_read_no_poll_blocking) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ static const char payload[] = "no-poll";
+ LoopReadNoPollBlockingContext ctx = {
+ .pipefd = pipefd,
+ .data = payload,
+ .len = sizeof(payload) - 1,
+ };
+
+ _cleanup_(sd_future_unrefp) sd_future *fr = NULL, *fw = NULL;
+ ASSERT_OK(sd_fiber_new(e, "loop-read-np-blk", loop_read_no_poll_blocking_reader_fiber,
+ &ctx, /* destroy= */ NULL, &fr));
+ ASSERT_OK(sd_future_set_priority(fr, 0));
+ ASSERT_OK(sd_fiber_new(e, "loop-write-np-blk", loop_read_no_poll_blocking_writer_fiber,
+ &ctx, /* destroy= */ NULL, &fw));
+ ASSERT_OK(sd_future_set_priority(fw, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_EQ(sd_future_result(fr), (int) ctx.len);
+ ASSERT_OK_ZERO(sd_future_result(fw));
+}
+
+/* Test: loop_*() helpers transparently fall back to blocking I/O when called outside any
+ * fiber context. */
+TEST(loop_read_write_fallback) {
+ _cleanup_close_pair_ int pipefd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(pipe2(pipefd, O_CLOEXEC));
+
+ ASSERT_OK(loop_write(pipefd[1], "fallback", STRLEN("fallback")));
+
+ char buf[16];
+ ssize_t n = loop_read(pipefd[0], buf, STRLEN("fallback"), /* do_poll= */ true);
+ ASSERT_OK_EQ(n, (ssize_t) STRLEN("fallback"));
+ ASSERT_EQ(memcmp(buf, "fallback", STRLEN("fallback")), 0);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd/sd-future/test-fiber.c b/src/libsystemd/sd-future/test-fiber.c
new file mode 100644
index 0000000000000..de9b61e29a52b
--- /dev/null
+++ b/src/libsystemd/sd-future/test-fiber.c
@@ -0,0 +1,954 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-event.h"
+#include "sd-future.h"
+
+#include "tests.h"
+#include "time-util.h"
+
+static int simple_fiber(void *userdata) {
+ int *value = ASSERT_PTR(userdata);
+ return *value;
+}
+
+TEST(fiber_simple) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int value = 5;
+ ASSERT_OK(sd_fiber_new(e, "simple", simple_fiber, &value, NULL, &f));
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_EQ(sd_future_result(f), 5);
+}
+
+/* Fiber that yields once */
+static int yielding_fiber(void *userdata) {
+ int *counter = userdata;
+ (*counter)++;
+
+ sd_fiber_yield();
+
+ (*counter)++;
+ return 0;
+}
+
+/* Test: Single fiber that yields */
+TEST(fiber_single_yield) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "yielding", yielding_fiber, &counter, /* destroy= */ NULL, &f));
+
+ /* First iteration: fiber runs until first yield */
+ ASSERT_EQ(counter, 0);
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_EQ(counter, 1);
+
+ /* Second iteration: fiber runs from yield to completion */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_EQ(counter, 2);
+
+ /* No more fibers to run */
+ ASSERT_OK_ZERO(sd_event_loop(e));
+}
+
+static int counting_fiber(void *userdata) {
+ int counter = 0;
+
+ for (int i = 0; i < 5; i++) {
+ counter++;
+ sd_fiber_yield();
+ }
+
+ return counter;
+}
+
+/* Test: Multiple fibers yielding cooperatively */
+TEST(fiber_multiple_yield) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[5] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++) {
+ _cleanup_free_ char *name = NULL;
+ ASSERT_OK(asprintf(&name, "counting-%zu", i));
+ ASSERT_OK(sd_fiber_new(e, name, counting_fiber, NULL, /* destroy= */ NULL, &fibers[i]));
+ }
+
+ ASSERT_OK(sd_event_loop(e));
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK_EQ(sd_future_result(fibers[i]), 5);
+}
+
+static int priority_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+
+ (*counter)++;
+ sd_fiber_yield();
+
+ return *counter;
+}
+
+/* Test: Priority-based scheduling */
+TEST(fiber_priority_ascending) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[5] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+ int counter = 0;
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++) {
+ _cleanup_free_ char *name = NULL;
+ ASSERT_OK(asprintf(&name, "priority-%zu", i));
+ ASSERT_OK(sd_fiber_new(e, name, priority_fiber, &counter, /* destroy= */ NULL, &fibers[i]));
+ ASSERT_OK(sd_future_set_priority(fibers[i], i));
+ }
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* The fibers have ascending priorities, so we the first one to run to completion,
+ * followed by the second one, etc. */
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_EQ(sd_future_result(fibers[i]), (int) i + 1);
+}
+
+TEST(fiber_priority_identical) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[5] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+ int counter = 0;
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++) {
+ _cleanup_free_ char *name = NULL;
+ ASSERT_OK(asprintf(&name, "priority-%zu", i));
+ ASSERT_OK(sd_fiber_new(e, name, priority_fiber, &counter, /* destroy= */ NULL, &fibers[i]));
+ }
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* The fibers have the same priorities, so we expect all of them to run once first, and then they'll
+ * all run again another time, so they should all return the same value. */
+
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_EQ(sd_future_result(fibers[i]), (int) 5);
+}
+
+static int error_fiber(void *userdata) {
+ return -ENOENT;
+}
+
+TEST(fiber_error_return) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "error", error_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_EQ(sd_future_result(f), -ENOENT);
+}
+
+static int cancel_fiber(void *userdata) {
+ return sd_fiber_yield();
+}
+
+TEST(fiber_cancel_basic) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int value = 42;
+ ASSERT_OK(sd_fiber_new(e, "cancel", cancel_fiber, &value, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_future_cancel(f));
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), ECANCELED);
+}
+
+static int fiber_that_yields(void *userdata) {
+ int *yield_count = userdata;
+ int r;
+
+ for (int i = 0; i < 5; i++) {
+ (*yield_count)++;
+ r = sd_fiber_yield();
+ if (r < 0)
+ return r; /* Propagate cancellation error */
+ }
+
+ return 0;
+}
+
+/* Test: fiber_yield() returns error when fiber is cancelled externally */
+TEST(fiber_cancel_propagation_via_yield) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int yield_count = 0;
+ ASSERT_OK(sd_fiber_new(e, "yielding", fiber_that_yields, &yield_count, /* destroy= */ NULL, &f));
+
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_EQ(yield_count, 1);
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_EQ(yield_count, 2);
+
+ ASSERT_OK(sd_future_cancel(f));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* sd_fiber should have been cancelled */
+ ASSERT_ERROR(sd_future_result(f), ECANCELED);
+ ASSERT_EQ(yield_count, 2);
+}
+
+/* Test: Cancel a fiber that has already completed */
+TEST(fiber_cancel_completed) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int value = 42;
+ ASSERT_OK(sd_fiber_new(e, "simple", simple_fiber, &value, /* destroy= */ NULL, &f));
+
+ /* Run the fiber to completion */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Canceling a completed fiber should be a no-op */
+ ASSERT_OK(sd_future_cancel(f));
+ ASSERT_EQ(sd_future_result(f), 42);
+}
+
+static int multiple_yield_fiber(void *userdata) {
+ int *counter = userdata;
+ int r;
+
+ for (int i = 0; i < 3; i++) {
+ (*counter)++;
+ r = sd_fiber_yield();
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+/* Test: Cancel one fiber among multiple */
+TEST(fiber_cancel_one_of_many) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[3] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+ int counters[3] = {0, 0, 0};
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK(sd_fiber_new(e, "multiple-yield", multiple_yield_fiber, &counters[i], /* destroy= */ NULL, &fibers[i]));
+
+ /* Run one iteration - all fibers yield after incrementing once */
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_EQ(counters[0], 1);
+ ASSERT_EQ(counters[1], 1);
+ ASSERT_EQ(counters[2], 1);
+
+ /* Cancel the second fiber */
+ ASSERT_OK(sd_future_cancel(fibers[1]));
+
+ /* Run to completion */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* First and third fibers should complete normally */
+ ASSERT_EQ(counters[0], 3);
+ ASSERT_EQ(counters[2], 3);
+ ASSERT_EQ(sd_future_result(fibers[0]), 0);
+ ASSERT_EQ(sd_future_result(fibers[2]), 0);
+
+ /* Second fiber should be canceled with counter at 1 */
+ ASSERT_EQ(counters[1], 1);
+ ASSERT_EQ(sd_future_result(fibers[1]), -ECANCELED);
+}
+
+/* Test: sd_fiber_await() - wait for a fiber to complete */
+static int slow_fiber(void *userdata) {
+ int *counter = userdata;
+
+ for (int i = 0; i < 3; i++) {
+ (*counter)++;
+ sd_fiber_yield();
+ }
+
+ return 42;
+}
+
+static int waiting_fiber(void *userdata) {
+ sd_future *target = userdata;
+ int r;
+
+ r = sd_fiber_await(target);
+ if (r < 0)
+ return r;
+
+ r = sd_future_result(target);
+ return r == 42 ? 0 : -EIO;
+}
+
+TEST(fiber_wait_for_basic) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ /* Create target fiber with lower priority (runs second) */
+ _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "slow", slow_fiber, &counter, /* destroy= */ NULL, &target));
+ ASSERT_OK(sd_future_set_priority(target, 1));
+
+ /* Create waiter fiber with higher priority (runs first) */
+ ASSERT_OK(sd_fiber_new(e, "waiting", waiting_fiber, target, /* destroy= */ NULL, &waiter));
+ ASSERT_OK(sd_future_set_priority(waiter, 0));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(waiter));
+ ASSERT_OK_EQ(sd_future_result(target), 42);
+ ASSERT_EQ(counter, 3);
+}
+
+/* Test: wait for already completed fiber */
+static int wait_for_completed_fiber(void *userdata) {
+ sd_future *target = userdata;
+ int r;
+
+ r = sd_fiber_await(target);
+ if (r < 0)
+ return r;
+
+ return sd_future_result(target);
+}
+
+TEST(fiber_wait_for_completed) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL;
+ int value = 100;
+
+ /* Create target fiber with higher priority (runs first) */
+ ASSERT_OK(sd_fiber_new(e, "simple", simple_fiber, &value, /* destroy= */ NULL, &target));
+ ASSERT_OK(sd_future_set_priority(target, 0));
+ /* Create waiter fiber with lower priority (runs second, after target completes) */
+ ASSERT_OK(sd_fiber_new(e, "wait-for-completed", wait_for_completed_fiber, target, /* destroy= */ NULL, &waiter));
+ ASSERT_OK(sd_future_set_priority(waiter, 1));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK_EQ(sd_future_result(waiter), 100);
+ ASSERT_OK_EQ(sd_future_result(target), 100);
+}
+
+/* Test: wait for cancelled fiber */
+static int wait_for_cancelled_fiber(void *userdata) {
+ sd_future *target = userdata;
+ int r;
+
+ r = sd_fiber_await(target);
+ if (r < 0)
+ return r;
+
+ return sd_future_result(target);
+}
+
+TEST(fiber_wait_for_cancelled) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *target = NULL, *waiter = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "yielding", fiber_that_yields, &counter, /* destroy= */ NULL, &target));
+ ASSERT_OK(sd_fiber_new(e, "wait-for-cancelled", wait_for_cancelled_fiber, target, /* destroy= */ NULL, &waiter));
+
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+ ASSERT_OK_POSITIVE(sd_event_run(e, 0));
+
+ ASSERT_OK(sd_future_cancel(target));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_ERROR(sd_future_result(waiter), ECANCELED);
+ ASSERT_ERROR(sd_future_result(target), ECANCELED);
+}
+
+/* Test: multiple fibers waiting for the same target */
+static int multi_waiter_fiber(void *userdata) {
+ sd_future *target = userdata;
+ int r;
+
+ r = sd_fiber_await(target);
+ if (r < 0)
+ return r;
+
+ return sd_future_result(target);
+}
+
+TEST(fiber_wait_for_multiple_waiters) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *target = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "slow", slow_fiber, &counter, /* destroy= */ NULL, &target));
+
+ sd_future *waiters[3] = {};
+ CLEANUP_ELEMENTS(waiters, sd_future_unref_array_clear);
+ for (size_t i = 0; i < ELEMENTSOF(waiters); i++)
+ ASSERT_OK(sd_fiber_new(e, "multi-waiter", multi_waiter_fiber, target, /* destroy= */ NULL, &waiters[i]));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ for (size_t i = 0; i < ELEMENTSOF(waiters); i++)
+ ASSERT_OK_EQ(sd_future_result(waiters[i]), 42);
+
+ ASSERT_OK_EQ(sd_future_result(target), 42);
+ ASSERT_EQ(counter, 3);
+}
+
+/* Test: chain of waiting fibers */
+static int chain_waiter_fiber(void *userdata) {
+ sd_future *target = userdata;
+ int r;
+
+ r = sd_fiber_await(target);
+ if (r < 0)
+ return r;
+
+ r = sd_future_result(target);
+ return r + 1;
+}
+
+TEST(fiber_wait_for_chain) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *fibers[5] = {};
+ CLEANUP_ELEMENTS(fibers, sd_future_unref_array_clear);
+ int value = 10;
+
+ ASSERT_OK(sd_fiber_new(e, "simple", simple_fiber, &value, /* destroy= */ NULL, &fibers[0]));
+
+ /* Each subsequent fiber waits for the previous and adds 1 */
+ for (size_t i = 1; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK(sd_fiber_new(e, "chain-waiter", chain_waiter_fiber, fibers[i - 1], /* destroy= */ NULL, &fibers[i]));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* Check results: 10, 11, 12, 13, 14 */
+ for (size_t i = 0; i < ELEMENTSOF(fibers); i++)
+ ASSERT_OK_EQ(sd_future_result(fibers[i]), 10 + (int) i);
+}
+
+static int nested_run_inner_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+
+ (*counter)++;
+ int r = sd_fiber_yield();
+ if (r < 0)
+ return r;
+ (*counter)++;
+
+ return 0;
+}
+
+static int nested_run_outer_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *nested = NULL;
+ int r;
+
+ /* Yield once before the nested loop: this forces the outer fiber to later resume through its own
+ * swapcontext() machinery after the inner fiber_run() has executed, which is exactly the path that
+ * breaks when the resume context is stored thread-globally instead of per-fiber. */
+ r = sd_fiber_yield();
+ if (r < 0)
+ return r;
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ r = sd_event_set_exit_on_idle(inner, true);
+ if (r < 0)
+ return r;
+
+ /* Spawn a fiber on the inner event loop. Driving it via sd_event_loop(inner) causes fiber_run() to
+ * be invoked while we are already executing inside fiber_run() for the outer fiber. */
+ r = sd_fiber_new(inner, "inner", nested_run_inner_fiber, counter, /* destroy= */ NULL, &nested);
+ if (r < 0)
+ return r;
+
+ r = sd_event_loop(inner);
+ if (r < 0)
+ return r;
+
+ r = sd_future_result(nested);
+ if (r < 0)
+ return r;
+
+ /* Yield again after the inner loop has returned. If the outer fiber's resume context was clobbered
+ * by the nested fiber_run(), the swapcontext() underneath this yield would jump into an already
+ * unwound stack frame. */
+ return sd_fiber_yield();
+}
+
+TEST(fiber_nested_run) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *outer = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "outer", nested_run_outer_fiber, &counter, /* destroy= */ NULL, &outer));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(outer));
+
+ /* The inner fiber incremented the counter once before yielding and once after resuming. */
+ ASSERT_EQ(counter, 2);
+}
+
+static int nested_current_check_inner_fiber(void *userdata) {
+ sd_future **slots = ASSERT_PTR(userdata);
+
+ slots[1] = sd_fiber_get_current();
+ int r = sd_fiber_yield();
+ if (r < 0)
+ return r;
+ /* After resuming, the current fiber must still be us, not the outer fiber that was current when
+ * fiber_run() re-entered. */
+ if (sd_fiber_get_current() != slots[1])
+ return -EBADF;
+
+ return 0;
+}
+
+static int nested_current_check_outer_fiber(void *userdata) {
+ sd_future **slots = ASSERT_PTR(userdata);
+ _cleanup_(sd_event_unrefp) sd_event *inner = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *nested = NULL;
+ int r;
+
+ slots[0] = sd_fiber_get_current();
+
+ r = sd_event_new(&inner);
+ if (r < 0)
+ return r;
+
+ r = sd_event_set_exit_on_idle(inner, true);
+ if (r < 0)
+ return r;
+
+ r = sd_fiber_new(inner, "inner", nested_current_check_inner_fiber, slots, /* destroy= */ NULL, &nested);
+ if (r < 0)
+ return r;
+
+ r = sd_event_loop(inner);
+ if (r < 0)
+ return r;
+
+ r = sd_future_result(nested);
+ if (r < 0)
+ return r;
+
+ /* After the nested fiber_run() has returned, the current fiber must have been restored to the
+ * outer fiber rather than left as NULL or pointing at the (now freed) inner fiber. */
+ if (sd_fiber_get_current() != slots[0])
+ return -EBADF;
+
+ return 0;
+}
+
+TEST(fiber_nested_run_current_restored) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *slots[2] = {};
+ _cleanup_(sd_future_unrefp) sd_future *outer = NULL;
+ ASSERT_OK(sd_fiber_new(e, "outer", nested_current_check_outer_fiber, slots, /* destroy= */ NULL, &outer));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(outer));
+
+ ASSERT_NOT_NULL(slots[0]);
+ ASSERT_NOT_NULL(slots[1]);
+ ASSERT_TRUE(slots[0] != slots[1]);
+}
+
+static int nested_cancellation_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *nested = NULL;
+ int r;
+
+ if (*counter >= 5)
+ return sd_fiber_sleep(10 * USEC_PER_SEC);
+
+ (*counter)++;
+
+ _cleanup_free_ char *name = NULL;
+ if (asprintf(&name, "nested-cancellation-%i", *counter) < 0)
+ return -ENOMEM;
+
+ /* Create a nested fiber within this fiber */
+ r = sd_fiber_new(sd_fiber_get_event(), name, nested_cancellation_fiber, counter, /* destroy= */ NULL, &nested);
+ if (r < 0)
+ return r;
+
+ /* Wait for the nested fiber to complete */
+ r = sd_fiber_await(nested);
+ if (r < 0)
+ return r;
+
+ /* If we got here without cancellation, verify the nested fiber completed */
+ return sd_future_result(nested);
+}
+
+static int exit_loop_fiber(void *userdata) {
+ /* Just exit the event loop, causing the outer fiber to be cancelled */
+ return sd_event_exit(sd_fiber_get_event(), 0);
+}
+
+TEST(fiber_nested_cancellation) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+
+ int counter = 0;
+
+ /* Create outer fiber with higher priority (runs first) */
+ _cleanup_(sd_future_unrefp) sd_future *outer = NULL;
+ ASSERT_OK(sd_fiber_new(e, "outer", nested_cancellation_fiber, &counter, /* destroy= */ NULL, &outer));
+
+ /* Create exit fiber with lower priority (runs after all nested fibers have suspended) */
+ _cleanup_(sd_future_unrefp) sd_future *exit_fiber = NULL;
+ ASSERT_OK(sd_fiber_new(e, "exit-loop", exit_loop_fiber, NULL, /* destroy= */ NULL, &exit_fiber));
+ ASSERT_OK(sd_future_set_priority(exit_fiber, 1));
+
+ /* Run the event loop - the exit fiber should cause it to exit,
+ * which should cancel the outer fiber, which should cancel the nested fiber, and so forth. */
+ ASSERT_OK(sd_event_loop(e));
+
+ /* The exit fiber should have completed successfully */
+ ASSERT_OK(sd_future_result(exit_fiber));
+
+ /* The outer fiber should have been cancelled */
+ ASSERT_ERROR(sd_future_result(outer), ECANCELED);
+
+ /* The nested fiber was created and incremented counter once before being cancelled */
+ ASSERT_GT(counter, 0);
+}
+
+static int nested_fiber_cleanup_nested_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+ int r;
+
+ r = sd_fiber_sleep(10 * USEC_PER_SEC);
+ if (r == -ECANCELED)
+ (*counter)++;
+ else if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int nested_fiber_cleanup_fiber(void *userdata) {
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *nested = NULL;
+ int r;
+
+ /* Create a nested fiber within this fiber. */
+ r = sd_fiber_new(sd_fiber_get_event(), "nested", nested_fiber_cleanup_nested_fiber, userdata, /* destroy= */ NULL, &nested);
+ if (r < 0)
+ return r;
+
+ /* Yield and then exit, the nested fiber should be cancelled. */
+ return sd_fiber_yield();
+}
+
+TEST(nested_fiber_cleanup) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *outer = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "outer", nested_fiber_cleanup_fiber, &counter, /* destroy= */ NULL, &outer));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ /* The outer fiber should have finished normally */
+ ASSERT_OK(sd_future_result(outer));
+
+ /* The nested fiber was created and incremented its counter once when it was cancelled. */
+ ASSERT_GT(counter, 0);
+}
+
+static int priority_check_fiber(void *userdata) {
+ int64_t *ret = ASSERT_PTR(userdata);
+
+ /* Verify that sd_fiber_get_priority() returns the value set via sd_future_set_priority() */
+ *ret = sd_fiber_get_priority();
+
+ /* Exercise sd_fiber_sleep() which internally creates a time future. This verifies that the priority
+ * is correctly propagated to the time event source (via f->time.source, not f->io.source). */
+ return sd_fiber_sleep(1);
+}
+
+TEST(fiber_priority_get) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ int64_t got_priority = 0;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "priority-check", priority_check_fiber, &got_priority, /* destroy= */ NULL, &f));
+ ASSERT_OK(sd_future_set_priority(f, 10));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK(sd_future_result(f));
+
+ /* Verify priority was stored and retrievable */
+ ASSERT_EQ(got_priority, 10);
+}
+
+static int floating_fiber(void *userdata) {
+ int *counter = ASSERT_PTR(userdata);
+
+ (*counter)++;
+ int r = sd_fiber_yield();
+ if (r < 0)
+ return r;
+ (*counter)++;
+
+ return 0;
+}
+
+TEST(fiber_floating) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "floating", floating_fiber, &counter, /* destroy= */ NULL, &f));
+
+ ASSERT_OK_ZERO(sd_fiber_get_floating(f));
+ ASSERT_OK(sd_fiber_set_floating(f, true));
+ ASSERT_OK_POSITIVE(sd_fiber_get_floating(f));
+
+ /* Drop our handle: the floating ref keeps the future alive until the fiber resolves, after
+ * which the self-unref frees it. If this didn't work we'd either leak (visible under ASan) or
+ * trip fiber_free()'s "state == COMPLETED" assertion. */
+ f = sd_future_unref(f);
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_EQ(counter, 2);
+}
+
+static int drop_extra_ref(sd_future *f) {
+ /* Drop an extra ref the test installed before the callback fires. After this returns, the
+ * floating self-ref is the only thing keeping the future alive — exercising the path where
+ * the floating unref in fiber_run() is the last unref. */
+ sd_future_unref(f);
+ return 0;
+}
+
+TEST(fiber_floating_callback_drops_ref) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ sd_future *f = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "floating-cb", floating_fiber, &counter, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_fiber_set_floating(f, true));
+
+ /* Bump the ref for the callback to drop, then install the callback. */
+ sd_future_ref(f);
+ ASSERT_OK(sd_future_set_callback(f, drop_extra_ref, NULL));
+
+ /* Drop our handle. Refs remaining: floating self-ref + the extra ref the callback will drop. */
+ f = sd_future_unref(f);
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_EQ(counter, 2);
+}
+
+TEST(fiber_floating_toggle) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ int counter = 0;
+ ASSERT_OK(sd_fiber_new(e, "floating-toggle", floating_fiber, &counter, /* destroy= */ NULL, &f));
+
+ /* Toggling floating on and off again should leave the refcount unchanged: set_floating(true)
+ * takes a ref and set_floating(false) drops it. If the accounting were off, the subsequent
+ * event loop would either free the future while the fiber still runs (fiber_free assertion)
+ * or leak it. */
+ ASSERT_OK(sd_fiber_set_floating(f, true));
+ ASSERT_OK(sd_fiber_set_floating(f, false));
+ ASSERT_OK_ZERO(sd_fiber_get_floating(f));
+
+ /* Setting floating to the same value twice should be a no-op. */
+ ASSERT_OK(sd_fiber_set_floating(f, false));
+ ASSERT_OK(sd_fiber_set_floating(f, true));
+ ASSERT_OK(sd_fiber_set_floating(f, true));
+
+ /* Drop our handle; the still-floating ref drives cleanup. */
+ f = sd_future_unref(f);
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_EQ(counter, 2);
+}
+
+/* Test: SD_FIBER_TIMEOUT scope expires while the fiber is suspended with no other wakeup source. */
+static int timeout_suspend_fiber(void *userdata) {
+ SD_FIBER_TIMEOUT(50 * USEC_PER_MSEC);
+
+ /* Plain suspend with no other future to wake us — only the deadline timer can resume. */
+ return sd_fiber_suspend();
+}
+
+TEST(fiber_timeout_suspend_expires) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "timeout-suspend", timeout_suspend_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), ETIME);
+}
+
+/* Test: SD_FIBER_TIMEOUT scope around a sleep that finishes before the deadline expires; the
+ * cleanup must cancel the timer cleanly without leaving a stale wakeup. */
+static int timeout_in_time_fiber(void *userdata) {
+ SD_FIBER_TIMEOUT(1 * USEC_PER_SEC);
+ return sd_fiber_sleep(10 * USEC_PER_MSEC);
+}
+
+TEST(fiber_timeout_sleep_in_time) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "in-time", timeout_in_time_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: SD_FIBER_TIMEOUT(USEC_INFINITY) is a no-op — no timer is created and the fiber completes
+ * normally. */
+static int timeout_infinite_fiber(void *userdata) {
+ SD_FIBER_TIMEOUT(USEC_INFINITY);
+ return sd_fiber_sleep(10 * USEC_PER_MSEC);
+}
+
+TEST(fiber_timeout_infinite_no_op) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "infinite", timeout_infinite_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_ZERO(sd_future_result(f));
+}
+
+/* Test: SD_FIBER_WITH_TIMEOUT block form returns -ETIME from the suspend inside it. */
+static int with_timeout_block_fiber(void *userdata) {
+ int r = 0;
+ SD_FIBER_WITH_TIMEOUT(50 * USEC_PER_MSEC)
+ r = sd_fiber_suspend();
+ return r;
+}
+
+TEST(fiber_with_timeout_block) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "with-timeout", with_timeout_block_fiber, NULL, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_ERROR(sd_future_result(f), ETIME);
+}
+
+/* Test: nested SD_FIBER_TIMEOUT — inner scope's timer fires first; once we're back in just the
+ * outer scope, suspending again must time out via the still-armed outer timer. */
+static int nested_timeout_fiber(void *userdata) {
+ int *fired = ASSERT_PTR(userdata);
+
+ SD_FIBER_TIMEOUT(50 * USEC_PER_MSEC); /* outer */
+
+ SD_FIBER_WITH_TIMEOUT(20 * USEC_PER_MSEC) { /* inner — expires first */
+ int r = sd_fiber_suspend();
+ if (r != -ETIME)
+ return -ENOTRECOVERABLE;
+ (*fired)++;
+ }
+
+ /* Inner scope is gone, but the outer timer is still armed (it only used ~20ms of its
+ * 100ms budget). Suspending again must eventually wake us with -ETIME. */
+ int r = sd_fiber_suspend();
+ if (r != -ETIME)
+ return -ENOTRECOVERABLE;
+ (*fired)++;
+
+ return 0;
+}
+
+TEST(fiber_timeout_nested) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
+
+ int fired = 0;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ ASSERT_OK(sd_fiber_new(e, "nested-timeout", nested_timeout_fiber, &fired, /* destroy= */ NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+ ASSERT_OK_ZERO(sd_future_result(f));
+ ASSERT_EQ(fired, 2);
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/test/test-sd-hwdb.c b/src/libsystemd/sd-hwdb/test-sd-hwdb.c
similarity index 100%
rename from src/test/test-sd-hwdb.c
rename to src/libsystemd/sd-hwdb/test-sd-hwdb.c
diff --git a/src/test/test-id128.c b/src/libsystemd/sd-id128/test-id128.c
similarity index 100%
rename from src/test/test-id128.c
rename to src/libsystemd/sd-id128/test-id128.c
diff --git a/src/libsystemd/sd-journal/test-journal-append.c b/src/libsystemd/sd-journal/test-journal-append.c
index 75a1fce6fc98c..c71240660dcd7 100644
--- a/src/libsystemd/sd-journal/test-journal-append.c
+++ b/src/libsystemd/sd-journal/test-journal-append.c
@@ -153,7 +153,7 @@ int main(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP: {
diff --git a/src/libsystemd/sd-json/json-stream.c b/src/libsystemd/sd-json/json-stream.c
index 3775691d47f67..629b82dc1d0d9 100644
--- a/src/libsystemd/sd-json/json-stream.c
+++ b/src/libsystemd/sd-json/json-stream.c
@@ -6,12 +6,12 @@
#include
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-json.h"
#include "alloc-util.h"
#include "errno-util.h"
#include "fd-util.h"
-#include "io-util.h"
#include "iovec-util.h"
#include "json-stream.h"
#include "list.h"
@@ -430,7 +430,7 @@ int json_stream_wait(JsonStream *s, usec_t timeout) {
};
}
- r = ppoll_usec(pollfd, n_poll_fd, timeout);
+ r = sd_fiber_poll(pollfd, n_poll_fd, timeout);
if (ERRNO_IS_NEG_TRANSIENT(r))
/* Treat EINTR as not a timeout, but also nothing happened, and the caller gets
* a chance to call back into us. */
diff --git a/src/libsystemd/sd-json/json-util.c b/src/libsystemd/sd-json/json-util.c
index c321579ef5093..40102a69989ed 100644
--- a/src/libsystemd/sd-json/json-util.c
+++ b/src/libsystemd/sd-json/json-util.c
@@ -291,6 +291,7 @@ int json_dispatch_path(const char *name, sd_json_variant *variant, sd_json_dispa
int json_dispatch_strv_path(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
_cleanup_strv_free_ char **n = NULL;
char ***l = ASSERT_PTR(userdata);
+ size_t s = 0;
int r;
assert(variant);
@@ -310,7 +311,7 @@ int json_dispatch_strv_path(const char *name, sd_json_variant *variant, sd_json_
if (r < 0)
return r;
- r = strv_extend(&n, a);
+ r = strv_extend_with_size(&n, &s, a);
if (r < 0)
return json_log_oom(variant, flags);
}
@@ -652,6 +653,9 @@ int json_dispatch_strv_environment(const char *name, sd_json_variant *variant, s
if (!sd_json_variant_is_array(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
+ if (sd_json_variant_elements(variant) > ENVIRONMENT_ASSIGNMENTS_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(E2BIG), "Too many environment variable assignments.");
+
sd_json_variant *i;
JSON_VARIANT_ARRAY_FOREACH(i, variant) {
const char *e;
diff --git a/src/libsystemd/sd-json/json-util.h b/src/libsystemd/sd-json/json-util.h
index cea2d368b43db..34d79d5238aaa 100644
--- a/src/libsystemd/sd-json/json-util.h
+++ b/src/libsystemd/sd-json/json-util.h
@@ -9,6 +9,8 @@
#include "sd-forward.h"
#include "string-util.h" /* IWYU pragma: keep */
+#define ENVIRONMENT_ASSIGNMENTS_MAX 1024U
+
#define JSON_VARIANT_REPLACE(v, q) \
do { \
typeof(v)* _v = &(v); \
diff --git a/src/libsystemd/sd-json/sd-json.c b/src/libsystemd/sd-json/sd-json.c
index 4c541275c42c5..659dffb2bac7e 100644
--- a/src/libsystemd/sd-json/sd-json.c
+++ b/src/libsystemd/sd-json/sd-json.c
@@ -5294,10 +5294,8 @@ _public_ int sd_json_dispatch_full(
done++;
} else {
- if (flags & SD_JSON_ALLOW_EXTENSIONS) {
- json_log(value, flags|SD_JSON_DEBUG, 0, "Unrecognized object field '%s', assuming extension.", sd_json_variant_string(key));
+ if (flags & SD_JSON_ALLOW_EXTENSIONS)
continue;
- }
json_log(value, flags, 0, "Unexpected object field '%s'.", sd_json_variant_string(key));
if (flags & SD_JSON_PERMISSIVE)
@@ -5663,6 +5661,7 @@ _public_ int sd_json_dispatch_strv(const char *name, sd_json_variant *variant, s
_cleanup_strv_free_ char **l = NULL;
char ***s = userdata;
sd_json_variant *e;
+ size_t n = 0;
int r;
assert_return(variant, -EINVAL);
@@ -5696,7 +5695,7 @@ _public_ int sd_json_dispatch_strv(const char *name, sd_json_variant *variant, s
if ((flags & SD_JSON_STRICT) && !string_is_safe(sd_json_variant_string(e), STRING_ALLOW_EMPTY|STRING_ALLOW_GLOBS))
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' contains unsafe characters, refusing.", strna(name));
- r = strv_extend(&l, sd_json_variant_string(e));
+ r = strv_extend_with_size(&l, &n, sd_json_variant_string(e));
if (r < 0)
return json_log(e, flags, r, "Failed to append array element: %m");
}
diff --git a/src/test/test-json.c b/src/libsystemd/sd-json/test-json.c
similarity index 100%
rename from src/test/test-json.c
rename to src/libsystemd/sd-json/test-json.c
diff --git a/src/test/test-sd-path.c b/src/libsystemd/sd-path/test-sd-path.c
similarity index 100%
rename from src/test/test-sd-path.c
rename to src/libsystemd/sd-path/test-sd-path.c
diff --git a/src/libsystemd/sd-varlink/sd-varlink.c b/src/libsystemd/sd-varlink/sd-varlink.c
index 2a5f677ef37d0..2c94623adbc36 100644
--- a/src/libsystemd/sd-varlink/sd-varlink.c
+++ b/src/libsystemd/sd-varlink/sd-varlink.c
@@ -6,6 +6,7 @@
#include "sd-daemon.h"
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-varlink.h"
#include "alloc-util.h"
@@ -959,6 +960,178 @@ static int generic_method_get_interface_description(
SD_JSON_BUILD_PAIR_STRING("description", text));
}
+static int varlink_dispatch_sentinel(sd_varlink *v) {
+ int r;
+
+ assert(v);
+ assert(v->sentinel);
+
+ if (v->previous) {
+ r = json_stream_enqueue_full(&v->stream, v->previous, v->previous_fds, v->n_previous_fds);
+ if (r >= 0) {
+ v->previous = sd_json_variant_unref(v->previous);
+ v->previous_fds = mfree(v->previous_fds);
+ v->n_previous_fds = 0;
+ /* Mirror sd_varlink_reply()'s post-enqueue state machine: PENDING_* means we're
+ * outside the dispatch stack frame (e.g. called from varlink_fiber_entry after
+ * the fiber returned), so we go straight to IDLE_SERVER ourselves. PROCESSING_*
+ * means we're inside varlink_dispatch_method(), which will transition us. */
+ if (IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE)) {
+ varlink_clear_current(v);
+ varlink_set_state(v, VARLINK_IDLE_SERVER);
+ } else
+ varlink_set_state(v, VARLINK_PROCESSED_METHOD);
+ }
+
+ return r;
+ }
+
+ char *sentinel = TAKE_PTR(v->sentinel);
+
+ /* Propagate the sentinel to the client if one was configured and no replies were enqueued by
+ * the callback. */
+ if (sentinel == POINTER_MAX)
+ r = sd_varlink_reply(v, NULL);
+ else {
+ r = sd_varlink_error(v, sentinel, NULL);
+ /* sd_varlink_error() deliberately returns a negative
+ * errno mapped from the error id on success (so method
+ * callbacks can `return sd_varlink_error(...);` to
+ * enqueue a reply and propagate a matching errno in one
+ * go). For sentinel dispatch we don't care about that
+ * mapping — the reply is either enqueued or not, which
+ * we detect via the state transition instead. */
+ if (IN_SET(v->state, VARLINK_PROCESSED_METHOD, VARLINK_IDLE_SERVER))
+ r = 0;
+ }
+
+ if (sentinel != POINTER_MAX)
+ free(sentinel);
+
+ return r;
+}
+
+typedef struct VarlinkFiberData {
+ sd_varlink *link;
+ sd_json_variant *parameters;
+ sd_varlink_method_flags_t flags;
+ void *userdata;
+ sd_varlink_method_t callback;
+} VarlinkFiberData;
+
+static VarlinkFiberData* varlink_fiber_data_free(VarlinkFiberData *d) {
+ if (!d)
+ return NULL;
+
+ sd_json_variant_unref(d->parameters);
+ sd_varlink_unref(d->link);
+ return mfree(d);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkFiberData*, varlink_fiber_data_free);
+
+static void varlink_fiber_data_destroy(void *userdata) {
+ varlink_fiber_data_free(userdata);
+}
+
+static int varlink_fiber_entry(void *userdata) {
+ VarlinkFiberData *d = ASSERT_PTR(userdata);
+ sd_varlink *v = d->link;
+ int r;
+
+ r = d->callback(v, d->parameters, d->flags, d->userdata);
+
+ /* The fiber runs after varlink_dispatch_method() has already transitioned the state from
+ * VARLINK_PROCESSING_METHOD{,_MORE} to VARLINK_PENDING_METHOD{,_MORE}, so that's what we match
+ * here to decide whether the call still needs a reply. Any other state (e.g. IDLE_SERVER after
+ * the callback replied, or DISCONNECTED after sd_varlink_close()) means no fixup is needed. */
+ if (!IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE))
+ return r;
+
+ if (r < 0) {
+ varlink_log_errno(v, r, "Fiber returned error: %m");
+
+ /* Propagate error to the client if the method call remains unanswered. */
+ r = sd_varlink_error_errno(v, r);
+ } else if (v->sentinel) {
+ r = varlink_dispatch_sentinel(v);
+ if (r < 0)
+ varlink_log_errno(v, r, "Failed to process sentinel: %m");
+ } else if (v->n_ref <= 2) {
+ /* Bare minimum refs (server + fiber data) means the connection wasn't stashed
+ * to reply later, so the fiber was supposed to reply itself but didn't. */
+ r = varlink_log_errno(v, SYNTHETIC_ERRNO(EPROTO),
+ "Fiber returned without enqueuing a reply or stashing connection, failing.");
+ goto fail;
+ } else
+ r = 0;
+
+ /* If we didn't manage to enqueue a response, then fail the connection completely. */
+ if (r < 0 && IN_SET(v->state, VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE))
+ goto fail;
+
+ return r;
+
+fail:
+ varlink_set_state(v, VARLINK_PROCESSING_FAILURE);
+ varlink_dispatch_local_error(v, SD_VARLINK_ERROR_PROTOCOL);
+ sd_varlink_close(v);
+
+ return r;
+}
+
+static int varlink_dispatch_fiber(sd_varlink *v, const char *method, sd_varlink_method_t callback, sd_json_variant *parameters, sd_varlink_method_flags_t flags) {
+ int r;
+
+ assert(v);
+ assert(v->server);
+ assert(method);
+ assert(callback);
+
+ if (!v->server->event)
+ return varlink_log_errno(v, SYNTHETIC_ERRNO(ENOTCONN),
+ "Cannot dispatch fiber method without event loop.");
+
+ _cleanup_(varlink_fiber_data_freep) VarlinkFiberData *d = new(VarlinkFiberData, 1);
+ if (!d)
+ return log_oom_debug();
+
+ *d = (VarlinkFiberData) {
+ .link = sd_varlink_ref(v),
+ .parameters = sd_json_variant_ref(parameters),
+ .flags = flags,
+ .userdata = v->userdata,
+ .callback = callback,
+ };
+
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ r = sd_fiber_new(v->server->event, method, varlink_fiber_entry, d, varlink_fiber_data_destroy, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(d); /* The fiber owns the data now. */
+
+ /* Run the fiber at a higher priority than the connection's quit event source, so that on event
+ * loop exit the fiber's exit source (which cancels it and drives its cleanup) fires before
+ * varlink's quit_callback closes the connection. This lets a fiber handler reply with an error
+ * or flush its sentinel on a still-open connection during graceful shutdown. */
+ int64_t priority;
+ r = sd_event_source_get_priority(v->quit_event_source, &priority);
+ if (r < 0)
+ return r;
+
+ r = sd_future_set_priority(f, priority > INT64_MIN ? priority - 1 : priority);
+ if (r < 0)
+ return r;
+
+ /* Hand the future's lifetime over to the event loop: it'll auto-unref on resolve. */
+ r = sd_fiber_set_floating(f, true);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
static int varlink_dispatch_method(sd_varlink *v) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *parameters = NULL;
sd_varlink_method_flags_t flags = 0;
@@ -1056,7 +1229,13 @@ static int varlink_dispatch_method(sd_varlink *v) {
v->protocol_upgrade || FLAGS_SET(v->server->flags, SD_VARLINK_SERVER_UPGRADABLE));
/* First consult user supplied method implementations */
+ bool is_fiber = false;
callback = hashmap_get(v->server->methods, method);
+ if (!callback) {
+ callback = hashmap_get(v->server->fiber_methods, method);
+ if (callback)
+ is_fiber = true;
+ }
if (!callback) {
if (streq(method, "org.varlink.service.GetInfo"))
callback = generic_method_get_info;
@@ -1108,7 +1287,13 @@ static int varlink_dispatch_method(sd_varlink *v) {
}
if (!invalid) {
- r = callback(v, parameters, flags, v->userdata);
+ if (is_fiber)
+ /* Spawn a fiber to run the callback. The VarlinkFiberData takes a ref on the
+ * connection (bumping n_ref above 2), so the post-callback logic below treats
+ * this as a deferred reply and moves state to PENDING_METHOD. */
+ r = varlink_dispatch_fiber(v, method, callback, parameters, flags);
+ else
+ r = callback(v, parameters, flags, v->userdata);
if (VARLINK_STATE_WANTS_REPLY(v->state)) {
if (r < 0) {
varlink_log_errno(v, r, "Callback for '%s' returned error: %m", method);
@@ -1117,37 +1302,7 @@ static int varlink_dispatch_method(sd_varlink *v) {
* if the method call remains unanswered. */
r = sd_varlink_error_errno(v, r);
} else if (v->sentinel) {
- if (v->previous) {
- r = json_stream_enqueue_full(&v->stream, v->previous, v->previous_fds, v->n_previous_fds);
- if (r >= 0) {
- v->previous = sd_json_variant_unref(v->previous);
- v->previous_fds = mfree(v->previous_fds);
- v->n_previous_fds = 0;
- varlink_set_state(v, VARLINK_PROCESSED_METHOD);
- }
- } else {
- char *sentinel = TAKE_PTR(v->sentinel);
-
- /* Propagate the sentinel to the client if one was configured
- * and no replies were enqueued by the callback. */
- if (sentinel == POINTER_MAX)
- r = sd_varlink_reply(v, NULL);
- else {
- r = sd_varlink_error(v, sentinel, NULL);
- /* sd_varlink_error() deliberately returns a negative
- * errno mapped from the error id on success (so method
- * callbacks can `return sd_varlink_error(...);` to
- * enqueue a reply and propagate a matching errno in one
- * go). For sentinel dispatch we don't care about that
- * mapping — the reply is either enqueued or not, which
- * we detect via the state transition instead. */
- if (v->state == VARLINK_PROCESSED_METHOD)
- r = 0;
- }
-
- if (sentinel != POINTER_MAX)
- free(sentinel);
- }
+ r = varlink_dispatch_sentinel(v);
if (r < 0)
varlink_log_errno(v, r, "Failed to process sentinel for method '%s': %m", method);
} else {
@@ -2599,8 +2754,12 @@ _public_ int sd_varlink_set_sentinel(sd_varlink *v, const char *error_id) {
if (v->state == VARLINK_PROCESSING_METHOD_ONEWAY)
return 0;
- /* This has to be called during a callback, and not after it has exited. */
- assert_return(IN_SET(v->state, VARLINK_PROCESSING_METHOD, VARLINK_PROCESSING_METHOD_MORE),
+ /* This has to be called during a callback, and not after it has exited. The PENDING states
+ * apply to fiber callbacks, which run after varlink_dispatch_method() has already transitioned
+ * the state from PROCESSING to PENDING. */
+ assert_return(IN_SET(v->state,
+ VARLINK_PROCESSING_METHOD, VARLINK_PROCESSING_METHOD_MORE,
+ VARLINK_PENDING_METHOD, VARLINK_PENDING_METHOD_MORE),
-EUCLEAN);
char *s = NULL;
@@ -2902,7 +3061,11 @@ static sd_varlink_server* varlink_server_destroy(sd_varlink_server *s) {
while ((m = hashmap_steal_first_key(s->methods)))
free(m);
+ while ((m = hashmap_steal_first_key(s->fiber_methods)))
+ free(m);
+
hashmap_free(s->methods);
+ hashmap_free(s->fiber_methods);
hashmap_free(s->interfaces);
hashmap_free(s->symbols);
hashmap_free(s->by_uid);
@@ -3593,23 +3756,32 @@ static bool varlink_symbol_in_interface(const char *method, const char *interfac
return !strchr(p+1, '.');
}
-_public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
+int varlink_server_bind_internal(sd_varlink_server *s, Hashmap **methods, const char *method, sd_varlink_method_t callback) {
_cleanup_free_ char *m = NULL;
int r;
- assert_return(s, -EINVAL);
- assert_return(method, -EINVAL);
- assert_return(callback, -EINVAL);
+ assert(s);
+ assert(methods);
+ assert(method);
+ assert(callback);
if (varlink_symbol_in_interface(method, "org.varlink.service") ||
varlink_symbol_in_interface(method, "io.systemd"))
return varlink_server_log_errno(s, SYNTHETIC_ERRNO(EEXIST), "Cannot bind server to '%s'.", method);
+ /* Refuse to register the same method in both the regular and fiber method maps: the dispatcher
+ * always consults methods first and would silently ignore a shadowed fiber_methods entry (or vice
+ * versa), hiding the misconfiguration. */
+ Hashmap *other = methods == &s->methods ? s->fiber_methods : s->methods;
+ if (hashmap_contains(other, method))
+ return varlink_server_log_errno(s, SYNTHETIC_ERRNO(EEXIST),
+ "Method '%s' is already bound in the other method map.", method);
+
m = strdup(method);
if (!m)
return log_oom_debug();
- r = hashmap_ensure_put(&s->methods, &string_hash_ops, m, callback);
+ r = hashmap_ensure_put(methods, &string_hash_ops, m, callback);
if (r == -ENOMEM)
return log_oom_debug();
if (r < 0)
@@ -3620,13 +3792,12 @@ _public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *met
return 0;
}
-_public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, ...) {
- va_list ap;
+int varlink_server_bind_many_internal(sd_varlink_server *s, Hashmap **methods, va_list ap) {
int r = 0;
- assert_return(s, -EINVAL);
+ assert(s);
+ assert(methods);
- va_start(ap, s);
for (;;) {
sd_varlink_method_t callback;
const char *method;
@@ -3637,10 +3808,30 @@ _public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, .
callback = va_arg(ap, sd_varlink_method_t);
- r = sd_varlink_server_bind_method(s, method, callback);
+ r = varlink_server_bind_internal(s, methods, method, callback);
if (r < 0)
break;
}
+
+ return r;
+}
+
+_public_ int sd_varlink_server_bind_method(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
+ assert_return(s, -EINVAL);
+ assert_return(method, -EINVAL);
+ assert_return(callback, -EINVAL);
+
+ return varlink_server_bind_internal(s, &s->methods, method, callback);
+}
+
+_public_ int sd_varlink_server_bind_method_many_internal(sd_varlink_server *s, ...) {
+ va_list ap;
+ int r;
+
+ assert_return(s, -EINVAL);
+
+ va_start(ap, s);
+ r = varlink_server_bind_many_internal(s, &s->methods, ap);
va_end(ap);
return r;
diff --git a/src/test/test-varlink-idl.c b/src/libsystemd/sd-varlink/test-varlink-idl.c
similarity index 99%
rename from src/test/test-varlink-idl.c
rename to src/libsystemd/sd-varlink/test-varlink-idl.c
index a645d4d9d360c..a5190897023fd 100644
--- a/src/test/test-varlink-idl.c
+++ b/src/libsystemd/sd-varlink/test-varlink-idl.c
@@ -44,6 +44,7 @@
#include "varlink-io.systemd.Resolve.h"
#include "varlink-io.systemd.Resolve.Hook.h"
#include "varlink-io.systemd.Resolve.Monitor.h"
+#include "varlink-io.systemd.StorageProvider.h"
#include "varlink-io.systemd.Udev.h"
#include "varlink-io.systemd.Unit.h"
#include "varlink-io.systemd.UserDatabase.h"
@@ -212,6 +213,7 @@ TEST(parse_format) {
&vl_interface_io_systemd_Resolve,
&vl_interface_io_systemd_Resolve_Hook,
&vl_interface_io_systemd_Resolve_Monitor,
+ &vl_interface_io_systemd_StorageProvider,
&vl_interface_io_systemd_Udev,
&vl_interface_io_systemd_Unit,
&vl_interface_io_systemd_UserDatabase,
diff --git a/src/test/test-varlink.c b/src/libsystemd/sd-varlink/test-varlink.c
similarity index 78%
rename from src/test/test-varlink.c
rename to src/libsystemd/sd-varlink/test-varlink.c
index 72edc033dd068..a628b9f701953 100644
--- a/src/test/test-varlink.c
+++ b/src/libsystemd/sd-varlink/test-varlink.c
@@ -2,12 +2,12 @@
#include
#include
-#include
#include
#include
#include
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-json.h"
#include "sd-varlink.h"
@@ -214,7 +214,12 @@ static void flood_test(const char *address) {
/* Block the main event loop while we flood */
ASSERT_OK_EQ_ERRNO(write(block_write_fd, &x, sizeof(x)), (ssize_t) sizeof(x));
- ASSERT_OK(sd_event_default(&e));
+ /* Create a fresh event loop for the flood test — we can't reuse the default event because the
+ * main test (and the fiber we're running in) is already running it, and sd_event_loop() asserts
+ * the event is in the INITIAL state. Exit-on-idle so the nested loop terminates once the
+ * overload reply has been received and all other work is quiesced. */
+ ASSERT_OK(sd_event_new(&e));
+ ASSERT_OK(sd_event_set_exit_on_idle(e, true));
/* Flood the server with connections */
ASSERT_NOT_NULL(connections = new0(sd_varlink*, OVERLOAD_CONNECTIONS));
@@ -249,7 +254,7 @@ static void flood_test(const char *address) {
connections[k] = sd_varlink_unref(connections[k]);
}
-static void *thread(void *arg) {
+static int client_fiber(void *arg) {
_cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *c = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *i = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *wrong = NULL;
@@ -261,7 +266,7 @@ static void *thread(void *arg) {
SD_JSON_BUILD_PAIR_INTEGER("b", 99))));
ASSERT_OK(sd_varlink_connect_address(&c, arg));
- ASSERT_OK(sd_varlink_set_description(c, "thread-client"));
+ ASSERT_OK(sd_varlink_set_description(c, "fiber-client"));
ASSERT_OK(sd_varlink_set_allow_fd_passing_input(c, true));
ASSERT_OK(sd_varlink_set_allow_fd_passing_output(c, true));
@@ -319,7 +324,7 @@ static void *thread(void *arg) {
ASSERT_OK(sd_varlink_send(c, "io.test.Done", NULL));
- return NULL;
+ return 0;
}
static int block_fd_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
@@ -346,8 +351,8 @@ TEST(chat) {
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
_cleanup_close_pair_ int block_fds[2] = EBADF_PAIR;
- pthread_t t;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -386,11 +391,11 @@ TEST(chat) {
ASSERT_OK(sd_varlink_attach_event(c, e, 0));
- ASSERT_OK(-pthread_create(&t, NULL, thread, (void*) sp));
+ ASSERT_OK(sd_fiber_new(e, "client", client_fiber, (void*) sp, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
- ASSERT_OK(-pthread_join(t, NULL));
+ ASSERT_OK(sd_future_result(f));
}
static int method_invalid(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
@@ -616,6 +621,173 @@ TEST(sentinel_oneway) {
ASSERT_OK(sd_event_loop(e));
}
+static int method_fiber_sentinel_error(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ /* Set an error sentinel from a fiber callback and return without sending a reply. The sentinel
+ * error should still be propagated by the fiber's post-callback logic, even though the varlink
+ * state has already been transitioned to VARLINK_PENDING_METHOD by the time the fiber runs. */
+ ASSERT_OK(sd_varlink_set_sentinel(link, "io.test.SentinelError"));
+ return 0;
+}
+
+TEST(fiber_sentinel_error) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_default(&e));
+
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+ ASSERT_OK(sd_varlink_server_new(&s, 0));
+
+ ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
+
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberSentinelError", method_fiber_sentinel_error));
+
+ int connfd[2];
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
+ ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
+ ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
+
+ ASSERT_OK(sd_varlink_attach_event(c, e, 0));
+
+ ASSERT_OK(sd_varlink_bind_reply(c, reply_sentinel_error));
+
+ ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberSentinelError", /* parameters= */ NULL));
+
+ ASSERT_OK(sd_event_loop(e));
+}
+
+static int method_fiber_errno(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ /* Return a negative errno without sending a reply. The fiber's post-callback logic should
+ * convert this into a SD_VARLINK_ERROR_SYSTEM reply. */
+ return -ENOSYS;
+}
+
+static int reply_fiber_errno(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
+ ASSERT_STREQ(error_id, SD_VARLINK_ERROR_SYSTEM);
+ ASSERT_EQ(sd_json_variant_integer(sd_json_variant_by_key(parameters, "errno")), ENOSYS);
+ ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
+ return 0;
+}
+
+TEST(fiber_errno) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_default(&e));
+
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+ ASSERT_OK(sd_varlink_server_new(&s, 0));
+
+ ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
+
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberErrno", method_fiber_errno));
+
+ int connfd[2];
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
+ ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
+ ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
+
+ ASSERT_OK(sd_varlink_attach_event(c, e, 0));
+
+ ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_errno));
+
+ ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberErrno", /* parameters= */ NULL));
+
+ ASSERT_OK(sd_event_loop(e));
+}
+
+static int method_fiber_no_reply(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ /* Return success without replying and without stashing a ref. The fiber's post-callback
+ * logic should detect this and fail the connection. */
+ return 0;
+}
+
+static int reply_fiber_no_reply(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
+ ASSERT_STREQ(error_id, SD_VARLINK_ERROR_DISCONNECTED);
+ ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
+ return 0;
+}
+
+TEST(fiber_no_reply) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_default(&e));
+
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+ ASSERT_OK(sd_varlink_server_new(&s, 0));
+
+ ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
+
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberNoReply", method_fiber_no_reply));
+
+ int connfd[2];
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
+ ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
+ ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
+
+ ASSERT_OK(sd_varlink_attach_event(c, e, 0));
+
+ ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_no_reply));
+
+ ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberNoReply", /* parameters= */ NULL));
+
+ ASSERT_OK(sd_event_loop(e));
+}
+
+static int fiber_stashed_deferred_reply(sd_event_source *s, void *userdata) {
+ _cleanup_(sd_varlink_unrefp) sd_varlink *link = ASSERT_PTR(userdata);
+
+ sd_event_source_disable_unref(s);
+ ASSERT_OK(sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("result", "stashed")));
+ return 0;
+}
+
+static int method_fiber_stash(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ /* Stash a ref on the connection so n_ref > 2 when the fiber returns, and reply later from a
+ * deferred event source. The fiber's post-callback logic should see the extra ref and treat
+ * this as a valid deferred-reply case instead of failing the connection. */
+ sd_event_source *source;
+
+ ASSERT_OK(sd_event_add_defer(sd_varlink_get_event(link), &source, fiber_stashed_deferred_reply, sd_varlink_ref(link)));
+ ASSERT_OK(sd_event_source_set_enabled(source, SD_EVENT_ONESHOT));
+ return 0;
+}
+
+static int reply_fiber_stash(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
+ ASSERT_NULL(error_id);
+ ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(parameters, "result")), "stashed");
+ ASSERT_OK(sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS));
+ return 0;
+}
+
+TEST(fiber_stash) {
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_default(&e));
+
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+ ASSERT_OK(sd_varlink_server_new(&s, 0));
+
+ ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
+
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.FiberStash", method_fiber_stash));
+
+ int connfd[2];
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, 0, connfd));
+ ASSERT_OK(sd_varlink_server_add_connection(s, connfd[0], /* ret= */ NULL));
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *c = NULL;
+ ASSERT_OK(sd_varlink_connect_fd(&c, connfd[1]));
+
+ ASSERT_OK(sd_varlink_attach_event(c, e, 0));
+
+ ASSERT_OK(sd_varlink_bind_reply(c, reply_fiber_stash));
+
+ ASSERT_OK(sd_varlink_invoke(c, "io.test.FiberStash", /* parameters= */ NULL));
+
+ ASSERT_OK(sd_event_loop(e));
+}
+
static int method_with_fd_sentinel(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
_cleanup_close_ int fd1 = -EBADF, fd2 = -EBADF;
@@ -766,8 +938,9 @@ static int method_upgrade(sd_varlink *link, sd_json_variant *parameters, sd_varl
if (r < 0)
return r;
- /* After upgrade, do raw I/O: read until EOF, reverse, write back.
- * The client shuts down its write side after sending, so we get a clean EOF. */
+ /* After upgrade, do raw I/O: read until the client shuts down its write side (giving us a clean
+ * EOF), reverse what we got, and write it back. Use suspending I/O so other fibers (the client)
+ * can make progress while we're waiting on the socket. */
char buf[64] = {};
ssize_t n = ASSERT_OK(loop_read(input_fd, buf, sizeof(buf) - 1, /* do_poll= */ true));
ASSERT_GT(n, 0);
@@ -787,12 +960,10 @@ static int method_upgrade_without_flag(sd_varlink *link, sd_json_variant *parame
/* Calling reply_and_upgrade without the client requesting it should fail with -EPROTO */
ASSERT_ERROR(sd_varlink_reply_and_upgrade(link, /* parameters= */ NULL, &input_fd, &output_fd), EPROTO);
- sd_event_exit(sd_varlink_get_event(link), EXIT_SUCCESS);
-
return sd_varlink_reply(link, /* parameters= */ NULL);
}
-static void *upgrade_thread(void *arg) {
+static int upgrade_client_fiber(void *arg) {
_cleanup_(sd_varlink_flush_close_unrefp) sd_varlink *c = NULL;
_cleanup_close_ int input_fd = -EBADF, output_fd = -EBADF;
sd_json_variant *o = NULL;
@@ -825,14 +996,15 @@ static void *upgrade_thread(void *arg) {
ASSERT_OK(sd_varlink_call(c2, "io.test.UpgradeWithoutFlag", /* parameters= */ NULL, &o, &error_id));
ASSERT_NULL(error_id);
- return NULL;
+ ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
+ return 0;
}
TEST(upgrade) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
- pthread_t t;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -842,31 +1014,23 @@ TEST(upgrade) {
ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE));
ASSERT_OK(sd_varlink_server_set_description(s, "upgrade-server"));
- ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.Upgrade", method_upgrade));
+ /* The method does raw I/O on the upgraded socket — bind it as a fiber method so it can
+ * suspend on loop_read()/loop_write() and the client fiber can make progress concurrently. */
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.Upgrade", method_upgrade));
ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.UpgradeWithoutFlag", method_upgrade_without_flag));
ASSERT_OK(sd_varlink_server_listen_address(s, sp, 0600));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
- ASSERT_OK(-pthread_create(&t, NULL, upgrade_thread, (void*) sp));
+ ASSERT_OK(sd_fiber_new(e, "upgrade-client", upgrade_client_fiber, (void*) sp, /* destroy= */ NULL, &f));
- /* Run the event loop until no more connections (the thread will disconnect when done) */
+ /* Run the event loop. Exits on idle once the client fiber completes and all server connections
+ * have been torn down. */
ASSERT_OK(sd_event_loop(e));
- ASSERT_OK(-pthread_join(t, NULL));
+ ASSERT_OK(sd_future_result(f));
}
-static int method_upgrade_and_exit(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
- sd_event *event = ASSERT_PTR(userdata);
-
- int r = method_upgrade(link, parameters, flags, /* userdata= */ NULL);
-
- /* Exit the event loop after the upgrade is handled. We can't use sd_varlink_get_event()
- * here because the connection is already disconnected after reply_and_upgrade. */
- (void) sd_event_exit(event, r < 0 ? r : EXIT_SUCCESS);
- return r;
-}
-
-static void *upgrade_pipelining_thread(void *arg) {
+static int upgrade_pipelining_client_fiber(void *arg) {
union sockaddr_union sa = {};
_cleanup_close_ int fd = -EBADF;
@@ -893,8 +1057,8 @@ static void *upgrade_pipelining_thread(void *arg) {
/* Shut down write side so server's method_upgrade sees EOF after raw payload */
ASSERT_OK_ERRNO(shutdown(fd, SHUT_WR));
- /* Read everything: upgrade reply (JSON + \0) + reversed raw payload. The server closes
- * the connection after writing, so loop_read() reads until EOF and gets it all. */
+ /* Read everything: upgrade reply (JSON + \0) + reversed raw payload. The server closes the
+ * connection after writing, so loop_read_suspend() reads until EOF and gets it all. */
char buf[256] = {};
ssize_t n = ASSERT_OK(loop_read(fd, buf, sizeof(buf) - 1, /* do_poll= */ true));
ASSERT_GT(n, 0);
@@ -909,14 +1073,15 @@ static void *upgrade_pipelining_thread(void *arg) {
ASSERT_EQ(raw_size, strlen(raw_payload));
ASSERT_STREQ(strndupa_safe(raw, raw_size), "!denilepiP");
- return NULL;
+ ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
+ return 0;
}
TEST(upgrade_pipelining) {
_cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
_cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
_cleanup_(sd_event_unrefp) sd_event *e = NULL;
- pthread_t t;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
const char *sp;
ASSERT_OK(mkdtemp_malloc("/tmp/varlink-test-XXXXXX", &tmpdir));
@@ -924,25 +1089,23 @@ TEST(upgrade_pipelining) {
ASSERT_OK(sd_event_new(&e));
- ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE|SD_VARLINK_SERVER_INHERIT_USERDATA));
+ ASSERT_OK(sd_varlink_server_new(&s, SD_VARLINK_SERVER_UPGRADABLE));
ASSERT_OK(sd_varlink_server_set_description(s, "upgrade-pipelining-server"));
- ASSERT_OK(sd_varlink_server_bind_method(s, "io.test.Upgrade", method_upgrade_and_exit));
+ /* method_upgrade does raw I/O on the upgraded socket, so bind as a fiber method. */
+ ASSERT_OK(varlink_server_bind_fiber(s, "io.test.Upgrade", method_upgrade));
ASSERT_OK(sd_varlink_server_listen_address(s, sp, 0600));
ASSERT_OK(sd_varlink_server_attach_event(s, e, 0));
- sd_varlink_server_set_userdata(s, e);
- ASSERT_OK(-pthread_create(&t, NULL, upgrade_pipelining_thread, (void*) sp));
+ ASSERT_OK(sd_fiber_new(e, "upgrade-pipelining-client", upgrade_pipelining_client_fiber, (void*) sp, /* destroy= */ NULL, &f));
ASSERT_OK(sd_event_loop(e));
- ASSERT_OK(-pthread_join(t, NULL));
+ ASSERT_OK(sd_future_result(f));
}
typedef struct ExecDirServer {
sd_varlink_server *server;
- sd_event *event;
const char *name;
- pthread_t thread;
} ExecDirServer;
static int method_execute_dir_ping(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
@@ -951,20 +1114,6 @@ static int method_execute_dir_ping(sd_varlink *link, sd_json_variant *parameters
return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_STRING("name", srv->name));
}
-static void on_execute_dir_disconnect(sd_varlink_server *s, sd_varlink *link, void *userdata) {
- ExecDirServer *srv = ASSERT_PTR(userdata);
-
- /* Only one client (from varlink_execute_directory()) connects per server — once it's gone, we're done. */
- ASSERT_OK(sd_event_exit(srv->event, 0));
-}
-
-static void *execute_dir_server_thread(void *arg) {
- ExecDirServer *srv = arg;
-
- ASSERT_OK(sd_event_loop(srv->event));
- return NULL;
-}
-
static int execute_dir_reply(sd_varlink *link, sd_json_variant *parameters, const char *error_id, sd_varlink_reply_flags_t flags, void *userdata) {
size_t *count = ASSERT_PTR(userdata);
@@ -975,51 +1124,28 @@ static int execute_dir_reply(sd_varlink *link, sd_json_variant *parameters, cons
return 0;
}
-TEST(execute_directory) {
- _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
- static const char * const names[] = { "alpha", "beta", "gamma" };
- ExecDirServer servers[ELEMENTSOF(names)] = {};
- size_t reply_count = 0;
+typedef struct ExecDirClientArgs {
+ const char *tmpdir;
+ size_t n_servers;
+ size_t *reply_count;
+} ExecDirClientArgs;
- ASSERT_OK(mkdtemp_malloc("/tmp/varlink-execdir-XXXXXX", &tmpdir));
-
- for (size_t i = 0; i < ELEMENTSOF(names); i++) {
- ExecDirServer *eds = servers + i;
- servers[i].name = names[i];
-
- _cleanup_free_ char *j = ASSERT_PTR(path_join(tmpdir, names[i]));
-
- ASSERT_OK(sd_event_new(&eds->event));
- ASSERT_OK(varlink_server_new(&eds->server,
- SD_VARLINK_SERVER_INHERIT_USERDATA,
- eds));
- ASSERT_OK(sd_varlink_server_bind_method(eds->server, "io.test.ExecDirPing", method_execute_dir_ping));
- ASSERT_OK(sd_varlink_server_bind_disconnect(eds->server, on_execute_dir_disconnect));
- ASSERT_OK(sd_varlink_server_listen_address(eds->server, j, 0600));
- ASSERT_OK(sd_varlink_server_attach_event(eds->server, eds->event, 0));
-
- ASSERT_OK(-pthread_create(&eds->thread, NULL, execute_dir_server_thread, eds));
- }
+static int execute_dir_client_fiber(void *arg) {
+ ExecDirClientArgs *a = ASSERT_PTR(arg);
ASSERT_OK_EQ(varlink_execute_directory(
- tmpdir,
+ a->tmpdir,
"io.test.ExecDirPing",
/* parameters= */ NULL,
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
- &reply_count), (ssize_t) ELEMENTSOF(names));
- ASSERT_EQ(reply_count, ELEMENTSOF(names));
-
- FOREACH_ELEMENT(eds, servers) {
- ASSERT_OK(-pthread_join(eds->thread, NULL));
- eds->server = sd_varlink_server_unref(eds->server);
- eds->event = sd_event_unref(eds->event);
- }
+ a->reply_count), (ssize_t) a->n_servers);
+ ASSERT_EQ(*a->reply_count, a->n_servers);
/* Calling the helper against a non-existent directory must fail. */
_cleanup_free_ char *nope = NULL;
- ASSERT_OK(asprintf(&nope, "%s/does-not-exist", tmpdir));
+ ASSERT_OK(asprintf(&nope, "%s/does-not-exist", a->tmpdir));
ASSERT_FAIL(varlink_execute_directory(
nope,
"io.test.ExecDirPing",
@@ -1027,13 +1153,13 @@ TEST(execute_directory) {
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
- &reply_count));
+ a->reply_count));
/* An empty directory must simply return 0 and not invoke the reply callback. */
- _cleanup_free_ char *empty = ASSERT_PTR(path_join(tmpdir, "empty"));
+ _cleanup_free_ char *empty = ASSERT_PTR(path_join(a->tmpdir, "empty"));
ASSERT_OK_ERRNO(mkdir(empty, 0755));
- size_t count_before = reply_count;
+ size_t count_before = *a->reply_count;
ASSERT_OK_ZERO(varlink_execute_directory(
empty,
"io.test.ExecDirPing",
@@ -1041,8 +1167,52 @@ TEST(execute_directory) {
/* more= */ false,
/* timeout_usec= */ USEC_INFINITY,
execute_dir_reply,
- &reply_count));
- ASSERT_EQ(reply_count, count_before);
+ a->reply_count));
+ ASSERT_EQ(*a->reply_count, count_before);
+
+ ASSERT_OK(sd_event_exit(sd_fiber_get_event(), EXIT_SUCCESS));
+ return 0;
+}
+
+TEST(execute_directory) {
+ _cleanup_(rm_rf_physical_and_freep) char *tmpdir = NULL;
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *f = NULL;
+ static const char * const names[] = { "alpha", "beta", "gamma" };
+ ExecDirServer servers[ELEMENTSOF(names)] = {};
+ size_t reply_count = 0;
+
+ ASSERT_OK(mkdtemp_malloc("/tmp/varlink-execdir-XXXXXX", &tmpdir));
+
+ ASSERT_OK(sd_event_new(&e));
+
+ for (size_t i = 0; i < ELEMENTSOF(names); i++) {
+ ExecDirServer *eds = servers + i;
+ servers[i].name = names[i];
+
+ _cleanup_free_ char *j = ASSERT_PTR(path_join(tmpdir, names[i]));
+
+ ASSERT_OK(varlink_server_new(&eds->server,
+ SD_VARLINK_SERVER_INHERIT_USERDATA,
+ eds));
+ ASSERT_OK(sd_varlink_server_bind_method(eds->server, "io.test.ExecDirPing", method_execute_dir_ping));
+ ASSERT_OK(sd_varlink_server_listen_address(eds->server, j, 0600));
+ ASSERT_OK(sd_varlink_server_attach_event(eds->server, e, 0));
+ }
+
+ ExecDirClientArgs args = {
+ .tmpdir = tmpdir,
+ .n_servers = ELEMENTSOF(names),
+ .reply_count = &reply_count,
+ };
+ ASSERT_OK(sd_fiber_new(e, "execute-dir-client", execute_dir_client_fiber, &args, NULL, &f));
+
+ ASSERT_OK(sd_event_loop(e));
+
+ ASSERT_OK(sd_future_result(f));
+
+ FOREACH_ELEMENT(eds, servers)
+ eds->server = sd_varlink_server_unref(eds->server);
}
DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd/sd-varlink/varlink-internal.h b/src/libsystemd/sd-varlink/varlink-internal.h
index 8087c2c432464..8e783b5c592a5 100644
--- a/src/libsystemd/sd-varlink/varlink-internal.h
+++ b/src/libsystemd/sd-varlink/varlink-internal.h
@@ -74,9 +74,8 @@ typedef enum VarlinkState {
typedef struct sd_varlink {
unsigned n_ref;
- sd_varlink_server *server;
-
VarlinkState state;
+ sd_varlink_server *server;
/* Transport layer: input/output buffers, fd passing, output queue, read/write/parse
* step functions, sd-event integration (input/output/time event sources, idle
@@ -87,6 +86,13 @@ typedef struct sd_varlink {
unsigned n_pending;
+ /* Per-call protocol-upgrade marker: set when the *current* method call carries the
+ * SD_VARLINK_METHOD_UPGRADE flag. Validated by sd_varlink_reply_and_upgrade() to
+ * ensure the caller's contract is honored. The transport-layer "stop reading at the
+ * next message boundary" behavior is governed independently by the JsonStream's
+ * bounded_reads flag. */
+ bool protocol_upgrade;
+
sd_varlink_reply_t reply_callback;
sd_json_variant *current;
@@ -102,13 +108,6 @@ typedef struct sd_varlink {
size_t n_previous_fds;
char *sentinel;
- /* Per-call protocol-upgrade marker: set when the *current* method call carries the
- * SD_VARLINK_METHOD_UPGRADE flag. Validated by sd_varlink_reply_and_upgrade() to
- * ensure the caller's contract is honored. The transport-layer "stop reading at the
- * next message boundary" behavior is governed independently by the JsonStream's
- * bounded_reads flag. */
- bool protocol_upgrade:1;
-
void *userdata;
sd_event_source *quit_event_source;
@@ -136,7 +135,8 @@ typedef struct sd_varlink_server {
LIST_HEAD(VarlinkServerSocket, sockets);
- Hashmap *methods; /* Fully qualified symbol name of a method → VarlinkMethod */
+ Hashmap *methods; /* Fully qualified symbol name of a method → sd_varlink_method_t */
+ Hashmap *fiber_methods; /* Fully qualified symbol name of a fiber method → sd_varlink_method_t */
Hashmap *interfaces; /* Fully qualified interface name → VarlinkInterface* */
Hashmap *symbols; /* Fully qualified symbol name of method/error → VarlinkSymbol* */
sd_varlink_connect_t connect_callback;
@@ -145,8 +145,12 @@ typedef struct sd_varlink_server {
sd_event *event;
int64_t event_priority;
- unsigned n_connections;
Hashmap *by_uid; /* UID_TO_PTR(uid) → UINT_TO_PTR(n_connections) */
+ unsigned n_connections;
+ unsigned connections_max;
+ unsigned connections_per_uid_max;
+
+ bool exit_on_idle;
void *userdata;
@@ -155,11 +159,6 @@ typedef struct sd_varlink_server {
char *product;
char *version;
char *url;
-
- unsigned connections_max;
- unsigned connections_per_uid_max;
-
- bool exit_on_idle;
} sd_varlink_server;
#define varlink_log_errno(v, error, fmt, ...) \
@@ -186,3 +185,6 @@ VarlinkServerSocket* varlink_server_socket_free(VarlinkServerSocket *ss);
DEFINE_TRIVIAL_CLEANUP_FUNC(VarlinkServerSocket *, varlink_server_socket_free);
int varlink_server_add_socket_event_source(sd_varlink_server *s, VarlinkServerSocket *ss);
+
+int varlink_server_bind_internal(sd_varlink_server *s, Hashmap **methods, const char *method, sd_varlink_method_t callback);
+int varlink_server_bind_many_internal(sd_varlink_server *s, Hashmap **methods, va_list ap);
diff --git a/src/libsystemd/sd-varlink/varlink-util.c b/src/libsystemd/sd-varlink/varlink-util.c
index 7b8797c92c85b..d7e22df20f5da 100644
--- a/src/libsystemd/sd-varlink/varlink-util.c
+++ b/src/libsystemd/sd-varlink/varlink-util.c
@@ -159,6 +159,27 @@ int varlink_many_error(Set *s, const char *error_id, sd_json_variant *parameters
return r;
}
+int varlink_server_bind_fiber(sd_varlink_server *s, const char *method, sd_varlink_method_t callback) {
+ assert_return(s, -EINVAL);
+ assert_return(method, -EINVAL);
+ assert_return(callback, -EINVAL);
+
+ return varlink_server_bind_internal(s, &s->fiber_methods, method, callback);
+}
+
+int varlink_server_bind_fiber_many_internal(sd_varlink_server *s, ...) {
+ va_list ap;
+ int r;
+
+ assert_return(s, -EINVAL);
+
+ va_start(ap, s);
+ r = varlink_server_bind_many_internal(s, &s->fiber_methods, ap);
+ va_end(ap);
+
+ return r;
+}
+
int varlink_set_info_systemd(sd_varlink_server *server) {
_cleanup_free_ char *product = NULL;
diff --git a/src/libsystemd/sd-varlink/varlink-util.h b/src/libsystemd/sd-varlink/varlink-util.h
index d6ecb03c54533..d5765ca2c72f1 100644
--- a/src/libsystemd/sd-varlink/varlink-util.h
+++ b/src/libsystemd/sd-varlink/varlink-util.h
@@ -19,6 +19,10 @@ int varlink_many_notifyb(Set *s, ...);
int varlink_many_reply(Set *s, sd_json_variant *parameters);
int varlink_many_error(Set *s, const char *error_id, sd_json_variant *parameters);
+int varlink_server_bind_fiber(sd_varlink_server *s, const char *method, sd_varlink_method_t callback);
+int varlink_server_bind_fiber_many_internal(sd_varlink_server *s, ...);
+#define varlink_server_bind_fiber_many(s, ...) varlink_server_bind_fiber_many_internal(s, __VA_ARGS__, NULL)
+
int varlink_set_info_systemd(sd_varlink_server *server);
int varlink_server_new(
diff --git a/src/libudev/test-libudev.c b/src/libudev/test-libudev.c
index a653f0c6c8fdd..06feb1ffbc61a 100644
--- a/src/libudev/test-libudev.c
+++ b/src/libudev/test-libudev.c
@@ -430,7 +430,7 @@ static int parse_args(int argc, char *argv[], const char **syspath, const char *
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/login/inhibit.c b/src/login/inhibit.c
index 4abfc1c6d3acd..78c784c30fad8 100644
--- a/src/login/inhibit.c
+++ b/src/login/inhibit.c
@@ -203,7 +203,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/machine-id-setup/machine-id-setup-main.c b/src/machine-id-setup/machine-id-setup-main.c
index 9dd389dbaa4f9..2363427b5f54e 100644
--- a/src/machine-id-setup/machine-id-setup-main.c
+++ b/src/machine-id-setup/machine-id-setup-main.c
@@ -78,7 +78,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/measure/measure-tool.c b/src/measure/measure-tool.c
index a92a418f61fce..eeb001f3fed4a 100644
--- a/src/measure/measure-tool.c
+++ b/src/measure/measure-tool.c
@@ -139,7 +139,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/modules-load/modules-load.c b/src/modules-load/modules-load.c
index f6806d604ab55..0917f800a1a84 100644
--- a/src/modules-load/modules-load.c
+++ b/src/modules-load/modules-load.c
@@ -361,7 +361,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/mountfsd/mountwork.c b/src/mountfsd/mountwork.c
index 54a5203da2cc6..9f469d6061fde 100644
--- a/src/mountfsd/mountwork.c
+++ b/src/mountfsd/mountwork.c
@@ -1362,7 +1362,7 @@ static int vl_method_make_directory(
struct stat parent_stat;
if (fstat(parent_fd, &parent_stat) < 0)
- return r;
+ return log_debug_errno(errno, "Failed to fstat parent directory fd: %m");
r = stat_verify_directory(&parent_stat);
if (r < 0)
diff --git a/src/mute-console/mute-console.c b/src/mute-console/mute-console.c
index be6b5fac09166..d5788de09b3b9 100644
--- a/src/mute-console/mute-console.c
+++ b/src/mute-console/mute-console.c
@@ -63,7 +63,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/network/generator/network-generator-main.c b/src/network/generator/network-generator-main.c
index 721d36b831945..df9ce9265dbbb 100644
--- a/src/network/generator/network-generator-main.c
+++ b/src/network/generator/network-generator-main.c
@@ -174,7 +174,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/notify/notify.c b/src/notify/notify.c
index 00f915dc7bff1..6c50e4c57c394 100644
--- a/src/notify/notify.c
+++ b/src/notify/notify.c
@@ -155,7 +155,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c
index e4e0359ce6d79..efe927f36e9b6 100644
--- a/src/nspawn/nspawn.c
+++ b/src/nspawn/nspawn.c
@@ -405,7 +405,8 @@ static int help(void) {
"Other",
};
- _cleanup_(table_unref_many) Table* tables[ELEMENTSOF(groups) + 1] = {};
+ Table* tables[ELEMENTSOF(groups)] = {};
+ CLEANUP_ELEMENTS(tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(groups); i++) {
r = option_parser_get_help_table_group(groups[i], &tables[i]);
@@ -604,7 +605,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c) {
+ FOREACH_OPTION_OR_RETURN(c, &opts) {
switch (c) {
OPTION_COMMON_HELP:
@@ -1420,7 +1421,7 @@ static int parse_argv(int argc, char *argv[]) {
* the old container user functionality. To maintain backwards compatibility
* with the space-separated form (--user NAME), if the next opts.arg does not look
* like an option, interpret it as a user name. */
- const char *t = option_parser_next_arg(&opts);
+ const char *t = option_parser_peek_next_arg(&opts);
if (t && t[0] != '-') {
opts.arg = option_parser_consume_next_arg(&opts);
log_warning("--user NAME is deprecated, use --uid=NAME instead.");
diff --git a/src/oom/oomctl.c b/src/oom/oomctl.c
index b73e2eb5abfe5..82ffe0e8379fd 100644
--- a/src/oom/oomctl.c
+++ b/src/oom/oomctl.c
@@ -93,7 +93,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/oom/oomd.c b/src/oom/oomd.c
index 2250d7ec7f189..62eecfc065c65 100644
--- a/src/oom/oomd.c
+++ b/src/oom/oomd.c
@@ -53,7 +53,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/path/path-tool.c b/src/path/path-tool.c
index 22544b9463854..29696501d03a0 100644
--- a/src/path/path-tool.c
+++ b/src/path/path-tool.c
@@ -206,7 +206,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/pcrextend/pcrextend.c b/src/pcrextend/pcrextend.c
index 5b846b9d3a9dc..f452363209d66 100644
--- a/src/pcrextend/pcrextend.c
+++ b/src/pcrextend/pcrextend.c
@@ -84,7 +84,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c
index 752f67cbdb990..09f49b2ed250e 100644
--- a/src/pcrlock/pcrlock.c
+++ b/src/pcrlock/pcrlock.c
@@ -5193,7 +5193,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
bool auto_location = true;
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/ptyfwd/ptyfwd-tool.c b/src/ptyfwd/ptyfwd-tool.c
index 6d98a8e7ef09e..e7b531c873088 100644
--- a/src/ptyfwd/ptyfwd-tool.c
+++ b/src/ptyfwd/ptyfwd-tool.c
@@ -65,7 +65,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/random-seed/random-seed-tool.c b/src/random-seed/random-seed-tool.c
index 2eabcea176c2a..f573e84412ffb 100644
--- a/src/random-seed/random-seed-tool.c
+++ b/src/random-seed/random-seed-tool.c
@@ -352,7 +352,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/repart/repart.c b/src/repart/repart.c
index 2d35da28c2b12..ad19f0ab1ec7a 100644
--- a/src/repart/repart.c
+++ b/src/repart/repart.c
@@ -9648,7 +9648,8 @@ static int help(void) {
"El Torito boot catalog",
};
- _cleanup_(table_unref_many) Table *option_tables[ELEMENTSOF(option_groups) + 1] = {};
+ Table *option_tables[ELEMENTSOF(option_groups)] = {};
+ CLEANUP_ELEMENTS(option_tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(option_groups); i++) {
r = option_parser_get_help_table_group(option_groups[i], &option_tables[i]);
@@ -9688,7 +9689,7 @@ static int parse_argv(int argc, char *argv[]) {
bool auto_public_key_pcr_mask = true, auto_pcrlock = true;
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_GROUP("Options"): {}
@@ -9896,7 +9897,13 @@ static int parse_argv(int argc, char *argv[]) {
OPTION_LONG("list-devices", NULL,
"List candidate block devices to operate on"):
- r = blockdev_list(BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|BLOCKDEV_LIST_SHOW_SYMLINKS|BLOCKDEV_LIST_IGNORE_ZRAM, /* ret_devices= */ NULL, /* ret_n_devices= */ NULL);
+ r = blockdev_list(
+ BLOCKDEV_LIST_SHOW_SYMLINKS|
+ BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|
+ BLOCKDEV_LIST_IGNORE_ZRAM|
+ BLOCKDEV_LIST_IGNORE_READ_ONLY,
+ /* ret_devices= */ NULL,
+ /* ret_n_devices= */ NULL);
if (r < 0)
return r;
@@ -10876,6 +10883,7 @@ static int vl_method_list_candidate_devices(
BLOCKDEV_LIST_REQUIRE_PARTITION_SCANNING|
BLOCKDEV_LIST_IGNORE_ZRAM|
BLOCKDEV_LIST_METADATA|
+ BLOCKDEV_LIST_IGNORE_READ_ONLY|
(p.ignore_empty ? BLOCKDEV_LIST_IGNORE_EMPTY : 0)|
(p.ignore_root ? BLOCKDEV_LIST_IGNORE_ROOT : 0),
&l,
diff --git a/src/report/report-basic-server.c b/src/report/report-basic-server.c
index 1e2eca31eae68..bca943fd7faee 100644
--- a/src/report/report-basic-server.c
+++ b/src/report/report-basic-server.c
@@ -59,7 +59,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/report/report.c b/src/report/report.c
index fef01c094ef9f..390871e942863 100644
--- a/src/report/report.c
+++ b/src/report/report.c
@@ -987,7 +987,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/run/run.c b/src/run/run.c
index ce35b48fba4cb..46b8014e580c5 100644
--- a/src/run/run.c
+++ b/src/run/run.c
@@ -151,7 +151,8 @@ static int help(void) {
"Timer options",
};
- _cleanup_(table_unref_many) Table *tables[ELEMENTSOF(groups) + 1] = {};
+ Table *tables[ELEMENTSOF(groups)] = {};
+ CLEANUP_ELEMENTS(tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(groups); i++) {
r = option_parser_get_help_table_full("systemd-run", groups[i], &tables[i]);
@@ -188,7 +189,7 @@ static int help_sudo_mode(void) {
* sudo's short switches, hence please do not introduce new short switches unless they have a roughly
* equivalent purpose on sudo. Use long options for everything private to run0. */
- r = option_parser_get_help_table_full("run0", /* group= */ NULL, &opts_table);
+ r = option_parser_get_help_table_ns("run0", &opts_table);
if (r < 0)
return r;
@@ -254,7 +255,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION, "systemd-run" };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_NAMESPACE("systemd-run"): {}
@@ -782,7 +783,7 @@ static int parse_argv_sudo_mode(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION, "run0" };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_NAMESPACE("run0"): {}
diff --git a/src/sbsign/sbsign.c b/src/sbsign/sbsign.c
index 7d866fde87555..f5a88b2849fe2 100644
--- a/src/sbsign/sbsign.c
+++ b/src/sbsign/sbsign.c
@@ -96,7 +96,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/shared/binfmt-util.c b/src/shared/binfmt-util.c
index d21fd10136fb4..0faca5966341c 100644
--- a/src/shared/binfmt-util.c
+++ b/src/shared/binfmt-util.c
@@ -18,6 +18,12 @@ int binfmt_mounted_and_writable(void) {
fd = RET_NERRNO(open("/proc/sys/fs/binfmt_misc", O_CLOEXEC | O_DIRECTORY | O_PATH));
if (fd == -ENOENT)
return false;
+ /* ELOOP happens when binfmt_misc is an automount point under a read-only bind mount of /proc —
+ * the kernel cannot trigger the automount and returns ELOOP instead. Common in mock/Koji buildroots. */
+ if (fd == -ELOOP || ERRNO_IS_NEG_PRIVILEGE(fd)) {
+ log_debug_errno(fd, "Failed to open /proc/sys/fs/binfmt_misc, ignoring: %m");
+ return false;
+ }
if (fd < 0)
return fd;
diff --git a/src/shared/blockdev-list.c b/src/shared/blockdev-list.c
index 5b11c8169477f..181afb42890f5 100644
--- a/src/shared/blockdev-list.c
+++ b/src/shared/blockdev-list.c
@@ -27,7 +27,7 @@ void block_device_done(BlockDevice *d) {
void block_device_array_free(BlockDevice *d, size_t n_devices) {
FOREACH_ARRAY(i, d, n_devices)
- block_device_done(d);
+ block_device_done(i);
free(d);
}
@@ -188,6 +188,20 @@ int blockdev_list(BlockDevListFlags flags, BlockDevice **ret_devices, size_t *re
}
}
+ int ro = -1;
+ if (FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_READ_ONLY) || FLAGS_SET(flags, BLOCKDEV_LIST_METADATA)) {
+ 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 (ro > 0 && FLAGS_SET(flags, BLOCKDEV_LIST_IGNORE_READ_ONLY)) {
+ log_device_debug(dev, "Device '%s' is read-only, skipping.", node);
+ continue;
+ }
+ }
+
_cleanup_strv_free_ char **list = NULL;
if (FLAGS_SET(flags, BLOCKDEV_LIST_SHOW_SYMLINKS)) {
FOREACH_DEVICE_DEVLINK(dev, sl)
@@ -198,17 +212,10 @@ 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) {
diff --git a/src/shared/blockdev-list.h b/src/shared/blockdev-list.h
index d82345435f7e2..67f8efba97187 100644
--- a/src/shared/blockdev-list.h
+++ b/src/shared/blockdev-list.h
@@ -11,6 +11,7 @@ typedef enum BlockDevListFlags {
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, read_only */
+ BLOCKDEV_LIST_IGNORE_READ_ONLY = 1 << 7, /* Ignore read-only block devices */
} BlockDevListFlags;
typedef struct BlockDevice {
diff --git a/src/shared/dns-question.c b/src/shared/dns-question.c
index ac4cc8e998007..28840d64b948a 100644
--- a/src/shared/dns-question.c
+++ b/src/shared/dns-question.c
@@ -608,6 +608,9 @@ int dns_json_dispatch_question(const char *name, sd_json_variant *variant, sd_js
if (!sd_json_variant_is_array(variant))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
+ if (sd_json_variant_elements(variant) > DNS_QUESTION_ITEMS_MAX)
+ return json_log(variant, flags, SYNTHETIC_ERRNO(E2BIG), "Too many questions in a single query.");
+
_cleanup_(dns_question_unrefp) DnsQuestion *nq = NULL;
nq = dns_question_new(sd_json_variant_elements(variant));
if (!nq)
diff --git a/src/shared/dns-question.h b/src/shared/dns-question.h
index 4b0fc68fd648c..85de7ad06d8d7 100644
--- a/src/shared/dns-question.h
+++ b/src/shared/dns-question.h
@@ -5,6 +5,8 @@
#include "shared-forward.h"
+#define DNS_QUESTION_ITEMS_MAX 128U
+
/* A simple array of resource keys */
typedef enum DnsQuestionFlags {
diff --git a/src/shared/firewall-util.c b/src/shared/firewall-util.c
index 651870e369889..4693972ff2752 100644
--- a/src/shared/firewall-util.c
+++ b/src/shared/firewall-util.c
@@ -50,7 +50,7 @@ static const char* dnat_map_name(void) {
return cached;
}
-static DEFINE_ARRAY_DONE_FUNC(sd_netlink_message*, sd_netlink_message_unref);
+static DEFINE_POINTER_ARRAY_CLEAR_FUNC(sd_netlink_message*, sd_netlink_message_unref);
static int nfnl_open_expr_container(sd_netlink_message *m, const char *name) {
int r;
@@ -724,7 +724,8 @@ static uint32_t concat_types2(enum nft_key_types a, enum nft_key_types b) {
}
static int fw_nftables_init_family(sd_netlink *nfnl, int family) {
- _cleanup_(sd_netlink_message_unref_many) sd_netlink_message *messages[10] = {};
+ sd_netlink_message *messages[10] = {};
+ CLEANUP_ELEMENTS(messages, sd_netlink_message_unref_array_clear);
size_t msgcnt = 0, ip_type_size;
uint32_t set_id = 0;
int ip_type, r;
@@ -1045,7 +1046,8 @@ static int fw_nftables_add_local_dnat_internal(
uint16_t remote_port,
const union in_addr_union *previous_remote) {
- _cleanup_(sd_netlink_message_unref_many) sd_netlink_message *messages[3] = {};
+ sd_netlink_message *messages[3] = {};
+ CLEANUP_ELEMENTS(messages, sd_netlink_message_unref_array_clear);
uint32_t data[5], key[2], dlen;
size_t msgcnt = 0;
int r;
diff --git a/src/shared/format-table.h b/src/shared/format-table.h
index 5b98d49017524..ba4d33cfc6719 100644
--- a/src/shared/format-table.h
+++ b/src/shared/format-table.h
@@ -101,7 +101,7 @@ Table* table_new_vertical(void);
Table* table_unref(Table *t);
DEFINE_TRIVIAL_CLEANUP_FUNC(Table*, table_unref);
-static inline DEFINE_ARRAY_DONE_FUNC(Table*, table_unref);
+static inline DEFINE_POINTER_ARRAY_CLEAR_FUNC(Table*, table_unref);
int table_add_cell_full(Table *t, TableCell **ret_cell, TableDataType dt, const void *data, size_t minimum_width, size_t maximum_width, unsigned weight, unsigned align_percent, unsigned ellipsize_percent);
static inline int table_add_cell(Table *t, TableCell **ret_cell, TableDataType dt, const void *data) {
diff --git a/src/shared/meson.build b/src/shared/meson.build
index c28fe040b6b2b..cd34b02f8506d 100644
--- a/src/shared/meson.build
+++ b/src/shared/meson.build
@@ -245,6 +245,7 @@ shared_sources = files(
'varlink-io.systemd.Resolve.Hook.c',
'varlink-io.systemd.Resolve.Monitor.c',
'varlink-io.systemd.Shutdown.c',
+ 'varlink-io.systemd.StorageProvider.c',
'varlink-io.systemd.Udev.c',
'varlink-io.systemd.Unit.c',
'varlink-io.systemd.UserDatabase.c',
diff --git a/src/shared/options.c b/src/shared/options.c
index 01684fc1fced5..85ab3155bf867 100644
--- a/src/shared/options.c
+++ b/src/shared/options.c
@@ -344,7 +344,7 @@ int option_parse(
return r;
}
-char* option_parser_next_arg(const OptionParser *state) {
+char* option_parser_peek_next_arg(const OptionParser *state) {
/* Peek at the next argument, whatever it is (option or position arg).
* May return NULL. */
@@ -360,7 +360,7 @@ char* option_parser_consume_next_arg(OptionParser *state) {
* so we won't try to interpret it as an option.
* May return NULL. */
- char *t = option_parser_next_arg(state);
+ char *t = option_parser_peek_next_arg(state);
if (t)
shift_arg(state->argv, state->positional_offset++, state->optind++);
return t;
@@ -388,6 +388,14 @@ size_t option_parser_get_n_args(const OptionParser *state) {
return state->argc - state->positional_offset;
}
+char* option_parser_get_arg(const OptionParser *state, size_t i) {
+ assert(state->optind > 0);
+ assert(state->state == OPTION_PARSER_DONE);
+ assert(state->positional_offset <= state->argc);
+
+ return (size_t) (state->argc - state->positional_offset) > i ? state->argv[state->positional_offset + i] : NULL;
+}
+
char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metavar) {
assert(opt);
assert(!(opt->flags & (OPTION_NAMESPACE_MARKER |
diff --git a/src/shared/options.h b/src/shared/options.h
index 5f55dd5d19fa7..1f28dab8ad51f 100644
--- a/src/shared/options.h
+++ b/src/shared/options.h
@@ -150,10 +150,13 @@ typedef struct Option {
"(file, provider:PROVIDER)")
/* A form used in udev code for compatibility. -V is accepted but not documented. */
-#define OPTION_COMMON_VERSION_WITH_HIDDEN_V \
- OPTION_COMMON_VERSION: {} \
+#define OPTION_COMMON_VERSION_WITH_HIDDEN_V \
+ OPTION_COMMON_VERSION: {} \
OPTION_SHORT('V', NULL, /* help= */ NULL)
+#define OPTION_COMMON_RESOLVE_NAMES \
+ OPTION('N', "resolve-names", "MODE", \
+ "When to resolve users and groups (early, late, or never)")
/* This is magically mapped to the beginning and end of the section */
extern const Option __start_SYSTEMD_OPTIONS[];
@@ -213,19 +216,27 @@ int option_parse(
const Option options_end[],
OptionParser *state);
-/* Iterate over options. */
-#define FOREACH_OPTION(c, state, on_error) \
+/* Iterate over options. Don't forget to handle errors (negative c)! */
+#define FOREACH_OPTION(c, state) \
+ for (int c; (c = option_parse(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, state)) != 0; )
+
+#define FOREACH_OPTION_OR_RETURN(c, state) \
for (int c; (c = option_parse(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, state)) != 0; ) \
- if (c < 0) { \
- on_error; \
- break; \
- } else
+ if (c < 0) \
+ return c; \
+ else
-char* option_parser_next_arg(const OptionParser *state);
+/* Those helpers are used *during* option parsing and allow looking at or taking the next item in
+ * the argv array, either an option or a positional parameter. */
+char* option_parser_peek_next_arg(const OptionParser *state);
char* option_parser_consume_next_arg(OptionParser *state);
+/* Those helpers are used *after* option parsing and return the positional arguments (and unparsed
+ * options in case option parsing was stopped early, e.g. via "--"). */
char** option_parser_get_args(const OptionParser *state);
size_t option_parser_get_n_args(const OptionParser *state);
+char* option_parser_get_arg(const OptionParser *state, size_t i);
+
char* option_get_synopsis(const Option *opt, const char *joiner, bool show_metavar);
int _option_parser_get_help_table_full(
@@ -236,6 +247,8 @@ int _option_parser_get_help_table_full(
Table **ret);
#define option_parser_get_help_table_full(namespace, group, ret) \
_option_parser_get_help_table_full(ALIGN_PTR(__start_SYSTEMD_OPTIONS), __stop_SYSTEMD_OPTIONS, namespace, group, ret)
+#define option_parser_get_help_table_ns(ns, ret) \
+ option_parser_get_help_table_full(ns, /* group= */ NULL, ret)
#define option_parser_get_help_table_group(group, ret) \
option_parser_get_help_table_full(/* namespace= */ NULL, group, ret)
#define option_parser_get_help_table(ret) \
diff --git a/src/shared/qmp-client.c b/src/shared/qmp-client.c
index 41b0c6dd57034..cb8f37daa8d1f 100644
--- a/src/shared/qmp-client.c
+++ b/src/shared/qmp-client.c
@@ -1,6 +1,7 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-json.h"
#include "alloc-util.h"
@@ -226,19 +227,23 @@ static int qmp_extract_response_id(sd_json_variant *v, uint64_t *ret) {
return 1;
}
-/* Returns 0 on success (ret_result = "return" value), -EIO on QMP error (reterr_desc set). */
-static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, const char **reterr_desc) {
+/* Returns 0 on success (ret_result = freshly reffed "return" value), -EIO on QMP error
+ * (ret_error_desc set to a freshly allocated string). Caller owns both outputs. */
+static int qmp_parse_response(sd_json_variant *v, sd_json_variant **ret_result, char **ret_error_desc) {
const char *desc;
desc = qmp_extract_error_description(v);
if (desc) {
- if (reterr_desc)
- *reterr_desc = desc;
+ if (ret_error_desc) {
+ *ret_error_desc = strdup(desc);
+ if (!*ret_error_desc)
+ return -ENOMEM;
+ }
return -EIO;
}
if (ret_result)
- *ret_result = sd_json_variant_by_key(v, "return");
+ *ret_result = sd_json_variant_ref(sd_json_variant_by_key(v, "return"));
return 0;
}
@@ -273,8 +278,8 @@ static int qmp_client_build_command(
/* Route c->current to event callback or matching async slot. Returns 1 on dispatch. */
static int qmp_client_dispatch(QmpClient *c) {
- sd_json_variant *result = NULL;
- const char *desc = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
+ _cleanup_free_ char *desc = NULL;
uint64_t id;
int error, r;
@@ -318,8 +323,8 @@ static int qmp_client_dispatch(QmpClient *c) {
}
/* Synchronous slot (no callback): leave c->current pinned so qmp_client_call() can
- * pick up the reply and hand out borrowed pointers into it. The sync caller owns a
- * ref on the slot and detects completion by observing slot->client turning NULL. */
+ * pick the reply up after its pump loop. The sync caller owns a ref on the slot and
+ * detects completion by observing slot->client turning NULL. */
if (!slot->callback) {
qmp_slot_disconnect(slot, /* unref= */ true);
return 1;
@@ -574,6 +579,10 @@ static void qmp_client_clear(QmpClient *c) {
qmp_client_detach_event(c);
qmp_client_clear_current(c);
json_stream_done(&c->stream);
+ /* qmp_client_handle_disconnect() above drained every entry via qmp_client_fail_pending();
+ * the set is borrow-only for non-floating slots, so set_free() can't safely run a
+ * destructor over leftovers — enforce the drain invariant instead. */
+ assert(set_isempty(c->slots));
c->slots = set_free(c->slots);
}
@@ -745,7 +754,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(QmpClientArgs*, qmp_client_args_close_fds);
/* Shared send path for qmp_client_invoke() and qmp_client_call(). A NULL callback registers
* a "synchronous" slot: dispatch_reply leaves c->current pinned on match instead of invoking
- * a callback, so qmp_client_call() can hand out borrowed pointers into the reply. If ret_slot
+ * a callback, so qmp_client_call() can pick the reply up after its pump loop. If ret_slot
* is NULL the slot is allocated as floating (owned by c->slots); otherwise a reference is
* handed back to the caller. */
static int qmp_client_send(
@@ -810,21 +819,173 @@ int qmp_client_invoke(
return qmp_client_send(c, command, args, callback, userdata, ret_slot);
}
+typedef struct QmpFuture {
+ sd_promise *promise;
+ QmpSlot *slot; /* owned, non-floating; NULL once disconnected */
+ sd_json_variant *result;
+ char *error_desc;
+} QmpFuture;
+
+static void* qmp_future_free(void *userdata) {
+ QmpFuture *f = userdata;
+ if (!f)
+ return NULL;
+
+ qmp_slot_unref(f->slot);
+ sd_json_variant_unref(f->result);
+ free(f->error_desc);
+ return mfree(f);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(QmpFuture*, qmp_future_free);
+
+static int qmp_future_cancel(void *userdata) {
+ QmpFuture *f = ASSERT_PTR(userdata);
+
+ /* Drop the pending slot so dispatch_reply won't try to fire our callback (and touch
+ * freed memory) when the reply eventually arrives. */
+ f->slot = qmp_slot_unref(f->slot);
+ return sd_promise_resolve(f->promise, -ECANCELED);
+}
+
+static const sd_future_ops qmp_call_future_ops = {
+ .free = qmp_future_free,
+ .cancel = qmp_future_cancel,
+};
+
+static int qmp_future_callback(
+ QmpClient *c,
+ sd_json_variant *result,
+ const char *desc,
+ int error,
+ void *userdata) {
+
+ QmpFuture *f = ASSERT_PTR(userdata);
+
+ if (result)
+ f->result = sd_json_variant_ref(result);
+ if (desc) {
+ f->error_desc = strdup(desc);
+ if (!f->error_desc)
+ /* No usable reply payload to surface — propagate as transport-style
+ * failure so suspend() / sd_future_result() see the OOM. */
+ return sd_promise_resolve(f->promise, -ENOMEM);
+ }
+
+ /* Resolve with 0 whenever a reply landed (success or QMP-level error) so the future's
+ * result encodes only "no reply will arrive" — i.e. transport failure or cancellation.
+ * The reply payload is dispatched in future_get_qmp_reply(). */
+ return sd_promise_resolve(f->promise, (result || desc) ? 0 : error);
+}
+
+int qmp_client_call_future(
+ QmpClient *c,
+ const char *command,
+ QmpClientArgs *args,
+ sd_future **ret) {
+
+ int r;
+
+ assert(c);
+ assert(command);
+ assert(ret);
+
+ _cleanup_(qmp_future_freep) QmpFuture *impl = new0(QmpFuture, 1);
+ if (!impl)
+ return -ENOMEM;
+
+ r = qmp_client_send(c, command, args, qmp_future_callback, impl, &impl->slot);
+ if (r < 0)
+ return r;
+
+ sd_future *f;
+ r = sd_future_new(&qmp_call_future_ops, impl, &f);
+ if (r < 0)
+ return r;
+
+ TAKE_PTR(impl);
+ *ret = TAKE_PTR(f);
+ return 0;
+}
+
+/* Extract the reply from a resolved qmp_client_call_future(). On success *ret_result is a fresh
+ * reference (caller unrefs) and *ret_error_desc is a freshly allocated string (caller frees).
+ * Returns -EIO when a QMP-level error was returned but the caller passed a NULL ret_error_desc. */
+int future_get_qmp_reply(sd_future *f, sd_json_variant **ret_result, char **ret_error_desc) {
+
+ assert(f);
+ assert(sd_future_get_ops(f) == &qmp_call_future_ops);
+ assert(sd_future_state(f) == SD_FUTURE_RESOLVED);
+
+ QmpFuture *impl = ASSERT_PTR(sd_future_get_impl(f));
+
+ if (impl->error_desc && !ret_error_desc)
+ return -EIO;
+
+ if (ret_error_desc && impl->error_desc) {
+ char *desc = strdup(impl->error_desc);
+ if (!desc)
+ return -ENOMEM;
+ *ret_error_desc = desc;
+ } else if (ret_error_desc)
+ *ret_error_desc = NULL;
+
+ if (ret_result)
+ *ret_result = sd_json_variant_ref(impl->result);
+ return 0;
+}
+
+int qmp_client_call_suspend(
+ QmpClient *c,
+ const char *command,
+ QmpClientArgs *args,
+ sd_json_variant **ret_result,
+ char **ret_error_desc) {
+
+ int r;
+
+ assert(c);
+ assert(command);
+ assert(sd_fiber_is_running());
+
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *call = NULL;
+ r = qmp_client_call_future(c, command, args, &call);
+ if (r < 0)
+ return r;
+
+ /* The call future resolves with 0 once a reply (success or QMP-level error) lands,
+ * negative on transport failure or cancellation; sd_fiber_suspend() propagates that. */
+ r = sd_fiber_suspend();
+ if (r < 0)
+ return r;
+
+ r = future_get_qmp_reply(call, ret_result, ret_error_desc);
+ if (r < 0)
+ return r;
+
+ return 1;
+}
+
int qmp_client_call(
QmpClient *c,
const char *command,
QmpClientArgs *args,
sd_json_variant **ret_result,
- const char **ret_error_desc) {
+ char **ret_error_desc) {
- _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
+ _cleanup_free_ char *desc = NULL;
int r;
assert_return(c, -EINVAL);
assert_return(command, -EINVAL);
- /* Drop any reply pinned by a previous qmp_client_call() before we pin a new one. */
- qmp_client_clear_current(c);
+ /* If we're on a fiber sharing the QMP client's event loop, use the async + suspend path so
+ * multiple concurrent qmp_client_call() invocations across fibers don't deadlock each other
+ * on the process+wait pump. */
+ if (sd_fiber_is_running() && qmp_client_get_event(c) == sd_fiber_get_event())
+ return qmp_client_call_suspend(c, command, args, ret_result, ret_error_desc);
+
+ _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
/* NULL callback marks this as a synchronous slot: dispatch_reply matches on id like
* any other slot (so stray unknown-id replies still get logged and dropped), but
@@ -855,18 +1016,19 @@ int qmp_client_call(
return r;
}
- sd_json_variant *result = NULL;
- const char *desc = NULL;
- int error = qmp_parse_response(c->current, &result, &desc);
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *current = TAKE_PTR(c->current);
+ r = qmp_parse_response(current, &result, &desc);
+ if (r < 0 && r != -EIO)
+ return r;
- /* If caller doesn't ask for the error string, surface the error as the return code. */
- if (!ret_error_desc && error < 0)
- return error;
+ /* If caller doesn't ask for the error string, surface QMP errors as -EIO. */
+ if (desc && !ret_error_desc)
+ return -EIO;
if (ret_result)
- *ret_result = result;
+ *ret_result = TAKE_PTR(result);
if (ret_error_desc)
- *ret_error_desc = desc;
+ *ret_error_desc = TAKE_PTR(desc);
return 1;
}
diff --git a/src/shared/qmp-client.h b/src/shared/qmp-client.h
index 7dcd53355d06c..6313653923841 100644
--- a/src/shared/qmp-client.h
+++ b/src/shared/qmp-client.h
@@ -68,15 +68,43 @@ int qmp_client_invoke(
qmp_command_callback_t callback,
void *userdata);
-/* Synchronous send + receive. Pumps the event loop until the reply arrives. *ret_result and
- * *ret_error_desc are borrowed pointers into the last reply, valid until the next
- * qmp_client_call(). Same contract as sd_varlink_call(). */
+/* Synchronous send + receive. Pumps the event loop until the reply arrives. On success
+ * *ret_result is a fresh reference (caller unrefs) and *ret_error_desc is a freshly allocated
+ * string (caller frees) — multiple concurrent calls on the same client therefore don't
+ * invalidate each other's outputs. */
int qmp_client_call(
QmpClient *client,
const char *command,
QmpClientArgs *args,
sd_json_variant **ret_result,
- const char **ret_error_desc);
+ char **ret_error_desc);
+
+/* Issue a QMP command asynchronously and return an sd_future that resolves when the reply
+ * arrives. sd_future_result(f) is 0 once a reply has landed (success or QMP-level error;
+ * use future_get_qmp_reply() to retrieve the result/error_desc), or a negative errno on
+ * transport failure or cancellation. */
+int qmp_client_call_future(
+ QmpClient *client,
+ const char *command,
+ QmpClientArgs *args,
+ sd_future **ret);
+
+/* Extract the reply from a resolved qmp_client_call_future(). On success *ret_result is a fresh
+ * reference (caller unrefs) and *ret_error_desc is a freshly allocated string (caller frees). */
+int future_get_qmp_reply(
+ sd_future *f,
+ sd_json_variant **ret_result,
+ char **ret_error_desc);
+
+/* Fiber-suspending variant of qmp_client_call(): only valid on a fiber whose event loop matches
+ * the client's. Same ownership contract as qmp_client_call(): on success *ret_result is a fresh
+ * reference (caller unrefs) and *ret_error_desc is a freshly allocated string (caller frees). */
+int qmp_client_call_suspend(
+ QmpClient *client,
+ const char *command,
+ QmpClientArgs *args,
+ sd_json_variant **ret_result,
+ char **ret_error_desc);
void qmp_client_bind_event(QmpClient *c, qmp_event_callback_t callback, void *userdata);
void qmp_client_bind_disconnect(QmpClient *c, qmp_disconnect_callback_t callback, void *userdata);
diff --git a/src/test/test-varlink-idl-util.h b/src/shared/test-varlink-idl-util.h
similarity index 100%
rename from src/test/test-varlink-idl-util.h
rename to src/shared/test-varlink-idl-util.h
diff --git a/src/shared/user-record.c b/src/shared/user-record.c
index 4dfb2c72d70f0..cf33d92215b8d 100644
--- a/src/shared/user-record.c
+++ b/src/shared/user-record.c
@@ -518,6 +518,7 @@ static int json_dispatch_locales(const char *name, sd_json_variant *variant, sd_
char ***l = userdata;
const char *locale;
sd_json_variant *e;
+ size_t s = 0;
int r;
if (sd_json_variant_is_null(variant)) {
@@ -536,7 +537,7 @@ static int json_dispatch_locales(const char *name, sd_json_variant *variant, sd_
if (!locale_is_valid(locale))
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array of valid locales.", strna(name));
- r = strv_extend(&n, locale);
+ r = strv_extend_with_size(&n, &s, locale);
if (r < 0)
return json_log_oom(variant, flags);
}
@@ -593,6 +594,7 @@ static int json_dispatch_weight(const char *name, sd_json_variant *variant, sd_j
int json_dispatch_user_group_list(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
char ***list = ASSERT_PTR(userdata);
_cleanup_strv_free_ char **l = NULL;
+ size_t s = 0;
int r;
if (!sd_json_variant_is_array(variant))
@@ -606,7 +608,7 @@ int json_dispatch_user_group_list(const char *name, sd_json_variant *variant, sd
if (!valid_user_group_name(sd_json_variant_string(e), FLAGS_SET(flags, SD_JSON_RELAX) ? VALID_USER_RELAX : 0))
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not a valid user/group name: %s", sd_json_variant_string(e));
- r = strv_extend(&l, sd_json_variant_string(e));
+ r = strv_extend_with_size(&l, &s, sd_json_variant_string(e));
if (r < 0)
return json_log(e, flags, r, "Failed to append array element: %m");
}
diff --git a/src/shared/varlink-io.systemd.StorageProvider.c b/src/shared/varlink-io.systemd.StorageProvider.c
new file mode 100644
index 0000000000000..cd2a4f3fda0bc
--- /dev/null
+++ b/src/shared/varlink-io.systemd.StorageProvider.c
@@ -0,0 +1,119 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "bus-polkit.h"
+#include "varlink-io.systemd.StorageProvider.h"
+
+static SD_VARLINK_DEFINE_ENUM_TYPE(
+ VolumeType,
+ SD_VARLINK_FIELD_COMMENT("Block device storage volumes, block-addressable"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(blk),
+ SD_VARLINK_FIELD_COMMENT("Regular file storage volumes, byte-addressable"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(reg),
+ SD_VARLINK_FIELD_COMMENT("POSIX file system storage volumes, path/offset-addressable"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(dir));
+
+static SD_VARLINK_DEFINE_ENUM_TYPE(
+ CreateMode,
+ SD_VARLINK_FIELD_COMMENT("Open if exists already, create if missing"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(any),
+ SD_VARLINK_FIELD_COMMENT("Create if missing, fail if exists already"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(new),
+ SD_VARLINK_FIELD_COMMENT("Open if exists already, fail if missing"),
+ SD_VARLINK_DEFINE_ENUM_VALUE(open));
+
+static SD_VARLINK_DEFINE_METHOD(
+ Acquire,
+ SD_VARLINK_FIELD_COMMENT("The name of the storage volume to acquire"),
+ SD_VARLINK_DEFINE_INPUT(name, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("Determines whether to open or create a storage volume"),
+ SD_VARLINK_DEFINE_INPUT_BY_TYPE(createMode, CreateMode, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The template to use when creating a new storage volume"),
+ SD_VARLINK_DEFINE_INPUT(template, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Controls read/write access to the storage volume. If false and the storage volume cannot be opened in writable mode the call will fail. If null, storage volume will be acquired in writable mode if possible, read-only otherwise. If true, storage volume will be opened in read-only mode (and fail if that's not possible)."),
+ SD_VARLINK_DEFINE_INPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Dictates what kind of storage volume to request. Some storage volumes can be acquired either as regular file or as block device. In all other cases if this value doesn't match the volume type, the request will fail."),
+ SD_VARLINK_DEFINE_INPUT_BY_TYPE(requestAs, VolumeType, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The size of the storage volume, if one is created. Has no effect if no storage volume is created."),
+ SD_VARLINK_DEFINE_INPUT(createSizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT,
+ SD_VARLINK_FIELD_COMMENT("Returns an index into the array of file descriptors associated with this reply. This may be used to get the file descriptor of the volume. The file descriptor must be properly opened, i.e. not an O_PATH file descriptor."),
+ SD_VARLINK_DEFINE_OUTPUT(fileDescriptorIndex, SD_VARLINK_INT, 0),
+ SD_VARLINK_FIELD_COMMENT("The storage volume type, i.e. ultimately the inode type of the returned file descriptor"),
+ SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0),
+ SD_VARLINK_FIELD_COMMENT("Whether storage volume has been opened in read-only mode"),
+ SD_VARLINK_DEFINE_OUTPUT(readOnly, SD_VARLINK_BOOL, 0),
+ SD_VARLINK_FIELD_COMMENT("Base UID for the returned file descriptor (if directory). If not specified shall default to 0."),
+ SD_VARLINK_DEFINE_OUTPUT(baseUID, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Base GID for the returned file descriptor (if directory). If not specified shall default to 0."),
+ SD_VARLINK_DEFINE_OUTPUT(baseGID, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+ ListVolumes,
+ SD_VARLINK_REQUIRES_MORE,
+ SD_VARLINK_FIELD_COMMENT("Specifies a shell glob to filter enumeration by"),
+ SD_VARLINK_DEFINE_INPUT(matchName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The storage volume's primary name"),
+ SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("Additional names"),
+ SD_VARLINK_DEFINE_OUTPUT(aliases, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ SD_VARLINK_FIELD_COMMENT("The type of the storage volume"),
+ SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0),
+ SD_VARLINK_FIELD_COMMENT("Whether the storage volume is read-only."),
+ SD_VARLINK_DEFINE_OUTPUT(readOnly, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Size in bytes, if known"),
+ SD_VARLINK_DEFINE_OUTPUT(sizeBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Used bytes, if known"),
+ SD_VARLINK_DEFINE_OUTPUT(usedBytes, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
+
+static SD_VARLINK_DEFINE_METHOD_FULL(
+ ListTemplates,
+ SD_VARLINK_REQUIRES_MORE,
+ SD_VARLINK_FIELD_COMMENT("Specifies a shell glob to filter enumeration by"),
+ SD_VARLINK_DEFINE_INPUT(matchName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The template's name"),
+ SD_VARLINK_DEFINE_OUTPUT(name, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The type of the storage volumes defined by this template"),
+ SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(type, VolumeType, 0));
+
+static SD_VARLINK_DEFINE_ERROR(NoSuchVolume);
+static SD_VARLINK_DEFINE_ERROR(VolumeExists);
+static SD_VARLINK_DEFINE_ERROR(NoSuchTemplate);
+static SD_VARLINK_DEFINE_ERROR(TypeNotSupported);
+static SD_VARLINK_DEFINE_ERROR(WrongType);
+static SD_VARLINK_DEFINE_ERROR(CreateNotSupported);
+static SD_VARLINK_DEFINE_ERROR(CreateSizeRequired);
+static SD_VARLINK_DEFINE_ERROR(ReadOnlyVolume);
+static SD_VARLINK_DEFINE_ERROR(BadTemplate);
+
+SD_VARLINK_DEFINE_INTERFACE(
+ io_systemd_StorageProvider,
+ "io.systemd.StorageProvider",
+ SD_VARLINK_INTERFACE_COMMENT("Storage Provider API, a generic interface for acquiring access to storage volumes"),
+ SD_VARLINK_SYMBOL_COMMENT("Encodes three classes of storage volumes. This follows the kernel's nomenclature for inode types, i.e. reg, dir, blk."),
+ &vl_type_VolumeType,
+ SD_VARLINK_SYMBOL_COMMENT("Determines whether to open existing or create a new storage volume."),
+ &vl_type_CreateMode,
+ SD_VARLINK_SYMBOL_COMMENT("Acquires a file descriptor for a storage volume."),
+ &vl_method_Acquire,
+ SD_VARLINK_SYMBOL_COMMENT("Lists available storage volumes."),
+ &vl_method_ListVolumes,
+ SD_VARLINK_SYMBOL_COMMENT("Lists available templates."),
+ &vl_method_ListTemplates,
+ SD_VARLINK_SYMBOL_COMMENT("No storage volume under the specified name exists."),
+ &vl_error_NoSuchVolume,
+ SD_VARLINK_SYMBOL_COMMENT("A storage volume under the specified name already exists."),
+ &vl_error_VolumeExists,
+ SD_VARLINK_SYMBOL_COMMENT("No template under the specified name exists."),
+ &vl_error_NoSuchTemplate,
+ SD_VARLINK_SYMBOL_COMMENT("The specified volume type is not supported by this backend or system."),
+ &vl_error_TypeNotSupported,
+ SD_VARLINK_SYMBOL_COMMENT("The volume's type does not match the requested volume type."),
+ &vl_error_WrongType,
+ SD_VARLINK_SYMBOL_COMMENT("This backend does not support storage volume creation of the requested type."),
+ &vl_error_CreateNotSupported,
+ SD_VARLINK_SYMBOL_COMMENT("This backend or selected volume type requires a storage volume size to be specified if the storage volume does not exist yet and needs to be created."),
+ &vl_error_CreateSizeRequired,
+ SD_VARLINK_SYMBOL_COMMENT("A storage volume was to be acquired in writable mode, but only read-only access is permitted."),
+ &vl_error_ReadOnlyVolume,
+ SD_VARLINK_SYMBOL_COMMENT("Template not suitable for this storage volume type."),
+ &vl_error_BadTemplate);
diff --git a/src/shared/varlink-io.systemd.StorageProvider.h b/src/shared/varlink-io.systemd.StorageProvider.h
new file mode 100644
index 0000000000000..707d05644f2cf
--- /dev/null
+++ b/src/shared/varlink-io.systemd.StorageProvider.h
@@ -0,0 +1,6 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-varlink-idl.h"
+
+extern const sd_varlink_interface vl_interface_io_systemd_StorageProvider;
diff --git a/src/shutdown/shutdown.c b/src/shutdown/shutdown.c
index c572138596d38..131550c46b20d 100644
--- a/src/shutdown/shutdown.c
+++ b/src/shutdown/shutdown.c
@@ -66,7 +66,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_RETURN_POSITIONAL_ARGS };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_LOG_LEVEL:
diff --git a/src/sleep/sleep.c b/src/sleep/sleep.c
index 3b2f9d698bb84..53f306a8faefc 100644
--- a/src/sleep/sleep.c
+++ b/src/sleep/sleep.c
@@ -731,7 +731,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/socket-activate/socket-activate.c b/src/socket-activate/socket-activate.c
index 03cf327b6259e..768a2a3ea7235 100644
--- a/src/socket-activate/socket-activate.c
+++ b/src/socket-activate/socket-activate.c
@@ -358,7 +358,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/socket-proxy/socket-proxyd.c b/src/socket-proxy/socket-proxyd.c
index ea68009b35802..77dc903535633 100644
--- a/src/socket-proxy/socket-proxyd.c
+++ b/src/socket-proxy/socket-proxyd.c
@@ -423,7 +423,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/ssh-generator/ssh-issue.c b/src/ssh-generator/ssh-issue.c
index ee128b5e1811c..2028d3f942393 100644
--- a/src/ssh-generator/ssh-issue.c
+++ b/src/ssh-generator/ssh-issue.c
@@ -163,7 +163,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
const char *verb = NULL;
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/stdio-bridge/stdio-bridge.c b/src/stdio-bridge/stdio-bridge.c
index 4be5205d59894..01686d2cd6c0b 100644
--- a/src/stdio-bridge/stdio-bridge.c
+++ b/src/stdio-bridge/stdio-bridge.c
@@ -49,7 +49,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/storage/io.systemd.storage.policy b/src/storage/io.systemd.storage.policy
new file mode 100644
index 0000000000000..7b25553501520
--- /dev/null
+++ b/src/storage/io.systemd.storage.policy
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+ The systemd Project
+ https://systemd.io
+
+
+ Allow access to block storage volumes
+ Authentication is required for an application to gain access to block storage volume '$(name)'.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
+
+ Allow access to file system storage volumes
+ Authentication is required for an application to gain access to file system storage volume '$(name)'.
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
diff --git a/src/storage/meson.build b/src/storage/meson.build
new file mode 100644
index 0000000000000..21456141dec8c
--- /dev/null
+++ b/src/storage/meson.build
@@ -0,0 +1,27 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+executables += [
+ libexec_template + {
+ 'name' : 'systemd-storage-block',
+ 'sources' : files('storage-block.c'),
+ 'extract' : files('storage-util.c')
+ },
+ libexec_template + {
+ 'name' : 'systemd-storage-fs',
+ 'sources' : files('storage-fs.c'),
+ 'objects' : ['systemd-storage-block'],
+ },
+ executable_template + {
+ 'name' : 'storagectl',
+ 'public' : true,
+ 'sources' : files('storagectl.c'),
+ 'objects' : ['systemd-storage-block'],
+ },
+]
+
+install_symlink('mount.storage',
+ pointing_to : sbin_to_bin + 'storagectl',
+ install_dir : sbindir)
+
+install_data('io.systemd.storage.policy',
+ install_dir : polkitpolicydir)
diff --git a/src/storage/storage-block.c b/src/storage/storage-block.c
new file mode 100644
index 0000000000000..e5454a29c28a0
--- /dev/null
+++ b/src/storage/storage-block.c
@@ -0,0 +1,439 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-device.h"
+#include "sd-json.h"
+#include "sd-varlink.h"
+
+#include "blockdev-list.h"
+#include "build.h"
+#include "bus-polkit.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "hashmap.h"
+#include "help-util.h"
+#include "json-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "options.h"
+#include "path-util.h"
+#include "storage-util.h"
+#include "strv.h"
+#include "varlink-io.systemd.StorageProvider.h"
+#include "varlink-util.h"
+
+static int block_device_pick_name(
+ const BlockDevice *d,
+ const char **ret_name,
+ char ***ret_aliases) {
+
+ int r;
+
+ assert(d);
+ assert(d->node);
+ assert(ret_name);
+ assert(ret_aliases);
+
+ static const char *const prefixes[] = {
+ /* The list of preferred prefixes, in order of preference. Note: for security reasons we only
+ * use identifiers that do not depend on the *contents* of the device, i.e. we restrict
+ * ourselves to IDs whose fields are either chosen by whoever created the kernel device or are
+ * hardware properties, but not names generated from superblock metainformation or similar. */
+ "/dev/mapper",
+ "/dev/disk/by-loop-ref",
+ "/dev/disk/by-id",
+ "/dev/disk/by-path",
+ };
+
+ const char* found[ELEMENTSOF(prefixes)] = {};
+ _cleanup_strv_free_ char **aliases = NULL;
+ size_t best = SIZE_MAX;
+ STRV_FOREACH(sl, d->symlinks) {
+ bool matched = false;
+ for (size_t i = 0; i < ELEMENTSOF(prefixes); i++) {
+ if (!path_startswith(*sl, prefixes[i]))
+ continue;
+
+ if (found[i]) {
+ /* Two symlinks with the same prefix? Then keep the lower one. */
+ if (path_compare(*sl, found[i]) > 0)
+ continue;
+
+ r = strv_extend(&aliases, found[i]);
+ if (r < 0)
+ return r;
+ }
+
+ found[i] = *sl;
+ if (i < best)
+ best = i;
+ matched = true;
+ }
+
+ if (!matched) {
+ r = strv_extend(&aliases, *sl);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ if (best == SIZE_MAX) /* No preferred prefix found, use the kernel device name */
+ *ret_name = d->node;
+ else {
+ /* We found a preferred prefix, add the kernel device name to the aliases then. */
+ r = strv_extend(&aliases, d->node);
+ if (r < 0)
+ return r;
+
+ /* If there are any less preferred prefixes also add them to the aliases array */
+ for (size_t i = best + 1; i < ELEMENTSOF(prefixes); i++) {
+ if (!found[i])
+ continue;
+
+ r = strv_extend(&aliases, found[i]);
+ if (r < 0)
+ return r;
+ }
+
+ *ret_name = found[best];
+ }
+
+ strv_sort(aliases);
+ *ret_aliases = TAKE_PTR(aliases);
+
+ return 0;
+}
+
+static bool block_device_match(const BlockDevice *d, const char *match) {
+ assert(d);
+ assert(d->node);
+
+ if (!match)
+ return true;
+
+ if (fnmatch(match, d->node, FNM_NOESCAPE) == 0)
+ return true;
+
+ STRV_FOREACH(sl, d->symlinks)
+ if (fnmatch(match, *sl, FNM_NOESCAPE) == 0)
+ return true;
+
+ return false;
+}
+
+static int vl_method_list_volumes(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ int r;
+
+ assert(link);
+ assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
+
+ struct {
+ const char *match_name;
+ } p = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ BlockDevice *l = NULL;
+ size_t n = 0;
+ CLEANUP_ARRAY(l, n, block_device_array_free);
+
+ r = blockdev_list(
+ BLOCKDEV_LIST_SHOW_SYMLINKS|
+ BLOCKDEV_LIST_IGNORE_ROOT|
+ BLOCKDEV_LIST_IGNORE_EMPTY|
+ BLOCKDEV_LIST_METADATA,
+ &l,
+ &n);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchVolume");
+ if (r < 0)
+ return r;
+
+ FOREACH_ARRAY(d, l, n) {
+ const char *name = NULL;
+ _cleanup_strv_free_ char **aliases = NULL;
+
+ if (!block_device_match(d, p.match_name))
+ continue;
+
+ r = block_device_pick_name(d, &name, &aliases);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ JSON_BUILD_PAIR_STRV_NON_EMPTY("aliases", aliases),
+ SD_JSON_BUILD_PAIR_STRING("type", "blk"),
+ SD_JSON_BUILD_PAIR_CONDITION(d->read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(d->read_only)),
+ JSON_BUILD_PAIR_UNSIGNED_NOT_EQUAL("sizeBytes", d->size, UINT64_MAX));
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int vl_method_list_templates(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ /* This storage provider does not support templates */
+ assert(link);
+ assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
+
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
+}
+
+static int device_open_disk_auto_rw(sd_device *d, int *read_only) {
+ assert(d);
+ assert(read_only);
+
+ int fd = sd_device_open(d, *read_only > 0 ? O_RDONLY : O_RDWR);
+ if (fd < 0) {
+ if (!ERRNO_IS_NEG_FS_WRITE_REFUSED(fd) || *read_only >= 0)
+ return log_device_debug_errno(d, fd, "Failed to open device in %s mode: %m", *read_only > 0 ? "read-only" : "read-write");
+
+ /* Try again in read-only mode */
+ fd = sd_device_open(d, O_RDONLY);
+ if (fd < 0)
+ return log_device_debug_errno(d, fd, "Failed to open device in read-only mode, too: %m");
+
+ *read_only = true;
+ } else
+ *read_only = *read_only > 0;
+
+ return fd;
+}
+
+static int vl_method_acquire(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ Hashmap **polkit_registry = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+
+ struct {
+ const char *name;
+ CreateMode create_mode;
+ const char *template;
+ int read_only;
+ VolumeType request_as;
+ uint64_t create_size;
+ } p = {
+ .create_mode = CREATE_ANY,
+ .read_only = -1,
+ .request_as = _VOLUME_TYPE_INVALID,
+ .create_size = UINT64_MAX, /* never actually used here, just validated; we don't allow creation of block devices here */
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
+ { "createMode", SD_JSON_VARIANT_STRING, json_dispatch_create_mode, voffsetof(p, create_mode), 0 },
+ { "template", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, template), 0 },
+ { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
+ { "requestAs", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(p, request_as), 0 },
+ { "createSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, create_size), 0 },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (!storage_volume_name_is_valid(p.name))
+ return sd_varlink_error_invalid_parameter_name(link, "name");
+ if (!path_startswith(p.name, "/dev") || !path_is_normalized(p.name))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
+
+ if (!IN_SET(p.create_mode, CREATE_ANY, CREATE_OPEN))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
+
+ /* off_t is signed, hence refuse overly long requests */
+ if (p.create_size != UINT64_MAX && p.create_size > INT64_MAX)
+ return sd_varlink_error_invalid_parameter_name(link, "createSizeBytes");
+
+ if (!isempty(p.template)) {
+ if (!storage_template_name_is_valid(p.template))
+ return sd_varlink_error_invalid_parameter_name(link, "template");
+
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
+ }
+
+ if (p.request_as >= 0 && p.request_as != VOLUME_BLK)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.TypeNotSupported", NULL);
+
+ const char *details[] = {
+ "name", p.name,
+ NULL
+ };
+
+ r = varlink_verify_polkit_async(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.storage.block.acquire",
+ details,
+ polkit_registry);
+ if (r <= 0)
+ return r;
+
+ _cleanup_(sd_device_unrefp) sd_device *d = NULL;
+ r = sd_device_new_from_devname(&d, p.name);
+ if (ERRNO_IS_NEG_DEVICE_ABSENT(r))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
+ if (r < 0)
+ return r;
+
+ if (!device_in_subsystem(d, "block"))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
+
+ /* The error returns are sometimes a bit inconclusive (i.e. read-only media might appear as
+ * inaccessible due to a permission issue), hence let's do an explicit check first, to give good
+ * answers */
+ if (p.read_only <= 0) {
+ r = device_get_sysattr_bool(d, "ro");
+ if (r < 0)
+ log_device_debug_errno(d, r, "Failed to acquire read-only flag of device '%s', ignoring: %m", p.name);
+ else if (r > 0) {
+ if (p.read_only == 0)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
+
+ p.read_only = true;
+ }
+ }
+
+ _cleanup_close_ int fd = device_open_disk_auto_rw(d, &p.read_only);
+ if (ERRNO_IS_NEG_FS_WRITE_REFUSED(fd))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
+ if (fd < 0)
+ return fd;
+
+ assert(p.read_only >= 0); /* flag is now definitely initialized to either true or false, not negative anymore */
+
+ int idx = sd_varlink_push_fd(link, fd);
+ if (idx < 0)
+ return idx;
+
+ TAKE_FD(fd);
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", idx),
+ SD_JSON_BUILD_PAIR_STRING("type", "blk"),
+ SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", p.read_only));
+}
+
+static int vl_server(void) {
+ int r;
+
+ _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL;
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
+ r = varlink_server_new(
+ &varlink_server,
+ SD_VARLINK_SERVER_HANDLE_SIGINT|
+ SD_VARLINK_SERVER_HANDLE_SIGTERM|
+ SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT|
+ SD_VARLINK_SERVER_INHERIT_USERDATA,
+ &polkit_registry);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_StorageProvider);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = sd_varlink_server_bind_method_many(
+ varlink_server,
+ "io.systemd.StorageProvider.Acquire", vl_method_acquire,
+ "io.systemd.StorageProvider.ListVolumes", vl_method_list_volumes,
+ "io.systemd.StorageProvider.ListTemplates", vl_method_list_templates);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+ r = sd_varlink_server_loop_auto(varlink_server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return 0;
+}
+
+static int help(void) {
+ int r;
+
+ help_cmdline("[OPTIONS...]");
+ help_abstract("Simple block device backed storage provider");
+
+ _cleanup_(table_unrefp) Table *options = NULL;
+ r = option_parser_get_help_table(&options);
+ if (r < 0)
+ return r;
+
+ help_section("Options:");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("systemd-storage-block", "8");
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv };
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION_COMMON_VERSION:
+ return version();
+ }
+
+ if (option_parser_get_n_args(&opts) > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ return 1;
+}
+
+static int run(int argc, char* argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return vl_server();
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/storage/storage-fs.c b/src/storage/storage-fs.c
new file mode 100644
index 0000000000000..c01e91a4cefe6
--- /dev/null
+++ b/src/storage/storage-fs.c
@@ -0,0 +1,807 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+#include
+#include
+#include
+
+#include "sd-device.h"
+#include "sd-json.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "bus-polkit.h"
+#include "chase.h"
+#include "chattr-util.h"
+#include "device-private.h"
+#include "device-util.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "fs-util.h"
+#include "hashmap.h"
+#include "help-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "options.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "recurse-dir.h"
+#include "runtime-scope.h"
+#include "stat-util.h"
+#include "storage-util.h"
+#include "string-table.h"
+#include "tmpfile-util.h"
+#include "uid-classification.h"
+#include "varlink-io.systemd.StorageProvider.h"
+#include "varlink-util.h"
+
+static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+
+/* For now we maintain a simple, compiled-in list of templates. One of those days we might want to move these
+ * into configurable drop-in files on disk. */
+typedef enum Template {
+ TEMPLATE_SPARSE_FILE,
+ TEMPLATE_ALLOCATED_FILE,
+ TEMPLATE_DIRECTORY,
+ TEMPLATE_SUBVOLUME,
+ _TEMPLATE_MAX,
+ _TEMPLATE_INVALID = -EINVAL,
+} Template;
+
+static const char *template_table[_TEMPLATE_MAX] = {
+ [TEMPLATE_SPARSE_FILE] = "sparse-file",
+ [TEMPLATE_ALLOCATED_FILE] = "allocated-file",
+ [TEMPLATE_DIRECTORY] = "directory",
+ [TEMPLATE_SUBVOLUME] = "subvolume",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP(template, Template);
+
+static VolumeType volume_type_from_template(Template t) {
+ switch (t) {
+
+ case TEMPLATE_SPARSE_FILE:
+ case TEMPLATE_ALLOCATED_FILE:
+ return VOLUME_REG;
+
+ case TEMPLATE_DIRECTORY:
+ case TEMPLATE_SUBVOLUME:
+ return VOLUME_DIR;
+
+ default:
+ return _VOLUME_TYPE_INVALID;
+ }
+}
+
+static int open_storage_dir(void) {
+ int r;
+
+ _cleanup_free_ char *state_dir = NULL;
+ r = state_directory_generic(arg_runtime_scope, /* suffix= */ NULL, &state_dir);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get state directory path: %m");
+
+ _cleanup_close_ int state_fd = chase_and_open(state_dir, /* root= */ NULL, CHASE_TRIGGER_AUTOFS|CHASE_MKDIR_0755|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_CREAT|O_DIRECTORY, /* ret_path= */ NULL);
+ if (state_fd < 0)
+ return log_error_errno(state_fd, "Failed to open '%s': %m", state_dir);
+
+ /* First we try to open the storage directory. If it exists this will work and we are happy. If we
+ * get ENOENT we'll try to create it. If that works, great. If we get EEXIST we'll try to reopen it
+ * again, to deal with other instances of ourselves racing with us. We only do this exactly once
+ * though, under the assumption that the dir is never removed, only created during runtime. */
+ _cleanup_close_ int storage_fd = chase_and_openat(XAT_FDROOT, state_fd, "storage", CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_DIRECTORY, /* ret_path= */ NULL);
+ if (storage_fd == -ENOENT) {
+ storage_fd = xopenat_full(state_fd, "storage", O_EXCL|O_CREAT|O_CLOEXEC|O_DIRECTORY|O_NOFOLLOW, XO_LABEL|XO_SUBVOLUME, 0700);
+ if (storage_fd == -EEXIST)
+ storage_fd = chase_and_openat(XAT_FDROOT, state_fd, "storage", CHASE_TRIGGER_AUTOFS|CHASE_MUST_BE_DIRECTORY, O_CLOEXEC|O_DIRECTORY, /* ret_path= */ NULL);
+ }
+ if (storage_fd < 0)
+ return log_error_errno(storage_fd, "Failed to open '%s/storage/': %m", state_dir);
+
+ return TAKE_FD(storage_fd);
+}
+
+static int vl_method_list_volumes(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ int r;
+
+ assert(link);
+ assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
+
+ struct {
+ const char *match_name;
+ } p = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ _cleanup_close_ int fd = open_storage_dir();
+ if (fd < 0)
+ return fd;
+
+ _cleanup_free_ DirectoryEntries *dentries = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT, &dentries);
+ if (r < 0)
+ return r;
+
+ r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchVolume");
+ if (r < 0)
+ return r;
+
+ FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
+ struct dirent *d = *dp;
+
+ if (!IN_SET(d->d_type, DT_REG, DT_DIR, DT_LNK, DT_BLK, DT_UNKNOWN))
+ continue;
+
+ const char *e = endswith(d->d_name, ".volume");
+ if (!e)
+ continue;
+
+ _cleanup_free_ char *n = strndup(d->d_name, e - d->d_name);
+ if (!n)
+ return log_oom_debug();
+
+ if (!storage_volume_name_is_valid(n))
+ continue;
+
+ if (p.match_name && fnmatch(p.match_name, n, FNM_NOESCAPE) != 0)
+ continue;
+
+ _cleanup_close_ int pin_fd = -EBADF;
+ r = chaseat(XAT_FDROOT, fd, d->d_name, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to stat() '%s' in storage directory, ignoring: %m", d->d_name);
+ continue;
+ }
+
+ struct stat st;
+ if (fstat(pin_fd, &st) < 0)
+ return log_debug_errno(errno, "Failed to stat() '%s' in storage directory: %m", d->d_name);
+
+ uint64_t size = UINT64_MAX, used = UINT64_MAX;
+ bool ro = false;
+
+ switch (st.st_mode & S_IFMT) {
+ case S_IFREG:
+ ro = (st.st_mode & 0222) == 0;
+ size = st.st_size;
+ used = (uint64_t) st.st_blocks * UINT64_C(512);
+ break;
+
+ case S_IFDIR:
+ r = fd_is_read_only_fs(pin_fd);
+ if (r < 0)
+ log_debug_errno(r, "Failed to determine if '%s' is read-only, ignoring", d->d_name);
+ else
+ ro = r > 0;
+ break;
+
+ case S_IFBLK: {
+ _cleanup_(sd_device_unrefp) sd_device *dev = NULL;
+
+ r = sd_device_new_from_stat_rdev(&dev, &st);
+ if (r < 0)
+ log_debug_errno(r, "Failed to acquire device for '%s', ignoring: %m", d->d_name);
+ else {
+ r = device_get_sysattr_bool(dev, "ro");
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to get read/only state of '%s', ignoring: %m", d->d_name);
+ else
+ ro = r > 0;
+
+ r = device_get_sysattr_u64(dev, "size", &size);
+ if (r < 0)
+ log_device_debug_errno(dev, r, "Failed to acquire size of device '%s', ignoring: %m", d->d_name);
+ else
+ /* the 'size' sysattr is always in multiples of 512, even on 4K sector block devices! */
+ assert_se(MUL_ASSIGN_SAFE(&size, 512)); /* Overflow check for coverity */
+ }
+
+ break;
+ }
+
+ default:
+ log_debug("Volume of unexpected inode type, ignoring: %s", d->d_name);
+ continue;
+ }
+
+ r = sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_STRING("name", n),
+ SD_JSON_BUILD_PAIR_STRING("type", inode_type_to_string(st.st_mode)),
+ SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", ro),
+ SD_JSON_BUILD_PAIR_CONDITION(size != UINT64_MAX, "sizeBytes", SD_JSON_BUILD_UNSIGNED(size)),
+ SD_JSON_BUILD_PAIR_CONDITION(used != UINT64_MAX, "usedBytes", SD_JSON_BUILD_UNSIGNED(used)));
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int vl_method_list_templates(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ int r;
+
+ assert(link);
+ assert(FLAGS_SET(flags, SD_VARLINK_METHOD_MORE));
+
+ struct {
+ const char *match_name;
+ } p = {};
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "matchName", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, match_name), 0 },
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ r = sd_varlink_set_sentinel(link, "io.systemd.StorageProvider.NoSuchTemplate");
+ if (r < 0)
+ return r;
+
+ for (Template t = 0; t < _TEMPLATE_MAX; t++) {
+ const char *n = template_to_string(t);
+
+ if (p.match_name && fnmatch(p.match_name, n, FNM_NOESCAPE) != 0)
+ continue;
+
+ r = sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_STRING("name", n),
+ SD_JSON_BUILD_PAIR_STRING("type", volume_type_to_string(volume_type_from_template(t))));
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int create_volume_dir(
+ int storage_fd,
+ const char *filename,
+ Template t) {
+
+ int r;
+
+ assert(storage_fd >= 0);
+ assert(filename);
+
+ XOpenFlags xopen_flags;
+ switch (t) {
+
+ case TEMPLATE_DIRECTORY:
+ xopen_flags = 0;
+ break;
+
+ case TEMPLATE_SUBVOLUME:
+ xopen_flags = XO_SUBVOLUME;
+ break;
+
+ default:
+ return -ENOMEDIUM; /* Recognizable error for: template doesn't apply here */
+ }
+
+ _cleanup_free_ char *tf = NULL;
+ r = tempfn_random(filename, /* extra= */ NULL, &tf);
+ if (r < 0)
+ return r;
+
+ _cleanup_close_ int volume_fd = xopenat_full(storage_fd, tf, O_CREAT|O_EXCL|O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW, xopen_flags, 0700);
+ if (volume_fd < 0)
+ return volume_fd;
+
+ _cleanup_close_ int root_fd = xopenat_full(volume_fd, "root", O_CREAT|O_EXCL|O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW, xopen_flags, 0755);
+ if (root_fd < 0) {
+ r = root_fd;
+ goto fail;
+ }
+
+ r = RET_NERRNO(fchown(root_fd, FOREIGN_UID_MIN, FOREIGN_UID_MIN));
+ if (r < 0)
+ goto fail;
+
+ r = rename_noreplace(storage_fd, tf, storage_fd, filename);
+ if (r < 0)
+ goto fail;
+
+ return TAKE_FD(root_fd);
+
+fail:
+ if (root_fd >= 0) {
+ assert(volume_fd >= 0);
+ root_fd = safe_close(root_fd);
+ (void) unlinkat(volume_fd, "root", AT_REMOVEDIR);
+ }
+
+ if (volume_fd >= 0) {
+ volume_fd = safe_close(volume_fd);
+ (void) unlinkat(storage_fd, tf, AT_REMOVEDIR);
+ }
+
+ return r;
+}
+
+static int create_volume_reg(
+ int storage_fd,
+ const char *filename,
+ Template t,
+ uint64_t create_size) {
+ int r;
+
+ assert(storage_fd >= 0);
+ assert(filename);
+
+ bool sparse;
+ switch (t) {
+
+ case TEMPLATE_SPARSE_FILE:
+ sparse = true;
+ break;
+
+ case TEMPLATE_ALLOCATED_FILE:
+ sparse = false;
+ break;
+
+ default:
+ return -ENOMEDIUM; /* Recognizable error for: template doesn't apply here */
+ }
+
+ _cleanup_free_ char *tf = NULL;
+ _cleanup_close_ int fd = open_tmpfile_linkable_at(storage_fd, filename, O_RDWR|O_CLOEXEC, &tf);
+ if (fd < 0)
+ return fd;
+
+ CLEANUP_TMPFILE_AT(storage_fd, tf);
+
+ r = chattr_fd(fd, FS_NOCOW_FL, FS_NOCOW_FL);
+ if (r < 0 && !ERRNO_IS_IOCTL_NOT_SUPPORTED(r))
+ return r;
+
+ if (create_size > 0) {
+ if (sparse)
+ r = RET_NERRNO(ftruncate(fd, create_size));
+ else
+ r = RET_NERRNO(fallocate(fd, /* mode= */ 0, /* offset= */ 0, create_size));
+ if (r < 0)
+ return r;
+ }
+
+ r = RET_NERRNO(fchmod(fd, 0600));
+ if (r < 0)
+ return r;
+
+ r = link_tmpfile_at(fd, storage_fd, tf, filename, /* flags= */ 0);
+ if (r < 0)
+ return r;
+
+ tf = mfree(tf); /* disarm clean-up */
+
+ return TAKE_FD(fd);
+}
+
+static int vl_method_acquire(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ sd_varlink_method_flags_t flags,
+ void *userdata) {
+
+ Hashmap **polkit_registry = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+
+ struct {
+ const char *name;
+ CreateMode create_mode;
+ const char *template;
+ int read_only;
+ VolumeType request_as;
+ uint64_t create_size;
+ } p = {
+ .create_mode = CREATE_ANY,
+ .read_only = -1,
+ .request_as = _VOLUME_TYPE_INVALID,
+ .create_size = UINT64_MAX,
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), SD_JSON_MANDATORY },
+ { "createMode", SD_JSON_VARIANT_STRING, json_dispatch_create_mode, voffsetof(p, create_mode), 0 },
+ { "template", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, template), 0 },
+ { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
+ { "requestAs", SD_JSON_VARIANT_STRING, json_dispatch_volume_type, voffsetof(p, request_as), 0 },
+ { "createSizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, create_size), 0 },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ if (!storage_volume_name_is_valid(p.name))
+ return sd_varlink_error_invalid_parameter_name(link, "name");
+
+ if (!IN_SET(p.create_mode, CREATE_ANY, CREATE_OPEN, CREATE_NEW))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
+
+ /* off_t is signed, hence refuse overly long requests */
+ if (p.create_size != UINT64_MAX && p.create_size > INT64_MAX)
+ return sd_varlink_error_invalid_parameter_name(link, "createSizeBytes");
+
+ Template t = _TEMPLATE_INVALID;
+ if (!isempty(p.template)) {
+ if (!storage_template_name_is_valid(p.template))
+ return sd_varlink_error_invalid_parameter_name(link, "template");
+
+ t = template_from_string(p.template);
+ if (t < 0)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchTemplate", NULL);
+ }
+
+ if (p.read_only > 0) {
+ if (p.create_mode == CREATE_NEW)
+ return sd_varlink_error_invalid_parameter_name(link, "readOnly");
+
+ p.create_mode = CREATE_OPEN;
+ }
+
+ /* Add a suffix so that we are never attempted to open a temporary file assuming it was a valid
+ * volume. */
+ _cleanup_free_ char *filename = strjoin(p.name, ".volume");
+ if (!filename)
+ return log_oom_debug();
+
+ if (!filename_is_valid(filename))
+ return sd_varlink_error_invalid_parameter_name(link, "name");
+
+ if (arg_runtime_scope != RUNTIME_SCOPE_USER) {
+ const char *details[] = {
+ "name", p.name,
+ NULL
+ };
+
+ r = varlink_verify_polkit_async(
+ link,
+ /* bus= */ NULL,
+ "io.systemd.storage.fs.acquire",
+ details,
+ polkit_registry);
+ if (r <= 0)
+ return r;
+ }
+
+ _cleanup_close_ int storage_fd = open_storage_dir();
+ if (storage_fd < 0)
+ return storage_fd;
+
+ _cleanup_close_ int pin_fd = -EBADF, real_fd = -EBADF;
+ r = chaseat(XAT_FDROOT, storage_fd, filename, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
+ if (r < 0) {
+ if (r != -ENOENT)
+ return r;
+ if (p.create_mode == CREATE_OPEN || p.read_only > 0)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.NoSuchVolume", NULL);
+
+ /* Doesn't exist yet: create it now */
+
+ if (p.request_as < 0) /* Make a choice: pick default type */
+ p.request_as = t < 0 ? VOLUME_DIR : volume_type_from_template(t);
+
+ /* Try to create the volume */
+ switch (p.request_as) {
+
+ case VOLUME_DIR: {
+
+ if (t < 0) /* Make a choice: pick default template */
+ t = TEMPLATE_SUBVOLUME;
+
+ real_fd = create_volume_dir(storage_fd, filename, t);
+ break;
+ }
+
+ case VOLUME_REG: {
+ if (p.create_size == UINT64_MAX)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.CreateSizeRequired", NULL);
+
+ if (t < 0) /* Make a choice: pick default template */
+ t = TEMPLATE_SPARSE_FILE;
+
+ real_fd = create_volume_reg(storage_fd, filename, t, p.create_size);
+ break;
+ }
+
+ case VOLUME_BLK:
+ /* We don't support creating block devices, we only support if they are symlinked
+ * into the storage directory. */
+ return sd_varlink_error(link, "io.systemd.StorageProvider.CreateNotSupported", NULL);
+
+ default:
+ assert_not_reached();
+ }
+
+ if (real_fd == -ENOMEDIUM)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.BadTemplate", NULL);
+ if (real_fd == -EEXIST) {
+ if (p.create_mode == CREATE_NEW)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.VolumeExists", NULL);
+
+ /* If we failed to open the volume and reached this point, then the volume already
+ * exists by now (i.e. we ran into a race). In that case, try to pin it a second time
+ * (but only once, let's never loop around this). */
+ r = chaseat(XAT_FDROOT, storage_fd, filename, CHASE_TRIGGER_AUTOFS, /* ret_path= */ NULL, &pin_fd);
+ if (r < 0)
+ return r;
+ } else if (real_fd < 0)
+ return real_fd;
+
+ } else if (p.create_mode == CREATE_NEW)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.VolumeExists", NULL);
+
+ /* At this point, we either already opened the real fd, or we managed to pin it (but not both) */
+ assert((real_fd >= 0) != (pin_fd >= 0));
+
+ /* Let's first settle the volume type */
+ struct stat st;
+ if (fstat(real_fd >= 0 ? real_fd : pin_fd, &st) < 0)
+ return -errno;
+
+ if (p.request_as == VOLUME_REG) {
+ /* First, check for the other supported types and generate a nice error */
+ if (IN_SET(st.st_mode & S_IFMT, S_IFDIR, S_IFBLK))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
+
+ /* Second verify cover all other types */
+ r = stat_verify_regular(&st);
+ if (r < 0)
+ return r;
+ } else if (p.request_as == VOLUME_DIR) {
+ if (IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFBLK))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
+
+ r = stat_verify_directory(&st);
+ if (r < 0)
+ return r;
+ } else if (p.request_as == VOLUME_BLK) {
+ if (IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFDIR))
+ return sd_varlink_error(link, "io.systemd.StorageProvider.WrongType", NULL);
+
+ r = stat_verify_block(&st);
+ if (r < 0)
+ return r;
+
+ } else if (S_ISREG(st.st_mode))
+ p.request_as = VOLUME_REG;
+ else if (S_ISDIR(st.st_mode))
+ p.request_as = VOLUME_DIR;
+ else if (S_ISBLK(st.st_mode))
+ p.request_as = VOLUME_BLK;
+ else
+ return log_debug_errno(SYNTHETIC_ERRNO(EBADF), "Unexpected inode type, refusing.");
+
+ /* Let's now acquire a real fd for the pinned fd, if we still need to */
+ if (real_fd < 0) {
+ assert(pin_fd >= 0);
+
+ XOpenFlags xopen_flags =
+ (p.read_only < 0 && !S_ISDIR(st.st_mode) ? XO_AUTO_RW_RO : 0);
+ int open_flags =
+ (p.read_only < 0 ? 0 : (p.read_only > 0 || S_ISDIR(st.st_mode) ? O_RDONLY : O_RDWR));
+
+ const char *subdir = NULL;
+ if (p.request_as == VOLUME_DIR) {
+ /* We place the root of the directory tree one level down, to separate ownership of
+ * the inode: the upper inode is owned by the host, the lower one by the volume. This
+ * matters so that the host one can be owned by the host's root, and the volume one
+ * by the foreign UID range. */
+ subdir = "root";
+ open_flags |= O_DIRECTORY|O_NOFOLLOW;
+ }
+
+ real_fd = xopenat_full(pin_fd, subdir, open_flags|O_CLOEXEC, xopen_flags, /* mode= */ MODE_INVALID);
+ if (real_fd < 0)
+ return log_debug_errno(real_fd, "Failed to reopen volume fd for '%s': %m", filename);
+
+ /* In directory mode we might be looking at a different inode node, refresh the stat data */
+ if (p.request_as == VOLUME_DIR && fstat(real_fd, &st) < 0)
+ return -errno;
+ }
+
+ assert(real_fd >= 0);
+
+ bool ro;
+ switch (p.request_as) {
+
+ case VOLUME_REG:
+ case VOLUME_BLK: {
+ assert(IN_SET(st.st_mode & S_IFMT, S_IFREG, S_IFBLK));
+
+ int open_flags = fcntl(real_fd, F_GETFL, 0);
+ if (open_flags < 0)
+ return -errno;
+
+ ro = (open_flags & O_ACCMODE_STRICT) == O_RDONLY;
+ break;
+ }
+
+ case VOLUME_DIR: {
+ assert(S_ISDIR(st.st_mode));
+
+ if (!uid_is_foreign(st.st_uid) ||
+ !gid_is_foreign(st.st_gid))
+ return log_debug_errno(SYNTHETIC_ERRNO(EPERM), "Storage directory not owned by foreign UID/GID range.");
+
+ /* Let's now generate a new mount for the directory tree, where propagation is disabled, and the
+ * flags are all set to good defaults */
+ _cleanup_close_ int mount_fd = open_tree_attr_with_fallback(
+ real_fd,
+ /* path= */ NULL,
+ OPEN_TREE_CLONE|OPEN_TREE_CLOEXEC|AT_SYMLINK_NOFOLLOW,
+ &(struct mount_attr) {
+ .attr_set = (p.read_only > 0 ? MOUNT_ATTR_RDONLY : 0),
+ .attr_clr = MOUNT_ATTR_NOSUID|MOUNT_ATTR_NOEXEC|MOUNT_ATTR_NODEV,
+ .propagation = MS_PRIVATE,
+ });
+ if (mount_fd < 0)
+ return log_debug_errno(mount_fd, "Failed to generate per-volume mount: %m");
+
+ /* Let's turn on propagation again now that it is disconnected, simply because MS_SHARED is
+ * generally the default for everything we return. */
+
+ if (mount_setattr(mount_fd, "", AT_EMPTY_PATH|AT_SYMLINK_NOFOLLOW,
+ &(struct mount_attr) {
+ .propagation = MS_SHARED,
+ }, MOUNT_ATTR_SIZE_VER0) < 0)
+ return log_debug_errno(errno, "Failed to enable propagation on per-volume mount: %m");
+
+ close_and_replace(real_fd, mount_fd);
+
+ r = fd_is_read_only_fs(real_fd);
+ if (r < 0)
+ return r;
+
+ ro = r > 0;
+ break;
+ }
+
+ default:
+ assert_not_reached();
+ }
+
+ if (p.read_only == 0 && ro)
+ return sd_varlink_error(link, "io.systemd.StorageProvider.ReadOnlyVolume", NULL);
+
+ int idx = sd_varlink_push_fd(link, real_fd);
+ if (idx < 0)
+ return idx;
+
+ TAKE_FD(real_fd);
+
+ return sd_varlink_replybo(
+ link,
+ SD_JSON_BUILD_PAIR_INTEGER("fileDescriptorIndex", idx),
+ SD_JSON_BUILD_PAIR_STRING("type", inode_type_to_string(st.st_mode)),
+ SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", ro),
+ SD_JSON_BUILD_PAIR_CONDITION(p.request_as == VOLUME_DIR, "baseUID", SD_JSON_BUILD_INTEGER(FOREIGN_UID_BASE)),
+ SD_JSON_BUILD_PAIR_CONDITION(p.request_as == VOLUME_DIR, "baseGID", SD_JSON_BUILD_INTEGER(FOREIGN_UID_BASE)));
+}
+
+static int vl_server(void) {
+ int r;
+
+ _cleanup_(hashmap_freep) Hashmap *polkit_registry = NULL;
+ _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *varlink_server = NULL;
+ r = varlink_server_new(
+ &varlink_server,
+ SD_VARLINK_SERVER_HANDLE_SIGINT|
+ SD_VARLINK_SERVER_HANDLE_SIGTERM|
+ SD_VARLINK_SERVER_ALLOW_FD_PASSING_OUTPUT|
+ SD_VARLINK_SERVER_INHERIT_USERDATA,
+ &polkit_registry);
+ if (r < 0)
+ return log_error_errno(r, "Failed to allocate Varlink server: %m");
+
+ r = sd_varlink_server_add_interface(varlink_server, &vl_interface_io_systemd_StorageProvider);
+ if (r < 0)
+ return log_error_errno(r, "Failed to add Varlink interface: %m");
+
+ r = sd_varlink_server_bind_method_many(
+ varlink_server,
+ "io.systemd.StorageProvider.Acquire", vl_method_acquire,
+ "io.systemd.StorageProvider.ListVolumes", vl_method_list_volumes,
+ "io.systemd.StorageProvider.ListTemplates", vl_method_list_templates);
+ if (r < 0)
+ return log_error_errno(r, "Failed to bind Varlink methods: %m");
+
+ r = sd_varlink_server_loop_auto(varlink_server);
+ if (r < 0)
+ return log_error_errno(r, "Failed to run Varlink event loop: %m");
+
+ return 0;
+}
+
+static int help(void) {
+ int r;
+
+ help_cmdline("[OPTIONS...]");
+ help_abstract("Simple file system backed storage provider");
+
+ _cleanup_(table_unrefp) Table *options = NULL;
+ r = option_parser_get_help_table(&options);
+ if (r < 0)
+ return r;
+
+ help_section("Options:");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("systemd-storage-fs", "8");
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv };
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION_COMMON_VERSION:
+ return version();
+
+ OPTION_LONG("system", NULL, "Operate in system mode"):
+ arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+ break;
+
+ OPTION_LONG("user", NULL, "Operate in user mode"):
+ arg_runtime_scope = RUNTIME_SCOPE_USER;
+ break;
+ }
+
+ if (option_parser_get_n_args(&opts) > 0)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "This program takes no arguments.");
+
+ return 1;
+}
+
+static int run(int argc, char* argv[]) {
+ int r;
+
+ log_setup();
+
+ r = parse_argv(argc, argv);
+ if (r <= 0)
+ return r;
+
+ return vl_server();
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/storage/storage-util.c b/src/storage/storage-util.c
new file mode 100644
index 0000000000000..793946c03a63e
--- /dev/null
+++ b/src/storage/storage-util.c
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "json-util.h"
+#include "string-table.h"
+#include "storage-util.h"
+
+static const char *volume_type_table[_VOLUME_TYPE_MAX] = {
+ [VOLUME_BLK] = "blk",
+ [VOLUME_REG] = "reg",
+ [VOLUME_DIR] = "dir",
+};
+
+static const char *create_mode_table[_CREATE_MODE_MAX] = {
+ [CREATE_ANY] = "any",
+ [CREATE_NEW] = "new",
+ [CREATE_OPEN] = "open",
+};
+
+DEFINE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
+DEFINE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
+
+JSON_DISPATCH_ENUM_DEFINE(json_dispatch_volume_type, VolumeType, volume_type_from_string);
+JSON_DISPATCH_ENUM_DEFINE(json_dispatch_create_mode, CreateMode, create_mode_from_string);
diff --git a/src/storage/storage-util.h b/src/storage/storage-util.h
new file mode 100644
index 0000000000000..f7a62aeec0835
--- /dev/null
+++ b/src/storage/storage-util.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-json.h"
+
+#include "string-table-fundamental.h"
+#include "string-util.h"
+
+/* This closely follows the kernel's inode type naming, i.e. is supposed to be a subset of what
+ * inode_type_from_string() parses. */
+typedef enum VolumeType {
+ VOLUME_BLK,
+ VOLUME_REG,
+ VOLUME_DIR,
+ _VOLUME_TYPE_MAX,
+ _VOLUME_TYPE_INVALID = -EINVAL,
+} VolumeType;
+
+typedef enum CreateMode {
+ CREATE_ANY,
+ CREATE_NEW,
+ CREATE_OPEN,
+ _CREATE_MODE_MAX,
+ _CREATE_MODE_INVALID = -EINVAL,
+} CreateMode;
+
+DECLARE_STRING_TABLE_LOOKUP(volume_type, VolumeType);
+DECLARE_STRING_TABLE_LOOKUP(create_mode, CreateMode);
+
+int json_dispatch_volume_type(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
+int json_dispatch_create_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
+
+static inline bool storage_volume_name_is_valid(const char *n) {
+ return string_is_safe(n, /* flags= */ 0);
+}
+
+static inline bool storage_template_name_is_valid(const char *n) {
+ return string_is_safe(n, /* flags= */ 0);
+}
+
+static inline bool storage_provider_name_is_valid(const char *n) {
+ return string_is_safe(n, STRING_FILENAME);
+}
diff --git a/src/storage/storagectl.c b/src/storage/storagectl.c
new file mode 100644
index 0000000000000..2bc7b7c2a3e40
--- /dev/null
+++ b/src/storage/storagectl.c
@@ -0,0 +1,812 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-varlink.h"
+
+#include
+#include
+#include
+#include
+
+#include "alloc-util.h"
+#include "ansi-color.h"
+#include "argv-util.h"
+#include "build.h"
+#include "bus-util.h"
+#include "errno-list.h"
+#include "escape.h"
+#include "extract-word.h"
+#include "fd-util.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "help-util.h"
+#include "json-util.h"
+#include "main-func.h"
+#include "mount-util.h"
+#include "namespace-util.h"
+#include "options.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "path-lookup.h"
+#include "path-util.h"
+#include "polkit-agent.h"
+#include "recurse-dir.h"
+#include "runtime-scope.h"
+#include "socket-util.h"
+#include "stat-util.h"
+#include "stdio-util.h"
+#include "storage-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "user-util.h"
+#include "varlink-util.h"
+#include "verbs.h"
+
+static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
+static PagerFlags arg_pager_flags = 0;
+static bool arg_legend = true;
+static bool arg_ask_password = true;
+static RuntimeScope arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+
+static int help(void) {
+ int r;
+
+ help_cmdline("[OPTIONS...] COMMAND");
+ help_abstract("Enumerate storage volumes and providers.");
+
+ _cleanup_(table_unrefp) Table *verbs = NULL;
+ r = verbs_get_help_table(&verbs);
+ if (r < 0)
+ return r;
+
+ _cleanup_(table_unrefp) Table *options = NULL;
+ r = option_parser_get_help_table(&options);
+ if (r < 0)
+ return r;
+
+ (void) table_sync_column_widths(0, verbs, options);
+
+ help_section("Commands:");
+
+ r = table_print_or_warn(verbs);
+ if (r < 0)
+ return r;
+
+ help_section("Options:");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("storagectl", "1");
+ return 0;
+}
+
+VERB_COMMON_HELP_HIDDEN(help);
+
+static const char *ro_color(int ro) {
+ if (ro > 0)
+ return ansi_highlight_red();
+ if (ro == 0)
+ return ansi_highlight_green();
+
+ return NULL;
+}
+
+static int on_list_reply(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ const char *error_id,
+ sd_varlink_reply_flags_t flags,
+ void* userdata) {
+
+ Table *t = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+
+ const char *d = ASSERT_PTR(sd_varlink_get_description(link));
+
+ if (error_id) {
+ log_debug("%s: Received error '%s', ignoring.", d, error_id);
+ return 0;
+ }
+
+ _cleanup_free_ char *provider = NULL;
+ r = path_extract_filename(d, &provider);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract provider name from socket path: %m");
+
+ struct {
+ const char *name;
+ const char *type;
+ int read_only;
+ uint64_t size_bytes;
+ uint64_t used_bytes;
+ } p = {
+ .read_only = -1,
+ .size_bytes = UINT64_MAX,
+ .used_bytes = UINT64_MAX,
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
+ { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
+ { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
+ { "sizeBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, size_bytes), 0 },
+ { "usedBytes", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, voffsetof(p, used_bytes), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode List() reply: %m");
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, provider,
+ TABLE_STRING, p.name,
+ TABLE_STRING, p.type,
+ TABLE_TRISTATE, p.read_only,
+ TABLE_SET_COLOR, ro_color(p.read_only));
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (p.size_bytes == UINT64_MAX)
+ r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
+ else
+ r = table_add_many(t, TABLE_SIZE, p.size_bytes, TABLE_SET_ALIGN_PERCENT, 100);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ if (p.used_bytes == UINT64_MAX)
+ r = table_add_many(t, TABLE_EMPTY, TABLE_SET_ALIGN_PERCENT, 100);
+ else
+ r = table_add_many(t, TABLE_SIZE, p.used_bytes, TABLE_SET_ALIGN_PERCENT, 100);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+VERB(verb_list_volumes, "volumes", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, VERB_DEFAULT, "List storage volumes");
+static int verb_list_volumes(int argc, char *argv[], uintptr_t data, void *userdata) {
+ int r;
+
+ assert(argc <= 2);
+
+ _cleanup_free_ char *socket_path = NULL;
+ r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine socket directory: %m");
+
+ _cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type", "ro", "size", "used");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_sort(t, (size_t) 0, (size_t) 1);
+ table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ if (argc >= 2) {
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
+ if (r < 0)
+ return log_oom();
+ }
+
+ ssize_t n = varlink_execute_directory(
+ socket_path,
+ "io.systemd.StorageProvider.ListVolumes",
+ v,
+ /* more= */ true,
+ /* timeout_usec= */ 0, /* 0 means default */
+ on_list_reply,
+ t);
+ if (n < 0 && n != -ENOENT)
+ return log_error_errno(n, "Failed to enumerate storage volumes: %m");
+
+ if (!table_isempty(t)) {
+ r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+ if (table_isempty(t))
+ printf("No storage volumes.\n");
+ else
+ printf("\n%zu storage volumes listed.\n", table_get_rows(t) - 1);
+ }
+
+ return 0;
+}
+
+static int on_list_templates_reply(
+ sd_varlink *link,
+ sd_json_variant *parameters,
+ const char *error_id,
+ sd_varlink_reply_flags_t flags,
+ void* userdata) {
+
+ Table *t = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+
+ const char *d = ASSERT_PTR(sd_varlink_get_description(link));
+
+ if (error_id) {
+ log_debug("%s: Received error '%s', ignoring.", d, error_id);
+ return 0;
+ }
+
+ _cleanup_free_ char *provider = NULL;
+ r = path_extract_filename(d, &provider);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract provider name from socket path: %m");
+
+ struct {
+ const char *name;
+ const char *type;
+ } p = {
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, name), 0 },
+ { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode ListTemplates() reply: %m");
+
+ r = table_add_many(
+ t,
+ TABLE_STRING, provider,
+ TABLE_STRING, p.name,
+ TABLE_STRING, p.type);
+ if (r < 0)
+ return table_log_add_error(r);
+
+ return 0;
+}
+
+VERB(verb_templates, "templates", "GLOB", /* min_args= */ VERB_ANY, /* max_args= */ 2, /* flags= */ 0, "List storage volume templates");
+static int verb_templates(int argc, char *argv[], uintptr_t data, void *userdata) {
+ int r;
+
+ assert(argc <= 2);
+
+ _cleanup_free_ char *socket_path = NULL;
+ r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine socket directory: %m");
+
+ _cleanup_(table_unrefp) Table *t = table_new("provider", "name", "type");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_sort(t, (size_t) 0, (size_t) 1);
+ table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+ if (argc >= 2) {
+ r = sd_json_buildo(
+ &v,
+ SD_JSON_BUILD_PAIR_STRING("matchName", argv[1]));
+ if (r < 0)
+ return log_oom();
+ }
+
+ ssize_t n = varlink_execute_directory(
+ socket_path,
+ "io.systemd.StorageProvider.ListTemplates",
+ v,
+ /* more= */ true,
+ /* timeout_usec= */ 0, /* 0 means default */
+ on_list_templates_reply,
+ t);
+ if (n < 0 && n != -ENOENT)
+ return log_error_errno(n, "Failed to enumerate storage volume templates: %m");
+
+ if (!table_isempty(t)) {
+ r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+ if (table_isempty(t))
+ printf("No templates.\n");
+ else
+ printf("\n%zu templates listed.\n", table_get_rows(t) - 1);
+ }
+
+ return 0;
+}
+
+VERB_NOARG(verb_providers, "providers", "List storage providers");
+static int verb_providers(int argc, char *argv[], uintptr_t data, void *userdata) {
+ int r;
+
+ _cleanup_free_ char *socket_path = NULL;
+ r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine socket directory: %m");
+
+ _cleanup_(table_unrefp) Table *t = table_new("provider", "listening");
+ if (!t)
+ return log_oom();
+
+ (void) table_set_sort(t, (size_t) 0);
+ table_set_ersatz_string(t, TABLE_ERSATZ_DASH);
+
+ _cleanup_close_ int fd = open(socket_path, O_RDONLY|O_CLOEXEC|O_DIRECTORY);
+ if (fd < 0) {
+ if (errno != ENOENT)
+ return log_error_errno(errno, "Failed to open '%s': %m", socket_path);
+ } else {
+ _cleanup_free_ DirectoryEntries *dentries = NULL;
+ r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &dentries);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enumerate '%s': %m", socket_path);
+
+ FOREACH_ARRAY(dp, dentries->entries, dentries->n_entries) {
+ struct dirent *de = *dp;
+
+ if (de->d_type != DT_SOCK)
+ continue;
+
+ if (!storage_provider_name_is_valid(de->d_name))
+ continue;
+
+ _cleanup_close_ int socket_fd = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+ if (socket_fd < 0)
+ return log_error_errno(errno, "Failed to allocate AF_UNIX/SOCK_STREAM socket: %m");
+
+ _cleanup_free_ char *no = NULL;
+ r = connect_unix_path(socket_fd, fd, de->d_name);
+ if (r < 0) {
+ no = strjoin("no (", ERRNO_NAME(r), ")");
+ if (!no)
+ return log_oom();
+ }
+
+ r = table_add_many(t,
+ TABLE_STRING, de->d_name,
+ TABLE_STRING, no ?: "yes",
+ TABLE_SET_COLOR, ansi_highlight_green_red(!no));
+ if (r < 0)
+ return table_log_add_error(r);
+ }
+ }
+
+ if (!table_isempty(t)) {
+ r = table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, arg_legend);
+ if (r < 0)
+ return r;
+ }
+
+ if (arg_legend && FLAGS_SET(arg_json_format_flags, SD_JSON_FORMAT_OFF)) {
+ if (table_isempty(t))
+ printf("No providers.\n");
+ else
+ printf("\n%zu providers listed.\n", table_get_rows(t) - 1);
+ }
+
+ return 0;
+}
+
+static int parse_argv(int argc, char *argv[], char ***args) {
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv };
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION_COMMON_VERSION:
+ return version();
+
+ OPTION_COMMON_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ OPTION_COMMON_NO_LEGEND:
+ arg_legend = false;
+ break;
+
+ OPTION_COMMON_JSON:
+ r = parse_json_argument(opts.arg, &arg_json_format_flags);
+ if (r <= 0)
+ return r;
+ break;
+
+ OPTION_COMMON_NO_ASK_PASSWORD:
+ arg_ask_password = false;
+ break;
+
+ OPTION_LONG("system", NULL, "Operate in system mode"):
+ arg_runtime_scope = RUNTIME_SCOPE_SYSTEM;
+ break;
+
+ OPTION_LONG("user", NULL, "Operate in user mode"):
+ arg_runtime_scope = RUNTIME_SCOPE_USER;
+ break;
+ }
+
+ *args = option_parser_get_args(&opts);
+ return 1;
+}
+
+static int run_as_mount_helper(int argc, char *argv[]) {
+ int c, r;
+
+ /* Implements util-linux "external helper" command line interface, as per mount(8) man page.
+ *
+ * Usage:
+ *
+ * mount -t storage fs:mydirvolume /some/place # Directory volumes
+ * mount -t storage.ext4 fs:myblkvolume /some/place # Block volumes
+ */
+
+ const char *fstype = NULL, *options = NULL;
+ bool fake = false;
+
+ while ((c = getopt(argc, argv, "sfnvN:o:t:")) >= 0) {
+ switch (c) {
+
+ case 'f':
+ fake = true;
+ break;
+
+ case 'o':
+ options = optarg;
+ break;
+
+ case 't':
+ fstype = startswith(optarg, "storage.");
+ if (fstype) {
+ /* Paranoia: don't allow "storage.storage.storage.…" chains... */
+ if (startswith(fstype, "storage.") || streq(fstype, "storage"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Refusing nested storage volumes.");
+ } else if (!streq(optarg, "storage"))
+ log_warning("Unexpected file system type '%s', ignoring.", optarg);
+
+ break;
+
+ case 's': /* sloppy mount options */
+ case 'n': /* aka --no-mtab */
+ case 'v': /* aka --verbose */
+ log_debug("Ignoring option -%c, not implemented.", c);
+ break;
+
+ case 'N': /* aka --namespace= */
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Option -%c is not implemented, refusing.", c);
+
+ case '?':
+ return -EINVAL;
+ }
+ }
+
+ if (optind + 2 != argc)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Expected a storage volume specification and target directory as only arguments.");
+
+ const char *colon = strchr(argv[optind], ':');
+ if (!colon)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume specification, refusing: %s", argv[optind]);
+
+ _cleanup_free_ char *provider = strndup(argv[optind], colon - argv[optind]);
+ if (!provider)
+ return log_oom();
+ if (!storage_provider_name_is_valid(provider))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage provider name: %s", provider);
+
+ _cleanup_free_ char *name = strdup(colon + 1);
+ if (!name)
+ return log_oom();
+ if (!storage_volume_name_is_valid(name))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid storage volume name: %s", name);
+
+ _cleanup_free_ char *path = NULL;
+ r = parse_path_argument(argv[optind+1], /* suppress_root= */ false, &path);
+ if (r < 0)
+ return r;
+
+ _cleanup_free_ char *filtered = NULL, *template = NULL;
+ CreateMode create_mode = _CREATE_MODE_INVALID;
+ uint64_t create_size = UINT64_MAX;
+ int read_only = -1;
+ for (const char *p = options;;) {
+ _cleanup_free_ char *word = NULL;
+
+ r = extract_first_word(&p, &word, ",", EXTRACT_KEEP_QUOTE|EXTRACT_UNESCAPE_SEPARATORS);
+ if (r < 0)
+ return log_error_errno(r, "Failed to extract mount option: %m");
+ if (r == 0)
+ break;
+
+ const char *t = startswith(word, "storage.");
+ if (t) {
+ const char *v;
+ if ((v = startswith(t, "create="))) {
+ create_mode = create_mode_from_string(v);
+ if (create_mode < 0)
+ return log_error_errno(create_mode, "Failed to parse storage.create= parameter: %s", v);
+ } else if ((v = startswith(t, "create-size="))) {
+ r = parse_size(v, /* base= */ 1024, &create_size);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse storage.create-size= parameter: %s", v);
+ } else if ((v = startswith(t, "template="))) {
+ if (!storage_template_name_is_valid(v))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid template name, refusing: %s", v);
+
+ r = free_and_strdup(&template, v);
+ if (r < 0)
+ return log_oom();
+ } else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown mount option '%s', refusing.", word);
+ } else if (streq(word, "ro"))
+ read_only = true;
+ else if (streq(word, "rw"))
+ read_only = false;
+ else if (!strextend_with_separator(&filtered, ",", word))
+ return log_oom();
+ }
+
+ if (fake)
+ return 0;
+
+ _cleanup_free_ char *socket_path = NULL;
+ r = runtime_directory_generic(arg_runtime_scope, "systemd/io.systemd.StorageProvider", &socket_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to determine socket directory: %m");
+
+ if (!path_extend(&socket_path, provider))
+ return log_oom();
+
+ _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
+ r = sd_varlink_connect_address(&link, socket_path);
+ if (r < 0)
+ return log_error_errno(r, "Failed to connect to '%s': %m", socket_path);
+
+ r = sd_varlink_set_allow_fd_passing_input(link, true);
+ if (r < 0)
+ return log_error_errno(r, "Failed to enable file descriptor passing: %m");
+
+ (void) polkit_agent_open_if_enabled(BUS_TRANSPORT_LOCAL, arg_ask_password);
+
+ sd_json_variant *mreply = NULL;
+ const char *merror_id = NULL, *vtype = fstype ? "reg" : "dir";
+ r = sd_varlink_callbo(
+ link,
+ "io.systemd.StorageProvider.Acquire",
+ &mreply,
+ &merror_id,
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
+ SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
+ SD_JSON_BUILD_PAIR_STRING("requestAs", vtype),
+ SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *reply = sd_json_variant_ref(mreply);
+ if (merror_id) {
+ /* Copy out the error ID, as the follow-up call will invalidate it */
+ _cleanup_free_ char *error_id = strdup(merror_id);
+ if (!error_id)
+ return log_oom();
+
+ /* Hmm, the type might not have been right for the backend or the volume? then try
+ * again, and switch from "reg" to "blk", maybe it works then. (We keep the original
+ * reply referenced, since we prefer generating an error for the first error.) */
+ if (streq(vtype, "reg") && STR_IN_SET(error_id,
+ "io.systemd.StorageProvider.TypeNotSupported",
+ "io.systemd.StorageProvider.WrongType")) {
+
+ sd_json_variant *freply = NULL;
+ const char *ferror_id = NULL;
+ r = sd_varlink_callbo(
+ link,
+ "io.systemd.StorageProvider.Acquire",
+ &freply,
+ &ferror_id,
+ SD_JSON_BUILD_PAIR_STRING("name", name),
+ SD_JSON_BUILD_PAIR_CONDITION(create_mode >= 0, "createMode", SD_JSON_BUILD_STRING(create_mode_to_string(create_mode))),
+ JSON_BUILD_PAIR_STRING_NON_EMPTY("template", template),
+ SD_JSON_BUILD_PAIR_CONDITION(read_only >= 0, "readOnly", SD_JSON_BUILD_BOOLEAN(read_only)),
+ SD_JSON_BUILD_PAIR_STRING("requestAs", "blk"),
+ SD_JSON_BUILD_PAIR_CONDITION(create_size != UINT64_MAX, "createSizeBytes", SD_JSON_BUILD_UNSIGNED(create_size)),
+ SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", arg_ask_password));
+ if (r < 0)
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+ if (!ferror_id) {
+ /* The 2nd call worked? then let's forget about the first failure */
+ sd_json_variant_unref(reply);
+ reply = sd_json_variant_ref(freply);
+ error_id = mfree(error_id);
+ }
+
+ /* NB: if both fail we show the Varlink error of the first call here, i.e. of the preferred type */
+ }
+
+ if (error_id) {
+ if (streq(error_id, "io.systemd.StorageProvider.NoSuchVolume"))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Volume '%s' not known.", name);
+ if (streq(error_id, "io.systemd.StorageProvider.NoSuchTemplate"))
+ return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Template '%s' not known.", template);
+ if (streq(error_id, "io.systemd.StorageProvider.VolumeExists"))
+ return log_error_errno(SYNTHETIC_ERRNO(EEXIST), "Volume '%s' exists already.", name);
+ if (streq(error_id, "io.systemd.StorageProvider.TypeNotSupported"))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support the specified volume type '%s'.", vtype);
+ if (streq(error_id, "io.systemd.StorageProvider.WrongType"))
+ return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Volume '%s' is not of type '%s'.", name, vtype);
+ if (streq(error_id, "io.systemd.StorageProvider.CreateNotSupported"))
+ return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Storage provider does not support creating volumes.");
+ if (streq(error_id, "io.systemd.StorageProvider.CreateSizeRequired"))
+ return log_error_errno(SYNTHETIC_ERRNO(ENODATA), "Storage provider requires a create size to be provided when creating volumes on-the-fly. Use 'storage.create-size=' mount option.");
+ if (streq(error_id, "io.systemd.StorageProvider.ReadOnlyVolume"))
+ return log_error_errno(SYNTHETIC_ERRNO(EROFS), "Volume '%s' is read-only.", name);
+ if (streq(error_id, "io.systemd.StorageProvider.BadTemplate"))
+ return log_error_errno(SYNTHETIC_ERRNO(EADDRNOTAVAIL), "Template does not apply to this volume type.");
+
+ r = sd_varlink_error_to_errno(error_id, reply); /* If this is a system errno style error, output it with %m */
+ if (r != -EBADR)
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %m");
+
+ return log_error_errno(r, "Failed to issue io.systemd.StorageProvider.Acquire() varlink call: %s", error_id);
+ }
+ }
+
+ struct {
+ unsigned fd_idx;
+ int read_only;
+ const char *type;
+ uid_t base_uid;
+ gid_t base_gid;
+ } p = {
+ .fd_idx = UINT_MAX,
+ .read_only = -1,
+ .base_uid = UID_INVALID,
+ .base_gid = GID_INVALID,
+ };
+
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "fileDescriptorIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint, voffsetof(p, fd_idx), SD_JSON_MANDATORY },
+ { "readOnly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_tristate, voffsetof(p, read_only), 0 },
+ { "type", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, voffsetof(p, type), SD_JSON_MANDATORY },
+ { "baseUID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_uid), 0 },
+ { "baseGID", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uid_gid, voffsetof(p, base_gid), 0 },
+ {}
+ };
+
+ r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p);
+ if (r < 0)
+ return log_error_errno(r, "Failed to decode Acquire() reply: %m");
+
+ _cleanup_close_ int fd = sd_varlink_take_fd(link, p.fd_idx);
+ if (fd < 0)
+ return log_error_errno(fd, "Failed to acquire fd from Varlink connection: %m");
+
+ struct stat st;
+ if (fstat(fd, &st) < 0)
+ return log_error_errno(errno, "Failed to stat returned file descriptor: %m");
+
+ _cleanup_strv_free_ char **cmdline = strv_new("mount", "-c");
+ if (!cmdline)
+ return log_oom();
+
+ if (fstype) {
+ if (!STR_IN_SET(p.type, "reg", "blk"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mounting as file system type '%s' requested, but volume is not a block device or regular file.", fstype);
+
+ r = stat_verify_regular_or_block(&st);
+ if (r < 0)
+ return log_error_errno(r, "File descriptor for block/regular volume is not a block or regular inode: %m");
+
+ if (strv_extend_strv(&cmdline, STRV_MAKE("-t", fstype), /* filter_duplicates= */ false) < 0)
+ return log_oom();
+ } else {
+ if (!streq(p.type, "dir"))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Mount as directory requested, but volume is not a directory.");
+
+ if (!uid_is_valid(p.base_uid) || !gid_is_valid(p.base_gid))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Provider did not report base UID/GID, cannot mount.");
+
+ if (p.base_uid > UINT32_MAX - 0x10000U ||
+ p.base_gid > UINT32_MAX - 0x10000U)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Returned base UID/GID out of range.");
+
+ r = stat_verify_directory(&st);
+ if (r < 0)
+ return log_error_errno(r, "File descriptor for directory volume is not a directory inode: %m");
+
+ if (st.st_uid < p.base_uid || st.st_uid >= p.base_uid + 0x10000 ||
+ st.st_gid < p.base_gid || st.st_gid >= p.base_gid + 0x10000)
+ return log_error_errno(SYNTHETIC_ERRNO(EPERM), "File descriptor for directory volume is not owned by base UID/GID range, refusing.");
+
+ /* Now move the mount into our own UID/GID range */
+ _cleanup_free_ char *uid_line = asprintf_safe(
+ UID_FMT " " UID_FMT " " UID_FMT "\n",
+ p.base_uid, (uid_t) 0, (uid_t) 0x10000);
+ _cleanup_free_ char *gid_line = asprintf_safe(
+ GID_FMT " " GID_FMT " " GID_FMT "\n",
+ p.base_gid, (gid_t) 0, (gid_t) 0x10000);
+ if (!uid_line || !gid_line)
+ return log_oom();
+
+ _cleanup_close_ int userns_fd = userns_acquire(uid_line, gid_line, /* setgroups_deny= */ true);
+ if (userns_fd < 0)
+ return log_error_errno(userns_fd, "Failed to acquire new user namespace: %m");
+
+ _cleanup_close_ int remapped_fd = open_tree_attr_with_fallback(
+ fd,
+ /* path= */ NULL,
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC,
+ &(struct mount_attr) {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ .userns_fd = userns_fd,
+ });
+ if (remapped_fd < 0)
+ return log_error_errno(remapped_fd, "Failed to set ID mapping on returned mount: %m");
+
+ close_and_replace(fd, remapped_fd);
+
+ if (strv_extend(&cmdline, "--bind") < 0)
+ return log_oom();
+ }
+
+ if (p.read_only > 0)
+ read_only = true;
+
+ if (!strextend_with_separator(&filtered, ",", read_only > 0 ? "ro" : "rw"))
+ return log_oom();
+
+ if (strv_extend_strv(&cmdline, STRV_MAKE("-o", filtered), /* filter_duplicates= */ false) < 0)
+ return log_oom();
+
+ if (strv_extend_strv(&cmdline, STRV_MAKE(FORMAT_PROC_FD_PATH(fd), path), /* filter_duplicates= */ false) < 0)
+ return log_oom();
+
+ r = fd_cloexec(fd, false);
+ if (r < 0)
+ return log_error_errno(r, "Failed to disable O_CLOEXEC for mount fd: %m");
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *q = quote_command_line(cmdline, SHELL_ESCAPE_EMPTY);
+ log_debug("Chain-loading: %s", strna(q));
+ }
+
+ /* NB: we do not honour $PATH here, since as plugin to /bin/mount we might be called in a setuid()
+ * context, and hence don't want to chain to programs potentially under user control. */
+ execv("/bin/mount", cmdline);
+ return log_error_errno(errno, "Failed to execute mount tool: %m");
+}
+
+static int run(int argc, char *argv[]) {
+ int r;
+
+ log_setup();
+
+ if (invoked_as(argv, "mount.storage"))
+ return run_as_mount_helper(argc, argv);
+
+ char **args = NULL;
+ r = parse_argv(argc, argv, &args);
+ if (r <= 0)
+ return r;
+
+ return dispatch_verb_with_args(args, /* userdata= */ NULL);
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/storagetm/storagetm.c b/src/storagetm/storagetm.c
index 5129887d795fe..384e88f7b88bc 100644
--- a/src/storagetm/storagetm.c
+++ b/src/storagetm/storagetm.c
@@ -80,7 +80,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/sysctl/sysctl.c b/src/sysctl/sysctl.c
index 6a9e33e6e6f7b..e124b56fc9c12 100644
--- a/src/sysctl/sysctl.c
+++ b/src/sysctl/sysctl.c
@@ -362,7 +362,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/systemd/_sd-common.h b/src/systemd/_sd-common.h
index f9c9a2627d55c..8da080bf18e61 100644
--- a/src/systemd/_sd-common.h
+++ b/src/systemd/_sd-common.h
@@ -69,6 +69,20 @@ typedef void (*_sd_destroy_t)(void *userdata);
# define _SD_STRINGIFY(x) _SD_XSTRINGIFY(x)
#endif
+/* Mirror of CONCATENATE / UNIQ from macro-fundamental.h, available to public sd-* headers. */
+#ifndef _SD_CONCATENATE
+# define _SD_XCONCATENATE(x, y) x ## y
+# define _SD_CONCATENATE(x, y) _SD_XCONCATENATE(x, y)
+#endif
+
+#ifndef _SD_UNIQ
+# ifdef __COUNTER__
+# define _SD_UNIQ __COUNTER__
+# else
+# define _SD_UNIQ __LINE__
+# endif
+#endif
+
#ifndef _SD_BEGIN_DECLARATIONS
# ifdef __cplusplus
# define _SD_BEGIN_DECLARATIONS \
diff --git a/src/systemd/meson.build b/src/systemd/meson.build
index d7335cee558de..0d0e9ab68f174 100644
--- a/src/systemd/meson.build
+++ b/src/systemd/meson.build
@@ -37,6 +37,7 @@ _not_installed_headers = [
'sd-dhcp6-option.h',
'sd-dhcp6-protocol.h',
'sd-dns-resolver.h',
+ 'sd-future.h',
'sd-ipv4acd.h',
'sd-ipv4ll.h',
'sd-lldp-rx.h',
diff --git a/src/systemd/sd-bus-vtable.h b/src/systemd/sd-bus-vtable.h
index 5c11ca8ae5b71..036bda3fe47e9 100644
--- a/src/systemd/sd-bus-vtable.h
+++ b/src/systemd/sd-bus-vtable.h
@@ -44,6 +44,7 @@ __extension__ enum {
SD_BUS_VTABLE_PROPERTY_EXPLICIT = 1ULL << 7,
SD_BUS_VTABLE_SENSITIVE = 1ULL << 8, /* covers both directions: method call + reply */
SD_BUS_VTABLE_ABSOLUTE_OFFSET = 1ULL << 9,
+ /* Bit 10 is reserved for the private SD_BUS_VTABLE_METHOD_FIBER flag (see bus-internal.h). */
_SD_BUS_VTABLE_CAPABILITY_MASK = 0xFFFFULL << 40
};
diff --git a/src/systemd/sd-future.h b/src/systemd/sd-future.h
new file mode 100644
index 0000000000000..451a3c62858e8
--- /dev/null
+++ b/src/systemd/sd-future.h
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef foosdfuturefoo
+#define foosdfuturefoo
+
+/***
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ systemd is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with systemd; If not, see .
+***/
+
+#include
+
+#include "_sd-common.h"
+
+_SD_BEGIN_DECLARATIONS;
+
+struct iovec;
+struct pollfd;
+struct sockaddr;
+struct msghdr;
+
+typedef struct sd_event sd_event;
+typedef struct sd_future sd_future;
+typedef struct sd_future_ops sd_future_ops;
+typedef struct sd_promise sd_promise;
+typedef int (*sd_future_func_t)(sd_future *f);
+typedef int (*sd_fiber_func_t)(void *userdata);
+typedef void (*sd_fiber_destroy_t)(void *userdata);
+
+struct sd_future_ops {
+ void* (*free)(void *userdata);
+ int (*cancel)(void *userdata);
+ int (*set_priority)(void *userdata, int64_t priority);
+};
+
+enum {
+ SD_FUTURE_PENDING,
+ SD_FUTURE_RESOLVED
+};
+
+int sd_future_new(const sd_future_ops *ops, void *impl, sd_future **ret);
+int sd_future_cancel(sd_future *f);
+int sd_promise_resolve(sd_promise *p, int result);
+
+_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_future);
+_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_unref);
+void sd_future_unref_array_clear(sd_future **array, size_t n);
+void sd_future_unref_array(sd_future **array, size_t n);
+
+sd_future* sd_future_cancel_wait_unref(sd_future *f);
+_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_future, sd_future_cancel_wait_unref);
+void sd_future_cancel_wait_unref_array_clear(sd_future **array, size_t n);
+void sd_future_cancel_wait_unref_array(sd_future **array, size_t n);
+
+int sd_future_state(sd_future *f);
+int sd_future_result(sd_future *f);
+void* sd_future_get_userdata(sd_future *f);
+void* sd_future_get_impl(sd_future *f);
+const sd_future_ops* sd_future_get_ops(sd_future *f);
+
+int sd_future_set_callback(sd_future *f, sd_future_func_t callback, void *userdata);
+int sd_future_set_priority(sd_future *f, int64_t priority);
+
+int sd_future_new_wait(sd_future *target, sd_future **ret);
+
+int sd_fiber_new(sd_event *e, const char *name, sd_fiber_func_t func, void *userdata, sd_fiber_destroy_t destroy, sd_future **ret);
+
+int sd_fiber_set_floating(sd_future *f, int b);
+int sd_fiber_get_floating(sd_future *f);
+
+int sd_fiber_is_running(void);
+sd_future* sd_fiber_get_current(void);
+const char* sd_fiber_get_name(sd_future *f);
+int64_t sd_fiber_get_priority(void);
+sd_event* sd_fiber_get_event(void);
+
+int sd_fiber_yield(void);
+int sd_fiber_sleep(uint64_t usec);
+int sd_fiber_await(sd_future *target);
+int sd_fiber_suspend(void);
+int sd_fiber_resume(sd_future *f, int result);
+
+sd_future* sd_fiber_timeout(uint64_t timeout);
+
+#define SD_FIBER_TIMEOUT(timeout) _SD_FIBER_TIMEOUT(_SD_UNIQ, (timeout))
+#define _SD_FIBER_TIMEOUT(uniq, timeout) \
+ sd_future *_SD_CONCATENATE(_sd_fto_, uniq) __attribute__((cleanup(sd_future_cancel_wait_unrefp), unused)) = sd_fiber_timeout(timeout)
+
+#define SD_FIBER_WITH_TIMEOUT(timeout) _SD_FIBER_WITH_TIMEOUT(_SD_UNIQ, (timeout))
+#define _SD_FIBER_WITH_TIMEOUT(uniq, timeout) \
+ for (sd_future *_SD_CONCATENATE(_sd_fto_, uniq) __attribute__((cleanup(sd_future_cancel_wait_unrefp), unused)) = sd_fiber_timeout(timeout), \
+ *_SD_CONCATENATE(_sd_fto_b_, uniq) = (sd_future *) 1; \
+ _SD_CONCATENATE(_sd_fto_b_, uniq); \
+ _SD_CONCATENATE(_sd_fto_b_, uniq) = NULL)
+
+/* Fiber I/O operations - use sd-event for non-blocking I/O when in fiber context */
+ssize_t sd_fiber_read(int fd, void *buf, size_t count);
+ssize_t sd_fiber_write(int fd, const void *buf, size_t count);
+ssize_t sd_fiber_readv(int fd, const struct iovec *iov, int iovcnt);
+ssize_t sd_fiber_writev(int fd, const struct iovec *iov, int iovcnt);
+ssize_t sd_fiber_recv(int sockfd, void *buf, size_t len, int flags);
+ssize_t sd_fiber_send(int sockfd, const void *buf, size_t len, int flags);
+int sd_fiber_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
+ssize_t sd_fiber_recvmsg(int sockfd, struct msghdr *msg, int flags);
+ssize_t sd_fiber_sendmsg(int sockfd, const struct msghdr *msg, int flags);
+ssize_t sd_fiber_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
+ssize_t sd_fiber_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
+int sd_fiber_accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
+int sd_fiber_poll(struct pollfd *fds, size_t n_fds, uint64_t timeout);
+
+_SD_END_DECLARATIONS;
+
+#endif
diff --git a/src/sysupdate/sysupdate.c b/src/sysupdate/sysupdate.c
index 648dd093e6160..ff8829115148e 100644
--- a/src/sysupdate/sysupdate.c
+++ b/src/sysupdate/sysupdate.c
@@ -1865,7 +1865,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/sysupdate/updatectl.c b/src/sysupdate/updatectl.c
index 65d2c7675ed45..f09c18cd173bb 100644
--- a/src/sysupdate/updatectl.c
+++ b/src/sysupdate/updatectl.c
@@ -1689,7 +1689,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_LONG("reboot", NULL, "Reboot after updating to newer version"):
diff --git a/src/sysusers/sysusers.c b/src/sysusers/sysusers.c
index 38fe4f4515161..05a3e2db509e4 100644
--- a/src/sysusers/sysusers.c
+++ b/src/sysusers/sysusers.c
@@ -2103,7 +2103,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_CAT_CONFIG:
diff --git a/src/test/meson.build b/src/test/meson.build
index 6f9a24eb04483..09c367d3074f3 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -120,7 +120,6 @@ simple_tests += files(
'test-hmac.c',
'test-hostname-setup.c',
'test-hostname-util.c',
- 'test-id128.c',
'test-image-filter.c',
'test-image-policy.c',
'test-import-util.c',
@@ -180,8 +179,6 @@ simple_tests += files(
'test-replace-var.c',
'test-rlimit-util.c',
'test-rm-rf.c',
- 'test-sd-hwdb.c',
- 'test-sd-path.c',
'test-secure-bits.c',
'test-serialize.c',
'test-set.c',
@@ -346,10 +343,6 @@ executables += [
'sources' : files('test-ipcrm.c'),
'type' : 'unsafe',
},
- test_template + {
- 'sources' : files('test-json.c'),
- 'dependencies' : libm,
- },
test_template + {
'sources' : files('test-kexec.c'),
'link_with' : [libshared],
@@ -496,14 +489,6 @@ executables += [
'sources' : files('test-utmp.c'),
'conditions' : ['ENABLE_UTMP'],
},
- test_template + {
- 'sources' : files('test-varlink.c'),
- 'dependencies' : threads,
- },
- test_template + {
- 'sources' : files('test-varlink-idl.c'),
- 'dependencies' : threads,
- },
core_test_template + {
'sources' : files('test-varlink-idl-unit.c'),
},
diff --git a/src/test/test-chase-manual.c b/src/test/test-chase-manual.c
index daa8713f48009..410522ceb161f 100644
--- a/src/test/test-chase-manual.c
+++ b/src/test/test-chase-manual.c
@@ -39,7 +39,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/test/test-options.c b/src/test/test-options.c
index 04fddc1c34700..d00262fa34bb2 100644
--- a/src/test/test-options.c
+++ b/src/test/test-options.c
@@ -58,7 +58,12 @@ static void test_option_parse_one(
ASSERT_TRUE(strv_equal(args, remaining));
ASSERT_STREQ(argv[0], saved_argv0);
- ASSERT_EQ(option_parser_get_n_args(&opts), strv_length(remaining));
+ size_t l = strv_length(remaining);
+ ASSERT_EQ(option_parser_get_n_args(&opts), l);
+ ASSERT_STREQ(option_parser_get_arg(&opts, 0), l > 0 ? remaining[0] : NULL);
+ ASSERT_STREQ(option_parser_get_arg(&opts, 1), l > 1 ? remaining[1] : NULL);
+ ASSERT_STREQ(option_parser_get_arg(&opts, 2), l > 2 ? remaining[2] : NULL);
+ ASSERT_STREQ(option_parser_get_arg(&opts, 3), l > 3 ? remaining[3] : NULL);
}
static void test_option_invalid_one(
@@ -837,7 +842,10 @@ static void test_macros_parse_one(
OptionParser opts = { argc, argv, mode, namespace };
- FOREACH_OPTION(c, &opts, assert_not_reached()) {
+ FOREACH_OPTION(c, &opts) {
+
+ assert(c >= 0);
+
log_debug("%c %s: %s=%s",
opts.opt->short_code != 0 ? opts.opt->short_code : ' ',
opts.opt->long_code ?: "",
@@ -1331,7 +1339,7 @@ TEST(option_optional_arg_consume) {
/* --user without arg: next arg is positional (doesn't start with -).
* The option parser returns NULL for the arg. The caller would then
- * use option_parser_next_arg/consume_next_arg to grab it. */
+ * use option_parser_peek_next_arg/consume_next_arg to grab it. */
{
char **argv = STRV_MAKE("arg0", "--user", "someuser", "pos1");
int argc = strv_length(argv);
@@ -1341,7 +1349,7 @@ TEST(option_optional_arg_consume) {
ASSERT_OK_POSITIVE(option_parse(options, options + 3, &opts));
ASSERT_STREQ(opts.opt->long_code, "user");
ASSERT_NULL(opts.arg);
- ASSERT_STREQ(option_parser_next_arg(&opts), "someuser");
+ ASSERT_STREQ(option_parser_peek_next_arg(&opts), "someuser");
ASSERT_STREQ(option_parser_consume_next_arg(&opts), "someuser");
ASSERT_EQ(option_parse(options, options + 3, &opts), 0);
@@ -1361,7 +1369,7 @@ TEST(option_optional_arg_consume) {
ASSERT_OK_POSITIVE(option_parse(options, options + 3, &opts));
ASSERT_STREQ(opts.opt->long_code, "user");
ASSERT_NULL(opts.arg);
- ASSERT_NULL(option_parser_next_arg(&opts));
+ ASSERT_NULL(option_parser_peek_next_arg(&opts));
ASSERT_NULL(option_parser_consume_next_arg(&opts));
ASSERT_EQ(option_parse(options, options + 3, &opts), 0);
@@ -1381,12 +1389,12 @@ TEST(option_optional_arg_consume) {
ASSERT_OK_POSITIVE(option_parse(options, options + 3, &opts));
ASSERT_STREQ(opts.opt->long_code, "user");
ASSERT_NULL(opts.arg);
- ASSERT_STREQ(option_parser_next_arg(&opts), "-u");
+ ASSERT_STREQ(option_parser_peek_next_arg(&opts), "-u");
ASSERT_OK_POSITIVE(option_parse(options, options + 3, &opts));
ASSERT_STREQ(opts.opt->long_code, "uid");
ASSERT_STREQ(opts.arg, "nobody");
- ASSERT_NULL(option_parser_next_arg(&opts));
+ ASSERT_NULL(option_parser_peek_next_arg(&opts));
ASSERT_NULL(option_parser_consume_next_arg(&opts));
ASSERT_EQ(option_parse(options, options + 3, &opts), 0);
@@ -1413,7 +1421,7 @@ TEST(option_optional_arg_consume) {
const char *arg = opts.arg;
if (!arg) {
- const char *t = option_parser_next_arg(&opts);
+ const char *t = option_parser_peek_next_arg(&opts);
if (t && t[0] != '-')
arg = option_parser_consume_next_arg(&opts);
}
diff --git a/src/test/test-qmp-client.c b/src/test/test-qmp-client.c
index befee02484588..dce1d12e5d7d7 100644
--- a/src/test/test-qmp-client.c
+++ b/src/test/test-qmp-client.c
@@ -1,33 +1,28 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include
-#include
+#include
#include
#include "sd-event.h"
+#include "sd-future.h"
#include "sd-json.h"
#include "errno-util.h"
#include "fd-util.h"
#include "json-stream.h"
-#include "pidref.h"
-#include "process-util.h"
#include "qmp-client.h"
#include "string-util.h"
#include "tests.h"
-/* Mock QMP server: runs in the child process of a fork, communicates via one end of a socketpair.
- * Uses JsonStream as the transport so framing (CRLF delimiter, message queuing, SCM_RIGHTS) is
- * handled the same way as on the client side — individual recv() syscalls may coalesce multiple
- * messages, and the parser must re-emit each one on its own. */
+/* Mock QMP server runs as an sd-fiber alongside the client on the same event loop. Its
+ * JsonStream uses the suspending json_stream_wait()/json_stream_flush() helpers, so the mock
+ * fiber yields whenever it's blocked on I/O and the client makes progress in the meantime. */
-/* We drive the stream manually via read/parse/wait; always report READING so json_stream_wait()
- * asks for POLLIN. */
static JsonStreamPhase mock_qmp_phase(void *userdata) {
return JSON_STREAM_PHASE_READING;
}
-/* Never reached — we don't wire the mock stream up to sd-event — but required at init. */
static int mock_qmp_dispatch(void *userdata) {
return 0;
}
@@ -43,9 +38,6 @@ static void mock_qmp_init(JsonStream *s, int fd) {
ASSERT_OK(json_stream_connect_fd_pair(s, fd, fd));
}
-/* Read one complete JSON message, blocking until available. Handles the case where multiple
- * client messages arrived coalesced into a single recv(): the parser walks the input buffer
- * one CRLF-delimited message at a time. */
static void mock_qmp_recv(JsonStream *s, sd_json_variant **ret) {
int r;
@@ -62,142 +54,135 @@ static void mock_qmp_recv(JsonStream *s, sd_json_variant **ret) {
}
}
-/* Enqueue one JSON variant and block until it has been fully written. */
static void mock_qmp_send(JsonStream *s, sd_json_variant *v) {
ASSERT_OK(json_stream_enqueue(s, v));
ASSERT_OK(json_stream_flush(s));
}
-/* Parse a literal JSON string and send it. Used for fixed greetings and unsolicited events. */
-static void mock_qmp_send_literal(JsonStream *s, const char *msg) {
+static void mock_qmp_send_greeting(JsonStream *s) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
- ASSERT_OK(sd_json_parse(msg, 0, &v, NULL, NULL));
+ ASSERT_OK(sd_json_buildo(&v,
+ SD_JSON_BUILD_PAIR("QMP", SD_JSON_BUILD_OBJECT(
+ SD_JSON_BUILD_PAIR("version", SD_JSON_BUILD_OBJECT(
+ SD_JSON_BUILD_PAIR("qemu", SD_JSON_BUILD_OBJECT(
+ SD_JSON_BUILD_PAIR_UNSIGNED("micro", 0),
+ SD_JSON_BUILD_PAIR_UNSIGNED("minor", 2),
+ SD_JSON_BUILD_PAIR_UNSIGNED("major", 9))))),
+ SD_JSON_BUILD_PAIR("capabilities", SD_JSON_BUILD_STRV(STRV_MAKE("oob")))))));
mock_qmp_send(s, v);
}
-/* Read a command from the client, verify it contains the expected command name, and send a
- * reply carrying the same id. If reply_data is NULL, an empty return object is sent. */
-static void mock_qmp_expect_and_reply(JsonStream *s, const char *expected_command, sd_json_variant *reply_data) {
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *reply_obj = NULL, *response = NULL;
-
- mock_qmp_recv(s, &cmd);
-
- sd_json_variant *execute = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "execute"));
- ASSERT_STREQ(sd_json_variant_string(execute), expected_command);
-
- sd_json_variant *id = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "id"));
+/* Receive one command, assert it matches `expected_command`, return its id (borrowed from *cmd). */
+static sd_json_variant* mock_qmp_expect(JsonStream *s, const char *expected_command, sd_json_variant **cmd) {
+ mock_qmp_recv(s, cmd);
+ ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(*cmd, "execute")), expected_command);
+ return ASSERT_NOT_NULL(sd_json_variant_by_key(*cmd, "id"));
+}
- if (!reply_data)
- ASSERT_OK(sd_json_variant_new_object(&reply_obj, NULL, 0));
+/* Send a reply for a previously-received command id. Passing NULL reply_data sends {}. */
+static void mock_qmp_reply(JsonStream *s, sd_json_variant *id, sd_json_variant *reply_data) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *response = NULL;
- ASSERT_OK(sd_json_buildo(
- &response,
- SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(reply_data ?: reply_obj)),
+ if (reply_data)
+ ASSERT_OK(sd_json_buildo(&response,
+ SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(reply_data)),
+ SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
+ else
+ ASSERT_OK(sd_json_buildo(&response,
+ SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_EMPTY_OBJECT),
SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
mock_qmp_send(s, response);
}
-/* Same shape as mock_qmp_expect_and_reply() but replies with a QMP error object. */
-static void mock_qmp_expect_and_reply_error(JsonStream *s, const char *expected_command, const char *error_desc) {
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *error_obj = NULL, *response = NULL;
-
- mock_qmp_recv(s, &cmd);
-
- sd_json_variant *execute = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "execute"));
- ASSERT_STREQ(sd_json_variant_string(execute), expected_command);
+static void mock_qmp_expect_and_reply(JsonStream *s, const char *expected_command, sd_json_variant *reply_data) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL;
+ mock_qmp_reply(s, mock_qmp_expect(s, expected_command, &cmd), reply_data);
+}
- sd_json_variant *id = ASSERT_NOT_NULL(sd_json_variant_by_key(cmd, "id"));
+static void mock_qmp_expect_and_reply_error(JsonStream *s, const char *expected_command, const char *error_desc) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cmd = NULL, *response = NULL;
+ sd_json_variant *id = mock_qmp_expect(s, expected_command, &cmd);
- ASSERT_OK(sd_json_buildo(
- &error_obj,
+ ASSERT_OK(sd_json_buildo(&response,
+ SD_JSON_BUILD_PAIR("error", SD_JSON_BUILD_OBJECT(
SD_JSON_BUILD_PAIR_STRING("class", "GenericError"),
- SD_JSON_BUILD_PAIR_STRING("desc", error_desc)));
-
- ASSERT_OK(sd_json_buildo(
- &response,
- SD_JSON_BUILD_PAIR("error", SD_JSON_BUILD_VARIANT(error_obj)),
- SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
+ SD_JSON_BUILD_PAIR_STRING("desc", error_desc))),
+ SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(id))));
mock_qmp_send(s, response);
}
-static _noreturn_ void mock_qmp_server(int fd) {
- _cleanup_(json_stream_done) JsonStream s = {};
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
-
- mock_qmp_init(&s, fd);
+static void mock_qmp_handshake(JsonStream *s) {
+ mock_qmp_send_greeting(s);
+ mock_qmp_expect_and_reply(s, "qmp_capabilities", NULL);
+}
- /* Send QMP greeting */
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 2, \"major\": 9}}, \"capabilities\": [\"oob\"]}}");
+/* Reply to query-status with a running=true/status="running" payload. */
+static void mock_qmp_query_status_running(JsonStream *s) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
- /* Accept qmp_capabilities */
- mock_qmp_expect_and_reply(&s, "qmp_capabilities", NULL);
+ ASSERT_OK(sd_json_buildo(&v,
+ SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
+ SD_JSON_BUILD_PAIR_STRING("status", "running")));
+ mock_qmp_expect_and_reply(s, "query-status", v);
+}
- /* Accept query-status, reply with running state */
- ASSERT_OK(sd_json_buildo(
- &status_return,
- SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
- SD_JSON_BUILD_PAIR_STRING("status", "running")));
- mock_qmp_expect_and_reply(&s, "query-status", status_return);
+/* Drive a mock+client pair on a single event loop. The client fiber runs as userdata=client,
+ * the mock fiber as userdata=fd (the server-side socket). */
+static void run_qmp_test(sd_fiber_func_t mock_fn, sd_fiber_func_t client_fn) {
+ _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+ _cleanup_(sd_future_unrefp) sd_future *client_f = NULL;
+ _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
+ _cleanup_close_pair_ int qmp_fds[2] = EBADF_PAIR;
- /* Accept stop */
- mock_qmp_expect_and_reply(&s, "stop", NULL);
+ ASSERT_OK(sd_event_new(&event));
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
- /* Send a STOP event */
- mock_qmp_send_literal(&s,
- "{\"event\": \"STOP\", \"timestamp\": {\"seconds\": 1234, \"microseconds\": 5678}}");
+ ASSERT_OK(qmp_client_connect_fd(&client, TAKE_FD(qmp_fds[0])));
+ ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
- /* Accept cont */
- mock_qmp_expect_and_reply(&s, "cont", NULL);
+ ASSERT_OK(sd_fiber_new(event, "mock", mock_fn, FD_TO_PTR(TAKE_FD(qmp_fds[1])), NULL, NULL));
+ ASSERT_OK(sd_fiber_new(event, "client", client_fn, client, NULL, &client_f));
- /* json_stream_done() on cleanup closes our fd and signals EOF. */
- _exit(EXIT_SUCCESS);
+ ASSERT_OK(sd_event_loop(event));
+ ASSERT_OK(sd_future_result(client_f));
}
-/* Test helper: tracks an async QMP command result and signals completion. */
-typedef struct {
- sd_json_variant *result;
- char *error_desc;
- int error;
- bool done;
-} QmpTestResult;
-
-static int on_test_result(
- QmpClient *client,
- sd_json_variant *result,
- const char *error_desc,
- int error,
- void *userdata) {
-
- QmpTestResult *t = ASSERT_PTR(userdata);
-
- t->error = error;
- if (result)
- t->result = sd_json_variant_ref(result);
- if (error_desc)
- t->error_desc = strdup(error_desc);
- t->done = true;
- return 0;
-}
+/* Define a test whose body runs as the client fiber on an event loop shared with `mock_fn`.
+ * The body receives `QmpClient *client` as its argument. */
+#define QMP_TEST(name, mock_fn) \
+ static int test_##name##_body(QmpClient *client); \
+ static int test_##name##_fiber(void *userdata) { \
+ int r = test_##name##_body(userdata); \
+ ASSERT_OK(sd_event_exit(sd_fiber_get_event(), 0)); \
+ return r; \
+ } \
+ TEST(name) { \
+ run_qmp_test(mock_fn, test_##name##_fiber); \
+ } \
+ static int test_##name##_body(QmpClient *client)
+
+static int mock_qmp_basic_fiber(void *userdata) {
+ _cleanup_(json_stream_done) JsonStream s = {};
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *stop_event = NULL;
-/* Run the event loop until the test result callback fires. */
-static void qmp_test_wait(sd_event *event, QmpTestResult *t) {
- assert(event);
- assert(t);
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ mock_qmp_handshake(&s);
- while (!t->done)
- ASSERT_OK(sd_event_run(event, UINT64_MAX));
-}
+ mock_qmp_query_status_running(&s);
+ mock_qmp_expect_and_reply(&s, "stop", NULL);
-static void qmp_test_result_done(QmpTestResult *t) {
- assert(t);
+ ASSERT_OK(sd_json_buildo(&stop_event,
+ SD_JSON_BUILD_PAIR_STRING("event", "STOP"),
+ SD_JSON_BUILD_PAIR("timestamp", SD_JSON_BUILD_OBJECT(
+ SD_JSON_BUILD_PAIR_UNSIGNED("seconds", 1234),
+ SD_JSON_BUILD_PAIR_UNSIGNED("microseconds", 5678)))));
+ mock_qmp_send(&s, stop_event);
- sd_json_variant_unref(t->result);
- free(t->error_desc);
- *t = (QmpTestResult) {};
+ mock_qmp_expect_and_reply(&s, "cont", NULL);
+ return 0;
}
static int test_event_callback(
@@ -208,516 +193,276 @@ static int test_event_callback(
bool *event_received = ASSERT_PTR(userdata);
- /* We may also receive a synthetic SHUTDOWN event when the mock server closes the connection;
- * only validate the STOP event we actually care about. */
+ /* Ignore the synthetic SHUTDOWN emitted when the mock closes the connection. */
if (streq(event, "STOP"))
*event_received = true;
return 0;
}
-TEST(qmp_client_basic) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(sd_event_unrefp) sd_event *event = NULL;
- _cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
- QmpTestResult t = {};
- sd_json_variant *running, *status;
- int qmp_fds[2];
- int r;
-
- ASSERT_OK(sd_event_new(&event));
-
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
-
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
-
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server(qmp_fds[1]);
- }
-
- safe_close(qmp_fds[1]);
-
- /* Connect then attach to event loop — handshake completes transparently
- * inside the first call()/invoke(). */
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
- ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
-
- /* Set event callback to catch STOP event during cont */
+QMP_TEST(qmp_client_basic, mock_qmp_basic_fiber) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
+ _cleanup_free_ char *error_desc = NULL;
bool event_received = false;
+
qmp_client_bind_event(client, test_event_callback, &event_received);
- /* Execute query-status */
- ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "query-status", NULL, on_test_result, &t));
- qmp_test_wait(event, &t);
- ASSERT_EQ(t.error, 0);
- ASSERT_NOT_NULL(t.result);
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "query-status", NULL, &result, &error_desc));
+ ASSERT_NULL(error_desc);
- running = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "running"));
+ sd_json_variant *running = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "running"));
ASSERT_TRUE(sd_json_variant_boolean(running));
-
- status = ASSERT_NOT_NULL(sd_json_variant_by_key(t.result, "status"));
+ sd_json_variant *status = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "status"));
ASSERT_STREQ(sd_json_variant_string(status), "running");
- qmp_test_result_done(&t);
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "stop", NULL, NULL, NULL));
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "cont", NULL, NULL, NULL));
- /* Execute stop */
- ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "stop", NULL, on_test_result, &t));
- qmp_test_wait(event, &t);
- ASSERT_EQ(t.error, 0);
- qmp_test_result_done(&t);
+ ASSERT_TRUE(event_received);
+ return 0;
+}
- /* Execute cont -- the STOP event should be dispatched by the IO callback */
- ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "cont", NULL, on_test_result, &t));
- qmp_test_wait(event, &t);
- ASSERT_EQ(t.error, 0);
- qmp_test_result_done(&t);
+static int mock_qmp_eof_fiber(void *userdata) {
+ _cleanup_(json_stream_done) JsonStream s = {};
- /* Verify the STOP event was received */
- ASSERT_TRUE(event_received);
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ mock_qmp_handshake(&s);
+ /* Return; _cleanup_ closes the fd → client sees EOF. */
+ return 0;
+}
- /* Wait for child and verify clean exit */
- siginfo_t si = {};
- ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
- ASSERT_EQ(si.si_code, CLD_EXITED);
- ASSERT_EQ(si.si_status, EXIT_SUCCESS);
+QMP_TEST(qmp_client_eof, mock_qmp_eof_fiber) {
+ int r = qmp_client_call(client, "query-status", NULL, NULL, NULL);
+ ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
+ return 0;
}
-TEST(qmp_client_eof) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(sd_event_unrefp) sd_event *event = NULL;
- _cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
- QmpTestResult t = {};
- int qmp_fds[2];
- int r;
+static int mock_qmp_call_fiber(void *userdata) {
+ _cleanup_(json_stream_done) JsonStream s = {};
- ASSERT_OK(sd_event_new(&event));
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ mock_qmp_handshake(&s);
+
+ mock_qmp_query_status_running(&s);
+ mock_qmp_expect_and_reply_error(&s, "stop", "not running");
+ mock_qmp_expect_and_reply_error(&s, "stop", "still not running");
+ return 0;
+}
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-eof)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
+QMP_TEST(qmp_client_call, mock_qmp_call_fiber) {
+ _cleanup_(sd_future_cancel_wait_unrefp) sd_future *f = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
+ _cleanup_free_ char *error_desc = NULL;
- if (r == 0) {
- _cleanup_(json_stream_done) JsonStream s = {};
+ /* Exercise qmp_client_call_future() + sd_fiber_await() + future_get_qmp_reply()
+ * directly — success path. */
+ ASSERT_OK(qmp_client_call_future(client, "query-status", NULL, &f));
+ ASSERT_OK(sd_fiber_await(f));
+ ASSERT_OK(sd_future_result(f));
+ ASSERT_OK(future_get_qmp_reply(f, &result, &error_desc));
+
+ ASSERT_NULL(error_desc);
+ sd_json_variant *running = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "running"));
+ ASSERT_TRUE(sd_json_variant_boolean(running));
+ sd_json_variant *status = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "status"));
+ ASSERT_STREQ(sd_json_variant_string(status), "running");
- safe_close(qmp_fds[0]);
- mock_qmp_init(&s, qmp_fds[1]);
+ /* QMP-level error: future resolves with 0 (the reply landed); future_get_qmp_reply()
+ * surfaces the error via error_desc, with result left NULL. */
+ f = sd_future_unref(f);
+ result = sd_json_variant_unref(result);
+ error_desc = mfree(error_desc);
- /* Send greeting and accept capabilities, then die */
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
+ ASSERT_OK(qmp_client_call_future(client, "stop", NULL, &f));
+ ASSERT_OK(sd_fiber_await(f));
+ ASSERT_OK(sd_future_result(f));
+ ASSERT_OK(future_get_qmp_reply(f, &result, &error_desc));
- mock_qmp_expect_and_reply(&s, "qmp_capabilities", NULL);
+ ASSERT_NULL(result);
+ ASSERT_STREQ(error_desc, "not running");
- /* _exit() closes our fd via kernel teardown, signalling EOF to the peer. */
- _exit(EXIT_SUCCESS);
- }
+ /* qmp_client_call() surfaces QMP errors as -EIO when the caller doesn't ask for the desc. */
+ ASSERT_ERROR(qmp_client_call(client, "stop", NULL, NULL, NULL), EIO);
+ return 0;
+}
- safe_close(qmp_fds[1]);
+static int mock_qmp_call_disconnect_fiber(void *userdata) {
+ _cleanup_(json_stream_done) JsonStream s = {};
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *stop_cmd = NULL;
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
- ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ mock_qmp_handshake(&s);
- /* Executing a command should fail with a disconnect error because the server
- * closed. The handshake may succeed or fail inside invoke() — either way the
- * invoke itself or the async callback should report a disconnect. */
- r = qmp_client_invoke(client, /* ret_slot= */ NULL, "query-status", NULL, on_test_result, &t);
- if (r < 0)
- ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
- else {
- qmp_test_wait(event, &t);
- ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(t.error));
- qmp_test_result_done(&t);
- }
+ /* Consume the stop command but don't reply — cleanup closes the fd and the client
+ * sees a disconnect while suspended. */
+ mock_qmp_recv(&s, &stop_cmd);
+ return 0;
+}
- siginfo_t si = {};
- ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
- ASSERT_EQ(si.si_code, CLD_EXITED);
- ASSERT_EQ(si.si_status, EXIT_SUCCESS);
+QMP_TEST(qmp_client_call_disconnect, mock_qmp_call_disconnect_fiber) {
+ int r = qmp_client_call(client, "stop", NULL, NULL, NULL);
+ ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
+ return 0;
}
-/* Mock QMP server for the fd-passing test. Drives the wire dance:
- * greeting → recv qmp_capabilities → reply → recv add-fd → reply
- * Asserts that exactly one SCM_RIGHTS fd arrives total across the two recvs. We can't
- * require the fd to come attached to add-fd specifically: AF_UNIX coalesces the client's
- * non-SCM cap sendmsg forward into the SCM-bearing add-fd sendmsg, so the fd may surface
- * with either recv depending on kernel scheduling. QEMU's FIFO fd queue doesn't care. */
-static _noreturn_ void mock_qmp_server_fd(int fd) {
+static int mock_qmp_fd_fiber(void *userdata) {
_cleanup_(json_stream_done) JsonStream s = {};
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *cap_cmd = NULL,
- *addfd_cmd = NULL,
- *cap_reply = NULL,
- *addfd_return = NULL,
- *addfd_reply = NULL;
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *cap_cmd = NULL, *addfd_cmd = NULL,
+ *addfd_return = NULL;
- mock_qmp_init(&s, fd);
- ASSERT_OK(json_stream_set_allow_fd_passing_input(&s, true, /* with_sockopt= */ true));
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ ASSERT_OK(json_stream_set_allow_fd_passing_input(&s, true, true));
- /* Greeting */
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
+ mock_qmp_send_greeting(&s);
- /* Receive qmp_capabilities (may or may not carry the fd depending on coalescing). */
- mock_qmp_recv(&s, &cap_cmd);
+ /* The fd may ride with either command depending on AF_UNIX coalescing; count across both. */
+ sd_json_variant *cap_id = mock_qmp_expect(&s, "qmp_capabilities", &cap_cmd);
size_t n_fds_total = json_stream_get_n_input_fds(&s);
- ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(cap_cmd, "execute")), "qmp_capabilities");
json_stream_close_input_fds(&s);
+ mock_qmp_reply(&s, cap_id, NULL);
- sd_json_variant *cap_id = ASSERT_NOT_NULL(sd_json_variant_by_key(cap_cmd, "id"));
- ASSERT_OK(sd_json_buildo(
- &cap_reply,
- SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_EMPTY_OBJECT),
- SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(cap_id))));
- mock_qmp_send(&s, cap_reply);
-
- /* Receive add-fd (fd may already have been consumed with cap's recv). */
- mock_qmp_recv(&s, &addfd_cmd);
+ sd_json_variant *addfd_id = mock_qmp_expect(&s, "add-fd", &addfd_cmd);
n_fds_total += json_stream_get_n_input_fds(&s);
- ASSERT_STREQ(sd_json_variant_string(sd_json_variant_by_key(addfd_cmd, "execute")), "add-fd");
json_stream_close_input_fds(&s);
-
ASSERT_EQ(n_fds_total, (size_t) 1);
- sd_json_variant *addfd_id = ASSERT_NOT_NULL(sd_json_variant_by_key(addfd_cmd, "id"));
- ASSERT_OK(sd_json_buildo(
- &addfd_return,
- SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0),
- SD_JSON_BUILD_PAIR_UNSIGNED("fd", 42)));
- ASSERT_OK(sd_json_buildo(
- &addfd_reply,
- SD_JSON_BUILD_PAIR("return", SD_JSON_BUILD_VARIANT(addfd_return)),
- SD_JSON_BUILD_PAIR("id", SD_JSON_BUILD_VARIANT(addfd_id))));
- mock_qmp_send(&s, addfd_reply);
-
- _exit(EXIT_SUCCESS);
+ ASSERT_OK(sd_json_buildo(&addfd_return,
+ SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0),
+ SD_JSON_BUILD_PAIR_UNSIGNED("fd", 42)));
+ mock_qmp_reply(&s, addfd_id, addfd_return);
+ return 0;
}
-/* End-to-end fd-passing through qmp_client_invoke() with QMP_CLIENT_ARGS_FD(): open a real
- * fd, send add-fd, confirm the mock received a single SCM_RIGHTS fd and replied successfully. */
-TEST(qmp_client_invoke_with_fd) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(sd_event_unrefp) sd_event *event = NULL;
- _cleanup_(pidref_done) PidRef pid = PIDREF_NULL;
+QMP_TEST(qmp_client_invoke_with_fd, mock_qmp_fd_fiber) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
_cleanup_close_ int fd_to_pass = -EBADF;
- QmpTestResult t = {};
- int qmp_fds[2];
- int r;
-
- ASSERT_OK(sd_event_new(&event));
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *result = NULL;
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-fd)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
-
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server_fd(qmp_fds[1]);
- }
-
- safe_close(qmp_fds[1]);
-
- /* Open a real fd to pass — /dev/null is universally available. */
- fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
- ASSERT_OK(fd_to_pass);
-
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
- ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
+ fd_to_pass = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
- ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "add-fd",
- QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
- on_test_result, &t));
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "add-fd",
+ QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
+ &result, NULL));
+ ASSERT_NOT_NULL(result);
+ return 0;
+}
- qmp_test_wait(event, &t);
- ASSERT_EQ(t.error, 0);
- ASSERT_NOT_NULL(t.result);
- qmp_test_result_done(&t);
+static int on_dead_peer_reply(
+ QmpClient *client,
+ sd_json_variant *result,
+ const char *error_desc,
+ int error,
+ void *userdata) {
- /* Wait for the mock. If its fd-count assertion tripped, si.si_status is non-zero. */
- siginfo_t si = {};
- ASSERT_OK(pidref_wait_for_terminate(&pid, &si));
- ASSERT_EQ(si.si_code, CLD_EXITED);
- ASSERT_EQ(si.si_status, EXIT_SUCCESS);
+ /* Peer was closed before the write hit the wire; expect a disconnect. */
+ ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(error));
+ return 0;
}
-/* Regression: the caller-supplied fds — already TAKE_FD()'d through QMP_CLIENT_ARGS_FD() —
- * must never leak, regardless of whether the invoke reaches the wire. Verified here via a
- * dead peer: invoke enqueues (non-blocking), the queue item owns the fd, and client teardown
- * must close it. */
+/* Verify caller-supplied fds passed through QMP_CLIENT_ARGS_FD() are closed on client teardown
+ * even when the peer is already dead: invoke enqueues, the queue item owns the fd, unref closes. */
TEST(qmp_client_invoke_failure_closes_fds) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *args = NULL;
_cleanup_close_ int fd_to_pass = -EBADF;
QmpClient *client = NULL;
- QmpTestResult t = {};
- int qmp_fds[2];
+ _cleanup_close_pair_ int qmp_fds[2] = EBADF_PAIR;
int saved_fd_value;
ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+ qmp_fds[1] = safe_close(qmp_fds[1]);
- /* Close the peer end immediately so any write attempt sees EPIPE. */
- safe_close(qmp_fds[1]);
-
- fd_to_pass = open("/dev/null", O_RDWR|O_CLOEXEC);
- ASSERT_OK(fd_to_pass);
- saved_fd_value = fd_to_pass; /* remember the int value for the closed-check */
+ fd_to_pass = ASSERT_OK_ERRNO(eventfd(0, EFD_CLOEXEC));
+ saved_fd_value = fd_to_pass;
ASSERT_OK(sd_json_buildo(&args, SD_JSON_BUILD_PAIR_UNSIGNED("fdset-id", 0)));
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
+ ASSERT_OK(qmp_client_connect_fd(&client, TAKE_FD(qmp_fds[0])));
- /* invoke no longer blocks on the handshake — it just enqueues. The fd is now
- * owned by the underlying JsonStream output queue. */
- ASSERT_OK(qmp_client_invoke(client, /* ret_slot= */ NULL, "add-fd",
+ ASSERT_OK(qmp_client_invoke(client, NULL, "add-fd",
QMP_CLIENT_ARGS_FD(args, TAKE_FD(fd_to_pass)),
- on_test_result, &t));
- ASSERT_EQ(fd_to_pass, -EBADF); /* TAKE_FD cleared our local handle */
-
- /* The fd is still open here (held in JsonStream's queue). */
+ on_dead_peer_reply, NULL));
+ ASSERT_EQ(fd_to_pass, -EBADF);
ASSERT_OK_ERRNO(fcntl(saved_fd_value, F_GETFD));
- /* Client teardown (json_stream_done) must close queued output fds, otherwise the
- * saved fd number would still be valid. */
client = qmp_client_unref(client);
- ASSERT_EQ(fcntl(saved_fd_value, F_GETFD), -1);
- ASSERT_EQ(errno, EBADF);
+ ASSERT_ERROR_ERRNO(fcntl(saved_fd_value, F_GETFD), EBADF);
}
-/* Mock for the slot lifecycle + cancel tests: greets, accepts capabilities, then accepts
- * query-status and stop, replying with dummy returns. A cancelled query-status still gets
- * sent on the wire (cancel merely removes the pending slot), so the server must be prepared
- * to read and reply to it. */
-static _noreturn_ void mock_qmp_server_slot(int fd) {
+/* Shared mock for the two slot tests: the follow-up stop is what drives the event loop long
+ * enough to dispatch the query-status reply. */
+static int mock_qmp_slot_fiber(void *userdata) {
_cleanup_(json_stream_done) JsonStream s = {};
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
-
- mock_qmp_init(&s, fd);
-
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
-
- mock_qmp_expect_and_reply(&s, "qmp_capabilities", NULL);
- ASSERT_OK(sd_json_buildo(
- &status_return,
- SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
- SD_JSON_BUILD_PAIR_STRING("status", "running")));
- mock_qmp_expect_and_reply(&s, "query-status", status_return);
+ mock_qmp_init(&s, PTR_TO_FD(userdata));
+ mock_qmp_handshake(&s);
+ mock_qmp_query_status_running(&s);
mock_qmp_expect_and_reply(&s, "stop", NULL);
-
- _exit(EXIT_SUCCESS);
+ return 0;
}
-/* Verify that when qmp_client_invoke() returns a slot, qmp_slot_get_client() tracks the
- * connection state: the client pointer is reported while the call is in flight, and flipped
- * back to NULL once the reply has been dispatched. The caller must still be able to drop its
- * ref safely after that. */
-TEST(qmp_client_invoke_slot_lifecycle) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(sd_event_unrefp) sd_event *event = NULL;
- _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
- _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
- QmpTestResult t = {};
- int qmp_fds[2];
- int r;
+static int nop_callback(
+ QmpClient *client,
+ sd_json_variant *result,
+ const char *error_desc,
+ int error,
+ void *userdata) {
- ASSERT_OK(sd_event_new(&event));
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+ return 0;
+}
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-slot-life)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server_slot(qmp_fds[1]);
- }
- safe_close(qmp_fds[1]);
+/* Tripwire for the cancel test: if it fires, the cancel didn't do its job. */
+static int tripwire_callback(
+ QmpClient *client,
+ sd_json_variant *result,
+ const char *error_desc,
+ int error,
+ void *userdata) {
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
- ASSERT_OK(qmp_client_attach_event(client, event, SD_EVENT_PRIORITY_NORMAL));
+ bool *fired = ASSERT_PTR(userdata);
+ *fired = true;
+ return 0;
+}
- ASSERT_OK(qmp_client_invoke(client, &slot, "query-status", NULL, on_test_result, &t));
+QMP_TEST(qmp_client_invoke_slot_lifecycle, mock_qmp_slot_fiber) {
+ _cleanup_(qmp_slot_unrefp) QmpSlot *slot = NULL;
- /* While in flight the slot still references its client. */
- ASSERT_NOT_NULL(slot);
+ ASSERT_OK(qmp_client_invoke(client, &slot, "query-status", NULL, nop_callback, NULL));
ASSERT_PTR_EQ(qmp_slot_get_client(slot), client);
- qmp_test_wait(event, &t);
- ASSERT_EQ(t.error, 0);
- ASSERT_NOT_NULL(t.result);
+ /* Drive the loop via a follow-up stop; its suspending call lets both replies dispatch. */
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "stop", NULL, NULL, NULL));
- /* Once dispatched, the slot is disconnected from the client but still owned by us. */
+ /* After dispatch the slot is disconnected from the client but still owned by us. */
ASSERT_NULL(qmp_slot_get_client(slot));
- qmp_test_result_done(&t);
-
- /* Drop our ref explicitly (out of order w.r.t. cleanup) to exercise the
- * already-disconnected path in qmp_slot_free(). */
+ /* Explicit out-of-order unref exercises the already-disconnected path in qmp_slot_free(). */
slot = qmp_slot_unref(slot);
- ASSERT_NULL(slot);
+ return 0;
}
-/* Verify that dropping the only reference on a pending slot before the reply arrives cancels
- * the callback. The command is already enqueued on the stream at that point, so the server
- * still sees it and replies — but the reply lands on an unknown id and is discarded. */
-TEST(qmp_client_invoke_slot_cancel) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
- QmpTestResult t_cancelled = {};
+QMP_TEST(qmp_client_invoke_slot_cancel, mock_qmp_slot_fiber) {
QmpSlot *slot = NULL;
- int qmp_fds[2];
- int r;
-
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
+ bool fired = false;
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-slot-cancel)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server_slot(qmp_fds[1]);
- }
- safe_close(qmp_fds[1]);
+ ASSERT_OK(qmp_client_invoke(client, &slot, "query-status", NULL, tripwire_callback, &fired));
- /* Drive without an event loop so the subsequent qmp_client_call() owns all pumping;
- * it serializes write→read round-trips, which avoids the mock server seeing the
- * cancelled query-status and the follow-up stop concatenated into a single recv(). */
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
-
- ASSERT_OK(qmp_client_invoke(client, &slot, "query-status", NULL, on_test_result, &t_cancelled));
- ASSERT_NOT_NULL(slot);
-
- /* Drop our sole ref → slot disconnects itself from the client's pending set. The
- * enqueued query-status is still on the wire; when its reply arrives, dispatch_reply
- * won't find a matching slot and will log-and-discard it. */
+ /* Drop our sole ref → slot disconnects from the client's pending set. The enqueued
+ * query-status is still on the wire; its reply lands on an unknown id and is discarded. */
slot = qmp_slot_unref(slot);
- ASSERT_NULL(slot);
-
- /* Synchronous call drives its own process+wait pump: it first drains the already-
- * enqueued query-status write, consumes (and discards) its reply, then sends stop
- * and waits for that reply. Any improper fire of the cancelled callback would have
- * happened during that process() pass. */
- ASSERT_EQ(qmp_client_call(client, "stop", NULL, NULL, NULL), 1);
- /* The cancelled callback must never have fired. */
- ASSERT_FALSE(t_cancelled.done);
- ASSERT_NULL(t_cancelled.result);
- ASSERT_NULL(t_cancelled.error_desc);
-}
-
-/* Drives a small wire dance for the sync call test: greeting, capabilities, one successful
- * command reply, and two error replies (one for the ret_error_desc path, one for the -EIO
- * path). */
-static _noreturn_ void mock_qmp_server_call(int fd) {
- _cleanup_(json_stream_done) JsonStream s = {};
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *status_return = NULL;
-
- mock_qmp_init(&s, fd);
-
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
-
- mock_qmp_expect_and_reply(&s, "qmp_capabilities", NULL);
-
- ASSERT_OK(sd_json_buildo(
- &status_return,
- SD_JSON_BUILD_PAIR_BOOLEAN("running", true),
- SD_JSON_BUILD_PAIR_STRING("status", "running")));
- mock_qmp_expect_and_reply(&s, "query-status", status_return);
-
- mock_qmp_expect_and_reply_error(&s, "stop", "not running");
- mock_qmp_expect_and_reply_error(&s, "stop", "still not running");
-
- _exit(EXIT_SUCCESS);
-}
-
-TEST(qmp_client_call) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
- int qmp_fds[2];
- int r;
-
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
-
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-call)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server_call(qmp_fds[1]);
- }
- safe_close(qmp_fds[1]);
-
- /* qmp_client_call() drives its own process()+wait() pump, so no event loop needed. */
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
-
- /* Successful call: borrowed result pointer is valid until the next call. */
- sd_json_variant *result = NULL;
- const char *error_desc = NULL;
- ASSERT_EQ(qmp_client_call(client, "query-status", NULL, &result, &error_desc), 1);
- ASSERT_NULL(error_desc);
- ASSERT_NOT_NULL(result);
-
- sd_json_variant *running = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "running"));
- ASSERT_TRUE(sd_json_variant_boolean(running));
- sd_json_variant *status = ASSERT_NOT_NULL(sd_json_variant_by_key(result, "status"));
- ASSERT_STREQ(sd_json_variant_string(status), "running");
+ ASSERT_OK_POSITIVE(qmp_client_call(client, "stop", NULL, NULL, NULL));
- /* QMP error with ret_error_desc provided: returns 1, result NULL, desc set. */
- result = (sd_json_variant*) 0x1; /* poison to catch lack-of-write */
- error_desc = NULL;
- ASSERT_EQ(qmp_client_call(client, "stop", NULL, &result, &error_desc), 1);
- ASSERT_NULL(result);
- ASSERT_STREQ(error_desc, "not running");
-
- /* QMP error without ret_error_desc: surfaces as -EIO. */
- ASSERT_EQ(qmp_client_call(client, "stop", NULL, NULL, NULL), -EIO);
-}
-
-/* Server variant for the sync-call disconnect test: greets, accepts capabilities, reads one
- * command without replying, then closes the socket so the client sees EOF mid-wait. */
-static _noreturn_ void mock_qmp_server_call_disconnect(int fd) {
- _cleanup_(json_stream_done) JsonStream s = {};
- _cleanup_(sd_json_variant_unrefp) sd_json_variant *stop_cmd = NULL;
-
- mock_qmp_init(&s, fd);
-
- mock_qmp_send_literal(&s,
- "{\"QMP\": {\"version\": {\"qemu\": {\"micro\": 0, \"minor\": 0, \"major\": 9}}, \"capabilities\": []}}");
-
- mock_qmp_expect_and_reply(&s, "qmp_capabilities", NULL);
-
- /* Consume the stop command but don't reply — json_stream_done() on cleanup closes
- * our fd, triggering EOF while the client is blocked in qmp_client_call()'s
- * process+wait pump. */
- mock_qmp_recv(&s, &stop_cmd);
-
- _exit(EXIT_SUCCESS);
-}
-
-TEST(qmp_client_call_disconnect) {
- _cleanup_(qmp_client_unrefp) QmpClient *client = NULL;
- _cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
- int qmp_fds[2];
- int r;
-
- ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, qmp_fds));
-
- r = ASSERT_OK(pidref_safe_fork("(mock-qmp-call-disc)", FORK_DEATHSIG_SIGKILL|FORK_LOG, &pid));
- if (r == 0) {
- safe_close(qmp_fds[0]);
- mock_qmp_server_call_disconnect(qmp_fds[1]);
- }
- safe_close(qmp_fds[1]);
-
- ASSERT_OK(qmp_client_connect_fd(&client, qmp_fds[0]));
-
- /* The server reads our stop command and closes without replying. qmp_client_call()
- * is driving its own pump, so it must notice the EOF, transition to DISCONNECTED,
- * and return a disconnect error rather than hanging. */
- r = qmp_client_call(client, "stop", NULL, NULL, NULL);
- ASSERT_TRUE(r < 0);
- ASSERT_TRUE(ERRNO_IS_NEG_DISCONNECT(r));
+ ASSERT_FALSE(fired);
+ return 0;
}
TEST(qmp_schema_has_member) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *schema = NULL;
- /* QEMU introspection uses opaque numeric type ids ("0", "1", ...) — only member names are
- * the actual QAPI strings. Verify we walk all object entries and find the member by name. */
+ /* QEMU introspection uses opaque numeric type ids ("0", "1", ...); only member names
+ * are the real QAPI strings. Verify we walk all object entries to find members by name. */
ASSERT_OK(sd_json_build(&schema,
SD_JSON_BUILD_ARRAY(
SD_JSON_BUILD_OBJECT(
@@ -747,10 +492,4 @@ TEST(qmp_schema_has_member) {
ASSERT_FALSE(qmp_schema_has_member(NULL, "discard-no-unref"));
}
-static int intro(void) {
- /* Ignore SIGPIPE so that write() to a closed socket returns EPIPE instead of killing us */
- ASSERT_TRUE(signal(SIGPIPE, SIG_IGN) != SIG_ERR);
- return 0;
-}
-
-DEFINE_TEST_MAIN_FULL(LOG_DEBUG, intro, NULL);
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/timedate/timedatectl.c b/src/timedate/timedatectl.c
index 211f7d7a6d280..c35b090035eac 100644
--- a/src/timedate/timedatectl.c
+++ b/src/timedate/timedatectl.c
@@ -949,7 +949,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/tmpfiles/test-offline-passwd.c b/src/tmpfiles/test-offline-passwd.c
index 21b2697ceeab6..f357ef8865d8d 100644
--- a/src/tmpfiles/test-offline-passwd.c
+++ b/src/tmpfiles/test-offline-passwd.c
@@ -45,7 +45,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION('r', "root", "PATH", "Operate on an alternate filesystem root"):
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index 0cc06ca8ec0aa..44843f3ca77ec 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -4189,7 +4189,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_LONG("create", NULL, "Create and adjust files and directories"):
diff --git a/src/tpm2-setup/tpm2-clear.c b/src/tpm2-setup/tpm2-clear.c
index b65905c03dbcd..19186ecc02fd8 100644
--- a/src/tpm2-setup/tpm2-clear.c
+++ b/src/tpm2-setup/tpm2-clear.c
@@ -52,7 +52,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c
index b8e585225be2a..bb08e31a81c87 100644
--- a/src/tpm2-setup/tpm2-setup.c
+++ b/src/tpm2-setup/tpm2-setup.c
@@ -78,7 +78,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/tty-ask-password-agent/tty-ask-password-agent.c b/src/tty-ask-password-agent/tty-ask-password-agent.c
index cd49503156db7..d675e4269ac16 100644
--- a/src/tty-ask-password-agent/tty-ask-password-agent.c
+++ b/src/tty-ask-password-agent/tty-ask-password-agent.c
@@ -475,7 +475,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/ata_id/ata_id.c b/src/udev/ata_id/ata_id.c
index ea28ad027d313..c2fabdcdb844b 100644
--- a/src/udev/ata_id/ata_id.c
+++ b/src/udev/ata_id/ata_id.c
@@ -379,7 +379,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/cdrom_id/cdrom_id.c b/src/udev/cdrom_id/cdrom_id.c
index b78096bde6362..27423e985155e 100644
--- a/src/udev/cdrom_id/cdrom_id.c
+++ b/src/udev/cdrom_id/cdrom_id.c
@@ -920,7 +920,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/dmi_memory_id/dmi_memory_id.c b/src/udev/dmi_memory_id/dmi_memory_id.c
index 269ea15252101..a1708c128c928 100644
--- a/src/udev/dmi_memory_id/dmi_memory_id.c
+++ b/src/udev/dmi_memory_id/dmi_memory_id.c
@@ -664,7 +664,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/fido_id/fido_id.c b/src/udev/fido_id/fido_id.c
index 6b31f49a48076..a19c7eebec6e7 100644
--- a/src/udev/fido_id/fido_id.c
+++ b/src/udev/fido_id/fido_id.c
@@ -49,7 +49,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/iocost/iocost.c b/src/udev/iocost/iocost.c
index 3b926fa4a24f2..eadab1cb8a091 100644
--- a/src/udev/iocost/iocost.c
+++ b/src/udev/iocost/iocost.c
@@ -86,7 +86,7 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/mtd_probe/mtd_probe.c b/src/udev/mtd_probe/mtd_probe.c
index 3e5f162343dbb..fe9924f1b6e28 100644
--- a/src/udev/mtd_probe/mtd_probe.c
+++ b/src/udev/mtd_probe/mtd_probe.c
@@ -56,7 +56,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/udev/scsi_id/scsi_id.c b/src/udev/scsi_id/scsi_id.c
index f272648c420c9..d7970722848c8 100644
--- a/src/udev/scsi_id/scsi_id.c
+++ b/src/udev/scsi_id/scsi_id.c
@@ -228,7 +228,7 @@ static int set_options(int argc, char **argv, char *maj_min_dev) {
OptionParser opts = { argc, argv };
int r;
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
@@ -319,7 +319,7 @@ static int per_dev_options(struct scsi_id_device *dev_scsi, int *good_bad, enum
/* We reuse the option parser, but only a subset of the options is supported here.
* If any others are encountered, return an error. */
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
if (opts.opt->short_code == 'b')
*good_bad = 0;
else if (opts.opt->short_code == 'g')
diff --git a/src/udev/udev-builtin-blkid.c b/src/udev/udev-builtin-blkid.c
index ca40b62d782b9..4cd22a889fcf7 100644
--- a/src/udev/udev-builtin-blkid.c
+++ b/src/udev/udev-builtin-blkid.c
@@ -10,7 +10,6 @@
#endif
#include
-#include
#include
#include
#include
@@ -26,6 +25,7 @@
#include "fd-util.h"
#include "initrd-util.h"
#include "gpt.h"
+#include "options.h"
#include "parse-util.h"
#include "string-util.h"
#include "strv.h"
@@ -506,13 +506,6 @@ static int builtin_blkid(UdevEvent *event, int argc, char *argv[]) {
int64_t offset = 0;
int r;
- static const struct option options[] = {
- { "offset", required_argument, NULL, 'o' },
- { "hint", required_argument, NULL, 'H' },
- { "noraid", no_argument, NULL, 'R' },
- {}
- };
-
r = dlopen_libblkid(LOG_DEBUG);
if (r < 0)
return log_device_debug_errno(dev, r, "blkid not available: %m");
@@ -522,32 +515,32 @@ static int builtin_blkid(UdevEvent *event, int argc, char *argv[]) {
if (!pr)
return log_device_debug_errno(dev, errno_or_else(ENOMEM), "Failed to create blkid prober: %m");
- for (;;) {
- int option;
+ OptionParser opts = { argc, argv, .namespace = "udev-builtin-blkid" };
- option = getopt_long(argc, argv, "o:H:R", options, NULL);
- if (option == -1)
- break;
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_NAMESPACE("udev-builtin-blkid"): {}
- switch (option) {
- case 'H':
+ OPTION('H', "hint", "HINT", NULL):
errno = 0;
- r = sym_blkid_probe_set_hint(pr, optarg, 0);
+ r = sym_blkid_probe_set_hint(pr, opts.arg, 0);
if (r < 0)
- return log_device_error_errno(dev, errno_or_else(ENOMEM), "Failed to use '%s' probing hint: %m", optarg);
+ return log_device_error_errno(dev, errno_or_else(ENOMEM), "Failed to use '%s' probing hint: %m", opts.arg);
break;
- case 'o':
- r = safe_atoi64(optarg, &offset);
+
+ OPTION('o', "offset", "OFFSET", NULL):
+ r = safe_atoi64(opts.arg, &offset);
if (r < 0)
- return log_device_error_errno(dev, r, "Failed to parse '%s' as an integer: %m", optarg);
+ return log_device_error_errno(dev, r, "Failed to parse '%s' as an integer: %m", opts.arg);
if (offset < 0)
return log_device_error_errno(dev, SYNTHETIC_ERRNO(EINVAL), "Invalid offset %"PRIi64".", offset);
break;
- case 'R':
+
+ OPTION('R', "noraid", NULL, NULL):
noraid = true;
break;
}
- }
r = sd_device_get_devname(dev, &devnode);
if (r < 0)
diff --git a/src/udev/udev-builtin-hwdb.c b/src/udev/udev-builtin-hwdb.c
index 082af2e6031bd..dececd9c0377c 100644
--- a/src/udev/udev-builtin-hwdb.c
+++ b/src/udev/udev-builtin-hwdb.c
@@ -1,7 +1,6 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include
-#include
#include
#include "sd-hwdb.h"
@@ -9,6 +8,7 @@
#include "alloc-util.h"
#include "device-util.h"
#include "hwdb-util.h"
+#include "options.h"
#include "parse-util.h"
#include "string-util.h"
#include "udev-builtin.h"
@@ -128,13 +128,6 @@ static int udev_builtin_hwdb_search(
}
static int builtin_hwdb(UdevEvent *event, int argc, char *argv[]) {
- static const struct option options[] = {
- { "filter", required_argument, NULL, 'f' },
- { "device", required_argument, NULL, 'd' },
- { "subsystem", required_argument, NULL, 's' },
- { "lookup-prefix", required_argument, NULL, 'p' },
- {}
- };
const char *filter = NULL, *device = NULL, *subsystem = NULL, *prefix = NULL;
_cleanup_(sd_device_unrefp) sd_device *srcdev = NULL;
sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev);
@@ -143,35 +136,34 @@ static int builtin_hwdb(UdevEvent *event, int argc, char *argv[]) {
if (!hwdb)
return -EINVAL;
- for (;;) {
- int option;
+ OptionParser opts = { argc, argv, .namespace = "udev-builtin-hwdb" };
- option = getopt_long(argc, argv, "f:d:s:p:", options, NULL);
- if (option == -1)
- break;
+ FOREACH_OPTION_OR_RETURN(c, &opts)
+ switch (c) {
+
+ OPTION_NAMESPACE("udev-builtin-hwdb"): {}
- switch (option) {
- case 'f':
- filter = optarg;
+ OPTION('f', "filter", "FILTER", NULL):
+ filter = opts.arg;
break;
- case 'd':
- device = optarg;
+ OPTION('d', "device", "DEVICE", NULL):
+ device = opts.arg;
break;
- case 's':
- subsystem = optarg;
+ OPTION('s', "subsystem", "SUBSYSTEM", NULL):
+ subsystem = opts.arg;
break;
- case 'p':
- prefix = optarg;
+ OPTION('p', "lookup-prefix", "PREFIX", NULL):
+ prefix = opts.arg;
break;
}
- }
/* query a specific key given as argument */
- if (argv[optind]) {
- r = udev_builtin_hwdb_lookup(event, prefix, argv[optind], filter);
+ char *modalias = option_parser_get_arg(&opts, 0);
+ if (modalias) {
+ r = udev_builtin_hwdb_lookup(event, prefix, modalias, filter);
if (r < 0)
return log_device_debug_errno(dev, r, "Failed to look up hwdb: %m");
if (r == 0)
diff --git a/src/udev/udev-config.c b/src/udev/udev-config.c
index 17deadfe76071..541ba16dd906b 100644
--- a/src/udev/udev-config.c
+++ b/src/udev/udev-config.c
@@ -1,6 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
#include
#include "conf-parser.h"
@@ -8,10 +7,12 @@
#include "daemon-util.h"
#include "fd-util.h"
#include "fileio.h"
+#include "format-table.h"
#include "hashmap.h"
+#include "help-util.h"
#include "limits-util.h"
+#include "options.h"
#include "parse-util.h"
-#include "pretty-print.h"
#include "proc-cmdline.h"
#include "serialize.h"
#include "signal-util.h"
@@ -149,110 +150,91 @@ static int parse_proc_cmdline_item(const char *key, const char *value, void *dat
}
static int help(void) {
- _cleanup_free_ char *link = NULL;
+ _cleanup_(table_unrefp) Table *options = NULL;
int r;
- r = terminal_urlify_man("systemd-udevd.service", "8", &link);
+ r = option_parser_get_help_table_ns("udevd", &options);
if (r < 0)
- return log_oom();
-
- printf("%s [OPTIONS...]\n\n"
- "Rule-based manager for device events and files.\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -d --daemon Detach and run in the background\n"
- " -D --debug Enable debug output\n"
- " -c --children-max=INT Set maximum number of workers\n"
- " -e --exec-delay=SECONDS Seconds to wait before executing RUN=\n"
- " -t --event-timeout=SECONDS Seconds to wait before terminating an event\n"
- " -N --resolve-names=early|late|never\n"
- " When to resolve users and groups\n"
- "\nSee the %s for details.\n",
- program_invocation_short_name,
- link);
+ return r;
+
+ help_cmdline("[OPTIONS...]");
+ help_abstract("Rule-based manager for device events and files.");
+
+ help_section("Options:");
+
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("systemd-udevd.service", "8");
return 0;
}
static int parse_argv(int argc, char *argv[], UdevConfig *config) {
- enum {
- ARG_TIMEOUT_SIGNAL,
- };
-
- static const struct option options[] = {
- { "daemon", no_argument, NULL, 'd' },
- { "debug", no_argument, NULL, 'D' },
- { "children-max", required_argument, NULL, 'c' },
- { "exec-delay", required_argument, NULL, 'e' },
- { "event-timeout", required_argument, NULL, 't' },
- { "resolve-names", required_argument, NULL, 'N' },
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- { "timeout-signal", required_argument, NULL, ARG_TIMEOUT_SIGNAL },
- {}
- };
-
- int c, r;
+ int r;
assert(argc >= 0);
assert(argv);
assert(config);
- while ((c = getopt_long(argc, argv, "c:de:Dt:N:hV", options, NULL)) >= 0) {
+ OptionParser opts = { argc, argv, OPTION_PARSER_NORMAL, "udevd" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'd':
+ OPTION_NAMESPACE("udevd"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ printf("%s\n", GIT_VERSION);
+ return 0;
+
+ OPTION('d', "daemon", NULL, "Detach and run in the background"):
arg_daemonize = true;
break;
- case 'c':
- r = safe_atou(optarg, &config->children_max);
- if (r < 0)
- log_warning_errno(r, "Failed to parse --children-max= value '%s', ignoring: %m", optarg);
+
+ OPTION('D', "debug", NULL, "Enable debug output"):
+ arg_debug = true;
+ config->log_level = LOG_DEBUG;
break;
- case 'e':
- r = parse_sec(optarg, &config->exec_delay_usec);
+
+ OPTION('c', "children-max", "INT", "Set maximum number of workers"):
+ r = safe_atou(opts.arg, &config->children_max);
if (r < 0)
- log_warning_errno(r, "Failed to parse --exec-delay= value '%s', ignoring: %m", optarg);
+ log_warning_errno(r, "Failed to parse --children-max= value '%s', ignoring: %m", opts.arg);
break;
- case ARG_TIMEOUT_SIGNAL:
- r = signal_from_string(optarg);
- if (r <= 0)
- log_warning_errno(r, "Failed to parse --timeout-signal= value '%s', ignoring: %m", optarg);
- else
- config->timeout_signal = r;
- break;
- case 't':
- r = parse_sec(optarg, &config->timeout_usec);
+ OPTION('e', "exec-delay", "SECONDS", "Seconds to wait before executing RUN="):
+ r = parse_sec(opts.arg, &config->exec_delay_usec);
if (r < 0)
- log_warning_errno(r, "Failed to parse --event-timeout= value '%s', ignoring: %m", optarg);
+ log_warning_errno(r, "Failed to parse --exec-delay= value '%s', ignoring: %m", opts.arg);
break;
- case 'D':
- arg_debug = true;
- config->log_level = LOG_DEBUG;
+
+ OPTION('t', "event-timeout", "SECONDS", "Seconds to wait before terminating an event"):
+ r = parse_sec(opts.arg, &config->timeout_usec);
+ if (r < 0)
+ log_warning_errno(r, "Failed to parse --event-timeout= value '%s', ignoring: %m", opts.arg);
break;
- case 'N': {
- ResolveNameTiming t;
- t = resolve_name_timing_from_string(optarg);
+ OPTION_COMMON_RESOLVE_NAMES: {
+ ResolveNameTiming t = resolve_name_timing_from_string(opts.arg);
if (t < 0)
- log_warning("Invalid --resolve-names= value '%s', ignoring.", optarg);
+ log_warning("Invalid --resolve-names= value '%s', ignoring.", opts.arg);
else
config->resolve_name_timing = t;
break;
}
- case 'h':
- return help();
- case 'V':
- printf("%s\n", GIT_VERSION);
- return 0;
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
+ OPTION_LONG("timeout-signal", "SIGNAL", "Signal used when terminating an event"):
+ r = signal_from_string(opts.arg);
+ if (r <= 0)
+ log_warning_errno(r, "Failed to parse --timeout-signal= value '%s', ignoring: %m", opts.arg);
+ else
+ config->timeout_signal = r;
+ break;
}
- }
return 1;
}
diff --git a/src/udev/udevadm-cat.c b/src/udev/udevadm-cat.c
index 9d94f5a86c652..62d30d0234d24 100644
--- a/src/udev/udevadm-cat.c
+++ b/src/udev/udevadm-cat.c
@@ -1,14 +1,14 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
-
#include "alloc-util.h"
#include "conf-files.h"
+#include "format-table.h"
+#include "help-util.h"
#include "log.h"
+#include "options.h"
#include "parse-argument.h"
#include "pretty-print.h"
#include "static-destruct.h"
-#include "strv.h"
#include "udevadm.h"
#include "udevadm-util.h"
@@ -19,83 +19,75 @@ static bool arg_config = false;
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
static int help(void) {
- _cleanup_free_ char *link = NULL;
+ _cleanup_(table_unrefp) Table *options = NULL;
int r;
- r = terminal_urlify_man("udevadm", "8", &link);
+ r = option_parser_get_help_table_ns("udevadm-cat", &options);
if (r < 0)
- return log_oom();
-
- printf("%s cat [OPTIONS] [FILE...]\n"
- "\n%sShow udev rules files.%s\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " --root=PATH Operate on an alternate filesystem root\n"
- " --tldr Skip comments and empty lines\n"
- " --config Show udev.conf rather than udev rules files\n"
- "\nSee the %s for details.\n",
- program_invocation_short_name,
- ansi_highlight(),
- ansi_normal(),
- link);
+ return r;
+ help_cmdline("cat [OPTIONS...] [FILE...]");
+ help_abstract("Show udev rules files.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("udevadm", "8");
return 0;
}
-static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_ROOT = 0x100,
- ARG_TLDR,
- ARG_CONFIG,
- };
- static const struct option options[] = {
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- { "root", required_argument, NULL, ARG_ROOT },
- { "tldr", no_argument, NULL, ARG_TLDR },
- { "config", no_argument, NULL, ARG_CONFIG },
- {}
- };
-
- int r, c;
+static int parse_argv(int argc, char *argv[], char ***remaining_args) {
+ int r;
assert(argc >= 0);
assert(argv);
+ assert(remaining_args);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-cat" };
- while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'h':
+
+ OPTION_NAMESPACE("udevadm-cat"): {}
+
+ OPTION_COMMON_HELP:
return help();
- case 'V':
+
+ OPTION_COMMON_VERSION_WITH_HIDDEN_V:
return print_version();
- case ARG_ROOT:
- r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root);
+
+ OPTION_LONG("root", "PATH",
+ "Operate on an alternate filesystem root"):
+ r = parse_path_argument(opts.arg, /* suppress_root= */ true, &arg_root);
if (r < 0)
return r;
break;
- case ARG_TLDR:
+
+ OPTION_LONG("tldr", NULL,
+ "Skip comments and empty lines"):
arg_cat_flags = CAT_TLDR;
break;
- case ARG_CONFIG:
+
+ OPTION_LONG("config", NULL,
+ "Show udev.conf rather than udev rules files"):
arg_config = true;
break;
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
- if (arg_config && optind < argc)
+ if (arg_config && option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Combination of --config and FILEs is not supported.");
+ *remaining_args = option_parser_get_args(&opts);
return 1;
}
int verb_cat_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ char **args = NULL;
int r;
- r = parse_argv(argc, argv);
+ r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
@@ -107,7 +99,7 @@ int verb_cat_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
CLEANUP_ARRAY(files, n_files, conf_file_free_array);
- r = search_rules_files(strv_skip(argv, optind), arg_root, &files, &n_files);
+ r = search_rules_files(args, arg_root, &files, &n_files);
if (r < 0)
return r;
diff --git a/src/udev/udevadm-control.c b/src/udev/udevadm-control.c
index 964f721731ceb..a6ffe83cecaf6 100644
--- a/src/udev/udevadm-control.c
+++ b/src/udev/udevadm-control.c
@@ -1,12 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
-#include
#include
#include "creds-util.h"
#include "errno-util.h"
+#include "format-table.h"
+#include "help-util.h"
#include "log.h"
+#include "options.h"
#include "parse-argument.h"
#include "parse-util.h"
#include "static-destruct.h"
@@ -47,151 +48,121 @@ static bool arg_has_control_commands(void) {
}
static int help(void) {
- printf("%s control OPTION\n\n"
- "Control the udev daemon.\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -e --exit Instruct the daemon to cleanup and exit\n"
- " -l --log-level=LEVEL Set the udev log level for the daemon\n"
- " -s --stop-exec-queue Do not execute events, queue only\n"
- " -S --start-exec-queue Execute events, flush queue\n"
- " -R --reload Reload rules and databases\n"
- " -p --property=KEY=VALUE Set a global property for all events\n"
- " -m --children-max=N Maximum number of children\n"
- " --ping Wait for udev to respond to a ping message\n"
- " --trace=BOOL Enable/disable trace logging\n"
- " --revert Revert previously set configurations\n"
- " -t --timeout=SECONDS Maximum time to block for a reply\n"
- " --load-credentials Load udev rules from credentials\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-control", &options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("control OPTION");
+ help_abstract("Control the udev daemon.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_PING = 0x100,
- ARG_TRACE,
- ARG_REVERT,
- ARG_LOAD_CREDENTIALS,
- };
-
- static const struct option options[] = {
- { "exit", no_argument, NULL, 'e' },
- { "log-level", required_argument, NULL, 'l' },
- { "log-priority", required_argument, NULL, 'l' }, /* for backward compatibility */
- { "stop-exec-queue", no_argument, NULL, 's' },
- { "start-exec-queue", no_argument, NULL, 'S' },
- { "reload", no_argument, NULL, 'R' },
- { "reload-rules", no_argument, NULL, 'R' }, /* alias for -R */
- { "property", required_argument, NULL, 'p' },
- { "env", required_argument, NULL, 'p' }, /* alias for -p */
- { "children-max", required_argument, NULL, 'm' },
- { "ping", no_argument, NULL, ARG_PING },
- { "trace", required_argument, NULL, ARG_TRACE },
- { "revert", no_argument, NULL, ARG_REVERT },
- { "timeout", required_argument, NULL, 't' },
- { "load-credentials", no_argument, NULL, ARG_LOAD_CREDENTIALS },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- {}
- };
-
- int c, r;
+ int r;
assert(argc >= 0);
assert(argv);
- while ((c = getopt_long(argc, argv, "el:sSRp:m:t:Vh", options, NULL)) >= 0)
+ OptionParser opts = { argc, argv, .namespace = "udevadm-control" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'e':
+ OPTION_NAMESPACE("udevadm-control"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('e', "exit", NULL, "Instruct the daemon to cleanup and exit"):
arg_exit = true;
break;
- case 'l':
- arg_log_level = log_level_from_string(optarg);
+ OPTION_LONG("log-priority", "LEVEL", NULL): {} /* backward compat alias for --log-level */
+ OPTION('l', "log-level", "LEVEL", "Set the udev log level for the daemon"):
+ arg_log_level = log_level_from_string(opts.arg);
if (arg_log_level < 0)
- return log_error_errno(arg_log_level, "Failed to parse log level '%s': %m", optarg);
+ return log_error_errno(arg_log_level, "Failed to parse log level '%s': %m", opts.arg);
break;
- case 's':
+ OPTION('s', "stop-exec-queue", NULL, "Do not execute events, queue only"):
arg_start_exec_queue = false;
break;
- case 'S':
+ OPTION('S', "start-exec-queue", NULL, "Execute events, flush queue"):
arg_start_exec_queue = true;
break;
- case 'R':
+ OPTION_LONG("reload-rules", NULL, NULL): {} /* hidden alias for -R */
+ OPTION('R', "reload", NULL, "Reload rules and databases"):
arg_reload = true;
break;
- case 'p':
- if (!strchr(optarg, '='))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect = instead of '%s'", optarg);
+ OPTION_LONG("env", "KEY=VALUE", NULL): {} /* hidden alias for -p */
+ OPTION('p', "property", "KEY=VALUE", "Set a global property for all events"):
+ if (!strchr(opts.arg, '='))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "expect = instead of '%s'", opts.arg);
- r = strv_extend(&arg_env, optarg);
+ r = strv_extend(&arg_env, opts.arg);
if (r < 0)
return log_error_errno(r, "Failed to extend environment: %m");
break;
- case 'm': {
+ OPTION('m', "children-max", "N", "Maximum number of children"): {
unsigned i;
- r = safe_atou(optarg, &i);
+ r = safe_atou(opts.arg, &i);
if (r < 0)
- return log_error_errno(r, "Failed to parse maximum number of children '%s': %m", optarg);
+ return log_error_errno(r, "Failed to parse maximum number of children '%s': %m", opts.arg);
arg_max_children = i;
break;
}
- case ARG_PING:
+ OPTION_LONG("ping", NULL, "Wait for udev to respond to a ping message"):
arg_ping = true;
break;
- case ARG_TRACE:
- r = parse_boolean_argument("--trace=", optarg, NULL);
+ OPTION_LONG("trace", "BOOL", "Enable/disable trace logging"):
+ r = parse_boolean_argument("--trace=", opts.arg, NULL);
if (r < 0)
return r;
arg_trace = r;
break;
- case ARG_REVERT:
+ OPTION_LONG("revert", NULL, "Revert previously set configurations"):
arg_revert = true;
break;
- case 't':
- r = parse_sec(optarg, &arg_timeout);
+ OPTION('t', "timeout", "SECONDS", "Maximum time to block for a reply"):
+ r = parse_sec(opts.arg, &arg_timeout);
if (r < 0)
- return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg);
+ return log_error_errno(r, "Failed to parse timeout value '%s': %m", opts.arg);
break;
- case ARG_LOAD_CREDENTIALS:
+ OPTION_LONG("load-credentials", NULL, "Load udev rules from credentials"):
arg_load_credentials = true;
break;
-
- case 'V':
- return print_version();
-
- case 'h':
- return help();
-
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
}
if (!arg_has_control_commands() && !arg_load_credentials)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"No control command option is specified.");
- if (optind < argc)
+ if (option_parser_get_n_args(&opts) > 0)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Extraneous argument: %s", argv[optind]);
+ "This subprogram takes no positional arguments.");
return 1;
}
diff --git a/src/udev/udevadm-hwdb.c b/src/udev/udevadm-hwdb.c
index 5810efefd8ce2..b029db2262a04 100644
--- a/src/udev/udevadm-hwdb.c
+++ b/src/udev/udevadm-hwdb.c
@@ -1,10 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
-#include
#include
+#include "format-table.h"
+#include "help-util.h"
#include "hwdb-util.h"
#include "log.h"
+#include "options.h"
#include "udevadm.h"
static const char *arg_test = NULL;
@@ -14,65 +16,64 @@ static bool arg_update = false;
static bool arg_strict = false;
static int help(void) {
- printf("%s hwdb [OPTIONS]\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -u --update Update the hardware database\n"
- " -s --strict When updating, return non-zero exit value on any parsing error\n"
- " --usr Generate in " UDEVLIBEXECDIR " instead of /etc/udev\n"
- " -t --test=MODALIAS Query database and print result\n"
- " -r --root=PATH Alternative root path in the filesystem\n\n"
- "NOTE:\n"
- "The sub-command 'hwdb' is deprecated, and is left for backwards compatibility.\n"
- "Please use systemd-hwdb instead.\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-hwdb", &options);
+ if (r < 0)
+ return r;
+ help_cmdline("hwdb [OPTIONS]");
+ help_abstract("Update or query the hardware database.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ printf("\nNOTE:\n"
+ "The sub-command 'hwdb' is deprecated, and is left for backwards compatibility.\n"
+ "Please use systemd-hwdb instead.\n");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_USR = 0x100,
- };
-
- static const struct option options[] = {
- { "update", no_argument, NULL, 'u' },
- { "usr", no_argument, NULL, ARG_USR },
- { "strict", no_argument, NULL, 's' },
- { "test", required_argument, NULL, 't' },
- { "root", required_argument, NULL, 'r' },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- {}
- };
-
- int c;
-
- while ((c = getopt_long(argc, argv, "ust:r:Vh", options, NULL)) >= 0)
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-hwdb" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'u':
+
+ OPTION_NAMESPACE("udevadm-hwdb"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('u', "update", NULL, "Update the hardware database"):
arg_update = true;
break;
- case ARG_USR:
- arg_hwdb_bin_dir = UDEVLIBEXECDIR;
- break;
- case 's':
+
+ OPTION('s', "strict", NULL,
+ "When updating, return non-zero exit value on any parsing error"):
arg_strict = true;
break;
- case 't':
- arg_test = optarg;
+
+ OPTION_LONG("usr", NULL,
+ "Generate in " UDEVLIBEXECDIR " instead of /etc/udev"):
+ arg_hwdb_bin_dir = UDEVLIBEXECDIR;
+ break;
+
+ OPTION('t', "test", "MODALIAS", "Query database and print result"):
+ arg_test = opts.arg;
break;
- case 'r':
- arg_root = optarg;
+
+ OPTION('r', "root", "PATH", "Alternative root path in the filesystem"):
+ arg_root = opts.arg;
break;
- case 'V':
- return print_version();
- case 'h':
- return help();
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
return 1;
diff --git a/src/udev/udevadm-info.c b/src/udev/udevadm-info.c
index 62d7dce4217de..a5cbedc8deeda 100644
--- a/src/udev/udevadm-info.c
+++ b/src/udev/udevadm-info.c
@@ -2,7 +2,6 @@
#include
#include
-#include
#include
#include
#include
@@ -20,7 +19,10 @@
#include "errno-util.h"
#include "fd-util.h"
#include "fileio.h"
+#include "format-table.h"
#include "glyph-util.h"
+#include "help-util.h"
+#include "options.h"
#include "pager.h"
#include "parse-argument.h"
#include "sort-util.h"
@@ -800,51 +802,21 @@ static int query_device(QueryType query, sd_device* device) {
}
static int help(void) {
- printf("%s info [OPTIONS] [DEVPATH|FILE]\n\n"
- "Query sysfs or the udev database.\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -q --query=TYPE Query device information:\n"
- " name Name of device node\n"
- " symlink Pointing to node\n"
- " path sysfs device path\n"
- " property The device properties\n"
- " all All values\n"
- " --property=NAME Show only properties by this name\n"
- " --value When showing properties, print only their values\n"
- " -p --path=SYSPATH sysfs device path used for query or attribute walk\n"
- " -n --name=NAME Node or symlink name used for query or attribute walk\n"
- " -r --root Prepend dev directory to path names\n"
- " -a --attribute-walk Print all key matches walking along the chain\n"
- " of parent devices\n"
- " -t --tree Show tree of devices\n"
- " -d --device-id-of-file=FILE Print major:minor of device containing this file\n"
- " -x --export Export key/value pairs\n"
- " -P --export-prefix Export the key name with a prefix\n"
- " -e --export-db Export the content of the udev database\n"
- " -c --cleanup-db Clean up the udev database\n"
- " -w --wait-for-initialization[=SECONDS]\n"
- " Wait for device to be initialized\n"
- " --no-pager Do not pipe output into a pager\n"
- " --json=pretty|short|off Generate JSON output\n"
- " --subsystem-match=SUBSYSTEM\n"
- " Query devices matching a subsystem\n"
- " --subsystem-nomatch=SUBSYSTEM\n"
- " Query devices not matching a subsystem\n"
- " --attr-match=FILE[=VALUE]\n"
- " Query devices that match an attribute\n"
- " --attr-nomatch=FILE[=VALUE]\n"
- " Query devices that do not match an attribute\n"
- " --property-match=KEY=VALUE\n"
- " Query devices with matching properties\n"
- " --tag-match=TAG Query devices with a matching tag\n"
- " --sysname-match=NAME Query devices with this /sys path\n"
- " --name-match=NAME Query devices with this /dev name\n"
- " --parent-match=NAME Query devices with this parent device\n"
- " --initialized-match Query devices that are already initialized\n"
- " --initialized-nomatch Query devices that are not initialized yet\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-info", &options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("info [OPTIONS] [DEVPATH|FILE]");
+ help_abstract("Query sysfs or the udev database.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
@@ -1006,90 +978,64 @@ static int print_tree(sd_device* below) {
}
static int parse_argv(int argc, char *argv[]) {
-
- enum {
- ARG_PROPERTY = 0x100,
- ARG_VALUE,
- ARG_NO_PAGER,
- ARG_JSON,
- ARG_SUBSYSTEM_MATCH,
- ARG_SUBSYSTEM_NOMATCH,
- ARG_ATTR_MATCH,
- ARG_ATTR_NOMATCH,
- ARG_PROPERTY_MATCH,
- ARG_TAG_MATCH,
- ARG_SYSNAME_MATCH,
- ARG_NAME_MATCH,
- ARG_PARENT_MATCH,
- ARG_INITIALIZED_MATCH,
- ARG_INITIALIZED_NOMATCH,
- };
-
- static const struct option options[] = {
- { "attribute-walk", no_argument, NULL, 'a' },
- { "tree", no_argument, NULL, 't' },
- { "cleanup-db", no_argument, NULL, 'c' },
- { "device-id-of-file", required_argument, NULL, 'd' },
- { "export", no_argument, NULL, 'x' },
- { "export-db", no_argument, NULL, 'e' },
- { "export-prefix", required_argument, NULL, 'P' },
- { "help", no_argument, NULL, 'h' },
- { "name", required_argument, NULL, 'n' },
- { "path", required_argument, NULL, 'p' },
- { "property", required_argument, NULL, ARG_PROPERTY },
- { "query", required_argument, NULL, 'q' },
- { "root", no_argument, NULL, 'r' },
- { "value", no_argument, NULL, ARG_VALUE },
- { "version", no_argument, NULL, 'V' },
- { "wait-for-initialization", optional_argument, NULL, 'w' },
- { "no-pager", no_argument, NULL, ARG_NO_PAGER },
- { "json", required_argument, NULL, ARG_JSON },
- { "subsystem-match", required_argument, NULL, ARG_SUBSYSTEM_MATCH },
- { "subsystem-nomatch", required_argument, NULL, ARG_SUBSYSTEM_NOMATCH },
- { "attr-match", required_argument, NULL, ARG_ATTR_MATCH },
- { "attr-nomatch", required_argument, NULL, ARG_ATTR_NOMATCH },
- { "property-match", required_argument, NULL, ARG_PROPERTY_MATCH },
- { "tag-match", required_argument, NULL, ARG_TAG_MATCH },
- { "sysname-match", required_argument, NULL, ARG_SYSNAME_MATCH },
- { "name-match", required_argument, NULL, ARG_NAME_MATCH },
- { "parent-match", required_argument, NULL, ARG_PARENT_MATCH },
- { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH },
- { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH },
- {}
- };
-
- int c, r;
+ int r;
assert(argc >= 0);
assert(argv);
- while ((c = getopt_long(argc, argv, "atced:n:p:q:rxP:w::Vh", options, NULL)) >= 0)
+ OptionParser opts = { argc, argv, .namespace = "udevadm-info" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case ARG_PROPERTY:
+ OPTION_NAMESPACE("udevadm-info"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('q', "query", "TYPE", "Query device information:"): {}
+ OPTION_HELP_VERBATIM(" name", "- name of device node"): {}
+ OPTION_HELP_VERBATIM(" symlink", "- pointing to node"): {}
+ OPTION_HELP_VERBATIM(" path", "- sysfs device path"): {}
+ OPTION_HELP_VERBATIM(" property", "- the device properties"): {}
+ OPTION_HELP_VERBATIM(" all", "- all values"):
+ arg_query = query_type_from_string(opts.arg);
+ if (arg_query < 0) {
+ if (streq(opts.arg, "env")) /* deprecated */
+ arg_query = QUERY_PROPERTY;
+ else
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown query type '%s'", opts.arg);
+ }
+ break;
+
+ OPTION_LONG("property", "NAME", "Show only properties by this name"):
/* Make sure that if the empty property list was specified, we won't show any
properties. */
- if (isempty(optarg) && !arg_properties) {
+ if (isempty(opts.arg) && !arg_properties) {
arg_properties = new0(char*, 1);
if (!arg_properties)
return log_oom();
} else {
- r = strv_split_and_extend(&arg_properties, optarg, ",", true);
+ r = strv_split_and_extend(&arg_properties, opts.arg, ",", true);
if (r < 0)
return log_oom();
}
break;
- case ARG_VALUE:
+ OPTION_LONG("value", NULL,
+ "When showing properties, print only their values"):
arg_value = true;
break;
- case 'n':
- case 'p': {
- const char *prefix = c == 'n' ? "/dev/" : "/sys/";
+ OPTION('p', "path", "SYSPATH", "sysfs device path used for query or attribute walk"): {} /* fall through */
+ OPTION('n', "name", "NAME", "Node or symlink name used for query or attribute walk"): {
+ const char *prefix = opts.opt->short_code == 'n' ? "/dev/" : "/sys/";
char *path;
- path = path_join(path_startswith(optarg, prefix) ? NULL : prefix, optarg);
+ path = path_join(path_startswith(opts.arg, prefix) ? NULL : prefix, opts.arg);
if (!path)
return log_oom();
@@ -1099,159 +1045,147 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
- case 'q':
- arg_query = query_type_from_string(optarg);
- if (arg_query < 0) {
- if (streq(optarg, "env")) /* deprecated */
- arg_query = QUERY_PROPERTY;
- else
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown query type '%s'", optarg);
- }
+ OPTION('r', "root", NULL, "Prepend dev directory to path names"):
+ arg_root = true;
break;
- case 'r':
- arg_root = true;
+ OPTION('a', "attribute-walk", NULL,
+ "Print all key matches walking along the chain of parent devices"):
+ arg_action_type = ACTION_ATTRIBUTE_WALK;
+ break;
+
+ OPTION('t', "tree", NULL, "Show tree of devices"):
+ arg_action_type = ACTION_TREE;
break;
- case 'd':
+ OPTION('d', "device-id-of-file", "FILE",
+ "Print major:minor of device containing this file"):
arg_action_type = ACTION_DEVICE_ID_FILE;
- r = free_and_strdup(&arg_name, optarg);
+ r = free_and_strdup(&arg_name, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'a':
- arg_action_type = ACTION_ATTRIBUTE_WALK;
+ OPTION('x', "export", NULL, "Export key/value pairs"):
+ arg_export = true;
break;
- case 't':
- arg_action_type = ACTION_TREE;
+ OPTION('P', "export-prefix", "NAME", "Export the key name with a prefix"):
+ arg_export = true;
+ arg_export_prefix = opts.arg;
break;
- case 'e':
+ OPTION('e', "export-db", NULL, "Export the content of the udev database"):
arg_action_type = ACTION_EXPORT;
break;
- case 'c':
+ OPTION('c', "cleanup-db", NULL, "Clean up the udev database"):
arg_action_type = ACTION_CLEANUP_DB;
break;
- case 'x':
- arg_export = true;
- break;
-
- case 'P':
- arg_export = true;
- arg_export_prefix = optarg;
- break;
-
- case 'w':
- if (optarg) {
- r = parse_sec(optarg, &arg_wait_for_initialization_timeout);
+ OPTION_FULL(OPTION_OPTIONAL_ARG, 'w', "wait-for-initialization", "SECS",
+ "Wait for device to be initialized"):
+ if (opts.arg) {
+ r = parse_sec(opts.arg, &arg_wait_for_initialization_timeout);
if (r < 0)
return log_error_errno(r, "Failed to parse timeout value: %m");
} else
arg_wait_for_initialization_timeout = USEC_INFINITY;
break;
- case 'V':
- return print_version();
-
- case 'h':
- return help();
-
- case ARG_NO_PAGER:
+ OPTION_COMMON_NO_PAGER:
arg_pager_flags |= PAGER_DISABLE;
break;
- case ARG_JSON:
- r = parse_json_argument(optarg, &arg_json_format_flags);
+ OPTION_COMMON_JSON:
+ r = parse_json_argument(opts.arg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
- case ARG_SUBSYSTEM_MATCH:
- r = strv_extend(&arg_subsystem_match, optarg);
+ OPTION_LONG("subsystem-match", "SUBSYSTEM",
+ "Query devices matching a subsystem"):
+ r = strv_extend(&arg_subsystem_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_SUBSYSTEM_NOMATCH:
- r = strv_extend(&arg_subsystem_nomatch, optarg);
+ OPTION_LONG("subsystem-nomatch", "SUBSYSTEM",
+ "Query devices not matching a subsystem"):
+ r = strv_extend(&arg_subsystem_nomatch, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_ATTR_MATCH:
- if (!strchr(optarg, '='))
+ OPTION_LONG("attr-match", "FILE[=VALUE]",
+ "Query devices that match an attribute"):
+ if (!strchr(opts.arg, '='))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Expected = instead of '%s'", optarg);
+ "Expected = instead of '%s'", opts.arg);
- r = strv_extend(&arg_attr_match, optarg);
+ r = strv_extend(&arg_attr_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_ATTR_NOMATCH:
- if (!strchr(optarg, '='))
+ OPTION_LONG("attr-nomatch", "FILE[=VALUE]",
+ "Query devices that do not match an attribute"):
+ if (!strchr(opts.arg, '='))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Expected = instead of '%s'", optarg);
+ "Expected = instead of '%s'", opts.arg);
- r = strv_extend(&arg_attr_nomatch, optarg);
+ r = strv_extend(&arg_attr_nomatch, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_PROPERTY_MATCH:
- if (!strchr(optarg, '='))
+ OPTION_LONG("property-match", "KEY=VALUE",
+ "Query devices with matching properties"):
+ if (!strchr(opts.arg, '='))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
- "Expected = instead of '%s'", optarg);
+ "Expected = instead of '%s'", opts.arg);
- r = strv_extend(&arg_property_match, optarg);
+ r = strv_extend(&arg_property_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_TAG_MATCH:
- r = strv_extend(&arg_tag_match, optarg);
+ OPTION_LONG("tag-match", "TAG", "Query devices with a matching tag"):
+ r = strv_extend(&arg_tag_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_SYSNAME_MATCH:
- r = strv_extend(&arg_sysname_match, optarg);
+ OPTION_LONG("sysname-match", "NAME", "Query devices with this /sys path"):
+ r = strv_extend(&arg_sysname_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_NAME_MATCH:
- r = strv_extend(&arg_name_match, optarg);
+ OPTION_LONG("name-match", "NAME", "Query devices with this /dev name"):
+ r = strv_extend(&arg_name_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_PARENT_MATCH:
- r = strv_extend(&arg_parent_match, optarg);
+ OPTION_LONG("parent-match", "NAME", "Query devices with this parent device"):
+ r = strv_extend(&arg_parent_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_INITIALIZED_MATCH:
+ OPTION_LONG("initialized-match", NULL,
+ "Query devices that are already initialized"):
arg_initialized_match = MATCH_INITIALIZED_YES;
break;
- case ARG_INITIALIZED_NOMATCH:
+ OPTION_LONG("initialized-nomatch", NULL,
+ "Query devices that are not initialized yet"):
arg_initialized_match = MATCH_INITIALIZED_NO;
break;
-
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
}
- r = strv_extend_strv(&arg_devices, argv + optind, /* filter_duplicates= */ false);
+ r = strv_extend_strv(&arg_devices, option_parser_get_args(&opts), /* filter_duplicates= */ false);
if (r < 0)
return log_error_errno(r, "Failed to build argument list: %m");
diff --git a/src/udev/udevadm-lock.c b/src/udev/udevadm-lock.c
index 483b64973d401..c1c3211d34992 100644
--- a/src/udev/udevadm-lock.c
+++ b/src/udev/udevadm-lock.c
@@ -1,6 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
#include
#include
#include
@@ -9,11 +8,14 @@
#include "device-util.h"
#include "fd-util.h"
#include "fdset.h"
+#include "format-table.h"
+#include "glyph-util.h"
#include "hash-funcs.h"
+#include "help-util.h"
#include "lock-util.h"
+#include "options.h"
#include "path-util.h"
#include "pidref.h"
-#include "pretty-print.h"
#include "process-util.h"
#include "signal-util.h"
#include "sort-util.h"
@@ -33,70 +35,52 @@ STATIC_DESTRUCTOR_REGISTER(arg_backing, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_cmdline, strv_freep);
static int help(void) {
- _cleanup_free_ char *link = NULL;
+ _cleanup_(table_unrefp) Table *options = NULL;
int r;
- r = terminal_urlify_man("udevadm", "8", &link);
+ r = option_parser_get_help_table_ns("udevadm-lock", &options);
if (r < 0)
- return log_oom();
+ return r;
- printf("%s [OPTIONS...] COMMAND\n"
- "%s [OPTIONS...] --print\n"
- "\n%sLock a block device and run a command.%s\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -d --device=DEVICE Block device to lock\n"
- " -b --backing=FILE File whose backing block device to lock\n"
- " -t --timeout=SECS Block at most the specified time waiting for lock\n"
- " -p --print Only show which block device the lock would be taken on\n"
- "\nSee the %s for details.\n",
- program_invocation_short_name,
- program_invocation_short_name,
- ansi_highlight(),
- ansi_normal(),
- link);
+ help_cmdline("lock [OPTIONS...] COMMAND");
+ help_cmdline("lock [OPTIONS...] --print");
+ help_abstract("Lock a block device and run a command.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
-
- static const struct option options[] = {
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- { "device", required_argument, NULL, 'd' },
- { "backing", required_argument, NULL, 'b' },
- { "timeout", required_argument, NULL, 't' },
- { "print", no_argument, NULL, 'p' },
- {}
- };
-
- int c, r;
+ int r;
assert(argc >= 0);
assert(argv);
- /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
- * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
- optind = 0;
- while ((c = getopt_long(argc, argv, arg_print ? "hVd:b:t:p" : "+hVd:b:t:p", options, NULL)) >= 0)
+ OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION, "udevadm-lock" };
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'h':
+ OPTION_NAMESPACE("udevadm-lock"): {}
+
+ OPTION_COMMON_HELP:
return help();
- case 'V':
+ OPTION('V', "version", NULL, "Show package version"):
return print_version();
- case 'd':
- case 'b': {
+ OPTION('d', "device", "DEVICE", "Block device to lock"): {} /* fall through */
+ OPTION('b', "backing", "FILE", "File whose backing block device to lock"): {
_cleanup_free_ char *s = NULL;
- char ***l = c == 'd' ? &arg_devices : &arg_backing;
+ char ***l = opts.opt->short_code == 'd' ? &arg_devices : &arg_backing;
- r = path_make_absolute_cwd(optarg, &s);
+ r = path_make_absolute_cwd(opts.arg, &s);
if (r < 0)
- return log_error_errno(r, "Failed to make path '%s' absolute: %m", optarg);
+ return log_error_errno(r, "Failed to make path '%s' absolute: %m", opts.arg);
path_simplify(s);
@@ -107,31 +91,26 @@ static int parse_argv(int argc, char *argv[]) {
break;
}
- case 't':
- r = parse_sec(optarg, &arg_timeout_usec);
+ OPTION('t', "timeout", "SECS", "Block at most the specified time waiting for lock"):
+ r = parse_sec(opts.arg, &arg_timeout_usec);
if (r < 0)
- return log_error_errno(r, "Failed to parse --timeout= parameter: %s", optarg);
+ return log_error_errno(r, "Failed to parse --timeout= parameter: %s", opts.arg);
break;
- case 'p':
+ OPTION('p', "print", NULL, "Only show which block device the lock would be taken on"):
arg_print = true;
break;
-
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
}
+ char **args = option_parser_get_args(&opts);
if (arg_print) {
- if (optind != argc)
+ if (!strv_isempty(args))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No arguments expected.");
} else {
- if (optind + 1 > argc)
+ if (strv_isempty(args))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Too few arguments, command to execute.");
- arg_cmdline = strv_copy(argv + optind);
+ arg_cmdline = strv_copy(args);
if (!arg_cmdline)
return log_oom();
}
diff --git a/src/udev/udevadm-monitor.c b/src/udev/udevadm-monitor.c
index 6f33cc3710cca..c7d1f40fc49b6 100644
--- a/src/udev/udevadm-monitor.c
+++ b/src/udev/udevadm-monitor.c
@@ -1,7 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
-
#include "sd-device.h"
#include "sd-event.h"
@@ -9,8 +7,11 @@
#include "device-monitor-private.h"
#include "device-private.h"
#include "device-util.h"
+#include "format-table.h"
#include "format-util.h"
#include "hashmap.h"
+#include "help-util.h"
+#include "options.h"
#include "set.h"
#include "static-destruct.h"
#include "string-util.h"
@@ -99,60 +100,70 @@ static int setup_monitor(MonitorNetlinkGroup sender, sd_event *event, sd_device_
}
static int help(void) {
- printf("%s monitor [OPTIONS]\n\n"
- "Listen to kernel and udev events.\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -p --property Print the event properties\n"
- " -k --kernel Print kernel uevents\n"
- " -u --udev Print udev events\n"
- " -s --subsystem-match=SUBSYSTEM[/DEVTYPE] Filter events by subsystem\n"
- " -t --tag-match=TAG Filter events by tag\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+ r = option_parser_get_help_table_ns("udevadm-monitor", &options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("monitor [OPTIONS]");
+ help_abstract("Listen to kernel and udev events.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- static const struct option options[] = {
- { "property", no_argument, NULL, 'p' },
- { "environment", no_argument, NULL, 'e' }, /* alias for -p */
- { "kernel", no_argument, NULL, 'k' },
- { "udev", no_argument, NULL, 'u' },
- { "subsystem-match", required_argument, NULL, 's' },
- { "tag-match", required_argument, NULL, 't' },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- {}
- };
-
- int r, c;
-
- while ((c = getopt_long(argc, argv, "pekus:t:Vh", options, NULL)) >= 0)
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-monitor" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'p':
- case 'e':
+
+ OPTION_NAMESPACE("udevadm-monitor"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('e', "environment", NULL, /* help= */ NULL): {} /* hidden alias for -p */
+ OPTION('p', "property", NULL, "Print the event properties"):
arg_show_property = true;
break;
- case 'k':
+
+ OPTION('k', "kernel", NULL, "Print kernel uevents"):
arg_print_kernel = true;
break;
- case 'u':
+
+ OPTION('u', "udev", NULL, "Print udev events"):
arg_print_udev = true;
break;
- case 's': {
+
+ OPTION('s', "subsystem-match", "SUBSYSTEM[/DEVTYPE]",
+ "Filter events by subsystem"): {
_cleanup_free_ char *subsystem = NULL, *devtype = NULL;
const char *slash;
- slash = strchr(optarg, '/');
+ slash = strchr(opts.arg, '/');
if (slash) {
devtype = strdup(slash + 1);
if (!devtype)
return log_oom();
- subsystem = strndup(optarg, slash - optarg);
+ subsystem = strndup(opts.arg, slash - opts.arg);
} else
- subsystem = strdup(optarg);
+ subsystem = strdup(opts.arg);
if (!subsystem)
return log_oom();
@@ -165,20 +176,12 @@ static int parse_argv(int argc, char *argv[]) {
TAKE_PTR(devtype);
break;
}
- case 't':
- r = set_put_strdup(&arg_tag_filter, optarg);
+
+ OPTION('t', "tag-match", "TAG", "Filter events by tag"):
+ r = set_put_strdup(&arg_tag_filter, opts.arg);
if (r < 0)
return log_oom();
break;
-
- case 'V':
- return print_version();
- case 'h':
- return help();
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
if (!arg_print_kernel && !arg_print_udev) {
diff --git a/src/udev/udevadm-settle.c b/src/udev/udevadm-settle.c
index b71759dc818e6..211a8ff1fbf8c 100644
--- a/src/udev/udevadm-settle.c
+++ b/src/udev/udevadm-settle.c
@@ -4,7 +4,6 @@
* Copyright © 2009 Scott James Remnant
*/
-#include
#include
#include "sd-bus.h"
@@ -14,6 +13,9 @@
#include "alloc-util.h"
#include "bus-util.h"
+#include "format-table.h"
+#include "help-util.h"
+#include "options.h"
#include "path-util.h"
#include "string-util.h"
#include "strv.h"
@@ -28,60 +30,63 @@ static usec_t arg_timeout_usec = 120 * USEC_PER_SEC;
static const char *arg_exists = NULL;
static int help(void) {
- printf("%s settle [OPTIONS]\n\n"
- "Wait for pending udev events.\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -t --timeout=SEC Maximum time to wait for events\n"
- " -E --exit-if-exists=FILE Stop waiting if file exists\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-settle", &options);
+ if (r < 0)
+ return r;
+ help_cmdline("settle [OPTIONS]");
+ help_abstract("Wait for pending udev events.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- static const struct option options[] = {
- { "timeout", required_argument, NULL, 't' },
- { "exit-if-exists", required_argument, NULL, 'E' },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- { "seq-start", required_argument, NULL, 's' }, /* removed */
- { "seq-end", required_argument, NULL, 'e' }, /* removed */
- { "quiet", no_argument, NULL, 'q' }, /* removed */
- {}
- };
-
- int c, r;
-
- while ((c = getopt_long(argc, argv, "t:E:Vhs:e:q", options, NULL)) >= 0) {
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-settle" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 't':
- r = parse_sec(optarg, &arg_timeout_usec);
+
+ OPTION_NAMESPACE("udevadm-settle"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('t', "timeout", "SEC", "Maximum time to wait for events"):
+ r = parse_sec(opts.arg, &arg_timeout_usec);
if (r < 0)
- return log_error_errno(r, "Failed to parse timeout value '%s': %m", optarg);
+ return log_error_errno(r, "Failed to parse timeout value '%s': %m", opts.arg);
break;
- case 'E':
- if (!path_is_valid(optarg))
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path: %s", optarg);
- arg_exists = optarg;
+ OPTION('E', "exit-if-exists", "FILE", "Stop waiting if file exists"):
+ if (!path_is_valid(opts.arg))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid path: %s", opts.arg);
+
+ arg_exists = opts.arg;
break;
- case 'V':
- return print_version();
- case 'h':
- return help();
- case 's':
- case 'e':
- case 'q':
+
+ OPTION('s', "seq-start", "ARG", NULL): {} /* removed */
+ OPTION('e', "seq-end", "ARG", NULL): {} /* removed */
+ OPTION('q', "quiet", NULL, NULL): /* removed */
return log_info_errno(SYNTHETIC_ERRNO(EINVAL),
"Option -%c no longer supported.",
- c);
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
+ opts.opt->short_code);
}
- }
return 1;
}
diff --git a/src/udev/udevadm-test-builtin.c b/src/udev/udevadm-test-builtin.c
index f17df9a7d51a2..9c0082800f37a 100644
--- a/src/udev/udevadm-test-builtin.c
+++ b/src/udev/udevadm-test-builtin.c
@@ -1,11 +1,11 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
-#include
-
#include "device-private.h"
#include "device-util.h"
+#include "format-table.h"
+#include "help-util.h"
#include "log.h"
+#include "options.h"
#include "udev-builtin.h"
#include "udevadm.h"
#include "udevadm-util.h"
@@ -15,51 +15,57 @@ static const char *arg_command = NULL;
static const char *arg_syspath = NULL;
static int help(void) {
- printf("%s test-builtin [OPTIONS] COMMAND DEVPATH\n\n"
- "Test a built-in command.\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -a --action=ACTION|help Set action string\n"
- "\nCommands:\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
- udev_builtin_list();
+ r = option_parser_get_help_table_ns("udevadm-test-builtin", &options);
+ if (r < 0)
+ return r;
+ help_cmdline("test-builtin [OPTIONS] COMMAND DEVPATH");
+ help_abstract("Test a built-in command.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+
+ help_section("Commands:");
+ udev_builtin_list();
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- static const struct option options[] = {
- { "action", required_argument, NULL, 'a' },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- {}
- };
+ int r;
- int r, c;
+ assert(argc >= 0);
+ assert(argv);
- while ((c = getopt_long(argc, argv, "a:Vh", options, NULL)) >= 0)
+ OptionParser opts = { argc, argv, .namespace = "udevadm-test-builtin" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'a':
- r = parse_device_action(optarg, &arg_action);
+
+ OPTION_NAMESPACE("udevadm-test-builtin"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('a', "action", "ACTION|help", "Set action string"):
+ r = parse_device_action(opts.arg, &arg_action);
if (r <= 0)
return r;
break;
- case 'V':
- return print_version();
- case 'h':
- return help();
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
- if (argc != optind + 2)
+ if (option_parser_get_n_args(&opts) != 2)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Expected two arguments: command string and device path.");
- arg_command = ASSERT_PTR(argv[optind]);
- arg_syspath = ASSERT_PTR(argv[optind+1]);
+ char **args = option_parser_get_args(&opts);
+ arg_command = ASSERT_PTR(args[0]);
+ arg_syspath = ASSERT_PTR(args[1]);
return 1;
}
diff --git a/src/udev/udevadm-test.c b/src/udev/udevadm-test.c
index f3ac39717e946..ac368e0f00eec 100644
--- a/src/udev/udevadm-test.c
+++ b/src/udev/udevadm-test.c
@@ -3,7 +3,6 @@
* Copyright © 2003-2004 Greg Kroah-Hartman
*/
-#include
#include
#include
@@ -12,7 +11,10 @@
#include "alloc-util.h"
#include "device-private.h"
+#include "format-table.h"
+#include "help-util.h"
#include "log.h"
+#include "options.h"
#include "parse-argument.h"
#include "static-destruct.h"
#include "strv.h"
@@ -33,55 +35,59 @@ static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF;
STATIC_DESTRUCTOR_REGISTER(arg_extra_rules_dir, strv_freep);
static int help(void) {
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-test", &options);
+ if (r < 0)
+ return r;
- printf("%s test [OPTIONS] DEVPATH\n\n"
- "Test an event run.\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -a --action=ACTION|help Set action string\n"
- " -N --resolve-names=early|late|never When to resolve names\n"
- " -D --extra-rules-dir=DIR Also load rules from the directory\n"
- " -v --verbose Show verbose logs\n"
- " --json=pretty|short|off Generate JSON output\n",
- program_invocation_short_name);
+ help_cmdline("test [OPTIONS] DEVPATH");
+ help_abstract("Test an event run.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_JSON = 0x100,
- };
-
- static const struct option options[] = {
- { "action", required_argument, NULL, 'a' },
- { "resolve-names", required_argument, NULL, 'N' },
- { "extra-rules-dir", required_argument, NULL, 'D' },
- { "verbose", no_argument, NULL, 'v' },
- { "json", required_argument, NULL, ARG_JSON },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- {}
- };
-
- int r, c;
-
- while ((c = getopt_long(argc, argv, "a:N:D:vVh", options, NULL)) >= 0)
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-test" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'a':
- r = parse_device_action(optarg, &arg_action);
+
+ OPTION_NAMESPACE("udevadm-test"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('a', "action", "ACTION|help", "Set action string"):
+ r = parse_device_action(opts.arg, &arg_action);
if (r <= 0)
return r;
break;
- case 'N':
- r = parse_resolve_name_timing(optarg, &arg_resolve_name_timing);
+
+ OPTION_COMMON_RESOLVE_NAMES:
+ r = parse_resolve_name_timing(opts.arg, &arg_resolve_name_timing);
if (r <= 0)
return r;
break;
- case 'D': {
+
+ OPTION('D', "extra-rules-dir", "DIR", "Also load rules from the directory"): {
_cleanup_free_ char *p = NULL;
- r = parse_path_argument(optarg, /* suppress_root= */ false, &p);
+ r = parse_path_argument(opts.arg, /* suppress_root= */ false, &p);
if (r < 0)
return r;
@@ -90,25 +96,20 @@ static int parse_argv(int argc, char *argv[]) {
return log_oom();
break;
}
- case 'v':
+
+ OPTION('v', "verbose", NULL, "Show verbose logs"):
arg_verbose = true;
break;
- case ARG_JSON:
- r = parse_json_argument(optarg, &arg_json_format_flags);
+
+ OPTION_COMMON_JSON:
+ r = parse_json_argument(opts.arg, &arg_json_format_flags);
if (r <= 0)
return r;
break;
- case 'V':
- return print_version();
- case 'h':
- return help();
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
- arg_syspath = argv[optind];
+ char **args = option_parser_get_args(&opts);
+ arg_syspath = args[0];
if (!arg_syspath)
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "syspath parameter missing.");
diff --git a/src/udev/udevadm-trigger.c b/src/udev/udevadm-trigger.c
index afa6a84262084..583d85be0b8d8 100644
--- a/src/udev/udevadm-trigger.c
+++ b/src/udev/udevadm-trigger.c
@@ -1,6 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
#include
#include "sd-device.h"
@@ -10,7 +9,10 @@
#include "device-enumerator-private.h"
#include "device-private.h"
#include "device-util.h"
+#include "format-table.h"
+#include "help-util.h"
#include "id128-util.h"
+#include "options.h"
#include "set.h"
#include "static-destruct.h"
#include "string-table.h"
@@ -320,214 +322,170 @@ static int setup_matches(sd_device_enumerator *e) {
}
static int help(void) {
- printf("%s trigger [OPTIONS] DEVPATH\n\n"
- "Request events from the kernel.\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -v --verbose Print the list of devices while running\n"
- " -n --dry-run Do not actually trigger the events\n"
- " -q --quiet Suppress error logging in triggering events\n"
- " -t --type= Type of events to trigger\n"
- " devices sysfs devices (default)\n"
- " subsystems sysfs subsystems and drivers\n"
- " all sysfs devices, subsystems, and drivers\n"
- " -c --action=ACTION|help Event action value, default is \"change\"\n"
- " -s --subsystem-match=SUBSYSTEM Trigger devices from a matching subsystem\n"
- " -S --subsystem-nomatch=SUBSYSTEM Exclude devices from a matching subsystem\n"
- " -a --attr-match=FILE[=VALUE] Trigger devices with a matching attribute\n"
- " -A --attr-nomatch=FILE[=VALUE] Exclude devices with a matching attribute\n"
- " -p --property-match=KEY=VALUE Trigger devices with a matching property\n"
- " -g --tag-match=TAG Trigger devices with a matching tag\n"
- " -y --sysname-match=NAME Trigger devices with this /sys path\n"
- " --name-match=NAME Trigger devices with this /dev name\n"
- " -b --parent-match=NAME Trigger devices with that parent device\n"
- " --include-parents Trigger parent devices of found devices\n"
- " --initialized-match Trigger devices that are already initialized\n"
- " --initialized-nomatch Trigger devices that are not initialized yet\n"
- " -w --settle Wait for the triggered events to complete\n"
- " --wait-daemon[=SECONDS] Wait for udevd daemon to be initialized\n"
- " before triggering uevents\n"
- " --uuid Print synthetic uevent UUID\n"
- " --prioritized-subsystem=SUBSYSTEM[,SUBSYSTEM…]\n"
- " Trigger devices from a matching subsystem first\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-trigger", &options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("trigger [OPTIONS] DEVPATH");
+ help_abstract("Request events from the kernel.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_NAME = 0x100,
- ARG_PING,
- ARG_UUID,
- ARG_PRIORITIZED_SUBSYSTEM,
- ARG_INITIALIZED_MATCH,
- ARG_INITIALIZED_NOMATCH,
- ARG_INCLUDE_PARENTS,
- };
-
- static const struct option options[] = {
- { "verbose", no_argument, NULL, 'v' },
- { "dry-run", no_argument, NULL, 'n' },
- { "quiet", no_argument, NULL, 'q' },
- { "type", required_argument, NULL, 't' },
- { "action", required_argument, NULL, 'c' },
- { "subsystem-match", required_argument, NULL, 's' },
- { "subsystem-nomatch", required_argument, NULL, 'S' },
- { "attr-match", required_argument, NULL, 'a' },
- { "attr-nomatch", required_argument, NULL, 'A' },
- { "property-match", required_argument, NULL, 'p' },
- { "tag-match", required_argument, NULL, 'g' },
- { "sysname-match", required_argument, NULL, 'y' },
- { "name-match", required_argument, NULL, ARG_NAME },
- { "parent-match", required_argument, NULL, 'b' },
- { "include-parents", no_argument, NULL, ARG_INCLUDE_PARENTS },
- { "initialized-match", no_argument, NULL, ARG_INITIALIZED_MATCH },
- { "initialized-nomatch", no_argument, NULL, ARG_INITIALIZED_NOMATCH },
- { "settle", no_argument, NULL, 'w' },
- { "wait-daemon", optional_argument, NULL, ARG_PING },
- { "version", no_argument, NULL, 'V' },
- { "help", no_argument, NULL, 'h' },
- { "uuid", no_argument, NULL, ARG_UUID },
- { "prioritized-subsystem", required_argument, NULL, ARG_PRIORITIZED_SUBSYSTEM },
- {}
- };
-
- int c, r;
+ int r;
assert(argc >= 0);
assert(argv);
- while ((c = getopt_long(argc, argv, "vnqt:c:s:S:a:A:p:g:y:b:wVh", options, NULL)) >= 0) {
+ OptionParser opts = { argc, argv, .namespace = "udevadm-trigger" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'v':
+
+ OPTION_NAMESPACE("udevadm-trigger"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('v', "verbose", NULL, "Print the list of devices while running"):
arg_verbose = true;
break;
- case 'n':
+ OPTION('n', "dry-run", NULL, "Do not actually trigger the events"):
arg_dry_run = true;
break;
- case 'q':
+ OPTION('q', "quiet", NULL, "Suppress error logging in triggering events"):
arg_quiet = true;
break;
- case 't':
- arg_scan_type = scan_type_from_string(optarg);
+ OPTION('t', "type", "TYPE", "Type of sysfs events to trigger:"): {}
+ OPTION_HELP_VERBATIM(" devices", "- devices (default)"): {}
+ OPTION_HELP_VERBATIM(" subsystems", "- subsystems and drivers"): {}
+ OPTION_HELP_VERBATIM(" all", "- devices, subsystems, and drivers"):
+ arg_scan_type = scan_type_from_string(opts.arg);
if (arg_scan_type < 0)
- return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", optarg);
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Unknown type --type=%s", opts.arg);
break;
- case 'c':
- r = parse_device_action(optarg, &arg_action);
+ OPTION('c', "action", "ACTION|help", "Event action value, default is \"change\""):
+ r = parse_device_action(opts.arg, &arg_action);
if (r <= 0)
return r;
break;
- case 's':
- r = strv_extend(&arg_subsystem_match, optarg);
+ OPTION('s', "subsystem-match", "SUBSYSTEM",
+ "Trigger devices from a matching subsystem"):
+ r = strv_extend(&arg_subsystem_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'S':
- r = strv_extend(&arg_subsystem_nomatch, optarg);
+ OPTION('S', "subsystem-nomatch", "SUBSYSTEM",
+ "Exclude devices from a matching subsystem"):
+ r = strv_extend(&arg_subsystem_nomatch, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'a':
- r = strv_extend(&arg_attr_match, optarg);
+ OPTION('a', "attr-match", "FILE[=VALUE]",
+ "Trigger devices with a matching attribute"):
+ r = strv_extend(&arg_attr_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'A':
- r = strv_extend(&arg_attr_nomatch, optarg);
+ OPTION('A', "attr-nomatch", "FILE[=VALUE]",
+ "Exclude devices with a matching attribute"):
+ r = strv_extend(&arg_attr_nomatch, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'p':
- r = strv_extend(&arg_property_match, optarg);
+ OPTION('p', "property-match", "KEY=VALUE",
+ "Trigger devices with a matching property"):
+ r = strv_extend(&arg_property_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'g':
- r = strv_extend(&arg_tag_match, optarg);
+ OPTION('g', "tag-match", "TAG", "Trigger devices with a matching tag"):
+ r = strv_extend(&arg_tag_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'y':
- r = strv_extend(&arg_sysname_match, optarg);
+ OPTION('y', "sysname-match", "NAME", "Trigger devices with this /sys path"):
+ r = strv_extend(&arg_sysname_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case 'b':
- r = strv_extend(&arg_parent_match, optarg);
+ OPTION_LONG("name-match", "NAME", "Trigger devices with this /dev name"):
+ r = strv_extend(&arg_name_match, opts.arg);
if (r < 0)
return log_oom();
break;
- case ARG_INCLUDE_PARENTS:
+ OPTION('b', "parent-match", "NAME", "Trigger devices with that parent device"):
+ r = strv_extend(&arg_parent_match, opts.arg);
+ if (r < 0)
+ return log_oom();
+ break;
+
+ OPTION_LONG("include-parents", NULL, "Trigger parent devices of found devices"):
arg_include_parents = true;
break;
- case 'w':
- arg_settle = true;
+ OPTION_LONG("initialized-match", NULL,
+ "Trigger devices that are already initialized"):
+ arg_initialized_match = MATCH_INITIALIZED_YES;
break;
- case ARG_NAME:
- r = strv_extend(&arg_name_match, optarg);
- if (r < 0)
- return log_oom();
+ OPTION_LONG("initialized-nomatch", NULL,
+ "Trigger devices that are not initialized yet"):
+ arg_initialized_match = MATCH_INITIALIZED_NO;
+ break;
+
+ OPTION('w', "settle", NULL, "Wait for the triggered events to complete"):
+ arg_settle = true;
break;
- case ARG_PING:
+ OPTION_LONG_FLAGS(OPTION_OPTIONAL_ARG, "wait-daemon", "SECONDS",
+ "Wait for udevd daemon to be initialized before triggering uevents"):
arg_ping = true;
- if (optarg) {
- r = parse_sec(optarg, &arg_ping_timeout_usec);
+ if (opts.arg) {
+ r = parse_sec(opts.arg, &arg_ping_timeout_usec);
if (r < 0)
- log_error_errno(r, "Failed to parse timeout value '%s', ignoring: %m", optarg);
+ log_error_errno(r, "Failed to parse timeout value '%s', ignoring: %m", opts.arg);
}
break;
- case ARG_UUID:
+ OPTION_LONG("uuid", NULL, "Print synthetic uevent UUID"):
arg_uuid = true;
break;
- case ARG_PRIORITIZED_SUBSYSTEM:
- r = strv_split_and_extend(&arg_prioritized_subsystems, optarg, ",", /* filter_duplicates= */ false);
+ OPTION_LONG("prioritized-subsystem", "SUBSYSTEM[,SUBSYSTEM…]",
+ "Trigger devices from a matching subsystem first"):
+ r = strv_split_and_extend(&arg_prioritized_subsystems, opts.arg, ",", /* filter_duplicates= */ false);
if (r < 0)
return log_oom();
break;
-
- case ARG_INITIALIZED_MATCH:
- arg_initialized_match = MATCH_INITIALIZED_YES;
- break;
-
- case ARG_INITIALIZED_NOMATCH:
- arg_initialized_match = MATCH_INITIALIZED_NO;
- break;
-
- case 'V':
- return print_version();
-
- case 'h':
- return help();
-
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
}
- }
- r = strv_extend_strv(&arg_devices, argv + optind, /* filter_duplicates= */ false);
+ r = strv_extend_strv(&arg_devices, option_parser_get_args(&opts), /* filter_duplicates= */ false);
if (r < 0)
return log_error_errno(r, "Failed to build argument list: %m");
diff --git a/src/udev/udevadm-util.c b/src/udev/udevadm-util.c
index c30af47ff7c73..7e2420a77e8a9 100644
--- a/src/udev/udevadm-util.c
+++ b/src/udev/udevadm-util.c
@@ -144,7 +144,7 @@ int parse_resolve_name_timing(const char *str, ResolveNameTiming *ret) {
if (streq(str, "help"))
return DUMP_STRING_TABLE(resolve_name_timing, ResolveNameTiming, _RESOLVE_NAME_TIMING_MAX);
- ResolveNameTiming v = resolve_name_timing_from_string(optarg);
+ ResolveNameTiming v = resolve_name_timing_from_string(str);
if (v < 0)
return log_error_errno(v, "--resolve-names= must be 'early', 'late', or 'never'.");
diff --git a/src/udev/udevadm-verify.c b/src/udev/udevadm-verify.c
index 6af7f06ab05fe..f4388f843adc6 100644
--- a/src/udev/udevadm-verify.c
+++ b/src/udev/udevadm-verify.c
@@ -1,16 +1,17 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
#include
#include "alloc-util.h"
+#include "ansi-color.h"
#include "conf-files.h"
#include "errno-util.h"
+#include "format-table.h"
+#include "help-util.h"
#include "log.h"
+#include "options.h"
#include "parse-argument.h"
-#include "pretty-print.h"
#include "static-destruct.h"
-#include "strv.h"
#include "udev-rules.h"
#include "udevadm.h"
#include "udevadm-util.h"
@@ -23,81 +24,66 @@ static bool arg_style = true;
STATIC_DESTRUCTOR_REGISTER(arg_root, freep);
static int help(void) {
- _cleanup_free_ char *link = NULL;
+ _cleanup_(table_unrefp) Table *options = NULL;
int r;
- r = terminal_urlify_man("udevadm", "8", &link);
+ r = option_parser_get_help_table_ns("udevadm-verify", &options);
if (r < 0)
- return log_oom();
-
- printf("%s verify [OPTIONS] [FILE...]\n"
- "\n%sVerify udev rules files.%s\n\n"
- " -h --help Show this help\n"
- " -V --version Show package version\n"
- " -N --resolve-names=early|late|never When to resolve names\n"
- " --root=PATH Operate on an alternate filesystem root\n"
- " --no-summary Do not show summary\n"
- " --no-style Ignore style issues\n"
- "\nSee the %s for details.\n",
- program_invocation_short_name,
- ansi_highlight(),
- ansi_normal(),
- link);
+ return r;
+
+ help_cmdline("verify [OPTIONS] [FILE...]");
+ help_abstract("Verify udev rules files.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
-static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_ROOT = 0x100,
- ARG_NO_SUMMARY,
- ARG_NO_STYLE,
- };
- static const struct option options[] = {
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- { "resolve-names", required_argument, NULL, 'N' },
- { "root", required_argument, NULL, ARG_ROOT },
- { "no-summary", no_argument, NULL, ARG_NO_SUMMARY },
- { "no-style", no_argument, NULL, ARG_NO_STYLE },
- {}
- };
-
- int r, c;
+static int parse_argv(int argc, char *argv[], char ***remaining_args) {
+ int r;
assert(argc >= 0);
assert(argv);
+ assert(remaining_args);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-verify" };
- while ((c = getopt_long(argc, argv, "hVN:", options, NULL)) >= 0)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'h':
+
+ OPTION_NAMESPACE("udevadm-verify"): {}
+
+ OPTION_COMMON_HELP:
return help();
- case 'V':
+
+ OPTION('V', "version", NULL, "Show package version"):
return print_version();
- case 'N':
- r = parse_resolve_name_timing(optarg, &arg_resolve_name_timing);
+
+ OPTION_COMMON_RESOLVE_NAMES:
+ r = parse_resolve_name_timing(opts.arg, &arg_resolve_name_timing);
if (r <= 0)
return r;
break;
- case ARG_ROOT:
- r = parse_path_argument(optarg, /* suppress_root= */ true, &arg_root);
+
+ OPTION_LONG("root", "PATH", "Operate on an alternate filesystem root"):
+ r = parse_path_argument(opts.arg, /* suppress_root= */ true, &arg_root);
if (r < 0)
return r;
break;
- case ARG_NO_SUMMARY:
+
+ OPTION_LONG("no-summary", NULL, "Do not show summary"):
arg_summary = false;
break;
- case ARG_NO_STYLE:
+ OPTION_LONG("no-style", NULL, "Ignore style issues"):
arg_style = false;
break;
-
- case '?':
- return -EINVAL;
- default:
- assert_not_reached();
}
+ *remaining_args = option_parser_get_args(&opts);
return 1;
}
@@ -158,9 +144,10 @@ static int verify_rules(UdevRules *rules, ConfFile * const *files, size_t n_file
int verb_verify_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
_cleanup_(udev_rules_freep) UdevRules *rules = NULL;
+ char **args = NULL;
int r;
- r = parse_argv(argc, argv);
+ r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
@@ -173,7 +160,7 @@ int verb_verify_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
CLEANUP_ARRAY(files, n_files, conf_file_free_array);
- r = search_rules_files(strv_skip(argv, optind), arg_root, &files, &n_files);
+ r = search_rules_files(args, arg_root, &files, &n_files);
if (r < 0)
return r;
diff --git a/src/udev/udevadm-wait.c b/src/udev/udevadm-wait.c
index 0e285fc36b247..6017401440689 100644
--- a/src/udev/udevadm-wait.c
+++ b/src/udev/udevadm-wait.c
@@ -1,7 +1,5 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
-#include
#include
#include
@@ -10,7 +8,10 @@
#include "device-monitor-private.h"
#include "device-util.h"
#include "event-util.h"
+#include "format-table.h"
#include "fs-util.h"
+#include "help-util.h"
+#include "options.h"
#include "parse-util.h"
#include "path-util.h"
#include "static-destruct.h"
@@ -297,79 +298,72 @@ static int setup_periodic_timer(sd_event *event) {
}
static int help(void) {
- printf("%s wait [OPTIONS] DEVICE [DEVICE…]\n\n"
- "Wait for devices or device symlinks being created.\n\n"
- " -h --help Print this message\n"
- " -V --version Print version of the program\n"
- " -t --timeout=SEC Maximum time to wait for the device\n"
- " --initialized=BOOL Wait for devices being initialized by systemd-udevd\n"
- " --removed Wait for devices being removed\n"
- " --settle Also wait for all queued events being processed\n",
- program_invocation_short_name);
+ _cleanup_(table_unrefp) Table *options = NULL;
+ int r;
+
+ r = option_parser_get_help_table_ns("udevadm-wait", &options);
+ if (r < 0)
+ return r;
+
+ help_cmdline("wait [OPTIONS] DEVICE [DEVICE…]");
+ help_abstract("Wait for devices or device symlinks being created.");
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
+ help_man_page_reference("udevadm", "8");
return 0;
}
static int parse_argv(int argc, char *argv[]) {
- enum {
- ARG_INITIALIZED = 0x100,
- ARG_REMOVED,
- ARG_SETTLE,
- };
-
- static const struct option options[] = {
- { "timeout", required_argument, NULL, 't' },
- { "initialized", required_argument, NULL, ARG_INITIALIZED },
- { "removed", no_argument, NULL, ARG_REMOVED },
- { "settle", no_argument, NULL, ARG_SETTLE },
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- {}
- };
-
- int c, r;
-
- while ((c = getopt_long(argc, argv, "t:hV", options, NULL)) >= 0)
+ int r;
+
+ assert(argc >= 0);
+ assert(argv);
+
+ OptionParser opts = { argc, argv, .namespace = "udevadm-wait" };
+
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 't':
- r = parse_sec(optarg, &arg_timeout_usec);
+
+ OPTION_NAMESPACE("udevadm-wait"): {}
+
+ OPTION_COMMON_HELP:
+ return help();
+
+ OPTION('V', "version", NULL, "Show package version"):
+ return print_version();
+
+ OPTION('t', "timeout", "SEC", "Maximum time to wait for the device"):
+ r = parse_sec(opts.arg, &arg_timeout_usec);
if (r < 0)
- return log_error_errno(r, "Failed to parse -t/--timeout= parameter: %s", optarg);
+ return log_error_errno(r, "Failed to parse -t/--timeout= parameter: %s", opts.arg);
break;
- case ARG_INITIALIZED:
- r = parse_boolean(optarg);
+ OPTION_LONG("initialized", "BOOL",
+ "Wait for devices being initialized by systemd-udevd"):
+ r = parse_boolean(opts.arg);
if (r < 0)
- return log_error_errno(r, "Failed to parse --initialized= parameter: %s", optarg);
+ return log_error_errno(r, "Failed to parse --initialized= parameter: %s", opts.arg);
arg_wait_until = r ? WAIT_UNTIL_INITIALIZED : WAIT_UNTIL_ADDED;
break;
- case ARG_REMOVED:
+ OPTION_LONG("removed", NULL, "Wait for devices being removed"):
arg_wait_until = WAIT_UNTIL_REMOVED;
break;
- case ARG_SETTLE:
+ OPTION_LONG("settle", NULL, "Also wait for all queued events being processed"):
arg_settle = true;
break;
-
- case 'V':
- return print_version();
-
- case 'h':
- return help();
-
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
}
- if (optind >= argc)
+ char **args = option_parser_get_args(&opts);
+ if (strv_isempty(args))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"Too few arguments, expected at least one device path or device symlink.");
- arg_devices = strv_copy(argv + optind);
+ arg_devices = strv_copy(args);
if (!arg_devices)
return log_oom();
diff --git a/src/udev/udevadm.c b/src/udev/udevadm.c
index 70ff213cb9999..47d4335baec7f 100644
--- a/src/udev/udevadm.c
+++ b/src/udev/udevadm.c
@@ -1,87 +1,93 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
-#include
#include
-#include "alloc-util.h"
#include "argv-util.h"
+#include "format-table.h"
+#include "help-util.h"
#include "label-util.h"
#include "main-func.h"
-#include "pretty-print.h"
+#include "options.h"
#include "udev-util.h"
#include "udevadm.h"
#include "udevd.h"
#include "verbs.h"
static int help(void) {
- static const char *const short_descriptions[][2] = {
- { "info", "Query sysfs or the udev database" },
- { "trigger", "Request events from the kernel" },
- { "settle", "Wait for pending udev events" },
- { "control", "Control the udev daemon" },
- { "monitor", "Listen to kernel and udev events" },
- { "test", "Test an event run" },
- { "test-builtin", "Test a built-in command" },
- { "verify", "Verify udev rules files" },
- { "cat", "Show udev rules files" },
- { "wait", "Wait for device or device symlink" },
- { "lock", "Lock a block device" },
- };
-
- _cleanup_free_ char *link = NULL;
+ _cleanup_(table_unrefp) Table *verbs = NULL, *options = NULL;
int r;
- r = terminal_urlify_man("udevadm", "8", &link);
+ r = verbs_get_help_table(&verbs);
if (r < 0)
- return log_oom();
+ return r;
+
+ r = option_parser_get_help_table_ns("udevadm", &options);
+ if (r < 0)
+ return r;
- printf("%s [--help] [--version] [--debug] COMMAND [COMMAND OPTIONS]\n\n"
- "Send control commands or test the device manager.\n\n"
- "Commands:\n",
- program_invocation_short_name);
+ (void) table_sync_column_widths(0, verbs, options);
+
+ help_cmdline("[OPTIONS…] COMMAND [COMMAND OPTIONS…]");
+ help_abstract("Send control commands or test the device manager.");
+
+ help_section("Commands:");
+ r = table_print_or_warn(verbs);
+ if (r < 0)
+ return r;
- FOREACH_ELEMENT(desc, short_descriptions)
- printf(" %-12s %s\n", (*desc)[0], (*desc)[1]);
+ help_section("Options:");
+ r = table_print_or_warn(options);
+ if (r < 0)
+ return r;
- printf("\nSee the %s for details.\n", link);
+ help_man_page_reference("udevadm", "8");
return 0;
}
-static int parse_argv(int argc, char *argv[]) {
- static const struct option options[] = {
- { "debug", no_argument, NULL, 'd' },
- { "help", no_argument, NULL, 'h' },
- { "version", no_argument, NULL, 'V' },
- {}
- };
- int c;
+VERB_COMMON_HELP(help);
+
+VERB_SCOPE(, verb_info_main, "info", "[DEVPATH|FILE]", VERB_ANY, VERB_ANY, 0, "Query sysfs or the udev database");
+VERB_SCOPE(, verb_trigger_main, "trigger", "DEVPATH", VERB_ANY, VERB_ANY, 0, "Request events from the kernel");
+VERB_SCOPE(, verb_settle_main, "settle", NULL, VERB_ANY, VERB_ANY, 0, "Wait for pending udev events");
+VERB_SCOPE(, verb_control_main, "control", "OPTION", VERB_ANY, VERB_ANY, 0, "Control the udev daemon");
+VERB_SCOPE(, verb_monitor_main, "monitor", NULL, VERB_ANY, VERB_ANY, 0, "Listen to kernel and udev events");
+VERB_SCOPE(, verb_test_main, "test", "DEVPATH", VERB_ANY, VERB_ANY, 0, "Test an event run");
+VERB_SCOPE(, verb_builtin_main, "test-builtin", "COMMAND DEVPATH", VERB_ANY, VERB_ANY, 0, "Test a built-in command");
+VERB_SCOPE(, verb_verify_main, "verify", "[FILE…]", VERB_ANY, VERB_ANY, 0, "Verify udev rules files");
+VERB_SCOPE(, verb_cat_main, "cat", "[FILE…]", VERB_ANY, VERB_ANY, 0, "Show udev rules files");
+VERB_SCOPE(, verb_wait_main, "wait", "DEVICE [DEVICE…]", VERB_ANY, VERB_ANY, 0, "Wait for device or device symlink");
+VERB_SCOPE(, verb_lock_main, "lock", "[OPTIONS…] COMMAND", VERB_ANY, VERB_ANY, 0, "Lock a block device");
+VERB_SCOPE(, verb_hwdb_main, "hwdb", NULL, VERB_ANY, VERB_ANY, 0, /* help= */ NULL); /* deprecated */
+
+VERB_NOARG(verb_version_main, "version", /* help= */ NULL);
+static int verb_version_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ return print_version();
+}
+static int parse_argv(int argc, char *argv[], char ***remaining_args) {
assert(argc >= 0);
assert(argv);
+ assert(remaining_args);
+
+ OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION, "udevadm" };
- /* Resetting to 0 forces the invocation of an internal initialization routine of getopt_long()
- * that checks for GNU extensions in optstring ('-' or '+' at the beginning). */
- optind = 0;
- while ((c = getopt_long(argc, argv, "+dhV", options, NULL)) >= 0)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
- case 'd':
- log_set_max_level(LOG_DEBUG);
- break;
+ OPTION_NAMESPACE("udevadm"): {}
- case 'h':
+ OPTION_COMMON_HELP:
return help();
- case 'V':
+ OPTION_COMMON_VERSION_WITH_HIDDEN_V:
return print_version();
- case '?':
- return -EINVAL;
-
- default:
- assert_not_reached();
+ OPTION('d', "debug", NULL, "Enable debug logging"):
+ log_set_max_level(LOG_DEBUG);
+ break;
}
+ *remaining_args = option_parser_get_args(&opts);
return 1; /* work to do */
}
@@ -91,37 +97,8 @@ int print_version(void) {
return 0;
}
-static int verb_version_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
- return print_version();
-}
-
-static int verb_help_main(int argc, char *argv[], uintptr_t _data, void *userdata) {
- return help();
-}
-
-static int udevadm_main(int argc, char *argv[]) {
- static const Verb verbs[] = {
- { "cat", VERB_ANY, VERB_ANY, 0, verb_cat_main },
- { "info", VERB_ANY, VERB_ANY, 0, verb_info_main },
- { "trigger", VERB_ANY, VERB_ANY, 0, verb_trigger_main },
- { "settle", VERB_ANY, VERB_ANY, 0, verb_settle_main },
- { "control", VERB_ANY, VERB_ANY, 0, verb_control_main },
- { "monitor", VERB_ANY, VERB_ANY, 0, verb_monitor_main },
- { "hwdb", VERB_ANY, VERB_ANY, 0, verb_hwdb_main },
- { "test", VERB_ANY, VERB_ANY, 0, verb_test_main },
- { "test-builtin", VERB_ANY, VERB_ANY, 0, verb_builtin_main },
- { "wait", VERB_ANY, VERB_ANY, 0, verb_wait_main },
- { "lock", VERB_ANY, VERB_ANY, 0, verb_lock_main },
- { "verify", VERB_ANY, VERB_ANY, 0, verb_verify_main },
- { "version", VERB_ANY, VERB_ANY, 0, verb_version_main },
- { "help", VERB_ANY, VERB_ANY, 0, verb_help_main },
- {}
- };
-
- return dispatch_verb(argc, argv, verbs, NULL);
-}
-
static int run(int argc, char *argv[]) {
+ char **args = NULL;
int r;
if (invoked_as(argv, "udevd"))
@@ -130,7 +107,7 @@ static int run(int argc, char *argv[]) {
(void) udev_parse_config();
log_setup();
- r = parse_argv(argc, argv);
+ r = parse_argv(argc, argv, &args);
if (r <= 0)
return r;
@@ -138,7 +115,7 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return r;
- return udevadm_main(argc, argv);
+ return dispatch_verb_with_args(args, NULL);
}
DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
diff --git a/src/udev/v4l_id/v4l_id.c b/src/udev/v4l_id/v4l_id.c
index 93ca2d3b997fe..1a53e1092fb7a 100644
--- a/src/udev/v4l_id/v4l_id.c
+++ b/src/udev/v4l_id/v4l_id.c
@@ -43,7 +43,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/update-done/update-done.c b/src/update-done/update-done.c
index b55c9941a9de3..67ce353e114d2 100644
--- a/src/update-done/update-done.c
+++ b/src/update-done/update-done.c
@@ -98,7 +98,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/validatefs/validatefs.c b/src/validatefs/validatefs.c
index 58f8feb805dca..506b8198709d5 100644
--- a/src/validatefs/validatefs.c
+++ b/src/validatefs/validatefs.c
@@ -66,7 +66,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
return help();
diff --git a/src/varlinkctl/varlinkctl.c b/src/varlinkctl/varlinkctl.c
index fbf4b217ff691..fbd5e2499a5d5 100644
--- a/src/varlinkctl/varlinkctl.c
+++ b/src/varlinkctl/varlinkctl.c
@@ -126,7 +126,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/vmspawn/vmspawn.c b/src/vmspawn/vmspawn.c
index 14df0fc989f65..81c035c250d62 100644
--- a/src/vmspawn/vmspawn.c
+++ b/src/vmspawn/vmspawn.c
@@ -235,7 +235,8 @@ static int help(void) {
"Credentials",
};
- _cleanup_(table_unref_many) Table* tables[ELEMENTSOF(groups) + 1] = {};
+ Table* tables[ELEMENTSOF(groups)] = {};
+ CLEANUP_ELEMENTS(tables, table_unref_array_clear);
for (size_t i = 0; i < ELEMENTSOF(groups); i++) {
r = option_parser_get_help_table_group(groups[i], &tables[i]);
@@ -327,7 +328,7 @@ static int parse_argv(int argc, char *argv[]) {
OptionParser opts = { argc, argv, OPTION_PARSER_STOP_AT_FIRST_NONOPTION };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION_COMMON_HELP:
diff --git a/src/vpick/vpick-tool.c b/src/vpick/vpick-tool.c
index f18edb263f8e9..f0b5ef44dfb67 100644
--- a/src/vpick/vpick-tool.c
+++ b/src/vpick/vpick-tool.c
@@ -101,7 +101,7 @@ static int parse_argv(int argc, char *argv[], char ***ret_args) {
OptionParser opts = { argc, argv };
- FOREACH_OPTION(c, &opts, /* on_error= */ return c)
+ FOREACH_OPTION_OR_RETURN(c, &opts)
switch (c) {
OPTION('B', "basename", "BASENAME", "Look for specified basename"):
diff --git a/test/units/TEST-87-AUX-UTILS-VM.storagectl.sh b/test/units/TEST-87-AUX-UTILS-VM.storagectl.sh
new file mode 100755
index 0000000000000..a11a952a8e8da
--- /dev/null
+++ b/test/units/TEST-87-AUX-UTILS-VM.storagectl.sh
@@ -0,0 +1,211 @@
+#!/usr/bin/env bash
+# SPDX-License-Identifier: LGPL-2.1-or-later
+set -eux
+set -o pipefail
+
+if ! command -v storagectl >/dev/null; then
+ echo "storagectl not found, skipping."
+ exit 77
+fi
+
+# shellcheck source=test/units/util.sh
+. "$(dirname "$0")"/util.sh
+
+# Unset $PAGER so we don't have to use --no-pager everywhere
+export PAGER=
+
+# storagectl runs in a VM-only test
+if systemd-detect-virt -cq ; then
+ echo "can't run in a container, skipping."
+ exit 77
+fi
+
+at_exit() {
+ set +e
+
+ if [[ -n "${MOUNT_DIR:-}" ]] && mountpoint -q "$MOUNT_DIR"; then
+ umount "$MOUNT_DIR"
+ fi
+ if [[ -n "${LOOP:-}" ]]; then
+ systemd-dissect --detach "$LOOP"
+ fi
+ if [[ -n "${WORK_DIR:-}" ]]; then
+ rm -fr "$WORK_DIR"
+ fi
+ rm -fr /var/lib/storage/test-87-storage-*.volume
+}
+trap at_exit EXIT
+
+# The storage providers are socket-activated by sockets.target, so the listening
+# AF_UNIX sockets should already exist.
+test -S /run/systemd/io.systemd.StorageProvider/block
+test -S /run/systemd/io.systemd.StorageProvider/fs
+
+WORK_DIR="$(mktemp -d /tmp/test-storagectl.XXXXXXXXXX)"
+MOUNT_DIR="$WORK_DIR/mnt"
+mkdir -p "$MOUNT_DIR"
+
+# --- storagectl basic ---
+
+storagectl --help
+storagectl --version
+storagectl help
+
+# Unknown verb / option
+(! storagectl this-verb-does-not-exist)
+(! storagectl --no-such-option providers)
+
+# --- storagectl providers ---
+
+storagectl providers
+storagectl providers --no-legend
+storagectl providers --no-pager
+storagectl providers --json=pretty | jq .
+storagectl providers --json=short | jq .
+
+providers_output="$(storagectl providers --no-legend)"
+assert_in 'block' "$providers_output"
+assert_in 'fs' "$providers_output"
+assert_in 'yes' "$providers_output"
+
+# --- storagectl volumes ---
+
+# 'volumes' is the default verb
+storagectl
+storagectl volumes
+storagectl volumes --no-legend
+storagectl volumes --no-pager
+storagectl volumes --json=pretty | jq .
+storagectl volumes --json=short | jq .
+
+# Glob filter that matches nothing should not error
+storagectl volumes 'no-such-volume-*'
+
+# --- storagectl templates ---
+
+storagectl templates
+storagectl templates --no-legend --no-pager
+storagectl templates --json=pretty | jq .
+storagectl templates --json=short | jq --seq .
+
+templates_output="$(storagectl templates --no-legend)"
+assert_in 'sparse-file' "$templates_output"
+assert_in 'allocated-file' "$templates_output"
+assert_in 'directory' "$templates_output"
+assert_in 'subvolume' "$templates_output"
+
+# Glob filter
+storagectl templates 'sparse-*' --no-legend | grep sparse-file >/dev/null
+(! storagectl templates 'sparse-*' --no-legend | grep allocated-file >/dev/null)
+storagectl templates 'no-such-template-*'
+
+# --- direct varlink calls ---
+
+varlinkctl introspect /run/systemd/io.systemd.StorageProvider/block io.systemd.StorageProvider
+varlinkctl introspect /run/systemd/io.systemd.StorageProvider/fs io.systemd.StorageProvider
+
+# Block provider does not expose templates
+varlinkctl call --more /run/systemd/io.systemd.StorageProvider/block \
+ io.systemd.StorageProvider.ListTemplates '{}' \
+ --graceful=io.systemd.StorageProvider.NoSuchTemplate
+
+# fs provider lists the four built-in templates
+varlinkctl call --more --json=short /run/systemd/io.systemd.StorageProvider/fs \
+ io.systemd.StorageProvider.ListTemplates '{}' | grep '"name":"sparse-file"' >/dev/null
+
+# Block provider rejects names not under /dev/
+varlinkctl call /run/systemd/io.systemd.StorageProvider/block \
+ io.systemd.StorageProvider.Acquire '{"name":"/tmp/no-such-dev"}' \
+ --graceful=io.systemd.StorageProvider.NoSuchVolume
+
+# fs provider rejects bad volume names (contain '/' → not a valid filename)
+varlinkctl call /run/systemd/io.systemd.StorageProvider/fs \
+ io.systemd.StorageProvider.Acquire '{"name":"bad/name"}' \
+ --graceful=org.varlink.service.InvalidParameter
+
+# --- mount.storage: regular file via fs provider ---
+
+TESTVOL_REG="test-87-storage-reg-$RANDOM"
+truncate -s 32M "/var/lib/storage/$TESTVOL_REG.volume"
+mkfs.ext4 "/var/lib/storage/$TESTVOL_REG.volume"
+mount -t storage.ext4 "fs:$TESTVOL_REG" "$MOUNT_DIR"
+mountpoint -q "$MOUNT_DIR"
+echo "hello reg" >"$MOUNT_DIR/hello"
+umount "$MOUNT_DIR"
+
+# Volume now appears in 'storagectl volumes'
+volumes_after_create="$(storagectl volumes "$TESTVOL_REG" --no-legend)"
+assert_in "$TESTVOL_REG" "$volumes_after_create"
+assert_in 'reg' "$volumes_after_create"
+
+# Re-mount existing (default storage.create=any)
+mount -t storage.ext4 "fs:$TESTVOL_REG" "$MOUNT_DIR"
+test -f "$MOUNT_DIR/hello"
+umount "$MOUNT_DIR"
+
+# storage.create=open succeeds for existing volume
+mount -t storage.ext4 -o "storage.create=open" "fs:$TESTVOL_REG" "$MOUNT_DIR"
+umount "$MOUNT_DIR"
+
+# storage.create=new on existing volume must fail
+(! mount -t storage.ext4 -o "storage.create=new,storage.create-size=16M" "fs:$TESTVOL_REG" "$MOUNT_DIR")
+
+# Read-only mount
+mount -t storage.ext4 -o ro "fs:$TESTVOL_REG" "$MOUNT_DIR"
+findmnt -n -o options "$MOUNT_DIR" | grep -E '(^|,)ro(,|$)' >/dev/null
+(! touch "$MOUNT_DIR/readonly-test")
+umount "$MOUNT_DIR"
+
+rm -f "/var/lib/storage/$TESTVOL_REG.volume"
+
+# storage.create=open on missing volume must fail
+(! mount -t storage.ext4 -o "storage.create=open" "fs:test-87-storage-missing-$RANDOM" "$MOUNT_DIR")
+
+# --- mount.storage: directory volume via fs provider (requires idmapped mounts) ---
+
+TESTVOL_DIR="test-87-storage-dir-$RANDOM"
+if mount -t storage "fs:$TESTVOL_DIR" "$MOUNT_DIR"; then
+ mountpoint -q "$MOUNT_DIR"
+ test -d "/var/lib/storage/$TESTVOL_DIR.volume/root"
+ echo "dir test" >"$MOUNT_DIR/hello"
+ test -f "/var/lib/storage/$TESTVOL_DIR.volume/root/hello"
+ umount "$MOUNT_DIR"
+ rm -fr "/var/lib/storage/$TESTVOL_DIR.volume"
+else
+ echo "Directory volume mounting failed (idmapped mounts unsupported?), skipping."
+ rm -fr "/var/lib/storage/$TESTVOL_DIR.volume"
+fi
+
+# --- mount.storage: block device via block provider ---
+
+truncate -s 32M "$WORK_DIR/block.img"
+mkfs.ext4 -L sd-storage-blk "$WORK_DIR/block.img"
+LOOP="$(systemd-dissect --attach --loop-ref=test-storagectl "$WORK_DIR/block.img")"
+
+mount -t storage.ext4 "block:$LOOP" "$MOUNT_DIR"
+mountpoint -q "$MOUNT_DIR"
+echo "hello blk" >"$MOUNT_DIR/hello"
+umount "$MOUNT_DIR"
+
+# Read-only mount of the block volume
+mount -t storage.ext4 -o ro "block:$LOOP" "$MOUNT_DIR"
+findmnt -n -o options "$MOUNT_DIR" | grep -E '(^|,)ro(,|$)' >/dev/null
+test -f "$MOUNT_DIR/hello"
+umount "$MOUNT_DIR"
+
+# Block volume is enumerable; matchName globs over device node and aliases
+varlinkctl call --more --json=short /run/systemd/io.systemd.StorageProvider/block \
+ io.systemd.StorageProvider.ListVolumes "{\"matchName\":\"$LOOP\"}" |
+ grep '"type":"blk"' >/dev/null
+
+systemd-dissect --detach "$LOOP"
+unset LOOP
+
+# --- error cases ---
+
+# Bad provider name (no such socket)
+(! mount -t storage.ext4 "no-such-provider:foo" "$MOUNT_DIR")
+# Bad volume specification (no colon)
+(! mount -t storage.ext4 "no-colon-here" "$MOUNT_DIR")
+# Refuse nested storage volumes (FS type "storage.storage")
+(! mount -t storage.storage "fs:something" "$MOUNT_DIR")
diff --git a/units/meson.build b/units/meson.build
index 622e1e69cf7c2..0f7ce75bd8967 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -804,6 +804,20 @@ units = [
'conditions' : ['ENABLE_SYSUSERS'],
'symlinks' : ['sysinit.target.wants/'],
},
+ {
+ 'file' : 'systemd-storage-block.socket',
+ 'symlinks' : ['sockets.target.wants/']
+ },
+ {
+ 'file' : 'systemd-storage-block@.service.in',
+ },
+ {
+ 'file' : 'systemd-storage-fs.socket',
+ 'symlinks' : ['sockets.target.wants/']
+ },
+ {
+ 'file' : 'systemd-storage-fs@.service.in',
+ },
{
'file' : 'systemd-storagetm.service.in',
'conditions' : ['ENABLE_STORAGETM'],
diff --git a/units/systemd-storage-block.socket b/units/systemd-storage-block.socket
new file mode 100644
index 0000000000000..1d18b481a375a
--- /dev/null
+++ b/units/systemd-storage-block.socket
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple Block Device Backed Storage Provider
+Documentation=man:systemd-storage-block@..service(8)
+DefaultDependencies=no
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/io.systemd.StorageProvider/block
+FileDescriptorName=varlink
+SocketMode=0666
+Accept=yes
+MaxConnectionsPerSource=16
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-storage-block@.service.in b/units/systemd-storage-block@.service.in
new file mode 100644
index 0000000000000..801551e2ff802
--- /dev/null
+++ b/units/systemd-storage-block@.service.in
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple Block Device Backed Storage Provider
+Documentation=man:systemd-storage-block@.service(8)
+DefaultDependencies=no
+Conflicts=shutdown.target initrd-switch-root.target
+Before=shutdown.target initrd-switch-root.target
+
+[Service]
+ExecStart=-{{LIBEXECDIR}}/systemd-storage-block
diff --git a/units/systemd-storage-fs.socket b/units/systemd-storage-fs.socket
new file mode 100644
index 0000000000000..c83cf0a11fda8
--- /dev/null
+++ b/units/systemd-storage-fs.socket
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple File System Backed Storage Provider
+Documentation=man:systemd-storage-fs@.service(8)
+DefaultDependencies=no
+RequiresMountsFor=/var/lib/storage
+Before=sockets.target
+
+[Socket]
+ListenStream=/run/systemd/io.systemd.StorageProvider/fs
+FileDescriptorName=varlink
+SocketMode=0666
+Accept=yes
+MaxConnectionsPerSource=16
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/systemd-storage-fs@.service.in b/units/systemd-storage-fs@.service.in
new file mode 100644
index 0000000000000..39b6da36ee76b
--- /dev/null
+++ b/units/systemd-storage-fs@.service.in
@@ -0,0 +1,19 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple File System Backed Storage Provider
+Documentation=man:systemd-storage-fs@.service(8)
+DefaultDependencies=no
+RequiresMountsFor=/var/lib/storage
+Conflicts=shutdown.target initrd-switch-root.target
+Before=shutdown.target initrd-switch-root.target
+
+[Service]
+ExecStart=-{{LIBEXECDIR}}/systemd-storage-fs
diff --git a/units/user/meson.build b/units/user/meson.build
index a9c6d44281c28..39c41a4c1cd8c 100644
--- a/units/user/meson.build
+++ b/units/user/meson.build
@@ -61,6 +61,13 @@ units = [
'file' : 'systemd-journalctl.socket',
'symlinks' : ['sockets.target.wants/'],
},
+ {
+ 'file' : 'systemd-storage-fs.socket',
+ 'symlinks' : ['sockets.target.wants/']
+ },
+ {
+ 'file' : 'systemd-storage-fs@.service.in',
+ },
{ 'file' : 'systemd-tmpfiles-clean.service' },
{ 'file' : 'systemd-tmpfiles-clean.timer' },
{ 'file' : 'systemd-tmpfiles-setup.service' },
diff --git a/units/user/systemd-storage-fs.socket b/units/user/systemd-storage-fs.socket
new file mode 100644
index 0000000000000..fa8018b2e8552
--- /dev/null
+++ b/units/user/systemd-storage-fs.socket
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple File System Backed Storage Provider
+Documentation=man:systemd-storage-fs.service(8)
+Before=sockets.target
+
+[Socket]
+ListenStream=%t/systemd/io.systemd.StorageProvider/fs
+FileDescriptorName=varlink
+SocketMode=0600
+Accept=yes
+MaxConnectionsPerSource=16
+
+[Install]
+WantedBy=sockets.target
diff --git a/units/user/systemd-storage-fs@.service.in b/units/user/systemd-storage-fs@.service.in
new file mode 100644
index 0000000000000..95afa9165fa5f
--- /dev/null
+++ b/units/user/systemd-storage-fs@.service.in
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Simple File System Backed Storage Provider
+Documentation=man:systemd-storage-fs.service(8)
+
+[Service]
+ExecStart=-{{LIBEXECDIR}}/systemd-storage-fs --user