Skip to content

boot: measure Type #1 DeviceTrees into PCR 12 - #43791

Open
gaato wants to merge 5 commits into
systemd:mainfrom
gaato:measure-bls-type1-dtb
Open

gaato wants to merge 5 commits into
systemd:mainfrom
gaato:measure-bls-type1-dtb

Conversation

@gaato

@gaato gaato commented Sep 17, 2026 •

Copy link
Copy Markdown

Summary

  • extend the DeviceTree measurement introduced by stub: add support for dtb addons #28699 to external DeviceTrees referenced by BLS Type #1 entries
  • log the raw, pre-fixup DTB using the existing DeviceTree event tag
  • document the PCR policy impact and add UEFI/TPM integration coverage

Testing

  • meson test -C build -v --suite boot
  • meson test -C build -v check-version-systemd-pcrlock check-help-systemd-pcrlock
  • meson test -C build --setup=integration -v TEST-70-TPM2-BLS-DTB
  • shellcheck -x test/units/TEST-70-TPM2-BLS-DTB.sh
  • manually tested the cross-built systemd-bootaa64.efi on Fedora 44/aarch64 with QEMU, AAVMF, and swtpm; verified that the Type #1 DTB was measured exactly once into PCR 12 and that the event log replay matched the PCR value

@github-actions github-actions Bot added the please-review PR is ready for (re-)review by a maintainer label Sep 17, 2026
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Reviewed cd15dec (architecture, correctness, lifetimes, security, style, tests). No new issues found, 2 still open.

Must fix

  • test/integration-tests/integration-test-wrapper.py:728 hypervisor=off is an x86-only CPU property, so qemu refuses to start on aarch64 — where Type #​1 devicetree matters most — and -cpu max also overrides mkosi's own CPU choice (e.g. host under KVM). Gate the -cpu part on x86; the SMBIOS blob alone suffices elsewhere.

Suggestions

  • src/boot/boot.c:3054 This measurement fails open: devicetree_install() has already published the blob via BS->InstallConfigurationTable(EFI_DTB_TABLE) by the time we get here, so when tpm_log_tagged_event() returns an error we log it and fall through to BS->StartImage(). The kernel then consumes the (Secure-Boot-unverified) DTB while PCR 12 is left at exactly the value it would have had for a "no DTB" boot, i.e. a PCR 12 policy sealed against the DTB-less state still unseals. Since tpm_log_tagged_event() already returns EFI_SUCCESS with measured=false when neither the TCG2 nor the CC protocol is present, a non-EFI_SUCCESS return here means a protocol was present and the extend genuinely failed, so treating it as fatal would not regress TPM-less machines. Given that the whole point of the !secure_boot_enabled() guard above is that this DTB is unverified, the measurement is the compensating control and losing it silently is worth avoiding: either measure before the blob is published, or propagate the error so the entry is not started (the _cleanup_(devicetree_cleanup) on dtstate will unwind the install). Note the asymmetry with install_addon_devicetrees() in stub.c, which does abort on measurement failure. Also consider passing a real ret_measured out-parameter instead of NULL, as stub.c does, so the loader knows whether the measurement actually happened.
  • src/fundamental/tpm2-pcr.h:50 Sharing tag 0x6c46f751 between two producers makes the PCR 12 event log ambiguous. An EV_EVENT_TAG record carries only the PCR, the tag id and the description string, so after this change a single boot's PCR 12 log can contain a Type #​1 DTB record emitted by systemd-boot plus DTB addon records emitted by systemd-stub with nothing to tell them apart except the incidental shape of the description (a full ESP path like \EFI\Linux\foo.dtb vs a bare addon filename) — which is exactly why the pcrlock label had to be weakened from "systemd-stub: devicetree addon %s" to "systemd: devicetree %s". Event log tags are effectively ABI once shipped and the tag space is cheap, so allocating a separate id for the sd-boot measurement (keeping DEVICETREE_ADDON_EVENT_TAG_ID for stub addons) would keep both producers individually recognizable for log parsers and PCR 12 policy authors, and would make both the rename and the label change unnecessary. If sharing the tag really is intentional, the comment here should say so explicitly and name both producers, since the macro name no longer hints at either.
  • src/boot/boot.c:3049 This failure is tolerated — the return value of log_error_status() is dropped and the DTB stays installed — but neither the log level nor the message text says so. docs/CODING_STYLE.md asks that log messages for a failure which is not fatal to the immediate operation be suffixed with …, ignoring: %m or similar and logged at warning level. So either log_warning_status(err, "Unable to extend PCR %i with DTB '%ls', ignoring: %m", ...), or — if an unmeasured-but-installed DTB should not be tolerated at all — make it fatal with return log_error_status(...), which is closer to what install_addon_devicetrees() in src/boot/stub.c does. Note the stale err left by the fall-through is itself harmless: every path out of the following switch reassigns err before it is read.
  • test/units/TEST-70-TPM2-BLS-DTB.sh:18 These prerequisites hard-fail instead of skipping. Sibling tests print a reason and exit 77 when EFI, TPM2 or systemd-pcrlock is unavailable (see TEST-92-TPM2-SWTPM.sh:35, TEST-70-TPM2.pcrlock.sh:15). (outdated)

