From 2d54e6b56e2ab2fce624405c2ba7f426ea588147 Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:40:47 +0200 Subject: [PATCH 1/8] tpm2-util: make -EUCLEAN from tpm2_unseal() mean "PCRs kept changing" So far tpm2_unseal() used -EUCLEAN together with the errors that signal that the PCR state does not match the policy. That is misleading: -EUCLEAN (and TPM2_RC_PCR_CHANGED) is only ever generated when the TPM's global PCR update counter moves while we are building the policy or unsealing, i.e. when something on the system extends a PCR concurrentlyi (usually IMA extending PCR 10). It says nothing about whether the policy itself matches. Until now the retry loop only recognized those conditions while retries were left (i > 0), and once the budget was exhausted the last iteration fell through to the generic paths, reporting either an unrelated policy error or -ENOTRECOVERABLE. Handle the last iteration explicitly instead, and return -EUCLEAN with a message that names the actual problem. Drop -EUCLEAN from ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(), it is not a "bad PCR state" error. Signed-off-by: Alberto Planas --- src/shared/tpm2-util.c | 21 ++++++++++++++++----- src/shared/tpm2-util.h | 6 ++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 7a20965658..cda3f94ab1 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7197,13 +7197,17 @@ int tpm2_unseal(Tpm2Context *c, * -ENOLCK → TPM is in dictionary lockout mode * -EREMCHG → submitted policy doesn't match NV index stored policy (in case of PolicyAuthorizeNV) * -ENOANO → none of the PolicyOR branches of a policy matched current state - * -EUCLEAN → PCR state doesn't match expectations + * -EUCLEAN → a PCR kept being extended while we were unsealing * -EPERM → stored policy does not match TPM state * -ENOTRECOVERABLE → all other kinds of TPM errors * -EILSEQ → bad PIN * - * Of these all four of EREMCHG, ENOANO, EUCLEAN, EPERM can all mean that PCR state is not matching - * expectations. */ + * Of these EREMCHG, ENOANO and EPERM mean that PCR state is not matching expectations. EUCLEAN on + * the other hand says nothing about the correctness of the policy: it means the TPM's global PCR + * update counter kept moving while we built the policy or unsealed, i.e. something on this system + * extends a PCR continuously (the usual suspect is PCR 10 when IMA is enabled directly, or + * indirectly via secure boot). We make at most RETRY_UNSEAL_MAX + 1 attempts before reporting + * it. */ TSS2_RC rc; int r; @@ -7361,7 +7365,10 @@ int tpm2_unseal(Tpm2Context *c, !!pin, (shard == 1 || !iovec_is_set(pubkey)) ? pcrlock_policy : NULL, &policy_digest); - if (r == -EUCLEAN && i > 0) { + if (r == -EUCLEAN) { + if (i == 0) + return log_debug_errno(r, "PCR values kept changing while building the TPM2 policy, giving up after %u unsealing attempts.", RETRY_UNSEAL_MAX + 1); + log_debug("A PCR value changed during the TPM2 policy session, restarting HMAC key unsealing (%u tries left).", i); retry = true; break; @@ -7403,7 +7410,11 @@ int tpm2_unseal(Tpm2Context *c, encryption_session->esys_handle, /* use HMAC session to enable parameter encryption */ ESYS_TR_NONE, &unsealed); - if (rc == TPM2_RC_PCR_CHANGED && i > 0) { + if (rc == TPM2_RC_PCR_CHANGED) { + if (i == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), + "PCR values kept changing while unsealing, giving up after %u unsealing attempts.", RETRY_UNSEAL_MAX + 1); + log_debug("A PCR value changed during the TPM2 policy session, restarting HMAC key unsealing (%u tries left).", i); retry = true; break; diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 3723281f42..8c0b136b08 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -51,8 +51,10 @@ static inline bool TPM2_PCR_MASK_VALID(uint32_t pcr_mask) { int dlopen_tpm2(int log_level) _dlopen_loader_; /* tpm2_unseal() returns a bunch of different errors for various flavours of PCR issues, let's group them. - * Kept outside the HAVE_TPM2 guard as callers switch on these errnos even in builds without TPM2. */ -#define ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r) IN_SET(r, -EREMCHG, -ENOANO, -EUCLEAN, -EPERM) + * Kept outside the HAVE_TPM2 guard as callers switch on these errnos even in builds without TPM2. Note that + * -EUCLEAN is not part of this: when returned by tpm2_unseal() it means a PCR was continuously extended + * while unsealing, which says nothing about whether the PCR state matches the policy. */ +#define ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r) IN_SET(r, -EREMCHG, -ENOANO, -EPERM) /* Errors that mean the tried TPM2 token does not match the boot state, be it due to wrong PCR state, a * different PCR signing key/policy, a different TPM, or an unusable NV index. The caller should keep trying From 941db14b8e3bf1b517f83975a2b948304871e79d Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:41:05 +0200 Subject: [PATCH 2/8] cryptsetup,cryptenroll: report continuously changing PCRs explicitly tpm2_unseal() returns -EUCLEAN specifically when a PCR kept being extended while unsealing, but the unseal callers no longer catch it via ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR() and would fall back to the generic "Failed to unseal secret using TPM2" message. Give it a message of its own instead, so that the log points at the actual cause instead of suggesting the system was tampered with. Signed-off-by: Alberto Planas --- src/cryptenroll/cryptenroll-tpm2.c | 2 ++ src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c | 2 ++ src/shared/cryptsetup-tpm2.c | 4 ++++ 3 files changed, 8 insertions(+) diff --git a/src/cryptenroll/cryptenroll-tpm2.c b/src/cryptenroll/cryptenroll-tpm2.c index 6165a31642..21b77bd9c0 100644 --- a/src/cryptenroll/cryptenroll-tpm2.c +++ b/src/cryptenroll/cryptenroll-tpm2.c @@ -589,6 +589,8 @@ int enroll_tpm2(const EnrollContext *c, n_policy_hash, &srk, &secret2); + if (r == -EUCLEAN) + return log_error_errno(r, "PCR values kept changing while unsealing the TPM2 secret, giving up. Something on this system extends a PCR continuously."); if (r < 0) return log_error_errno(r, "Failed to unseal secret using TPM2: %m"); diff --git a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c index bb26193153..40a9ed1ffd 100644 --- a/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c +++ b/src/cryptsetup/cryptsetup-tokens/luks2-tpm2.c @@ -135,6 +135,8 @@ int acquire_luks2_key( return log_error_errno(SYNTHETIC_ERRNO(ENOANO), "Bad %s.", argon2id ? "password" : "PIN"); if (r == -ENOLCK) return log_error_errno(r, "TPM is in dictionary attack lock-out mode."); + if (r == -EUCLEAN) + return log_error_errno(r, "PCR values kept changing while unsealing the TPM2 secret, giving up. Something on this system extends a PCR continuously."); if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) { log_warning_errno(r, "TPM policy does not match current system state. Either system has been tampered with or policy out-of-date: %m"); /* Normalize to -EPERM so callers don't confuse it with -ENOANO's "needs PIN" meaning. */ diff --git a/src/shared/cryptsetup-tpm2.c b/src/shared/cryptsetup-tpm2.c index ecb312a960..e989c3c5fd 100644 --- a/src/shared/cryptsetup-tpm2.c +++ b/src/shared/cryptsetup-tpm2.c @@ -177,6 +177,8 @@ int acquire_tpm2_key( return log_warning_errno(r, "TPM key integrity check failed. Key enrolled in superblock most likely does not belong to this TPM."); if (r == -EADDRNOTAVAIL) return log_warning_errno(r, "NV index referenced by token is missing, unwritten, or unusable, it could be for another system."); + if (r == -EUCLEAN) + return log_error_errno(r, "PCR values kept changing while unsealing the TPM2 secret, giving up. Something on this system extends a PCR continuously."); if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) { log_warning_errno(r, "TPM policy does not match current system state. Either system has been tampered with or policy out-of-date: %m"); /* Normalize to -EPERM so callers don't confuse it with -ENOANO's "needs PIN" meaning. */ @@ -257,6 +259,8 @@ int acquire_tpm2_key( return log_warning_errno(r, "TPM key integrity check failed. Key enrolled in superblock most likely does not belong to this TPM."); if (r == -EADDRNOTAVAIL) return log_warning_errno(r, "NV index referenced by token is missing, unwritten, or unusable, it could be for another system."); + if (r == -EUCLEAN) + return log_error_errno(r, "PCR values kept changing while unsealing the TPM2 secret, giving up. Something on this system extends a PCR continuously."); if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) { log_warning_errno(r, "TPM policy does not match current system state. Either system has been tampered with or policy out-of-date: %m"); /* Normalize to -EPERM so callers don't confuse it with -ENOANO's "needs PIN" meaning. */ From 42f3ac664a320a051267cda3c379558205e1bcab Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:41:05 +0200 Subject: [PATCH 3/8] creds: add io.systemd.Credentials.PCRsKeptChanging error io.systemd.Credentials.UnexpectedPCRState used to be mapped to EUCLEAN, which now means "a PCR kept being extended while unsealing" rather than "the PCR state does not match the policy". Map it to EPERM instead, which is one of the errnos ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR() covers, and introduce a separate PCRsKeptChanging error for EUCLEAN. Also report the condition explicitly in decrypt_credential_and_warn(), like the other TPM2 unseal callers do. Signed-off-by: Alberto Planas --- src/shared/creds-util.c | 5 ++++- src/shared/varlink-io.systemd.Credentials.c | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 7fbddf7a27..766da2c9d5 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -1534,6 +1534,8 @@ int decrypt_credential_and_warn( return log_error_errno(r, "TPM key integrity check failed. Key most likely does not belong to this TPM."); if (r == -EADDRNOTAVAIL) return log_error_errno(r, "NV index referenced by key is missing, unwritten, or unusable, it could be for another system."); + if (r == -EUCLEAN) + return log_error_errno(r, "PCR values kept changing while unsealing the TPM2 secret, giving up. Something on this system extends a PCR continuously."); if (ERRNO_IS_NEG_TPM2_UNSEAL_BAD_PCR(r)) return log_error_errno(r, "TPM policy does not match current system state. Either system has been tampered with or policy out-of-date: %m"); if (r < 0) @@ -1985,7 +1987,8 @@ static const CredentialsVarlinkError credentials_varlink_error_table[] = { { "io.systemd.Credentials.NullKeyNotAllowed", EHWPOISON, "The key was encrypted with a null key, but that's not allowed during decryption." }, { "io.systemd.Credentials.KeyBelongsToOtherTPM", EREMOTE, "The TPM integrity check for this key failed, key probably belongs to another TPM, or was corrupted." }, { "io.systemd.Credentials.TPMInDictionaryLockout", ENOLCK, "The TPM is in dictionary lockout mode, cannot operate." }, - { "io.systemd.Credentials.UnexpectedPCRState" , EUCLEAN, "Unexpected TPM PCR state of the system." }, + { "io.systemd.Credentials.UnexpectedPCRState", EPERM, "Unexpected TPM PCR state of the system." }, + { "io.systemd.Credentials.PCRsKeptChanging", EUCLEAN, "PCR values kept changing while unsealing the TPM2 secret, something on this system extends a PCR continuously." }, { "io.systemd.Credentials.NVIndexUnusable", EADDRNOTAVAIL, "The NV index referenced by the key is missing, unwritten, or unusable, it could be for another system." }, }; diff --git a/src/shared/varlink-io.systemd.Credentials.c b/src/shared/varlink-io.systemd.Credentials.c index d44af7e5f8..620c4f6f51 100644 --- a/src/shared/varlink-io.systemd.Credentials.c +++ b/src/shared/varlink-io.systemd.Credentials.c @@ -79,6 +79,7 @@ static SD_VARLINK_DEFINE_ERROR(NullKeyNotAllowed); static SD_VARLINK_DEFINE_ERROR(KeyBelongsToOtherTPM); static SD_VARLINK_DEFINE_ERROR(TPMInDictionaryLockout); static SD_VARLINK_DEFINE_ERROR(UnexpectedPCRState); +static SD_VARLINK_DEFINE_ERROR(PCRsKeptChanging); static SD_VARLINK_DEFINE_ERROR(NVIndexUnusable); SD_VARLINK_DEFINE_INTERFACE( @@ -113,5 +114,7 @@ SD_VARLINK_DEFINE_INTERFACE( &vl_error_TPMInDictionaryLockout, SD_VARLINK_SYMBOL_COMMENT("Unexpected TPM PCR state of the system."), &vl_error_UnexpectedPCRState, + SD_VARLINK_SYMBOL_COMMENT("PCR values kept changing while unsealing the TPM2 secret, something on this system extends a PCR continuously."), + &vl_error_PCRsKeptChanging, SD_VARLINK_SYMBOL_COMMENT("The NV index referenced by the key is missing, unwritten, or unusable, it could be for another system."), &vl_error_NVIndexUnusable); From 2994ca5226458678650734f99590fdff564fa7e9 Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:41:05 +0200 Subject: [PATCH 4/8] creds: don't report a malformed PCR signature file as changing PCRs tpm2_load_pcr_signature() propagates the JSON parser's errno, and the parser reports a string that is not valid UTF-8 as -EUCLEAN. With EUCLEAN now mapping to io.systemd.Credentials.PCRsKeptChanging, such a file would be reported as if a PCR were being extended continuously. Translate it where it originates, so that the errno stays single valued for every caller of tpm2_load_pcr_signature() and not only for the creds one: a signature file that cannot be parsed is -EBADMSG, whichever way it is malformed. A syntax error used to come out as -EINVAL, which matched nothing on the way out either. Signed-off-by: Alberto Planas --- src/shared/creds-util.c | 4 +++- src/shared/tpm2-util.c | 12 ++++++++++-- src/shared/tpm2-util.h | 3 +++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 766da2c9d5..2eefb7f575 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -1362,7 +1362,7 @@ int decrypt_credential_and_warn( /* Relevant error codes: * - * -EBADMSG → Corrupted file + * -EBADMSG → Corrupted file, or unparsable PCR signature file * -EOPNOTSUPP → Unsupported file type (could be: requires TPM but we have no TPM) * -EHOSTDOWN → Need PCR signature file, but couldn't find it * -EHWPOISON → Attempt to unlock with NULL key and either CREDENTIAL_ALLOW_REFUSE is on, or CREDENTIAL_ALLOW_NULL is off, but the system has a TPM and SecureBoot is on @@ -1386,6 +1386,8 @@ int decrypt_credential_and_warn( r = tpm2_load_pcr_signature(tpm2_signature_path, &signature_json); if (r == -ENOENT) return log_error_errno(SYNTHETIC_ERRNO(EHOSTDOWN), "Couldn't find PCR signature file: %m"); + if (r == -EBADMSG) + return log_error_errno(r, "PCR signature file is malformed."); if (r < 0) return log_error_errno(r, "Failed to load PCR signature: %m"); } diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index cda3f94ab1..a381b1cf54 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -12036,8 +12036,16 @@ int tpm2_load_pcr_signature(const char *path, sd_json_variant **ret) { return log_debug_errno(r, "Failed to find TPM PCR signature file '%s': %m", path); r = sd_json_parse_file(f, discovered_path, 0, ret, NULL, NULL); - if (r < 0) - return log_debug_errno(r, "Failed to parse TPM PCR signature JSON object '%s': %m", discovered_path); + if (r == -ENOMEM) + return log_oom_debug(); + if (r < 0) { + log_debug_errno(r, "Failed to parse TPM PCR signature JSON object '%s': %m", discovered_path); + + /* Don't propagate the parser's errno: it reports a string that is not valid UTF-8 as + * -EUCLEAN, and to our callers that errno means "a PCR kept being extended". Report a + * malformed file as such instead, whichever way it is malformed. */ + return -EBADMSG; + } return 0; } diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 8c0b136b08..475ac54014 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -624,6 +624,9 @@ int tpm2_parse_pcr_argument(const char *arg, Tpm2PCRValue **ret_pcr_values, size int tpm2_parse_pcr_argument_append(const char *arg, Tpm2PCRValue **pcr_values, size_t *n_pcr_values); int tpm2_parse_pcr_argument_to_mask(const char *arg, uint32_t *mask); +/* Note: tpm2_load_pcr_signature() returns -ENOENT if there is no such file, and -EBADMSG if there is one but + * it cannot be parsed. In particular it does not propagate the JSON parser's -EUCLEAN, which means something + * else to the TPM2 callers of this. */ int tpm2_load_pcr_signature(const char *path, sd_json_variant **ret); int tpm2_load_pcr_public_key(const char *path, void **ret_pubkey, size_t *ret_pubkey_size); From 1fc5351945cb7f389849d46b1388803b7cbcdb04 Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:41:05 +0200 Subject: [PATCH 5/8] tpm2-util: log transient PCR changes in tpm2_policy_super_pcr() at debug level tpm2_policy_pcr() returns -EUCLEAN when the TPM's global PCR update counter moved while the policy was being submitted, not when the recorded PCR values disagree with the current state. Logging "Either the system has been tampered with or the provided policy is incorrect" at error level is therefore wrong, and it is noisy too: the condition is transient and callers are expected to restart the policy session. Downgrade both occurrences to debug and describe what actually happened, and document the contract at the declarations. Signed-off-by: Alberto Planas --- src/shared/tpm2-util.c | 15 +++++---------- src/shared/tpm2-util.h | 3 +++ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index a381b1cf54..98f3d9b5ba 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -9836,15 +9836,10 @@ int tpm2_policy_super_pcr( session, &pcr_selection, ¤t_policy_digest); - if (r == -EUCLEAN) { - _cleanup_free_ char *j = NULL; - - for (uint32_t pcr = 0; pcr < TPM2_PCRS_MAX; pcr++) - if (single_value_pcrs & (UINT32_C(1) << pcr)) - (void) strextendf_with_separator(&j, ", ", "%" PRIu32, pcr); - - return log_error_errno(r, "Combined value for PCR(s) %s encoded in policy does not match the current TPM state. Either the system has been tampered with or the provided policy is incorrect.", strna(j)); - } + /* -EUCLEAN means some PCR got extended while we were submitting the policy. That's + * transient, so only log at debug level and let the caller restart the session. */ + if (r == -EUCLEAN) + return log_debug_errno(r, "PCR state changed while submitting the combined PolicyPCR expression, session needs to be restarted."); if (r < 0) return log_error_errno(r, "Failed to submit PCR policy to TPM: %m"); @@ -9876,7 +9871,7 @@ int tpm2_policy_super_pcr( &pcr_selection, ¤t_policy_digest); if (r == -EUCLEAN) - return log_error_errno(r, "Value for PCR %" PRIu32 " encoded in policy does not match the current TPM state. Either the system has been tampered with or the provided policy is incorrect.", pcr); + return log_debug_errno(r, "PCR state changed while submitting the PolicyPCR expression for PCR %" PRIu32 ", session needs to be restarted.", pcr); if (r < 0) return log_error_errno(r, "Failed to submit PCR policy to TPM: %m"); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 475ac54014..1d0ed81138 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -340,6 +340,9 @@ int tpm2_make_policy_session(Tpm2Context *c, const Tpm2Handle *primary, const Tp int tpm2_policy_auth_value(Tpm2Context *c, const Tpm2Handle *session, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_nv_written(Tpm2Context *c, const Tpm2Handle *session, bool written_set, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_authorize_nv(Tpm2Context *c, const Tpm2Handle *session, const Tpm2Handle *nv_handle, TPM2B_DIGEST **ret_policy_digest); +/* Note: tpm2_policy_pcr() and tpm2_policy_super_pcr() return -EUCLEAN if the TPM's global PCR update counter + * moved while the policy was submitted, i.e. if something extended a PCR in the meantime. That's transient, + * callers are supposed to restart the policy session and try again. */ int tpm2_policy_pcr(Tpm2Context *c, const Tpm2Handle *session, const TPML_PCR_SELECTION *pcr_selection, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_or(Tpm2Context *c, const Tpm2Handle *session, const TPM2B_DIGEST *branches, size_t n_branches, TPM2B_DIGEST **ret_policy_digest); int tpm2_policy_super_pcr(Tpm2Context *c, const Tpm2Handle *session, const Tpm2PCRPrediction *prediction, uint16_t algorithm); From 299989fe0bac97e6df9f69ca0af72892808d8d0b Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 10:41:06 +0200 Subject: [PATCH 6/8] pcrlock: retry policy submission when PCR values keep changing make_policy() already retries unsealing the recovery PIN when the TPM reports that PCR values changed, but the tpm2_policy_super_pcr() call that builds the policy for that unseal attempt was not covered: a concurrent PCR extension there aborted the whole operation on the first try. This is easy to hit when IMA is enabled (e.g. implicitly through secure boot), as PCR 10 is extended for every kernel module that gets loaded. Retry that step within the existing loop, and give the retry budget a name while at it. Signed-off-by: Alberto Planas --- src/pcrlock/pcrlock.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pcrlock/pcrlock.c b/src/pcrlock/pcrlock.c index 4f5a384072..8906cdff25 100644 --- a/src/pcrlock/pcrlock.c +++ b/src/pcrlock/pcrlock.c @@ -124,6 +124,10 @@ COMMAND( #define PCRLOCK_FILE_SYSTEM_PATH_PREFIX "/var/lib/pcrlock.d/840-file-system-" #define PCRLOCK_PE_INPUT_MAX (2U * U64_GB) +/* Maximum number of retries when PCR values keep changing while we submit the policy for, or unseal, the + * recovery PIN */ +#define PCRLOCK_RETRY_UNSEAL_MAX 16U + /* The default set of PCRs to lock to */ #define DEFAULT_PCR_MASK \ ((UINT32_C(1) << TPM2_PCR_PLATFORM_CODE) | \ @@ -3849,6 +3853,16 @@ static int make_policy(bool force, RecoveryPinMode recovery_pin_mode) { policy_session, &old_policy.prediction, old_policy.algorithm); + if (r == -EUCLEAN) { + if (attempt >= PCRLOCK_RETRY_UNSEAL_MAX) + return log_error_errno(r, + "PCR values kept changing while submitting the policy, giving up after %u attempts. " + "Something on this system extends a PCR continuously.", + attempt + 1); + + log_debug("Trying again (attempt %u), as PCR values changed while submitting the policy.", attempt + 1); + continue; + } if (r < 0) return r; @@ -3868,12 +3882,12 @@ static int make_policy(bool force, RecoveryPinMode recovery_pin_mode) { policy_session, encryption_session, &secret); - if (r < 0 && (r != -ESTALE || attempt >= 16)) + if (r < 0 && (r != -ESTALE || attempt >= PCRLOCK_RETRY_UNSEAL_MAX)) return log_error_errno(r, "Failed to unseal PIN: %m"); if (r == 0) break; - log_debug("Trying again (attempt %u), as PCR values changed during unlock attempt.", attempt+1); + log_debug("Trying again (attempt %u), as PCR values changed during unlock attempt.", attempt + 1); } if (secret.iov_len > sizeof_field(TPM2B_AUTH, buffer)) From f08bfdbf3d4e712a7de9d1c27c322f09e7092a33 Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 13:21:32 +0200 Subject: [PATCH 7/8] tpm2-util: retry NvPCR initialization when PCR values keep changing tpm2_nvpcr_initialize() already retries the initializing write when tpm2_extend_nvpcr_nv_index() reports that PCR values changed under its feet, which the comment there attributes to IMA measurements performed by the kernel. The policy session that write needs is opened in the same loop, and a PCR extended while that policy is submitted makes tpm2_policy_pcr() return -EUCLEAN, which was propagated as a hard failure instead. That is the same transient condition observed one step earlier, so let the loop absorb it too. Signed-off-by: Alberto Planas --- src/shared/tpm2-util.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 98f3d9b5ba..1c5fffa8c8 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -9296,6 +9296,13 @@ int tpm2_nvpcr_initialize( signature_json, &authorize_policy, &policy_session); + if (r == -EUCLEAN) { + /* A PCR was extended while we submitted the policy, so this session is unusable. + * Same situation as below, just observed while building the policy rather than + * while writing. */ + if (i > 0) + continue; + } if (r < 0) return r; From 827099139b23e7fe45813735916af685f7687c4d Mon Sep 17 00:00:00 2001 From: Alberto Planas Date: Mon, 14 Sep 2026 13:33:22 +0200 Subject: [PATCH 8/8] test: check the credentials Varlink error table credentials_varlink_error_by_errno() answers with the first row whose errno matches, so two rows sharing one errno make the second unreachable and the client is told the wrong error. This series re-keys UnexpectedPCRState to EPERM and adds PCRsKeptChanging on EUCLEAN, so walk the table by id and check that the errno lookup comes back to the same row, in both errno signs. Co-Authored-By: Claude Opus 5 --- src/test/test-creds.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/test/test-creds.c b/src/test/test-creds.c index 661a766502..cb57784c4b 100644 --- a/src/test/test-creds.c +++ b/src/test/test-creds.c @@ -503,4 +503,41 @@ TEST(mime_type_matches) { } } +TEST(credentials_varlink_error) { + /* The errno lookup answers with the first row that matches, so two rows sharing an errno would make + * one of them unreachable, and the error the client is told would silently be the wrong one. */ + + static const char* const ids[] = { + "io.systemd.Credentials.BadFormat", + "io.systemd.Credentials.NameMismatch", + "io.systemd.Credentials.TimeMismatch", + "io.systemd.Credentials.NoSuchUser", + "io.systemd.Credentials.BadScope", + "io.systemd.Credentials.CantFindPCRSignature", + "io.systemd.Credentials.NullKeyNotAllowed", + "io.systemd.Credentials.KeyBelongsToOtherTPM", + "io.systemd.Credentials.TPMInDictionaryLockout", + "io.systemd.Credentials.UnexpectedPCRState", + "io.systemd.Credentials.PCRsKeptChanging", + "io.systemd.Credentials.NVIndexUnusable", + }; + + FOREACH_ELEMENT(id, ids) { + const CredentialsVarlinkError *e = ASSERT_PTR(credentials_varlink_error_by_id(*id)); + + ASSERT_NOT_NULL(e->msg); + ASSERT_STREQ(ASSERT_PTR(credentials_varlink_error_by_errno(e->errnum))->id, *id); + ASSERT_PTR_EQ(credentials_varlink_error_by_errno(-e->errnum), e); + } + + /* The two errnos the PCR diagnosis hangs on */ + ASSERT_STREQ(ASSERT_PTR(credentials_varlink_error_by_errno(EPERM))->id, + "io.systemd.Credentials.UnexpectedPCRState"); + ASSERT_STREQ(ASSERT_PTR(credentials_varlink_error_by_errno(EUCLEAN))->id, + "io.systemd.Credentials.PCRsKeptChanging"); + + ASSERT_NULL(credentials_varlink_error_by_id("io.systemd.Credentials.NoSuchError")); + ASSERT_NULL(credentials_varlink_error_by_errno(EDOM)); +} + DEFINE_TEST_MAIN(LOG_INFO);