Philip Withnall [Mon, 13 Jul 2026 14:55:41 +0000 (15:55 +0100)]
sysupdate: Allow multiple documentation URLs for a feature
Change the varlink API for Feature structs to allow multiple
documentation URLs for them, to match what systemd already does for
units etc.
This is a deviation from what the sysupdated D-Bus API allows and, for
the moment, from what’s supported internally by sysupdate. Internally it
continues to support 0-1 URLs for now.
But by defining the API as a strv, multiple URLs can be supported in
future without API breaks.
Philip Withnall [Thu, 9 Jul 2026 12:55:01 +0000 (13:55 +0100)]
sysupdate: Run ListFeatures in offline mode
Historically, the ListFeatures API (in both D-Bus and now varlink) was
run without an `--offline` flag. This appears like it’s an oversight, but
actually `verb_features()` always unconditionally loaded in offline
mode.
In any case, there doesn’t appear to be a reason for the context to be
online (i.e. for it to check sources for available updates). Features are
defined in local config files and are loaded by `read_features()`, which
is called in both offline and online mode.
When the varlink ListFeatures API was added, it was put into online mode
in order to match the lack of `--offline` argument in the existing D-Bus
API implementation. Change both of them to be explicitly in offline mode.
Signed-off-by: Philip Withnall <pwithnall@gnome.org>
Philip Withnall [Mon, 22 Jun 2026 19:20:20 +0000 (20:20 +0100)]
sysupdate: Downgrade an info to a debug log message
Since we now enumerate all targets on a varlink call (to validate the
requested target), this message gets printed multiple times in the log.
It’s not really necessary, so downgrade it to a debug message.
repart: Fix growing the partition preceding a FreeArea from leftover space (#42969)
The "Donate to preceding partition" logic is dead code since commit
https://github.com/jonas2515/systemd/commit/19903a433507897449c086b72abb5e133e431336
("repart: split out context_grow_partition_one()").
context_grow_partition_one() gets passed a free area and a partition,
and it has
an early-return check to ensure the partition it got passed belongs to
the free
area it got passed. That means we compare the FreeArea a to the FreeArea
a->after->allocated_to_area, which always yields FALSE.
Fix the behavior of donating any left over space to the preceding
partition
by adding that partition to the loop below (and relying on the
partitions list
being ordered according to physical partition offsets).
Since this behavior is not that easy to trigger, mention how to trigger
it in a
comment, and add a test for it as well.
When no prefix is specified, portablectl inspect first tries the
prefix derived from the image name. This keeps inspect aligned with
attach behavior.
If that lookup finds no matching units, retry with validated
PORTABLE_PREFIXES read from the image os-release. This makes inspect
work for images whose filename does not match their portable service
prefix.
Keep metadata error handling explicit so request-construction failures
are not logged twice, while sd_bus_call() failures still include the
inspect context.
Add a TEST-29-PORTABLE regression case for a directory image whose
name does not match the portable service prefix.
man: clarify that --when= is a lower bound, not a condition
`systemctl reboot --when=yesterday` reboots the machine immediately, which
surprised users enough to be reported as a bug. It is not one: the timestamp
passed to --when= (and to ScheduleShutdown(), and to shutdown(8)) declares the
earliest point in time the action may be taken, it is not a condition that is
evaluated and that could fail.
Behaving any differently would be racy and surprising: "--when=now" refers to
the past by the time the request is processed, and "--when=+50ms" may well have
elapsed already due to scheduling latencies. In both cases we must still carry
out the action the user asked for.
Document the semantics explicitly in systemctl(1), shutdown(8) and the
org.freedesktop.login1(5) D-Bus interface documentation.
boot: skip boot counter logic for entries marked read-only
If the read-only FAT file attribute is set on a boot entry file with a
counter in its name, don't attempt to rename it, but simply skip the
boot counter logic for it, taking the flag as a hint that the entry
shall not be subject to boot assessment.
boot-secret: don't initialize secret mixin file if marked read-only
If the boot secret mixin file exists but is empty we'd normally fill it
with a fresh mixin. Refrain from that if the read-only FAT file
attribute is set on it, taking that as a hint that the file shall not
be initialized. Reading an existing, fully populated mixin file remains
unaffected by the flag.
boot: skip random seed handling if seed file is marked read-only
If the read-only FAT file attribute is set on /loader/random-seed,
don't update the seed file — and hence don't use it either, since a
seed we cannot update would be the same on every boot.
This gives users an explicit way to turn off random seed handling by
marking the file read-only, useful for example in pre-built OS images
that are replicated to many systems, where the baked-in seed is shared
and hence must not be credited.
The check is done upfront in process_random_seed(), before any other
work, mirroring the existing check for read-only volumes. This covers
both systemd-boot and systemd-stub, which share this code.
Before, the command timed out with exit status 124 while the service
stayed active. --wait waits for deactivation, but RemainAfterExit=yes
keeps the service active after the command exits.
Reproducer:
sudo systemd-run --wait --verbose --json=short /bin/echo hi
Before, systemd-run printed JSON metadata to stdout while --verbose
also spawned journalctl output on stdout. The resulting stream mixed JSON
with journal lines, so reject the conflicting options.
Reproducer:
sudo systemd-run --scope --json=short /bin/echo hi
Before, systemd-run printed JSON metadata to stdout and then executed
the scope command on the same stdout. The combined stream was not valid
JSON, so reject --json= in scope mode.
Before, trigger mode accepted --json=short but printed only human-readable
"Running timer as unit" and "Will run service" lines. Reject the option
until trigger mode has structured output.
Reproducer:
sudo systemd-run --wait --pipe --json=short /bin/echo hi
Before, systemd-run wrote its JSON metadata to stdout and then passed
the command stdout through the same stream. The combined output was not
valid machine-readable JSON, so reject the conflicting modes.
Before, the command ran as nobody but kept the caller supplementary
root group, for example groups=65534(nogroup),0(root). Scope mode
performs the uid/gid switch locally, so initialize the target user groups
before dropping privileges.
Before, an explicit .path unit name was not recognized as the
trigger unit. It was mangled again as a service name, so PID 1 rejected
the transient request with an already-loaded unit conflict.
Before, the option was accepted but had no effect because scope mode
executes the command locally after creating the scope. The flag is only
encoded into service ExecStart properties, so accept it only where it can
be applied.
Before, systemd-run still waited for the trigger unit job and
propagated the socket start failure. With --no-block it should only
verify and enqueue the request, as the service path already does.
userdb: suppress userdb queries for backends indicating uid/gid/name range info via xattrs on entrypoint sockets (#42961)
Let's optimize userdb queries a bit: by encoding the covered UID/GID
ranges and user/group name patterns on the varlink entrypoint sockets
for userdb backends we can make them wake up less and reduce the work
triggered by queries.
repart: allow empty EncryptedVolume= volume name (#42889)
Treat an empty volume name alongside other fields as unset instead of
rejecting it as invalid.
Example use case:
```
EncryptedVolume=:none:discard
```
In this case, the volume name is not specified so it can be generated as
luks-UUID.
From the docs:
> EncryptedVolume=
> Specifies how the encrypted partition should be set up. Takes at least
one and at most four fields separated with a colon (":"). The first
field specifies the encrypted volume name under /dev/mapper/. If not
specified, "luks-UUID" will be used where "UUID" is the LUKS UUID.
Jonas Dreßler [Mon, 13 Jul 2026 14:37:56 +0000 (16:37 +0200)]
repart: Make use of partitions list being ordered by offsets while looping
Let's do the same thing we did above with the last commit here too and make use
of the partitions list being ordered by physical offsets. This makes the code a
little simpler.
Jonas Dreßler [Mon, 13 Jul 2026 11:56:03 +0000 (13:56 +0200)]
repart: Fix growing the partition preceding a FreeArea from leftover space
The "Donate to preceding partition" logic is dead code since commit 19903a4 ("repart: split out context_grow_partition_one()").
context_grow_partition_one() gets passed a free area and a partition, and it has
an early-return check to ensure the partition it got passed belongs to the free
area it got passed. That means we compare the FreeArea a to the FreeArea
a->after->allocated_to_area, which always yields FALSE.
Fix the behavior of donating any left over space to the preceding partition
by adding that partition to the loop below (and relying on the partitions list
being ordered according to physical partition offsets).
Since this behavior is not that easy to trigger, mention how to trigger it in a
comment, and add a test for it as well.
process-util: introduce prctl_safe() and port everything over to it (#43006)
prctl() is an API full of pitfalls: it is variadic, and some interfaces
don't expect zero-initialization of excess arguments, and others do.
Moreover, the parameters are "long", and nonetheless we usually pass
"int" to them. If we are too dumb to call it properly, let's just not
call it directly anymore, but let's add a wrapper around it that makes
the function non-variadic and declares the right types. Then, let's port
over everything to it.
This is inspired by #42996, but we had issues with this many times,
before and looking at this PR one can see that we otherwise still are
having the issue at numerous other places.
cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment (#41859)
The current TPM2 PIN mode is flawed as a compromised TPM directly
exposes
the sealed secret which is the LUKS volume key itself
(https://github.com/systemd/systemd/pull/27502 and
https://github.com/systemd/systemd/issues/37386).
Goal: add Argon2id-based PIN hardening to TPM2 enrollment, making
the TPM a second factor rather than a single point of failure:
1. Password + salt → Argon2id → 512-bit key split into Key1 + Key2
2. Key2 (base64-encoded) is used as the PIN to seal a random secret
in the TPM
3. Key1 + unsealed secret → HKDF-SHA256 → final LUKS volume key
This implementation ensures that if the TPM is compromised, an attacker
still needs the password to derive Key1 and combine it with the unsealed
secret.
The --tpm2-with-pin= option now accepts three values:
- false (no PIN used)
- true (PIN hardened with Argon2id - default)
- "direct" (legacy PIN without Argon2id for backward compatibility)
These default to a function of available CPUs and physical memory, with
a benchmark that scales iterations to the target time (default: 2s) and
falls back to ARGON2ID_PARAMETERS_DEFAULT (64 MiB, 8 iter, 4 lanes) when
auto detection fails.
Also if the runtime OpenSSL lacks Argon2id support (< 3.2), the feature
silently falls back to direct PIN mode with a warning.
Added includes:
- src/cryptenroll/cryptenroll.c: cpu-set-util.h, limits-util.h,
time-util.h
for Argon2id benchmark auto-tuning (cpus_online, physical_memory_scale,
now/usec_t)
- src/cryptenroll/cryptenroll-tpm2.c: crypto-util.h for
Argon2IdParameters
struct in load_volume_key_tpm2()
- src/shared/tpm2-util.h: crypto-util.h for Argon2IdParameters in
tpm2_make_luks2_json() API
- src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for
kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
sysupdate: add support for --component-all + --feature-all + --feature-suggested to enable-feature verb
This adds similar concepts as we already have for enable-component:
let's add a way to enable all features or the suggeste dones, and
possibly on all components.
Or in other words, with this:
systemd-sysupdate enable-component -S
systemd-sysupdate enable-feature -A -s
We'll automatically enable all suggested components, and all features of
them.
sysupdate: implement --component-all and --component-suggested for enable-component and disable-component
Let's now introduce "systemd-sysupdate enable-component
--component-suggested" and systemd-sysupdate enable-component
--component-all" for enabling all or all suggested components at once.
Similar, if used for disable-component will disable all components or
those not suggested.
sysupdate: add a "suggests" concept to features and components
Let's make it possible to "suggest" that certain features or components
are enabled under some conditions.
For this, both features and components gain two things:
1. A Suggested= field which takes a boolean. If true the
feature/component will be suggested for installation, if false it
will not.
2. A set of SuggestedOnXYZ= settings are modelled after ConditionXYZ= in
unit files (and implement a subset of them), will suggest some
component/feature under specific conditions.
The result of the condition is shown in the various output tools.
3) timer_time_change() fires and calls timer_enter_waiting(t, true)
Because time_change=true, v->next_elapse is not recalculated and
keeps the original M0 + 60s value. The timer then correctly computes
the remaining time as ~12.8 seconds:
Jul 08 06:58:13 ...: Monotonic timer elapses in 12.785674s.
4) Timer elapses at 06:58:26
Jul 08 06:58:26 ...: Timer elapsed.
timer_enter_running() sets last_trigger.monotonic to CLOCK_MONOTONIC
(which equals to M0 + 12s before suspend + 13s after suspend, thus
+ 25s)
5) Unit deactivates at 06:58:29
timer_trigger_notify() calls timer_enter_waiting(t, false) -
time_change=false; that means that this time v->next_elapse gets
recalculated:
base = inactive_exit_timestamp.monotonic = M0 (i.e. when the timer was originally armed)
usec_shift_clock(M0, CLOCK_MONOTONIC, CLOCK_BOOTTIME_ALARM)
a = now(CLOCK_MONOTONIC) = M0 + 28s
b = now(CLOCK_BOOTTIME_ALARM) = M0 + 64s
result = b - (a - M0) = (M0 + 64s) - 28s = M0 + 36s => the time spent in suspend
is false, because the v->next_elapse (M0 + 96s) is not less than the
current time (M0 + 64s), so the timer is re-armed again:
Jul 08 06:58:29 ...: Monotonic timer elapses in 33.544681s
Let's mitigate this by skipping the next elapse timestamp recalculations
for one-shot timers for which we've already calculated the value in this
activation cycle.
Let's the common function for querying the local thread name.
This changes the escaping rules when the therad name is not quite
kosher: previously we'd just escape invalid UTF-8 charcaters, now we do
what we usually do: also escape control characters and such, and limit
us to ASCII.
The description generated here is mostly for debug purposes, and process
names should normally not require this escaping anyway (it's mostly
paranoia), hence I think this change in behaviour should be fine, it's
not part of the API in any form.
With 2c6f9af8e5425c2086fbc8ca496843f162e4af9b sd-varlink gained protocol
upgrade support, however only in a synchronous fashion. This adds
asynchronous protocol upgrade for the server side, thus enabling
multiplexing daemons (i.e. those that handle multiple connections from
the same event loop) to support protocol upgrades too.
I plan to use this in the upcoming "ptybroker" component that allows
acquiring a pty through varlink.
Jonas Dreßler [Thu, 9 Jul 2026 13:22:13 +0000 (15:22 +0200)]
repart: Don't copy trailing padding when using --copy-from=
Currently, --copy-from= copies the paddings in between the source partitions,
as well as the trailing padding that is at the end of the source partition table.
It doesn't copy the leading padding at the start of the source partition table
though.
This seems inconsistent, and likely it was an oversight that the trailing padding
is copied.
Fix that, and add a test to ensure we don't regress.
Jonas Dreßler [Tue, 7 Jul 2026 11:15:36 +0000 (13:15 +0200)]
repart: Clarify and test that --copy-from= argument respects grain size
The --copy-from= argument currently is documented as "copied partitions will have
the same size". This doesn't hold true in the case where a different grain-size is
passed to repart. Because `partition_min/max_size()` currently do rounding, the
size is implicitly rounded to grain size, and therefore partitions are enlarged
to align to grain size whenever possible.
Clarify this behavior and change the manpage, and also add a test for it.
Jonas Dreßler [Tue, 7 Jul 2026 12:41:16 +0000 (14:41 +0200)]
repart: Don't get old grain size from fdisk for --copy-from=
This is a little bit confusing, but grain size is not actually stored in the gpt
metadata. Rather, fdisk's `get_grain_size()` returns an autodiscovered "optimal io
size" value as grain size. This might not actually be the grain size that the
disk we're copying is using.
Since we're setting the padding of the copied partitions using that value from
fdisk, we're rounding the new paddings by fdisk's optimal grain size, which is
usually 1MiB (a lot more then the default 4KiB that we're using otherwise).
Set the grain size here to 1 byte instead, ensuring that the min/max padding set
is exactly the padding that was present before.
Also add a test to confirm the behavior is fixed: The test calls --copy-from= on
an existing disk with 4MiB grain size, and because we pass --grain-size=512, now
no rounding should happen and the paddings should be transferred to exactly the
same size.
tree-wide: add missing libopenssl_cflags to targets using tpm2-util.h
tpm2-util.h includes crypto-util.h, which requires OpenSSL compiler
flags when HAVE_OPENSSL is defined. Add libopenssl_cflags to every
build target that uses tpm2_cflags but was missing the transitive
OpenSSL dependency.
fuldeka [Sun, 28 Jun 2026 14:42:47 +0000 (16:42 +0200)]
cryptsetup: add Argon2id-based PIN mode for TPM2 enrollment
The current TPM2 PIN mode is flawed as a compromised TPM directly exposes
the sealed secret which is the LUKS volume key itself (#27502 and #37386).
Goal: add Argon2id-based PIN hardening to TPM2 enrollment, making
the TPM a second factor rather than a single point of failure:
1. Password + salt -> Argon2id -> 512-bit key split into Key1 + Key2
2. Key2 (base64-encoded) is used as the PIN to seal a random secret
in the TPM
3. Key1 + unsealed secret -> HKDF-SHA256 -> final LUKS volume key
This implementation ensures that if the TPM is compromised, an attacker
still needs the password to derive Key1 and combine it with the unsealed
secret.
The --tpm2-with-pin= option now accepts three values:
- false (no PIN used)
- true (PIN hardened with Argon2id - default)
- "direct" (legacy PIN without Argon2id for backward compatibility)
These default to a function of available CPUs and physical memory, with
a benchmark that scales iterations to the target time (default: 2s) and
falls back to ARGON2ID_PARAMETERS_DEFAULT (64 MiB, 8 iter, 4 lanes) when
auto detection fails.
Also if the runtime OpenSSL lacks Argon2id support (< 3.2), the feature
silently falls back to direct PIN mode with a warning.
Added includes:
- src/cryptenroll/cryptenroll.c: cpu-set-util.h, limits-util.h, time-util.h
for Argon2id benchmark auto-tuning (cpus_online, physical_memory_scale,
now/usec_t)
- src/cryptenroll/cryptenroll-tpm2.c: crypto-util.h for Argon2IdParameters
struct in load_volume_key_tpm2()
- src/shared/tpm2-util.h: crypto-util.h for Argon2IdParameters in
tpm2_make_luks2_json() API
- src/shared/tpm2-util.c: limits-util.h, tpm2-util.h for physical_memory() validation
of Argon2id memory cost and function prototypes
- src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c: crypto-util.h for
kdf_argon2id_derive()/kdf_hkdf_sha256() on the token unlock path
homed: set user.userdb xattrs on homed's userdb socket
We can't make a lot of restrictions here, since users are allowed to
basically freely pick their user names and UIDs too. But let's at least
exclude system UID ranges and those beyond the 16bit range.
userdb: suppress lookups outside of indicated ranges
When doing a userdb lookup we might end up issuing a lot of IPC calls in
parallel to backends and cause them all to do work, with us waiting for
it. Let's optimize this a bit, and indicate on the socket inodes via
xattrs hints which kind of records are provided by a backend. That way
we can suppress lookups to them and optimize runtime behaviour.
This only works on Linux 7.1 and newer where socket inodes gained
support for extended attributes.
This are to the existing sd_varlink_call_and_upgrade() what
sd_varlink_callb() and sd_varlink_callbo() are to sd_varlink_call():
they put together an object on the fly, via the usual JSON builder
logic.
ci: check 'update-man-rules' to ensure it is not forgotten
We often forget to run this command when updating manpages, and notice
only much later. Add a step in the CI to flag it, as we already do for
the D-Bus docs.
Cache the IPv6 enabled state while sizing and filling NSS result
buffers, so a transient sysctl read result cannot change the tuple or
address layout after the scratch buffer size has been computed. Also
zero-initialize gaih_addrtuple records before filling IPv4 addresses.
Automatic key modes tolerate a failed TPM2 sealing attempt and
fall back to a host or null key. Do not consume TPM2 blob output in
that case, tpm2_seal() leaves it empty on failure, so the fallback
path should continue without TPM2 metadata.
NDisc packets received through the socket path are filtered before
parser dispatch, but parser entry points should still reject malformed
packet bytes instead of asserting on them. Return EBADMSG for non-zero
ICMPv6 code values in RA, NA, and Redirect messages.
An OCI index redirect already carries the digest of the selected
manifest. Store it in the expected checksum field so pull-job
verifies the downloaded manifest instead of overwriting the digest
with the computed checksum before comparison.
efi_get_variable() allocates one byte for probing whether efivarfs
has grown since fstat(), then three more bytes for NUL termination.
Account for both sizes separately so a full readv() result is treated
as concurrent growth and retried before the terminators are written.
network: do not regenerate MAC address if already set by userspace
When MACAddress= is unspecified, a stable MAC is generated to seed a newly
created netdev. Since existing netdevs are reconfigured on reload/restart,
this seed got re-applied to an already existing interface, conflicting with
an explicit MACAddress= from the matching .network file and flipping MAC.
Now, when MACAddress= is unspecified, only generate a new address if
addr_assign_type is not NET_ADDR_SET (i.e. not already set by userspace). On
first creation the interface does not exist yet, so this is a no-op and the
address is generated as before. Mirrors src/udev/net/link-config.c.
Popax21 [Thu, 19 Mar 2026 02:33:26 +0000 (03:33 +0100)]
creds-util: implement TPM2 SRK pinning
Stores the TPM2 SRK within the credential header, allowing for parameter decryption to be utilized when decrypting the credential.
A new dimension is added to the credential ID matrix to encode this capability.
This also allows for usage of TPM2-bound credentials when a TPM owner password is set since `Esys_CreatePrimary` is no longer used for sealing credentials.
Michael Vogt [Fri, 10 Jul 2026 15:27:11 +0000 (17:27 +0200)]
creds: allow normal users to encrypt with`--with-key=null`
When encrypting with the `--with-key=null` option systemd-creds
is currently doing the encryption via IPC. This is not needed
for the null key, no privs are required so we can just do the
in-process operation. So instead simply check for the null-key and
if its requested use the in-process path.
* b322b8d98e Install new files for upstream build
* 3fd1b81c94 Update changelog for 261.1-3 release
* 8f95f1370c Move modules-load from systemd package to udev package
* bcdf90c670 debian/libpam-systemd.postinst: pam-auth-update does not use getopt
* 743e3399ac d/README.source: document policy for adding new binary packages
* cbea74783c Install new files for upstream build
* ef267b3ad6 debian/libpam-systemd.postinst: run pam-auth-update with --root=$DPKG_ROOT
* 54df2859b4 d/t/upstream: use mkosi from archive for Ubuntu autopkgtest
* 21959e8a59 Fix zsh installation path, again
* f01dddb047 Fix zsh installation path
* e31737edc4 Install new files for upstream build
* ca0630a51e Update changelog for 261.1-2 release
* 1ebb987599 d/t/control: pull in cpio for upstream suite
* fcf5a24f47 Two more fixups for d/copyright
* 6ad198086d d/t/control: pull new packages in upstream test suite
* bb9fd757fd Make systemd actually temporarily depend on systemd-tpm
credentials: add policy that can allow key=null creds from the ESP (#42555)
This PR only sets the default to "relaxed" - I can change the default
to "tofu" if desired. But for that we will also need to update the NEWS
file to ensure everyone is aware of this new default.
---
This PR adds a new `systemd.credentials-boot=` kernel
commandline that allows to control if credentials with
a `null` key are accepted.
The possible options are:
* strict: always insist on tpm encryption
* tofu: allow null encryption in firstboot mode and when no tpm is
available
* relaxed: allow null encryption when sb is off, or no tpm is available
* off: allow null encryption always
The default is `relaxed` which is exactly the behavior we had before.
This replaces the initial idea of using plaintext credentials
at firstboot (thanks to Lennart for this nicer and simpler design).
---
With that we can drop `- firstboot: optionally accept credentials at
firstboot without authentication` from TODO.md
These are philosophically similar concepts: one deletes old versions
based on whether they are old, and the other deletes old files based on
whether they are orphaned, let's list them together.
Let's switch to string_is_safe(), and make this available to the rest of
the sysupdate sources too.
This both relaxes and tighten the rules slightly. i.e. control character
and stuff are no longer allowed, but valid UTF-8 (as opposed to ASCII)
now is.