tpm2-util: keep the measurement log's torn-write marker intact

The userspace measurement log carries a sticky-bit marker while a writer
is between updating a measurement register and appending the matching
record, so that a writer dying in between leaves the log detectably
incomplete.

However, the next successful writer used to clear the marker again after
appending its own record, erasing the evidence that an earlier writer
had died and the log is still missing a record. Keep the marker set in
that case instead; the new record is appended and synced regardless. The
marker likewise stays set if the measurement itself fails, so that any
non-clean completion remains flagged: a spurious flag on a failed but
harmless measurement is preferable to erasing evidence of a real gap,
and PCR replay stays authoritative either way.

Finally, warn when systemd-pcrlock loads a marked log, so the resulting
PCR validation failures come with a hint at their cause.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
This commit is contained in:
Paul Meyer
2026-07-16 01:59:38 +09:00
committed by Yu Watanabe
parent cb7b533b92
commit afff448cf4
2 changed files with 29 additions and 11 deletions
+8
View File
@@ -1179,6 +1179,7 @@ static int event_log_load_userspace(EventLog *el) {
_cleanup_free_ char *b = NULL;
bool beginning = true;
const char *path;
struct stat st;
size_t bn = 0;
int r;
@@ -1197,6 +1198,13 @@ static int event_log_load_userspace(EventLog *el) {
if (flock(fileno(f), LOCK_SH) < 0)
return log_error_errno(errno, "Failed to lock userspace TPM measurement log file: %m");
/* The sticky bit marks the log as incomplete: a writer died between updating a PCR and appending
* the matching record. Load the log anyway, the affected PCRs will simply fail validation. */
if (fstat(fileno(f), &st) < 0)
log_warning_errno(errno, "Failed to stat userspace TPM measurement log file, cannot determine whether it is complete, ignoring: %m");
else if (FLAGS_SET(st.st_mode, S_ISVTX))
log_warning("Userspace TPM measurement log file is marked as incomplete, PCR validation may fail.");
for (;;) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
EventLogRecord *record;
+21 -11
View File
@@ -7778,8 +7778,9 @@ static int tpm2_userspace_log_dirty(int fd) {
/* We set the sticky bit when we are about to append to the log file. We'll unset it afterwards
* again. If we manage to take a lock on a file that has it set we know we didn't write it fully and
* it is corrupted. Ideally we'd like to use user xattrs for this, but unfortunately tmpfs (which is
* our assumed backend fs) doesn't know user xattrs. */
* it is corrupted. We return -ESTALE then; callers shall not reset the marker when they are done,
* so that the incompleteness remains detectable. Ideally we'd like to use user xattrs for this, but
* unfortunately tmpfs (which is our assumed backend fs) doesn't know user xattrs. */
if (fstat(fd, &st) < 0)
return log_debug_errno(errno, "Failed to fstat TPM log file, ignoring: %m");
@@ -7793,7 +7794,7 @@ static int tpm2_userspace_log_dirty(int fd) {
return 0;
}
static int tpm2_userspace_log_clean(int fd) {
static int tpm2_userspace_log_clean(int fd, bool reset_marker) {
int r;
if (fd < 0) /* Apparently tpm2_local_log_open() failed earlier, let's not complain again */
@@ -7802,6 +7803,12 @@ static int tpm2_userspace_log_clean(int fd) {
if (fsync(fd) < 0)
return log_debug_errno(errno, "Failed to sync JSON data: %m");
/* If the dirty marker was already set when we acquired the log, an earlier writer died before
* writing its record, i.e. the log is missing a record. Keep the marker then, so that the
* incompleteness remains detectable. */
if (!reset_marker)
return 0;
/* Unset S_ISVTX again */
if (fchmod(fd, 0600) < 0)
return log_debug_errno(errno, "Failed to chmod() TPM log file, ignoring: %m");
@@ -7820,7 +7827,8 @@ static int tpm2_userspace_log(
const char *nv_index_name,
const TPML_DIGEST_VALUES *values,
Tpm2UserspaceEventType event_type,
const char *description) {
const char *description,
bool reset_marker) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL, *array = NULL;
_cleanup_free_ char *f = NULL;
@@ -7909,7 +7917,7 @@ static int tpm2_userspace_log(
if (r < 0)
return log_debug_errno(r, "Failed to write JSON data to log: %m");
r = tpm2_userspace_log_clean(fd);
r = tpm2_userspace_log_clean(fd, reset_marker);
if (r < 0)
return r;
@@ -7986,7 +7994,7 @@ int tpm2_pcr_extend_bytes(
* and our measurement and change either */
log_fd = tpm2_userspace_log_open();
(void) tpm2_userspace_log_dirty(log_fd);
bool reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
rc = sym_Esys_PCR_Extend(
c->esys_context,
ESYS_TR_PCR0 + pcr_index,
@@ -8009,7 +8017,8 @@ int tpm2_pcr_extend_bytes(
/* nv_index_name= */ NULL,
&values,
event_type,
description);
description,
reset_marker);
return 0;
#else /* HAVE_OPENSSL */
@@ -8179,7 +8188,7 @@ static int nvpcr_extend_bytes(
log_debug("Successfully acquired handle to existing NV index 0x%" PRIx32 ".", p.nv_index);
(void) tpm2_userspace_log_dirty(log_fd);
bool reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
r = tpm2_extend_nvpcr_nv_index(
c,
@@ -8203,7 +8212,8 @@ static int nvpcr_extend_bytes(
name,
&digest_values,
event_type,
description);
description,
reset_marker);
return 0;
#else /* HAVE_OPENSSL */
@@ -8739,7 +8749,7 @@ int tpm2_nvpcr_initialize(
log_debug("Successfully acquired handle to NV index 0x%" PRIx32 ".", p.nv_index);
tpm2_userspace_log_dirty(log_fd);
bool reset_marker = tpm2_userspace_log_dirty(log_fd) >= 0;
rc = sym_Esys_NV_Extend(
c->esys_context,
/* authHandle= */ nv_handle->esys_handle,
@@ -8777,7 +8787,7 @@ int tpm2_nvpcr_initialize(
if (r < 0)
return log_debug_errno(r, "Failed to write anchor file: %m");
tpm2_userspace_log_clean(log_fd);
(void) tpm2_userspace_log_clean(log_fd, reset_marker);
log_fd = safe_close(log_fd);
/* Now also measure the initialization into PCR 9, so that there's a trace of it in regular PCRs. You