From 468f8131ec7220689427d44aacd8db895a8cbdef Mon Sep 17 00:00:00 2001 From: Sean Rhodes Date: Sun, 1 Feb 2026 21:46:56 +0000 Subject: [PATCH] security/tcg/opal_s3: hook into default SMI/resume paths Provide common entry points for the OPAL S3 unlock feature and wire them into the generic x86 SMM and S3 resume code. - Add opal_s3_smi_{apmc,sleep,sleep_finalize} helpers. - Call these helpers from the default weak mainboard SMI hooks when CONFIG(TCG_OPAL_S3_UNLOCK) is enabled. This keeps the feature usable without forcing boards to implement new SMI handlers. - Trigger the SMM unlock on S3 resume from arch/x86/acpi_s3.c. Select SMM_OPAL_S3_STATE_SMRAM so the secret is persisted across SMM handler reload. Add a delay and retry loop before unlock, and restore NVMe BAR0 if the device loses PCI config state across S3. The SMM side continues to whitelist only the OPAL service and unlock APMC commands and fails closed if any invariant is violated. TEST=tested with rest of patch train Change-Id: I86a44760a189219a95914bd3549997880fb0242b Signed-off-by: Sean Rhodes Reviewed-on: https://review.coreboot.org/c/coreboot/+/91045 Reviewed-by: Angel Pons Tested-by: build bot (Jenkins) --- src/arch/x86/acpi_s3.c | 4 + src/include/security/tcg/opal_s3_resume.h | 15 + src/include/security/tcg/opal_s3_smm.h | 36 ++ src/security/tcg/opal_s3/Kconfig | 1 + src/security/tcg/opal_s3/opal_s3_resume.c | 13 + src/security/tcg/opal_s3/opal_s3_smm.c | 450 ++++++++++++++++++++ src/soc/intel/common/block/smm/smihandler.c | 8 + 7 files changed, 527 insertions(+) create mode 100644 src/include/security/tcg/opal_s3_resume.h create mode 100644 src/include/security/tcg/opal_s3_smm.h create mode 100644 src/security/tcg/opal_s3/opal_s3_resume.c create mode 100644 src/security/tcg/opal_s3/opal_s3_smm.c diff --git a/src/arch/x86/acpi_s3.c b/src/arch/x86/acpi_s3.c index f893e3a9f6..d65a079829 100644 --- a/src/arch/x86/acpi_s3.c +++ b/src/arch/x86/acpi_s3.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #define WAKEUP_BASE 0x600 @@ -20,6 +21,9 @@ void __noreturn acpi_resume(void *wake_vec) /* Call mainboard resume handler first, if defined. */ mainboard_suspend_resume(); + if (CONFIG(TCG_OPAL_S3_UNLOCK)) + opal_s3_resume_unlock(); + /* Copy wakeup trampoline in place. */ memcpy((void *)WAKEUP_BASE, &__wakeup, __wakeup_size); diff --git a/src/include/security/tcg/opal_s3_resume.h b/src/include/security/tcg/opal_s3_resume.h new file mode 100644 index 0000000000..f14ee90b49 --- /dev/null +++ b/src/include/security/tcg/opal_s3_resume.h @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef SECURITY_TCG_OPAL_S3_RESUME_H +#define SECURITY_TCG_OPAL_S3_RESUME_H + +/* + * Trigger an SMM-assisted OPAL unlock during S3 resume. + * + * The unlock implementation lives in SMM; this is a small ramstage helper + * that issues the APMC trigger. It is called automatically from the x86 + * resume path when enabled. + */ +void opal_s3_resume_unlock(void); + +#endif diff --git a/src/include/security/tcg/opal_s3_smm.h b/src/include/security/tcg/opal_s3_smm.h new file mode 100644 index 0000000000..798ed0ae0a --- /dev/null +++ b/src/include/security/tcg/opal_s3_smm.h @@ -0,0 +1,36 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef SECURITY_TCG_OPAL_S3_SMM_H +#define SECURITY_TCG_OPAL_S3_SMM_H + +#include + +/* + * OPAL S3 SMM helpers. + * + * These helpers are called from SMI handlers. When the feature is disabled, + * these APIs compile to no-ops so callers do not need preprocessor guards. + */ +#if CONFIG(TCG_OPAL_S3_UNLOCK) +int opal_s3_smi_apmc(u8 apmc); +void opal_s3_smi_sleep(u8 slp_typ); +void opal_s3_smi_sleep_finalize(u8 slp_typ); +#else +static inline int opal_s3_smi_apmc(u8 apmc) +{ + (void)apmc; + return 0; +} + +static inline void opal_s3_smi_sleep(u8 slp_typ) +{ + (void)slp_typ; +} + +static inline void opal_s3_smi_sleep_finalize(u8 slp_typ) +{ + (void)slp_typ; +} +#endif + +#endif diff --git a/src/security/tcg/opal_s3/Kconfig b/src/security/tcg/opal_s3/Kconfig index a34f34234e..15f19af46f 100644 --- a/src/security/tcg/opal_s3/Kconfig +++ b/src/security/tcg/opal_s3/Kconfig @@ -7,6 +7,7 @@ config TCG_OPAL_S3_UNLOCK depends on HAVE_ACPI_RESUME depends on PCI select SMM_OPAL_S3_SCRATCH_CBMEM + select SMM_OPAL_S3_STATE_SMRAM help Provide an SMM handler that accepts an OPAL password for the current sleep cycle and performs an NVMe Security Send/Receive unlock on S3 diff --git a/src/security/tcg/opal_s3/opal_s3_resume.c b/src/security/tcg/opal_s3/opal_s3_resume.c new file mode 100644 index 0000000000..415564c204 --- /dev/null +++ b/src/security/tcg/opal_s3/opal_s3_resume.c @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include +#include +#include + +void opal_s3_resume_unlock(void) +{ + u32 rc = call_smm(APM_CNT_OPAL_S3_UNLOCK, 0, NULL); + + if (CONFIG(DEBUG_SMI) && rc) + printk(BIOS_DEBUG, "OPAL-S3: resume unlock rc=0x%x\n", rc); +} diff --git a/src/security/tcg/opal_s3/opal_s3_smm.c b/src/security/tcg/opal_s3/opal_s3_smm.c new file mode 100644 index 0000000000..4802b9dceb --- /dev/null +++ b/src/security/tcg/opal_s3/opal_s3_smm.c @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "opal_secure.h" + +#define OPAL_S3_SCRATCH_ALIGN 4096 +#define OPAL_S3_SCRATCH_PAGES 3 +#define OPAL_S3_SCRATCH_MIN_BYTES (OPAL_S3_SCRATCH_PAGES * OPAL_S3_SCRATCH_ALIGN) + +#define OPAL_S3_UNLOCK_RETRY_DELAY_MS 50 +#define OPAL_S3_UNLOCK_RETRIES 10 + +#define OPAL_S3_STATE_SIGNATURE 0x33534c4f /* "OLS3" */ +#define OPAL_S3_STATE_VERSION 0x0001 + +#define OPAL_S3_ARMED_NONE 0 +#define OPAL_S3_ARMED_S3 1 + +#define OPAL_S3_MAX_SECRETS 8 + +struct __packed opal_s3_secret { + u8 valid; + u8 reserved0; + u16 reserved1; + + u8 bus; + u8 dev; + u8 func; + u8 reserved2; + + u16 base_comid; + u8 password_len; + u8 reserved3; + u8 password[OPAL_S3_MAX_PASSWORD_LEN]; + + u32 nvme_bar0_low; + u32 nvme_bar0_high; + + u32 unlocked_cycle; +}; + +struct __packed opal_s3_state { + u32 signature; + u16 version; + u16 size; + + u8 armed_state; + u8 reserved0; + u16 reserved1; + + u32 sleep_cycle; + u32 armed_cycle; + + struct opal_s3_secret secret[OPAL_S3_MAX_SECRETS]; +}; + +static struct opal_s3_state state_fallback; + +static struct opal_s3_state *opal_s3_get_state(void) +{ +#if CONFIG(SMM_OPAL_S3_STATE_SMRAM) + uintptr_t base = 0; + size_t size = 0; + + smm_get_opal_s3_state_buffer(&base, &size); + if (base && size >= sizeof(struct opal_s3_state)) + return (struct opal_s3_state *)(uintptr_t)base; +#endif + + return &state_fallback; +} + +static bool opal_s3_validate_scratch(uintptr_t base, size_t size, uintptr_t *aligned_base_out, + size_t *aligned_size_out) +{ + uintptr_t aligned; + size_t aligned_size; + + if (!base || !size) + return false; + + if (base + size < base) + return false; + + aligned = ALIGN_UP(base, OPAL_S3_SCRATCH_ALIGN); + if (aligned < base) + return false; + + if (aligned - base > size) + return false; + + aligned_size = size - (aligned - base); + if (aligned_size < OPAL_S3_SCRATCH_MIN_BYTES) + return false; + + if ((aligned & (OPAL_S3_SCRATCH_ALIGN - 1)) != 0) + return false; + + if (smm_points_to_smram((void *)(uintptr_t)aligned, OPAL_S3_SCRATCH_MIN_BYTES)) + return false; + + if (aligned + OPAL_S3_SCRATCH_MIN_BYTES < aligned) + return false; + if (aligned + OPAL_S3_SCRATCH_MIN_BYTES > base + size) + return false; + + *aligned_base_out = aligned; + *aligned_size_out = aligned_size; + return true; +} + +static u32 opal_s3_clear_secret(void) +{ + struct opal_s3_state *st = opal_s3_get_state(); + + opal_explicit_bzero(st, sizeof(*st)); + + return 0; +} + +static struct opal_s3_secret *opal_s3_find_secret(struct opal_s3_state *st, + const struct opal_s3_smm_ctx *ctx) +{ + for (size_t i = 0; i < ARRAY_SIZE(st->secret); i++) { + struct opal_s3_secret *s = &st->secret[i]; + + if (!s->valid) + continue; + + if (s->bus == ctx->bus && s->dev == ctx->dev && s->func == ctx->func && + s->base_comid == ctx->base_comid) + return s; + } + + return NULL; +} + +static struct opal_s3_secret *opal_s3_alloc_secret(struct opal_s3_state *st) +{ + for (size_t i = 0; i < ARRAY_SIZE(st->secret); i++) { + struct opal_s3_secret *s = &st->secret[i]; + + if (!s->valid) + return s; + } + + return NULL; +} + +static u32 opal_s3_set_secret(const struct opal_s3_smm_ctx *ctx) +{ + struct opal_s3_state *st = opal_s3_get_state(); + struct opal_s3_secret *s; + uintptr_t scratch_base = 0; + size_t scratch_size = 0; + uintptr_t aligned_base = 0; + size_t aligned_size = 0; + pci_devfn_t nvme_dev; + + if (!ctx) + return 1; + + if (smm_points_to_smram(ctx, sizeof(*ctx))) + return 2; + + if (ctx->signature != OPAL_S3_SMM_CTX_SIGNATURE) + return 3; + + if (ctx->version != OPAL_S3_SMM_CTX_VERSION) + return 4; + + if (ctx->size < sizeof(*ctx)) + return 5; + + if (ctx->password_len == 0 || ctx->password_len > OPAL_S3_MAX_PASSWORD_LEN) + return 6; + + smm_get_opal_s3_scratch_buffer(&scratch_base, &scratch_size); + if (!opal_s3_validate_scratch(scratch_base, scratch_size, &aligned_base, + &aligned_size)) { + printk(BIOS_ERR, "OPAL: invalid scratch (base=0x%lx size=0x%zx)\n", + (unsigned long)scratch_base, scratch_size); + return 7; + } + + nvme_dev = PCI_DEV(ctx->bus, ctx->dev, ctx->func); + + if (st->signature != OPAL_S3_STATE_SIGNATURE || st->version != OPAL_S3_STATE_VERSION || + st->size != sizeof(*st)) { + opal_s3_clear_secret(); + st->signature = OPAL_S3_STATE_SIGNATURE; + st->version = OPAL_S3_STATE_VERSION; + st->size = sizeof(*st); + st->armed_state = OPAL_S3_ARMED_NONE; + } + + s = opal_s3_find_secret(st, ctx); + if (!s) + s = opal_s3_alloc_secret(st); + if (!s) + return 8; + + opal_explicit_bzero(s, sizeof(*s)); + s->valid = 1; + s->bus = ctx->bus; + s->dev = ctx->dev; + s->func = ctx->func; + s->base_comid = ctx->base_comid; + s->password_len = ctx->password_len; + memcpy(s->password, ctx->password, ctx->password_len); + + st->signature = OPAL_S3_STATE_SIGNATURE; + st->version = OPAL_S3_STATE_VERSION; + st->size = sizeof(*st); + + s->nvme_bar0_low = pci_read_config32(nvme_dev, PCI_BASE_ADDRESS_0); + s->nvme_bar0_high = pci_read_config32(nvme_dev, PCI_BASE_ADDRESS_0 + 4); + + return 0; +} + +static void opal_s3_arm_for_s3(void) +{ + struct opal_s3_state *st = opal_s3_get_state(); + bool any_secret = false; + + if (st->signature != OPAL_S3_STATE_SIGNATURE || st->version != OPAL_S3_STATE_VERSION || + st->size != sizeof(*st)) + return; + + if (st->armed_state == OPAL_S3_ARMED_S3) + return; + + for (size_t i = 0; i < ARRAY_SIZE(st->secret); i++) { + if (!st->secret[i].valid) + continue; + + any_secret = true; + st->secret[i].unlocked_cycle = 0; + } + if (!any_secret) + return; + + st->sleep_cycle++; + st->armed_cycle = st->sleep_cycle; + st->armed_state = OPAL_S3_ARMED_S3; +} + +static void opal_s3_restore_nvme_bar0_if_needed(pci_devfn_t nvme_dev, u32 saved_bar0_low, + u32 saved_bar0_high) +{ + const u32 cur_low = pci_read_config32(nvme_dev, PCI_BASE_ADDRESS_0); + const u32 cur_base = cur_low & ~PCI_BASE_ADDRESS_MEM_ATTR_MASK; + const u32 saved_base = saved_bar0_low & ~PCI_BASE_ADDRESS_MEM_ATTR_MASK; + const bool is_saved_mem = + ((saved_bar0_low & PCI_BASE_ADDRESS_SPACE) == PCI_BASE_ADDRESS_SPACE_MEMORY); + const bool is_saved_64 = + ((saved_bar0_low & PCI_BASE_ADDRESS_MEM_LIMIT_MASK) == PCI_BASE_ADDRESS_MEM_LIMIT_64); + + if (cur_low != 0xffffffff && cur_base != 0) + return; + + if (!saved_bar0_low || saved_base == 0 || !is_saved_mem) + return; + + pci_write_config32(nvme_dev, PCI_BASE_ADDRESS_0, saved_bar0_low); + if (is_saved_64) + pci_write_config32(nvme_dev, PCI_BASE_ADDRESS_0 + 4, saved_bar0_high); +} + +static bool opal_s3_unlock_should_keep_armed(u32 rc) +{ + return rc == 1 || rc == 3; +} + +static u32 opal_s3_unlock_if_armed(void) +{ + struct opal_s3_state *st = opal_s3_get_state(); + u32 rc = 0; + uintptr_t scratch_base = 0; + size_t scratch_size = 0; + uintptr_t aligned_base = 0; + size_t aligned_size = 0; + bool keep_armed = false; + + if (st->signature != OPAL_S3_STATE_SIGNATURE || st->version != OPAL_S3_STATE_VERSION || + st->size != sizeof(*st)) + return 0x10; + + if (st->armed_state == OPAL_S3_ARMED_NONE) + return 0x11; + + if (st->armed_cycle != st->sleep_cycle) { + printk(BIOS_ERR, "OPAL: unlock rejected (invalid sequence)\n"); + opal_s3_clear_secret(); + return 0x12; + } + + smm_get_opal_s3_scratch_buffer(&scratch_base, &scratch_size); + if (!opal_s3_validate_scratch(scratch_base, scratch_size, &aligned_base, + &aligned_size)) { + printk(BIOS_ERR, "OPAL: scratch invariant failed at unlock\n"); + st->armed_state = OPAL_S3_ARMED_NONE; + st->armed_cycle = 0; + return 2; + } + + for (size_t i = 0; i < ARRAY_SIZE(st->secret); i++) { + struct opal_s3_secret *s = &st->secret[i]; + pci_devfn_t nvme_dev; + u32 one_rc; + + if (!s->valid) + continue; + if (s->password_len == 0 || s->password_len > OPAL_S3_MAX_PASSWORD_LEN) + continue; + if (s->unlocked_cycle == st->sleep_cycle) + continue; + + nvme_dev = PCI_DEV(s->bus, s->dev, s->func); + opal_s3_restore_nvme_bar0_if_needed(nvme_dev, s->nvme_bar0_low, + s->nvme_bar0_high); + + one_rc = 1; + for (int attempt = 0; attempt < OPAL_S3_UNLOCK_RETRIES; attempt++) { + if (attempt) + mdelay(OPAL_S3_UNLOCK_RETRY_DELAY_MS); + + one_rc = opal_nvme_opal_unlock(nvme_dev, s->base_comid, s->password, + s->password_len, + (void *)(uintptr_t)aligned_base, + aligned_size); + if (one_rc == 0) + break; + if (one_rc != 1) + break; + } + + if (one_rc == 0) { + s->unlocked_cycle = st->sleep_cycle; + continue; + } + + if (opal_s3_unlock_should_keep_armed(one_rc)) { + keep_armed = true; + if (rc == 0 || rc == 1) + rc = one_rc; + continue; + } + + if (rc == 0) + rc = one_rc; + + s->unlocked_cycle = st->sleep_cycle; + } + + if (!keep_armed) { + st->armed_state = OPAL_S3_ARMED_NONE; + st->armed_cycle = 0; + } + + return rc; +} + +int opal_s3_smi_apmc(u8 apmc) +{ + int node; + u64 rax; + u64 rbx; + u8 subcmd; + u32 ret; + u32 unlock_rc; + + switch (apmc) { + case APM_CNT_OPAL_S3_UNLOCK: + unlock_rc = opal_s3_unlock_if_armed(); + + node = get_apmc_node(apmc); + if (node >= 0) { + const u64 rax_out = unlock_rc; + (void)set_save_state_reg(RAX, node, (void *)&rax_out, sizeof(rax_out)); + } + return 1; + + case APM_CNT_OPAL_SVC: + break; + + default: + return 0; + } + + node = get_apmc_node(apmc); + if (node < 0) + return 0; + + if (get_save_state_reg(RAX, node, &rax, sizeof(rax)) < 0) + return 0; + if (get_save_state_reg(RBX, node, &rbx, sizeof(rbx)) < 0) + return 0; + + subcmd = (rax >> 8) & 0xff; + + switch (subcmd) { + case OPAL_SMM_SUBCMD_SET_SECRET: + ret = opal_s3_set_secret((const struct opal_s3_smm_ctx *)(uintptr_t)rbx); + break; + case OPAL_SMM_SUBCMD_CLEAR_SECRET: + ret = opal_s3_clear_secret(); + break; + default: + ret = 0xfffffffe; + break; + } + + { + const u64 rax_out = ret; + + (void)set_save_state_reg(RAX, node, (void *)&rax_out, sizeof(rax_out)); + } + return 1; +} + +void opal_s3_smi_sleep(u8 slp_typ) +{ + if (slp_typ == ACPI_S3) + opal_s3_arm_for_s3(); + else + opal_s3_clear_secret(); +} + +void opal_s3_smi_sleep_finalize(u8 slp_typ) +{ + if (slp_typ != ACPI_S3) + opal_s3_clear_secret(); +} diff --git a/src/soc/intel/common/block/smm/smihandler.c b/src/soc/intel/common/block/smm/smihandler.c index 58c980672c..fd72c25685 100644 --- a/src/soc/intel/common/block/smm/smihandler.c +++ b/src/soc/intel/common/block/smm/smihandler.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -146,6 +147,8 @@ void smihandler_southbridge_sleep( printk(BIOS_SPEW, "SMI#: SLP = 0x%08x\n", reg32); slp_typ = acpi_sleep_from_pm1(reg32); + opal_s3_smi_sleep(slp_typ); + /* Do any mainboard sleep handling */ mainboard_smi_sleep(slp_typ); @@ -205,6 +208,8 @@ void smihandler_southbridge_sleep( break; } + opal_s3_smi_sleep_finalize(slp_typ); + /* Allow mainboard to restore wake sources (e.g. for S5 WOL). */ mainboard_smi_sleep_finalize(slp_typ); @@ -387,6 +392,9 @@ void smihandler_southbridge_apmc( break; } + if (opal_s3_smi_apmc(reg8)) + return; + mainboard_smi_apmc(reg8); }