From 55be5e189086a147a97cc25b8f58c0b89f6c3d05 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 08:52:16 +0200 Subject: [PATCH 1/3] tpm2: optionally disable TPMA_NV_ORDERLY for NvPCRs NVIndexes in TPMs can operate in two modes: 1. Backed by TPM RAM. In this case they are only written to NVRAM on an orderly TPM shutdown when the system goes down. (TPMA_NV_ORDERLY flag is on) 2. Backed by TPM NVRAM. In this case the nvindex value is written to NVRAM on every write, and things are not delayed until orderly shutdown. Normally mode 1 sounds like the obvious choice for NvPCRs, which reset to zero anyway at boot. However, things are more complicated since real-life TPMs tend to have a lot less RAM than NVRAM (both are constrained but RAM even more than NVRAM). Hence there's value in using NVRAM right-away. However, writing to NVRAM all the time means wearing it out (since NVRAM is more vulnerable to that). So far we unconditionally went for mode 1, but ran into space constraints of RAM due to that. Let's improve things a bit, and use orderly mode for NvPCRs we expect to write many times, and non-orderly mode for those we expect to write only a small, fixed number of times at boot, and not anymore during runtime. Right now, this is only the "hardware" NvPCR, which measures hw identity at boot. Hopefully, this stretches available resources a bit further. This also makes sure if the flag was set differently on allocation as we'd set now, we accept it and won't complain, to make upgrades safe. Suggested by Andreas Fuchs. --- docs/TPM2_PCR_MEASUREMENTS.md | 12 ++++++++++++ src/shared/tpm2-util.c | 9 +++++++-- src/tpm2-setup/nvpcr/hardware.nvpcr.in | 3 ++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index faa918f631..87d8d3b675 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -74,6 +74,18 @@ fields are: least important ones are skipped gracefully rather than the allocation failing arbitrarily. Ties are broken by name. Priority does not affect the NV index, the algorithm, or anything measured into the NvPCR. +* `orderly` — a boolean, defaulting to `true`. It controls whether the NV index + is allocated with the `TPMA_NV_ORDERLY` attribute set, which selects whether + the TPM keeps the NV index in RAM or in persistent memory (NVRAM). On + physical TPMs RAM is typically much more constrained than persistent memory, + but persistent memory is subject to wear. We hence prefer `TPMA_NV_ORDERLY` + disabled (i.e. NVRAM) for NvPCRs that are written only once each boot — which + translates into a conservative number of write cycles over the lifetime of a + TPM — but enabled (i.e. RAM) for NvPCRs we expect to be written many times + during runtime, so that we minimize wear. This reflects real-life experience + where the RAM in TPMs is so constrained that allocating many NvPCRs in TPM + RAM simply doesn't work. For now, only the `hardware` NvPCR (which is written + just once, early at boot) sets this flag to false. There's one complication: these NV indexes (like any NV indexes) can be deleted by anyone with access to the TPM, and then be recreated. This could be used to diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 3a9fbaa7e4..1920d6f171 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7183,6 +7183,7 @@ static int tpm2_define_nvpcr_nv_index( const Tpm2Handle *session, TPM2_HANDLE nv_index, TPMI_ALG_HASH algorithm, + bool orderly, Tpm2Handle **ret_nv_handle) { _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; @@ -7215,7 +7216,7 @@ static int tpm2_define_nvpcr_nv_index( .nvIndex = nv_index, .nameAlg = algorithm, .attributes = TPMA_NV_CLEAR_STCLEAR | - TPMA_NV_ORDERLY | + (orderly ? TPMA_NV_ORDERLY : 0) | TPMA_NV_OWNERWRITE | TPMA_NV_AUTHWRITE | TPMA_NV_OWNERREAD | @@ -7259,7 +7260,7 @@ static int tpm2_define_nvpcr_nv_index( if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || - ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~TPMA_NV_WRITTEN) != 0 || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), "Public data of nvindex 0x%x does not match our expectations.", nv_index); @@ -7976,6 +7977,7 @@ typedef struct NvPCRData { uint16_t algorithm; uint32_t nv_index; uint64_t priority; + bool orderly; } NvPCRData; static void nvpcr_data_done(NvPCRData *d) { @@ -8016,12 +8018,14 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { { "algorithm", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_tpm2_algorithm, offsetof(NvPCRData, algorithm), 0 }, { "nvIndex", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint32, offsetof(NvPCRData, nv_index), SD_JSON_MANDATORY }, { "priority", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(NvPCRData, priority), 0 }, + { "orderly", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(NvPCRData, orderly), 0 }, {}, }; _cleanup_(nvpcr_data_done) NvPCRData p = { .algorithm = TPM2_ALG_SHA256, .priority = TPM2_NVPCR_PRIORITY_DEFAULT, + .orderly = true, }; r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) @@ -8652,6 +8656,7 @@ int tpm2_nvpcr_initialize( session, p.nv_index, p.algorithm, + p.orderly, &nv_handle); if (r < 0) return r; diff --git a/src/tpm2-setup/nvpcr/hardware.nvpcr.in b/src/tpm2-setup/nvpcr/hardware.nvpcr.in index bb4a3afa69..0fdf32d309 100644 --- a/src/tpm2-setup/nvpcr/hardware.nvpcr.in +++ b/src/tpm2-setup/nvpcr/hardware.nvpcr.in @@ -2,5 +2,6 @@ "name" : "hardware", "algorithm" : "sha256", "nvIndex" : {{TPM2_NVPCR_BASE + 0}}, - "priority" : 500 + "priority" : 500, + "orderly" : false } From 8aaa8dec59a9d7dca37ed03ef6550a23c6268ec0 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 09:13:35 +0200 Subject: [PATCH 2/3] tpm2: cache NvPCR NV space exhaustion via flag files in /run/ When we run out of NV index space while allocating an NvPCR, the situation will unlikely improve until (at least) reboot. Retrying the (doomed) Esys_NV_DefineSpace call on every subsequent allocation attempt is wasteful (and very slow), so remember the exhaustion in a flag file under /run/ and fail early next time. We use two separate flag files, one for orderly and one for non-orderly NvPCRs, since the two draw on different TPM resources (RAM-backed vs. NVRAM-backed): exhaustion of one doesn't imply exhaustion of the other. The files live in /run/, hence are cleared on reboot, which is potentially is when NV space might become available again. --- src/shared/tpm2-util.c | 130 +++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 49 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 1920d6f171..27606ab73d 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -7186,7 +7186,6 @@ static int tpm2_define_nvpcr_nv_index( bool orderly, Tpm2Handle **ret_nv_handle) { - _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; TSS2_RC rc; int r; @@ -7204,11 +7203,13 @@ static int tpm2_define_nvpcr_nv_index( if ((size_t) digest_size > sizeof_field(TPM2B_MAX_NV_BUFFER, buffer)) return log_debug_errno(SYNTHETIC_ERRNO(E2BIG), "Digest too large for extension."); - r = tpm2_handle_new(c, &new_handle); - if (r < 0) - return r; - - new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ + /* If we already ran into NV index space exhaustion for this orderly mode during this boot, don't + * bother trying again — the situation is unlikely to improve until reboot. We track this via a flag + * file in /run/, with a separate file for orderly and non-orderly NvPCRs, since the two draw on + * different TPM resources (RAM-backed vs. NVRAM-backed). */ + const char *exhausted_flag = orderly ? + "/run/systemd/tpm2-nv-space-exhausted-orderly" : + "/run/systemd/tpm2-nv-space-exhausted-non-orderly"; TPM2B_NV_PUBLIC public_info = { .size = sizeof_field(TPM2B_NV_PUBLIC, nvPublic), @@ -7226,62 +7227,93 @@ static int tpm2_define_nvpcr_nv_index( }, }; - rc = sym_Esys_NV_DefineSpace( - c->esys_context, - /* authHandle= */ ESYS_TR_RH_OWNER, - /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, - /* shandle2= */ ESYS_TR_NONE, - /* shandle3= */ ESYS_TR_NONE, - /* auth= */ NULL, - &public_info, - &new_handle->esys_handle); - if (rc == TPM2_RC_NV_SPACE) - return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), - "NV index space on TPM exhausted, cannot allocate NvPCR."); - if (rc == TPM2_RC_NV_DEFINED) { - log_debug("NV index 0x%" PRIu32 " already registered.", nv_index); + bool exhausted; + if (access(exhausted_flag, F_OK) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check whether %s exists, assuming it does not: %m", exhausted_flag); - new_handle = tpm2_handle_free(new_handle); + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + r = tpm2_handle_new(c, &new_handle); + if (r < 0) + return r; - _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; - r = tpm2_nv_index_to_handle( - c, - nv_index, - session, - &nv_public_real, - /* ret_name= */ NULL, - &new_handle); - if (r <= 0) - return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to acquire handle to existing NV index 0x%" PRIu32 ".", nv_index); + new_handle->flush = false; /* This is a persistent NV index, don't flush hence */ - log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); + rc = sym_Esys_NV_DefineSpace( + c->esys_context, + /* authHandle= */ ESYS_TR_RH_OWNER, + /* shandle1= */ session ? session->esys_handle : ESYS_TR_PASSWORD, + /* shandle2= */ ESYS_TR_NONE, + /* shandle3= */ ESYS_TR_NONE, + /* auth= */ NULL, + &public_info, + &new_handle->esys_handle); + if (rc == TPM2_RC_NV_SPACE) { + /* Remember that we ran out of NV index space for this orderly mode, so that we don't keep + * retrying the (doomed) allocation until reboot. */ + r = touch(exhausted_flag); + if (r < 0) + log_debug_errno(r, "Failed to create %s flag file, ignoring: %m", exhausted_flag); - if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || - nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || - nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || - ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || - nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) - return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), - "Public data of nvindex 0x%x does not match our expectations.", nv_index); + return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), + "NV index space on TPM exhausted, cannot allocate NvPCR."); + } + if (rc == TSS2_RC_SUCCESS) { + log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); - log_debug("Public info for nvindex 0x%x checks out, using.", nv_index); + if (ret_nv_handle) + *ret_nv_handle = TAKE_PTR(new_handle); - if (ret_nv_handle) - *ret_nv_handle = TAKE_PTR(new_handle); + return 1; + } + if (rc != TPM2_RC_NV_DEFINED) + return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); - return 0; + log_debug("NV index 0x%" PRIx32 " already registered.", nv_index); + exhausted = false; + } else { + log_debug("TPM NV index space previously found exhausted (%s exists), refusing to allocate %s NvPCR, but checking if it already exists.", + exhausted_flag, orderly ? "orderly" : "non-orderly"); + exhausted = true; } - if (rc != TSS2_RC_SUCCESS) - return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), - "Failed to allocate NV index: %s", sym_Tss2_RC_Decode(rc)); - log_debug("NV Index 0x%" PRIx32 " successfully allocated.", nv_index); + /* We either got told that this NV index already exists or we didn't even try to allocate it, because + * it failed before. Let's get information about it, in the hope it exists. */ + + _cleanup_(Esys_Freep) TPM2B_NV_PUBLIC *nv_public_real = NULL; + _cleanup_(tpm2_handle_freep) Tpm2Handle *new_handle = NULL; + r = tpm2_nv_index_to_handle( + c, + nv_index, + session, + &nv_public_real, + /* ret_name= */ NULL, + &new_handle); + if (r <= 0) { + if (exhausted) + return log_debug_errno(SYNTHETIC_ERRNO(ENOBUFS), "Unable to acquire NvPCR and space exhaustion was indicated before."); + + return log_debug_errno(r < 0 ? r : SYNTHETIC_ERRNO(ENOTRECOVERABLE), + "Failed to acquire handle to NV index 0x%" PRIx32 ".", nv_index); + } + + log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", nv_index); + + if (nv_public_real->size < endoffsetof_field(TPMS_NV_PUBLIC, attributes) + sizeof_field(TPMS_NV_PUBLIC, dataSize) || + nv_public_real->nvPublic.nvIndex != public_info.nvPublic.nvIndex || + nv_public_real->nvPublic.nameAlg != public_info.nvPublic.nameAlg || + ((nv_public_real->nvPublic.attributes ^ public_info.nvPublic.attributes) & ~(TPMA_NV_WRITTEN|TPMA_NV_ORDERLY)) != 0 || + nv_public_real->nvPublic.dataSize != public_info.nvPublic.dataSize) + return log_debug_errno(SYNTHETIC_ERRNO(EEXIST), + "Public data of nvindex 0x%" PRIx32 " does not match our expectations.", nv_index); + + log_debug("Public info for nvindex 0x%" PRIx32 " checks out, using.", nv_index); if (ret_nv_handle) *ret_nv_handle = TAKE_PTR(new_handle); - return 1; + return 0; } static int tpm2_extend_nvpcr_nv_index( From a84642ed94cf25784449919468e110c08229d75d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 2 Jul 2026 09:56:05 +0200 Subject: [PATCH 3/3] update TODO --- TODO.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/TODO.md b/TODO.md index 8ef085f693..f7b9585f6d 100644 --- a/TODO.md +++ b/TODO.md @@ -938,16 +938,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later - drop nss-myhostname in favour of nss-resolve? -- drop NV_ORDERLY flag from the product uuid nvpcr. Effect of the flag is that - it pushes the thing into TPM RAM, but a TPM usually has very little of that, - less than NVRAM. hence setting the flag amplifies space issues. Unsetting the - flag increases wear issues on the NVRAM, however, but this should be limited - for the product uuid nvpcr, since its only changed once per boot. this needs - to be configurable by nvpcr however, as other nvpcrs are different, - i.e. verity one receives many writes during system uptime quite - possibly. (also, NV_ORDERLY makes stuff faster, and dropping it costs - possibly up to 100ms supposedly) - - **EFI:** - honor timezone efi variables for default timezone selection (if there are any?)