]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tpm2: re-manufacture software TPM when state dir is incomplete 42722/head
authorPaul Meyer <katexochen0@gmail.com>
Tue, 23 Jun 2026 14:07:34 +0000 (16:07 +0200)
committerPaul Meyer <katexochen0@gmail.com>
Thu, 25 Jun 2026 11:50:24 +0000 (13:50 +0200)
setup_swtpm() decided whether a software TPM had already been
manufactured by checking whether the state directory was empty. But
manufacture_swtpm() writes swtpm's config files before forking
swtpm_setup, so an interrupted manufacture leaves the directory
non-empty yet without a usable TPM. The next boot then mistook it for a
complete TPM and started swtpm against a broken state directory.

Keying off a swtpm state file like tpm2-00.permall is no better, as
swtpm_setup gives no guarantee any single one is written atomically or
last. Instead, have manufacture_swtpm() write a marker (.manufactured)
as its very last step, once swtpm_setup has exited successfully, and
gate on it: re-manufacture when it is missing in the initrd, and refuse
rather than start a broken TPM outside it.

Signed-off-by: Paul Meyer <katexochen0@gmail.com>
src/shared/swtpm-util.c
src/shared/swtpm-util.h
src/tpm2-setup/tpm2-swtpm.c
test/units/TEST-92-TPM2-SWTPM.sh

index 8ad221a996e07e47e1ed9a7563efea8307c9169f..1fa0ad37255771ca5e0190190387be3662069008 100644 (file)
@@ -1,5 +1,6 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 
+#include <fcntl.h>
 #include <unistd.h>
 
 #include "sd-json.h"
@@ -8,6 +9,7 @@
 #include "escape.h"
 #include "fd-util.h"
 #include "fileio.h"
+#include "fs-util.h"
 #include "json-util.h"
 #include "log.h"
 #include "memfd-util.h"
@@ -17,6 +19,7 @@
 #include "string-util.h"
 #include "strv.h"
 #include "swtpm-util.h"
