Traditionally, asynchronous programming in systemd has been achieved using
sd-event along with the asynchronous interfaces of sd-bus and sd-varlink.
This works well when the system is reacting to events and all code triggered
by those events can run without blocking. In these scenarios, the global
Manager object is passed as userdata to the callback, and the callback can
use the stack as usual, declaring local state and ensuring proper cleanup via
_cleanup_. Control flow structures, such as loops, work as expected, and
everything runs smoothly.
However, challenges arise when the code needs to perform long-running
operations within these callbacks. Since the system cannot block execution
within the callback, we can't directly invoke a long-running operation and
wait for its result without introducing complexities. Instead, we need to
initiate the long-running task, register for completion with sd-event,
sd-bus, or sd-varlink, and provide a callback to be invoked when the
operation completes.
This callback, however, only receives a single userdata pointer, which
forces us to bundle all local variables into a struct and pass it along as
part of the callback. On top of that, after queuing the asynchronous
operation, the caller continues executing. As the caller's stack unwinds
when the function exits, the resources and state within the local scope may
be prematurely cleaned up. Therefore, the struct must store copies of the
local variables or ensure proper reference counting to prevent premature
resource cleanup.
When multiple long-running operations need to be initiated within a loop,
the complexity grows further. We must introduce additional shared state to
track the completion of all operations before we can run any code that
depends on their results.
Furthermore, since the daemon may be shut down at any time, we must track
the lifecycle of each long-running operation in the global Manager struct,
ensuring proper cleanup even when stack unwinding can no longer manage the
resources for us.
Fibers, or green threads, provide a more natural way of handling
asynchronous operations. By enabling cooperative multitasking within a
single thread, fibers allow us to write code that looks like it’s running
synchronously, but with the ability to yield control at predefined points,
such as when waiting for long-running tasks to complete.
With fibers, we can simplify the control flow by running asynchronous
operations within a fiber, allowing us to "pause" execution while waiting
for the long-running operation to finish and then "resume" the operation once
it's complete. This eliminates the need for multiple callback chains,
extensive state tracking, and the potential pitfalls of stack unwinding.
This commit introduces the ability to execute long-running operations in a
non-blocking manner while maintaining the simplicity and readability of
synchronous code. The fiber-based approach will significantly improve the
handling of complex workflows, making the code easier to write and maintain.
The implementation is based on ucontext.h's makecontext() (with a fallback
to the venerable sigaltstack() approach on musl), sigsetjmp()/siglongjmp()
and sd-event. ucontext.h provides us with alternate stacks that we can switch
between. We use sigsetjmp()/siglongjmp() instead of swapcontext() because the
latter forcibly saves/restores a per context signal mask every time it is called.
Using sigsetjmp()/siglongjmp(), we can avoid the unnecessary syscall and maintain
a per thread signal mask, which makes much more sense than having a per fiber
signal mask.
The default stack size is the same as a regular thread. Because we
use mmap() to allocate the stack, the memory won't actually be used until it
is paged in by the kernel, so we don't actually use 8MB per fiber.
To integrate fibers with the event loop, each fiber is assigned a deferred
event source which resumes the fiber when enabled. The deferred event source
is oneshot by default so the fiber will run immediately until it yields or
suspends. If it yields, the deferred event source is enabled again (oneshot)
immediately. If it suspends, before it suspends, one or more event sources
are registered with sd-event that will enable the deferred event source
(oneshot) to resume the fiber once the operation it is waiting for completes.
Yielding or suspending the fiber is done by calling sd_fiber_yield() or
sd_fiber_suspend() respectively. Both of these return zero on success or any
error value from the async operation that caused the fiber to resume.
This is also how fiber cancellation is implemented. When a fiber is cancelled,
sd_fiber_yield() and sd_fiber_suspend() will return ECANCELED when the fiber
is resumed, allowing the fiber to unwind its stack (which allows cleanup to
happen automatically) and finish.
Instead of having applications work directly with fibers, we hide them behind
a generic futures interface to represent long-running operations, regardless of
whether those operations are running on a fiber or not. Aside from fibers, the
futures library (sd-future) will for example allow waiting for sd-event sources
and doing sd-bus calls in the background as well. Fibers can suspend until a
future is ready with sd_fiber_await() or by having the future wake up the fiber
explicitly in its callback. A future always defaults to waking up the current
fiber.
Each future kind plugs into the library by providing an sd_future_ops vtable
(alloc, free, cancel, set_priority). The library treats the impl pointer
returned by alloc() as a black box. Future Implementations retrieve it via
sd_future_get_private().
A future starts in SD_FUTURE_PENDING and transitions exactly once to
SD_FUTURE_RESOLVED, carrying an integer result. Consumers can react to that
transition either by installing a one-shot callback with
sd_future_set_callback() (callback-style code) or by waiting on it from a
fiber via sd_fiber_await() (synchronous-looking fiber code). sd_fiber_await()
is itself built on a "wait future" that resolves when its target resolves;
sd_future_new_wait() exposes the same primitive directly so non-fiber callers
can chain futures without involving a fiber.
Cancellation is cooperative: sd_future_cancel() invokes the future impl's
cancel callback, which is responsible for tearing down its work and ultimately
resolving the promise with -ECANCELED. For fiber futures this is what
surfaces as the ECANCELED return from sd_fiber_yield()/sd_fiber_suspend()
mentioned above.
Fire-and-forget fibers — created by passing a NULL ret to sd_fiber_new() —
take a self-reference on their future so they outlive the caller's scope.
The self-ref is dropped when the fiber resolves. This floating mechanism
(sd_fiber_set_floating()) is restricted to fiber futures because they
uniquely guarantee resolution; allowing it for arbitrary future kinds would
risk silent leaks for kinds that may never resolve.
Note that fiber cleanup depends on the runtime operating normally. Each
fiber's _cleanup_-style cleanups live on the fiber's own stack and run
only when the fiber is resumed and allowed to unwind, which requires a
working event loop to drive it to completion. The exit event source
registered for top-level fibers ensures unwind on a normal sd_event_exit(),
but if the event loop itself terminates abnormally (e.g. an unrecoverable
allocation failure mid-dispatch) before all fibers have resolved, their
stacks never unwind and any resources they own leak.
The code lives in libsystemd as sd-future (not exported) for the following reasons:
- We may want to make this a public libsystemd API in the future
- The code can't live in src/basic as it makes heavy use of sd-event
- The code can't live in src/shared as sd-bus and sd-event make use of it
The log and log-context headers are updated with functions to allow
fibers to have their own log prefix and log context.
Drop the standalone Unit-tests (musl) workflow that ran on an Alpine sandbox
spun up by jirutka/setup-alpine, and merge it into unit-tests.yml as a new
build-musl job that provisions a postmarketOS tools tree via mkosi and runs
the meson build + test suite through 'mkosi box'. postmarketOS is musl-native,
so the musl-gcc / -idirafter /usr/include wrappers the Fedora tools tree
needed are gone; the linter.yml's own musl build step also goes away since
the unit-tests workflow now covers it (and tests it).
postmarketOS doesn't ship a downstream systemd packaging spec, so the new
tools tree config in mkosi.tools.conf/mkosi.conf.d/postmarketos.conf does not
set PrepareScripts and lists build deps manually. mkosi.sync now early-exits
when PKG_SUBDIR is unset so the missing pkgenv entry doesn't trip set -u.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Instead of having CI runner specific checks, let's just
skip the test if we get EXIT_CGROUP which is what we get
when we can't create a cgroup. This makes the check work
independently of CI runner, and specifically also on github
actions.
if stdin is connected to a closed pipe or similar, imply
PTY_FORWARD_READ_ONLY so we don't even try to read from it
in the first place. Otherwise we'll immediately get a hangup
which will cause the forwarder to call sd_event_exit() and
shut down the event loop.
Debugged-by: Christian Brauner <brauner@kernel.org>
# Context
#40195 defines the initial proposal and the motivation behind this PR.
This PR introduces 3 new options for `.link` files `[Link]` section:
- `IRQAffinityPolicy=`
- `IRQAffinity=`
- `IRQAffinityNUMA=`
The purpose is to allow `systemd-udevd` to configure a NIC's IRQs
affinity to specific CPU(s).
`IRQAffinityPolicy=` supports two policies:
- `single`: assign all the NIC IRQs to CPU 0, or the first CPU in the
CPU set resulting from the union of `IRQAffinity=` and
`IRQAffinityNUMA=`.
- `spread`: assign all the NIC IRQs to all the CPUs (or the union of
`IRQAffinity=` and `IRQAffinityNUMA=` if defined) in a round-robin
fashion while optimizing for cache locally while spreading apart queues
on CPUs as much as possible.
Both `IRQAffinity=` and `IRQAffinityNUMA=` behaves as filters to reduce
the CPU set to assign IRQs to, and are only valid if
`IRQAffinityPolicy=` is defined.
# Spreading IRQs
This section describes the algorithm responsible for spreading IRQs over
different CPUs to maximize performance.
## 1. Discover CPU topology
Read from `/sys/devices/system/cpu/cpu*/topology` to identify:
- L3 cache domains (dies)
- Physical cores VS hyperthreads
- NUMA nodes
- Core ordering within each die
```
Example: Dual-socket server with 2 dies per socket, 4 cores per die
┌─────────────────────────────────────────────────────────────────┐
│ NUMA Node 0 │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Die 0 (L3 Cache) │ │ Die 1 (L3 Cache) │ │
│ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │
│ │ │ C0 ││ C1 ││ C2 ││ C3 │ │ │ C4 ││ C5 ││ C6 ││ C7 │ │
│ │ │0,16││1,17││2,18││3,19│ │ │4,20││5,21││6,22││7,23│ │
│ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ NUMA Node 1 │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Die 2 (L3 Cache) │ │ Die 3 (L3 Cache) │ │
│ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │
│ │ │ C8 ││ C9 ││C10 ││C11 │ │ │C12 ││C13 ││C14 ││C15 │ │
│ │ │8,24││9,25││10,2││11,2│ │ │12,2││13,2││14,3││15,3│ │
│ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
(Numbers show CPU IDs: first HT, second HT)
```
## 2. Filter the first hyperthread only
Use only the first hyperthread of each physical core to avoid SMT
contention. Two IRQs on sibling HTs contend for ALU/cache without cache
benefit.
```
Before: CPUs 0-31 (16 cores × 2 HTs)
After: CPUs 0-15 (first HT of each core)
```
## 3. Equidistant permutations
Reorder dies and CPUs so consecutive selections are maximally spread
apart.
```
Original order: [0, 1, 2, 3] (adjacent dies/CPUs)
|
v
Equidistant: [0, 2, 1, 3] (spread apart)
```
This ensures that even if only 2 IRQs are assigned, they land on dies 0
and 2 (not 0 and 1), maximizing physical distance. The permutation is
also applied within each die:
```
Die permutation: Die0 -> Die2 -> Die1 -> Die3
Within each die:
┌───────────────────────────────────────────────┐
│ Die 0: [C0,C1,C2,C3] -> [C0,C2,C1,C3] │
│ Die 1: [C4,C5,C6,C7] -> [C4,C6,C5,C7] │
│ Die 2: [C8,C9,C10,C11] -> [C8,C10,C9,C11] │
│ Die 3: [C12,C13,C14,C15] -> [C12,C14,C13,C15] │
└───────────────────────────────────────────────┘
```
## 4. Round-robin selection across dies
Pick one CPU from each die in rotation, following permuted order.
```
Round 1: Die0->C0 Die2->C8 Die1->C4 Die3->C12
| | | |
v v v v
IRQs: [IRQ0] [IRQ1] [IRQ2] [IRQ3]
Round 2: Die0->C2 Die2->C10 Die1->C6 Die3->C14
| | | |
v v v v
IRQs: [IRQ4] [IRQ5] [IRQ6] [IRQ7]
```
If there are more IRQs than physical cores, this logic wraps around and
reuse CPUs. Only the first hyperthread of each core is used to avoid
cache line contention between queues.
Closes#40195.
- test-random-util is reworked to not use sqrt()
- pretty-print.c inlines ceil() so libm doesn't have
to be linked into libshared
- We add fno-math-errno to allow inlining of more math
functions by not requiring standard math functions to
set errno on invalid input.
Run acquire_root_devno() on the varlink-provided node so symlinks (e.g.
/dev/disk/by-id/...) resolve to their canonical /dev/ path before being
used. Without this, sym_fdisk_partname() produces a "-partN" symlink
that udev hasn't created yet when repart calls open() on it right after
BLKPG_ADD_PARTITION, failing with ENOENT.
This also brings the varlink path in line with the CLI path's
partition-to-whole-disk and dm-crypt-to-backing resolution.
The fuzzy translations are always wrong, but meson's integration does
not allow skipping them. Add a tiny wrapper for 'msgmerge' to
workaround the issue and skip them when running ninja systemd-update-po
SNP requires a named, stable CPU model so the launch measurement is
reproducible across hosts. EPYC-v4 is the baseline that covers all
SNP-capable processors (Milan and later).
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add --confidential-computing=sev-snp to run the guest as an AMD SEV-SNP
confidential VM. Loads a raw OVMF firmware blob via -bios (SNP doesn't
support the pflash + NVRAM split), attaches a sev-snp-guest object,
and hashes the kernel, initrd and cmdline into the launch measurement
when direct kernel boot is used. Incompatible features (Secure Boot,
CXL, virtio-balloon, SMBIOS credentials) are rejected or disabled; an
attached vTPM must be treated as untrusted by the guest.
The feature is marked experimental in the man page.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Add support for filtering IRQ affinity to CPUs on a specific NUMA node
via the new IRQAffinityNUMA= option in .link files. The option accepts:
- "local": use the NUMA node local to the NIC's PCIe slot
- Explicit node number (0, 1, 2, ...): use CPUs on the specified node
When both IRQAffinity= and IRQAffinityNUMA= are specified, their
intersection is used. If the intersection is empty, an error is logged
and IRQ affinity configuration is skipped.
When "local" is specified but the device's NUMA node cannot be
determined (numa_node shows -1), a warning is logged and IRQ affinity
configuration is skipped.
Add IRQAffinity= option to .link files that filters the set of CPUs
eligible for IRQ placement. This works in conjunction with
IRQAffinityPolicy= to constrain which CPUs receive network IRQs.
When specified with spread policy, only the listed CPUs are considered
for IRQ distribution. When specified with single policy, IRQs are
pinned to the first CPU in the allowed set instead of CPU 0.
Implement the spread policy for IRQ affinity distribution using a
topology-aware algorithm. The algorithm:
1. Discovers CPU topology from sysfs (NUMA node, package, die/L3, core)
2. Groups CPUs by L3 cache domain (die) with equidistant ordering
3. Round-robins across dies, spreading IRQs across the system
4. Uses first hyperthread of each core before second hyperthreads
5. Applies IRQ affinity via /proc/irq/<n>/smp_affinity
When there are more IRQs than CPUs, queues wrap around using round-robin.
Add support for configuring IRQ affinity for network interfaces via
systemd .link files. For now, the new IRQAffinityPolicy= option in the [Link]
section only accepts "single", which pins all MSI IRQs to CPU 0.
This allows declarative IRQ affinity configuration for network devices
during udev processing, which is useful for optimizing network
performance on multi-core systems.
Further commits will expand the options supported by IRQAffinityPolicy=.
Let's bail out early if the packet claims to contain some
questions or answer RRs, but the remaining packet data size is not
enough to hold a single such entry.
Follow-up for e7cd836dcf.
For every built executable, internal shared library, and plugin module,
verify two link-time properties via readelf:
1. No imported GLIBC symbol's version is newer than 2.34.
2. The dynamic section's NEEDED entries reference only glibc, the
runtime linker, our own libraries.
exp10() has a symbol version > 2.34 on latest glibc. To allow
dropping our baseline required glibc runtime version to <= 2.34,
let's add our own version to prevent pulling in the newer symbol
from glibc.
Methods:
- `io.systemd.Job.List` — list all queued jobs or look up by `id`/`unit`
name, with streaming support. Uses context/runtime split: `JobContext`
(Unit, JobType) and `JobRuntime` (Id, State, Result,
ActivationDetails). Follows the same SELinux and parameter-conflict
patterns as `io.systemd.Unit.List`.
- `io.systemd.Job.Cancel` — cancel a specific job by ID, with SELinux
and polkit authorization.
- `io.systemd.Job.ClearAll` — cancel all pending jobs, with SELinux and
polkit authorization.
There exists a small window of time in icmp6_bind() between creating the
ICMPv6 socket and binding it to an ifindex, where the link-scoped socket
can process an ICMPv6 packet received on any interface. The applies to
both sd-radv and sd-ndisc codepaths.
This change adds an explicit check for ifindex on the receive path and
ignores packets received on other interfaces.
Co-developed-by: OpenAI Codex <noreply@openai.com>
`message_skip_fields()` recursively processes D-Bus variant types in
message header fields with no depth limit. A crafted message with deeply
nested variants can cause unbounded recursion and overflow the stack.
Add a `depth` parameter checked against `BUS_CONTAINER_DEPTH` (128),
matching the limit already enforced by the public
`sd_bus_message_skip()` API. All recursive call sites pass `depth + 1`,
and the top-level caller in `message_parse_fields()` passes `0`.
The existing error reporting for the varlink `StartTransient` code
was converting all errors into `VARLINK_ERROR_UNIT_BAD_SETTING`.
This is not correct in some cases, we need to have a more targted
pattern here, i.e. only convert EINVAL to VARLINK_ERROR_UNIT_BAD_SETTING
and otherwise return the matching varlink error from the errno instead.
This commit fixes this issue. Thanks to Ivan Kruglov for raising
this.
Each dlopen_*() wrapper kept its dl handle as a file-scope
'static void *xxx_dl = NULL;' even though only the wrapper itself
ever referenced it. Move each one inside the corresponding function
so its scope matches its actual use, leaving the rest of each
translation unit free of the unused file-scope name.
In pcre2-util.c the assert(pcre2_dl) in pattern_matches_and_log()
becomes assert(sym_pcre2_match), which carries the same invariant
(pattern_compile_and_log() ran dlopen_pcre2()).
Commit 881e4717c7 ("test: pin stress-ng --vm-method to a portable
scalar method in TEST-55-OOMD") pinned --vm-method=zero-one with the
rationale that it is "a long-standing scalar method". That rationale is
wrong: stress_vm_zero_one() in stress-ng's stress-vm.c is declared
static size_t TARGET_CLONES stress_vm_zero_one(...)
i.e. it carries the exact same TARGET_CLONES attribute as 33 of the 35
other vm methods. On x86_64 with GCC >=5, TARGET_CLONES expands (see
core-target-clones.h in stress-ng) to a target_clones attribute
including "arch=skylake-avx512", "arch=cooperlake", "arch=tigerlake",
"arch=sapphirerapids", and several other AVX-512-bearing arch variants,
plus "default". GCC generates AVX-512 clones of stress_vm_zero_one() and
the IFUNC resolver picks them on any CPU that advertises AVX-512.
The only vm methods in stress-ng's registry whose function definitions
omit TARGET_CLONES entirely (and are therefore guaranteed not to
dispatch to an AVX-512 clone) are lfsr32 (portable, always registered)
and write64ds (x86_64-only, gated on HAVE_ASM_X86_MOVDIRI, i.e. Intel
Tremont / Tiger Lake+ MOVDIRI instruction).
Switch the four stress-ng --vm invocations in TEST-55-OOMD to
--vm-method=lfsr32 so the AVX-512 SIGILL on CPUs without AVX-512 (e.g.
AMD Zen 1-3) can no longer occur regardless of compiler version,
optimization level, or stress-ng package build.
Follow-up for 881e4717c7
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
The test waited for the OnFailure= service's filesystem side effect
(`rmdir` of the directory) and then immediately invoked
`systemctl is-active`. Between `rmdir(2)` returning (which causes the
shell loop to exit) and PID1 reaping the child and transitioning the
oneshot service from `activating` to `active`, there is a small window
where `is-active` can observe `activating` and fail the test.
Wait directly on the unit state instead, matching the pattern used a
few lines above for the `is-failed` case.
[ 1880.326704] TEST-07-PID1.sh[21489]: + timeout --foreground 60 bash -c 'while [[ -d '\''/tmp/TEST-07-PID1-socket-8467/test'\'' ]]; do sleep .5; done'
[ 1880.330482] TEST-07-PID1.sh[21489]: + [[ ! -e /tmp/TEST-07-PID1-socket-8467/test ]]
[ 1880.330482] TEST-07-PID1.sh[21489]: + systemctl is-active TEST-07-PID1-socket-OnFailure.service
[ 1880.347470] TEST-07-PID1.sh[21520]: activating
[ 1880.349508] TEST-07-PID1.sh[21489]: + at_exit
[ 1880.349508] TEST-07-PID1.sh[21489]: + systemctl stop TEST-07-PID1-socket-8467.socket
[ 1880.367331] TEST-07-PID1.sh[107]: Subtest /usr/lib/systemd/tests/testdata/units/TEST-07-PID1.socket-on-failure.sh failed
[ 1880.367331] TEST-07-PID1.sh[107]: + return 1
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>