VYPR
High severity7.6NVD Advisory· Published Mar 18, 2026· Updated Apr 28, 2026

CVE-2026-32606

CVE-2026-32606

Description

IncusOS is an immutable OS image dedicated to running Incus. Prior to 202603142010, the default configuration of systemd-cryptenroll as used by IncusOS through mkosi allows for an attacker with physical access to the machine to access the encrypted data without requiring any interaction by the system's owner or any tampering of Secure Boot state or kernel (UKI) boot image. That's because in this configuration, the LUKS key is made available by the TPM so long as the system has the expected PCR7 value and the PCR11 policy matches. That default PCR11 policy importantly allows for the TPM to release the key to the booted system rather than just from the initrd part of the signed kernel image (UKI). The attack relies on the attacker being able to substitute the original encrypted root partition for one that they control. By doing so, the system will prompt for a recovery key on boot, which the attacker has defined and can provide, before booting the system using the attacker's root partition rather than the system's original one. The attacker only needs to put a systemd unit starting on system boot within their root partition to have the system run that logic on boot. That unit will then run in an environment where the TPM will allow for the retrieval of the encryption key of the real root disk, allowing the attacker to steal the LUKS volume key (immutable master key) and then use it against the real root disk, altering it or getting data out before putting the disk back the way it was and returning the system without a trace of this attack having happened. This is all possible because the system will have still booted with Secure Boot enabled, will have measured and ran the expected bootloader and kernel image (UKI). The initrd selects the root disk based on GPT partition identifiers making it possible to easily substitute the real root disk for an attacker controlled one. This doesn't lead to any change in the TPM state and therefore allows for retrieval of the LUKS key by the attacker through a boot time systemd unit on their alternative root partition. IncusOS version 202603142010 (2026/03/14 20:10 UTC) includes the new PCR15 logic and will automatically update the TPM policy on boot. Anyone suspecting that their system may have been physically accessed while shut down should perform a full system wipe and reinstallation as only that will rotate the LUKS volume key and prevent subsequent access to the encrypted data should the system have been previously compromised. There are no known workarounds other than updating to a version with corrected logic which will automatically rebind the LUKS keys to the new set of TPM registers and prevent this from being exploited.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/lxc/incus-os/incus-osdGo
< 0.0.0-20260313012803-e3b35f230d230.0.0-20260313012803-e3b35f230d23

Affected products

1

Patches

1
e3b35f230d23

Merge pull request #954 from gibmat/adjust-pcr-binding-logic

