This time, various commands with non-trivial requirements are handled.
So the series is shorter, but has a pattern of feature additions and
then those new features being used. The way that --help output is
formatted is changed a bit in various places. Those changes are
described in individual commits.
22 'static int help()' functions left after this.
As discussed in the PR, those fields can go down, so they cannot be advertised
as type COUNTER. But the type is shared for the whole family, so we need to
create a new family to keep the other ones as COUNTER and add the new ones
as GAUGE.
kernel/sched/cputime.c says above get_cpu_idle_time_us() and get_cpu_iowait_time_us():
> Note that this is partially broken due to the counter of iowait tasks that
> can be remotely updated without any synchronization. Therefore it is possible
> to observe backward values within two consecutive reads.
steal is apparently also unreliable because it is reported by the hyperviser.
So let's be cautious and report it as gauge too.
Extend the io.systemd.Basic metric families with host-level runtime
metrics: cumulative per-state CPU seconds, memory in use (MemTotal minus
MemAvailable), swap in use, the kernel's 10s pressure stall averages
(cpu/memory/io x some/full). Partitions and stacked devices are excluded
from the disk counters, since their traffic is already accounted for in
the parent device.
Also extend the TEST-74 report subtest with checks for the new metrics.
Network metrics are not added, because the service runs with
PrivateNetwork=yes, and it doesn't make sense to report stats for 'lo'.
After some back-and-forth, I settled on "CPUUsage". We use this scheme of
capitalization in other metric family names: "IPv4Public", "IPv6Public",
IPv4AddressState", "IPv6AddressState", "OSRelease" "SMBIOS" "BootID",
"CPUsOnline", "MachineID", "MachineInfo", "OSRelease",
"SMBIOS.SystemManufacturer", "TPM2.Manufacturer", "TPM2.VendorString",
"IOReadBytes", "IOReadOperations", "CGroup". The exception is
"io.systemd.cgroup.CpuUsage". I think we should treat that one as a
mistake and consider changing it to conform.
Almost all programs are simple and have only a single command. When
printing options, let's just use the first COMMAND block found by default.
This means we can drop the explicit program-name argument and reduce
copy&paste boilerplate.
This adds an initial TPM2 implementation of the io.systemd.Report.Signer
interface, which returns a set of signed TPM attestations that also
includes the supplied report digest.
The returned report consists of multiple components:
- A signed PCR quote generated with TPM2_Quote.
- Signed NvPCR attestations generated with TPM2_NV_Certify.
- A signed audit session attestation generated with
TPM2_GetSessionAuditDigest.
Each signed report component includes the JSON encoded TPMS_ATTEST
attestation structure and corresponding signature, in both PEM encoded
form and as a JSON encoded TPMT_SIGNATURE structure.
NvPCR report components also include the readable name of the NvPCR, the
NV index public area as the JSON encoded TPMS_NV_PUBLIC structure, and some
additional authenticated data that is digested and supplied as the
qualifying data to the TPM2_NV_Certify command. This authenticated data
includes the NvPCR name and priority.
The report digest that is supplied via varlink is supplied as the
qualifying data to the audit session attestation.
The returned report also includes the event log which is retrieved from
pcrlock's varlink interface, and the public area of the signing key in
both PEM encoded form and as a JSON encoded TPMT_PUBLIC structure.
The audit session attestation is always the last component. The
preceding components are all executed with the audit session, and the
signer requires the audit session to remain exclusive whilst generating
the report. What this does is provide evidence to a verifier that the
overall report is a single, consistent snapshot of the system (ie, no
other TPM commands executed whilst generating the report) because the
audit session attestation contains a field that indicates that the
session was exclusive at the time it was attested.
The set of attested PCRs and NvPCRs are currently hardcoded. It includes
all firmware PCRs with the exception of PCR6, and all other PCRs with
the exception of the IMA one (which isn't useful on its own without the
IMA log). All defined NvPCRs are included.
A verifier can verify the signed attestation components individually by
verifying their signatures against the supplied attestation data, using
the supplied public key.
For the PCR report component, a verifier can replay the measurements in
the log in order to reconstruct the PCR values, and then use this and
the PCR selection from the attestation in order to calculate the PCR
digest, and then check that this matches the one in the attestation.
For the NvPCR report components, a verifier checks that the NV index
name in the attestation matches one that is anchored in PCR9 to be sure
that the attestation is for a valid NvPCR. A verifier can then replay
the measurements in the log to ensure that the resulting NvPCR value
matches the one supplied in the attestation. The authenticated data is a
JSON blob containing the NvPCR name and priority. A verifier can digest
this and ensure it matches the qualifying data in the attestation.
The audit session attestation includes the audit session digest and
whether the audit session was still exclusive when the attestation was
generated. A verifier should confirm exclusivity. The digest includes a
record of all TPM commands executed with the audit session, as each
command results in it being extended with the command parameter hash and
the result parameter hash. A verifier should replay the preceding report
components to compute an expected audit digest and make sure that this
digest matches the one in the attestation in order to be sure that the
overall report represents a single, consistent snapshot. The other
report components provide sufficient information for a verifier to
reconstruct the cpHash and rpHash.
When reconstructing the cpHash for the PCR and NvPCR components, the
signing scheme is the NULL scheme (they use the attestation key's
signing scheme). The required signing key public area is supplied in
the report. The supplied qualifying data is in the extra data field of
the attestation.
The PCR selection is also required to reconstruct the cpHash for the
PCR component. The PCR selection is available in the attestation.
The name of the NV index is also required to reconstruct the cpHash for
the NvPCR components. The name is available in the attestation.
The rpHash for each component can be reconstructed from the supplied
attestation blob and signature.
In the future, we might want to add more report component types. Eg, we
could add report components for ordinary NV indexes. Using the audit
session means that we could also add additional components that the TPM
can't attest directly, such as the values of interesting TPM properties
(as the TPM2_GetCapability calls will appear in the audit session digest).
The attestation signing key is stored as a JSON file in the report
signer's state directory. Different types of keys are supported:
- Ordinary keys that are protected by an existing persistent TPM key
such as the SRK or EK.
- Persistent TPM keys.
- Primary keys, which are recreated using a template on use.
When an ordinary or primary key is used, the report signer saves the
object context in its runtime directory. This context is used on future
invocations. This has the benefit that where a primary key is used, it
doesn't need to be recreated on each use. When an ordinary key is used,
we don't need to run TPM2_Load, which requires authorization with the
parent key, on each This authorization currently requires a policy
session for low-range EKs.
Regardless of the key type, it must be a restricted signing key. If no
key exists, an ordinary key is generated automatically.
The attestation key can also have an associated credential which is
attached to the signed report. The credential can be issued by a CA or
some other party during onboarding, and is presented to a verifier as
proof that the attestation key is authentic. It is considered an opaque
blob in order to not require it to be a specific format, such as a X.509
certificate. I've called this a "voucher" though because "cred" or
"credential" has other meanings in systemd.
As a follow-up to this, I plan to add a varlink interface for creating
attestation keys that can be used for report signing.
The custom help() is replaced by a COMMAND() description, and
--introspect-cli is added. The referenced systemd-report-cgroup(8) man
page does not exist, so the reference is dropped.
The custom help() is replaced by a COMMAND() description, and
--introspect-cli is added. The abstract gets a trailing period. There is
no man page for this helper, so none is referenced.
The custom help() is replaced by a COMMAND() description, and
--introspect-cli is added. --help now goes through the pager, like other
commands with --no-pager.
Turn --sign=BOOL into --sign=no|best-effort|require-one|require-all,
making the multi-signer aggregation policy explicit: best-effort never
fails on signing, require-one requires at least one signature, and
require-all requires every signer to succeed (an empty reply, i.e. a
signer opting out, counts as failure). Signed reports are always emitted
as a JSON-SEQ stream. The mode is also exposed as an input to the
io.systemd.Report.GenerateSigned Varlink method.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
The option is generalized from a simple boolean switch that enables
a fixed list to a list-of-patterns.
The old value works, but is deprecated: -Dstandalone-binaries=true is translated
to -Dstandalone-binaries=repart,report,tmpfiles,sysusers,shutdown.
We could already build all normal executables as .standalone, but the
installation only supported a small fixes subset. This wasn't flexible
enough:
- packagers might want to provide additional standalone binaries then the
small subset that was already enabled
- but at the same time, whenever new binaries were added to this subset,
downstream packaging had to be adjusted in sync, at least in the case of
Fedora, because otherwise we'd get a complaint about unexpected .standalone
binaries in the temporary install root.
With the new option, downstreams can just specify the binaries that they
want to be installed in the .standalone variant.
As usual with this type of change, the build dir must be reprovisioned.
Since 4c0d8d9673, most dlopen notes are
set at the beginning of the executables. Let's manage all dlopen notes
there, rather than setting them where dlopen is called.
Note that the only exceptions are the LIBBPF_NOTE for networkd and
nsresource. Since dlopen_bpf() is wrapped in an `#if` guard, the notes
are instead set within the corresponding functionality.
As a result, the DLOPEN_FOO() wrapper macros are no longer needed and
can be dropped completely.
This also switches all these notes to be defined via SD_ELF_NOTE_DLOPEN_ANCHORED().
Consequently, any notes added within unreachable or unused functions will be
automatically garbage-collected by the linker (--gc-sections) instead of
bloating the final binary.
E.g. unused p11-kit library dependency is now dropped from
systemd-repart.standalone binary.
Before:
```
$ systemd-analyze dlopen-metadata build/systemd-repart.standalone
FEATURE DESCRIPTION SONAME PRIORITY
cryptsetup Support for disk encryption, integrity, and authentication libcryptsetup.so.12 recommended
blkid Support for block device identification libblkid.so.1 required
libcrypto Support for cryptographic operations libcrypto.so.4 libcrypto.so.3 recommended
mount Support for mount enumeration libmount.so.1 required
fdisk Support for reading and writing partition tables libfdisk.so.1 required
blkid Support for block device identification libblkid.so.1 recommended
libcrypto Support for cryptographic operations libcrypto.so.4 libcrypto.so.3 suggested
cryptsetup Support for disk encryption, integrity, and authentication libcryptsetup.so.12 suggested
fdisk Support for reading and writing partition tables libfdisk.so.1 suggested
idn Support for internationalized domain names libidn2.so.0 suggested
mount Support for mount enumeration libmount.so.1 recommended
selinux Support for SELinux libselinux.so.1 recommended
tpm Support for TPM libtss2-esys.so.0 suggested
tpm Support for TPM libtss2-rc.so.0 suggested
tpm Support for TPM libtss2-mu.so.0 suggested
tpm Support for TPM libtss2-tcti-device.so.0 suggested
p11-kit Support for PKCS11 hardware tokens libp11-kit.so.0 suggested
```
After:
```
$ systemd-analyze dlopen-metadata build/systemd-repart.standalone
FEATURE DESCRIPTION SONAME PRIORITY
cryptsetup Support for disk encryption, integrity, and authentication libcryptsetup.so.12 recommended
blkid Support for block device identification libblkid.so.1 required
libcrypto Support for cryptographic operations libcrypto.so.4 libcrypto.so.3 recommended
mount Support for mount enumeration libmount.so.1 required
fdisk Support for reading and writing partition tables libfdisk.so.1 required
blkid Support for block device identification libblkid.so.1 recommended
libcrypto Support for cryptographic operations libcrypto.so.4 libcrypto.so.3 suggested
cryptsetup Support for disk encryption, integrity, and authentication libcryptsetup.so.12 suggested
fdisk Support for reading and writing partition tables libfdisk.so.1 suggested
mount Support for mount enumeration libmount.so.1 recommended
selinux Support for SELinux libselinux.so.1 recommended
tpm Support for TPM libtss2-esys.so.0 suggested
tpm Support for TPM libtss2-rc.so.0 suggested
tpm Support for TPM libtss2-mu.so.0 suggested
tpm Support for TPM libtss2-tcti-device.so.0 suggested
```
Extend the 'install' keyword for executable definitions to accept four
modes—'yes', 'no', 'both', and 'static'—to elegantly manage the creation
and installation of both shared and statically-linked (.standalone) binaries.
- 'yes' / 'no': Standard behavior (mapped to true/false).
- 'both': Installs both the shared and static variants.
- 'static': Installs the static variant under the original name, while
suffixing the uninstalled shared variant with '.shared'.
With this change, any arbitrary executable can now have its `.standalone`
variant built on demand simply by invoking `ninja <target>.standalone`.
For example, `varlinkctl` did not previously support a standalone variant,
but it can now be built explicitly via `ninja varlinkctl.standalone`.
These `.standalone` binaries are not built by default unless explicitly
specified as a ninja target or enabled via `-Dstandalone-binaries=true`.
Thus, the default build time should remain unaffected.
This centralisation eliminates a massive amount of boilerplate and duplicated
target declarations across almost all subdirectories (e.g., systemd-repart,
systemd-tmpfiles, systemd-shutdown, and systemd-report tools).
The goal is to be allow the use of those binaries on systemd with older
systemd. The report stuff is generally independent of the running systemd
version.
When built with -Dinstall-tests=true -Dstandalone-binaries=true
-Dbuildtype=release -Db_lto=true, gcc-16.1.1-2.fc44.x86_64, the sizes
are quite reasonable:
$ ls -lG build-lto/*.standalone
-rwxr-xr-x 1 zbyszek 713200 Jul 3 13:16 build-lto/systemd-report.standalone
-rwxr-xr-x 1 zbyszek 662936 Jul 3 13:16 build-lto/systemd-report-basic.standalone
-rwxr-xr-x 1 zbyszek 567424 Jul 3 13:16 build-lto/systemd-report-cgroup.standalone
-rwxr-xr-x 1 zbyszek 592856 Jul 3 13:16 build-lto/systemd-report-files.standalone
-rwxr-xr-x 1 zbyszek 589864 Jul 3 13:16 build-lto/systemd-report-sign-plain.standalone
-rwxr-xr-x 1 zbyszek 548624 Jul 3 13:16 build-lto/systemd-report-sign-tsm.standalone
This makes each `foo_cflags` dependency define a `SYSTEMD_CFLAGS_MARKER_FOO`
macro, and checks if the macro is set when headers provided by external
libraries are included.
With this, we can fail fast at compile time if necessary `_cflags`
dependencies are omitted in meson.build. Missing dependencies found by this
mechanism have been added across the tree.
The .generate field of MetricFamily defaults to NULL when omitted from a
designated initializer, so spelling it out explicitly on the metric-family
entries (and the field-generating macros) that have no generator of their
own is just noise. Drop it.
A report signing backend that returns a confidential-computing
attestation report obtained via configfs-tsm. Implements
io.systemd.Report.Signer.Sign(): embeds the digest as the report's
inblob and returns the outblob (plus provider and any aux/manifest
blobs). Wired up as the "tsm" mechanism with a socket-activated service.
Signed-off-by: Paul Meyer <katexochen0@gmail.com>
Two PRs got merged at the same time, which cause a test to fail,
as they work individually but fail when combined
TEST-74-AUX-UTILS.sh[1688]: + /usr/lib/systemd/systemd-report generate io.systemd.Manager.UnitsTotal
TEST-74-AUX-UTILS.sh[1805]: {"mediaType":"application/vnd.io.systemd.report","metrics":[{"name":"io.systemd.Manager.UnitsTotal","value":249}],"timestamp":"Fri 2026-06-19 19:50:48 UTC"}
TEST-74-AUX-UTILS.sh[1806]: + /usr/lib/systemd/systemd-report generate io.systemd.Manager.UnitsTotal
TEST-74-AUX-UTILS.sh[1807]: + jq .
TEST-74-AUX-UTILS.sh[1807]: {
TEST-74-AUX-UTILS.sh[1807]: "mediaType": "application/vnd.io.systemd.report",
TEST-74-AUX-UTILS.sh[1807]: "metrics": [
TEST-74-AUX-UTILS.sh[1807]: {
TEST-74-AUX-UTILS.sh[1807]: "name": "io.systemd.Manager.UnitsTotal",
TEST-74-AUX-UTILS.sh[1807]: "value": 249
TEST-74-AUX-UTILS.sh[1807]: }
TEST-74-AUX-UTILS.sh[1807]: ],
TEST-74-AUX-UTILS.sh[1807]: "timestamp": "Fri 2026-06-19 19:50:48 UTC"
TEST-74-AUX-UTILS.sh[1807]: }
TEST-74-AUX-UTILS.sh[1688]: + /usr/lib/systemd/systemd-report upload --url=http://localhost:8089/
TEST-74-AUX-UTILS.sh[1808]: Failed to normalize report JSON: Wrong medium type
https://github.com/systemd/systemd/pull/42594https://github.com/systemd/systemd/pull/42595
Disable normalization for now, and track the issue at
https://github.com/systemd/systemd/issues/42669
Follow-up for 3c2f7c6002
These signatures can be provided by arbitrary Varlink backends symlinked
into /run/systemd/report/sign.
Also add a proper Varlink IDL for the upload interface, since we are
extended it now.
let's name the dir "/run/systemd/report.upload/" (rather than
"/run/systemd/metrics-upload/"). After all, these are reports that we
upload, not indiviudual metrics. And it would be particular confusing
since the dir to pick up metrics is called /run/systemd/report/, rather
than /run/systemd/metrics/. Hence the thing that deals with reports is
nmamed metrics, and the thing that deals in metrics is named reports...
We really want to use io.systemd.Report for the interface
provided by systemd-report itself, not by its backend. hence, rename the
interface that uploading plugins shall implement to
io.systemd.Report.Uploader.
Note that we ideally should have a varlink interface definition for that
interface. if we had, we'd have noticed that earlier.
Switch the executable-owned call sites from the plain dlopen_<lib>()
helpers to the new DLOPEN_<LIB>() macros, so that each binary that loads
an optional library at runtime now carries its own .note.dlopen note
instead of relying solely on libsystemd-shared. Each call site passes a
priority reflecting whether it hard-depends on the library (required) or
degrades gracefully without it (recommended).
Co-developed-by: Claude Opus 4.8 <noreply@anthropic.com>
Old gcc is confused about initialization:
In function ‘io_read_send’,
inlined from ‘walk_cgroups’ at ../src/report/report-cgroup.c:288:24:
../src/report/report-cgroup.c:167:21: error: ‘values[0]’ may be used uninitialized [-Werror=maybe-uninitialized]
167 | r = metric_build_send_unsigned(mf + i, link, unit, values[i], /* fields= */ NULL);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Maybe this helps.
Add io.systemd.Basic.OSRelease metric families that reports select fields
from os-release.
$ cat /etc/os-release
NAME="Fedora Linux"
VERSION="45 (Cloud Edition Prerelease)"
RELEASE_TYPE=development
ID=fedora
VERSION_ID=45
VERSION_CODENAME=""
PRETTY_NAME="Fedora Linux 45 (Cloud Edition Prerelease)"
ANSI_COLOR="0;38;2;60;110;180"
LOGO=fedora-logo-icon
CPE_NAME="cpe:/o:fedoraproject:fedora:45"
HOME_URL="https://fedoraproject.org/"
DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/rawhide/"
SUPPORT_URL="https://ask.fedoraproject.org/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Fedora"
REDHAT_BUGZILLA_PRODUCT_VERSION=rawhide
REDHAT_SUPPORT_PRODUCT="Fedora"
REDHAT_SUPPORT_PRODUCT_VERSION=rawhide
SUPPORT_END=2027-11-24
VARIANT="Cloud Edition"
VARIANT_ID=cloud
$ varlinkctl call --more ./build/systemd-report-basic io.systemd.Metrics.List {} | jq --seq -c
...
{"name":"io.systemd.Basic.OSRelease.NAME","value":"Fedora Linux 44 (Workstation Edition)"}
{"name":"io.systemd.Basic.OSRelease.ID","value":"fedora"}
{"name":"io.systemd.Basic.OSRelease.CPE_NAME","value":"cpe:/o:fedoraproject:fedora:44"}
{"name":"io.systemd.Basic.OSRelease.VARIANT_ID","value":"workstation"}
{"name":"io.systemd.Basic.OSRelease.VERSION_ID","value":"44"}
{"name":"io.systemd.Basic.OSRelease.SUPPORT_END","value":"2027-05-19"}
I picked the fields that contain useful information about the specific
version/image/variant/experiment/flavour of the system. Also, either
NAME or PRETTY_NAME is included. This one is intended for human readers
to be able to identify the OS version easily.
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.
We removed the concept of facts, so we need to update the existing
io.systemd.Basic facts provider to metrics. This commit does just
that. Its mostly mechanical.
This also means that facts.{c,h} and varlink-io.systemd.Facts.{c,h}
are gone now.
When a report upload fails the backend often provides useful
details via the varlink error. Show them as part of the upload
error message. For now we just dump the json because we have
no structure that the backends should follow. We may want to
consider adding one (like check for an "error_message" key in
the json). But for now this is a nice step forward.