tpm2: optionally disable TPMA_NV_ORDERLY for NvPCRs (#42848)

This commit is contained in:
Lennart Poettering
2026-07-03 07:18:02 +02:00
committed by GitHub
4 changed files with 101 additions and 61 deletions
-10
View File
@@ -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?)
+12
View File
@@ -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
+87 -50
View File
@@ -7183,9 +7183,9 @@ 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;
TSS2_RC rc;
int r;
@@ -7203,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),
@@ -7215,7 +7217,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 |
@@ -7225,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) != 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(
@@ -7976,6 +8009,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 +8050,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 +8688,7 @@ int tpm2_nvpcr_initialize(
session,
p.nv_index,
p.algorithm,
p.orderly,
&nv_handle);
if (r < 0)
return r;
+2 -1
View File
@@ -2,5 +2,6 @@
"name" : "hardware",
"algorithm" : "sha256",
"nvIndex" : {{TPM2_NVPCR_BASE + 0}},
"priority" : 500
"priority" : 500,
"orderly" : false
}