0.2.4: the trustlet comes from the active slot, unpinned, and a refusal says why
All checks were successful
package / package (push) Successful in 1m22s

0.2.3's manifest pinned the sha256 of one Android build's focal64
(16.82.0, the build the dev phone runs). Fairphone re-signs the trustlet
every release, so that pin matched exactly one of the six builds seen,
and two of two field reports had no sensor: one user on 16.100.0 edited
the manifest by hand, another ended up with a file QTEE refuses.

The manifest now carries '-' instead of a hash and depends on
fp6-vendor-blobs 1-r3, which tries the active slot first and verifies the
image's structure; QTEE's signature check is the gate it always was (one
flipped byte -> ERROR_ELF_SIGNATURE_ERROR, measured 2026-09-03 and again
today).

post-install reassembles the trustlet right away, so 'apk add' no longer
needs a boot for it. post-upgrade re-derives it from the active slot,
which replaces a hand-placed or wrongly pinned file, and then restarts
the daemon -- a plain restart, so a daemon that exited on a refused
trustlet comes back up on the re-derived one.

loadFromBuffer failures name the loader's verdict. The field's first
report was a bare result=12; it now reads ERROR_ELF_SIGNATURE_ERROR with
what to do about it. Probed on the phone with this build: a one-byte
tampered image and 100000 random bytes both print it, the pristine image
loads, and the suites pass 8/8.
This commit is contained in:
Jorijn van der Graaf 2026-09-11 13:04:35 +02:00
commit 823c710b15
6 changed files with 76 additions and 24 deletions

View file

@ -68,7 +68,7 @@ namespace {
// Bumping this is what publishes a package: the registry answers 409 for a
// version it already has, which a build treats as a no-op.
constexpr const char* Version = "0.2.3";
constexpr const char* Version = "0.2.4";
bool g_verbose = false;
// 500 ms was the research harness's pace, chosen so a human could read the
@ -952,6 +952,24 @@ void UnloadStale(qcomtee_object* loader) {
qcomtee_object_refs_dec(p[2].object);
}
// The loader's verdict on the image itself (IQSEEComCompatAppLoader.h in
// Qualcomm's securemsm-kernel), for the codes a phone in the field has produced
// or plausibly will. Named because the first field report was a bare 12: QTEE
// refusing an image whose signature or per-segment hashes do not check out,
// which on this device means a trustlet that is not the active slot's own.
const char* LoaderVerdict(qcomtee_result_t result) {
switch (result) {
case 11: return " ERROR_PIL_ROLLBACK_FAILURE: the image is older than the anti-rollback state allows";
case 12: return " ERROR_ELF_SIGNATURE_ERROR: QTEE refused the image's signature or segment hashes;"
" the trustlet is damaged or not this slot's build. Re-derive it from the"
" active slot: /usr/lib/fp6-vendor-blobs/extract --refresh";
case 13: return " ERROR_METADATA_INVALID: not a QTEE trustlet image";
case 16: return " ERROR_ALREADY_LOADED";
case 28: return " ERROR_ELF_LOADING";
default: return "";
}
}
qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) {
UnloadStale(loader);
@ -981,8 +999,8 @@ qcomtee_object* LoadTrustlet(qcomtee_object* loader, const std::string& path) {
p[3].attr = QCOMTEE_OBJREF_OUTPUT;
qcomtee_result_t result = 0;
if (qcomtee_object_invoke(loader, 1, p, 4, &result) || result) {
std::println(std::cerr, "loadFromBuffer failed, result={}",
static_cast<int>(result));
std::println(std::cerr, "loadFromBuffer failed, result={}{}",
static_cast<int>(result), LoaderVerdict(result));
return QCOMTEE_OBJECT_NULL;
}
std::println("trustlet loaded from {} ({} bytes), distName='{}'", path,