Nits

  • src/boot/devicetree.c:98 len is used as both the requested size and the returned size here, which obscures what this check does: info->FileSize < FDT_V1_SIZE was already rejected above, so this can only fire on a short read (chunked_read() breaks out and reports fewer bytes when the file is smaller than requested). As written it silently accepts any truncation down to 28 bytes, so a file that shrank between get_file_info() and the read is installed and measured as a partial DTB whose embedded total_size no longer matches the buffer. Using a separate output variable (as e.g. src/boot/boot.c:2794 does) and comparing it against info->FileSize would express the intent of rejecting truncated reads without duplicating the constant.
  • docs/TPM2_PCR_MEASUREMENTS.md:185 The description is not the path verbatim as it appears in the Type #​1 file: entry->devicetree comes from xstr8_to_path() (src/boot/util.c:48), which runs convert_efi_path() and turns / into \ (and collapses doubled separators). So a devicetree /EFI/Linux/foo.dtb line is recorded as \EFI\Linux\foo.dtb. Since this document is the reference used to precompute/match these events, it is worth saying that the recorded string is the EFI-style path with backslash separators rather than the literal field value.
  • src/boot/boot.c:3045 docs/CODING_STYLE.md asks that when passing NULL or another value meaning "unset" to a function, a comment indicate the argument name. The pre-existing tpm_log_tagged_event() call in this same file — the loader.conf measurement at boot.c:1626 — already spells it /* ret_measured= */ NULL. Please match that here.
  • test/integration-tests/integration-test-wrapper.py:650 This ~22-line struct.pack() of SMBIOS constants is fully static — it depends on nothing in main(), which is already a very long function of mostly argument plumbing. This file already keeps fixed blobs at module level (EMERGENCY_EXIT_DROPIN, EMERGENCY_EXIT_SERVICE at the top), so hoisting it to a module-level SMBIOS_TYPE0 = struct.pack(...) + b'\0\0' constant (or a tiny helper) and leaving just smbios_type0.write(SMBIOS_TYPE0) here would keep main() scannable.
  • test/integration-tests/TEST-70-TPM2/meson.build:18 The name is hardcoded while the entry above derives it via fs.name(), and the configuration/wants/after/cmdline block is copy-pasted. Either derive this dict from the first entry, or give the test its own directory as TEST-92-TPM2-SWTPM does.

Workflow run

