From 2de1322dc2c312aa7f65ae08040e70c2fe613faa Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:19:42 +0200 Subject: [PATCH 1/6] tpm2-setup: introduce a 'priority' concept for NvPCRs --- src/analyze/analyze-nvpcrs.c | 5 ++-- src/shared/tpm2-util.c | 18 ++++++++++--- src/shared/tpm2-util.h | 9 +++++-- src/tpm2-setup/tpm2-setup.c | 52 ++++++++++++++++++++++++++++++++++-- 4 files changed, 74 insertions(+), 10 deletions(-) diff --git a/src/analyze/analyze-nvpcrs.c b/src/analyze/analyze-nvpcrs.c index 56b5c9a204..c3967ca01a 100644 --- a/src/analyze/analyze-nvpcrs.c +++ b/src/analyze/analyze-nvpcrs.c @@ -16,6 +16,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { _cleanup_free_ char *h = NULL; uint32_t nv_index = 0; + uint64_t priority = 0; if (c) { if (!*c) { r = tpm2_context_new_or_warn(/* device= */ NULL, c); @@ -24,7 +25,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { } _cleanup_(iovec_done) struct iovec digest = {}; - r = tpm2_nvpcr_read(*c, /* session= */ NULL, name, &digest, &nv_index); + r = tpm2_nvpcr_read(*c, /* session= */ NULL, name, &digest, &nv_index, &priority); if (r < 0) return log_error_errno(r, "Failed to read NvPCR '%s': %m", name); if (r > 0) { /* set? */ @@ -33,7 +34,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { return log_oom(); } } else { - r = tpm2_nvpcr_get_index(name, &nv_index); + r = tpm2_nvpcr_get_index(name, &nv_index, &priority); if (r < 0) return log_error_errno(r, "Failed to get NV index of NvPCR '%s': %m", name); } diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 533cdd6306..26ce8d7246 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -6991,6 +6991,7 @@ typedef struct NvPCRData { char *name; uint16_t algorithm; uint32_t nv_index; + uint64_t priority; } NvPCRData; static void nvpcr_data_done(NvPCRData *d) { @@ -7030,11 +7031,13 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { { "name", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, offsetof(NvPCRData, name), SD_JSON_MANDATORY }, { "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 }, {}, }; _cleanup_(nvpcr_data_done) NvPCRData p = { .algorithm = TPM2_ALG_SHA256, + .priority = TPM2_NVPCR_PRIORITY_DEFAULT, }; r = sd_json_dispatch(v, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &p); if (r < 0) @@ -7047,7 +7050,7 @@ static int nvpcr_data_load(const char *name, NvPCRData *ret) { return 0; } -int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority) { int r; _cleanup_(nvpcr_data_done) NvPCRData p = {}; @@ -7055,8 +7058,10 @@ int tpm2_nvpcr_get_index(const char *name, uint32_t *ret) { if (r < 0) return r; - if (ret) - *ret = p.nv_index; + if (ret_nv_index) + *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return 0; } @@ -7748,7 +7753,8 @@ int tpm2_nvpcr_read( const Tpm2Handle *session, const char *name, struct iovec *ret_value, - uint32_t *ret_nv_index) { + uint32_t *ret_nv_index, + uint64_t *ret_priority) { #if HAVE_OPENSSL int r; @@ -7775,6 +7781,8 @@ int tpm2_nvpcr_read( *ret_value = (struct iovec) {}; if (ret_nv_index) *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return 0; } @@ -7811,6 +7819,8 @@ int tpm2_nvpcr_read( if (ret_nv_index) *ret_nv_index = p.nv_index; + if (ret_priority) + *ret_priority = p.priority; return r; #else /* HAVE_OPENSSL */ diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index f2f63a94a4..bf2321a514 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -158,11 +158,16 @@ typedef enum Tpm2UserspaceEventType { DECLARE_STRING_TABLE_LOOKUP(tpm2_userspace_event_type, Tpm2UserspaceEventType); int tpm2_pcr_extend_bytes(Tpm2Context *c, char **banks, unsigned pcr_index, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); -int tpm2_nvpcr_get_index(const char *name, uint32_t *ret); + +/* Default allocation priority for NvPCRs that do not specify one explicitly. Lower values are more + * important and are allocated first when the TPM's NV index space is constrained. */ +#define TPM2_NVPCR_PRIORITY_DEFAULT UINT64_C(1000) + +int tpm2_nvpcr_get_index(const char *name, uint32_t *ret_nv_index, uint64_t *ret_priority); int tpm2_nvpcr_extend_bytes(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *data, const struct iovec *secret, Tpm2UserspaceEventType event_type, const char *description); int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary); int tpm2_nvpcr_initialize(Tpm2Context *c, const Tpm2Handle *session, const char *name, const struct iovec *anchor_secret); -int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index); +int tpm2_nvpcr_read(Tpm2Context *c, const Tpm2Handle *session, const char *name, struct iovec *ret, uint32_t *ret_nv_index, uint64_t *ret_priority); uint32_t tpm2_tpms_pcr_selection_to_mask(const TPMS_PCR_SELECTION *s); void tpm2_tpms_pcr_selection_from_mask(uint32_t mask, TPMI_ALG_HASH hash, TPMS_PCR_SELECTION *ret); diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index bb08e31a81..ac294f7fae 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -23,6 +23,7 @@ #include "parse-util.h" #include "pretty-print.h" #include "set.h" +#include "sort-util.h" #include "string-util.h" #include "strv.h" #include "tmpfile-util.h" @@ -434,6 +435,26 @@ static int setup_nvpcr_one( return 0; } +typedef struct NvPCREntry { + const char *name; /* points into the strv 'l' in setup_nvpcr() */ + uint64_t priority; +} NvPCREntry; + +static int nvpcr_entry_compare(const NvPCREntry *a, const NvPCREntry *b) { + int r; + + assert(a); + assert(b); + + /* Lower priority value means more important, hence allocate it first. Break ties by name + * (ascending) so the resulting order is fully deterministic. */ + r = CMP(a->priority, b->priority); + if (r != 0) + return r; + + return strcmp(a->name, b->name); +} + static int setup_nvpcr(void) { _cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {}; int r; @@ -448,8 +469,33 @@ static int setup_nvpcr(void) { if (r < 0) return log_error_errno(r, "Failed to find .nvpcr files: %m"); + /* Pair each NvPCR name with its allocation priority, then sort, so that we allocate the most + * important NvPCRs first. This matters when the TPM's NV index space is too small to fit all of + * them: we then degrade gracefully, skipping the least important NvPCRs. */ + size_t n = strv_length(l); + _cleanup_free_ NvPCREntry *entries = new(NvPCREntry, n); + if (!entries) + return log_oom(); + + for (size_t i = 0; i < n; i++) { + uint64_t priority = TPM2_NVPCR_PRIORITY_DEFAULT; + + r = tpm2_nvpcr_get_index(l[i], /* ret_nv_index= */ NULL, &priority); + if (r < 0) + /* If we can't read the definition, assume the default priority and let the + * per-NvPCR error path in setup_nvpcr_one() report the actual problem. */ + log_warning_errno(r, "Failed to read priority of NvPCR '%s', assuming default priority %" PRIu64 ": %m", l[i], priority); + + entries[i] = (NvPCREntry) { + .name = l[i], + .priority = priority, + }; + } + + typesafe_qsort(entries, n, nvpcr_entry_compare); + int ret = 0; - STRV_FOREACH(i, l) { + FOREACH_ARRAY(e, entries, n) { if (!c.tpm2_context) { /* Inability to contact the TPM shall be fatal for us */ r = tpm2_context_new_or_warn(arg_tpm2_device, &c.tpm2_context); @@ -457,8 +503,10 @@ static int setup_nvpcr(void) { return r; } + log_debug("Setting up NvPCR '%s' (priority %" PRIu64 ").", e->name, e->priority); + /* But if we fail to initialize some NvPCR, we go on */ - RET_GATHER(ret, setup_nvpcr_one(&c, *i)); + RET_GATHER(ret, setup_nvpcr_one(&c, e->name)); } if (c.n_already > 0 && c.n_anchored == 0 && !arg_early) From 2fca7519a422ea8d71efb93595a24f6f1f4a0826 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:20:37 +0200 Subject: [PATCH 2/6] analyze: show priority in 'nvpcrs' output --- src/analyze/analyze-nvpcrs.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/analyze/analyze-nvpcrs.c b/src/analyze/analyze-nvpcrs.c index c3967ca01a..dde31be073 100644 --- a/src/analyze/analyze-nvpcrs.c +++ b/src/analyze/analyze-nvpcrs.c @@ -43,6 +43,7 @@ static int add_nvpcr_to_table(Tpm2Context **c, Table *t, const char *name) { t, TABLE_STRING, name, TABLE_UINT32_HEX_0x, nv_index, + TABLE_UINT64, priority, TABLE_STRING, h); if (r < 0) return table_log_add_error(r); @@ -62,16 +63,17 @@ int verb_nvpcrs(int argc, char *argv[], uintptr_t _data, void *userdata) { if (!have_tpm2) log_notice("System lacks full TPM2 support, not showing NvPCR state."); - table = table_new("name", "nvindex", "value"); + table = table_new("name", "nvindex", "priority", "value"); if (!table) return log_oom(); (void) table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + (void) table_set_align_percent(table, table_get_cell(table, 0, 2), 100); table_set_ersatz_string(table, TABLE_ERSATZ_DASH); - (void) table_set_sort(table, (size_t) 0); + (void) table_set_sort(table, (size_t) 2, (size_t) 0); if (!have_tpm2) - (void) table_hide_column_from_display(table, (size_t) 2); + (void) table_hide_column_from_display(table, (size_t) 3); if (strv_isempty(strv_skip(argv, 1))) { _cleanup_strv_free_ char **l = NULL; From 533aa457f6d9e01b6e0f76ab26ccfecb1dafb579 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:21:02 +0200 Subject: [PATCH 3/6] nvpcr: define some reasonable priorities for the nvpcrs we already defined --- src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in | 3 ++- src/tpm2-setup/nvpcr/hardware.nvpcr.in | 3 ++- src/tpm2-setup/nvpcr/verity.nvpcr.in | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in index acec95206f..3f01ef34eb 100644 --- a/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in +++ b/src/tpm2-setup/nvpcr/cryptsetup.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "cryptsetup", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 1}} + "nvIndex" : {{TPM2_NVPCR_BASE + 1}}, + "priority" : 700 } diff --git a/src/tpm2-setup/nvpcr/hardware.nvpcr.in b/src/tpm2-setup/nvpcr/hardware.nvpcr.in index ddeb7f99e5..bb4a3afa69 100644 --- a/src/tpm2-setup/nvpcr/hardware.nvpcr.in +++ b/src/tpm2-setup/nvpcr/hardware.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "hardware", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 0}} + "nvIndex" : {{TPM2_NVPCR_BASE + 0}}, + "priority" : 500 } diff --git a/src/tpm2-setup/nvpcr/verity.nvpcr.in b/src/tpm2-setup/nvpcr/verity.nvpcr.in index b4fb62bd76..7d1630d6cd 100644 --- a/src/tpm2-setup/nvpcr/verity.nvpcr.in +++ b/src/tpm2-setup/nvpcr/verity.nvpcr.in @@ -1,5 +1,6 @@ { "name" : "verity", "algorithm" : "sha256", - "nvIndex" : {{TPM2_NVPCR_BASE + 2}} + "nvIndex" : {{TPM2_NVPCR_BASE + 2}}, + "priority" : 300 } From 101a7af63fec5dd5f80282c2739e6186a1b079a2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:21:28 +0200 Subject: [PATCH 4/6] tpm2-util: be more graceful with nvindex space exhaustion issues Let's tone down the language, and say how many we skipped. --- src/tpm2-setup/tpm2-setup.c | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index ac294f7fae..eb25d7fd7f 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -370,7 +370,8 @@ static int setup_srk(void) { typedef struct SetupNvPCRContext { Tpm2Context *tpm2_context; struct iovec anchor_secret; - size_t n_already, n_anchored, n_failed; + size_t n_already, n_anchored, n_failed, n_skipped; + bool nv_space_exhausted; /* Set once the TPM ran out of NV index space, so we skip the rest. */ Set *done; } SetupNvPCRContext; @@ -394,6 +395,15 @@ static int setup_nvpcr_one( if (set_contains(c->done, name)) return 0; + /* If the TPM already ran out of NV index space, don't even try to allocate further NvPCRs. As + * NvPCRs are processed in order of priority (most important first), everything still pending is + * less important than what already didn't fit. */ + if (c->nv_space_exhausted) { + log_debug("TPM NV index space already exhausted, skipping allocation of NvPCR '%s'.", name); + c->n_skipped++; + return 0; + } + r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret); if (r == -EUNATCH) { assert(!iovec_is_set(&c->anchor_secret)); @@ -414,9 +424,13 @@ static int setup_nvpcr_one( LOG_MESSAGE_ID(SD_MESSAGE_TPM_NVPCR_UNSUPPORTED_STR)); } if (r == -ENOSPC) { - c->n_failed++; - return log_struct_errno(LOG_ERR, r, - LOG_MESSAGE("The TPM's NV index space is exhausted, unable to allocate NvPCR '%s': %m", name), + /* The TPM's NV index space is exhausted. Remember this so we skip the remaining (less + * important) NvPCRs, and report it gracefully at the end rather than failing the boot. + * Logged at notice level, not error. */ + c->nv_space_exhausted = true; + c->n_skipped++; + return log_struct_errno(LOG_NOTICE, r, + LOG_MESSAGE("The TPM's NV index space is exhausted, skipping allocation of NvPCR '%s' and any less important ones: %m", name), LOG_MESSAGE_ID(SD_MESSAGE_TPM_NVINDEX_EXHAUSTED_STR)); } if (r < 0) { @@ -515,6 +529,9 @@ static int setup_nvpcr(void) { * yet. Hence, let's explicitly do so now, to catch up. */ RET_GATHER(ret, tpm2_nvpcr_acquire_anchor_secret(/* ret= */ NULL, /* sync_secondary= */ true)); + if (c.nv_space_exhausted) + log_notice("Skipped %zu lowest-priority NvPCR(s) because the TPM's NV index space is exhausted, proceeding anyway.", c.n_skipped); + if (c.n_failed > 0) log_warning("%zu NvPCRs failed to initialize, proceeding anyway.", c.n_failed); @@ -525,7 +542,7 @@ static int setup_nvpcr(void) { log_info("%zu NvPCRs initialized. (%zu NvPCRs were already initialized.)", c.n_anchored, c.n_already); } else if (c.n_already > 0) log_info("%zu NvPCRs already initialized.", c.n_already); - else if (c.n_failed == 0) + else if (c.n_failed == 0 && c.n_skipped == 0) log_debug("No NvPCRs defined, nothing initialized."); /* Turn some errors into recognizable ones, which we can catch with From 89c837a5f867ae408e69c683cf92738443b742ba Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:22:03 +0200 Subject: [PATCH 5/6] docs: document the .nvpcr file format superficially --- docs/TPM2_PCR_MEASUREMENTS.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/TPM2_PCR_MEASUREMENTS.md b/docs/TPM2_PCR_MEASUREMENTS.md index 571a4c1077..5d0801a907 100644 --- a/docs/TPM2_PCR_MEASUREMENTS.md +++ b/docs/TPM2_PCR_MEASUREMENTS.md @@ -58,7 +58,22 @@ away there's a naming concept, so that nvindexes are referenced by name string rather than number. NvPCRs are defined in little JSON snippets in `/usr/lib/nvpcr/*.nvpcr`, that -match up index number and name, as well as pick a hash algorithm. +match up index number and name, as well as pick a hash algorithm. The recognized +fields are: + +* `name` — the NvPCR name (string), which must match the file name (without the + `.nvpcr` suffix). Mandatory. +* `nvIndex` — the fixed TPM2 NV index handle (number) to allocate for this NvPCR. + Mandatory. +* `algorithm` — the hash algorithm to use (string), e.g. `sha256` (the default). +* `priority` — an unsigned integer allocation priority, defaulting to `1000`. + Lower values are considered more important and are allocated first. This only + affects the order in which `systemd-tpm2-setup.service` attempts allocation at + boot: if the TPM's NV index space is too small to fit all NvPCRs, the most + important ones (lowest `priority` value) win the available space, and the + 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. 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 From f4c5fabbf311f429282e8507bc09c66a14de120e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Thu, 21 May 2026 14:22:29 +0200 Subject: [PATCH 6/6] ci: add a CI test for nvpcr priorization --- test/units/TEST-70-TPM2.nvpcr.sh | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/units/TEST-70-TPM2.nvpcr.sh b/test/units/TEST-70-TPM2.nvpcr.sh index 05ae378d84..d84bb1feb3 100755 --- a/test/units/TEST-70-TPM2.nvpcr.sh +++ b/test/units/TEST-70-TPM2.nvpcr.sh @@ -21,7 +21,8 @@ at_exit() { fi rm -rf /run/nvpcr /tmp/nvpcr - rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt /run/systemd/nvpcr/test.anchor + rm -f /var/tmp/nvpcr.raw /run/verity.d/test-70-nvpcr.crt + rm -f /run/systemd/nvpcr/test.anchor /run/systemd/nvpcr/test2.anchor /run/systemd/nvpcr/aaa.anchor /run/systemd/nvpcr/zzz.anchor } trap at_exit EXIT @@ -54,6 +55,33 @@ DIGEST_MEASURED2="$(echo -n "schnurz" | openssl dgst -sha256 -hex -r | cut -d' ' DIGEST_EXPECTED2="$(echo "$DIGEST_EXPECTED$DIGEST_MEASURED2" | tr '[:lower:]' '[:upper:]' | basenc --base16 -d | openssl dgst -sha256 -hex -r | cut -d' ' -f1)" test "$DIGEST_ACTUAL2" = "$DIGEST_EXPECTED2" +# Verify the 'priority' field round-trips through the JSON definition. The 'test' NvPCR above sets no +# priority, so it must report the default (1000). +PRIORITY_DEFAULT="$(systemd-analyze nvpcrs test --json=pretty | jq -r '.[] | select(.name=="test") | .priority')" +test "$PRIORITY_DEFAULT" = "1000" + +# A definition with an explicit priority must report exactly that value. +cat >/run/nvpcr/test2.nvpcr </run/nvpcr/aaa.nvpcr </run/nvpcr/zzz.nvpcr <