On `bootctl install`, two EFI boot entries are registered: one for the
primary sd-boot binary and one for a fallback. On `bootctl update`, the
existing primary binary is rotated to the fallback path before the new
version is installed, so the fallback entry always points to the
previous known-good binary.
```
$ sudo bootctl install
...
Created EFI boot entry "Linux Boot Manager".
Created EFI boot entry "Fallback Linux Boot Manager".
$ sudo bootctl update
Copied "/boot/EFI/systemd/systemd-bootaa64.efi" to "/boot/EFI/systemd/systemd-boot-fallbackaa64.efi".
Copied "/usr/lib/systemd/boot/efi/systemd-bootaa64.efi" to "/boot/EFI/systemd/systemd-bootaa64.efi".
$ efibootmgr
...
Boot0004* Linux Boot Manager HD(...)/\EFI\systemd\systemd-bootaa64.efi
Boot0005* Fallback Linux Boot Manager HD(...)/\EFI\systemd\systemd-boot-fallbackaa64.efi
```
Fixes: #23805
This is supposed to protect our SMBIOS type 11 importing for
credentials. Note that firmwares are supposed to measure SMBIOS anyway
to PCR 1. Alas firmware doesn't really do that in various cases. Hence
let's do so again, for select objects.
This closes a gap where some of the input for OS (i.e. system
credentials places in smbios11) isn't measured properly.
(I really want this to get into v261, because this will fuck up the PCRs
a bit more, and we already have the new separator measurement in v261,
hence there's value in getting this merged at the same time, so that we
don't break the measurements a 2nd time)
This cleans up the fallback Boot#### entry that was registered on
install. The logic cleaning up variables was moved from verb_remove into
a new remove_variables function, which mirrors the install side.
This adds a second install_boot_option call to register a Boot#### entry
pointing at systemd-boot-fallback{arch}.efi, and place it immediately
after the primary entry in BootOrder.
The fallback file does not exist on the ESP on first install and is
only created on first update when the existing primary binary is
rotated to the fallback path. We register the variable anyway, so
that the entry exists in the BootOrder once the fallback file shows up.
Until then, firmware that reaches the fallback entry will fail to
load it and fall through to the next entry in BootOrder, which is
fine. install_boot_option gains a require_existing parameter so the
existing early return on a missing ESP path can be skipped for the
fallback, where a missing path is expected.
This also does a bit of refactoring by splitting the bottom part of
run_install() into a new install_variables() function that handles
registering both the primary and fallback entries.
When a primary sd-boot binary already exists on the ESP and is being
updated, it is copied to systemd-boot-fallback{arch}.efi before installing
the new version. This gives firmware a fallback Boot#### entry pointing
to the previous binary in case the new one fails to load.
The fallback is preserved (not overwritten) when its product and version
match the currently booted bootloader (read from the LoaderInfo EFI
variable), since that means it already holds the known good binary that
booted this session. In all other cases it is overwritten with the current
primary, when no fallback exists yet, when LoaderInfo is unavailable, or
when the fallback's product or version differs from what booted.
This also moves the version_check() call up so its result determines
both the rotation decision and the main copy, and avoids a duplicate
check (and duplicate "Skipping..." log) when the binary is already
current.
This adds an after_slot parameter that, when not set to UINT16_MAX,
requests that the new slot be placed immediately after the given slot in
BootOrder. When after_slot is set and the new slot already exists in
BootOrder, it will leave its position alone. This is so that if a user
reorders it, we don't stomp on their changes.
This moves creation of the EFI boot option description out of
install_boot_option and into the caller, and adds a ret_slot output
parameter for capturing the assigned BootOrder slot. This allows reusing
the function for installing variables with different descriptions.
remove_variables looks up the EFI boot entry by matching both the path
and the partition UUID and it wasn't actually removing any entries
because verb_remove was passing SD_ID128_NULL, so the lookup never
matched and Boot#### entries were left behind on uninstall.
Fixes 38433a6
load_kexec_kernel() retries kexec_file_load() with an extracted kernel
(decompressed Image / ZBOOT PE / UKI) when the kernel rejects the image,
but it only does so when kexec_file_load() failed with ENOEXEC. On arm64
that retry never happens: arm64's image_probe()
(arch/arm64/kernel/kexec_image.c) returns -EINVAL on an ARM64_IMAGE_MAGIC
mismatch, whereas x86's bzImage64_probe() and the generic
kexec_image_probe_default() return -ENOEXEC. So `systemctl kexec` of a
UKI on arm64 skips the extraction path and falls back to the
/usr/sbin/kexec binary, which is no longer a dependency since e107c7ead0
("systemctl: replace kexec-tools dependency with direct kexec_file_load()
syscall") -- leaving kexec broken.
Accept EINVAL in addition to ENOEXEC. This is safe: the extraction in
kexec_maybe_decompress_kernel() re-gates on the actual file magic (MZ /
compression headers) and is a no-op returning 0 for anything else, so an
EINVAL that is not a format mismatch just falls through to the existing
fallback as before.
Fixing this in systemd (rather than only in the kernel) is appropriate:
systemd must keep working with already-shipped arm64 kernels whose
kexec_file_load() returns EINVAL for an unrecognized image magic.
Relates to: https://github.com/systemd/systemd/issues/28538
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
For management purposes it's useful to be able to "tag" a machine with
various labels. Let's add a field for that to /etc/machine-info and make
it settable.
Fixes: #38591
nvindex space is very scarce, let's hence introduce "priorities" on
nvpcrs, so that if we haven't got enough space for all we have some
control which ones get dropped and which ones kept.
get_user_creds would use getpwnam() and then returns strings pointing
into the static buffer. This seems very iffy. Let's duplicate the
strings properly
The param was input/output, which is unexpected and confusing and
actually made most callers much more complicated than they needed to be.
The function was playing fast and loose with the return value. In some
cases it was returning a static string, which would be completely fine.
But in other case it was returning a pointer into the getgrgid static
buffer, i.e. that return value could be overwritten. AFAICT, this didn't
matter in any of the callers, but we shouldn't do that anyway.
So use a separate output param with an allocated string that the caller
is responsible for.
It turns out that all callers pass NULL (outside of tests) and zero for
flags. So the function _could_ be simplified. But get_user_creds is
called with all the params actually used, and I think having the the
functions so different wouldn't be nice. I first wrote a commit to
drop the unused params, but then I discarded it.
The error message in exec-invoke.c is fixed.
Regression coverage for the soft-reboot linger bug fixed in
9f25feb4ed ("logind: keep lingering users at startup-time GC", #41789):
a lingering user's user@.service was started after a hardware reboot but
not after `systemctl soft-reboot`, because logind GC'd the user at
startup before user_start() ran.
On the first boot, enable lingering for the pre-existing testuser and
wait for its user@UID.service to come up. After the first soft-reboot
(which keeps the same persistent rootfs), assert the service is active
again — this is the regression check. Disable lingering again before the
nextroot switch in the second boot so the lingering user doesn't leak
into the later boots' minimal overlay rootfs.
Verified locally in a qemu/KVM VM: passes with the fix present, and with
the fix reverted the second-boot assertion times out (the lingering user
is GC'd in logind startup and user@UID.service never comes back),
confirming the test exercises the bug.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
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.