https://github.com/lxc/incus-osStéphane GraberMar 13, 2026via ghsa
22 files changed · +317 55
  • doc/reference/security.md+6 1 modified
    @@ -69,7 +69,7 @@ KEK, db, and dbx updates are signed offline and then made available via a Provid
     - Each `.auth` file is signed by a KEK certificate already enrolled on the machine IncusOS is running on. If the file is tampered with, enrollment will fail, so there is no special need to protect or checksum received updates.
     
     ## Use of TPM PCRs
    -IncusOS relies on three PCRs (4, 7 & 11) to bind disk encryption keys.
    +IncusOS relies on four PCRs (4, 7, 11 & 15) to bind disk encryption keys.
     
     ### PCR 4
     
    @@ -103,6 +103,11 @@ IncusOS only ever needs to worry about re-binding PCR 11 when the Secure Boot ke
     - Changing the signature on the `systemd-boot` stub will affect the PCR 7 value at next boot, so follow the steps outlined above to predict the new PCR 7 value.
     - Re-bind the TPM PCR 11 policies with the new signing certificate and predicted PCR 7 value. Doing this invalidates the current TPM state, so we must rely on a recovery key known to IncusOS to update the LUKS header. The update is performed in as an atomic process as possible, to prevent having the LUKS header in a state where it doesn't have a TPM enrolled.
     
    +### PCR 15
    +PCR 15 is populated by systemd at various points during the boot process with system identity events, such as when the LUKS root volume is unlocked, the machine ID, and basic information about the root (`/`) file system. IncusOS binds its encryption to an uninitialized PCR 15 (all zeros), which allows for automatic decryption by the TPM in the initrd only. As part of opening the LUKS root volume in the initrd, systemd extends PCR 15 which changes its value. This prevents the TPM from unlocking the LUKS volume again once the system has booted.
    +
    +IncusOS depends on PCR 15 to thwart the attack described in a [blog post](https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/) by oddlama. In the future, improvements in the `mkosi` and `ukify` build tools may allow for more targeted PCR 11 policies to be generated which would only be valid while in the initrd. This would remove the need to depend on PCR 15.
    +
     ### Implications
     Any unexpected change to PCR values will cause auto-unlock to fail, and require the entry of a recovery password to boot the system. Such a change is a **strong** indication that something unexpected occurred to the system's security configuration outside of what IncusOS expected. It could be triggered by an innocuous change, or could point to malicious activity. In either case, be careful before blindly entering a recovery password at boot.
     
    
  • doc/.wordlist.txt+2 0 modified
    @@ -46,6 +46,7 @@ hugepages
     IMG
     Incus
     IncusOS
    +initrd
     IPs
     IPv
     iSCSI
    @@ -74,6 +75,7 @@ NICs
     NTP
     NVMe
     OCI
    +oddlama
     OEM
     OVMF
     OVN
    
  • incus-osd/cmd/incus-osd/main.go+56 0 modified
    @@ -423,6 +423,62 @@ func startup(ctx context.Context, s *state.State, t *tui.TUI) error { //nolint:r
     		}
     	}
     
    +	// Check if the root and swap partitions include a binding on PCR15. If not, update the LUKS bindings before proceeding.
    +	// This is required to counter the attack described at https://oddlama.org/blog/bypassing-disk-encryption-with-tpm2-unlock/.
    +	//
    +	// Binding to exact values of PCRs 4+7 and a PCR11 policy are insufficient when an attacker has physical access to the system
    +	// and can create a malicious root partition. Because the system will boot with an unmodified UKI and SecureBoot/TPM state,
    +	// after the system exits the initrd IncusOS will behave like it's a first boot, but more critically the TPM will be in a
    +	// known "good" state and happily release its encryption key used by LUKS allowing the attacker to trivially extract the
    +	// LUKS volume key. They can then undo their malicious changes to the disk, and IncusOS wlll have no idea an attack occurred
    +	// while the attacker now can decrypt the LUKS volumes at any time to exfiltrate data, mess with the system, etc.
    +	//
    +	// We add a binding to an empty PCR15. This PCR is extended when a root LUKS volume is successfully opened in the initrd, so the
    +	// only time the TPM state could automatically unlock things for us is at the beginning of the initrd. After that point, PCR15
    +	// will have a different value which cannot be reset, rendering the attack impossible.
    +	//
    +	// Performing the check and PCR binding update here catches both fresh installs as well as existing deployments. In either case,
    +	// the TPM state will allow a single re-bind, after which it will only work in the initrd. At some point after September 2026
    +	// we can move this logic into the recovery key generation block above so only fresh installs are inspected. systemd v259 did
    +	// add a TPM2PCRs= option to systemd-repart which would also make life easier.
    +	luksVolumes, err := util.GetLUKSVolumePartitions(ctx)
    +	if err != nil {
    +		return err
    +	}
    +
    +	isBoundPCR15, err := storage.LUKSBoundToPCR(ctx, luksVolumes["root"], 15)
    +	if err != nil {
    +		return err
    +	}
    +
    +	if !isBoundPCR15 {
    +		slog.InfoContext(ctx, "Upgrading LUKS TPM PCR bindings, this may take a few seconds")
    +
    +		err := secureboot.HandleSecureBootKeyChange(ctx, fmt.Sprintf("/boot/EFI/Linux/%s_%s.efi", s.OS.Name, s.OS.RunningRelease), "")
    +		if err != nil {
    +			return err
    +		}
    +	}
    +
    +	// Enable swap, if present. Swap isn't normally activated until after exiting the initrd, and because we're not able to
    +	// rely on the TPM to automatically unlock that partition, systemd cannot enable swap for us during system boot.
    +	_, err = os.Stat("/dev/mapper/swap")
    +	if err != nil && os.IsNotExist(err) {
    +		_, err := os.Stat("/dev/disk/by-partlabel/swap")
    +		if err == nil {
    +			// Unlock the LUKS swap volume.
    +			_, err := subprocess.RunCommandContext(ctx, "systemd-cryptsetup", "attach", "swap", "/dev/disk/by-partlabel/swap", "/var/lib/incus-os/recovery.swap.key")
    +			if err != nil {
    +				slog.WarnContext(ctx, "Unable to decrypt LUKS swap partition")
    +			} else {
    +				_, err := subprocess.RunCommandContext(ctx, "swapon", "/dev/mapper/swap")
    +				if err != nil {
    +					slog.WarnContext(ctx, "Unable to activate encrypted swap partition")
    +				}
    +			}
    +		}
    +	}
    +
     	// Get the machine ID.
     	machineID, err := s.MachineID()
     	if err != nil {
    
  • incus-osd/cmd/incusos-initrd-utils/main.go+24 0 modified
    @@ -30,6 +30,8 @@ func main() {
     		switch os.Args[1] {
     		case "measure-pcrs":
     			err = measurePCRs()
    +		case "seal-pcr15":
    +			err = sealPCR15()
     		case "validate-pe-binaries":
     			err = secureboot.ValidatePEBinaries()
     			if err != nil && os.IsNotExist(err) {
    @@ -109,3 +111,25 @@ func measurePCRs() error {
     
     	return nil
     }
    +
    +// sealPCR15 is used when running swtpm to extend PCR15 with a static value so it is
    +// initialized while in the initrd. Normally, this is done automatically by systemd-cryptsetup
    +// after unlocking the root LUKS volume.
    +func sealPCR15() error {
    +	// Open the TPM.
    +	tpmDev, err := tpm2.OpenTPM("/dev/tpm0")
    +	if err != nil {
    +		return fmt.Errorf("can't open TPM: %s", err.Error())
    +	}
    +	defer tpmDev.Close()
    +
    +	// Measure a static value into PCR15.
    +	h := sha256.Sum256([]byte("IncusOS"))
    +
    +	err = tpm2.PCRExtend(tpmDev, tpmutil.Handle(15), tpm2.AlgSHA256, h[:], "")
    +	if err != nil {
    +		return err
    +	}
    +
    +	return nil
    +}
    
  • incus-osd/internal/install/install.go+6 2 modified
    @@ -167,8 +167,12 @@ func CheckSystemRequirements(ctx context.Context, t *tui.TUI) error { //nolint:r
     		// Sanity check: If the "IncusOSInstallComplete" UEFI variable is set, that means an IncusOS install
     		// completed successfully but incus-osd hasn't yet cleared this UEFI variable on its first boot. This
     		// means we've likely accidentally booted from the install media rather than the newly installed system.
    -		_, err = os.Stat("/sys/firmware/efi/efivars/IncusOSInstallComplete-12f075e0-2d07-493d-811a-00920a72c04c")
    -		if err == nil {
    +		contents, err := secureboot.ReadEFIVariable("IncusOSInstallComplete")
    +		if err != nil {
    +			return err
    +		}
    +
    +		if len(contents) != 0 {
     			return errors.New("install media detected, but the system is already installed; please remove USB/CDROM and reboot the system")
     		}
     
    
  • incus-osd/internal/secureboot/efi_vars.go+9 5 modified
    @@ -39,7 +39,7 @@ func GetCertificatesFromVar(varName string) ([]*x509.Certificate, error) {
     	if sbEnabled {
     		// In normal operation, Secure Boot will be enabled and we can
     		// directly fetch certificates from a trusted EFI variable.
    -		val, err := readEFIVariable(varName)
    +		val, err := ReadEFIVariable(varName)
     		if err != nil {
     			return nil, err
     		}
    @@ -324,15 +324,15 @@ func appendEFIVarUpdate(ctx context.Context, efiUpdateFile string, varName strin
     	}
     
     	// Update the LUKS-encrypted volumes to use the new PCR7 value.
    -	newPCR7String := hex.EncodeToString(newPCR7)
    +	pcrBindingArg := "--tpm2-pcrs=7:sha256=" + hex.EncodeToString(newPCR7) + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     
     	luksVolumes, err := util.GetLUKSVolumePartitions(ctx)
     	if err != nil {
     		return err
     	}
     
     	for name, volume := range luksVolumes {
    -		_, err = subprocess.RunCommandContext(ctx, "systemd-cryptenroll", "--unlock-key-file=/var/lib/incus-os/recovery."+name+".key", "--tpm2-device=auto", "--wipe-slot=tpm2", "--tpm2-pcrlock=", "--tpm2-pcrs=7:sha256="+newPCR7String, volume)
    +		_, err = subprocess.RunCommandContext(ctx, "systemd-cryptenroll", "--unlock-key-file=/var/lib/incus-os/recovery."+name+".key", "--tpm2-device=auto", "--wipe-slot=tpm2", "--tpm2-pcrlock=", pcrBindingArg, volume)
     		if err != nil {
     			return err
     		}
    @@ -408,8 +408,8 @@ func checkDbxUpdateWouldBrickUKI(dbxFilePath string) error {
     	return nil
     }
     
    -// readEFIVariable returns the current value, if any, of the specified EFI variable.
    -func readEFIVariable(variableName string) ([]byte, error) {
    +// ReadEFIVariable returns the current value, if any, of the specified EFI variable.
    +func ReadEFIVariable(variableName string) ([]byte, error) {
     	// Determine which file to open.
     	filename, err := efiVariableToFilename(variableName)
     	if err != nil {
    @@ -469,6 +469,10 @@ func efiVariableToFilename(variableName string) (string, error) {
     		return "/sys/firmware/efi/efivars/dbx-d719b2cb-3d3a-4596-a3bc-dad00e67656f", nil
     	case "LoaderEntrySelected":
     		return "/sys/firmware/efi/efivars/LoaderEntrySelected-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f", nil
    +	case "IncusOSInstallComplete":
    +		return "/sys/firmware/efi/efivars/IncusOSInstallComplete-12f075e0-2d07-493d-811a-00920a72c04c", nil
    +	case "IncusOSTPMState":
    +		return "/sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c", nil
     	default:
     		return "", fmt.Errorf("unsupported EFI variable '%s'", variableName)
     	}
    
  • incus-osd/internal/secureboot/event_log.go+2 2 modified
    @@ -238,7 +238,7 @@ func SynthesizeTPMEventLog() ([]byte, error) {
     
     		switch e.header.eventType { //nolint:exhaustive
     		case tcg.EFIVariableDriverConfig, tcg.EFIVariableAuthority:
    -			contents, err = readEFIVariable(e.name)
    +			contents, err = ReadEFIVariable(e.name)
     			if err != nil {
     				return nil, err
     			}
    @@ -415,7 +415,7 @@ func getSigningCertBytes(contents []byte) ([]byte, error) {
     // Determine what UKI was booted, so we can compute the proper PCR11 values.
     func getUKIImage() (string, error) {
     	// Use the EFI variable LoaderEntrySelected to determine what UKI was booted.
    -	rawUKIName, err := readEFIVariable("LoaderEntrySelected")
    +	rawUKIName, err := ReadEFIVariable("LoaderEntrySelected")
     	if err != nil {
     		return "", err
     	}
    
  • incus-osd/internal/secureboot/pcrs.go+13 14 modified
    @@ -43,18 +43,17 @@ func ForceUpdatePCRBindings(ctx context.Context, osName string, osVersion string
     		return err
     	}
     
    -	atLeastOneVolumeNeedsFixing := false
    -
    -	for volumeName, volumeDev := range luksVolumes {
    -		_, err = subprocess.RunCommandContext(ctx, "cryptsetup", "luksOpen", "--test-passphrase", volumeDev, volumeName)
    -		if err != nil {
    -			atLeastOneVolumeNeedsFixing = true
    -
    -			break
    -		}
    +	// Get the state of the TPM as recorded in the UEFI variable.
    +	tpmStateContents, err := ReadEFIVariable("IncusOSTPMState")
    +	if err != nil {
    +		return err
     	}
     
    -	if !atLeastOneVolumeNeedsFixing {
    +	// If there are four bytes in the UEFI variable and the last is zero, that means the TPM
    +	// was able to unlock things automatically in the initrd.
    +	tpmStateIsGood := len(tpmStateContents) == 4 && tpmStateContents[3] == 0
    +
    +	if tpmStateIsGood {
     		return errors.New("refusing to reset TPM encryption bindings because current state can unlock all volumes")
     	}
     
    @@ -88,11 +87,11 @@ func ForceUpdatePCRBindings(ctx context.Context, osName string, osVersion string
     	pcr4String := hex.EncodeToString(pcr4)
     	pcr7String := hex.EncodeToString(pcr7)
     
    -	pcrBindingArg := "--tpm2-pcrs=7:sha256=" + pcr7String
    +	pcrBindingArg := "--tpm2-pcrs=7:sha256=" + pcr7String + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     
     	// When Secure Boot is disabled, we also bind to PCR4.
     	if !sbEnabled {
    -		pcrBindingArg = "--tpm2-pcrs=4:sha256=" + pcr4String + "+7:sha256=" + pcr7String
    +		pcrBindingArg = "--tpm2-pcrs=4:sha256=" + pcr4String + "+7:sha256=" + pcr7String + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     	}
     
     	// Handle an edge case where the system boots with a recovery passphrase, but hasn't yet been
    @@ -140,7 +139,7 @@ func ForceUpdatePCRBindings(ctx context.Context, osName string, osVersion string
     				return err
     			}
     
    -			pcrRandomBindingArg := "--tpm2-pcrs=7:sha256=" + hex.EncodeToString(randomPCR)
    +			pcrRandomBindingArg := "--tpm2-pcrs=7:sha256=" + hex.EncodeToString(randomPCR) + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     
     			if luksKey == "" {
     				// Set a bad PCR policy.
    @@ -405,7 +404,7 @@ func computeExpectedVariableDriverConfig(rawBuf []byte) ([]byte, error) {
     	}
     
     	// Read the current variable.
    -	buf, err := readEFIVariable(v.VarName())
    +	buf, err := ReadEFIVariable(v.VarName())
     	if err != nil {
     		return nil, err
     	}
    
  • incus-osd/internal/secureboot/secureboot.go+12 8 modified
    @@ -32,7 +32,7 @@ var systemdStubGUID = [16]byte{0xf8, 0xd1, 0xc5, 0x55, 0xcd, 0x4, 0xb5, 0x46, 0x
     
     // Enabled checks if Secure Boot is currently enabled.
     func Enabled() (bool, error) {
    -	state, err := readEFIVariable("SecureBoot")
    +	state, err := ReadEFIVariable("SecureBoot")
     	if err != nil {
     		return false, err
     	}
    @@ -48,7 +48,7 @@ func Enabled() (bool, error) {
     // this shouldn't be possible when Secure Boot is enabled, but buggy UEFI
     // implementations can allow this.
     func InAuditMode() (bool, error) {
    -	state, err := readEFIVariable("AuditMode")
    +	state, err := ReadEFIVariable("AuditMode")
     	if err != nil {
     		return false, err
     	}
    @@ -96,9 +96,11 @@ func HandleSecureBootKeyChange(ctx context.Context, ukiFile string, usrImageFile
     	}
     
     	// Part 2 -- Update the systemd-boot EFI stub.
    -	err = updateEFIBootStub(ctx, usrImageFile)
    -	if err != nil {
    -		return err
    +	if usrImageFile != "" {
    +		err := updateEFIBootStub(ctx, usrImageFile)
    +		if err != nil {
    +			return err
    +		}
     	}
     
     	// Part 3 -- Compute the new PCR4 and PCR7 values.
    @@ -121,11 +123,11 @@ func HandleSecureBootKeyChange(ctx context.Context, ukiFile string, usrImageFile
     	newPCR4String := hex.EncodeToString(newPCR4)
     	newPCR7String := hex.EncodeToString(newPCR7)
     
    -	pcrBindingArg := "--tpm2-pcrs=7:sha256=" + newPCR7String
    +	pcrBindingArg := "--tpm2-pcrs=7:sha256=" + newPCR7String + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     
     	// When Secure Boot is disabled, we also bind to PCR4.
     	if !sbEnabled {
    -		pcrBindingArg = "--tpm2-pcrs=4:sha256=" + newPCR4String + "+7:sha256=" + newPCR7String
    +		pcrBindingArg = "--tpm2-pcrs=4:sha256=" + newPCR4String + "+7:sha256=" + newPCR7String + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
     	}
     
     	luksVolumes, err := util.GetLUKSVolumePartitions(ctx)
    @@ -192,8 +194,10 @@ func UpdatePCR4Binding(ctx context.Context, ukiFile string) error {
     		return err
     	}
     
    +	pcrBindingArg := "--tpm2-pcrs=4:sha256=" + newPCR4String + "+7:sha256=" + pcr7String + "+15:sha256=0000000000000000000000000000000000000000000000000000000000000000"
    +
     	for name, volume := range luksVolumes {
    -		_, err := subprocess.RunCommandContext(ctx, "systemd-cryptenroll", "--unlock-key-file=/var/lib/incus-os/recovery."+name+".key", "--tpm2-device=auto", "--wipe-slot=tpm2", "--tpm2-pcrlock=", "--tpm2-pcrs=4:sha256="+newPCR4String+"+7:sha256="+pcr7String, volume)
    +		_, err := subprocess.RunCommandContext(ctx, "systemd-cryptenroll", "--unlock-key-file=/var/lib/incus-os/recovery."+name+".key", "--tpm2-device=auto", "--wipe-slot=tpm2", "--tpm2-pcrlock=", pcrBindingArg, volume)
     		if err != nil {
     			return err
     		}
    
  • incus-osd/internal/storage/luks.go+36 0 modified
    @@ -3,18 +3,27 @@ package storage
     import (
     	"context"
     	"encoding/base64"
    +	"encoding/json"
     	"errors"
     	"fmt"
     	"log/slog"
     	"os"
     	"path/filepath"
    +	"slices"
     	"strings"
     
     	"github.com/lxc/incus/v6/shared/subprocess"
     
     	"github.com/lxc/incus-os/incus-osd/internal/util"
     )
     
    +type cryptsetupLuksDumpPartialParse struct {
    +	Tokens map[string]struct {
    +		Type     string `json:"type"`
    +		TPM2PCRS []int  `json:"tpm2-pcrs"` //nolint:tagliatelle
    +	} `json:"tokens"`
    +}
    +
     // EncryptDrive wipes and formats a drive as a LUKS device.
     func EncryptDrive(ctx context.Context, devPath string) error {
     	if !strings.HasPrefix(devPath, "/dev/disk/by-id/") {
    @@ -148,6 +157,33 @@ func GetDriveKeys() (map[string]string, error) {
     	return keys, nil
     }
     
    +// LUKSBoundToPCR determines if the given LUKS volume is bound to the specified PCR.
    +func LUKSBoundToPCR(ctx context.Context, devPath string, pcrIndex int) (bool, error) {
    +	output, err := subprocess.RunCommandContext(ctx, "cryptsetup", "luksDump", "--dump-json-metadata", devPath)
    +	if err != nil {
    +		return false, err
    +	}
    +
    +	state := cryptsetupLuksDumpPartialParse{}
    +
    +	err = json.Unmarshal([]byte(output), &state)
    +	if err != nil {
    +		return false, err
    +	}
    +
    +	for _, token := range state.Tokens {
    +		if token.Type != "systemd-tpm2" {
    +			continue
    +		}
    +
    +		if slices.Contains(token.TPM2PCRS, pcrIndex) {
    +			return true, nil
    +		}
    +	}
    +
    +	return false, nil
    +}
    +
     func unlockDrive(ctx context.Context, devPath string) error {
     	devName := filepath.Base(devPath)
     	keyfilePath := "/var/lib/incus-os/luks." + devName + ".key"
    
  • incus-osd/internal/systemd/cryptenroll.go+12 5 modified
    @@ -207,7 +207,17 @@ func ListEncryptedVolumes(ctx context.Context) ([]api.SystemSecurityEncryptedVol
     		return ret, err
     	}
     
    -	for volumeName, volumeDev := range luksVolumes {
    +	// Get the state of the TPM as recorded in the UEFI variable.
    +	tpmStateContents, err := secureboot.ReadEFIVariable("IncusOSTPMState")
    +	if err != nil {
    +		return ret, err
    +	}
    +
    +	// If there are four bytes in the UEFI variable and the last is zero, that means the TPM
    +	// was able to unlock things automatically in the initrd.
    +	tpmStateIsGood := len(tpmStateContents) == 4 && tpmStateContents[3] == 0
    +
    +	for volumeName := range luksVolumes {
     		// First, check if the volume is mapped, and therefore unlocked.
     		_, err := subprocess.RunCommandContext(ctx, "dmsetup", "info", volumeName)
     		if err != nil {
    @@ -220,10 +230,7 @@ func ListEncryptedVolumes(ctx context.Context) ([]api.SystemSecurityEncryptedVol
     		}
     
     		// Second, test if we can auto-unlock with the current TPM state.
    -		// Ideally we wouldn't have to depend on cryptsetup, but systemd-cryptenroll (and friends) don't
    -		// seem to have an equivalent of "--test-passphrase".
    -		_, err = subprocess.RunCommandContext(ctx, "cryptsetup", "luksOpen", "--test-passphrase", volumeDev, volumeName)
    -		if err != nil {
    +		if !tpmStateIsGood {
     			// Do we have a PCR mismatch on the TPM? If so, assume we can unlock with the TPM upon reboot.
     			if secureboot.TPMStatus() == secureboot.TPMPCRMismatch {
     				ret = append(ret, api.SystemSecurityEncryptedVolume{
    
  • incus-osd/tests/incusos_tests/tests_incusos_api_system_security.py+2 0 modified
    @@ -64,6 +64,8 @@ def TestIncusOSAPISystemSecurityTPMRebind(install_image):
                 raise IncusOSException("unexpected status code %d: %s" % (result["status_code"], result["error"]))
     
             # This is a sledgehammer approach, but fine for the test. :)
    +        vm.RunCommand("chattr", "-i", "/sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c")
    +        vm.RunCommand("rm", "/sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c")
             vm.RunCommand("tpm2_clear")
     
             # Now we expect TPM rebinding to work.
    
  • incus-osd/tests/incusos_tests/tests_incusos_live.py+28 6 modified
    @@ -27,6 +27,15 @@ def TestIncusOSLive(install_image):
             # Shouldn't see any mention of a degraded security state
             vm.LogDoesntContain("incus-osd", "Degraded security state:")
     
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
     def TestIncusOSLiveSWTPM(install_image):
         test_name = "incusos-live-swtpm"
         test_seed = None
    @@ -58,7 +67,7 @@ def TestIncusOSLiveSWTPM(install_image):
             vm.WaitExpectedLog("incus-osd", "Downloading application update application=incus version="+incusos_version)
             vm.WaitExpectedLog("incus-osd", "System is ready version="+incusos_version)
     
    -        # Check some PCR values: expect PCR0 to be empty with swtpm, while PCR7 and PCR11 should have non-zero values
    +        # Check some PCR values: expect PCR0 to be empty with swtpm, while PCR7, PCR11, and PCR15 should have non-zero values
             result = vm.RunCommand("tpm2_pcrread", "sha256:0")
             if "0x0000000000000000000000000000000000000000000000000000000000000000" not in str(result.stdout):
                 raise IncusOSException("PCR0 has a non-zero value")
    @@ -71,6 +80,19 @@ def TestIncusOSLiveSWTPM(install_image):
             if "0x0000000000000000000000000000000000000000000000000000000000000000" in str(result.stdout):
                 raise IncusOSException("PCR11 isn't initialized")
     
    +        result = vm.RunCommand("tpm2_pcrread", "sha256:15")
    +        if "0x0000000000000000000000000000000000000000000000000000000000000000" in str(result.stdout):
    +            raise IncusOSException("PCR15 isn't initialized")
    +
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
     def TestIncusOSLiveNoSecureBoot(install_image):
         test_name = "incusos-live-no-secure-boot"
         test_seed = None
    @@ -94,11 +116,11 @@ def TestIncusOSLiveNoSecureBoot(install_image):
             vm.WaitExpectedLog("incus-osd", "Downloading application update application=incus version="+incusos_version)
             vm.WaitExpectedLog("incus-osd", "System is ready version="+incusos_version)
     
    -        # Verify that LUKS encryption is bound to PCRs 4+7+11
    +        # Verify that LUKS encryption is bound to PCRs 4+7+11+15
             result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part9")
    -        if "tpm2-hash-pcrs:   4+7" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    -            raise IncusOSException("LUKS swap partition not properly bound to PCRs 4+7+11")
    +        if "tpm2-hash-pcrs:   4+7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 4+7+11+15")
     
             result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_live--image-part10")
    -        if "tpm2-hash-pcrs:   4+7" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    -            raise IncusOSException("LUKS root partition not properly bound to PCRs 4+7+11")
    +        if "tpm2-hash-pcrs:   4+7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 4+7+11+15")
    
  • incus-osd/tests/incusos_tests/tests_install_secureboot_disabled.py+5 5 modified
    @@ -22,14 +22,14 @@ def TestInstallSecureBootDisabled(install_image):
             if "Variable PK has no entries" not in str(result.stdout) or "Variable db has no entries" not in str(result.stdout):
                 raise IncusOSException("SecureBoot EFI variables shouldn't be populated")
     
    -        # Verify that LUKS encryption is bound to PCRs 4+7+11
    +        # Verify that LUKS encryption is bound to PCRs 4+7+11+15
             result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part9")
    -        if "tpm2-hash-pcrs:   4+7" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    -            raise IncusOSException("LUKS swap partition not properly bound to PCRs 4+7+11")
    +        if "tpm2-hash-pcrs:   4+7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 4+7+11+15")
     
             result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part10")
    -        if "tpm2-hash-pcrs:   4+7" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    -            raise IncusOSException("LUKS root partition not properly bound to PCRs 4+7+11")
    +        if "tpm2-hash-pcrs:   4+7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 4+7+11+15")
     
             # Verify Secure Boot being disabled is reflected in security state
             result = vm.APIRequest("/1.0/system/security")
    
  • incus-osd/tests/incusos_tests/tests_install_smoke.py+36 0 modified
    @@ -37,6 +37,15 @@ def TestBaselineInstall(install_image):
             # Shouldn't see any mention of a degraded security state
             vm.LogDoesntContain("incus-osd", "Degraded security state:")
     
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
     def TestBaselineInstallReadonlyImage(install_image):
         test_name = "baseline-install-readonly-image"
         test_seed = {
    @@ -51,6 +60,15 @@ def TestBaselineInstallReadonlyImage(install_image):
             # Shouldn't see any mention of a degraded security state
             vm.LogDoesntContain("incus-osd", "Degraded security state:")
     
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
     def TestBaselineInstallNVME(install_image):
         test_name = "baseline-install-nvme"
         test_seed = {
    @@ -67,6 +85,15 @@ def TestBaselineInstallNVME(install_image):
             # Shouldn't see any mention of a degraded security state
             vm.LogDoesntContain("incus-osd", "Degraded security state:")
     
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_incus_root-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_incus_root-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
     def TestBaselineInstallNVMEReadonlyImage(install_image):
         test_name = "baseline-install-nvme-readonly-image"
         test_seed = {
    @@ -82,3 +109,12 @@ def TestBaselineInstallNVMEReadonlyImage(install_image):
     
             # Shouldn't see any mention of a degraded security state
             vm.LogDoesntContain("incus-osd", "Degraded security state:")
    +
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_incus_root-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/nvme-QEMU_NVMe_Ctrl_incus_root-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    
  • incus-osd/tests/incusos_tests/tests_install_swtpm.py+14 1 modified
    @@ -18,7 +18,7 @@ def TestInstallUseSWTPM(install_image):
             # Should see a log message about swtpm
             vm.WaitExpectedLog("incus-osd", "Degraded security state: no physical TPM found, using swtpm")
     
    -        # Check some PCR values: expect PCR0 to be empty with swtpm, while PCR7 and PCR11 should have non-zero values
    +        # Check some PCR values: expect PCR0 to be empty with swtpm, while PCR7, PCR11, and PCR15 should have non-zero values
             result = vm.RunCommand("tpm2_pcrread", "sha256:0")
             if "0x0000000000000000000000000000000000000000000000000000000000000000" not in str(result.stdout):
                 raise IncusOSException("PCR0 has a non-zero value")
    @@ -31,6 +31,19 @@ def TestInstallUseSWTPM(install_image):
             if "0x0000000000000000000000000000000000000000000000000000000000000000" in str(result.stdout):
                 raise IncusOSException("PCR11 isn't initialized")
     
    +        result = vm.RunCommand("tpm2_pcrread", "sha256:15")
    +        if "0x0000000000000000000000000000000000000000000000000000000000000000" in str(result.stdout):
    +            raise IncusOSException("PCR15 isn't initialized")
    +
    +        # Verify that LUKS encryption is bound to PCRs 7+11+15
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part9")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS swap partition not properly bound to PCRs 7+11+15")
    +
    +        result = vm.RunCommand("cryptsetup", "luksDump", "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_incus_root-part10")
    +        if "tpm2-hash-pcrs:   7+15" not in str(result.stdout) or "tpm2-pubkey-pcrs: 11" not in str(result.stdout):
    +            raise IncusOSException("LUKS root partition not properly bound to PCRs 7+11+15")
    +
             # Verify the security endpoint reflects swtpm is in use
             result = vm.APIRequest("/1.0/system/security")
             if result["status_code"] != 200:
    
  • mkosi.conf+10 1 modified
    @@ -34,7 +34,16 @@ Bootable=true
     BaseTrees=%O/base
     UnifiedKernelImages=true
     UnifiedKernelImageFormat=%i_%v
    -KernelCommandLine=rw vt.handoff=1 intel_iommu=on quiet loglevel=0 systemd.show_status=0 rootflags=noexec,nodev,nosuid rd.systemd.mount-extra=/dev/disk/by-partlabel/esp:/boot:vfat:rw modprobe.blacklist=nvidiafb
    +KernelCommandLine=rw
    +                  vt.handoff=1
    +                  intel_iommu=on
    +                  quiet
    +                  loglevel=0
    +                  systemd.show_status=0
    +                  rootflags=noexec,nodev,nosuid
    +                  rd.systemd.mount-extra=/dev/disk/by-partlabel/esp:/boot:vfat:rw
    +                  modprobe.blacklist=nvidiafb
    +                  systemd.image_policy=esp=unprotected:usr=signed:root=encrypted+absent:swap=encrypted+absent:=ignore
     KernelModulesInitrd=true
     KernelModulesInitrdExclude=.*
     KernelModulesInitrdInclude=default
    
  • mkosi.packages/incusos-initrd-utils/debian/incusos-initrd-utils.install+2 0 modified
    @@ -1,12 +1,14 @@
     incusos-initrd-utils          usr/bin/
     initrd-boot-message.sh        usr/bin/
     initrd-debug-info.sh          usr/bin/
    +initrd-finalize-luks-state.sh usr/bin/
     initrd-multipath.sh           usr/bin/
     initrd-multipath-partition.sh usr/bin/
     initrd-startup-checks.sh      usr/bin/
     
     initrd-boot-message.service        usr/lib/systemd/system/
     initrd-debug-info.service          usr/lib/systemd/system/
    +initrd-finalize-luks-state.service usr/lib/systemd/system/
     initrd-multipath.service           usr/lib/systemd/system/
     initrd-multipath-partition.service usr/lib/systemd/system/
     initrd-startup-checks.service      usr/lib/systemd/system/
    
  • mkosi.packages/incusos-initrd-utils/debian/incusos-initrd-utils.links+1 0 modified
    @@ -1,5 +1,6 @@
     usr/lib/systemd/system/initrd-boot-message.service        usr/lib/systemd/system/boot.mount.wants/initrd-boot-message.service
     usr/lib/systemd/system/initrd-debug-info.service          usr/lib/systemd/system/emergency.target.wants/initrd-debug-info.service
    +usr/lib/systemd/system/initrd-finalize-luks-state.service usr/lib/systemd/system/initrd.target.wants/initrd-finalize-luks-state.service
     usr/lib/systemd/system/initrd-multipath.service           usr/lib/systemd/system/boot.mount.wants/initrd-multipath.service
     usr/lib/systemd/system/initrd-multipath.service           usr/lib/systemd/system/swap.target.wants/initrd-multipath.service
     usr/lib/systemd/system/initrd-multipath.service           usr/lib/systemd/system/systemd-cryptsetup@root.service.wants/initrd-multipath.service
    
  • mkosi.packages/incusos-initrd-utils/initrd-finalize-luks-state.service+15 0 added
    @@ -0,0 +1,15 @@
    +[Unit]
    +Description=Finalize and record LUKS state
    +After=initrd-fs.target
    +Requires=initrd-fs.target
    +Before=initrd.target
    +DefaultDependencies=no
    +
    +[Service]
    +Type=oneshot
    +RemainAfterExit=yes
    +
    +ExecStart=/usr/bin/initrd-finalize-luks-state.sh
    +
    +[Install]
    +WantedBy=initrd.target
    
  • mkosi.packages/incusos-initrd-utils/initrd-finalize-luks-state.sh+25 0 added
    @@ -0,0 +1,25 @@
    +#!/bin/sh
    +
    +# Check if we were able to automatically unlock the root LUKS volume.
    +if systemctl status systemd-cryptsetup@root.service > /dev/null 2>&1; then
    +    # Clear any existing UEFI variable
    +    if [ -e /sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c ]; then
    +        chattr -i /sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c
    +        rm /sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c
    +    fi
    +
    +    if journalctl -b -g "TPM2 operation failed, falling back to traditional unlocking" -u systemd-cryptsetup@root.service > /dev/null 2>&1; then
    +        # A recovery password was used to unlock the volume
    +        printf "\07\00\00\00\00\00\00\01" > /sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c
    +    else
    +        # The TPM was able to unlock the volume
    +        printf "\07\00\00\00\00\00\00\00" > /sys/firmware/efi/efivars/IncusOSTPMState-12f075e0-2d07-493d-811a-00920a72c04c
    +    fi
    +fi
    +
    +# If we are using swtpm, extend PCR15's value before we exit the initrd.
    +if [ -d /boot/swtpm/ ]; then
    +    /usr/bin/incusos-initrd-utils seal-pcr15
    +
    +    /usr/bin/swtpm_ioctl --unix /run/swtpm.sock -v
    +fi
    
  • mkosi.packages/incusos-initrd-utils/initrd-multipath-partition.sh+1 5 modified
    @@ -25,14 +25,10 @@ for MP in $(dmsetup ls --target multipath | cut -f1); do
             kpartx -p "-part" -a "/dev/mapper/${MP}"
         fi
     
    -    # Attempt to unlock swap and root partitions.
    +    # Attempt to unlock root partition.
         if [ -e "/dev/mapper/${MP}-part9" ]; then
             # Unlock root partition, which will be automatically detected and then mounted.
             systemd-cryptsetup attach "root" "/dev/mapper/${MP}-part10" "" "tpm2-device=auto,tpm2-measure-pcr=yes,tries=0"
    -
    -        # Unlock swap and manually activate since it's not automatically picked up by systemd.
    -        systemd-cryptsetup attach "swap" "/dev/mapper/${MP}-part9" "" "tpm2-device=auto,tpm2-measure-pcr=yes,tries=0"
    -        swapon /dev/mapper/swap
         fi
     
         break
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.