2026-09-03 17:46:33 +02:00
|
|
|
#!/bin/sh
|
|
|
|
|
# fplearn.sh -- the template-learning measurement, as a protocol rather than a
|
|
|
|
|
# pile of remembered commands.
|
|
|
|
|
#
|
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.
2026-09-05 01:44:15 +02:00
|
|
|
# fplearn.sh snapshot [name] save the current template aside (forensics
|
|
|
|
|
# only -- see restore)
|
|
|
|
|
# fplearn.sh restore <name> REFUSES: a restore DESTROYS the template on
|
|
|
|
|
# this device, see the note in the command
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
# fplearn.sh wipe remove every stored template (backup first)
|
|
|
|
|
# fplearn.sh enrol [finger] enrol at the config's sample count
|
2026-09-03 17:46:33 +02:00
|
|
|
# fplearn.sh base [n] [w] trial with learning OFF (the baseline)
|
|
|
|
|
# fplearn.sh trend [n] [w] three trials with learning ON
|
|
|
|
|
# fplearn.sh sizes just print the template container sizes
|
|
|
|
|
#
|
2026-09-04 23:49:25 +02:00
|
|
|
# SNAPSHOT BEFORE EVERY MEASUREMENT BLOCK. Learning rewrites the template in
|
|
|
|
|
# place, so without a snapshot an improvement cannot be attributed: a rate that
|
|
|
|
|
# climbs over an evening is equally explained by the template getting better, by
|
|
|
|
|
# the SENSOR being wiped clean, or by the USER learning where the sensor likes
|
|
|
|
|
# to be pressed (Jorijn, 2026-09-04, having spotted both while the numbers were
|
|
|
|
|
# still going up). All three move the same way on the same time axis.
|
|
|
|
|
#
|
|
|
|
|
# A snapshot makes the paired test possible: measure the learned template, then
|
|
|
|
|
# restore the older one and measure again WITHIN THE SAME SESSION, so the sensor
|
|
|
|
|
# is equally clean and the user equally practised for both halves. A rate that
|
|
|
|
|
# falls back on restore is learning. A rate that stays up is not.
|
|
|
|
|
#
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
# WIPE FIRST when re-enrolling a finger that is already in the group. Measured
|
|
|
|
|
# 2026-09-04: the duplicated-finger check refuses a finger the group already
|
|
|
|
|
# holds -- 0 accepted of 7 presses, rc=0 on every one, while a never-enrolled
|
a template no name accounts for is removed, not kept in the way
The templates live on the persist partition and survive a reflash; the
finger-name map lives on the rootfs and does not. A reflashed phone comes
back with the trustlet holding every template it had and the daemon
knowing none of them by name -- and with five of them the group is full, so
every enrolment from Settings answered -207 "Up to the limit" (fennek182,
2026-10-13). No D-Bus client could get out of that: the list answered from
the map, delete removed only the fids the map knew, and ENUMERATE was sent
with an empty payload and read for its count alone.
A finger re-enrolled under its old name produced the same state one
template at a time: the map took the new fid and the old template stayed
in its slot.
ENUMERATE now asks with room for every slot the group can hold, and the
reply names the templates: one 8-byte entry per loaded one, its fid in the
second word. Verified live: the three fids the map names came back among
the four listed. With the list in hand:
- An enrolment removes the templates no name accounts for before ENROLL.
They can match nothing (a verify filters on the map's fids, standby
arms from them), and each is a slot the enrolment may need or the
duplicate the trustlet would refuse it for.
- Deleting every finger deletes them too, and the job is posted even
when the map is empty, which after a reflash it is.
- Re-enrolling a finger drops its name at EnrollStart, so the old
template is unnamed and goes under the first rule. The trustlet forces
the order: with duplicate checking on, a re-enrolment of a finger it
still holds does not complete (2026-09-04: one attempt 0 accepted of 7
presses, another stalled at sample 8 of 20).
- The claim log counts them: "4 template(s) loaded, 1 of them unnamed".
The map now decides what an enrolment keeps, so the Store module says so,
and three rules keep that from turning a damaged map into lost templates.
A line the decoder cannot use -- a typo in a hand rebuild, a finger named
twice -- makes the map untrusted, and no enrolment removes anything on its
account. A line it cannot read at all is written back, word for word, by
every save but a delete of every finger, so the distrust outlasts the next
enrolment; a line it can read but not seat, the repeated name or a sixth
finger, is kept as the finger and fid it names and goes with that finger's
delete, template included, instead of coming back as the finger's name;
and the finger named twice now answers to its FIRST line, where before
the last line's fid replaced the earlier one's. A file that will not open
or read, for any reason but its absence, refuses
enrolment and deletion outright and is never written over. And the map is
written with GLib's g_file_set_contents_full under its consistent and
durable flags, and a save that did not land is reported: the old map
stays, a re-enrolment whose name drop did not land is refused with the
name put back, a delete whose save did not land removes nothing, and an
enrolment whose name did not land is reported to the client as failed,
not completed. Of the daemon's own paths, deleting every finger is the
one removal of unnamed templates on the user's word rather than the
map's: in a managed group it takes every template, named or not. A delete is refused while the
claimant's enrolment or verify for that user is in progress, because the
running job's keep-list came from the claim's map; a standby verify for
that user is stopped and the delete goes ahead. Group 0 is never tidied:
every daemon path that removes a template the map does not name, and the
name drop before a re-enrolment, asks one predicate, and it says no for
group 0 -- root's,
which no session user is, and the group a stock Android install's primary
user also writes. Root re-enrolling a finger behaves as it did before.
The --probe-remove diagnostic removes the fid it is given in any group,
with no map involved, as before.
A result belongs to the operation that asked for it. EnrollStart and
VerifyStart number the operation, the job and every event it posts carry
the number, and a Stop or the claim's end clears it: a job still queued
under a cleared number exits at dequeue instead of running an operation
nobody wants, and a result still in flight is neither named nor reported
-- its template, unnamed, goes with the group's next enrolment. Without
this, a stop followed by a new start could name a late result under the
new finger and remove the just-saved template as unnamed. An enrolment is
also refused before anything changes when the store is read-only, and the
claim's map is re-read from disk at every EnrollStart, so a file rebuilt
by hand under a held claim counts.
The probe passes the loaded list as the named one. A diagnostic does not
tidy the group it is probing. fplearn.sh's header no longer says that
re-enrolment adds a template with no trustlet-side remove, and its `enrol`
refusal gives the protocol's own reason (a template with no history)
instead of the trustlet's duplicate refusal.
Tested on the dev phone. An earlier build of this change, before the
shared predicate and the damaged-map rule, removed the phone's one stray
template at an EnrollStart for a finger not in the map (fid 1313616972,
8 -> 6 containers), reloaded the group as the three named ones, got rc=0
from ENROLL and cancelled cleanly. A build of this series then ran the
same claim and EnrollStart on the group it left behind: three loaded, none
unnamed, nothing removed, ENROLL rc=0, cancelled cleanly. The build of this
series' tip then enrolled a fourth finger to completion (20 samples
accepted, one refused), which the map saved and listed, and deleted it
again under the same claim: its template removed, the group and the map
back to the three they started with.
2026-10-13 21:55:15 +02:00
|
|
|
# finger progressed normally. Since 0.2.6 the daemon removes a re-enrolled
|
|
|
|
|
# finger's old template itself before ENROLL (in a managed group, on a map it
|
|
|
|
|
# read in full). This protocol still starts from a wiped group, so that the
|
|
|
|
|
# template it measures has no history: `enrol` refuses a finger name that is
|
|
|
|
|
# already in the map and points here.
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
#
|
2026-09-03 17:46:33 +02:00
|
|
|
# WHY IT IS SHAPED LIKE THIS. Learning is cumulative: every matched press folds
|
|
|
|
|
# frames into the stored template, so a second run is not a repeat of the first
|
|
|
|
|
# and an A/B against a moving template is not an A/B at all. The only honest
|
|
|
|
|
# comparison is on ONE template lineage, in order:
|
|
|
|
|
#
|
|
|
|
|
# 1. enrol a fresh template, 20 samples, no position prompts --
|
|
|
|
|
# which is also the outstanding replication of the 7/10
|
|
|
|
|
# result, the one measurement this lane was parked on
|
|
|
|
|
# 2. base learning off: the clean number for THIS template, and
|
|
|
|
|
# the only figure comparable to every rate in the journal
|
|
|
|
|
# 3. trend learning on, three times: the rate should climb, and
|
|
|
|
|
# the container size is an independent witness that it
|
|
|
|
|
# is the template moving and not the weather
|
|
|
|
|
#
|
|
|
|
|
# Do NOT read run 1 of the trend as "learning made it better". Run 1 starts on
|
|
|
|
|
# the same template the baseline ended on; it is the first run that can improve
|
|
|
|
|
# it, not one that has already been improved.
|
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
GROUP=/mnt/persist/data/RIY7A+mQm3EA4FsCUmkJo0b9dFUYP2YZ4P5hmMiZgeA_Alt
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
MAP=/var/lib/fingerprintd/fingers-10000.map
|
2026-09-03 17:46:33 +02:00
|
|
|
UNIT=fingerprintd-test
|
|
|
|
|
BIN=/tmp/fingerprintd
|
|
|
|
|
COMMON="--daemon --verbose --edge-wake --sfs-root=/var/lib/fingerprintd/sfs --sfs-writable --rpmb-write"
|
|
|
|
|
|
2026-09-04 23:43:47 +02:00
|
|
|
# How many frames learning actually folded, out of the daemon's own transcript.
|
|
|
|
|
# Without this a trend run cannot tell "learning fired and did not help" from
|
|
|
|
|
# "learning never fired" -- which is exactly the confusion that made the first
|
|
|
|
|
# trend run measure a static template three times.
|
|
|
|
|
folds() {
|
|
|
|
|
L=$(ls -t /var/log/fingerprintd/*.log 2>/dev/null | head -1)
|
|
|
|
|
[ -n "$L" ] || { echo " (no transcript)"; return; }
|
|
|
|
|
m=$(grep -c -- "-> MATCH fid=" "$L" 2>/dev/null || echo 0)
|
|
|
|
|
f=$(grep -c "folded in (metric" "$L" 2>/dev/null || echo 0)
|
|
|
|
|
s=$(grep -c "learn: template saved" "$L" 2>/dev/null || echo 0)
|
|
|
|
|
echo " folds: $f frame(s) over $m matched press(es), $s save(s)"
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 17:46:33 +02:00
|
|
|
sizes() {
|
|
|
|
|
# A template is stored twice, the container and its backup, so the sizes
|
|
|
|
|
# come in pairs. The BODY is the container minus its 4096-byte header.
|
|
|
|
|
sudo ls -la "$GROUP" 2>/dev/null | awk '$5 > 200000 { printf " %9d body %9d %s\n", $5, $5-4096, $9 }' | sort -u
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restart() { # $1 = extra args
|
|
|
|
|
sudo systemctl stop "$UNIT" 2>/dev/null
|
|
|
|
|
sleep 2
|
|
|
|
|
sudo systemd-run --unit="$UNIT" --collect $BIN $COMMON $1 >/dev/null 2>&1
|
|
|
|
|
printf 'daemon starting'
|
|
|
|
|
i=0
|
|
|
|
|
while [ $i -lt 40 ]; do
|
|
|
|
|
if busctl --system list 2>/dev/null | grep -q net.reactivated.Fprint; then
|
|
|
|
|
# Owning the name is not the same as being ready: the session comes
|
|
|
|
|
# up on the worker thread afterwards.
|
|
|
|
|
sleep 6; printf ' ready\n'; return 0
|
|
|
|
|
fi
|
|
|
|
|
printf '.'; sleep 1; i=$((i+1))
|
|
|
|
|
done
|
|
|
|
|
printf ' TIMED OUT\n'; return 1
|
|
|
|
|
}
|
|
|
|
|
|
Fold one frame per press, not eight -- and put snapshots where the user can find them
Four matches, each followed by a template save, then five consecutive failures.
Jorijn asked whether it was user error. It was not.
Over one evening the harvest folded 185 frames into a template whose
max_sub_template_num is 96, and the template went from matching ten presses out
of ten to failing five in a row. The frames one press contributes are
near-duplicates of a single image, so folding eight of them per press does not
add coverage: it spends the template's slots on one finger position and evicts
the diversity the enrolment put there. Stock's roughly forty-six updates in a
session are spread across many separate presses, which is where diversity
actually comes from.
So one fold per matched press, which also needs no extra capture at all because
the matching frame is already inside the trustlet. Eight remains available for a
deliberate experiment.
The snapshot directory was resolved from $HOME while the script is run under
sudo, so every snapshot landed in /root/fp6-backups and looked, from the user's
shell, like it had never been taken. That is the one thing that would have made
this unrecoverable, and it was caught only because the restore was needed. It
now resolves the invoking user's home.
2026-09-05 01:36:10 +02:00
|
|
|
# NOT $HOME: this script is run under sudo, where HOME is root's, so snapshots
|
|
|
|
|
# silently landed in /root/fp6-backups and `ls ~/fp6-backups/snapshots` said
|
|
|
|
|
# they did not exist. Resolve the INVOKING user's home instead.
|
|
|
|
|
SNAPHOME=$(getent passwd "${SUDO_USER:-$(id -un)}" | cut -d: -f6)
|
|
|
|
|
SNAPDIR=${SNAPHOME:-$HOME}/fp6-backups/snapshots
|
2026-09-04 23:49:25 +02:00
|
|
|
|
2026-09-03 17:46:33 +02:00
|
|
|
case "${1:-}" in
|
2026-09-04 23:49:25 +02:00
|
|
|
snapshot)
|
|
|
|
|
NAME=${2:-$(date +%Y%m%d-%H%M%S)}
|
|
|
|
|
D=$SNAPDIR/$NAME
|
|
|
|
|
mkdir -p "$D"
|
|
|
|
|
n=0
|
|
|
|
|
for f in $(sudo ls "$GROUP"); do
|
|
|
|
|
sz=$(sudo stat -c %s "$GROUP/$f")
|
|
|
|
|
if [ "$sz" -gt 200000 ]; then sudo cp -p "$GROUP/$f" "$D/$f"; n=$((n+1)); fi
|
|
|
|
|
done
|
|
|
|
|
sudo cp -p "$MAP" "$D/" 2>/dev/null || true
|
|
|
|
|
sudo chown -R "$(id -u):$(id -g)" "$D"
|
|
|
|
|
echo "snapshot '$NAME': $n container(s)"
|
|
|
|
|
ls -la "$D" | awk 'NR>3{printf " %9d %s\n", $5, $9}'
|
|
|
|
|
echo "restore with: fplearn.sh restore $NAME" ;;
|
|
|
|
|
|
|
|
|
|
restore)
|
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.
2026-09-05 01:44:15 +02:00
|
|
|
# DISABLED, and it must stay disabled: restoring a template DESTROYS it.
|
|
|
|
|
#
|
|
|
|
|
# Measured 2026-09-05. A byte-exact restore of an earlier container made the
|
|
|
|
|
# trustlet read its header, UNLINK it, look for a .bak, and bump the RPMB
|
|
|
|
|
# counter -- for both containers. The enrolled finger was gone, and no copy
|
|
|
|
|
# can bring it back, because QTEE binds every SFS object to a hardware
|
|
|
|
|
# anti-rollback counter that has since advanced. An older container is not
|
|
|
|
|
# stale to QTEE, it is TAMPERED, and the response to tampering is deletion.
|
|
|
|
|
#
|
|
|
|
|
# So the paired within-session comparison this was built for is IMPOSSIBLE
|
|
|
|
|
# on this hardware. A template can be measured, and it can be replaced by
|
|
|
|
|
# enrolling again, but it can never be put back.
|
|
|
|
|
cat <<'WARN'
|
|
|
|
|
REFUSING: restoring a template DESTROYS it on this device.
|
|
|
|
|
|
|
|
|
|
QTEE binds every stored object to a hardware anti-rollback counter. An earlier
|
|
|
|
|
container is not stale to it, it is tampered, and it responds by UNLINKING the
|
|
|
|
|
file. Measured 2026-09-05: a byte-exact restore of the previous template made
|
|
|
|
|
the trustlet unlink BOTH containers and advance the RPMB counter. The enrolled
|
|
|
|
|
finger was destroyed and no copy could bring it back.
|
|
|
|
|
|
|
|
|
|
Snapshots are still worth taking -- they are forensic evidence of what a
|
|
|
|
|
template looked like -- but they cannot be put back. To change the template,
|
|
|
|
|
enrol again.
|
|
|
|
|
WARN
|
|
|
|
|
exit 1 ;;
|
|
|
|
|
|
|
|
|
|
restore-force-DESTRUCTIVE)
|
2026-09-04 23:49:25 +02:00
|
|
|
NAME=${2:-}
|
|
|
|
|
D=$SNAPDIR/$NAME
|
|
|
|
|
[ -n "$NAME" ] && [ -d "$D" ] || { echo "usage: fplearn.sh restore <name>"; echo "available:"; ls "$SNAPDIR" 2>/dev/null | sed 's/^/ /'; exit 1; }
|
|
|
|
|
echo "=== restore snapshot '$NAME' ==="
|
|
|
|
|
echo "before:"; sizes
|
|
|
|
|
# The trustlet holds the template in memory once loaded, so a restore that
|
|
|
|
|
# does not restart the daemon changes the file and nothing else.
|
|
|
|
|
sudo systemctl stop "$UNIT" 2>/dev/null; sleep 2
|
|
|
|
|
for f in $(ls "$D"); do
|
|
|
|
|
case "$f" in fingers-*.map) sudo cp -p "$D/$f" "$MAP" ;;
|
|
|
|
|
*) sudo cp -p "$D/$f" "$GROUP/$f" ;; esac
|
|
|
|
|
done
|
|
|
|
|
sudo sync
|
|
|
|
|
restart "--learn=0" || exit 1
|
|
|
|
|
echo "after:"; sizes
|
|
|
|
|
fprintd-list user 2>&1 | tail -1
|
|
|
|
|
echo
|
|
|
|
|
echo "Daemon is up with learning OFF, so the restored template stays put."
|
|
|
|
|
echo "Measure it now, in this session, against the block you just ran:"
|
|
|
|
|
echo " fplearn.sh base" ;;
|
|
|
|
|
|
2026-09-03 17:46:33 +02:00
|
|
|
sizes)
|
|
|
|
|
echo "template containers now:"; sizes ;;
|
|
|
|
|
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
wipe)
|
|
|
|
|
# The procedure of 2026-09-03 and 2026-09-04, and its reasoning: ONLY the
|
|
|
|
|
# template containers go (>200000 bytes; each template is stored twice).
|
|
|
|
|
# The 1588-byte group index is KEPT -- deleting it risks the RPMB
|
|
|
|
|
# anti-rollback counters going stale against object ids QTEE would then
|
|
|
|
|
# recreate, which already cost an index restore once -- and a missing
|
|
|
|
|
# template container is the known-good "not exist, skip" case. The daemon
|
|
|
|
|
# is stopped first so the trustlet reloads from the store, and a backup is
|
|
|
|
|
# taken first because a template is not reproducible without a finger.
|
|
|
|
|
echo "=== wipe: every stored template ==="
|
|
|
|
|
echo "before:"; sizes
|
Fold one frame per press, not eight -- and put snapshots where the user can find them
Four matches, each followed by a template save, then five consecutive failures.
Jorijn asked whether it was user error. It was not.
Over one evening the harvest folded 185 frames into a template whose
max_sub_template_num is 96, and the template went from matching ten presses out
of ten to failing five in a row. The frames one press contributes are
near-duplicates of a single image, so folding eight of them per press does not
add coverage: it spends the template's slots on one finger position and evicts
the diversity the enrolment put there. Stock's roughly forty-six updates in a
session are spread across many separate presses, which is where diversity
actually comes from.
So one fold per matched press, which also needs no extra capture at all because
the matching frame is already inside the trustlet. Eight remains available for a
deliberate experiment.
The snapshot directory was resolved from $HOME while the script is run under
sudo, so every snapshot landed in /root/fp6-backups and looked, from the user's
shell, like it had never been taken. That is the one thing that would have made
this unrecoverable, and it was caught only because the restore was needed. It
now resolves the invoking user's home.
2026-09-05 01:36:10 +02:00
|
|
|
D=${SNAPHOME:-$HOME}/fp6-backups/$(date +%Y-%m-%d-%H%M)-pre-wipe
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
mkdir -p "$D"
|
|
|
|
|
sudo tar -cf "$D/persist-data.tar" -C /mnt/persist data
|
|
|
|
|
sudo cp "$MAP" "$D/" 2>/dev/null || true
|
|
|
|
|
sudo chown -R "$(id -u):$(id -g)" "$D"
|
|
|
|
|
echo "backup: $D ($(tar -tf "$D/persist-data.tar" | wc -l) entries, $(sha256sum "$D/persist-data.tar" | cut -c1-16))"
|
|
|
|
|
sudo systemctl stop "$UNIT" 2>/dev/null; sleep 2
|
|
|
|
|
n=0
|
|
|
|
|
for f in $(sudo ls "$GROUP"); do
|
|
|
|
|
sz=$(sudo stat -c %s "$GROUP/$f")
|
|
|
|
|
if [ "$sz" -gt 200000 ]; then sudo rm -f "$GROUP/$f"; n=$((n+1)); fi
|
|
|
|
|
done
|
|
|
|
|
sudo sync
|
|
|
|
|
sudo sh -c ": > $MAP"
|
|
|
|
|
echo "removed $n container(s); index and small containers kept; map cleared"
|
2026-09-05 02:07:26 +02:00
|
|
|
# Off, like enrol: a wipe is the start of a controlled sequence, and coming
|
|
|
|
|
# back with learning on invites the next thing that verifies to start
|
|
|
|
|
# folding before anyone intended it.
|
|
|
|
|
restart "--learn=0" || exit 1
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
fprintd-list user 2>&1 | tail -1
|
|
|
|
|
echo "after:"; sizes
|
|
|
|
|
echo; echo "next: fplearn.sh enrol <finger>" ;;
|
|
|
|
|
|
2026-09-03 17:46:33 +02:00
|
|
|
enrol)
|
|
|
|
|
F=${2:-right-middle-finger}
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
if sudo grep -q "^$F " "$MAP" 2>/dev/null; then
|
|
|
|
|
echo "$F is already enrolled (in $MAP)."
|
a template no name accounts for is removed, not kept in the way
The templates live on the persist partition and survive a reflash; the
finger-name map lives on the rootfs and does not. A reflashed phone comes
back with the trustlet holding every template it had and the daemon
knowing none of them by name -- and with five of them the group is full, so
every enrolment from Settings answered -207 "Up to the limit" (fennek182,
2026-10-13). No D-Bus client could get out of that: the list answered from
the map, delete removed only the fids the map knew, and ENUMERATE was sent
with an empty payload and read for its count alone.
A finger re-enrolled under its old name produced the same state one
template at a time: the map took the new fid and the old template stayed
in its slot.
ENUMERATE now asks with room for every slot the group can hold, and the
reply names the templates: one 8-byte entry per loaded one, its fid in the
second word. Verified live: the three fids the map names came back among
the four listed. With the list in hand:
- An enrolment removes the templates no name accounts for before ENROLL.
They can match nothing (a verify filters on the map's fids, standby
arms from them), and each is a slot the enrolment may need or the
duplicate the trustlet would refuse it for.
- Deleting every finger deletes them too, and the job is posted even
when the map is empty, which after a reflash it is.
- Re-enrolling a finger drops its name at EnrollStart, so the old
template is unnamed and goes under the first rule. The trustlet forces
the order: with duplicate checking on, a re-enrolment of a finger it
still holds does not complete (2026-09-04: one attempt 0 accepted of 7
presses, another stalled at sample 8 of 20).
- The claim log counts them: "4 template(s) loaded, 1 of them unnamed".
The map now decides what an enrolment keeps, so the Store module says so,
and three rules keep that from turning a damaged map into lost templates.
A line the decoder cannot use -- a typo in a hand rebuild, a finger named
twice -- makes the map untrusted, and no enrolment removes anything on its
account. A line it cannot read at all is written back, word for word, by
every save but a delete of every finger, so the distrust outlasts the next
enrolment; a line it can read but not seat, the repeated name or a sixth
finger, is kept as the finger and fid it names and goes with that finger's
delete, template included, instead of coming back as the finger's name;
and the finger named twice now answers to its FIRST line, where before
the last line's fid replaced the earlier one's. A file that will not open
or read, for any reason but its absence, refuses
enrolment and deletion outright and is never written over. And the map is
written with GLib's g_file_set_contents_full under its consistent and
durable flags, and a save that did not land is reported: the old map
stays, a re-enrolment whose name drop did not land is refused with the
name put back, a delete whose save did not land removes nothing, and an
enrolment whose name did not land is reported to the client as failed,
not completed. Of the daemon's own paths, deleting every finger is the
one removal of unnamed templates on the user's word rather than the
map's: in a managed group it takes every template, named or not. A delete is refused while the
claimant's enrolment or verify for that user is in progress, because the
running job's keep-list came from the claim's map; a standby verify for
that user is stopped and the delete goes ahead. Group 0 is never tidied:
every daemon path that removes a template the map does not name, and the
name drop before a re-enrolment, asks one predicate, and it says no for
group 0 -- root's,
which no session user is, and the group a stock Android install's primary
user also writes. Root re-enrolling a finger behaves as it did before.
The --probe-remove diagnostic removes the fid it is given in any group,
with no map involved, as before.
A result belongs to the operation that asked for it. EnrollStart and
VerifyStart number the operation, the job and every event it posts carry
the number, and a Stop or the claim's end clears it: a job still queued
under a cleared number exits at dequeue instead of running an operation
nobody wants, and a result still in flight is neither named nor reported
-- its template, unnamed, goes with the group's next enrolment. Without
this, a stop followed by a new start could name a late result under the
new finger and remove the just-saved template as unnamed. An enrolment is
also refused before anything changes when the store is read-only, and the
claim's map is re-read from disk at every EnrollStart, so a file rebuilt
by hand under a held claim counts.
The probe passes the loaded list as the named one. A diagnostic does not
tidy the group it is probing. fplearn.sh's header no longer says that
re-enrolment adds a template with no trustlet-side remove, and its `enrol`
refusal gives the protocol's own reason (a template with no history)
instead of the trustlet's duplicate refusal.
Tested on the dev phone. An earlier build of this change, before the
shared predicate and the damaged-map rule, removed the phone's one stray
template at an EnrollStart for a finger not in the map (fid 1313616972,
8 -> 6 containers), reloaded the group as the three named ones, got rc=0
from ENROLL and cancelled cleanly. A build of this series then ran the
same claim and EnrollStart on the group it left behind: three loaded, none
unnamed, nothing removed, ENROLL rc=0, cancelled cleanly. The build of this
series' tip then enrolled a fourth finger to completion (20 samples
accepted, one refused), which the map saved and listed, and deleted it
again under the same claim: its template removed, the group and the map
back to the three they started with.
2026-10-13 21:55:15 +02:00
|
|
|
echo "This protocol measures a template with no history; run"
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
echo " fplearn.sh wipe"
|
a template no name accounts for is removed, not kept in the way
The templates live on the persist partition and survive a reflash; the
finger-name map lives on the rootfs and does not. A reflashed phone comes
back with the trustlet holding every template it had and the daemon
knowing none of them by name -- and with five of them the group is full, so
every enrolment from Settings answered -207 "Up to the limit" (fennek182,
2026-10-13). No D-Bus client could get out of that: the list answered from
the map, delete removed only the fids the map knew, and ENUMERATE was sent
with an empty payload and read for its count alone.
A finger re-enrolled under its old name produced the same state one
template at a time: the map took the new fid and the old template stayed
in its slot.
ENUMERATE now asks with room for every slot the group can hold, and the
reply names the templates: one 8-byte entry per loaded one, its fid in the
second word. Verified live: the three fids the map names came back among
the four listed. With the list in hand:
- An enrolment removes the templates no name accounts for before ENROLL.
They can match nothing (a verify filters on the map's fids, standby
arms from them), and each is a slot the enrolment may need or the
duplicate the trustlet would refuse it for.
- Deleting every finger deletes them too, and the job is posted even
when the map is empty, which after a reflash it is.
- Re-enrolling a finger drops its name at EnrollStart, so the old
template is unnamed and goes under the first rule. The trustlet forces
the order: with duplicate checking on, a re-enrolment of a finger it
still holds does not complete (2026-09-04: one attempt 0 accepted of 7
presses, another stalled at sample 8 of 20).
- The claim log counts them: "4 template(s) loaded, 1 of them unnamed".
The map now decides what an enrolment keeps, so the Store module says so,
and three rules keep that from turning a damaged map into lost templates.
A line the decoder cannot use -- a typo in a hand rebuild, a finger named
twice -- makes the map untrusted, and no enrolment removes anything on its
account. A line it cannot read at all is written back, word for word, by
every save but a delete of every finger, so the distrust outlasts the next
enrolment; a line it can read but not seat, the repeated name or a sixth
finger, is kept as the finger and fid it names and goes with that finger's
delete, template included, instead of coming back as the finger's name;
and the finger named twice now answers to its FIRST line, where before
the last line's fid replaced the earlier one's. A file that will not open
or read, for any reason but its absence, refuses
enrolment and deletion outright and is never written over. And the map is
written with GLib's g_file_set_contents_full under its consistent and
durable flags, and a save that did not land is reported: the old map
stays, a re-enrolment whose name drop did not land is refused with the
name put back, a delete whose save did not land removes nothing, and an
enrolment whose name did not land is reported to the client as failed,
not completed. Of the daemon's own paths, deleting every finger is the
one removal of unnamed templates on the user's word rather than the
map's: in a managed group it takes every template, named or not. A delete is refused while the
claimant's enrolment or verify for that user is in progress, because the
running job's keep-list came from the claim's map; a standby verify for
that user is stopped and the delete goes ahead. Group 0 is never tidied:
every daemon path that removes a template the map does not name, and the
name drop before a re-enrolment, asks one predicate, and it says no for
group 0 -- root's,
which no session user is, and the group a stock Android install's primary
user also writes. Root re-enrolling a finger behaves as it did before.
The --probe-remove diagnostic removes the fid it is given in any group,
with no map involved, as before.
A result belongs to the operation that asked for it. EnrollStart and
VerifyStart number the operation, the job and every event it posts carry
the number, and a Stop or the claim's end clears it: a job still queued
under a cleared number exits at dequeue instead of running an operation
nobody wants, and a result still in flight is neither named nor reported
-- its template, unnamed, goes with the group's next enrolment. Without
this, a stop followed by a new start could name a late result under the
new finger and remove the just-saved template as unnamed. An enrolment is
also refused before anything changes when the store is read-only, and the
claim's map is re-read from disk at every EnrollStart, so a file rebuilt
by hand under a held claim counts.
The probe passes the loaded list as the named one. A diagnostic does not
tidy the group it is probing. fplearn.sh's header no longer says that
re-enrolment adds a template with no trustlet-side remove, and its `enrol`
refusal gives the protocol's own reason (a template with no history)
instead of the trustlet's duplicate refusal.
Tested on the dev phone. An earlier build of this change, before the
shared predicate and the damaged-map rule, removed the phone's one stray
template at an EnrollStart for a finger not in the map (fid 1313616972,
8 -> 6 containers), reloaded the group as the three named ones, got rc=0
from ENROLL and cancelled cleanly. A build of this series then ran the
same claim and EnrollStart on the group it left behind: three loaded, none
unnamed, nothing removed, ENROLL rc=0, cancelled cleanly. The build of this
series' tip then enrolled a fourth finger to completion (20 samples
accepted, one refused), which the map saved and listed, and deleted it
again under the same claim: its template removed, the group and the map
back to the three they started with.
2026-10-13 21:55:15 +02:00
|
|
|
echo "first. (The daemon replaces a re-enrolled finger's template by itself since 0.2.6.)"
|
fplearn.sh: a wipe step, and enrol refuses a finger the group already holds
Measured 2026-09-04: the trustlet's duplicated-finger check refuses to re-enrol
a finger it already has a template for -- 0 accepted of 7 presses, rc=0 on each,
while a never-enrolled finger progressed normally. Re-enrolment adds a template
and there is no trustlet-side remove, so a measured finger's old template has to
go before it can be enrolled again.
wipe does that the way it has been done by hand twice: backup first, daemon
stopped so the trustlet reloads from the store, only the template containers
removed, the group index kept because deleting it risks the RPMB anti-rollback
counters going stale, the name map cleared, then a restart and the sizes.
enrol checks the name map and refuses a finger that is already there, pointing
at wipe, so the refusal cannot be mistaken for bad pressing again.
2026-09-04 23:06:45 +02:00
|
|
|
exit 1
|
|
|
|
|
fi
|
2026-09-03 17:46:33 +02:00
|
|
|
echo "=== enrol $F ==="
|
|
|
|
|
echo "sizes before:"; sizes
|
2026-09-05 02:06:24 +02:00
|
|
|
# Learning OFF for the enrolment itself. Enrolment does not use it, and a
|
|
|
|
|
# daemon left with it on will start folding into the brand-new template the
|
|
|
|
|
# moment anything verifies -- which is exactly the contamination a
|
|
|
|
|
# fresh-template control exists to avoid. `trend` turns it back on.
|
|
|
|
|
restart "--learn=0" || exit 1
|
2026-09-03 17:46:33 +02:00
|
|
|
/tmp/fpenrol.sh "$F"
|
|
|
|
|
echo; echo "sizes after the enrolment:"; sizes
|
|
|
|
|
echo; echo "next: fplearn.sh base" ;;
|
|
|
|
|
|
|
|
|
|
base)
|
2026-09-04 23:43:47 +02:00
|
|
|
N=${2:-15}; W=${3:-0}
|
2026-09-03 17:46:33 +02:00
|
|
|
echo "=== BASELINE: learning OFF ==="
|
|
|
|
|
restart "--learn=0" || exit 1
|
|
|
|
|
echo "sizes before:"; sizes
|
|
|
|
|
/tmp/fptrial.sh "$N" "$W"
|
|
|
|
|
echo; echo "sizes after (MUST be unchanged -- learning was off):"; sizes
|
|
|
|
|
echo; echo "next: fplearn.sh trend" ;;
|
|
|
|
|
|
|
|
|
|
trend)
|
2026-09-04 23:43:47 +02:00
|
|
|
# No wrong-finger taps by default: zero false accepts is settled over forty
|
|
|
|
|
# of them, and the presses are better spent on the false-negative rate.
|
|
|
|
|
N=${2:-15}; W=${3:-0}
|
2026-09-03 17:46:33 +02:00
|
|
|
echo "=== TREND: learning ON, three runs on one template ==="
|
|
|
|
|
restart "--learn=1" || exit 1
|
|
|
|
|
echo "sizes at the start:"; sizes
|
|
|
|
|
r=1
|
|
|
|
|
while [ $r -le 3 ]; do
|
|
|
|
|
echo; echo "----- learning run $r of 3 -----"
|
|
|
|
|
/tmp/fptrial.sh "$N" "$W"
|
2026-09-04 23:43:47 +02:00
|
|
|
echo "after run $r:"; sizes; folds
|
2026-09-03 17:46:33 +02:00
|
|
|
r=$((r+1))
|
|
|
|
|
done
|
|
|
|
|
echo
|
|
|
|
|
echo "Read it as a trend, not three numbers. A rate that climbs while the"
|
|
|
|
|
echo "container grows is learning; a rate that moves while the container"
|
|
|
|
|
echo "does not is noise, and the daemon's own 'learn:' lines say which." ;;
|
|
|
|
|
|
|
|
|
|
*)
|
2026-09-04 23:49:25 +02:00
|
|
|
sed -n '2,52p' "$0"; exit 1 ;;
|
2026-09-03 17:46:33 +02:00
|
|
|
esac
|