Comment thread src/boot/boot.c
Comment on lines +3039 to +3051
err = tpm_log_tagged_event(
TPM2_PCR_KERNEL_CONFIG,
POINTER_TO_PHYSICAL_ADDRESS(dtb.iov_base),
dtb.iov_len,
DEVICETREE_EVENT_TAG_ID,
entry->devicetree,
NULL);
if (err != EFI_SUCCESS)
log_error_status(
err,
"Unable to extend PCR %i with DTB '%ls': %m",
TPM2_PCR_KERNEL_CONFIG,
entry->devicetree);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: suggestion: This measurement fails open: devicetree_install() has already published the blob via BS->InstallConfigurationTable(EFI_DTB_TABLE) by the time we get here, so when tpm_log_tagged_event() returns an error we log it and fall through to BS->StartImage(). The kernel then consumes the (Secure-Boot-unverified) DTB while PCR 12 is left at exactly the value it would have had for a "no DTB" boot, i.e. a PCR 12 policy sealed against the DTB-less state still unseals. Since tpm_log_tagged_event() already returns EFI_SUCCESS with measured=false when neither the TCG2 nor the CC protocol is present, a non-EFI_SUCCESS return here means a protocol was present and the extend genuinely failed, so treating it as fatal would not regress TPM-less machines. Given that the whole point of the !secure_boot_enabled() guard above is that this DTB is unverified, the measurement is the compensating control and losing it silently is worth avoiding: either measure before the blob is published, or propagate the error so the entry is not started (the _cleanup_(devicetree_cleanup) on dtstate will unwind the install). Note the asymmetry with install_addon_devicetrees() in stub.c, which does abort on measurement failure. Also consider passing a real ret_measured out-parameter instead of NULL, as stub.c does, so the loader knows whether the measurement actually happened.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that boot continues after a measurement error. However, install_addon_devicetrees() also returns only from the helper; its caller continues booting with the DTB installed.

Also, a non-success return does not necessarily mean the PCR was unchanged. For example, EFI_VOLUME_FULL means the extend succeeded but the event could not be logged.

These points do not eliminate the concern about an actual extend failure. Making that fatal is a reasonable policy to consider, but it would differ from the existing stub behavior.

Comment thread src/fundamental/tpm2-pcr.h
Comment thread src/boot/boot.c Outdated
Comment thread src/boot/devicetree.c Outdated
Comment thread docs/TPM2_PCR_MEASUREMENTS.md Outdated
Comment thread src/boot/boot.c Outdated
Comment thread test/integration-tests/integration-test-wrapper.py Outdated
Comment thread test/units/TEST-70-TPM2-BLS-DTB.sh Outdated
Comment on lines +16 to +18
[[ -d /sys/firmware/efi ]]
systemd-analyze has-tpm2
[[ -x /usr/lib/systemd/systemd-pcrlock ]]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: suggestion: These prerequisites hard-fail instead of skipping. Sibling tests print a reason and exit 77 when EFI, TPM2 or systemd-pcrlock is unavailable (see TEST-92-TPM2-SWTPM.sh:35, TEST-70-TPM2.pcrlock.sh:15).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a skip when systemd-pcrlock is unavailable, but keep the EFI and TPM checks as assertions. This test explicitly requests UEFI firmware and a TPM, so skipping those failures could hide setup or detection regressions.

Comment thread test/integration-tests/TEST-70-TPM2/meson.build Outdated
@gaato
gaato force-pushed the measure-bls-type1-dtb branch from f095658 to b22a1ef Compare September 23, 2026 14:31
Comment thread test/integration-tests/integration-test-wrapper.py Outdated
gaato added 5 commits October 16, 2026 18:26
Reject incomplete DTB reads before installing the blob. Keep measurement errors non-fatal and make that behavior explicit in the warning.

Clarify the shared DeviceTree event tag and normalized path description, and move the unchanged SMBIOS Type 0 payload to a module-level constant.
Skip the test on its first boot when systemd-pcrlock is unavailable. Keep UEFI, TPM and Secure Boot checks as assertions, since the test explicitly requests the required firmware and TPM setup.

Share the common TPM2 integration test configuration without changing either test's effective settings.
@gaato
gaato force-pushed the measure-bls-type1-dtb branch from 233c7e1 to cd15dec Compare October 16, 2026 09:30
@gaato
gaato requested a review from daandemeyer as a code owner October 16, 2026 09:30

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-review please-review PR is ready for (re-)review by a maintainer tree-wide

Development

Successfully merging this pull request may close these issues.

2 participants