Skip to content

TPM read fails after interrupted at LUKS-passphrase on T480 - no TOTP/HOTP prompt #2205

Description

@unpaidbanana

A. Provide Hardware Details

  1. What board are you using? (Choose from the list of boards here)

  2. Does your computer have a dGPU or is it iGPU-only?

    • dGPU (Distinct GPU other then internal GPU)
    • iGPU-only (Internal GPU, normally Intel GPU)
  3. Who installed Heads on this computer?

  4. What PGP key is being used?

    • Librem Key (Nitrokey Pro 2 rebranded)
    • Nitrokey Pro
    • Nitrokey Pro 2
    • Nitrokey 3 NFC
    • Nitrokey 3 NFC Mini
    • Nitrokey Storage
    • Nitrokey Storage 2
    • Yubikey
    • Other
  5. Are you using the PGP key to provide HOTP verification?

    • Yes
    • No
    • I don't know

B. Identify how the board was flashed

  1. Heads initially flashed?

    • External flashing
    • Internal-only / 1vyprep+1vyrain / skulls
    • Don't know
  2. Was the board flashed with a maximized or non-maximized/legacy rom?

    • Maximized
    • Non-maximized / legacy
    • I don't know
  3. If Heads was externally flashed, was IFD unlocked?

    • Yes
    • No
    • Don't know

C. Identify the rom related to this bug report

  1. Did you download or build the rom at issue in this bug report?

    • I downloaded it
    • I built it
  2. If you downloaded your rom, where did you get it from?

    • Heads CircleCi
    • Purism
    • Nitrokey
    • Dasharo DTS (Novacustom)
    • Somewhere else (please identify)

EOL_t480-hotp-maximized Heads-v0.2.1-3034-gf3004e0

Please describe the problem

I interrupted two boots at the LUKS passphrase prompt, and in between
I tried booting from USB a couple of times (one attempt found no device
and dropped me into the recovery shell). Since then Heads doesn't get
to the TOTP/HOTP prompt anymore. Instead I get:

"TPM integrity counter cannot be read. Possible cause: TPM was
swapped or reset. This could indicate a TPM swap attack"

I didn't flash anything, no dom0 update, and I never triggered a TPM
reset. /boot still verifies fine: kexec.sig VERIFIED, boot files OK,
Nitrokey matches the ROM-trusted key. The message also survives a full
power off with battery and PSU removed.

To Reproduce

  1. Boot, get to the LUKS passphrase prompt, interrupt before it unlocks.
  2. Repeat once.
  3. Somewhere in between, try USB boot; one attempt found no USB device
    and dropped to the recovery shell.
  4. Boot normally again.
  5. No TOTP/HOTP prompt, the preflight error shows up instead.

I don't know whether this reproduces on purpose. It's just what I did.

Expected behavior
Normal boot with TOTP/HOTP like before.

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
I looked at the source at f3004e0 before filing this.

preflight_rollback_counter_before_reseal() reads counter id 1070ab1 out
of /boot/kexec_rollback.txt and then calls tpm2_counter_read
(tpmr.sh:277, nvread 0x$index). That read fails and fail_preflight
fires.

The index itself is still there. tpm2 getcap handles-nv-index lists
0x1070AB1 along with 0x165BA4F, 0x1800001, 0x1800003, 0x1800004,
0x1C00002 and 0x1C0000A. So the NV index hasn't vanished, the read just
doesn't succeed.

As far as I can tell the counter index is only created at TPM
ownership/reset and otherwise only incremented, and kexec_rollback.txt
is only rewritten by update_checksums / kexec-sign-config.sh, which I
never ran here. So a file-vs-TPM desync doesn't look likely to me.

My guess is DA lockout from the interrupted unseal attempts, but I
can't show it from the log, because the read is called as:

if ! tpmr.sh counter_read -ix "$counter_id" >/dev/null 2>&1; then

The 2>&1 throws away the actual tpm2 error, so there's no way to tell
lockout from a missing handle or anything else. That's the part I'd
suggest changing regardless of what my case turns out to be: keeping
that stderr in the debug log would make this kind of report much easier
to act on.

cbmem.txt
debug.log

Activity

  1. unpaidbanana commented on Sep 8, 2026

    @unpaidbanana
    Author

    Update — direct read confirms DA lockout; counter and TPM are intact

    Build: Heads-v0.2.1-3034-gf3004e0, board EOL_t480-hotp-maximized (CircleCI artifact, externally flashed, IFD unlocked, LUKS not sealed to TPM).

    The NV index still exists and is a valid counter:

    # tpm2 nvreadpublic 0x1070ab1
    0x1070ab1:
      name: 000ba03d18cd57c38f074a82a20cf3bee4f258c5d799b69211e21dc859e1242b180d
      hash algorithm:
        friendly: sha256
        value: 0xB
      attributes:
        friendly: authwrite|nt=0x1|ownerread|authread|written
        value: 0x20060014
      size: 8
    

    nt=0x1 confirms it is a counter. no_da is absent, so the index is itself
    subject to DA protection and becomes unreadable whenever the TPM is in lockout —
    independent of the counter's own state.

    Reading it directly returns the reason the preflight discards:

    # tpm2 nvread 0x1070ab1
    ERROR: Esys_NV_Read(0x921) - tpm:warn(2.0): authorizations for objects subject to
    DA protection are not allowed at this time because the TPM is in DA lockout mode
    ERROR: Failed to read NVRAM area at index 0x1070AB1
    

    Lockout state at the same time:

    inLockout:                1
    TPM2_PT_LOCKOUT_COUNTER:  0xA
    TPM2_PT_MAX_AUTH_FAIL:    0xA
    TPM2_PT_LOCKOUT_INTERVAL: 0xE10
    TPM2_PT_LOCKOUT_RECOVERY: 0x0
    

    So nothing was swapped and no counter was lost — the read is simply refused.
    Because preflight_rollback_counter_before_reseal invokes counter_read with
    >/dev/null 2>&1, TPM_RC_LOCKOUT (0x921) and a genuinely missing handle both
    end in the same "TPM was swapped or reset" warning, which sends users looking
    for tampering.

    This is the TPM2 counterpart of the TPM1 lockout problem already documented in
    doc/tpm.md (PR #2068 regression, fixed in #2117), where accumulated auth
    failures produced the same lockout and the same "hours of waiting". The
    mechanism is known; on TPM2 the resulting error just never reaches the user or
    the debug log.

    Suggested fix, consistent with the pipeline-safety pattern in
    doc/ux-patterns.md: keep stderr from the counter read and write it to the
    debug log, so a transient DA lockout is distinguishable from real tampering.
    Happy to test a patch — the machine is still in this state.

  2. tlaurion commented on Sep 10, 2026

    @tlaurion
    Collaborator

    @unpaidbanana thanks for your report. Working on it, but getting something working for both tpm1/tpm2 is not so easy.
    Also implementing helpers so you will be able to replicate and see if this fixes the problem you had.

    Waiting an hour frees a slot (there are 10) or one hour each of tolerence, basically, 10 unclean reboots, forced ctrl-alt-del and bad tpm auth within an hour permitted. Not sure how you arrived to that. 3rd report of such thing for TPM DA lockout outside of real bruteforce attempts against TPM, but yet again, you say you have no TPM DUK setup.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions