93958 Commits
Author SHA1 Message Date
Yu Watanabe 95e60821f7 fuzz-execute-serialize: use ASSERT_OK() and friends
Replace assert_se() with the typed ASSERT_* macros, so that a failing
assertion logs the values involved.
2026-11-05 03:21:37 +09:00
Yu Watanabe 94bdace8d1 fuzz-efi-string: use ASSERT_OK() and friends
Replace assert_se() with the typed ASSERT_* macros, so that a failing
assertion logs the values involved.
2026-11-05 03:21:37 +09:00
Yu Watanabe 38d6199fe4 fuzz-efi-osrel: use ASSERT_OK() and friends
Replace assert_se() with the typed ASSERT_* macros, so that a failing
assertion logs the values involved.
2026-11-05 03:21:37 +09:00
Yu Watanabe d357624819 fuzz-bcd: use ASSERT_OK() and friends
Replace assert_se() with the typed ASSERT_* macros, so that a failing
assertion logs the values involved.
2026-11-05 03:21:37 +09:00
Yu Watanabe af23e453a5 test: use ASSERT_IOVW_EQ()
Replace ASSERT_TRUE(iovw_equal(...)) with the new dedicated macro, which
shows which elements diverge on assertion failure.
2026-11-05 03:21:37 +09:00
Yu Watanabe ac5bb0b1f5 tests: add ASSERT_IOVW_EQ()
Assert that two iovec wrappers are equal and, on mismatch, show which
elements diverge via hexdump_iovw_compare(). More useful than
ASSERT_TRUE(iovw_equal(...)).
2026-11-05 03:21:37 +09:00
Yu Watanabe 45d43b8912 hexdump: add hexdump_iovw_compare()
Add a helper dumping the differing elements of two iovec wrappers,
element by element, reusing hexdump_memcmp(). Meant for assertion
failure messages.
2026-11-05 03:21:37 +09:00
Yu Watanabe df7f23cc2c test: use ASSERT_MEMEQ_BYTE()/ASSERT_MEMEQ_ZERO()
Replace ASSERT_TRUE(memeqbyte(...)) and ASSERT_TRUE(memeqzero(...)) with
the new dedicated macros, which show which bytes diverge on assertion
failure.
2026-11-05 03:21:31 +09:00
Yu Watanabe fe2987979b tests: add ASSERT_MEMEQ_BYTE() and ASSERT_MEMEQ_ZERO()
Assert that a memory region consists entirely of a given byte (or zero)
and, on mismatch, show which bytes diverge via hexdump_memcmp_byte().
More useful than ASSERT_TRUE(memeqbyte(...)) / ASSERT_TRUE(memeqzero(...)).
2026-11-05 03:21:31 +09:00
Yu Watanabe 7ff3f83104 hexdump: add hexdump_memcmp_byte()
Add a helper comparing a memory region against a region of the same
length filled with a single byte, reusing hexdump_memcmp() to show which
bytes diverge. Meant for assertion failure messages.
2026-11-05 03:21:31 +09:00
Yu Watanabe 8d52bb5507 test: use ASSERT_MEMEQ()/ASSERT_MEMEQ_NN()/ASSERT_IOVEC_EQ()
Replace ASSERT_EQ(memcmp(...), 0), ASSERT_EQ(memcmp_nn(...), 0),
ASSERT_OK_ZERO(memcmp_nn(...)), ASSERT_EQ(iovec_memcmp(...), 0) and
ASSERT_TRUE(iovec_equal(...)) with the new dedicated macros, which show
where the contents diverge on assertion failure.
2026-11-05 03:21:23 +09:00
Yu Watanabe 824ef8cc1e tests: add ASSERT_MEMEQ(), ASSERT_MEMEQ_NN() and ASSERT_IOVEC_EQ()
Compare memory regions / iovecs for equality and, on mismatch, show
where the contents diverge via hexdump_memcmp(). More useful than
ASSERT_TRUE(memcmp(...) == 0).
2026-11-05 03:05:09 +09:00
Yu Watanabe b26ac01e42 hexdump: add hexdump_memcmp()
Add a helper producing a hexdump(3)-like side-by-side dump of two memory
regions, marking the bytes that differ. Meant for assertion failure
messages.
2026-11-05 03:05:09 +09:00
Yu Watanabe 9b4509d2a6 hexdump: move hexdump() to its own source file under src/shared/
hexdump() is only used by test and fuzz code, not by anything in
src/basic/, so move it out of basic/hexdecoct into a new shared/hexdump.
This makes room for test helpers built on top of it to live next to it.
Move the corresponding test from test-hexdecoct to a new test-hexdump
too.

