From 574a64030d42736b2aa970a246ed495d27f5ad01 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 15 Oct 2026 15:47:41 +0200 Subject: [PATCH 1/5] dissect-image: measure only validated verity signatures When a verity signature fails validation both in the kernel and in userspace, and the image policy allows activation without a signature, we still pass the signature to pcrextend_verity_now(). The PCR then records a signature that nothing checked, which looks the same as a validated one to anyone reading the log. Let's only measure the signature if the kernel or userspace validated it, like systemd-veritysetup already does. --- src/shared/dissect-image.c | 10 +++----- test/units/TEST-70-TPM2.nvpcr.sh | 44 ++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index d6fbfd662c0..67560059e17 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -3520,13 +3520,11 @@ static int do_crypt_activate_verity( return r < 0 ? r : -EDESTADDRREQ; log_debug("Activation of signed Verity volume without validating signature is permitted by policy. Continuing."); - } else + measure_signature = false; + } else { log_debug("Verity activation via userspace signature logic worked, activating by root hash."); - - /* Otherwise let's see what signature-less activation results in. */ - - measure_signature = true; - + measure_signature = true; + } } else if (!FLAGS_SET(policy_flags, PARTITION_POLICY_VERITY)) return log_debug_errno(SYNTHETIC_ERRNO(ERFKILL), "No-signature activation of Verity volume not allowed by policy, refusing."); diff --git a/test/units/TEST-70-TPM2.nvpcr.sh b/test/units/TEST-70-TPM2.nvpcr.sh index 0fd05dc222c..97553408545 100755 --- a/test/units/TEST-70-TPM2.nvpcr.sh +++ b/test/units/TEST-70-TPM2.nvpcr.sh @@ -26,7 +26,7 @@ at_exit() { mv -f /run/systemd/tpm2-pcr-signature.json.bak /run/systemd/tpm2-pcr-signature.json rm -f /tmp/tpm2-pcr-private-key.pem /tmp/tpm2-pcr-public-key.pem /tmp/tpm2-pcr-signature.json.valid rm -rf /run/nvpcr /tmp/nvpcr - rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt + rm -f /var/tmp/nvpcr.raw /var/tmp/nvpcr-untrusted.raw /run/verity.d/test-70-nvpcr.crt rm -f /run/systemd/nvpcr/test.auth /run/systemd/nvpcr/test2.auth /run/systemd/nvpcr/aaa.auth /run/systemd/nvpcr/zzz.auth /run/systemd/nvpcr/unauth.auth /run/systemd/nvpcr/unauth2.auth rm -f /tmp/test.policy rm -f /run/systemd/tpm2-nv-space-exhausted-orderly @@ -368,17 +368,9 @@ fi mkdir -p /tmp/nvpcr/tree touch /tmp/nvpcr/tree/file -if machine_supports_verity_keyring; then - SYSTEMD_REPART_OVERRIDE_FSTYPE=squashfs \ - systemd-repart -P \ - -s /tmp/nvpcr/tree \ - --certificate=/usr/share/mkosi.crt \ - --private-key=/usr/share/mkosi.key \ - /var/tmp/nvpcr.raw -else - OPENSSL_CONFIG="/tmp/nvpcr/opensslconfig" - # Unfortunately OpenSSL insists on reading some config file, hence provide one with mostly placeholder contents - cat >"${OPENSSL_CONFIG:?}" <"${OPENSSL_CONFIG:?}" < Date: Thu, 15 Oct 2026 16:45:06 +0200 Subject: [PATCH 2/5] dissect-image: fix verity_sig_ready of unpartitioned images dissect_image_from_unpartitioned() marks the image as having a usable verity signature if a root hash is configured, instead of a root hash signature. An image with sidecar .roothash and .verity files but no .roothash.p7s file is then treated as signed. image_policy_new_from_dissected() pins "signed" for such an image when it is attached as a portable service. Let's check the root hash signature, like before the conversion to struct iovec. Fixes: 08d75cfb99fd2ec5266e6a062c90e9b18f9cc18f --- src/shared/dissect-image.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 67560059e17..0de829bac9f 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -1062,7 +1062,7 @@ static int dissect_image_from_unpartitioned( m->verity_ready = verity_settings_data_covers(verity, PARTITION_ROOT); m->has_verity_sig = false; /* signature not embedded, must be specified */ - m->verity_sig_ready = m->verity_ready && iovec_is_set(&verity->root_hash); + m->verity_sig_ready = m->verity_ready && iovec_is_set(&verity->root_hash_sig); if (uuid) m->image_uuid = *uuid; From 8ae2ad1eec0f193c273df5ae759454b5cc4807f0 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 16 Oct 2026 16:12:55 +0200 Subject: [PATCH 3/5] dissect-image: fix arguments of mountfsd varlink calls mountfsd_mount_image_fd() and mountfsd_mount_directory_fd() pass &error_id to varlink_callbo_and_log(), but that macro has no error ID parameter. It passes all arguments after the reply to SD_JSON_BUILD_OBJECT(), so &error_id ends up as an argument of the JSON builder. sd_json_buildv() reads it with va_arg(ap, int), which is undefined behavior, and skips the unknown value. error_id is never set. Let's drop the argument. varlink_callbo_and_log() logs and converts errors itself. --- src/shared/dissect-image.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 0de829bac9f..12f2dd61dc0 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5456,7 +5456,6 @@ int mountfsd_mount_image_fd( _cleanup_(dissected_image_unrefp) DissectedImage *di = NULL; _cleanup_close_ int verity_data_fd = -EBADF; _cleanup_free_ char *ps = NULL; - const char *error_id; int r; assert(image_fd >= 0); @@ -5540,7 +5539,6 @@ int mountfsd_mount_image_fd( vl, "io.systemd.MountFileSystem.MountImage", &reply, - &error_id, SD_JSON_BUILD_PAIR_UNSIGNED("imageFileDescriptor", 0), SD_JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(1)), SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY)), @@ -5710,12 +5708,10 @@ int mountfsd_mount_directory_fd( } sd_json_variant *reply = NULL; - const char *error_id = NULL; r = varlink_callbo_and_log( vl, "io.systemd.MountFileSystem.MountDirectory", &reply, - &error_id, SD_JSON_BUILD_PAIR_UNSIGNED("directoryFileDescriptor", 0), SD_JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(1)), SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY)), From 8a7800535f07b3695d5205332cdcf027b9f985c8 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 16 Oct 2026 15:29:01 +0200 Subject: [PATCH 4/5] dissect-image: map mountfsd verity errors to errnos If mountfsd refuses to set up an image with io.systemd.MountFileSystem.KeyNotFound or io.systemd.MountFileSystem.DeniedByImagePolicy, mountfsd_mount_image_fd() returns -EBADR, because sd-varlink can't map the error IDs to an errno. Callers can't tell the refusal apart from other errors. dissected_image_decrypt() reports the same refusals as -EDESTADDRREQ and -ERFKILL. Let's call sd_varlink_callbo() directly, which returns the error ID, and return -EDESTADDRREQ for KeyNotFound and -ERFKILL for DeniedByImagePolicy. Callers can then report the refusal the same way, whether they mount the image themselves or through mountfsd. We log other errors like varlink_call_and_log() does. --- src/shared/dissect-image.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/shared/dissect-image.c b/src/shared/dissect-image.c index 12f2dd61dc0..ea6387e5f1f 100644 --- a/src/shared/dissect-image.c +++ b/src/shared/dissect-image.c @@ -5535,10 +5535,12 @@ int mountfsd_mount_image_fd( } sd_json_variant *reply = NULL; - r = varlink_callbo_and_log( + const char *error_id = NULL; + r = sd_varlink_callbo( vl, "io.systemd.MountFileSystem.MountImage", &reply, + &error_id, SD_JSON_BUILD_PAIR_UNSIGNED("imageFileDescriptor", 0), SD_JSON_BUILD_PAIR_CONDITION(userns_fd >= 0, "userNamespaceFileDescriptor", SD_JSON_BUILD_UNSIGNED(1)), SD_JSON_BUILD_PAIR_BOOLEAN("readOnly", FLAGS_SET(flags, DISSECT_IMAGE_MOUNT_READ_ONLY)), @@ -5552,7 +5554,23 @@ int mountfsd_mount_image_fd( SD_JSON_BUILD_PAIR_CONDITION(verity && iovec_is_set(&verity->root_hash_sig), "verityRootHashSignature", JSON_BUILD_IOVEC_BASE64(&verity->root_hash_sig)), SD_JSON_BUILD_PAIR_BOOLEAN("allowInteractiveAuthentication", FLAGS_SET(flags, DISSECT_IMAGE_ALLOW_INTERACTIVE_AUTH))); if (r < 0) - return r; + return log_error_errno(r, "Failed to issue MountImage() varlink call: %m"); + if (error_id) { + /* Return the same errors as dissected_image_decrypt(), so that callers can report them the + * same way, whether they mount the image themselves or through mountfsd. */ + if (streq(error_id, "io.systemd.MountFileSystem.KeyNotFound")) + return log_error_errno(SYNTHETIC_ERRNO(EDESTADDRREQ), + "mountfsd refused the image, because no trusted key validates its signature."); + if (streq(error_id, "io.systemd.MountFileSystem.DeniedByImagePolicy")) + return log_error_errno(SYNTHETIC_ERRNO(ERFKILL), + "mountfsd refused the image, because the image policy doesn't permit it."); + + r = sd_varlink_error_to_errno(error_id, reply); + if (r != -EBADR) + return log_error_errno(r, "Failed to issue MountImage() varlink call: %m"); + + return log_error_errno(r, "Failed to issue MountImage() varlink call: %s", error_id); + } r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) From 28b52ad59bffa32a4d618370bf39391585085424 Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Fri, 16 Oct 2026 16:21:24 +0200 Subject: [PATCH 5/5] pcrextend-util: log at debug level without TPM2 support pcrextend_pcr_now() and pcrextend_nvpcr_now() log all their failures at debug level, except when systemd is built without TPM2 support. In that case they log "TPM2 support disabled, not measuring." at error level. Most callers ignore the result. On such builds, every dm-verity activation in dissect-image logs this error, for example when attaching a portable image or starting a service with RootImage=. systemd-imds logs it whenever it imports userdata. systemd-cryptsetup logs its own error when measuring fails. Let's log at debug level in this case too, like for the other failures. --- src/shared/pcrextend-util.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/pcrextend-util.c b/src/shared/pcrextend-util.c index afa1b88cf5b..4f2a75626bd 100644 --- a/src/shared/pcrextend-util.c +++ b/src/shared/pcrextend-util.c @@ -75,7 +75,7 @@ int pcrextend_pcr_now(unsigned pcr, const char *word, const struct iovec *secret log_debug("Measurement of '%s' into PCR %u completed.", word, pcr); return 1; #else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); #endif } @@ -116,7 +116,7 @@ int pcrextend_nvpcr_now(const char *nvpcr, const char *word, const char *event) log_debug("Measurement of '%s' into NvPCR '%s' completed.", word, nvpcr); return 1; #else - return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support disabled, not measuring."); #endif }