diff --git a/efi/preinstall/check_host_security_intel.go b/efi/preinstall/check_host_security_intel.go
index fb80293b..bedd4f92 100644
--- a/efi/preinstall/check_host_security_intel.go
+++ b/efi/preinstall/check_host_security_intel.go
@@ -279,9 +279,23 @@ func checkHostSecurityIntelBootGuard(env internal_efi.HostEnvironment) error {
return fmt.Errorf("cannot enumerate PCI devices with MEI class: %w", err)
}
if len(devices) == 0 {
- // We didn't find the PCI device, so indicate that this platform
- // isn't supported.
- return &UnsupportedPlatformError{errors.New("no MEI PCI device")}
+ // We didn't find the PCI device. This could be an Intel platform that
+ // is configured in High Assurance mode, so try this fallback using the
+ // BootGuard status MSR instead.
+ amd64Env, err := env.AMD64()
+ if err != nil {
+ return err
+ }
+ status, err := readIntelBootGuardStatus(amd64Env)
+ switch {
+ case errors.Is(err, internal_efi.ErrNoKernelMSRSupport):
+ return MissingKernelModuleError("msr")
+ case errors.Is(err, internal_efi.ErrNoMSRSupport):
+ return &UnsupportedPlatformError{errors.New("no MEI PCI device or BootGuard status MSR")}
+ case err != nil:
+ return fmt.Errorf("cannot read BootGuard status MSR: %w", err)
+ }
+ return checkHostSecurityIntelBootGuardMSR(status)
}
// We did find the PCI device, so indicate that we need the mei_me module
diff --git a/efi/preinstall/check_host_security_intel_btgmsr.go b/efi/preinstall/check_host_security_intel_btgmsr.go
new file mode 100644
index 00000000..d64a65bb
--- /dev/null
+++ b/efi/preinstall/check_host_security_intel_btgmsr.go
@@ -0,0 +1,53 @@
+//go:build amd64
+
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2026 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package preinstall
+
+import (
+ "errors"
+ "fmt"
+)
+
+// checkHostSecurityIntelBootGuardMSR checks the BootGuard configuration using the BootGuard status
+// MSR rather than the HFSTS registers. The MSR is mirrored by the startup ACM - it will contain
+// all zeroes if this didn't execute, in which case, BootGuard is not active.
+//
+// This has some limitations compared with using the HFSTS registers. Eg, it's not possible to
+// ensure that the system has properly transitioned out of manufacturing mode.
+func checkHostSecurityIntelBootGuardMSR(status bootGuardStatus) error {
+ if status&bootGuardCapability == 0 {
+ return &NoHardwareRootOfTrustError{errors.New("BootGuard ACM is not active")}
+ }
+
+ // Check the BootGuard profile.
+ profile, err := status.btgProfile()
+ if err != nil {
+ return &NoHardwareRootOfTrustError{fmt.Errorf("cannot determine BootGuard profile: %w", err)}
+ }
+ switch profile {
+ case btgProfileFVE, btgProfileFVME:
+ // We require verified boot, so the 2 profiles with forced
+ // verification are ok.
+ return nil
+ default:
+ return &NoHardwareRootOfTrustError{errors.New("unsupported BootGuard profile")}
+ }
+}
diff --git a/efi/preinstall/check_host_security_intel_btgmsr_test.go b/efi/preinstall/check_host_security_intel_btgmsr_test.go
new file mode 100644
index 00000000..0025edf7
--- /dev/null
+++ b/efi/preinstall/check_host_security_intel_btgmsr_test.go
@@ -0,0 +1,64 @@
+//go:build amd64
+
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2026 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package preinstall_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ . "github.com/snapcore/secboot/efi/preinstall"
+)
+
+type hostSecurityIntelBtgMSRSuite struct{}
+
+var _ = Suite(&hostSecurityIntelBtgMSRSuite{})
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRGoodFVMEProfile(c *C) {
+ c.Check(CheckHostSecurityIntelBootGuardMSR(0x000000030000007d), IsNil)
+}
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRGoodFVEProfile(c *C) {
+ c.Check(CheckHostSecurityIntelBootGuardMSR(0x000000030000005d), IsNil)
+}
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRErrInvalidProfile(c *C) {
+ err := CheckHostSecurityIntelBootGuardMSR(0x000000030000007c)
+ c.Check(err, ErrorMatches, `no hardware root-of-trust properly configured: cannot determine BootGuard profile: invalid profile`)
+ c.Check(err, FitsTypeOf, &NoHardwareRootOfTrustError{})
+}
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRErrUnsupportedNoFVMEProfile(c *C) {
+ err := CheckHostSecurityIntelBootGuardMSR(0x000000030000000c)
+ c.Check(err, ErrorMatches, `no hardware root-of-trust properly configured: unsupported BootGuard profile`)
+ c.Check(err, FitsTypeOf, &NoHardwareRootOfTrustError{})
+}
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRErrUnsupportedVMProfile(c *C) {
+ err := CheckHostSecurityIntelBootGuardMSR(0x000000030000006d)
+ c.Check(err, ErrorMatches, `no hardware root-of-trust properly configured: unsupported BootGuard profile`)
+ c.Check(err, FitsTypeOf, &NoHardwareRootOfTrustError{})
+}
+
+func (*hostSecurityIntelBtgMSRSuite) TestCheckHostSecurityIntelBootGuardMSRErrNoBtg(c *C) {
+ err := CheckHostSecurityIntelBootGuardMSR(0)
+ c.Check(err, ErrorMatches, `no hardware root-of-trust properly configured: BootGuard ACM is not active`)
+ c.Check(err, FitsTypeOf, &NoHardwareRootOfTrustError{})
+}
diff --git a/efi/preinstall/check_host_security_intel_csme11.go b/efi/preinstall/check_host_security_intel_csme11.go
index 94ece910..806cd6bb 100644
--- a/efi/preinstall/check_host_security_intel_csme11.go
+++ b/efi/preinstall/check_host_security_intel_csme11.go
@@ -123,6 +123,7 @@ func isInManufacturingModeCSME11(vers meVersion, regs hfstsRegistersCsme11) bool
return false
}
+// checkHostSecurityIntelBootGuardCSME11 checks the BootGuard configuration CSME versions 11 to 17.
func checkHostSecurityIntelBootGuardCSME11(vers meVersion, regs hfstsRegistersCsme11) error {
// These checks are based on the HSI checks performed in the pci-mei
// plugin in fwupd.
diff --git a/efi/preinstall/check_host_security_intel_csme18.go b/efi/preinstall/check_host_security_intel_csme18.go
index 69f144e9..4693e4e4 100644
--- a/efi/preinstall/check_host_security_intel_csme18.go
+++ b/efi/preinstall/check_host_security_intel_csme18.go
@@ -88,6 +88,7 @@ func isInManufacturingModeCSME18(regs hfstsRegistersCsme18) bool {
return false
}
+// checkHostSecurityIntelBootGuardCSME18 checks the BootGuard configuration CSME versions 18 onwards.
func checkHostSecurityIntelBootGuardCSME18(regs hfstsRegistersCsme18) error {
// These checks are based on the HSI checks performed in the pci-mei
// plugin in fwupd.
diff --git a/efi/preinstall/check_host_security_intel_test.go b/efi/preinstall/check_host_security_intel_test.go
index 27cedcda..81eb007e 100644
--- a/efi/preinstall/check_host_security_intel_test.go
+++ b/efi/preinstall/check_host_security_intel_test.go
@@ -378,6 +378,41 @@ func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardGoodFVMECSME
c.Check(err, IsNil)
}
+func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardNoMEDevice1(c *C) {
+ // Test the case where there are no mei devices, but we can
+ // fallback to using the bootguard status MSR.
+ env := efitest.NewMockHostEnvironmentWithOpts(
+ efitest.WithSysfsDevices(),
+ efitest.WithAMD64Environment("GenuineIntel", 0x6, nil, 4, map[uint32]uint64{0x13a: 0x000000030000007d}),
+ )
+ c.Check(CheckHostSecurityIntelBootGuard(env), IsNil)
+}
+
+func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardMEDevice2(c *C) {
+ // Test the case where the only mei device is not the required one.
+ device := efitest.NewMockSysfsDevice("/sys/devices/platform/intel_vsc/mei/mei0", map[string]string{"DEVNAME": "mei0"}, "mei", nil, efitest.NewMockSysfsDevice(
+ "/sys/devices/platform", map[string]string{"DRIVER": "intel_vsc"}, "platform", nil, nil,
+ ))
+ env := efitest.NewMockHostEnvironmentWithOpts(
+ efitest.WithSysfsDevices(device),
+ efitest.WithAMD64Environment("GenuineIntel", 0x6, nil, 4, map[uint32]uint64{0x13a: 0x000000030000007d}),
+ )
+ c.Check(CheckHostSecurityIntelBootGuard(env), IsNil)
+}
+
+func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardNoMEDevice3(c *C) {
+ // Test the case where there is a PCI mei device that isn't bound to the mei_me
+ // module. This probably isn't a realistic test case.
+ device := efitest.NewMockSysfsDevice("/sys/devices/pci0000:00/0000:00:16.0/mei/mei0", map[string]string{"DEVNAME": "mei0"}, "mei", nil, efitest.NewMockSysfsDevice(
+ "/sys/devices/pci0000:00:16:0", nil, "pci", nil, nil,
+ ))
+ env := efitest.NewMockHostEnvironmentWithOpts(
+ efitest.WithSysfsDevices(device),
+ efitest.WithAMD64Environment("GenuineIntel", 0x6, nil, 4, map[uint32]uint64{0x13a: 0x000000030000007d}),
+ )
+ c.Check(CheckHostSecurityIntelBootGuard(env), IsNil)
+}
+
func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardErrNoDevices(c *C) {
env := efitest.NewMockHostEnvironmentWithOpts()
err := CheckHostSecurityIntelBootGuard(env)
@@ -393,34 +428,23 @@ func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardErrNoMEModul
c.Check(err, Equals, MissingKernelModuleError("mei_me"))
}
-func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardErrNoMEDevice1(c *C) {
- // Test the case where there are no mei devices.
- env := efitest.NewMockHostEnvironmentWithOpts(efitest.WithSysfsDevices())
- err := CheckHostSecurityIntelBootGuard(env)
- c.Check(err, ErrorMatches, `unsupported platform: no MEI PCI device`)
- c.Check(err, FitsTypeOf, &UnsupportedPlatformError{})
-}
-
-func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardErrNoMEDevice2(c *C) {
- // Test the case where the only mei device is not the required one.
- device := efitest.NewMockSysfsDevice("/sys/devices/platform/intel_vsc/mei/mei0", map[string]string{"DEVNAME": "mei0"}, "mei", nil, efitest.NewMockSysfsDevice(
- "/sys/devices/platform", map[string]string{"DRIVER": "intel_vsc"}, "platform", nil, nil,
- ))
- env := efitest.NewMockHostEnvironmentWithOpts(efitest.WithSysfsDevices(device))
+func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardNoMEDeviceAndNoMSRModule(c *C) {
+ env := efitest.NewMockHostEnvironmentWithOpts(
+ efitest.WithSysfsDevices(),
+ efitest.WithAMD64Environment("GenuineIntel", 0x6, nil, 4, nil),
+ )
err := CheckHostSecurityIntelBootGuard(env)
- c.Check(err, ErrorMatches, `unsupported platform: no MEI PCI device`)
- c.Check(err, FitsTypeOf, &UnsupportedPlatformError{})
+ c.Check(err, ErrorMatches, `the kernel module "msr" must be loaded`)
+ c.Check(err, Equals, MissingKernelModuleError("msr"))
}
-func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardErrNoMEDevice3(c *C) {
- // Test the case where there is a PCI mei device that isn't bound to the mei_me
- // module. This probably isn't a realistic test case.
- device := efitest.NewMockSysfsDevice("/sys/devices/pci0000:00/0000:00:16.0/mei/mei0", map[string]string{"DEVNAME": "mei0"}, "mei", nil, efitest.NewMockSysfsDevice(
- "/sys/devices/pci0000:00:16:0", nil, "pci", nil, nil,
- ))
- env := efitest.NewMockHostEnvironmentWithOpts(efitest.WithSysfsDevices(device))
+func (s *hostSecurityIntelSuite) TestCheckHostSecurityIntelBootGuardNoMEDeviceAndNoBtgMSR(c *C) {
+ env := efitest.NewMockHostEnvironmentWithOpts(
+ efitest.WithSysfsDevices(),
+ efitest.WithAMD64Environment("GenuineIntel", 0x6, nil, 4, map[uint32]uint64{}),
+ )
err := CheckHostSecurityIntelBootGuard(env)
- c.Check(err, ErrorMatches, `unsupported platform: no MEI PCI device`)
+ c.Check(err, ErrorMatches, `unsupported platform: no MEI PCI device or BootGuard status MSR`)
c.Check(err, FitsTypeOf, &UnsupportedPlatformError{})
}
diff --git a/efi/preinstall/check_tpm_intel.go b/efi/preinstall/check_tpm_intel.go
index cda0a42d..1a5d9fe0 100644
--- a/efi/preinstall/check_tpm_intel.go
+++ b/efi/preinstall/check_tpm_intel.go
@@ -28,30 +28,8 @@ import (
internal_efi "github.com/snapcore/secboot/internal/efi"
)
-const bootGuardStatusMsr = 0x13a
-
-type bootGuardStatus uint64
-
-const (
- bootGuardStatusTPMShift = 1
- bootGuardStatusTPMMask bootGuardStatus = (3 << bootGuardStatusTPMShift)
-)
-
-type bootGuardTPMStatus uint64
-
-const (
- bootGuardTPMStatusNone bootGuardTPMStatus = 0
- bootGuardTPMStatus12 bootGuardTPMStatus = 1
- bootGuardTPMStatus2 bootGuardTPMStatus = 2
- bootGuardTPMStatusPTT bootGuardTPMStatus = 3
-)
-
-func (s bootGuardStatus) tpmStatus() bootGuardTPMStatus {
- return bootGuardTPMStatus(s&bootGuardStatusTPMMask) >> bootGuardStatusTPMShift
-}
-
func isTPMDiscreteFromIntelBootGuard(env internal_efi.HostEnvironmentAMD64) (bool, error) {
- msrValue, err := env.ReadMSRs(bootGuardStatusMsr)
+ status, err := readIntelBootGuardStatus(env)
switch {
case errors.Is(err, internal_efi.ErrNoKernelMSRSupport):
return false, MissingKernelModuleError("msr")
@@ -59,9 +37,6 @@ func isTPMDiscreteFromIntelBootGuard(env internal_efi.HostEnvironmentAMD64) (boo
return false, fmt.Errorf("cannot read BootGuard status MSR: %w", err)
}
- status := bootGuardStatus(msrValue[0])
-
- // NOTE: bootGuardStatus[0] is fine because BootGuard status MSR has the same value across all CPUs
switch status.tpmStatus() {
case bootGuardTPMStatusNone, bootGuardTPMStatus12:
// System has no TPM or unsupported TPM 1.2 device
diff --git a/efi/preinstall/export_amd64_test.go b/efi/preinstall/export_amd64_test.go
index 73903a4b..03de4c17 100644
--- a/efi/preinstall/export_amd64_test.go
+++ b/efi/preinstall/export_amd64_test.go
@@ -45,6 +45,7 @@ var (
CheckHostSecurityIntelBootGuard = checkHostSecurityIntelBootGuard
CheckHostSecurityIntelBootGuardCSME11 = checkHostSecurityIntelBootGuardCSME11
CheckHostSecurityIntelBootGuardCSME18 = checkHostSecurityIntelBootGuardCSME18
+ CheckHostSecurityIntelBootGuardMSR = checkHostSecurityIntelBootGuardMSR
CheckHostSecurityIntelCPUDebuggingLocked = checkHostSecurityIntelCPUDebuggingLocked
DetermineCPUVendor = determineCPUVendor
IsTPMDiscreteFromIntelBootGuard = isTPMDiscreteFromIntelBootGuard
diff --git a/efi/preinstall/intel_util.go b/efi/preinstall/intel_util.go
new file mode 100644
index 00000000..f044d1b9
--- /dev/null
+++ b/efi/preinstall/intel_util.go
@@ -0,0 +1,91 @@
+//go:build amd64
+
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2026 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package preinstall
+
+import (
+ "errors"
+
+ internal_efi "github.com/snapcore/secboot/internal/efi"
+)
+
+const bootGuardStatusMsr = 0x13a
+
+type bootGuardStatus uint64
+
+const (
+ bootGuardNEM bootGuardStatus = 1 << 0
+ bootGuardFACB bootGuardStatus = 1 << 4
+ bootGuardMeasuredBoot bootGuardStatus = 1 << 5
+ bootGuardVerifiedBoot bootGuardStatus = 1 << 6
+ bootGuardCapability bootGuardStatus = 1 << 32
+
+ bootGuardStatusTPMShift = 1
+ bootGuardStatusTPMMask bootGuardStatus = (3 << bootGuardStatusTPMShift)
+)
+
+type bootGuardTPMStatus uint64
+
+const (
+ bootGuardTPMStatusNone bootGuardTPMStatus = 0
+ bootGuardTPMStatus12 bootGuardTPMStatus = 1
+ bootGuardTPMStatus2 bootGuardTPMStatus = 2
+ bootGuardTPMStatusPTT bootGuardTPMStatus = 3
+)
+
+func (s bootGuardStatus) tpmStatus() bootGuardTPMStatus {
+ return bootGuardTPMStatus(s&bootGuardStatusTPMMask) >> bootGuardStatusTPMShift
+}
+
+func (s bootGuardStatus) btgProfile() (btgProfile, error) {
+ // We can't check the error enforcement policy here, either because it's
+ // not mirrored into the MSR, or it is but we don't know which 2 bits to
+ // use. We just interpret the other bits into one of the well known No_FVME,
+ // VM, FVE and FVME profiles. This is ok because there are no known profiles
+ // where these bits are reused with different error enforcement policies.
+ f := s&bootGuardFACB > 0
+ v := s&bootGuardVerifiedBoot > 0
+ m := s&bootGuardMeasuredBoot > 0
+ p := s&bootGuardNEM > 0
+
+ switch {
+ case !f && !v && !m && !p:
+ return btgProfileNoFVME, nil
+ case !f && v && m && p:
+ return btgProfileVM, nil
+ case f && v && !m && p:
+ return btgProfileFVE, nil
+ case f && v && m && p:
+ return btgProfileFVME, nil
+ default:
+ return 0, errors.New("invalid profile")
+ }
+}
+
+func readIntelBootGuardStatus(env internal_efi.HostEnvironmentAMD64) (bootGuardStatus, error) {
+ msrValue, err := env.ReadMSRs(bootGuardStatusMsr)
+ if err != nil {
+ return 0, err
+ }
+
+ // NOTE: msrValue[0] is fine because BootGuard status MSR has the same value across all CPUs
+ return bootGuardStatus(msrValue[0]), nil
+}