Chris Mason [Fri, 22 May 2026 16:23:03 +0000 (09:23 -0700)]
dissect-image: fix wrong errno on pread failure
In acquire_sig_for_roothash() the pread() failure branch returns
-ENOMEM, which is copy-pasted from the malloc() check immediately
above it:
_cleanup_free_ char *buf = new(char, partition_size+1);
if (!buf)
return -ENOMEM;
ssize_t n = pread(fd, buf, partition_size, partition_offset);
if (n < 0)
return -ENOMEM;
pread() sets errno to the actual I/O failure (EIO, EINTR, EBADF,
...). Aliasing those to -ENOMEM misleads dissect_log_error() and
any caller that branches on the returned code; verity signature
lookup failures get reported as out-of-memory.
Fix by returning -errno from the pread() failure branch. The
malloc() branch above is correct and unchanged.
Fixes: 98ca65c36aa9 ("dissect: check that roothash in signature matches before selecting partition") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason <clm@meta.com>
Chris Mason [Fri, 22 May 2026 16:15:04 +0000 (09:15 -0700)]
dissect-image: fix swallowed open() error in mountfsd_make_directory
mountfsd_make_directory() opens the parent directory after a
successful path_extract_filename() call, but reports failure using
the stale path_extract_filename() return value:
r = path_extract_filename(path, &dirname);
if (r < 0)
return log_debug_errno(r, ...);
...
_cleanup_close_ int fd = open(parent, O_DIRECTORY|O_CLOEXEC);
if (fd < 0)
return log_debug_errno(r, "Failed to open '%s': %m", parent);
At the open() check site r holds the non-negative return of
path_extract_filename(). log_debug_errno() with a non-negative
first argument returns 0, so the function reports success even
though open() failed. Callers that pass a non-NULL ret_directory_fd
then consume an uninitialized fd value, and the diagnostic message
is suppressed because log_debug_errno(0, ...) emits nothing.
Fix by passing errno instead of r, matching the convention used in
mountfsd_mount_image() and mountfsd_mount_directory() in the same
file. The real open() failure (ENOENT, EACCES, ENOTDIR, EMFILE, ...)
now propagates to the caller and the log message is preserved.
Fixes: 1be8caa6be6f ("importd: support unpacking tarballs to foreign UID range") Assisted-by: kres (claude-opus-4-7) Signed-off-by: Chris Mason <clm@meta.com>
Julian Sparber [Fri, 22 May 2026 15:23:41 +0000 (17:23 +0200)]
repart: Flush varlink messages for progress updates
Progress updates are not send out immidialty and only once the handler
for the run method completes since the event loop is not processed while
the run handler is executed. Therefore flush varlink messages immidialty
after a progress update is queued.
Luca Boccassi [Fri, 22 May 2026 15:41:25 +0000 (16:41 +0100)]
test: fix test-fileio leaving directory behind and failing on rerun
/* test_write_data_file_atomic_at */
src/test/test-fileio.c:740: Assertion failed: Expected "write_data_file_atomic_at(XAT_FDROOT, "tmp/zzz/wdfa", &a, 0)" to fail with error -2/ENOENT, but it succeeded
RSA PKCS#1 v1.5 is vulnerable to Bleichenbacher-style padding oracle
attacks, albeit very difficult and unlikely to actually happen in the
real world. Still for hardedning, switch new enrollments to RSA-OAEP,
with SHA-256 preferred and SHA-1 as fallback (probed at enrollment time,
since e.g. SoftHSM only accepts SHA-1, and older token might as well).
The actual padding scheme used to wrap a given key is recorded as a new
optional 'pkcs11-padding' / 'padding' field in the LUKS2 token JSON and
the homed user record. Decryption defaults to PKCS#1 v1.5 when absent so
existing enrollments keep working.
Luca Boccassi [Fri, 22 May 2026 13:07:06 +0000 (14:07 +0100)]
sysupdate: List default component only if transfer definition exists (#42179)
`sysupdate --json=short components` lists the components known to
sysupdate; these are the components which something like `updatectl`
will try to update.
The `default` component represents the host, and is meant to be listed
if transfer definitions exist in (for example) `/etc/sysupdate.d`
corresponding to the host OS. This then corresponds to `TARGET_HOST` in
`updatectl` and causes it to try updating that target.
The logic for working out whether the `default` component was present
essentially boiled down to “does `{/run,/etc,/usr/lib}/sysupdate.d`
exist”, and it didn’t check whether a `.transfer` or `.conf` file
actually existed in the config directory.
This is quite the corner case, but becomes more evident on systems where
sysupdate is being used to update a portable service but not the main
OS. At that point, if `/etc/sysupdate.d` exists empty (for some reason),
`updatectl` falls over because it starts trying to update the host OS
without any configuration to do so.
So, modify `sysupdate` to more fully load the available configuration
when listing components, and query it a bit more deeply to check whether
a default component exists.
If `sysupdate` is called with various command line arguments to affect
how its configuration is loaded, do *not* say that a default component
exists, as these arguments essentially anull the possibility of a
default being used in that process.
Add an integration test based on the reproducer provided by the issue
reporter. This test has been tested to fail if the changes to
`sysupdate.c` aren’t applied — if so, the second call to `sysupdate
components` would return
`{"default":true,"components":["some-component"]}`.
Signed-off-by: Philip Withnall <pwithnall@gnome.org> Fixes: https://github.com/systemd/systemd/issues/41501
Rocker Zhang [Thu, 21 May 2026 16:02:51 +0000 (00:02 +0800)]
test: cover LUO serialize-side anti-hijack guard in TEST-91
manager_luo_serialize_fd_stores() refuses to serialize a unit fd store
entry that holds a child LUO session named like PID 1's own ("systemd"),
to stop a service from hijacking PID 1's reserved session namespace
across kexec. That guard had no test coverage.
Add a test-luo store-hijack/check-hijack subcommand pair: on the first
boot a system service preserves a child LUO session named "systemd" in
its fd store; after kexec the test asserts the entry was not restored --
the unit's NFileDescriptorStore is 0, and check-hijack, run as the unit's
own second-boot ExecStart, confirms the hijack fd is absent from its
restored LISTEN_FDNAMES -- proving PID 1 skipped it during serialization.
The restore-side guards (corrupt mapping, reserved token 0, invalid unit
name, missing child session) are intentionally not covered: they only run
against PID 1's own "systemd" session built by luo_preserve_fd_stores(),
which a cooperating userspace helper cannot corrupt without racing or
displacing PID 1 (it single-owns /dev/liveupdate at shutdown). Triggering
them reliably would need kernel fault injection.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Philip Withnall [Tue, 19 May 2026 14:46:36 +0000 (15:46 +0100)]
sysupdate: List default component only if transfer definition exists
`sysupdate --json=short components` lists the components known to
sysupdate; these are the components which something like `updatectl`
will try to update.
The `default` component represents the host, and is meant to be listed
if transfer definitions exist in (for example) `/etc/sysupdate.d`
corresponding to the host OS. This then corresponds to `TARGET_HOST` in
`updatectl` and causes it to try updating that target.
The logic for working out whether the `default` component was present
essentially boiled down to “does `{/run,/etc,/usr/lib}/sysupdate.d`
exist”, and it didn’t check whether a `.transfer` or `.conf` file
actually existed in the config directory.
This is quite the corner case, but becomes more evident on systems where
sysupdate is being used to update a portable service but not the main
OS. At that point, if `/etc/sysupdate.d` exists empty (for some reason),
`updatectl` falls over because it starts trying to update the host OS
without any configuration to do so.
So, modify `sysupdate` to more fully load the available configuration
when listing components, and query it a bit more deeply to check whether
a default component exists.
If `sysupdate` is called with various command line arguments to affect
how its configuration is loaded, do *not* say that a default component
exists, as these arguments essentially anull the possibility of a
default being used in that process.
Add an integration test based on the reproducer provided by the issue
reporter. This test has been tested to fail if the changes to
`sysupdate.c` aren’t applied — if so, the second call to `sysupdate
components` would return
`{"default":true,"components":["some-component"]}`.
Signed-off-by: Philip Withnall <pwithnall@gnome.org> Fixes: https://github.com/systemd/systemd/issues/41501
Philip Withnall [Thu, 21 May 2026 10:17:56 +0000 (11:17 +0100)]
sysupdate: Add a flag to control error behaviour in internal function
Optionally prevent `context_read_definitions()` erroring out if zero
transfer definitions were found.
This commit makes no functional changes (the flag is always passed to
calls to `context_make_offline()` for the moment), but the new flag will be
used in the following commit.
Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/41501
Philip Withnall [Thu, 21 May 2026 10:14:01 +0000 (11:14 +0100)]
sysupdate: Convert an internal bool argument to flags
This commit makes no functional changes, but the new flags will be
used and extended in the following commit. We need a flags variable to
avoid having two bool arguments, which would be confusing.
Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Helps: https://github.com/systemd/systemd/issues/41501
Let's return -EBADMSG if the PE headers reference stuff missing in the
file, regardless if that's because the offsets are larger than SSIZE_MAX
or just larger than the file size. We generally use EBADMSG for all
cases we deem the file to not be a conformant PE file, and these two
cases are the same. Hence, let's be systematic here.
pcrextend: add support for measuring a user record, to be executed on first login of the user
This is supposed to be useful to mark an interactive user login as a
"break glass" event in the measurement logs, i.e. as in many typically
headless scenerios this indicates debug access or similar.
sysupdate: Add separate polkit actions for cancellation (#42209)
This allows us to have a separate, more permissive, policy for
cancelling ongoing sysupdate jobs. The new default policy for
cancellation actions is to allow them for the active user, without admin
authentication, because typically the user can just pull the plug on the
computer to cancel a job anyway.
Signed-off-by: Philip Withnall <pwithnall@gnome.org> Fixes: https://github.com/systemd/systemd/issues/38568
Daan De Meyer [Thu, 21 May 2026 22:00:28 +0000 (22:00 +0000)]
efi-api: fix unaligned access in efi_guid_to_id128()
EFI_GUID requires 4-byte alignment due to its uint32_t Data1 field, but
callers may pass pointers at arbitrary offsets into serialized EFI
variable buffers (e.g. bootctl walking BootXXXX entries). UBSan flagged
the misaligned member access; the old comment claiming the struct was
packed was wrong. Copy the bytes into an aligned local first.
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
Yu Watanabe [Sun, 10 May 2026 15:26:33 +0000 (00:26 +0900)]
dhcp-server-request: rework when we should reply DHCPNAK
Previously, DHCPNAK was sent only when the client is in INIT-REBOOT
state. But, on selecting or renewing, the request is directed to a
specific server, so we can safely reply with DHCPNAK.
Also, verify existing bound lease even when there is no static lease for
the client.
Yaping Li [Sun, 10 May 2026 14:50:13 +0000 (14:50 +0000)]
logind: add ListInhibitors Varlink method
The Varlink ListInhibitors method is the counterpart of D-Bus
ListInhibitors. Like its D-Bus counterpart it is zero-filter and streams
the full list of currently registered inhibitors using the
SD_VARLINK_METHOD_MORE pattern, returning InhibitorInfo objects with
Id, What, Who, Why, Mode, UID, PID, and Since fields.
There is no D-Bus GetInhibitor getter to fold in, so no unique-key
filter is introduced here.
Yaping Li [Sun, 10 May 2026 14:50:13 +0000 (14:50 +0000)]
logind: add ListSeats Varlink method
The Varlink ListSeats method accepts an optional Id filter, folding in
the D-Bus GetSeat(s) lookup.
Passing Id yields a single reply on match, or NoSuchSeat on miss.
Passing no Id with the 'more' flag streams the full list; passing no
Id without 'more' resolves to the caller's seat (preserving the
ergonomic default of GetSeat). The Id filter supports the special
names "self" and "auto" which resolve to the caller's seat.
The SeatInfo type in the io.systemd.Login Varlink IDL carries all seat
properties matching the D-Bus org.freedesktop.login1.Seat interface.
Yaping Li [Sun, 10 May 2026 14:50:13 +0000 (14:50 +0000)]
logind: add ListUsers Varlink method
The Varlink ListUsers method accepts optional UID and PID filters,
folding in the D-Bus GetUser(u) and GetUserByPID(u) lookups.
Passing a unique-key filter (UID and/or PID) yields a single reply on
match, or NoSuchUser on miss. Passing no filter with the 'more' flag
streams the full list; passing no filter without 'more' resolves to
the caller's user (preserving the ergonomic default of GetUser). If
both UID and PID are specified they must reference the same user,
otherwise NoSuchUser is returned.
The UserInfo type in the io.systemd.Login Varlink IDL carries all
user properties matching the D-Bus org.freedesktop.login1.User
interface.
Yaping Li [Sun, 10 May 2026 14:50:12 +0000 (14:50 +0000)]
logind: add ListSessions Varlink method
The Varlink ListSessions method accepts optional Id and PID filters,
folding in the D-Bus GetSession(s) and GetSessionByPID(u) lookups.
Passing a unique-key filter (Id and/or PID) yields a single reply on
match, or NoSuchSession on miss. Passing no filter streams the full
list (requires the 'more' flag). Specifying both Id and PID acts as a
consistency check: both must refer to the same session, otherwise
NoSuchSession is returned.
The Id filter supports the special names "self" and "auto" which
resolve to the caller's session. The SessionInfo type in the
io.systemd.Login Varlink IDL carries all session properties matching
the D-Bus org.freedesktop.login1.Session interface.
Yaping Li [Sun, 10 May 2026 14:50:12 +0000 (14:50 +0000)]
logind: let {session,user,seat}_get_idle_hint() return bool and always set output timestamp
These helpers returned int but in practice only ever produced 0/1.
session_get_idle_hint() silently swallowed negative returns from
get_tty_atime()/get_process_ctty_atime() and fell through to
return false; user_get_idle_hint() and seat_get_idle_hint() propagated
errors from session_get_idle_hint() that could no longer occur.
Change all three to return bool and always set *t on non-NULL output.
session_get_idle_hint() previously did not set *t on the
!SESSION_CLASS_CAN_IDLE() early return; fix by defaulting *t at the
top of the function. Callers that read *t after a false return now see
DUAL_TIMESTAMP_NULL rather than uninitialized memory.
Update all callers: drop dead < 0 error checks, drop > 0 coercion,
drop DUAL_TIMESTAMP_NULL pre-init at sites that only pass &t to the
helpers.
Yaping Li [Tue, 28 Apr 2026 22:45:05 +0000 (15:45 -0700)]
sd-varlink: skip IDL validation on synthetic empty terminator
When a streaming method's callback returns 0 with no replies enqueued and
sd_varlink_set_sentinel() was passed NULL (POINTER_MAX), the framework
emits an empty parameters reply via sd_varlink_reply(v, NULL) as a clean
stream terminator. That call validated the empty parameters against the
method's output IDL — which always failed when the IDL declared any
mandatory output field, generating a spurious 'didn't pass validation'
warning even though the empty terminator is correct by construction.
Move the body of sd_varlink_reply() into a static varlink_reply_internal()
that takes a skip_validation flag. The public sd_varlink_reply() keeps
its existing behavior (validation enabled). Add a static
varlink_reply_terminator() helper that wraps
varlink_reply_internal(v, NULL, /* skip_validation= */ true), and route
varlink_dispatch_sentinel() through it for the synthetic empty terminator,
so both the synchronous dispatch path (varlink_dispatch_method()) and the
fiber path (varlink_fiber_entry()) skip validation in one place.
Without this, callers who want 'streaming method, possibly empty result'
semantics had to either fire a logically wrong 'NotFound'-style error
sentinel on empty (sysext.List, BootControl.ListBootEntries pattern) or
mark every output field NULLABLE just to placate the validator (the
io.systemd.Manager.EnqueueMarkedJobs approach), misrepresenting the
contract — exactly the tradeoff the FIXME in pcrlock.c flagged. With
this change, the empty terminator is invisible to the IDL validator and
the FIXME is no longer needed.
Co-developed-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Yu Watanabe [Sun, 10 May 2026 13:17:05 +0000 (22:17 +0900)]
sd-dhcp-server: use sd_dhcp_message object on sending reply
This also makes the conditions in dhcp_server_send_message() uses
the message that will be sent, rather than we received.
This does not change basic functionality, but changes/fixes several
minor behaviors, e.g.
- fix when the broadcast flag assignment,
- set server identifier in DHCPFORCERENEW.
Yu Watanabe [Thu, 7 May 2026 04:30:00 +0000 (13:30 +0900)]
sd-dhcp-server: use sd_dhcp_message to parse received messages
This is mostly refactoring. This does not change basic behavior, but
changes/fixes some minor/corner cases, e.g.
- extend the minimum default lease time from 1 second to 30 seconds, as
1 second is too short and causes the network unstable (though 30
seconds is stll too short, but hopefully that does not make the
network unstable).
- error code on broken/malicious message received may be changed.
Luca Boccassi [Thu, 21 May 2026 20:56:05 +0000 (21:56 +0100)]
bootctl: add A/B fallback for sd-boot updates (#41650)
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
```
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)
Yu Watanabe [Thu, 7 May 2026 02:59:23 +0000 (11:59 +0900)]
sd-dhcp-server: store more information in DHCPRequest
This makes DHCPRequest stores
- the message type of the received message,
- acquired address,
- found static DHCP lease,
This also moves call of dhcp_request_get_lifetime_timestamp() from
dhcp_server_ack() to dhcp_server_set_lease(), and rename
DHCPRequest.server_id -> .server_address.
Yu Watanabe [Mon, 4 May 2026 10:57:49 +0000 (19:57 +0900)]
sd-dhcp-server: refactoring for socket fd handling
This makes
- UDP socket fd is owned by IO event source,
- open RAW socket fd just before sending first packet,
- set TOS and socket priority,
- use AF_UNIX soxket pair in the unit test and fuzzer, so the unit test
can now run by unprivileged user.
bootctl: remove fallback EFI Boot#### variable on uninstall
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.
bootctl: register fallback EFI Boot#### entry on install
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.
bootctl: back up sd-boot binary to fallback path on update
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.
bootctl: add after_slot parameter to insert_into_order()
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.
bootctl: add description and ret_slot parameters to install_boot_option()
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.
Rocker Zhang [Thu, 21 May 2026 15:47:48 +0000 (23:47 +0800)]
systemctl: also attempt kexec image extraction on EINVAL
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>