tpm2: add nvpcr allocation priorities (#42227)

nvindex space is very scarce, let's hence introduce "priorities" on
nvpcrs, so that if we haven't got enough space for all we have some
control which ones get dropped and which ones kept.
This commit is contained in:
Lennart Poettering
2026-05-21 18:19:39 +02:00
committed by GitHub
9 changed files with 152 additions and 23 deletions
+16 -1
View File
@@ -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
+8 -5
View File
@@ -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);
}
@@ -42,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);
@@ -61,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;
+14 -4
View File
@@ -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 */
+7 -2
View File
@@ -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);
+2 -1
View File
@@ -1,5 +1,6 @@
{
"name" : "cryptsetup",
"algorithm" : "sha256",
"nvIndex" : {{TPM2_NVPCR_BASE + 1}}
"nvIndex" : {{TPM2_NVPCR_BASE + 1}},
"priority" : 700
}
+2 -1
View File
@@ -1,5 +1,6 @@
{
"name" : "hardware",
"algorithm" : "sha256",
"nvIndex" : {{TPM2_NVPCR_BASE + 0}}
"nvIndex" : {{TPM2_NVPCR_BASE + 0}},
"priority" : 500
}
+2 -1
View File
@@ -1,5 +1,6 @@
{
"name" : "verity",
"algorithm" : "sha256",
"nvIndex" : {{TPM2_NVPCR_BASE + 2}}
"nvIndex" : {{TPM2_NVPCR_BASE + 2}},
"priority" : 300
}
+72 -7
View File
@@ -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"
@@ -369,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;
@@ -393,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));
@@ -413,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) {
@@ -434,6 +449,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 +483,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 +517,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)
@@ -467,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);
@@ -477,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
+29 -1
View File
@@ -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 <<EOF
{"name":"test2","algorithm":"sha256","nvIndex":30474763,"priority":42}
EOF
PRIORITY_EXPLICIT="$(systemd-analyze nvpcrs test2 --json=pretty | jq -r '.[] | select(.name=="test2") | .priority')"
test "$PRIORITY_EXPLICIT" = "42"
# Verify NvPCRs are allocated in order of priority (lower value = more important = allocated first),
# independent of lexical filename order. 'aaa' is lexically first but less important (higher priority
# value), while 'zzz' is lexically last but more important (lower priority value), so 'zzz' must be set
# up before 'aaa'.
cat >/run/nvpcr/aaa.nvpcr <<EOF
{"name":"aaa","algorithm":"sha256","nvIndex":30474772,"priority":900}
EOF
cat >/run/nvpcr/zzz.nvpcr <<EOF
{"name":"zzz","algorithm":"sha256","nvIndex":30474773,"priority":100}
EOF
SETUP_LOG="$(/usr/lib/systemd/systemd-tpm2-setup 2>&1)"
AAA_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'aaa'" | cut -d: -f1)"
ZZZ_LINE="$(echo "$SETUP_LOG" | grep -n "Setting up NvPCR 'zzz'" | cut -d: -f1)"
test "$ZZZ_LINE" -lt "$AAA_LINE"
systemd-analyze identify-tpm2
udevadm test-builtin 'tpm2_id identify' /dev/tpmrm0