:: commit e99fdaa76e501c974caed3953f92ba5352d91156

Mintsuki <mintsuki@protonmail.com> — 2026-04-30 22:30

parents: a16e991133

lib, protos, docs: Reshuffle Measured Boot PCR allocation

diff --git a/USAGE.md b/USAGE.md
index 383a3414..bb9b4921 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -56,11 +56,18 @@ When measured boot is active, Limine extends the platform PCRs with the
 artifacts it loads, following the GRUB convention from the
 [UAPI Linux TPM PCR Registry](https://uapi-group.org/specifications/specs/linux_tpm_pcr_registry/):
 
-* **PCR 8** receives, in order, the on-disk `limine.conf` bytes (before any
-  in-memory cleanup), and the kernel command line of the booted entry.
-* **PCR 9** receives, in load order, the kernel image as read from disk, each
-  module/initrd in the order they appear in the config, and, when the booted
-  protocol consumes a device tree blob, the DTB as loaded (taken from
+* **PCR 8** receives, in order, the kernel command line of the booted entry
+  (from its `cmdline` config option, exactly as Limine will hand it to the
+  kernel), the kernel image's path, each module/initrd's path in the order
+  they appear in the config, and the DTB's path when the booted protocol
+  consumes one specified via `dtb_path` or `global_dtb`. Any trailing
+  `#<hash>` integrity suffix is stripped before measurement so PCR 8 stays
+  stable across kernel/module/DTB updates; the `$` decompression prefix is
+  preserved as part of the policy.
+* **PCR 9** receives, in order, the on-disk `limine.conf` bytes (before any
+  in-memory cleanup), the kernel image as read from disk, each module/initrd
+  in the order they appear in the config, and, when the booted protocol
+  consumes a device tree blob, the DTB as loaded (taken from
   `dtb_path`/`global_dtb` if set, otherwise from the firmware's
   `EFI_DTB_TABLE_GUID` table) before Limine's `/chosen` and memory-node
   fixups.
@@ -93,10 +100,21 @@ handoff is consistent across attempts:
 ### Reproducing the digests
 For an external verifier to recompute a PCR extend, hash exactly these bytes:
 
+For PCR 8:
+
+* `cmdline: <cmdline>`: the kernel command line bytes, without trailing
+  NUL, i.e. `strlen(cmdline)` bytes.
+* `path: <kernel_path>`: the kernel image's path string with any trailing
+  `#<hash>` suffix stripped, without trailing NUL.
+* `module_path: <module_path>`: the module/initrd's path string with any
+  trailing `#<hash>` suffix stripped, without trailing NUL.
+* `dtb_path: <dtb_path>`: the DTB file's path string with any trailing
+  `#<hash>` suffix stripped, without trailing NUL.
+
+For PCR 9:
+
 * `limine_cfg`: the on-disk `limine.conf` file bytes verbatim (no trailing
   newline added, no NUL appended).
-* `cmdline: <cmdline>`: the kernel command line as a string, without its
-  terminating NUL, i.e. `strlen(cmdline)` bytes.
 * `path: <kernel_path>`: the full file bytes of the kernel image as opened
   by Limine (post-decompression for `$`-prefixed paths, after BLAKE2B hash
   verification when Secure Boot is active).
diff --git a/common/lib/misc.c b/common/lib/misc.c
index 00c78489..714b8219 100644
--- a/common/lib/misc.c
+++ b/common/lib/misc.c
@@ -156,6 +156,7 @@ void *get_device_tree_blob(const char *config, size_t extra_size,
 
 #if defined (UEFI)
             if (measure) {
+                tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "dtb_path: ", dtb_path);
                 tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                             dtb, size, "dtb_path: ", dtb_path);
             }
diff --git a/common/lib/tpm.c b/common/lib/tpm.c
index 1d9a6e39..0cd2f731 100644
--- a/common/lib/tpm.c
+++ b/common/lib/tpm.c
@@ -172,6 +172,29 @@ void tpm_measure(uint32_t pcr, uint32_t event_type,
     }
 }
 
