A template restore destroys the template; refuse it, and stop caching a failed load

The paired test I built cannot exist on this hardware, and finding that out cost
Jorijn his enrolled finger.

Restoring an earlier container byte for byte made the trustlet read its header,
UNLINK it, look for a .bak and advance the RPMB counter -- for both containers.
QTEE binds every SFS object to a hardware anti-rollback counter, so an earlier
version is not stale to it, it is tampered, and the response to tampering is
deletion. No copy can undo that. The journal has warned about this counter since
August in the context of the group index; it applies to the template itself just
as hard, and I did not check before building a feature on the assumption that it
did not.

So restore refuses and explains, with the measurement in the message. Snapshots
stay: they are still forensic evidence of what a template looked like, they just
cannot be put back. Changing a template means enrolling again.

The daemon compounded it. The group-selection cache stored "0 templates loaded"
as a valid answer, so once the trustlet had unlinked everything, every later
claim answered "skipped (already active, 0 template(s))" and never tried again.
It now caches only a positive result, so a failed load is retried rather than
made permanent.
This commit is contained in:
Jorijn van der Graaf 2026-09-05 01:44:15 +02:00
commit 30bb987c27
2 changed files with 38 additions and 3 deletions

View file

@ -1230,7 +1230,12 @@ public:
// store: an enrolment, or a template removed underneath us.
int SetActiveGroup(std::uint32_t gid, bool force = false) {
namespace ta = fingerprintd::ta;
if (!force && gid == gid_ && templatesLoaded_ >= 0) {
// > 0, never >= 0: caching a ZERO turns a failed load into a
// permanent one. It did exactly that on 2026-09-05 -- the trustlet
// unlinked a restored template, reported 0, and every later claim
// answered "skipped (already active, 0 template(s))" without ever
// trying again.
if (!force && gid == gid_ && templatesLoaded_ > 0) {
std::println("SET_ACTIVE_GROUP gid={} skipped (already active, {} template(s))",
gid, templatesLoaded_);
return templatesLoaded_;