No functional change.
2026-11-05 03:05:09 +09:00
Yu Watanabe a3e51c63c7 boot: rename hexdump() to hexdump16()
The EFI hexdump() returns a char16_t hex string, unrelated to the
hexdump() in src/shared/ that writes a hexdump to a FILE*. Rename it to
hexdump16() so the two can coexist once shared code (via tests.h) pulls
in the latter.
2026-11-05 03:05:09 +09:00
Yu Watanabe bee7cf80db include: update kernel headers from v7.4-rc1 (#44634) 2026-11-05 02:43:00 +09:00
zouge666 1decc15e90 networkd: Cancel stale timezone requests
An older SetTimezone request may fail after a newer lease has changed the cached timezone.
Its error handler then clears the new value, so the current timezone is not retried until another lease update.

Keep the old request until its replacement has been queued successfully,
then cancel it. Release the completed request slot in the reply handler.
2026-11-05 01:54:46 +09:00
Daniel Rodríguez Fernández d5e1afb553 zsh: Add missing --boot-loader-entry option in systemctl 2026-11-04 17:26:16 +01:00
Daan De Meyer 696175871c ci: give the Claude reviewers the UAPI specifications
The review agents rely on memory when checking UAPI specifications. Let’s
clone the specifications alongside the kernel and libc sources and tell the
agents to consult them when a finding depends on a specification.
2026-11-04 15:31:40 +01:00
Anton Skorup c98072b0d8 test: cover loading a Type=dbus unit while GetId() is pending
Pause dbus.service with SIGSTOP across a daemon-reexec so that
bus_init_api() sets m->api_bus and queues the asynchronous GetId() call
while m->api_bus_ready remains false. Start a Type=dbus service over the
private bus during this window, resume dbus.service with SIGCONT, and
verify that the service reaches running, stops when its bus name is
released, and logs no duplicate NameOwnerChanged match subscription
error.

Signed-Off-By: Anton Skorup <anton.skorup@axis.com>
2026-11-04 15:04:50 +01:00
Anton Skorup 767a7c2110 core: Install D-Bus name match if API bus is ready
Since 1166f4472d ("core/dbus: do not block the manager on GetId during
bus (re-)connection"), bus_init_api() sets m->api_bus before the
asynchronous GetId call completes and defers bus_setup_api() until the
reply is processed by api_bus_instance_id_reply().

If a unit watching a D-Bus name (e.g. a Type=dbus service) is loaded
while the GetId reply is still pending, unit_watch_bus_name() sees
u->manager->api_bus != NULL, calls unit_install_bus_match() immediately,
and inserts the unit into u->manager->watch_bus. When the GetId reply
later arrives, bus_setup_api() iterates over m->watch_bus and calls
unit_install_bus_match() a second time for the same unit, which fails
with -EBUSY ("Failed to subscribe to NameOwnerChanged signal for '...':
Device or resource busy").

Check u->manager->api_bus_ready (introduced in 266b3e5021) instead of
u->manager->api_bus in unit_watch_bus_name() so that matches are only
installed directly once bus_setup_api() has already run, and are
otherwise left for bus_setup_api() to install when the API bus setup
completes.

Signed-off-by: Anton Skorup <anton.skorup@axis.com>
2026-11-04 15:04:44 +01:00
Luca Boccassi 4118674169 core: apply sandboxing settings to io_uring operations (#44617)
io_uring operations bypass several sandboxing settings today.
IORING_OP_SOCKET ignores RestrictAddressFamilies=, IORING_OP_OPENAT can
create SUID files despite RestrictSUIDSGID=, and SystemCallFilter=
doesn't
cover any operation that can do the work of a system call it denies.

Since Linux 7.0, a task can register classic BPF filters for io_uring
operations, and every ring that the task or its children create
afterwards
inherits them. Let's use them in the executor, so that these three
settings
also apply to the equivalent io_uring operations. We also add
IOUringFilter=, which takes an allow or deny list of io_uring
operations,
for operations like IORING_OP_URING_CMD that have no system call of
their
own.
"systemd-analyze io-uring-filter" lists the operations and the system
calls that SystemCallFilter= maps them to.

Once the executor registers io_uring filters, the kernel refuses
task-wide IORING_REGISTER_RESTRICTIONS for the service with EPERM. The
man
pages document that. On older kernels, the existing settings keep their
current behavior, and IOUringFilter= denies io_uring_setup() with
ENOSYS,
so that programs fall back to regular system calls.
2026-11-04 14:01:26 +00:00
Daan De Meyer 5163ed26cc nsresourced: add an inode-based BPF policy
The path policy rejects persistent OverlayFS uppers even for clients whose
identity mappings record only stable IDs. Let's choose inode hooks when
the kernel passes them mount idmaps (Linux 7.4 and newer), so we can check
ownership on the actual upper filesystem for each operation. We keep the
path policy for kernels without those signatures.

Let's preserve the policy maps and retire the old path attachments only
after the inode policy is active. We classify mapped IDs rather than the
caller's IDs, since a stable ID can map into a transient range.

PR #44620 restricts O_TMPFILE, RENAME_WHITEOUT and Btrfs subvolume creation
outside the path creation hooks. Let's use the inode policy's ownership
checks in those hooks too. We can't use inode_rename() for whiteouts,
because it gets neither the rename flags nor the mount idmap.

A link adds a name for an existing inode, so the inode policy checks the
owner of that inode instead of the caller's fsuid. Otherwise a client can
switch its fsuid to another mapped ID and link a file owned by its
transient ID onto persistent storage.

nsresourced reuses the program links it pinned earlier, so a restart after
an upgrade keeps the old programs attached until the next reboot. Let's
attach replacement programs before publishing their pins together, so a
failed upgrade leaves the previous policy attached.

The regression tests cover identity and idmapped OverlayFS uppers, unsafe
work directories, unnamed files, whiteouts, links, ownership changes, POSIX
queue lifetimes and failed pin replacement. Like nsresourced, the tests
remove the map entries of a namespace when they're done with it. Otherwise
a later namespace can reuse the inode number and match a stale entry.
2026-11-04 14:54:08 +01:00
Daan De Meyer 4b10f0a649 nsresourced: test POSIX queue namespace lifetime
POSIX queue creation bypasses the path hooks. Its resource accounting must
keep the creator's user namespace alive while the queue exists, or
nsresourced could recycle IDs still recorded in the queue's inode.

Let's check that no namespace-free notification arrives after the creator
exits and the namespace descriptors are closed, then require the
notification after unlinking the queue.
2026-11-04 14:54:08 +01:00
Daan De Meyer bdd20e5ff6 nsresourced: restrict Btrfs subvolume creation ioctls
Btrfs subvolume creation bypasses the path and inode creation hooks. A
managed user namespace can therefore leave a subvolume root owned by its
transient range on a file system that outlives the namespace.

Let's apply the existing creation restrictions to the native and compat
ioctl calls for subvolume creation. The tests check persistent and idmapped
mounts, nested user namespaces, unrelated ioctls and other file systems.

The compat ioctl hook is absent on older kernels. Let's preserve the
native policy when that hook isn't available.
2026-11-04 14:54:08 +01:00
Daan De Meyer 0e78427b8c install: support enablement shipped by the vendor (#44606)
Distributions install .wants/, .requires/, and .upholds/ symlinks below
/usr/lib/systemd/system/, and PID 1 starts those units at boot.
systemctl
doesn't know about them: is-enabled reports the units as disabled, and
disable
can't turn them off.

Let's report such units as enabled, but only for a symlink that enable
would
create, i.e. one in a directory that WantedBy=, RequiredBy=, or
UpheldBy= in the
[Install] section names. Any other vendor symlink is a static
dependency, and
we leave it alone. remote-cryptsetup.target has
WantedBy=multi-user.target, and
systemd installs a symlink to it in initrd-root-device.target.wants/.
is-enabled now exits 0 for these units where it used to exit 1, which
packaging
scripts may notice.

Disable can't remove the vendor symlinks, since /usr/ is often read-only
and an
image update would bring them back. So we mask them with a symlink to
/dev/null
in /etc/ instead, and enable removes those masks again. We don't mask
where
something else already exists in /etc/, since it already shadows the
vendor
symlink. The changes list gains "mask-dependency" and
"unmask-dependency", so
that callers can tell a mask from an unlink.

Disable also stops removing masks. Today, disabling a template removes
the
masks of its instances, which unmasks them.

A preset policy of "disable" leaves vendor enablement alone. Masking it
would
stop units from starting at boot on every system whose preset files
don't name
the units the vendor enabled. #43319 builds on this and adds systemctl
--vendor, which applies presets to /usr/ at image build time.

Fixes: #43908
2026-11-04 14:51:23 +01:00
Yu Watanabe 9d70bd8bcb wifi-util: update string tables
Follow-up for the kernel header update to v7.4-rc1.
2026-11-04 22:11:44 +09:00
Yu Watanabe d7c073ba1a include: drop redundant KEY_SPEC_BPF_KEYRING override
The uapi keyctl.h now carries KEY_SPEC_BPF_KEYRING, hence the override
is not necessary anymore.
2026-11-04 22:11:44 +09:00
Yu Watanabe 9e35acef06 include: update kernel headers from v7.4-rc1 2026-11-04 22:11:43 +09:00
Yu Watanabe 8a606b2512 Cleanups for logging (#44631) 2026-11-04 21:11:22 +09:00
Yu Watanabe abb49d8e5a firstboot: write settings and custom keys as a PE addon into an image (#44462)
`systemd-firstboot --image=` customizes an image by writing into its
/etc. That doesn't work for images whose root file system is signed,
which is exactly the kind of image you'd want to hand out and customize
per deployment. Everything firstboot configures is also picked up as
credentials at boot though, so the settings can just as well travel in a
systemd-stub addon on the ESP, which isn't covered by the signature.

This adds `--addon=NAME` for that. Instead of mounting the image, the
settings are turned into the credentials of the same meaning (plus
`system.machine_id` for the machine ID), packed into a cpio in the
`.initrd` section of a addon, a kernel command line goes into
`.cmdline`, and the result ends up as `loader/addons/NAME.addon.efi` in
the ESP. The image is never mounted for that, the partition table is
dissected to find the ESP and mtools does the writing.
`--key=[NAME=]PATH` adds custom keys, say a certificate for verifying
extension images, as `keyring.NAME` credentials. The name on the
consuming side isn't settled yet, so treat that as a placeholder.

The addon is unsigned, whether the stub picks it up is up to the Secure
Boot policy, see #44362 for the stub side of this.
2026-11-04 20:29:36 +09:00
Yu Watanabe 605bdc187c test: add test for ENOTCONN after bus is closed
Follow-up for 6cadc7f86b.

Cover the -ENOTCONN path added for sd_bus_negotiate_timestamp() and
sd_bus_negotiate_creds(). This would have caught a regression to the
old assert_return() abort.
2026-11-04 19:50:25 +09:00
Christian Brauner d0f0c1290a test: cover tmpfile and whiteout restrictions
The restriction tests don't cover unnamed files or whiteout creation.
Let's check that O_TMPFILE and RENAME_WHITEOUT follow the named-file
restrictions on host, private and idmapped file systems, including
O_TMPFILE with O_EXCL.

Let's probe O_TMPFILE support before checking the host directory and use
link_fd() so the positive link checks also work on kernels before 6.10.
2026-11-04 11:40:27 +01:00
Christian Brauner 075c364273 nsresourced: refuse RENAME_WHITEOUT where mknod is refused
renameat2() with RENAME_WHITEOUT leaves a whiteout in place of the
source. That's a character device owned by the caller's fsuid and fsgid
which the file system creates from its ->rename() method, so it never
passes the path_mknod hook. Since Linux 5.8 this doesn't require any
privileges either.

So a managed user namespace that can rename an entry on a file system
that outlives it, e.g., in a world-writable directory the user prepared
from the outside, can leave a whiteout owned by its transient range
behind.

The only hook that sees this is path_rename, which gets the flags.
Refuse RENAME_WHITEOUT there whenever validate_mount() would refuse a
mknod in the source directory.
2026-11-04 11:40:27 +01:00
Christian Brauner 304c9d3d9e nsresourced: refuse O_TMPFILE where named files are refused
O_TMPFILE creates an inode without calling the path creation hooks, so a
managed user namespace can leave a file owned by its transient range on a
file system that outlives it. O_EXCL prevents linking the inode, but a host
process can reopen it through /proc/self/fd without retaining credentials
that keep the creator's user namespace alive.

Let's apply the named-file restrictions from the file_open hook to every
O_TMPFILE, including O_EXCL, so an unnamed file can't outlive its IDs.
2026-11-04 11:40:27 +01:00
Daan De Meyer 846cafe9ae analyze: add io-uring-filter verb
IOUringFilter= takes the names of io_uring operations, and
SystemCallFilter= denies an operation if it denies one of the system calls
that the operation can do the work of. Users can only find the operation
names and the system calls of each operation in the source code.

Let's add "systemd-analyze io-uring-filter", which lists the operations
that systemd knows, together with the system calls of each operation,
like "systemd-analyze syscall-filter" does for the system call groups.
2026-11-04 10:54:29 +01:00
Daan De Meyer 9dcd9ffd89 core: add IOUringFilter=
SystemCallFilter= can allow or deny io_uring as a whole, but not
individual io_uring operations. It only denies an operation together with
the system calls that the operation can do the work of. Some operations,
such as IORING_OP_URING_CMD, which passes commands to drivers, or the
zero-copy network operations, can thus only be denied together with
ioctl() or the socket calls.

Let's add IOUringFilter=, which takes an allow or deny list of io_uring
operations, like SystemCallFilter= does for system calls. On kernels
without io_uring BPF filters, we can't enforce IOUringFilter=, so we deny
io_uring_setup() with ENOSYS instead. Programs fall back to regular
system calls when io_uring_setup() fails with ENOSYS.
2026-11-04 10:54:29 +01:00
Daan De Meyer f80d935027 core: apply SystemCallFilter= to io_uring operations
Many io_uring operations can do the work of a system call:
IORING_OP_OPENAT opens a file like openat(), IORING_OP_SOCKET creates a
socket like socket(), and so on. SystemCallFilter= doesn't apply to
io_uring operations. A service that can use io_uring can thus do things
that its system call filter denies.

On kernels with io_uring BPF filters, let's map each operation to the
system calls that it can do the work of, and deny the operation if the
filter denies any of them. IORING_OP_URING_CMD maps to ioctl() and also to
setsockopt(), getsockopt(), and getsockname(), because it runs those
socket calls on sockets.

We don't deny operations that we don't know about, even for an allow list.
A denied request fails with EACCES, while the kernel keeps reporting the
operation as supported, so programs couldn't fall back. Our seccomp allow
lists return ENOSYS for unknown system calls for the same reason.

We skip system calls that are denied with an error number of 0. A denied
io_uring request fails with EACCES and can't pretend to succeed. We also
don't register any io_uring filters if the system call filter denies
io_uring_setup(), because the service can't create rings then.
2026-11-04 10:54:29 +01:00
Daan De Meyer d2fe659ff6 core: apply RestrictSUIDSGID= to io_uring opens
RestrictSUIDSGID= uses seccomp to check the mode argument of open(),
openat(), and creat(). The IORING_OP_OPENAT and IORING_OP_OPENAT2
operations of io_uring don't go through those system calls, so they can
still create SUID and SGID files.

On kernels with io_uring BPF filters, let's refuse IORING_OP_OPENAT and
IORING_OP_OPENAT2 requests that create a file with the SUID or SGID bit
set. The io_uring filter can read the flags and mode of IORING_OP_OPENAT2,
so we don't have to deny IORING_OP_OPENAT2 entirely, like we do for
openat2().
2026-11-04 10:54:29 +01:00
Daan De Meyer b29c8d6a1e core: apply RestrictAddressFamilies= to io_uring sockets
RestrictAddressFamilies= only filters the socket() system call. A service
can still create sockets of any address family with the IORING_OP_SOCKET
operation of io_uring, and the man page says so.

On kernels with io_uring BPF filters, let's register a filter that checks
the address family of IORING_OP_SOCKET requests against the same list.
Unlike the seccomp filter, the io_uring filter works on all architectures,
including the ones that create sockets with socketcall().

Once a task has io_uring filters, the kernel refuses task-wide
IORING_REGISTER_RESTRICTIONS for it with EPERM. The service can still
restrict its own io_uring instances and add more BPF filters. We document
the EPERM in the man page.
2026-11-04 10:54:29 +01:00
Daan De Meyer 3726468d4a shared: add helpers to register io_uring BPF filters
Since Linux 7.0, a task can register classic BPF filters for io_uring
opcodes with io_uring_register(), and every ring that the task or its
children create afterwards inherits the filters. Let's add helpers that
deny opcodes, restrict the address families of IORING_OP_SOCKET, and
refuse IORING_OP_OPENAT and IORING_OP_OPENAT2 requests that create SUID or
SGID files. We'll use them to apply the sandboxing settings of services to
io_uring.

An allow list of opcodes also denies the opcodes that the kernel
implements but we don't know about. We register a deny filter for each
unknown opcode rather than use IO_URING_BPF_FILTER_DENY_REST. That flag
only applies to opcodes without a filter. A filter that the service
manager inherited would otherwise exempt its opcode from the allow list.

We probe for support in a child process. A nested service manager can run
under a seccomp filter that kills the process on io_uring_register(). A
probe in the executor itself would then kill the executor, and the
service would fail to start. We treat a killed child as missing support.
The child marks itself as not dumpable. The kernel then writes no core
dump when the seccomp filter kills it.
2026-11-04 10:54:29 +01:00
Yu Watanabe be0825c33a imds: turn off IPv4 reverse path filtering on IMDS links in locked mode (#44614)
With `systemd.imds.network=locked`, imdsd cannot reach IMDS at all. Our
sysctl.d/50-default.conf enables IPv4 reverse path filtering for all
links. Locked mode adds a "prohibit" route for the IMDS address to the
main table. imdsd's marked sockets use their own table, so its
_requests_ get out. But IMDS's _replies_ carry no mark: the reverse path
filter checks their source against the main table, finds the prohibit
route, and drops them.

To fix this, turn off reverse path filtering on the configured links in
locked mode. The prohibit route still blocks direct access for everybody
else.

Caveat: This does not help where `net.ipv4.conf.all.rp_filter` is on, as
the kernel applies the maximum of that and the link's setting. Ubuntu
does that by default, so for now this only works out of the box on
Debian, Fedora, etc. Cover that case in the tests, and expect a
timeout/failure with `all.rp_filter > 0`.

Fixes #44602
2026-11-04 17:52:25 +09:00
Yu Watanabe 289f03b077 tree-wide: use log_syntax_parse_error() for config parse error logging
Replace open-coded "Failed to parse ..." log_syntax() calls in config
parser callbacks with the log_syntax_parse_error() /
log_syntax_parse_error_full() helper, which emits a uniform message and
handles the return value and ENOMEM consistently. As the helper already
treats -ENOMEM as critical and routes it through log_oom(), the now
redundant "if (r == -ENOMEM) return log_oom();" guards right before it are
dropped.

Settings that carry secret values (WakeOnLanPassword=, the MACsec key and
the WireGuard keys) log a "(secret)" placeholder instead of the actual
value.

Addresses https://github.com/systemd/systemd/pull/44629#discussion_r4463122734.
2026-11-04 17:01:29 +09:00
Yu Watanabe 7ad370b36f core: do not pass the scheduling policy number as error to log_syntax()
config_parse_exec_cpu_sched_policy() passed the valid, non-negative policy
value as the error argument of the "Unsupported CPU scheduling policy"
warning, producing a bogus ERRNO= field (e.g. SCHED_DEADLINE=6 logged as
ENXIO). Pass 0 instead.
2026-11-04 16:56:10 +09:00
Yu Watanabe cf505e49ac network: reject out-of-range values in gred, teql and batadv parsers
These parsers logged an "ignoring assignment" warning for out-of-range
values but then stored the value anyway. Return early after the warning so
the invalid value is actually ignored.
2026-11-04 16:55:58 +09:00
Yu Watanabe b3783c5aa9 network: do not reset the parsed Pacing= value in the fq qdisc
parse_tristate() already stores the parsed value into fq->pacing. The
following assignment overwrote it with the function's 0 return value, so
Pacing=yes disabled pacing and an empty value stored 0 instead of leaving
it unset. Drop the stray assignment.
2026-11-04 16:55:36 +09:00
Adrian Reber bbb570204f test: add an integration test for systemd-firstboot --addon=
Creates an image with an ESP via repart, writes an addon into it with
all settings systemd-firstboot can provision, and checks what ended up
in the image: the sections of the (unsigned) addon, and the credentials
in its cpio. Also covers replacing an addon, a second one next to it,
the combinations that are refused, and an image without an ESP.
2026-11-04 07:13:41 +00:00
Adrian Reber 4fafdc5f27 firstboot: add --key= to provision kernel keyring certificates
Add --key=KEYRING:[NAME=]PATH to include certificates for the kernel
keyrings systemd-keyring-setup manages, e.g. for verifying extension
images, in the addon written by --addon=, as credentials
keyring-setup.KEYRING.NAME, which that service picks up at boot.
2026-11-04 07:13:41 +00:00
Adrian Reber 96d6d09360 firstboot: write the settings as a PE addon into an image
An image whose root file system is signed cannot be customized by
writing into its /etc, which is what --image= does so far. Everything
systemd-firstboot configures is however also consumed as credentials at
boot, so the settings can just as well travel in a systemd-stub addon
on the boot partition, which is not covered by the signature.

Add --addon=NAME for that: instead of mounting the image, turn the
settings into the credentials of the same meaning (plus
system.machine_id for the machine ID), pack them into a cpio in the
.initrd section of an addon, put a kernel command line into its
.cmdline section, and write it as loader/addons/NAME.addon.efi into the
partition the stub loads addons from, i.e. the XBOOTLDR partition if
the image has one and the ESP otherwise, via mtools after locating the
partition in the dissected partition table. The root password is
always hashed first. Prompting, copying from the host and the options
that modify files in the image are refused in this mode, as the image
is never looked into.

The addon is not signed. Whether the stub picks it up depends on the
Secure Boot policy of the system.
2026-11-04 07:13:41 +00:00
Adrian Reber ec05e04dd5 firstboot: validate locale and shell syntax already when parsing
--keymap=, --timezone= and --hostname= validate their argument when the
command line is parsed, --locale=, --locale-messages= and --root-shell=
do not: their values are only checked against what is installed in the
target, where that is possible. Check the syntax while parsing, like
the others do.
2026-11-04 07:13:41 +00:00