Commit Graph
11307 Commits
Author SHA1 Message Date
Yu Watanabe 5768f1dd6f Final batch of conversions to option and verb macros (#42127) 2026-05-17 03:50:48 +09:00
Zbigniew Jędrzejewski-Szmek d512d3c6d3 treewide: get rid of remaing getopt/getopt_long stuff 2026-05-16 18:37:12 +02:00
Zbigniew Jędrzejewski-Szmek 39a3647539 tree-wide: get rid of old dispatch_verb
Also rename dispatch_verb_with_args to dispatch_verb.
2026-05-16 18:37:10 +02:00
Zbigniew Jędrzejewski-Szmek cf88903637 tree-wide: get rid of most uses of ALIGN_PTR
This bothered me for a while, but I didn't think too much about it and just
copied the existing usage pattern. But it really doesn't make sense. We expect
the compiler to align the section properly. But if it didn't align it, applying
alignment after the fact would just cause our pointer to point to the middle
of the structure. That'd be even worse than a misaligned pointer.

Similarly, when doing pointer arithmetic, p++ should really result in a value
with the appropriate alignment. This is the basic principle of C pointer
addition. So we really shouldn't try to adjust the pointer ourselves. At most,
we can assert that it is indeed aligned in tests.
2026-05-16 18:32:14 +02:00
Zbigniew Jędrzejewski-Szmek d71c1e7d89 Convert machinectl to option and verb macros (#42117) 2026-05-16 17:18:10 +02:00
Zbigniew Jędrzejewski-Szmek cab5c5ef81 shared/verbs: add VERB_DEFAULT_NOARG 2026-05-16 15:54:18 +02:00
Luca Boccassi c043b90187 Revert "shared/pager: add support for more(1) pager in secure mode" 2026-05-16 09:38:25 +09:00
Daan De Meyer 5e04304793 Convert homectl to option and verb macros (#42113) 2026-05-15 20:15:06 +02:00
Zbigniew Jędrzejewski-Szmek 82b87471ad shared/options: fix display of OPTION_HELP_ENTRY_VERBATIM
So far it was used only once, in bootctl. It should not be indented:
  -R --print-root-device
  -RR
  ^
2026-05-15 18:47:17 +02:00
Luca Boccassi c10c2674f3 LUO: add support for preserving third party sessions
LUO sessions cannot be nested under other sessions. This means we need
to handle them explicitly, and held them open in the shutdown binary
like we do with our own internal session, to allow services to create
their own.

The requirement to support third party sessions comes from VMMs that
wish to preserve VM(s) state(s) across kexec, as some file descriptors
(KVM's vmfd from the KVM_CREATE_VM ioctl) cannot be transfered between
processes via SCM_RIGHTS, so they cannot be stashed in the FD Store
directly. Also some file descriptors have to be handled all together or
not at all, again to do with KVM and devices that are all part of the
same vm.
2026-05-15 13:46:08 +01:00
Luca Boccassi 3dddbee290 shutdown: prepare LUO session for FD Stores before kexec
Wires up the systemd-shutdown side of the kexec-via-LUO fd store preservation.

When rebooting via kexec, systemd builds a JSON description of the fd
stores of all loaded services and passes it to systemd-shutdown through
the SYSTEMD_LUO_SERIALIZE_FD environment variable. The FDs themselves
come in as part of the normal shutdown FDSet. systemd-shutdown's job is
then, at the very last moment before invoking the kexec syscall, to
move that state into a kernel LUO session so it survives the reboot.

Doing the LUO session creation here, rather than in PID 1, is
deliberate:

 * It's the last point where we can be sure all other processes have
   already been killed, so nothing else can race us into creating (or
   worse, hijacking) the "systemd" session, as /dev/liveupdate is a
   singleton and a session name is global.
 * Any kernel-visible side effects of preserving objects (memory
   pinning etc.) are delayed until the absolute last moment, minimizing
   the window in which they could affect the running system

No behaviour change for shutdown paths other than kexec, or for kexec
when systemd didn't hand over a serialization fd (e.g. because no
service had any fds stored, or because LUO wasn't supported at
serialization time).
2026-05-15 13:46:08 +01:00
Luca Boccassi 257c35c1a3 core: support FD Store preservation through kexec via LUO
The kernel Live Update Orchestrator (LUO) exposes /dev/liveupdate, which
allows userspace to hand a set of "preservable" kernel objects to the
new kernel across a kexec-based reboot. For now it only supports memfds,
with more object types (virtio devices, etc.) expected to be added later.

This is a natural fit for systemd's FD Store feature: services hand
memfds (containing serialized state or other service data) to PID 1 via
FDSTORE=1 sd_notify() messages, and get them back on their next start.
Today this works across service restarts, soft reboots and
initrd→rootfs transitions. With LUO we can extend the same mechanism to
work across kexec, too.

The protocol on the PID 1 side works roughly as follows:

 * All preservable fds are collected into a single LUO session named
   "systemd". Each FD gets uploaded with a token. Token 0 in that session
   is reserved for a "mapping" memfd, which carries a JSON object
   describing how to dispatch the other tokens back to units on the next
   boot:

       {
         "foo.service": [
           { "type": "fd", "name": "stateA", "token": 1 },
           { "type": "fd", "name": "stateB", "token": 2 }
         ],
         ...
       }

   unit IDs are used as the unit identifier, as they're stable
   across daemon-reexec, switch-root and kexec. token refers to the
   LUO token assigned to the object in the session.

 * On shutdown for MANAGER_KEXEC, just before manager_free(), systemd
   walks all services and serializes their persistent fd store contents
   (fds + FDNAMEs + unit IDs) into a JSON memfd. The FDs themselves are
   gathered into a FDSet to be kept around. The fdset and the
   serialization memfd are passed to systemd-shutdown via the
   SYSTEMD_LUO_SERIALIZE_FD environment variable providing the fd number,
   so the actual LUO session creation and ioctls can happen as the very
   last step before kexec (shutdown implementation is the next commit).

 * On boot, manager_luo_restore_fd_stores() opens /dev/liveupdate,
   tries to retrieve the "systemd" session, reads the mapping memfd,
   then for each entry retrieves the fd from the session and attempts
   to attach it to the matching unit's fd store.

 * The FDs are injected in the appropriate unit's FD stores using the
   same mechanism as the LISTEN_FDS propagation that was set up earlier.

Non-kexec shutdown paths are unaffected: if MANAGER_KEXEC is not the
final objective, no serialization file is produced and no LUO session
is ever created. Likewise if /dev/liveupdate does not exist, nothing
happens.
2026-05-15 13:46:08 +01:00
Luca Boccassi 79ac52b810 Add helpers for talking to /dev/liveupdate
This character device is the main kernel interface to deal with the
Live Update Orchestrator (LUO) feature. Add some basic wrappers for
the ioctls.

https://docs.kernel.org/userspace-api/liveupdate.html
2026-05-15 13:46:08 +01:00
Luca Boccassi 5608575e01 core: propagate FDs from store from user to system manager
In order to allow FD Stores of user units to survive a user
session restart, propagate FDs received via the protocol up one
level from user to system manager via sd_notify.

And the other way around, propagate them down via LISTEN_FDS
tagging them with the unit name so that the child manager can
inject them in the appropriate unit.

Ensure units that are dead or not loaded can get FDs added to
their stores, and that they are correctly propagated once the
unit is started or loaded. When the unit is not loaded we don't
know what the FD max limit is, so simply increase it for each FD
injected, and then when the unit is realised prune it down to
match the unit's now available config in case the limit is lower
than the number of FDs in the store.

Each FD sent up or down is assigned a monotonic index, and the manager
also sends a JSON map that associates the index with the original
unit and FDNAME:

 {
   "unit-name.service": [
     { "name": "fdname1", "index": 1 },
     { "name": "fdname2", "index": 2 }
   ],
   ...
 }

This allows the manager to assign back the FDs to the appropriate
unit using the appropriate name, given the FDNAMEs are not unique.
2026-05-15 13:46:08 +01:00
Zbigniew Jędrzejewski-Szmek dca9ab5b92 Convert PID1 to option macros (#42099) 2026-05-15 13:39:36 +02:00
Zbigniew Jędrzejewski-Szmek dad6c71728 core: add WorkingDirectory, Environment and SetCredential{,Encrypted} to io.systemd.Unit.StartTransient (#41874)
This PR adds some more properties to the io.systemd.Unit.StartTransient
varlink interface: WorkingDirectory, Environment and
SetCredential{,Encrypted}. Its also hopefully a useful starting point to
establish a pattern to add even more.
2026-05-15 11:29:19 +02:00
Luca Boccassi f33fa0910d Implement Service Context/Runtime for io.systemd.Unit.List (#42098)
The PR implements the following objects + tests for
io.systemd.Unit.List:
- ServiceContext
- ServiceRuntime

It's hopefully the last PR of the long sequence of:

* https://github.com/systemd/systemd/pull/37432
* https://github.com/systemd/systemd/pull/37646
* https://github.com/systemd/systemd/pull/38032
* https://github.com/systemd/systemd/pull/38212
* https://github.com/systemd/systemd/pull/39391
* https://github.com/systemd/systemd/pull/41980
* https://github.com/systemd/systemd/pull/42057
2026-05-15 10:20:56 +01:00
Ivan Kruglov 69b5c5db10 core: expand ServiceContext and add ServiceRuntime for io.systemd.Unit.List
Co-developed-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-15 00:02:01 -07:00
Ivan Kruglov 5305da009a shared: add exec_command_status_build_json() and ExecCommandStatus varlink type to common
Add exec_command_status_build_json() and exec_command_status_list_build_json() to varlink-common, alongside exec_command_build_json() and exec_command_list_build_json(). The status list function is the runtime counterpart of the command list function — the two arrays are positionally aligned so index N in the status array corresponds to index N in the command array. Commands that have not yet run produce null entries to preserve alignment.

Add the ExecCommandStatus varlink struct type to varlink-idl-common next to ExecCommand. It contains PID, timestamps, and mutually exclusive ExitStatus (int, for normal exit) / ExitSignal (string, for signal kill).
2026-05-15 00:02:01 -07:00
Lennart Poettering 2289ac82d4 shared/options: introduce OPTION_ERROR
[zjs: this was originally proposed in
https://github.com/systemd/systemd/commit/930fc9d6980f27b278527b0d6117f97296fcaf6a.
I'm rescuing one chunk from that patch.]
2026-05-14 19:59:21 +02:00
Yu Watanabe f795ecc3d7 A few more conversions of options and verbs (#41795)
I had those prepared before but I didn't submit them because the
automatic layout didn't work well. In two cases now the sync of widths
between verbs and options is disabled and one case is left with the
automatic alignment. I think it'd good enough to merge.
2026-05-15 02:47:23 +09:00
Yu Watanabe d6c79c13ff Implement Socket Context/Runtime for io.systemd.Unit.List (#42057)
The PR implements the following objects + tests for
io.systemd.Unit.List:
- SocketContext
- Socket Runtime

It's a continuation of the following PRs:

* https://github.com/systemd/systemd/pull/37432
* https://github.com/systemd/systemd/pull/37646
* https://github.com/systemd/systemd/pull/38032
* https://github.com/systemd/systemd/pull/38212
* https://github.com/systemd/systemd/pull/39391
* https://github.com/systemd/systemd/pull/41980
2026-05-14 23:29:47 +09:00
Luca Boccassi df630029c5 cgroup: Add CPUSetPartition= setting (#42013)
Add support for configuring cpuset partition type via the
CPUSetPartition= unit file setting. This controls the kernel's
cpuset.cpus.partition cgroup attribute.

The setting takes one of "member", "root", or "isolated". This is
useful for real-time workloads that require dedicated CPU resources
without interference from other processes.

When set, systemd will write the partition type to the
cpuset.cpus.partition cgroup file. If the kernel rejects the value
(e.g., due to partition hierarchy rules), a warning is logged and the
unit continues with the kernel's default partition type.

Co-developed-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Gabriele Monaco <gmonaco@redhat.com>
2026-05-14 13:10:13 +01:00
Zbigniew Jędrzejewski-Szmek 92fbc11f6a shared/options: introduce OPTION_COMMON_{ENTRY_TOKEN,MAKE_ENTRY_DIRECTORY} 2026-05-14 13:00:11 +02:00
Zbigniew Jędrzejewski-Szmek 42e7fd40e0 shared/verbs: split verbs in two lines when the synopsis is > 25 characters
The help tests would not pass because in cases where the verb synopsis
is very long, we'd format the table badly if the terminal is fairly
narrow. I experimented with a few solutions, but overall, it's hard to
achieve very good layout with the automatic formatting. I think the
approach in this commit works the best: we end up with an two- or
three-line verb synopis, which is similar to what we did manually
before.

$ COLUMNS=80 build/localectl -h
...
Commands:
  [status]                      Show current locale settings
  set-locale LOCALE...          Set system locale
  list-locales                  Show known locales
  set-keymap MAP [MAP]          Set console and X11 keyboard mappings
  list-keymaps                  Show known virtual console keyboard mappings
  set-x11-keymap LAYOUT [MODEL  Set X11 and console keyboard mappings
    [VARIANT [OPTIONS]]]
  list-x11-keymap-models        Show known X11 keyboard mapping models
  list-x11-keymap-layouts       Show known X11 keyboard mapping layouts
  list-x11-keymap-variants      Show known X11 keyboard mapping variants
    [LAYOUT]
  list-x11-keymap-options       Show known X11 keyboard mapping options

I think that almost nobody actually uses an 80 column terminal, and if
they do, they probably don't spend too much time looking at our --help
output there. So the goal here is to do something reasonable and robust
and get the tests to pass.

We can use strjoina here because the strings are fully under our
control.
2026-05-14 12:59:20 +02:00
Zbigniew Jędrzejewski-Szmek 596031030e shared/format-table: shorten code a bit
Define variables at point of initialization so the whole thing is easier
to read.
2026-05-14 12:17:54 +02:00
Zbigniew Jędrzejewski-Szmek a1e6f4a15b Convert systemctl to option and verb macros (#42088)
This one was non-trivial, so it'd benefit from a close review.
2026-05-14 11:33:09 +02:00
Zbigniew Jędrzejewski-Szmek 0a33b37e93 shared/options: implement the equivalent of 'opterr'
All log messages during option parsing are emitted using log_full,
and the level is set as LOG_ERR + state->log_level_shift. The default
shift is 0, but if set to e.g. 4, we log at LOG_DEBUG, and if set
to 5 or higher, logging is effectively suppressed. (Unless compiled
with LOG_TRACE, when it'd be suppressed if the shift if set to 6
or higher.)  So this gives something like 'opterr', except that
without global state and potentially more flexible.
2026-05-14 09:15:48 +02:00
glemco 43b53679da cgroup: Add CPUSetPartition= setting
Add support for configuring cpuset partition type via the
CPUSetPartition= unit file setting. This controls the kernel's
cpuset.cpus.partition cgroup attribute.

The setting takes one of "member", "root", or "isolated". This is
useful for real-time workloads that require dedicated CPU resources
without interference from other processes.

When set, systemd will write the partition type to the
cpuset.cpus.partition cgroup file. If the kernel rejects the value
(e.g., due to partition hierarchy rules), a warning is logged and the
unit continues with the kernel's default partition type.

Co-developed-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Gabriele Monaco <gmonaco@redhat.com>
2026-05-14 08:55:44 +02:00
Lennart Poettering 94ef901b1c cpu-set-util: introduce cpus_online().
Add a helper that tries to determine the number of installed CPUs. This
borrows heavily from physical_memory(), i.e. uses the physical number,
but caps by per-container cpuset.
2026-05-14 08:12:28 +02:00
Lennart Poettering f78e7a0949 cpu-set-util: add cpu_set_count() helper
Let's add a minor, simplifying helper for getting number of CPUs in a
mask.
2026-05-14 08:12:28 +02:00
Daan De Meyer a442440315 blockdev-util: Drop name argument from BLKPG functions
We don't use it, the kernel ignores it, let's just drop
the argument. Saves callers from having to ensure the name
they pass in fits in the 64 char buffer.
2026-05-13 23:47:29 +01:00
Michael Vogt de2ad965c2 core: add SetCredentials{,Encrypted} to varlink Unit.StartTransient
This commit adds support to set `SetCredentials` and
`SetCredentialsEncrypted` in the `io.systemd.Unit.StartTransient`
varlink call.
2026-05-13 19:28:35 +02:00
Michael Vogt 27fa92aa3c core: add support for Environment in io.systemd.Unit.StartTransient
This commit adds support to set `Environment` in the
`io.systemd.Unit.StartTransient` varlink call.

The behavior is similar to D-Bus, i.e. a `null` or `[]` clears the
environment. This is not needed for StartTransient() as there the env
always starts empty but it seems a good property to have if this is
reused.
2026-05-13 19:28:35 +02:00
Michael Vogt a2aa3a7450 core: add WorkingDirectory to Unit.StartTransient
This commit adds setting the WorkingDirectory to the
`io.systemd.Unit.StartTransient` varlink call. This is a
first step towards more complete StartTransient in varlink.

The goal is to be as close as possible to the D-Bus parameters.
The exception is WorkingDirectory which is an object here so that
we avoid the `-` prefixes and use a more type-safe approach by
making it an explicit `missingOK` parameter.

The key names stay the same as the D-Bus properties (PascalCase).
If there are no equivalent D-Bus properties the native varlink
convention of camelCase is used.
2026-05-13 19:28:35 +02:00
Michael Vogt 6e8e0ba26d varlink: make fields of ExecContext nullable for partial input
We want to allow partial inputs for the ExecContext when we pass
that to io.systemd.Unit.StartTransient. So this commit makes the
fields nullable. Without this the varlink input validation will
reject partial ExecContext objects.

This is similar to 913b81735e for UnitContext.
2026-05-13 19:28:35 +02:00
Lennart Poettering 94e05aca0d copy: retire splice use() for copying files on disk
Apparently splice() is quite problematic, hence just don't anymore. It's
also unnecessary these days since either copy_file_range() or sendfile()
nowadays typically work, the splice() fallback doesn't give us much
anymore.

(At least I am not aware of a combo of fds where splice() would work
where neither cfr nor sf would work).

This leaves one use of splice() in place, in
src/shared/socket-forward.c. We should probably kill that too, but
that'd require some reworking to use sendfile() I guess, and I am too
lazy for that right now. Moreover, in contrast to the other uses it's
probably even safe, since it uses an intermediary pipe always. But what
do I know...

Fixes: #29044
2026-05-13 15:29:36 +01:00
Christian Brauner e28aab8840 RestrictFileSystemAccess= — dm-verity filesystem access enforcement via BPF LSM (#41340)
This series adds a new `RestrictFileSystemAccess=` setting in the
`[Manager]` section of `system.conf` that enforces a deny-default
execution policy: only binaries residing on signed dm-verity block
devices (and the initramfs during early boot) are permitted to execute.
Everything else — tmpfs, procfs, sysfs, anonymous executable mappings,
unsigned dm-verity devices — is denied.

The directive takes the values `no` (default), `exec` (lock down
execution), and accepts `yes` as an alias for `exec`. The name is
deliberately broader than what the initial values cover so the same
setting can grow to restrict other filesystem access categories in the
future (e.g. `any` to deny all access from untrusted filesystems, not
just execution).

### How it works

The BPF program is entirely self-contained; PID1 loads it and the kernel
does the rest. When dm-verity brings up a device, the kernel calls
`security_bdev_setintegrity()` twice during `verity_preresume()`: once
with the root hash and once with the signature validity status. Our
`lsm/bdev_setintegrity` hook captures the second call and records the
device number in a BPF hash map if the signature is valid. When a device
is torn down, `lsm/bdev_free_security` cleans up the map entry. No
userspace map population is needed at any point.

The enforcement side hooks `bprm_check_security` (execve), `mmap_file`
(PROT_EXEC mappings including shared libraries), and `file_mprotect`
(W→X transitions like JIT and libffi). Each hook resolves the file's
backing device via `file->f_inode->i_sb->s_dev` and looks it up in the
verity device map. For block-backed filesystems, `s_dev` equals
`s_bdev->bd_dev`, which avoids an extra pointer chase and NULL check on
`s_bdev` — non-block filesystems simply miss in the map and get denied
by the default policy.

During early boot the initramfs needs to be trusted as well, since it
runs before any dm-verity volume is mounted. PID1 writes the initramfs
superblock's device number into a BPF global before attaching the
programs, and clears it after `switch_root` to close the trust window.
As a prerequisite, PID1 also verifies that
`dm_verity.require_signatures=1` is active — without it, unsigned
dm-verity devices could be created, which would weaken the security
model even though the BPF program would correctly deny execution from
them.

### Surviving daemon-reexec

The BPF programs and their verity device map must survive PID1
re-execution (daemon-reexec, switch_root, soft-reboot). Without
preservation, `manager_free()` would destroy the skeleton, the link FDs
would close, programs would detach, and the map would be freed. After
exec, a fresh skeleton would have an empty map — but existing dm-verity
devices have already signaled their integrity and won't do so again. A
deny-default policy plus an empty map means all execution denied and the
system is bricked.

We solve this by serializing the raw BPF link FDs and the `.bss` map FD
across exec using systemd's existing `serialize_fd` / `fdset_cloexec` /
`deserialize_fd` infrastructure. The kernel reference chain (link FD →
`struct bpf_link` → `struct bpf_prog` → `struct bpf_map`) keeps programs
attached and map data intact as long as the dup'd FDs survive. After
exec, PID1 detects the deserialized FDs and skips skeleton re-creation
entirely. If switching root, it uses the deserialized `.bss` map FD to
clear `initramfs_s_dev` via a targeted `mmap()` write, preserving the
other guard globals in `.bss`.

We intentionally avoid bpffs pinning. Pinned objects are discoverable
and manipulable by any process with sufficient privileges
(`BPF_OBJ_GET`, unlink). FD serialization keeps everything private to
PID1 with no external attack surface.

### Self-protection

BPF LSM programs attached via the tracing trampoline (`BPF_LSM_MAC`) are
inherently tamper-resistant — `bpf_tracing_link_lops` has no
`.update_prog` and no `.detach` callbacks, so the kernel rejects
`BPF_LINK_UPDATE` with `-EINVAL` and `BPF_LINK_DETACH` with
`-EOPNOTSUPP`. Once attached, our programs cannot be modified or
detached through the `bpf()` syscall.

The remaining attack vector is map injection: `BPF_MAP_GET_FD_BY_ID` to
obtain an FD to `verity_devices`, then `BPF_MAP_UPDATE_ELEM` to insert a
fake trusted device. The self-protection guard blocks this with three
hooks. `lsm/bpf_map` fires inside `bpf_map_new_fd()`, the chokepoint for
all code paths that produce a map FD, and denies access to our map IDs
from any process other than PID1 (identified via `tgid == 1`, which is
unspoofable — `bpf_get_current_pid_tgid()` reads `current->tgid` from
`pid->numbers[0].nr`, the init-namespace PID). `lsm/bpf_prog` provides
analogous protection for program FDs as defense-in-depth. `lsm/bpf`
handles `BPF_LINK_GET_FD_BY_ID` at the command level since there is no
`security_bpf_link()` hook in the kernel.

The guard starts inactive — all protected IDs default to 0 in `.bss`,
and no real BPF object has ID 0 — so there is no window where it
interferes with PID1's own setup. After attaching all programs, PID1
queries the kernel-assigned IDs via `bpf_obj_get_info_by_fd()` and
writes them into the guard's globals. From that point on, the guard is
active. The guard has zero collateral damage: it only denies access to
our specific object IDs, leaving bpftrace, bpftool,
`RestrictFileSystems=`, and all other BPF usage completely unaffected.

Additionally, a ptrace guard (`lsm/ptrace_access_check`) blocks
`PTRACE_MODE_ATTACH` to PID1 from other processes, preventing extraction
of sensitive state from PID1's address space via ptrace, `/proc/1/mem`,
`process_vm_readv()`, or `pidfd_getfd()`. `PTRACE_MODE_READ` is allowed
so that monitoring tools and `systemctl` continue to work normally.

### Limitations

- The enforcement hooks resolve trust by looking at
`file->f_inode->i_sb->s_dev` — the device number of the superblock that
owns the file's inode. This works correctly for files directly on a
dm-verity block device, but it does not see through overlayfs. When a
file is accessed on an overlay mount, `f_inode` points to the overlay
inode, and `i_sb->s_dev` is the overlay superblock's anonymous device
number — not the underlying dm-verity device. The overlay superblock has
no backing block device, so the lookup misses in the verity map and
execution is denied by the default policy.

This means that overlayfs mounts whose lower layers are on
dm-verity-protected volumes will currently have execution blocked, even
though the actual data is integrity-protected. The correct fix requires
a kernel extension that allows the BPF program to call something like
`d_real_inode()` to resolve through the overlay to the real inode on the
underlying filesystem, and then check that inode's superblock device
number against the verity map. I plan to add a BPF kfunc exposing this
functionality in a follow-up kernel series.

- Multi-device filesystems such as btrfs use entirely synthetic device
numbers and there is no way to reach the actual device backing the inode
from the inode itself. So `RestrictFileSystemAccess=` only works
reliably with a subset of filesystems. In practice this isn't a problem
because the feature is tailored to erofs; using it on arbitrary
filesystems requires careful vetting of the actual filesystem behaviour.

- The initial implementation also blocks JIT-style execution that relies
on memory mapped executable. This is part of `exec` semantics today and
can be loosened later by introducing finer-grained values (a common
pattern in systemd — following the precedent of `ProtectSystem=`, which
started as a boolean and later grew `auto`/`yes`/`full`/`strict`
semantics).

- The configuration is a system-wide setting with no per-unit opt-out.
This is intentional for the initial implementation: a global invariant
is easier to reason about and harder to accidentally weaken. Per-unit
relaxation can be added later if a concrete need arises.

### Testing

The series includes unit tests and integration tests covering both the
core enforcement logic and the self-protection guard. The unit test
loads the skeleton, attaches programs, populates guard globals, and
verifies that protected IDs are set correctly. The integration tests
exercise the guard by attempting `BPF_MAP_GET_FD_BY_ID` and
`BPF_PROG_GET_FD_BY_ID` from a non-PID1 process and verifying that
access is denied.

What we cannot currently test end-to-end is actual execution enforcement
against a dm-verity-signed root filesystem. The systemd test suite does
not yet have infrastructure for booting a VM with a signed dm-verity
rootfs image — the existing mkosi-based test framework lacks the ability
to produce and boot such images. This will hopefully change soon when
Daan integrates barrage into the test suite.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-13 13:58:46 +02:00
Daan De Meyer 450853c72c libc,shared: detect newer library symbols at runtime via weak references (#42065)
For libc syscall wrappers (pidfd_open, fsopen, openat2, etc.) we
previously
gated the calls behind build-time HAVE_* checks. Replace these with weak
external references, falling back to the raw syscall at runtime when the
loaded glibc lacks the symbol. Drop the corresponding cc.has_function()
loop
from meson.build and disable -Wredundant-decls /
readability-redundant-declaration
for src/libc/ via meson c_args and a local .clang-tidy.

For optional libraries (libcryptsetup, libdw, libarchive), drop the
per-symbol
HAVE_* checks. Always declare the prototypes, suppressing the
redundant-decl
warnings via DISABLE_WARNING_REDUNDANT_DECLS and NOLINT, and resolve the
symbols after the main dlopen via a new DLSYM_OPTIONAL() helper that
only
assigns on success. libarchive's *_is_set wrappers now use fallback
functions
as their pointer initializers, so call sites never need to NULL-check.

The same treatment applies to pidfd_spawn / posix_spawnattr_setcgroup_np
in
process-util.c and epoll_pwait2 in sd-event.c. coredump-config and
coredump-submit get a dlopen_dw_has_dwfl_set_sysroot() helper. The kexec
arch gate now uses defined(__NR_kexec_file_load) directly; pidfd.h uses
__has_include_next() to decide whether to pull in glibc's header.

This lets binaries built against newer glibc / libcryptsetup / libdw /
libarchive headers still load and run on older targets where these
symbols
are absent.
2026-05-13 13:06:35 +02:00
Zbigniew Jędrzejewski-Szmek a1f20a0746 Convert loginctl to option and verb macros (#42066) 2026-05-13 11:22:12 +02:00
Christian Brauner 4684df8f97 core: work around btf_ctx_access() rejection of const void * in BPF LSM
Kernels before v6.16 (missing commit 1271a40eeafa "bpf: Allow access to
const void pointer arguments in tracing programs") have a bug in
btf_ctx_access() where const void * parameters in LSM hook signatures
are not recognized as void pointers. The function checks t->type == 0
to detect void *, but for const void * the BTF chain is PTR -> CONST ->
void, so t->type points to the CONST node rather than directly to
type_id 0. This causes the verifier to reject any BPF program that
reads the const void *value argument of bdev_setintegrity:

  func 'bpf_lsm_bdev_setintegrity' arg2 type UNKNOWN is not a struct
  invalid bpf_context access off=16 size=8

Work around this by providing a compat variant of the
bdev_setintegrity BPF program that avoids reading the const void *value
argument entirely. Instead it reads the size argument (a scalar integer)
directly from the raw BPF context (ctx[3]), which is not subject to the
broken type check. This is safe because dm-verity guarantees that value
and size are always in lockstep: both NULL/0 for unsigned devices, both
non-zero for signed devices.

The loader tries the full version first (which reads both value and size
for defense-in-depth) and falls back to the compat variant if loading
fails. bpf_program__set_autoload(false) disables whichever variant is
not needed so the verifier never sees it.

This compat logic can be removed once the minimum kernel baseline
includes the 1271a40eeafa fix.

Signed-off-by: Christian Brauner <brauner@kernel.org>
2026-05-13 10:36:13 +02:00
Christian Brauner 6c0f509587 core: preserve RestrictFileSystemAccess= BPF state across daemon-reexec
The BPF link and .bss map FDs must survive PID1 re-execution
(daemon-reexec, switch_root, soft-reboot). Without serialization,
manager_free() closes them before execv, programs detach, and the
verity_devices map is freed. After exec a fresh skeleton would have
an empty map — but existing dm-verity devices have already called
bdev_setintegrity and won't call it again. The result would be a
deny-default policy with an empty map, i.e., all execution denied
and the system bricked.

Add serialize/deserialize support using systemd's existing
serialize_fd / fdset_cloexec / deserialize_fd infrastructure:

Before exec (in manager_serialize via bpf_restrict_fsaccess_serialize):
  - Dup each link FD and the .bss map FD into the FDSet
  - fdset_cloexec(fds, false) + execv() preserves them across exec

After exec (in manager_deserialize + bpf_restrict_fsaccess_setup):
  - Deserialize the link FDs and .bss map FD into the Manager struct
  - bpf_restrict_fsaccess_setup() detects the deserialized FDs and skips
    skeleton re-creation entirely — the programs are already attached
  - If no longer in initrd, clear initramfs_s_dev in the kernel map

No bpffs pinning is needed. This avoids a bpffs mount dependency and
eliminates the external attack surface that pinned objects would create
(discoverable/manipulable via unlink or BPF_OBJ_GET). The FDs remain
private to PID1.

Signed-off-by: Christian Brauner <brauner@kernel.org>
2026-05-13 10:36:12 +02:00
Christian Brauner 68fe7fa4d6 core: add RestrictFileSystemAccess= BPF LSM for dm-verity execution enforcement
Add a new RestrictFileSystemAccess= boolean setting in the [Manager] section of
system.conf that enforces execution only from signed dm-verity block
devices and the initramfs during early boot.

When RestrictFileSystemAccess=yes is set, PID1 loads a BPF LSM program early in boot
that:

Integrity tracking (self-populating, no userspace involvement):
 - bdev_setintegrity: records dm-verity signature status in a BPF hash
   map when the kernel signals device integrity via
   security_bdev_setintegrity()
 - bdev_free_security: removes devices from the map on teardown

Execution enforcement (deny-default policy):
 - bprm_check_security: blocks execve() from untrusted sources
 - mmap_file: blocks PROT_EXEC mmap (shared libs, anonymous exec memory)
 - file_mprotect: blocks W->X transitions (JIT, libffi, etc.)

Trust anchors:
 - Signed dm-verity volumes (sig_valid flag in the BPF map)
 - Initramfs (s_dev captured at load time, cleared after switch_root)
 - Everything else is denied (tmpfs, procfs, sysfs, anonymous PROT_EXEC)

PID1 requires dm-verity require_signatures=1 to be enabled and refuses
to load the BPF program otherwise, ensuring the kernel enforces that all
dm-verity devices carry valid signatures.

After attach, PID1 extracts owned FDs from the skeleton (link FDs +
.bss map FD) and lets the skeleton be destroyed. The dup'd link FDs
keep programs attached via the kernel reference chain (link FD ->
bpf_link -> bpf_prog -> bpf_map). Destroying the skeleton unmaps the
.bss page from PID1's address space so no BPF state is readable via
/proc/1/mem. The .bss map FD is retained for targeted writes (clearing
initramfs_s_dev after switch_root via mmap).

Signed-off-by: Christian Brauner <brauner@kernel.org>
2026-05-13 10:36:12 +02:00
Daan De Meyer f795d54591 libc,shared: detect newer library symbols at runtime
For libc syscall wrappers (pidfd_open, fsopen, openat2, etc.) we previously
gated the calls behind build-time HAVE_* checks. Replace these with shim
functions in src/libc/ that fall back to the raw syscall at runtime when the
loaded glibc lacks the symbol. The infrastructure lives in src/libc/libc-shim.h:
DEFINE_SYSCALL_SHIM falls back to a direct syscall, DEFINE_LIBC_SHIM returns
ENOSYS (for posix_spawn-family helpers that have no corresponding syscall), and
DEFINE_LIBC_ERRNO_SHIM sets errno=ENOSYS and returns -1 (for read/write-style
helpers). The weak reference to the libc symbol is bound via __asm__(\"name\")
rename so the bare libc identifier never appears as a C token — this avoids
both #undef boilerplate against override-header redirects and the resulting
-Wredundant-decls warning. Drop the corresponding cc.has_function() loop from
meson.build.

For optional libraries (libcryptsetup, libdw, libarchive), drop the per-symbol
HAVE_* checks. Always declare the prototypes, suppressing the redundant-decl
warnings via DISABLE_WARNING_REDUNDANT_DECLS and NOLINT, and resolve the symbols
after the main dlopen via a new DLSYM_OPTIONAL() helper that only assigns on
success. libcryptsetup's crypt_set_keyring_to_link / crypt_token_set_external_path
and libarchive's *_is_set wrappers use fallback functions as their pointer
initializers (returning -ENOSYS and 0 respectively), so call sites can invoke
the symbol unconditionally and just check for -ENOSYS where the \"not supported\"
distinction matters.

The same shim treatment applies to pidfd_spawn / posix_spawnattr_setcgroup_np
(src/libc/spawn.c) and epoll_pwait2 (src/libc/epoll.c), with corresponding
override headers in src/include/override/spawn.h and
src/include/override/sys/epoll.h. posix_spawn_wrapper() in process-util.c and
epoll_wait_usec() in sd-event.c now detect ENOSYS in the return value instead
of checking the function pointer, falling back to plain posix_spawn() and
epoll_wait() respectively. coredump-config and coredump-submit get a
dlopen_dw_has_dwfl_set_sysroot() helper. The kexec arch gate now uses
defined(__NR_kexec_file_load) directly; pidfd.h uses __has_include_next() to
decide whether to pull in glibc's header.

This lets binaries built against newer glibc / libcryptsetup / libdw /
libarchive headers still load and run on older targets where these symbols are
absent.
2026-05-13 10:29:50 +02:00
Zbigniew Jędrzejewski-Szmek e794b904da shared/verbs: allow all groups to be named
When verb groups were added, I assumed that the first group will always
by the unnamed group, or in other words, that VERB_GROUP() line cannot
appear first. This provides an additional check on the whether the verbs
haven't been reordered by the compiler or linker. But that check is weak
and we can do a better check anyway. And this limitation is unexpected,
since we allow that for OPTIONs. The code should all work without an
unnamed group, once this assertion is removed.
2026-05-13 09:00:41 +02:00
Christian Brauner fe18d94610 vmspawn: add io.systemd.MachineInstance.ReplaceStorage (#42017)
A follow-up to the AddStorage / RemoveStorage series. ReplaceStorage
swaps the *backing file* of an already-attached storage device on a
running vmspawn-managed VM, leaving the guest-visible device frontend
(virtio-blk, virtio-scsi, nvme, scsi-cd) and every other property of
the device untouched. The intended use is to point an existing disk
at a new image without the guest seeing a hot-unplug/hot-plug cycle.

The signature mirrors AddStorage minus the 'config' field: the
device frontend doesn't change, only the backing behind it. Read-
only / read-write is derived from the new fd's O_ACCMODE; scsi-cd is
forced read-only to match the boot-time policy. S_ISBLK on the new
fd selects host_device vs file driver, matching AddStorage.

The QMP primitive is blockdev-reopen. It cannot change a file /
host_device node's 'filename' so we can't just point the existing
file node at a new fd, but it can swap a format node's 'file' child
to a different existing monitor-owned node by node-name reference
(case 3 in qemu/qapi/block-core.json:5034-5040). The chain is:

  add-fd          (host fd → new fdset)
  blockdev-add    (new file node, filename=/dev/fdset/N — fd-only)
  remove-fd       (release monitor's ref; new file holds the dup)
  blockdev-reopen (format node, file = new file node-name)
  blockdev-del    (old file node; its dup release frees old fdset)

The reopen options must restate every option the original blockdev-
add emitted on the format node — blockdev-reopen resets any
unspecified option to its driver default. The 'file' field is a
node-name string reference, never a path.

No new errors and no new IDL types beyond the method itself;
everything is built on the existing NoSuchStorage / StorageImmutable
/ NotConnected / EBUSY vocabulary.

The series is:

  vmspawn: split blockdev-add into separate file and format calls
      Preparatory refactor. qemu/blockdev.c:3440 only marks the
      top-level BDS returned by blockdev-add as monitor-owned;
      inline children are NOT, so blockdev-del later rejects them
      with "Node X is not owned by the monitor". Split into two
      blockdev-add calls so the file node is independently
      deletable. DriveInfo gains qmp_file_node_name and a
      file_generation counter; the teardown helper deletes format
      then file (file-first is rejected as "node used as 'file'
      of Y"). The ephemeral path was already structured this way;
      only the regular add path changes. Drops the now-unused
      qmp_build_blockdev_add_inline().

  shared/varlink-io.systemd.MachineInstance: add ReplaceStorage method
      IDL only: ReplaceStorage(fileDescriptorIndex, name). No new
      errors.

  vmspawn: implement io.systemd.MachineInstance.ReplaceStorage
      vmspawn_qmp_replace_block_device() entry point, ReplaceCtx
      (refcounted, ReplaceCtxStateFlags for partial-state tracking)
      and four async callbacks plus an idempotent replace_fail.
      file_generation is bumped before issuing blockdev-add so
      retries don't collide on node-name.
      BLOCK_DEVICE_STATE_REPLACE_PENDING gates concurrent
      Replace / Remove on the same drive. On reopen success the
      trailing blockdev-del of the old file node fires from the
      reopen callback; its failure logs a warning and still replies
      success (the swap already committed; the orphan resolves at VM
      exit). QMP disconnect mid-replace routes via
      qmp_client_fail_pending → replace_fail → NotConnected.

  test: integration test for io.systemd.MachineInstance.ReplaceStorage
      TEST-87-AUX-UTILS-VM.replace-storage covers happy-path replace,
      successive replaces (file_generation rotation), StorageImmutable
      rejection on the boot-time drive, NoSuchStorage on unknown
      names, InvalidParameter on malformed names, and clean
      RemoveStorage after a replace (proves the new file node is
      monitor-owned and the teardown order works). Backing files are
      passed via 'varlinkctl --push-fd'; no machinectl front-end is
      added in this round.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-13 08:48:38 +02:00
Zbigniew Jędrzejewski-Szmek 735a5c10c5 report-basic: expose os-release fields as a metric (#41988)
Add io.systemd.Basic.OSRelease metric family that reports all the fields
in os-release.
2026-05-13 08:07:27 +02:00
Zbigniew Jędrzejewski-Szmek 5cba9704ad report: drop MetricsFamilyContext, CGroupContext, CGroupInfo
Previously, we passed around information about the MetricFamily'ies
and the varlink connection in a helper structure. Having a hybrid of
const static and runtime stuff is iffy. Let's simplify things by passing
two separate parameters.

Also, in report-cgroup.c we built a cache of parsed values. This
requires additional storage requirements and introduces complexity when
dealing with population of the cache at the appropriate time.
This cache is not useful: for each cgroup, we generate a list of
metrics, and we have all the information at hand. The only reason
why we'd create the cache and not generate all the relevant replies
at once was that the helper functions called the .generate function
for each MetricFamily separately.

The MetricFamily interface is changed, so that metrics can be
defined without a .generate function. This is understood to mean
that the preceding metric family's .generate function will also
genarate this family. This allows us to define related metrics
nicely in a table:
  { METRIC_IO_SYSTEMD_CGROUP_PREFIX "CpuUsage", generate_func },
  { METRIC_IO_SYSTEMD_CGROUP_PREFIX "IOReadBytes", NULL },
  { METRIC_IO_SYSTEMD_CGROUP_PREFIX "IOReadOperations", NULL },
  { METRIC_IO_SYSTEMD_CGROUP_PREFIX "SomethingElse", generate_func2 },
  ...
When implementing .Describe, we list all the families. When implementing
.List, we only call those with .generate, and we get the same results
as before.

This allows the .generate functions to be simplified: instead of
keeping state, they just spit out all the metrics for a given
object in a tight loop.
2026-05-12 22:56:04 +02:00
Christian Brauner 91575e3a5a varlink-io.systemd.MachineInstance,vmspawn: treat AddStorage/RemoveStorage name as opaque
The 'name' field on AddStorage and RemoveStorage was documented as
'<provider>:<volume>' and enforced via machine_storage_name_split() at
the varlink boundary. That form is only the convention machinectl
inherits from the StorageProvider routing path; the API itself only
needs a unique identifier the caller can re-use to detach the binding.

Drop the strict format check, require only a non-empty string, and
update the IDL docs to describe the field as a caller-supplied
identifier with machinectl's convention as a non-normative example.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:08 +02:00
Christian Brauner 3063448b51 shared/varlink-io.systemd.MachineInstance: add ReplaceStorage method
Define the IDL for io.systemd.MachineInstance.ReplaceStorage, a
runtime hot-swap of an already-attached storage volume's backing
file. The signature mirrors AddStorage minus the 'config' field
because the device frontend (virtio-blk, virtio-scsi, nvme, scsi-cd)
does not change — only the backing file behind it.

The implementation lives in vmspawn (next commit) and uses QMP
blockdev-reopen to swap the file child of the existing format node.
The reused error vocabulary (NoSuchStorage, StorageImmutable,
NotConnected, plus the generic errno path) covers every failure
mode; no new errors are added.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
2026-05-12 22:54:07 +02:00