+void tpm_measure_path(uint32_t pcr, uint32_t event_type,
+                      const char *desc_prefix, const char *path) {
+    if (!measured_boot || path == NULL) {
+        return;
+    }
+
+    const char *hash_sep = strchr(path, '#');
+    size_t path_len = hash_sep != NULL
+        ? (size_t)(hash_sep - path)
+        : strlen(path);
+
+    // Static scratch matches uri.c's URI_BUF_SIZE; URIs longer than that
+    // already panic in uri_resolve(), so a too-long path here is a bug.
+    static char stripped[4096];
+    if (path_len >= sizeof(stripped)) {
+        return;
+    }
+    memcpy(stripped, path, path_len);
+    stripped[path_len] = '\0';
+
+    tpm_measure(pcr, event_type, stripped, path_len, desc_prefix, stripped);
+}
+
 uint32_t tpm_calc_event_size(const void *event_p, const void *header_p) {
     const struct tpm_pcr_event2_head *event = event_p;
     const struct tpm_pcr_event_v1_2 *event_header = header_p;
diff --git a/common/lib/tpm.h b/common/lib/tpm.h
index 87b061d7..6c8cb7bd 100644
--- a/common/lib/tpm.h
+++ b/common/lib/tpm.h
@@ -25,6 +25,13 @@ void tpm_measure(uint32_t pcr, uint32_t event_type,
                  const void *data, size_t data_size,
                  const char *desc_prefix, const char *desc_value);
 
+// Measure a config-supplied URI string into the given PCR with any trailing
+// `#<hash>` suffix stripped, so the digest captures only the policy-stable
+// portion (resource, root, path, and any `$` decompression marker). The
+// event description shows the same stripped string.
+void tpm_measure_path(uint32_t pcr, uint32_t event_type,
+                      const char *desc_prefix, const char *path);
+
 // Capture the firmware TCG2 event log into bootloader-reclaimable memory
 // and expose the raw event stream. `format` receives the TCG event log
 // format identifier (1 = TCG 1.2, 2 = TCG 2.0 crypto-agile). Returns false
diff --git a/common/menu.c b/common/menu.c
index d11ac1ac..bf86e968 100644
--- a/common/menu.c
+++ b/common/menu.c
@@ -1192,7 +1192,7 @@ noreturn void _menu(bool first_run) {
     size_t raw_size;
     const char *raw = config_get_raw(&raw_size);
     if (raw != NULL) {
-        tpm_measure(TPM_PCR_BOOT_AUTH, TPM_EV_IPL,
+        tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                     raw, raw_size, "limine_cfg", NULL);
     }
 #endif
diff --git a/common/protos/limine.c b/common/protos/limine.c
index d0888d1f..aa89a3b6 100644
--- a/common/protos/limine.c
+++ b/common/protos/limine.c
@@ -524,6 +524,7 @@ noreturn void limine_load(char *config, char *cmdline) {
     uint8_t *kernel = kernel_file->fd;
 
 #if defined (UEFI)
+    tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "path: ", kernel_path);
     tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                 kernel, kernel_file->size, "path: ", kernel_path);
 #endif
@@ -1340,6 +1341,7 @@ FEAT_START
         *l = get_file(f, module_cmdline);
 
 #if defined (UEFI)
+        tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "module_path: ", module_path);
         tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                     f->fd, f->size, "module_path: ", module_path);
 #endif
diff --git a/common/protos/linux_risc.c b/common/protos/linux_risc.c
index 4f483f14..d49e27e3 100644
--- a/common/protos/linux_risc.c
+++ b/common/protos/linux_risc.c
@@ -158,6 +158,7 @@ static void load_module(struct boot_param *p, char *config) {
 
         char *module_path = config_get_value(config, i, "MODULE_PATH");
 
+        tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "module_path: ", module_path);
         tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                     p->module_base + offset, module_size, "module_path: ", module_path);
 
@@ -526,6 +527,7 @@ noreturn void linux_load(char *config, char *cmdline) {
     fclose(kernel_file);
     printv("linux: loaded kernel `%s` at %p, size %U\n", kernel_path, p.kernel_base, (uint64_t)p.kernel_size);
 
+    tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "path: ", kernel_path);
     tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                 p.kernel_base, p.kernel_size, "path: ", kernel_path);
 
diff --git a/common/protos/linux_x86.c b/common/protos/linux_x86.c
index b76fbd4d..9fed6999 100644
--- a/common/protos/linux_x86.c
+++ b/common/protos/linux_x86.c
@@ -424,6 +424,7 @@ noreturn void linux_load(char *config, char *cmdline) {
     fread(kernel_file, (void *)kernel_load_addr, real_mode_code_size, kernel_file->size - real_mode_code_size);
 
 #if defined (UEFI)
+    tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "path: ", kernel_path);
     tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                 kernel_file->fd, kernel_file->size, "path: ", kernel_path);
 #endif
@@ -516,6 +517,7 @@ noreturn void linux_load(char *config, char *cmdline) {
         fread(modules[i], (void *)_modules_mem_base, 0, modules[i]->size);
 
 #if defined (UEFI)
+        tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "module_path: ", module_path);
         tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                     (void *)_modules_mem_base, modules[i]->size, "module_path: ", module_path);
 #endif
diff --git a/common/protos/multiboot1.c b/common/protos/multiboot1.c
index fdd08afa..a3c79cf0 100644
--- a/common/protos/multiboot1.c
+++ b/common/protos/multiboot1.c
@@ -85,6 +85,7 @@ noreturn void multiboot1_load(char *config, char *cmdline) {
     size_t kernel_file_size = kernel_file->size;
 
 #if defined (UEFI)
+    tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "path: ", kernel_path);
     tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                 kernel, kernel_file_size, "path: ", kernel_path);
 #endif
@@ -362,6 +363,7 @@ noreturn void multiboot1_load(char *config, char *cmdline) {
             uint64_t module_target = (uint64_t)-1; /* no target preference, use top */
 
 #if defined (UEFI)
+            tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "module_path: ", module_path);
             tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                         module_addr, f->size, "module_path: ", module_path);
 #endif
diff --git a/common/protos/multiboot2.c b/common/protos/multiboot2.c
index d6677d01..46fb6886 100644
--- a/common/protos/multiboot2.c
+++ b/common/protos/multiboot2.c
@@ -105,6 +105,7 @@ noreturn void multiboot2_load(char *config, char* cmdline) {
     size_t kernel_file_size = kernel_file->size;
 
 #if defined (UEFI)
+    tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "path: ", kernel_path);
     tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                 kernel, kernel_file_size, "path: ", kernel_path);
 #endif
@@ -666,6 +667,7 @@ reloc_fail:
         uint64_t module_target = (uint64_t)-1;
 
 #if defined (UEFI)
+        tpm_measure_path(TPM_PCR_BOOT_AUTH, TPM_EV_IPL, "module_path: ", module_path);
         tpm_measure(TPM_PCR_LOADED_IMAGES, TPM_EV_IPL,
                     module_addr, f->size, "module_path: ", module_path);
 #endif
tab: 248 wrap: offon