+#include "sync-util.h"
 
 static int swtpm_find_best_profile(const char *swtpm_setup, char **ret) {
         int r;
@@ -135,9 +138,9 @@ int manufacture_swtpm(const char *state_dir, const char *secret) {
 
         assert(state_dir);
 
-        _cleanup_close_ int state_dir_fd = xopenat(AT_FDCWD, state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+        _cleanup_close_ int state_dir_fd = open(state_dir, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
         if (state_dir_fd < 0)
-                return log_error_errno(state_dir_fd, "Failed to open TPM state directory '%s': %m", state_dir);
+                return log_error_errno(errno, "Failed to open TPM state directory '%s': %m", state_dir);
 
         _cleanup_free_ char *swtpm_setup = NULL;
         r = find_executable("swtpm_setup", &swtpm_setup);
@@ -238,5 +241,15 @@ int manufacture_swtpm(const char *state_dir, const char *secret) {
                 _exit(EXIT_FAILURE);
         }
 
+        /* Persist swtpm_setup's freshly created TPM state before writing the completion marker. */
+        r = syncfs_path(state_dir_fd, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to sync TPM state directory: %m");
+
+        /* Marker, written last, signals that manufacturing completed successfully. */
+        _cleanup_close_ int marker_fd = xopenat(state_dir_fd, SWTPM_MANUFACTURED_MARKER, O_WRONLY|O_CREAT|O_CLOEXEC|O_NOFOLLOW);
+        if (marker_fd < 0)
+                return log_error_errno(marker_fd, "Failed to write '%s' marker: %m", SWTPM_MANUFACTURED_MARKER);
+
         return 0;
 }
index 9c1c7377218e52fb7622274d042be3bf74952786..8897d17126abc1d5b5c856403858ecadd1fdf8be 100644 (file)
@@ -1,4 +1,9 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 #pragma once
 
+/* Marker file written into the TPM state directory once swtpm_setup has successfully created the TPM state.
+ * It is written last, so its presence reliably means a complete TPM was manufactured, rather than a manufacture
+ * that was interrupted halfway through. */
+#define SWTPM_MANUFACTURED_MARKER ".manufactured"
+
 int manufacture_swtpm(const char *state_dir, const char *secret);
index db5ec09312b5e3dbe88f9381a5eb539fa96ddc8e..33d5defd06083ee80de0a87446d478972fa3e03b 100644 (file)
@@ -20,8 +20,8 @@
 #include "main-func.h"
 #include "path-lookup.h"
 #include "path-util.h"
+#include "rm-rf.h"
 #include "sha256.h"
-#include "stat-util.h"
 #include "string-util.h"
 #include "strv.h"
 #include "swtpm-util.h"
@@ -116,18 +116,28 @@ static int setup_swtpm(const char *state_dir, int state_fd, const char *secret)
                         return log_error_errno(r, "Failed to remove 'tpm2-00.volatilestate': %m");
         }
 
-        r = dir_is_empty_at(state_fd, /* path= */ NULL, /* ignore_hidden_or_backup= */ false);
-        if (r < 0)
-                return log_error_errno(r, "Failed to check if TPM state directory is empty: %m");
-        if (r == 0) {
-                log_debug("TPM state directory is already populated, not manufacturing a TPM.");
+        /* manufacture_swtpm() writes its marker only after swtpm_setup has fully created the TPM state.
+         * If the marker file is missing, the existing state is incomplete and recreation is needed. */
+        r = RET_NERRNO(faccessat(state_fd, SWTPM_MANUFACTURED_MARKER, F_OK, AT_SYMLINK_NOFOLLOW));
+        if (r >= 0) {
+                log_debug("TPM state directory holds a fully manufactured TPM, not manufacturing a TPM.");
                 return 0;
         }
+        if (r != -ENOENT)
+                return log_error_errno(r, "Failed to check for TPM manufacture marker: %m");
 
         if (!in_initrd())
                 return log_error_errno(SYNTHETIC_ERRNO(ESTALE), "swtpm TPM state directory has not been initialized in the initrd, refusing.");
 
-        log_debug("TPM state directory is unpopulated, manufacturing a TPM.");
+        /* Cleanup incomplete state before recreating. */
+        _cleanup_close_ int wipe_fd = fd_reopen(state_fd, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
+        if (wipe_fd < 0)
+                return log_error_errno(wipe_fd, "Failed to reopen swtpm state directory: %m");
+        r = rm_rf_children(TAKE_FD(wipe_fd), REMOVE_PHYSICAL, /* root_dev= */ NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to clear incomplete swtpm state directory: %m");
+
+        log_debug("TPM state directory holds no fully manufactured TPM, manufacturing a TPM.");
 
         return manufacture_swtpm(state_dir, secret);
 }
index 7793a7f58c9275ca9260d339a6c165e4c85ca36f..8e2c7d1143280aa8840a26591f6da4a7c019e0ca 100755 (executable)
@@ -3,12 +3,16 @@
 set -eux
 set -o pipefail
 
-# Exercises the software TPM fallback (systemd-tpm2-swtpm.service) across a reboot. The VM boots in EFI mode
+# Exercises the software TPM fallback (systemd-tpm2-swtpm.service) across reboots. The VM boots in EFI mode
 # without a hardware/firmware TPM and with "systemd.tpm2_software_fallback=yes" (see the test's meson.build),
 # so systemd-tpm2-generator manufactures a software TPM on the ESP in the initrd and chainloads swtpm.
 #
 #   boot 0: the TPM is manufactured in the initrd; seal a secret to it and stash the blob.
-#   boot 1: the TPM state persisted on the ESP across the reboot, so the secret still unseals.
+#   boot 1: the TPM state persisted on the ESP across the reboot, so the secret still unseals. Then mimic a
+#           manufacture that was interrupted before it completed (drop everything but the config files, so the
+#           ".manufactured" marker is gone) and reboot.
+#   boot 2: setup_swtpm() must notice the missing marker and re-manufacture, rather than mistaking the
+#           leftover config files for a complete TPM and starting swtpm against a stateless directory.
 #
 # See systemd-tpm2-swtpm.service(8).
 
@@ -17,6 +21,8 @@ set -o pipefail
 
 CRED=/var/lib/systemd-tpm2-swtpm-test.cred
 PLAINTEXT="swtpm round-trip"
+# Marker (SWTPM_MANUFACTURED_MARKER) that manufacture_swtpm() indicates completion with.
+MARKER=.manufactured
 
 if [[ -n "${ASAN_OPTIONS:-}" ]]; then
     # swtpm_setup is not built with sanitizers, but does NSS lookups that pull in the ASan-instrumented
@@ -38,6 +44,15 @@ assert_swtpm_up() {
     assert_in '\+driver' "$(systemd-analyze has-tpm2 || :)"
 }
 
+# Locate swtpm's state directory on the ESP.
+swtpm_state_dir() {
+    local d
+    for d in /boot/loader/swtpm /efi/loader/swtpm; do
+        [[ -d "$d" ]] && { echo "$d"; return 0; }
+    done
+    return 1
+}
+
 case "$REBOOT_COUNT" in
     0)
         assert_swtpm_up
@@ -53,6 +68,31 @@ case "$REBOOT_COUNT" in
         # Persistence: the TPM state survived the reboot on the ESP, so the blob still unseals.
         echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext
         systemd-creds decrypt --name= "$CRED" - | cmp /tmp/swtpm-plaintext -
+
+        # Mimic a manufacture interrupted after swtpm began writing TPM state but before the marker: stop
+        # swtpm, drop everything except the three config files, then leave a partial/corrupt state file
+        # behind. swtpm_setup --not-overwrite would refuse to recreate that, so recovery must clear it first.
+        statedir="$(swtpm_state_dir)"
+        systemctl stop systemd-tpm2-swtpm.service
+        find "$statedir" -mindepth 1 -maxdepth 1 -type f \
+             ! -name swtpm-localca.conf ! -name swtpm-localca.options ! -name swtpm_setup.conf -delete
+        echo "corrupt" >"$statedir/tpm2-00.permall"
+        test -e "$statedir/swtpm_setup.conf"
+        test ! -e "$statedir/$MARKER"
+        systemctl_final reboot
+        exec sleep infinity
+        ;;
+    2)
+        # setup_swtpm() must have re-manufactured instead of trusting the leftover config files: the marker is
+        # back and swtpm_setup re-ran swtpm_localca, recreating issuer-certificate.pem. The TPM must also work.
+        # Regression test for keying re-manufacture off an incomplete state directory.
+        assert_swtpm_up
+        statedir="$(swtpm_state_dir)"
+        test -e "$statedir/$MARKER"
+        test -e "$statedir/issuer-certificate.pem"
+        echo -n "$PLAINTEXT" >/tmp/swtpm-plaintext
+        systemd-creds encrypt --name= --with-key=tpm2 /tmp/swtpm-plaintext /tmp/swtpm-new.cred
+        systemd-creds decrypt --name= /tmp/swtpm-new.cred - | cmp /tmp/swtpm-plaintext -
         touch /testok
         ;;
     *)