From 170a8f1d408c872d32b62536a7a5d7efaf8f13a7 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 16:00:00 +0100 Subject: [PATCH 01/15] tpm2-util: mark two functions as static that are not used outside of tpm2-util.c --- src/shared/tpm2-util.c | 6 ++++-- src/shared/tpm2-util.h | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index e569436142..b0f6387b03 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -5976,7 +5976,8 @@ int tpm2_undefine_nv_index( return 0; } -int tpm2_define_nvpcr_nv_index( +#if HAVE_OPENSSL +static int tpm2_define_nvpcr_nv_index( Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, @@ -6095,7 +6096,7 @@ int tpm2_define_nvpcr_nv_index( return 1; } -int tpm2_extend_nvpcr_nv_index( +static int tpm2_extend_nvpcr_nv_index( Tpm2Context *c, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, @@ -6136,6 +6137,7 @@ int tpm2_extend_nvpcr_nv_index( return 0; } +#endif int tpm2_read_nv_index( Tpm2Context *c, diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 68289bec48..282aaa1e80 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -304,8 +304,6 @@ int tpm2_tpm2b_public_to_fingerprint(const TPM2B_PUBLIC *public, void **ret_fing int tpm2_define_policy_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE requested_nv_index, const TPM2B_DIGEST *write_policy, TPM2_HANDLE *ret_nv_index, Tpm2Handle **ret_nv_handle, TPM2B_NV_PUBLIC *ret_nv_public); int tpm2_write_policy_nv_index(Tpm2Context *c, const Tpm2Handle *policy_session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const TPM2B_DIGEST *policy_digest); -int tpm2_define_nvpcr_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, TPMI_ALG_HASH algorithm, Tpm2Handle **ret_nv_handle); -int tpm2_extend_nvpcr_nv_index(Tpm2Context *c, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, const struct iovec *digest); int tpm2_undefine_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle); int tpm2_read_nv_index(Tpm2Context *c, const Tpm2Handle *session, TPM2_HANDLE nv_index, const Tpm2Handle *nv_handle, struct iovec *ret_value); From 4dbb0cd0bf17a45aef21b70c9ddf8013d68c6a2d Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:07:57 +0100 Subject: [PATCH 02/15] parse_hwdb: introduce local variable for boolean syntax --- hwdb.d/parse_hwdb.py | 47 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/hwdb.d/parse_hwdb.py b/hwdb.d/parse_hwdb.py index 1a56f40c46..3c81fa371f 100755 --- a/hwdb.d/parse_hwdb.py +++ b/hwdb.d/parse_hwdb.py @@ -147,6 +147,7 @@ def property_grammar(): mount_matrix = Group(mount_matrix_row + ';' + mount_matrix_row + ';' + mount_matrix_row)('MOUNT_MATRIX') xkb_setting = Optional(Word(alphanums + '+-/@._')) id_input_setting = Optional(Or((Literal('0'), Literal('1')))) + zero_one = Or((Literal('0'), Literal('1'))) # Although this set doesn't cover all of characters in database entries, it's enough for test targets. name_literal = Word(printables + ' ') @@ -156,13 +157,13 @@ def property_grammar(): ('MOUSE_WHEEL_CLICK_ANGLE_HORIZONTAL', INTEGER), ('MOUSE_WHEEL_CLICK_COUNT', INTEGER), ('MOUSE_WHEEL_CLICK_COUNT_HORIZONTAL', INTEGER), - ('ID_INPUT_3D_MOUSE', Or((Literal('0'), Literal('1')))), - ('ID_AUTOSUSPEND', Or((Literal('0'), Literal('1')))), + ('ID_INPUT_3D_MOUSE', zero_one), + ('ID_AUTOSUSPEND', zero_one), ('ID_AUTOSUSPEND_DELAY_MS', INTEGER), - ('ID_AV_PRODUCTION_CONTROLLER', Or((Literal('0'), Literal('1')))), - ('ID_AV_LIGHTS', Or((Literal('0'), Literal('1')))), - ('ID_PERSIST', Or((Literal('0'), Literal('1')))), - ('ID_PDA', Or((Literal('0'), Literal('1')))), + ('ID_AV_PRODUCTION_CONTROLLER', zero_one), + ('ID_AV_LIGHTS', zero_one), + ('ID_PERSIST', zero_one), + ('ID_PDA', zero_one), ('ID_INPUT', id_input_setting), ('ID_INPUT_ACCELEROMETER', id_input_setting), ('ID_INPUT_JOYSTICK', id_input_setting), @@ -176,12 +177,12 @@ def property_grammar(): ('ID_INPUT_TOUCHPAD', id_input_setting), ('ID_INPUT_TOUCHSCREEN', id_input_setting), ('ID_INPUT_TRACKBALL', id_input_setting), - ('ID_SIGNAL_ANALYZER', Or((Literal('0'), Literal('1')))), - ('ID_MAKER_TOOL', Or((Literal('0'), Literal('1')))), - ('ID_HARDWARE_WALLET', Or((Literal('0'), Literal('1')))), - ('ID_SOFTWARE_RADIO', Or((Literal('0'), Literal('1')))), - ('ID_MM_DEVICE_IGNORE', Or((Literal('0'), Literal('1')))), - ('ID_NET_AUTO_LINK_LOCAL_ONLY', Or((Literal('0'), Literal('1')))), + ('ID_SIGNAL_ANALYZER', zero_one), + ('ID_MAKER_TOOL', zero_one), + ('ID_HARDWARE_WALLET', zero_one), + ('ID_SOFTWARE_RADIO', zero_one), + ('ID_MM_DEVICE_IGNORE', zero_one), + ('ID_NET_AUTO_LINK_LOCAL_ONLY', zero_one), ('POINTINGSTICK_SENSITIVITY', INTEGER), ('ID_INTEGRATION', Or(('internal', 'external'))), ('ID_INPUT_TOUCHPAD_INTEGRATION', Or(('internal', 'external'))), @@ -193,25 +194,25 @@ def property_grammar(): ('ACCEL_MOUNT_MATRIX', mount_matrix), ('ACCEL_LOCATION', Or(('display', 'base'))), ('PROXIMITY_NEAR_LEVEL', INTEGER), - ('IEEE1394_UNIT_FUNCTION_MIDI', Or((Literal('0'), Literal('1')))), - ('IEEE1394_UNIT_FUNCTION_AUDIO', Or((Literal('0'), Literal('1')))), - ('IEEE1394_UNIT_FUNCTION_VIDEO', Or((Literal('0'), Literal('1')))), + ('IEEE1394_UNIT_FUNCTION_MIDI', zero_one), + ('IEEE1394_UNIT_FUNCTION_AUDIO', zero_one), + ('IEEE1394_UNIT_FUNCTION_VIDEO', zero_one), ('ID_VENDOR_FROM_DATABASE', name_literal), ('ID_MODEL_FROM_DATABASE', name_literal), ('ID_TAG_MASTER_OF_SEAT', Literal('1')), - ('ID_INFRARED_CAMERA', Or((Literal('0'), Literal('1')))), + ('ID_INFRARED_CAMERA', zero_one), ('ID_CAMERA_DIRECTION', Or(('front', 'rear'))), ('SOUND_FORM_FACTOR', Or(('internal', 'webcam', 'speaker', 'headphone', 'headset', 'handset', 'microphone'))), - ('ID_SYS_VENDOR_IS_RUBBISH', Or((Literal('0'), Literal('1')))), - ('ID_PRODUCT_NAME_IS_RUBBISH', Or((Literal('0'), Literal('1')))), - ('ID_PRODUCT_VERSION_IS_RUBBISH', Or((Literal('0'), Literal('1')))), - ('ID_BOARD_VERSION_IS_RUBBISH', Or((Literal('0'), Literal('1')))), - ('ID_PRODUCT_SKU_IS_RUBBISH', Or((Literal('0'), Literal('1')))), - ('ID_CHASSIS_ASSET_TAG_IS_RUBBISH', Or((Literal('0'), Literal('1')))), + ('ID_SYS_VENDOR_IS_RUBBISH', zero_one), + ('ID_PRODUCT_NAME_IS_RUBBISH', zero_one), + ('ID_PRODUCT_VERSION_IS_RUBBISH', zero_one), + ('ID_BOARD_VERSION_IS_RUBBISH', zero_one), + ('ID_PRODUCT_SKU_IS_RUBBISH', zero_one), + ('ID_CHASSIS_ASSET_TAG_IS_RUBBISH', zero_one), ('ID_CHASSIS', name_literal), ('ID_SYSFS_ATTRIBUTE_MODEL', name_literal), ('ID_NET_NAME_FROM_DATABASE', name_literal), - ('ID_NET_NAME_INCLUDE_DOMAIN', Or((Literal('0'), Literal('1')))), + ('ID_NET_NAME_INCLUDE_DOMAIN', zero_one), ) fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE') for name, val in props] From 96a8bfcd7a3766e336c81b5031d8600577a01ab4 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:09:27 +0100 Subject: [PATCH 03/15] parse_hwdb: sort general matches --- hwdb.d/parse_hwdb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hwdb.d/parse_hwdb.py b/hwdb.d/parse_hwdb.py index 3c81fa371f..7efa61ad4b 100755 --- a/hwdb.d/parse_hwdb.py +++ b/hwdb.d/parse_hwdb.py @@ -100,13 +100,13 @@ TYPES = {'mouse': ('usb', 'bluetooth', 'ps2', '*'), # Patterns that are used to set general properties on a device GENERAL_MATCHES = {'acpi', 'bluetooth', - 'usb', + 'dmi', + 'ieee1394', + 'OUI', 'pci', 'sdio', + 'usb', 'vmbus', - 'OUI', - 'ieee1394', - 'dmi', } def upperhex_word(length): From 5f85409f932dfdc123d0e8ded8e8a9a6f9443119 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 14:27:36 +0100 Subject: [PATCH 04/15] tpm2-util: also load libtss2-tcti-device.so.0 in dlopen_tpm2() This TCTI module is the one we need to actually access a Linux TPM device, we'll hence pretty much always need it if we do TPM at all. Given that we nowadays turn off dlopen() after fork() in the child, let's explicitly load it as part of dlopen_tpm2() so that it is available whenever TPM2 is used. --- src/basic/dlfcn-util.c | 20 +++++++++++++++++++- src/basic/dlfcn-util.h | 1 + src/shared/tpm2-util.c | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/basic/dlfcn-util.c b/src/basic/dlfcn-util.c index 8574e99546..23bd1f666e 100644 --- a/src/basic/dlfcn-util.c +++ b/src/basic/dlfcn-util.c @@ -47,9 +47,11 @@ int dlsym_many_or_warn_sentinel(void *dl, int log_level, ...) { return r; } -int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_level, ...) { +int dlopen_verbose(void **dlp, const char *filename) { int r; + assert(dlp); + if (*dlp) return 0; /* Already loaded */ @@ -62,6 +64,22 @@ int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_l } log_debug("Loaded shared library '%s' via dlopen().", filename); + *dlp = TAKE_PTR(dl); + return 1; +} + +int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_level, ...) { + int r; + + assert(dlp); + + if (*dlp) + return 0; /* Already loaded */ + + _cleanup_(dlclosep) void *dl = NULL; + r = dlopen_verbose(&dl, filename); + if (r < 0) + return r; va_list ap; va_start(ap, log_level); diff --git a/src/basic/dlfcn-util.h b/src/basic/dlfcn-util.h index 367c47ddb8..9760b31da4 100644 --- a/src/basic/dlfcn-util.h +++ b/src/basic/dlfcn-util.h @@ -11,6 +11,7 @@ static inline void dlclosep(void **dlp) { safe_dlclose(*dlp); } +int dlopen_verbose(void **dlp, const char *filename); int dlsym_many_or_warn_sentinel(void *dl, int log_level, ...) _sentinel_; int dlopen_many_sym_or_warn_sentinel(void **dlp, const char *filename, int log_level, ...) _sentinel_; diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index b0f6387b03..0d4f51dc1e 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -53,6 +53,7 @@ static void *libtss2_esys_dl = NULL; static void *libtss2_rc_dl = NULL; static void *libtss2_mu_dl = NULL; +static void *libtss2_tcti_device_dl = NULL; static DLSYM_PROTOTYPE(Esys_Create) = NULL; static DLSYM_PROTOTYPE(Esys_CreateLoaded) = NULL; @@ -226,6 +227,20 @@ static int dlopen_tpm2_mu(void) { DLSYM_ARG(Tss2_MU_UINT32_Marshal)); } +static int dlopen_tpm2_tcti_device(void) { + /* The "device" TCTI is the most relevant one, let's also load it explicitly on dlopen_tpm2(), even + * if we don't resolve any symbols here. */ + + ELF_NOTE_DLOPEN("tpm", + "Support for TPM", + ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED, + "libtss2-tcti-device.so.0"); + + return dlopen_verbose( + &libtss2_tcti_device_dl, + "libtss2-tcti-device.so.0"); +} + #endif int dlopen_tpm2(void) { @@ -244,6 +259,10 @@ int dlopen_tpm2(void) { if (r < 0) return r; + r = dlopen_tpm2_tcti_device(); + if (r < 0) + return r; + return 0; #else return -EOPNOTSUPP; From 98a9844f3e012adfc92f9bc320628ad2fc54c3c8 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 12:27:55 +0100 Subject: [PATCH 05/15] tpm2-util: add tpm2_get_vendor_info() helper for getting TPM2 vendor info from the device --- src/shared/tpm2-util.c | 109 +++++++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 17 +++++++ 2 files changed, 126 insertions(+) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 0d4f51dc1e..64332fda32 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -43,6 +43,7 @@ #include "time-util.h" #include "tpm2-pcr.h" #include "tpm2-util.h" +#include "unaligned.h" #include "virt.h" #if HAVE_OPENSSL @@ -348,6 +349,114 @@ static int tpm2_get_capability( return more == TPM2_YES; } +static char *mangle_vendor_chars(char *c, size_t n) { + char *end = c; + assert(c || n == 0); + + /* Suppress all control characters, whitespace and non-ASCII bytes */ + for (char *x = c; x < c + n; x++) { + if (!IN_SET(*x, ' ', 0)) + end = x + 1; + + if ((unsigned char) *x <= (unsigned char) ' ' || + (unsigned char) *x >= 127) + *x = '_'; + } + + /* Drop trailing spaces and NUL bytes */ + *end = 0; + return c; +} + +int tpm2_get_vendor_info( + Tpm2Context *c, + Tpm2VendorInfo *ret) { + + int r; + + assert(c); + assert(ret); + + TPMU_CAPABILITIES capabilities = {}; + r = tpm2_get_capability( + c, + TPM2_CAP_TPM_PROPERTIES, + TPM2_PT_FAMILY_INDICATOR, + TPM2_PT_FIRMWARE_VERSION_2 - TPM2_PT_FAMILY_INDICATOR + 1, /* get all relevant fields at once */ + &capabilities); + if (r < 0) + return r; + + Tpm2VendorInfo info = {}; + + for (uint32_t i = 0; i < capabilities.tpmProperties.count; i++) { + TPMS_TAGGED_PROPERTY *p = capabilities.tpmProperties.tpmProperty + i; + + switch (p->property) { + + case TPM2_PT_FAMILY_INDICATOR: + unaligned_write_be32(info.family_indicator, p->value); + mangle_vendor_chars(info.family_indicator, sizeof(info.family_indicator)); + break; + + case TPM2_PT_LEVEL: + info.level = p->value; + break; + + case TPM2_PT_REVISION: + info.revision_major = p->value / 100; + info.revision_minor = p->value % 100; + break; + + case TPM2_PT_DAY_OF_YEAR: + info.day_of_year = p->value; + break; + + case TPM2_PT_YEAR: + info.year = p->value; + break; + + case TPM2_PT_MANUFACTURER: + unaligned_write_be32(info.manufacturer, p->value); + mangle_vendor_chars(info.manufacturer, sizeof(info.manufacturer)); + break; + + case TPM2_PT_VENDOR_STRING_1: + unaligned_write_be32(info.vendor_string+0, p->value); + break; + + case TPM2_PT_VENDOR_STRING_2: + unaligned_write_be32(info.vendor_string+4, p->value); + break; + + case TPM2_PT_VENDOR_STRING_3: + unaligned_write_be32(info.vendor_string+8, p->value); + break; + + case TPM2_PT_VENDOR_STRING_4: + unaligned_write_be32(info.vendor_string+12, p->value); + break; + + case TPM2_PT_VENDOR_TPM_TYPE: + info.vendor_tpm_type = p->value; + break; + + case TPM2_PT_FIRMWARE_VERSION_1: + info.firmware_version_major = p->value >> 16; + info.firmware_version_minor = p->value & 0xFFFFU; + break; + + case TPM2_PT_FIRMWARE_VERSION_2: + info.firmware_version2 = p->value; + break; + } + } + + mangle_vendor_chars(info.vendor_string, sizeof(info.vendor_string)); + *ret = TAKE_STRUCT(info); + return 0; +} + #define TPMA_CC_TO_TPM2_CC(cca) (((cca) & TPMA_CC_COMMANDINDEX_MASK) >> TPMA_CC_COMMANDINDEX_SHIFT) static int tpm2_cache_capabilities(Tpm2Context *c) { diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 282aaa1e80..cbdad19719 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -195,6 +195,23 @@ static inline int tpm2_digest_init(TPMI_ALG_HASH alg, TPM2B_DIGEST *digest) { return tpm2_digest_many(alg, digest, NULL, 0, false); } +typedef struct Tpm2VendorInfo { + uint32_t level; + uint32_t revision_major; + uint32_t revision_minor; + uint32_t day_of_year; + uint32_t year; + uint32_t vendor_tpm_type; + uint16_t firmware_version_major; + uint16_t firmware_version_minor; + uint32_t firmware_version2; + char family_indicator[4+1]; + char manufacturer[4+1]; + char vendor_string[4*4+1]; +} Tpm2VendorInfo; + +int tpm2_get_vendor_info(Tpm2Context *c, Tpm2VendorInfo *ret); + void tpm2_log_debug_tpml_pcr_selection(const TPML_PCR_SELECTION *l, const char *msg); void tpm2_log_debug_pcr_value(const Tpm2PCRValue *pcr_value, const char *msg); void tpm2_log_debug_buffer(const void *buffer, size_t size, const char *msg); From 205b70e3284da39c7fc19e5f8e597ae45a316cd2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 12:28:22 +0100 Subject: [PATCH 06/15] analyze: add "identify-tpm2" command that shows TPM2 chip information --- man/systemd-analyze.xml | 25 ++++++ shell-completion/bash/systemd-analyze | 2 +- src/analyze/analyze-has-tpm2.c | 122 ++++++++++++++++++++++++++ src/analyze/analyze-has-tpm2.h | 1 + src/analyze/analyze.c | 2 + 5 files changed, 151 insertions(+), 1 deletion(-) diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index e64f9be57e..6a02291666 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -195,6 +195,11 @@ OPTIONS has-tpm2 + + systemd-analyze + OPTIONS + identify-tpm2 + systemd-analyze OPTIONS @@ -1028,6 +1033,26 @@ default ignore - - + + <command>systemd-analyze identify-tpm2</command> + + Shows vendor information about the TPM 2.0 device discovered. + + + Example Output + + Family Indicator: 2.0 + Level: 0 + Revision: 1.59 +Specification Date: Mon 2023-01-09 + Manufacturer: STM + Vendor String: ST33KTPM2XSPI + Firmware Version: 9.258 + + + + + <command>systemd-analyze pcrs <optional><replaceable>PCR</replaceable>…</optional></command> diff --git a/shell-completion/bash/systemd-analyze b/shell-completion/bash/systemd-analyze index a863af7aff..b194c74b63 100644 --- a/shell-completion/bash/systemd-analyze +++ b/shell-completion/bash/systemd-analyze @@ -77,7 +77,7 @@ _systemd_analyze() { ) local -A VERBS=( - [STANDALONE]='time blame unit-files unit-paths exit-status compare-versions timestamp timespan pcrs nvpcrs srk has-tpm2 smbios11 chid image-policy' + [STANDALONE]='time blame unit-files unit-paths exit-status compare-versions timestamp timespan pcrs nvpcrs srk has-tpm2 identify-tpm2 smbios11 chid image-policy' [CRITICAL_CHAIN]='critical-chain' [DOT]='dot' [DUMP]='dump' diff --git a/src/analyze/analyze-has-tpm2.c b/src/analyze/analyze-has-tpm2.c index 3e13be9f16..a63c44ebc6 100644 --- a/src/analyze/analyze-has-tpm2.c +++ b/src/analyze/analyze-has-tpm2.c @@ -2,8 +2,130 @@ #include "analyze.h" #include "analyze-has-tpm2.h" +#include "format-table.h" +#include "log.h" +#include "string-util.h" +#include "time-util.h" #include "tpm2-util.h" int verb_has_tpm2(int argc, char **argv, void *userdata) { return verb_has_tpm2_generic(arg_quiet); } + +int verb_identify_tpm2(int argc, char **argv, void *userdata) { +#if HAVE_TPM2 + int r; + + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + r = tpm2_context_new_or_warn(/* device= */ NULL, &c); + if (r < 0) + return r; + + Tpm2VendorInfo info; + r = tpm2_get_vendor_info(c, &info); + if (r < 0) + return log_error_errno(r, "Failed to acquire TPM2 vendor information: %m"); + + _cleanup_(table_unrefp) Table *table = table_new_vertical(); + if (!table) + return log_oom(); + + if (!isempty(info.family_indicator)) { + r = table_add_many( + table, + TABLE_FIELD, "Family Indicator", + TABLE_STRING, info.family_indicator); + if (r < 0) + return table_log_add_error(r); + } + + _cleanup_free_ char *rv = NULL; + if (asprintf(&rv, "%" PRIu32 ".%" PRIu32, + info.revision_major, + info.revision_minor) < 0) + return log_oom(); + + r = table_add_many( + table, + TABLE_FIELD, "Level", + TABLE_UINT32, info.level, + TABLE_FIELD, "Revision", + TABLE_STRING, rv); + if (r < 0) + return table_log_add_error(r); + + if (info.year >= 1900) { + struct tm tm = { + .tm_year = info.year - 1900, + .tm_mon = 0, /* january */ + .tm_mday = info.day_of_year, /* timegm() will normalize this */ + }; + + usec_t ts; + r = mktime_or_timegm_usec(&tm, /* utc= */ true, &ts); + if (r < 0) + log_debug_errno(r, "Failed to convert the specification date, ignoring."); + else { + r = table_add_many( + table, + TABLE_FIELD, "Specification Date", + TABLE_TIMESTAMP_DATE, ts); + if (r < 0) + return table_log_add_error(r); + } + } + + if (!isempty(info.manufacturer)) { + r = table_add_many( + table, + TABLE_FIELD, "Manufacturer", + TABLE_STRING, info.manufacturer); + if (r < 0) + return table_log_add_error(r); + } + + if (!isempty(info.vendor_string)) { + r = table_add_many( + table, + TABLE_FIELD, "Vendor String", + TABLE_STRING, info.vendor_string); + if (r < 0) + return table_log_add_error(r); + } + + if (info.vendor_tpm_type != 0) { + r = table_add_many( + table, + TABLE_FIELD, "Vendor TPM Type", + TABLE_UINT32_HEX_0x, info.vendor_tpm_type); + if (r < 0) + return table_log_add_error(r); + } + + /* Show the first two 16bit words of the firmware version as major/minor */ + _cleanup_free_ char *fw = NULL; + if (asprintf(&fw, "%" PRIu16 ".%" PRIu16, + info.firmware_version_major, + info.firmware_version_minor) < 0) + return log_oom(); + + /* Show the second 32bit as a single value, if non-zero */ + if (info.firmware_version2 != 0 && strextendf(&fw, ".%" PRIu32, info.firmware_version2) < 0) + return log_oom(); + + r = table_add_many( + table, + TABLE_FIELD, "Firmware Version", + TABLE_STRING, fw); + if (r < 0) + return table_log_add_error(r); + + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ false); + if (r < 0) + return r; + + return EXIT_SUCCESS; +#else + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 support not enabled at build time."); +#endif +} diff --git a/src/analyze/analyze-has-tpm2.h b/src/analyze/analyze-has-tpm2.h index c7c639228d..b7d750090b 100644 --- a/src/analyze/analyze-has-tpm2.h +++ b/src/analyze/analyze-has-tpm2.h @@ -2,3 +2,4 @@ #pragma once int verb_has_tpm2(int argc, char *argv[], void *userdata); +int verb_identify_tpm2(int argc, char *argv[], void *userdata); diff --git a/src/analyze/analyze.c b/src/analyze/analyze.c index c69f03ec15..af456314db 100644 --- a/src/analyze/analyze.c +++ b/src/analyze/analyze.c @@ -258,6 +258,7 @@ static int help(int argc, char *argv[], void *userdata) { " dlopen-metadata FILE Parse and print ELF dlopen metadata\n" "\n%3$sTPM Operations:%4$s\n" " has-tpm2 Report whether TPM2 support is available\n" + " identify-tpm2 Show TPM2 vendor information\n" " pcrs [PCR...] Show TPM2 PCRs and their names\n" " nvpcrs [NVPCR...] Show additional TPM2 PCRs stored in NV indexes\n" " srk [>FILE] Write TPM2 SRK (to FILE)\n" @@ -810,6 +811,7 @@ static int run(int argc, char *argv[]) { { "fdstore", 2, VERB_ANY, 0, verb_fdstore }, { "image-policy", 2, 2, 0, verb_image_policy }, { "has-tpm2", VERB_ANY, 1, 0, verb_has_tpm2 }, + { "identify-tpm2", VERB_ANY, 1, 0, verb_identify_tpm2 }, { "pcrs", VERB_ANY, VERB_ANY, 0, verb_pcrs }, { "nvpcrs", VERB_ANY, VERB_ANY, 0, verb_nvpcrs }, { "srk", VERB_ANY, 1, 0, verb_srk }, From bafc13317825bfec1fe263b93c7e0b0bf0c5a250 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 12:28:45 +0100 Subject: [PATCH 07/15] tpm2-util,analyze: add helper for generating hwdb lookup key from TPM2 vendor data Our goal is to find TPM2 metadata in hwdb, hence let's compile a "modalias"-style string from the TPM2 metadata, we can use as hwdb lookup key. --- man/systemd-analyze.xml | 3 ++- src/analyze/analyze-has-tpm2.c | 11 ++++++++++ src/shared/tpm2-util.c | 37 ++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/man/systemd-analyze.xml b/man/systemd-analyze.xml index 6a02291666..f3dfd7479f 100644 --- a/man/systemd-analyze.xml +++ b/man/systemd-analyze.xml @@ -1047,7 +1047,8 @@ default ignore - - Specification Date: Mon 2023-01-09 Manufacturer: STM Vendor String: ST33KTPM2XSPI - Firmware Version: 9.258 + Firmware Version: 9.258 + Modalias String: fi2.0:lv0:rv1.59:sy2023:sd9:mfSTM:vsST33KTPM2XSPI:ty0:fw9.258.0: diff --git a/src/analyze/analyze-has-tpm2.c b/src/analyze/analyze-has-tpm2.c index a63c44ebc6..2e7890cea1 100644 --- a/src/analyze/analyze-has-tpm2.c +++ b/src/analyze/analyze-has-tpm2.c @@ -120,6 +120,17 @@ int verb_identify_tpm2(int argc, char **argv, void *userdata) { if (r < 0) return table_log_add_error(r); + _cleanup_free_ char *m = NULL; + if (tpm2_vendor_info_to_modalias(&info, &m) < 0) + return log_oom(); + + r = table_add_many( + table, + TABLE_FIELD, "Modalias String", + TABLE_STRING, m); + if (r < 0) + return table_log_add_error(r); + r = table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, /* show_header= */ false); if (r < 0) return r; diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 64332fda32..2705f3b794 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -349,6 +349,43 @@ static int tpm2_get_capability( return more == TPM2_YES; } +int tpm2_vendor_info_to_modalias(const Tpm2VendorInfo *info, char **ret) { + _cleanup_free_ char *m = NULL; + + assert(info); + assert(ret); + + /* Closely inspired by kernel modalias strings, this distills information from the TPM vendor data + * into a string suitable for matching hwdb */ + + if (asprintf(&m, + "fi%s:" + "lv%" PRIu32 ":" + "rv%" PRIu32 ".%" PRIu32 ":" + "sy%" PRIu32 ":" + "sd%" PRIu32 ":" + "mf%s:" + "vs%s:" + "ty%" PRIx32 ":" + "fw%" PRIu16 ".%" PRIu16 ".%" PRIu32 ":", + info->family_indicator, + info->level, + info->revision_major, + info->revision_minor, + info->year, + info->day_of_year, + info->manufacturer, + info->vendor_string, + info->vendor_tpm_type, + info->firmware_version_major, + info->firmware_version_minor, + info->firmware_version2) < 0) + return -ENOMEM; + + *ret = TAKE_PTR(m); + return 0; +} + static char *mangle_vendor_chars(char *c, size_t n) { char *end = c; assert(c || n == 0); diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index cbdad19719..9bccdd9712 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -210,6 +210,7 @@ typedef struct Tpm2VendorInfo { char vendor_string[4*4+1]; } Tpm2VendorInfo; +int tpm2_vendor_info_to_modalias(const Tpm2VendorInfo *info, char **ret); int tpm2_get_vendor_info(Tpm2Context *c, Tpm2VendorInfo *ret); void tpm2_log_debug_tpml_pcr_selection(const TPML_PCR_SELECTION *l, const char *msg); From dc75e542705d354d7ad67babb6d67c11c4920861 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 12:32:11 +0100 Subject: [PATCH 08/15] udev: add tpm2_id builtin We need to be able to look up tpm2 metadata from hwdb, hence add a way to synthesize a whdb lookup key from with udev rules. --- rules.d/60-tpm2-id.rules | 9 +++++ rules.d/meson.build | 1 + src/udev/meson.build | 4 +++ src/udev/udev-builtin-tpm2_id.c | 60 +++++++++++++++++++++++++++++++++ src/udev/udev-builtin.c | 3 ++ src/udev/udev-builtin.h | 3 ++ src/udev/udev-def.h | 3 ++ src/udev/udevd.c | 2 ++ 8 files changed, 85 insertions(+) create mode 100644 rules.d/60-tpm2-id.rules create mode 100644 src/udev/udev-builtin-tpm2_id.c diff --git a/rules.d/60-tpm2-id.rules b/rules.d/60-tpm2-id.rules new file mode 100644 index 0000000000..40ed0902bb --- /dev/null +++ b/rules.d/60-tpm2-id.rules @@ -0,0 +1,9 @@ +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="tpm2_id_end" +SUBSYSTEM!="tpmrm", GOTO="tpm2_id_end" +KERNEL!="tpmrm[0-9]*", GOTO="tpm2_id_end" + +IMPORT{program}="tpm2_id identify" + +LABEL="tpm2_id_end" diff --git a/rules.d/meson.build b/rules.d/meson.build index 839190658b..aa469ef7a7 100644 --- a/rules.d/meson.build +++ b/rules.d/meson.build @@ -23,6 +23,7 @@ rules = [ '60-persistent-v4l.rules', '60-sensor.rules', '60-serial.rules', + '60-tpm2-id.rules', '65-integration.rules', '70-camera.rules', '70-joystick.rules', diff --git a/src/udev/meson.build b/src/udev/meson.build index 7df5b3de85..700a8cc8d5 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -59,6 +59,10 @@ if conf.get('HAVE_ACL') == 1 udevadm_extract_sources += files('udev-builtin-uaccess.c') endif +if conf.get('HAVE_TPM2') == 1 + udevadm_extract_sources += files('udev-builtin-tpm2_id.c') +endif + ############################################################ generate_keyboard_keys_list = files('generate-keyboard-keys-list.sh') diff --git a/src/udev/udev-builtin-tpm2_id.c b/src/udev/udev-builtin-tpm2_id.c new file mode 100644 index 0000000000..6edf618e11 --- /dev/null +++ b/src/udev/udev-builtin-tpm2_id.c @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "device-util.h" +#include "string-util.h" +#include "tpm2-util.h" +#include "udev-builtin.h" + +static int builtin_tpm2_id(UdevEvent *event, int argc, char *argv[]) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + int r; + + if (argc != 2 || !streq(argv[1], "identify")) + return log_device_error_errno( + dev, SYNTHETIC_ERRNO(EINVAL), "%s: expected: identify", argv[0]); + + const char *dn; + r = sd_device_get_devname(dev, &dn); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get device node for device: %m"); + + _cleanup_(tpm2_context_unrefp) Tpm2Context *c = NULL; + r = tpm2_context_new(dn, &c); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to open device node '%s': %m", dn); + + Tpm2VendorInfo info; + r = tpm2_get_vendor_info(c, &info); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to acquire TPM2 vendor information: %m"); + + if (!isempty(info.manufacturer)) { + r = udev_builtin_add_property(event, "ID_TPM2_MANUFACTURER", info.manufacturer); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to set field: %m"); + } + + if (!isempty(info.vendor_string)) { + r = udev_builtin_add_property(event, "ID_TPM2_VENDOR_STRING", info.vendor_string); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to set field: %m"); + } + + _cleanup_free_ char *m = NULL; + r = tpm2_vendor_info_to_modalias(&info, &m); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to get modalias string for TPM2 device: %m"); + + r = udev_builtin_add_property(event, "ID_TPM2_MODALIAS", m); + if (r < 0) + return log_device_error_errno(dev, r, "Failed to set field: %m"); + + return 0; +} + +const UdevBuiltin udev_builtin_tpm2_id = { + .name = "tpm2_id", + .cmd = builtin_tpm2_id, + .help = "Identify TPM2 chips", + .run_once = true, +}; diff --git a/src/udev/udev-builtin.c b/src/udev/udev-builtin.c index 5f559f5c9a..121694e688 100644 --- a/src/udev/udev-builtin.c +++ b/src/udev/udev-builtin.c @@ -29,6 +29,9 @@ static const UdevBuiltin *const builtins[_UDEV_BUILTIN_MAX] = { [UDEV_BUILTIN_NET_ID] = &udev_builtin_net_id, [UDEV_BUILTIN_NET_LINK] = &udev_builtin_net_setup_link, [UDEV_BUILTIN_PATH_ID] = &udev_builtin_path_id, +#if HAVE_TPM2 + [UDEV_BUILTIN_TPM2_ID] = &udev_builtin_tpm2_id, +#endif #if HAVE_ACL [UDEV_BUILTIN_UACCESS] = &udev_builtin_uaccess, #endif diff --git a/src/udev/udev-builtin.h b/src/udev/udev-builtin.h index 3e830c77ea..504c4108c8 100644 --- a/src/udev/udev-builtin.h +++ b/src/udev/udev-builtin.h @@ -43,6 +43,9 @@ extern const UdevBuiltin udev_builtin_net_driver; extern const UdevBuiltin udev_builtin_net_id; extern const UdevBuiltin udev_builtin_net_setup_link; extern const UdevBuiltin udev_builtin_path_id; +#if HAVE_TPM2 +extern const UdevBuiltin udev_builtin_tpm2_id; +#endif #if HAVE_ACL extern const UdevBuiltin udev_builtin_uaccess; #endif diff --git a/src/udev/udev-def.h b/src/udev/udev-def.h index 836dd70454..d5e02e50b6 100644 --- a/src/udev/udev-def.h +++ b/src/udev/udev-def.h @@ -51,6 +51,9 @@ typedef enum UdevBuiltinCommand { UDEV_BUILTIN_NET_ID, UDEV_BUILTIN_NET_LINK, UDEV_BUILTIN_PATH_ID, +#if HAVE_TPM2 + UDEV_BUILTIN_TPM2_ID, +#endif #if HAVE_ACL UDEV_BUILTIN_UACCESS, #endif diff --git a/src/udev/udevd.c b/src/udev/udevd.c index ab3106334b..840febbd42 100644 --- a/src/udev/udevd.c +++ b/src/udev/udevd.c @@ -19,6 +19,7 @@ #include "process-util.h" #include "rlimit-util.h" #include "terminal-util.h" +#include "tpm2-util.h" #include "udev-config.h" #include "udev-manager.h" #include "udevd.h" @@ -61,6 +62,7 @@ int run_udevd(int argc, char *argv[]) { (void) dlopen_libblkid(); (void) dlopen_libkmod(); (void) dlopen_libmount(); + (void) dlopen_tpm2(); if (arg_daemonize) { pid_t pid; From f2eed3fa25e8c38b7a90d6ab3d22ee90e3569271 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 14:06:44 +0100 Subject: [PATCH 09/15] hwdb: introduce hwdb for tpm2 devices This hwdb is can carry hw quirks and similar for us, in particular tell us if nvpcrs have a chance of working. --- hwdb.d/60-tpm2.hwdb | 14 ++++++++++++++ hwdb.d/meson.build | 1 + hwdb.d/parse_hwdb.py | 2 ++ rules.d/60-tpm2-id.rules | 1 + 4 files changed, 18 insertions(+) create mode 100644 hwdb.d/60-tpm2.hwdb diff --git a/hwdb.d/60-tpm2.hwdb b/hwdb.d/60-tpm2.hwdb new file mode 100644 index 0000000000..2772bf3cde --- /dev/null +++ b/hwdb.d/60-tpm2.hwdb @@ -0,0 +1,14 @@ +# This file is part of systemd. +# +# Use "systemd-analyze identify-tpm2" to generate the modalias string for your +# hardware. Don't forget to prefix it with "tpm:" for inclusion in a match here. +# +# Currently, the only relevant property to set here is TPM2_BROKEN_NVPCR=1, +# which should be set on TPMs where NvPCRs don't work. Specifically, because +# on some hardware the combination of TPMA_NV_ORDERLY + TPM2_NT_EXTEND cause +# NV_Extend() operations to time out. For details, see: +# https://github.com/systemd/systemd/issues/40485 + +# ST33TPHF2ESPI Firmware 73.4 +tpm2:*:mfSTM:*:fw73.4.*: + TPM2_BROKEN_NVPCR=1 diff --git a/hwdb.d/meson.build b/hwdb.d/meson.build index 36a9937a60..9ba73b21d6 100644 --- a/hwdb.d/meson.build +++ b/hwdb.d/meson.build @@ -26,6 +26,7 @@ hwdb_files_test = files( '60-keyboard.hwdb', '60-seat.hwdb', '60-sensor.hwdb', + '60-tpm2.hwdb', '70-analyzers.hwdb', '70-av-production.hwdb', '70-cameras.hwdb', diff --git a/hwdb.d/parse_hwdb.py b/hwdb.d/parse_hwdb.py index 7efa61ad4b..e98510839b 100755 --- a/hwdb.d/parse_hwdb.py +++ b/hwdb.d/parse_hwdb.py @@ -105,6 +105,7 @@ GENERAL_MATCHES = {'acpi', 'OUI', 'pci', 'sdio', + 'tpm2', 'usb', 'vmbus', } @@ -213,6 +214,7 @@ def property_grammar(): ('ID_SYSFS_ATTRIBUTE_MODEL', name_literal), ('ID_NET_NAME_FROM_DATABASE', name_literal), ('ID_NET_NAME_INCLUDE_DOMAIN', zero_one), + ('TPM2_BROKEN_NVPCR', zero_one), ) fixed_props = [Literal(name)('NAME') - Suppress('=') - val('VALUE') for name, val in props] diff --git a/rules.d/60-tpm2-id.rules b/rules.d/60-tpm2-id.rules index 40ed0902bb..1e08f3b8e5 100644 --- a/rules.d/60-tpm2-id.rules +++ b/rules.d/60-tpm2-id.rules @@ -5,5 +5,6 @@ SUBSYSTEM!="tpmrm", GOTO="tpm2_id_end" KERNEL!="tpmrm[0-9]*", GOTO="tpm2_id_end" IMPORT{program}="tpm2_id identify" +ENV{ID_TPM2_MODALIAS}!="", IMPORT{builtin}="hwdb 'tpm2:$env{ID_TPM2_MODALIAS}'" LABEL="tpm2_id_end" From 9c40895f133309b14fd578c37b89b334c61e2c74 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 15:53:09 +0100 Subject: [PATCH 10/15] tpm2-util: remove strjoina() usage on user-controlled data --- src/shared/tpm2-util.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 2705f3b794..058983c965 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -881,7 +881,8 @@ int tpm2_context_new(const char *device, Tpm2Context **ret_context) { } if (device) { - const char *param, *driver, *fn; + _cleanup_free_ char *_driver = NULL; + const char *param, *driver; const TSS2_TCTI_INFO* info; TSS2_TCTI_INFO_FUNC func; size_t sz = 0; @@ -889,7 +890,9 @@ int tpm2_context_new(const char *device, Tpm2Context **ret_context) { param = strchr(device, ':'); if (param) { /* Syntax #1: Pair of driver string and arbitrary parameter */ - driver = strndupa_safe(device, param - device); + driver = _driver = strndup(device, param - device); + if (!driver) + return log_oom_debug(); if (isempty(driver)) return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "TPM2 driver name is empty, refusing."); @@ -903,7 +906,9 @@ int tpm2_context_new(const char *device, Tpm2Context **ret_context) { log_debug("Using TPM2 TCTI driver '%s' with device '%s'.", driver, param); - fn = strjoina("libtss2-tcti-", driver, ".so.0"); + _cleanup_free_ char *fn = strjoin("libtss2-tcti-", driver, ".so.0"); + if (!fn) + return log_oom_debug(); /* Better safe than sorry, let's refuse strings that cannot possibly be valid driver early, before going to disk. */ if (!filename_is_valid(fn)) From a420348cc346e74d0326dda3a37f54ca669918d6 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 15:59:40 +0100 Subject: [PATCH 11/15] tpm2-util: check udev db to determine if NvPCRs are going to work Fixes: #40485 --- catalog/systemd.catalog.in | 9 ++++++ src/shared/tpm2-util.c | 59 +++++++++++++++++++++++++++++++++++++ src/shared/tpm2-util.h | 2 ++ src/systemd/sd-messages.h | 3 ++ src/tpm2-setup/tpm2-setup.c | 6 ++++ 5 files changed, 79 insertions(+) diff --git a/catalog/systemd.catalog.in b/catalog/systemd.catalog.in index 6f6ca9c4ea..68ef28d197 100644 --- a/catalog/systemd.catalog.in +++ b/catalog/systemd.catalog.in @@ -1015,3 +1015,12 @@ non-volatile indexes (NV Indexes), could not be initialized. This typically means the persistent NV index memory available on the TPM is taken up by other resources, or is extremely limited. A TPM reset might help recovering space (but will invalidate all TPM bound keys and resources). + +-- 8f07a5b814ca4762b89fcc3082e48aed +Subject: TPM NV index backed PCRs not supported on the local TPM +Defined-By: systemd +Support: %SUPPORT_URL% + +The Trusted Platform Module's (TPM) NV index support is too limited to properly +implement NV index backed additional PCRs. NvPCRs will not be allocated or +initialized, and will not be available on the system. diff --git a/src/shared/tpm2-util.c b/src/shared/tpm2-util.c index 058983c965..9e05c847ed 100644 --- a/src/shared/tpm2-util.c +++ b/src/shared/tpm2-util.c @@ -3,6 +3,8 @@ #include #include +#include "sd-device.h" + #include "alloc-util.h" #include "ansi-color.h" #include "bitfield.h" @@ -11,6 +13,8 @@ #include "constants.h" #include "creds-util.h" #include "cryptsetup-util.h" +#include "device-private.h" +#include "device-util.h" #include "dirent-util.h" #include "dlfcn-util.h" #include "efi-api.h" @@ -836,6 +840,9 @@ static Tpm2Context *tpm2_context_free(Tpm2Context *c) { c->capability_commands = mfree(c->capability_commands); c->capability_ecc_curves = mfree(c->capability_ecc_curves); + c->tcti_driver = mfree(c->tcti_driver); + c->tcti_param = mfree(c->tcti_param); + return mfree(c); } @@ -948,6 +955,14 @@ int tpm2_context_new(const char *device, Tpm2Context **ret_context) { if (rc != TPM2_RC_SUCCESS) return log_debug_errno(SYNTHETIC_ERRNO(ENOTRECOVERABLE), "Failed to initialize TCTI context: %s", sym_Tss2_RC_Decode(rc)); + + context->tcti_driver = strdup(driver); + if (!context->tcti_driver) + return log_oom_debug(); + + context->tcti_param = strdup(param); + if (!context->tcti_param) + return log_oom_debug(); } rc = sym_Esys_Initialize(&context->esys_context, context->tcti_context, NULL); @@ -7479,6 +7494,44 @@ int tpm2_nvpcr_acquire_anchor_secret(struct iovec *ret, bool sync_secondary) { #endif } +#if HAVE_OPENSSL +static int tpm2_context_can_nvindex(Tpm2Context *c) { + int r; + + assert(c); + + if (!streq_ptr(c->tcti_driver, "device")) { + log_debug("Not checking udev database, because not using 'device' TCTI."); + return 1; + } + + if (!c->tcti_param) { + log_debug("No device specified, not checking udev database."); + return 1; + } + + _cleanup_(sd_device_unrefp) sd_device *d = NULL; + r = sd_device_new_from_devname(&d, c->tcti_param); + if (r < 0) + return log_debug_errno(r, "Failed to acquire udev entry for device '%s': %m", c->tcti_param); + + r = device_get_property_bool(d, "TPM2_BROKEN_NVPCR"); + if (r == -ENOENT) { + log_device_debug_errno(d, r, "No TPM2_BROKEN_NVPCR property for '%s', assuming NvPCRs work.", c->tcti_param); + return 1; + } + if (r < 0) + return log_device_debug_errno(d, r, "Failed to query TPM2_BROKEN_NVPCR for '%s': %m", c->tcti_param); + if (r > 0) { + log_device_debug(d, "TPM2_BROKEN_NVPCR property for '%s' explicitly set, NvPCRs do not work.", c->tcti_param); + return 0; + } + + log_device_debug(d, "TPM2_BROKEN_NVPCR property for '%s' explicitly set to false, hence NvPCRs work.", c->tcti_param); + return 1; +} +#endif + int tpm2_nvpcr_initialize( Tpm2Context *c, const Tpm2Handle *session, @@ -7492,6 +7545,12 @@ int tpm2_nvpcr_initialize( assert(c); assert(name); + r = tpm2_context_can_nvindex(c); + if (r < 0) + return r; + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "TPM2 device does not support NvPCRs, not initializing."); + _cleanup_(nvpcr_data_done) NvPCRData p = {}; r = nvpcr_data_load(name, &p); if (r < 0) diff --git a/src/shared/tpm2-util.h b/src/shared/tpm2-util.h index 9bccdd9712..51670e8b06 100644 --- a/src/shared/tpm2-util.h +++ b/src/shared/tpm2-util.h @@ -53,6 +53,8 @@ typedef struct Tpm2Context { void *tcti_dl; TSS2_TCTI_CONTEXT *tcti_context; ESYS_CONTEXT *esys_context; + char *tcti_driver; + char *tcti_param; /* Some selected cached capabilities of the TPM */ TPMS_ALG_PROPERTY *capability_algorithms; diff --git a/src/systemd/sd-messages.h b/src/systemd/sd-messages.h index 865f7247ac..6aabe88196 100644 --- a/src/systemd/sd-messages.h +++ b/src/systemd/sd-messages.h @@ -309,6 +309,9 @@ _SD_BEGIN_DECLARATIONS; #define SD_MESSAGE_TPM_NVINDEX_EXHAUSTED SD_ID128_MAKE(ab,98,4e,a0,08,96,4f,b8,8d,6e,38,9f,b5,13,fb,94) #define SD_MESSAGE_TPM_NVINDEX_EXHAUSTED_STR SD_ID128_MAKE_STR(ab,98,4e,a0,08,96,4f,b8,8d,6e,38,9f,b5,13,fb,94) +#define SD_MESSAGE_TPM_NVPCR_UNSUPPORTED SD_ID128_MAKE(8f,07,a5,b8,14,ca,47,62,b8,9f,cc,30,82,e4,8a,ed) +#define SD_MESSAGE_TPM_NVPCR_UNSUPPORTED_STR SD_ID128_MAKE_STR(8f,07,a5,b8,14,ca,47,62,b8,9f,cc,30,82,e4,8a,ed) + _SD_END_DECLARATIONS; #endif diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index a4d323a70c..cfb118f6ac 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -422,6 +422,12 @@ static int setup_nvpcr_one( r = tpm2_nvpcr_initialize(c->tpm2_context, /* session= */ NULL, name, &c->anchor_secret); } + if (r == -EOPNOTSUPP) { + c->n_failed++; + return log_struct_errno(LOG_ERR, r, + LOG_MESSAGE("The TPM does not correctly support NV indexes in NT_EXTEND mode, unable to allocate NvPCR '%s': %m", name), + LOG_MESSAGE_ID(SD_MESSAGE_TPM_NVPCR_UNSUPPORTED_STR)); + } if (r == -ENOSPC) { c->n_failed++; return log_struct_errno(LOG_ERR, r, From 9c8a2e53d337108a728ea0e54525bfec5db5c05e Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:20:13 +0100 Subject: [PATCH 12/15] test: add superficial testcase for tpm2 identification --- test/units/TEST-70-TPM2.nvpcr.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/units/TEST-70-TPM2.nvpcr.sh b/test/units/TEST-70-TPM2.nvpcr.sh index c5a6d1c213..29319e601a 100755 --- a/test/units/TEST-70-TPM2.nvpcr.sh +++ b/test/units/TEST-70-TPM2.nvpcr.sh @@ -103,3 +103,6 @@ systemd-dissect --image-policy='root=signed:=absent+unused' --mtree /var/tmp/nvp set +o pipefail diff /tmp/nvpcr/log-before /run/log/systemd/tpm2-measure.log | grep -F '"content":{"nvIndexName":"verity","string":"verity:' + +systemd-analyze identify-tpm2 +udevadm test-builtin 'tpm2_id identify' /dev/tpmrm0 From 3dc670901ee584cb61972415df2aaf8c7eed0ac3 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:50:49 +0100 Subject: [PATCH 13/15] tpm2-setup: use symbolic exit code 76 is the bsd exit code EX_PROTOCOL, which is kinda fitting here. Let#s hence use the symbolic exit code here. --- src/tpm2-setup/tpm2-setup.c | 5 +++-- units/systemd-tpm2-setup-early.service.in | 4 ++-- units/systemd-tpm2-setup.service.in | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index cfb118f6ac..3eed095d22 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -2,6 +2,7 @@ #include #include +#include #include "sd-messages.h" @@ -291,8 +292,8 @@ static int setup_srk(void) { log_struct_errno(LOG_INFO, r, LOG_MESSAGE("Insufficient permissions to access TPM, not generating SRK."), LOG_MESSAGE_ID(SD_MESSAGE_SRK_ENROLLMENT_NEEDS_AUTHORIZATION_STR)); - return 76; /* Special return value which means "Insufficient permissions to access TPM, - * cannot generate SRK". This isn't really an error when called at boot. */; + return EX_PROTOCOL; /* Special return value which means "Insufficient permissions to access TPM, + * cannot generate SRK". This isn't really an error when called at boot. */; } if (r < 0) return r; diff --git a/units/systemd-tpm2-setup-early.service.in b/units/systemd-tpm2-setup-early.service.in index 7fdb99b53f..bd69c9de7a 100644 --- a/units/systemd-tpm2-setup-early.service.in +++ b/units/systemd-tpm2-setup-early.service.in @@ -22,5 +22,5 @@ Type=oneshot RemainAfterExit=yes ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup --early=yes --graceful -# The tool returns 76 if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. -SuccessExitStatus=76 +# The tool returns EX_PROTOCOL if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. +SuccessExitStatus=PROTOCOL diff --git a/units/systemd-tpm2-setup.service.in b/units/systemd-tpm2-setup.service.in index 34404a24cb..a81b028214 100644 --- a/units/systemd-tpm2-setup.service.in +++ b/units/systemd-tpm2-setup.service.in @@ -23,5 +23,5 @@ Type=oneshot RemainAfterExit=yes ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup --graceful -# The tool returns 76 if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. -SuccessExitStatus=76 +# The tool returns EX_PROTOCOL if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. +SuccessExitStatus=PROTOCOL From 64fd94faa0c73c3f64e05674d106893dc209c256 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:51:38 +0100 Subject: [PATCH 14/15] tpm2-setup: don't fail service on two more types of failures Let's bubble up failures all the way until they reach the services, but then let's carefully gracefully handle some of them, that are about issues not immediately actionable to the admin, even if they are potentially quite problematic. --- src/tpm2-setup/tpm2-setup.c | 24 ++++++++++++++++------- units/systemd-tpm2-setup-early.service.in | 4 ++++ units/systemd-tpm2-setup.service.in | 4 ++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/tpm2-setup/tpm2-setup.c b/src/tpm2-setup/tpm2-setup.c index 3eed095d22..d243f199e9 100644 --- a/src/tpm2-setup/tpm2-setup.c +++ b/src/tpm2-setup/tpm2-setup.c @@ -453,7 +453,7 @@ static int setup_nvpcr_one( static int setup_nvpcr(void) { _cleanup_(setup_nvpcr_context_done) SetupNvPCRContext c = {}; - int r = 0; + int r; _cleanup_strv_free_ char **l = NULL; r = conf_files_list_nulstr( @@ -478,13 +478,11 @@ static int setup_nvpcr(void) { RET_GATHER(ret, setup_nvpcr_one(&c, *i)); } - if (c.n_already > 0 && c.n_anchored == 0 && !arg_early) { + if (c.n_already > 0 && c.n_anchored == 0 && !arg_early) /* If we didn't anchor anything right now, but we anchored something earlier, then it might * have happened in the initrd, and thus the anchor ID was not committed to /var/ or the ESP * 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.n_failed > 0) log_warning("%zu NvPCRs failed to initialize, proceeding anyway.", c.n_failed); @@ -499,6 +497,13 @@ static int setup_nvpcr(void) { else if (c.n_failed == 0) log_debug("No NvPCRs defined, nothing initialized."); + /* Turn some errors into recognizable ones, which we can catch with + * SuccessExitStatus= in the service unit file. */ + if (ret == -EOPNOTSUPP) + return EX_UNAVAILABLE; /* e.g. no NvPCR support in TPM */ + if (ret == -ENOSPC) + return EX_CANTCREAT; /* NV index space on TPM exhausted */ + return ret; } @@ -518,10 +523,15 @@ static int run(int argc, char *argv[]) { umask(0022); + /* Execute both jobs, and then return unlisted errors preferably, and listed errors + * (i.e. EX_UNAVAILABLE, EX_CANTCREAT, EX_PROTOCOL) otherwise. */ r = setup_srk(); - RET_GATHER(r, setup_nvpcr()); - - return r; + int k = setup_nvpcr(); + if (r < 0) + return r; + if (k < 0) + return k; + return r != EXIT_SUCCESS ? r : k; } DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/units/systemd-tpm2-setup-early.service.in b/units/systemd-tpm2-setup-early.service.in index bd69c9de7a..ce1ee94cc4 100644 --- a/units/systemd-tpm2-setup-early.service.in +++ b/units/systemd-tpm2-setup-early.service.in @@ -24,3 +24,7 @@ ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup --early=yes --graceful # The tool returns EX_PROTOCOL if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. SuccessExitStatus=PROTOCOL +# The tool returns EX_UNAVAILABLE if some key functionality (e.g. NvPCR) support is not available in the TPM device. +SuccessExitStatus=UNAVAILABLE +# The tool returns EX_CANTCREAT if the NV index space is exhausted. +SuccessExitStatus=CANTCREAT diff --git a/units/systemd-tpm2-setup.service.in b/units/systemd-tpm2-setup.service.in index a81b028214..dff516832d 100644 --- a/units/systemd-tpm2-setup.service.in +++ b/units/systemd-tpm2-setup.service.in @@ -25,3 +25,7 @@ ExecStart={{LIBEXECDIR}}/systemd-tpm2-setup --graceful # The tool returns EX_PROTOCOL if the TPM cannot be accessed due to an authorization failure and we can't generate an SRK. SuccessExitStatus=PROTOCOL +# The tool returns EX_UNAVAILABLE if some key functionality (e.g. NvPCR) support is not available in the TPM device. +SuccessExitStatus=UNAVAILABLE +# The tool returns EX_CANTCREAT if the NV index space is exhausted. +SuccessExitStatus=CANTCREAT From 378b2f0d4a48c8f15f49d2a3936d56dd4e296b99 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Tue, 3 Mar 2026 21:24:51 +0100 Subject: [PATCH 15/15] update NEWS --- NEWS | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/NEWS b/NEWS index 82b30951d2..792aee4761 100644 --- a/NEWS +++ b/NEWS @@ -231,6 +231,11 @@ CHANGES WITH 260 in spe: ID_INTEGRATION= because it was never used and the new variable covers the idea that variable was intended for better. + * A new udev builtin "tpm2_id" is now available which will extract + vendor/model identification from connected TPM2 devices as they are + probed. This is then used to import data from the udev database, + possibly containing quirk and other information about specific TPMs. + Changes in systemd-networkd: * MultiPathRoute= option now supports interface-bound ECMP routes. @@ -469,6 +474,9 @@ CHANGES WITH 260 in spe: system components to synchronously hook into the OOM killing logic, by registering a Varlink socket in a special directory. + * systemd-analyze learnt a new verb "identify-tpm2" which shows + vendor/model information extracted from the system's TPM. + Changes in units: * runlevel[0-6].target units that were removed in v258 have been