fingerprintd's own code now talks to QTEE. On the phone:
root object on /dev/tee0
client env obtained (uid 0, 13-byte credentials)
QSEECOM-compat app loader (UID 122) opened
The credentials object is ours rather than libqcomtee's. Upstream's exists only
to build a thirteen-byte CBOR map and drags in QCBOR to do it, so
packaging/make-libqcomtee.sh compiles the two sources that matter and drops
credentials_obj.c entirely -- nothing else references it, and the library then
has no dependency beyond libc. The map is built in Fingerprintd:Tee where it is
pinned byte-for-byte against the string verified on-device, and the object's
two-op read protocol is served here.
Three interop details, all of which cost a build cycle:
* libqcomtee's headers carry no extern "C" guard, having only ever been
consumed from C, so everything came out C++-mangled. They also pull in
<stdatomic.h> and <stdio.h>, which under libc++ drag in templates that may
not appear inside extern "C" -- so those are included first.
* tee_call_t's second parameter is unsigned long on glibc and int on musl.
The native build is glibc and the phone is musl; both forms are compiled.
* On the callback path a UBUF_OUTPUT param arrives with addr = NULL. The
dispatcher supplies the buffer, so a handler POINTS the param at its own
storage rather than writing through the incoming address. Doing the latter
is a null dereference that takes the supplicant thread with it, which is
how the first run against real QTEE ended -- with the correct behaviour
already spelled out in the module comment above the code that ignored it.
That comment now says so in as many words.
271 lines
9.7 KiB
C++
271 lines
9.7 KiB
C++
// SPDX-License-Identifier: GPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// lint-disable-file fixed-width-types
|
|
// lint-disable-file no-char-pointer
|
|
/*
|
|
fingerprintd — the daemon shell.
|
|
|
|
Everything that touches hardware lives here; the decisions live in
|
|
fingerprintd-core, which is tested without a phone. Right now this reaches QTEE
|
|
and stops: root object, credentials, client env, the QSEECOM-compat loader.
|
|
Enough to prove the transport, not yet to drive the sensor.
|
|
|
|
Why the process must be long-lived, once it does more: a listener registration
|
|
is held for as long as the process lives and QTEE's listener table is global to
|
|
the boot, and one sensor reset buys exactly one trustlet init. So the process
|
|
that powers the sensor has to be the process that holds the session.
|
|
*/
|
|
// libqcomtee is a C library and its headers carry no extern "C" guard -- it
|
|
// has only ever been consumed from C. Without one every symbol would be
|
|
// C++-mangled and none would link.
|
|
//
|
|
// The headers pull in <stdarg.h>, <stdatomic.h> and <stdio.h>, and under
|
|
// libc++ those drag in C++ templates, which may not appear inside an
|
|
// extern "C" block. Including them first makes the nested includes no-ops.
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdatomic.h>
|
|
extern "C" {
|
|
#include <qcomtee_object.h>
|
|
#include <qcomtee_object_types.h>
|
|
#include <qcomtee_errno.h>
|
|
}
|
|
|
|
#include <pthread.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/time.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
#include <stdarg.h>
|
|
|
|
import std;
|
|
import Fingerprintd;
|
|
|
|
namespace {
|
|
|
|
constexpr const char* Version = "0.0.2";
|
|
|
|
qcomtee_object* g_root = QCOMTEE_OBJECT_NULL;
|
|
|
|
// The ioctl trampoline libqcomtee calls. Cancellation is made asynchronous
|
|
// around it so the supplicant thread can be stopped while blocked in the
|
|
// kernel waiting for QTEE.
|
|
//
|
|
// tee_call_t's second parameter is `unsigned long` on glibc and `int` on musl
|
|
// (qcomtee_object.h keys it off __GLIBC__), so the signature has to match or
|
|
// the function pointer will not convert. The native build is glibc and the
|
|
// phone is musl, so both forms are compiled here.
|
|
#ifdef __GLIBC__
|
|
int TeeCall(int fd, unsigned long op, ...) {
|
|
#else
|
|
int TeeCall(int fd, int op, ...) {
|
|
#endif
|
|
va_list ap;
|
|
va_start(ap, op);
|
|
void* arg = va_arg(ap, void*);
|
|
va_end(ap);
|
|
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, nullptr);
|
|
int ret = ::ioctl(fd, static_cast<unsigned long>(op), arg);
|
|
pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, nullptr);
|
|
return ret;
|
|
}
|
|
|
|
// QTEE's callbacks are serviced here. Nothing QTEE asks of us happens without
|
|
// this running.
|
|
void* Supplicant(void*) {
|
|
for (;;) {
|
|
pthread_testcancel();
|
|
if (qcomtee_object_process_one(g_root))
|
|
break;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::uint64_t NowMs() {
|
|
timeval tv{};
|
|
::gettimeofday(&tv, nullptr);
|
|
return static_cast<std::uint64_t>(tv.tv_sec) * 1000
|
|
+ static_cast<std::uint64_t>(tv.tv_usec) / 1000;
|
|
}
|
|
|
|
// ---- The credentials object
|
|
//
|
|
// QTEE will not take the credentials blob directly on the Register path: it
|
|
// takes an object and calls back into it, twice, while our invoke is still in
|
|
// flight. Two ops, GET_LENGTH then READ_AT_OFFSET.
|
|
//
|
|
// libqcomtee ships one of these, but only by pulling in QCBOR to build the
|
|
// map. The map is thirteen bytes and lives in Fingerprintd:Tee under test, so
|
|
// this serves it and the library needs no dependency beyond libc.
|
|
struct CredentialsObject {
|
|
qcomtee_object object; // must be first: we cast between them
|
|
std::vector<std::byte> blob;
|
|
std::uint64_t lenStorage = 0; // op 0's answer, pointed at not copied
|
|
};
|
|
|
|
void CredentialsRelease(qcomtee_object* object) {
|
|
delete reinterpret_cast<CredentialsObject*>(object);
|
|
}
|
|
|
|
qcomtee_result_t CredentialsDispatch(qcomtee_object* object, qcomtee_op_t op,
|
|
qcomtee_param* params, int num) {
|
|
auto* self = reinterpret_cast<CredentialsObject*>(object);
|
|
|
|
// On the CALLBACK path a QCOMTEE_UBUF_OUTPUT param arrives with
|
|
// addr = NULL and size = the capacity QTEE will accept: the dispatcher
|
|
// supplies the buffer, so the handler POINTS the param at storage of its
|
|
// own and lets the framework marshal it. Writing through the incoming addr
|
|
// is a null dereference, which is exactly how this crashed the first time
|
|
// it ran against real QTEE.
|
|
if (op == static_cast<qcomtee_op_t>(fingerprintd::tee::CredOp::GetLength)) {
|
|
if (num != 1 || params[0].attr != QCOMTEE_UBUF_OUTPUT)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
if (params[0].ubuf.size < fingerprintd::tee::CredLengthReplySize)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
self->lenStorage = static_cast<std::uint64_t>(self->blob.size());
|
|
params[0].ubuf.addr = &self->lenStorage;
|
|
params[0].ubuf.size = sizeof(self->lenStorage);
|
|
return QCOMTEE_OK;
|
|
}
|
|
|
|
if (op == static_cast<qcomtee_op_t>(fingerprintd::tee::CredOp::ReadAtOffset)) {
|
|
if (num != 2 || params[0].attr != QCOMTEE_UBUF_INPUT
|
|
|| params[1].attr != QCOMTEE_UBUF_OUTPUT)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
// An INPUT param does carry a real address; only outputs arrive NULL.
|
|
if (params[0].ubuf.size < sizeof(std::uint64_t) || !params[0].ubuf.addr)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
std::uint64_t offset = 0;
|
|
::memcpy(&offset, params[0].ubuf.addr, sizeof(offset));
|
|
|
|
auto plan = fingerprintd::tee::PlanRead(self->blob.size(), offset,
|
|
params[1].ubuf.size);
|
|
if (!plan.valid)
|
|
return QCOMTEE_ERROR_INVALID;
|
|
// Same again: point at the blob, do not copy into QTEE's buffer. The
|
|
// storage has to outlive the dispatch, which the object owns.
|
|
params[1].ubuf.addr = self->blob.data() + plan.offset;
|
|
params[1].ubuf.size = plan.count;
|
|
return QCOMTEE_OK;
|
|
}
|
|
|
|
return QCOMTEE_ERROR_INVALID;
|
|
}
|
|
|
|
qcomtee_object_ops g_credOps = {
|
|
/* release */ CredentialsRelease,
|
|
/* dispatch */ CredentialsDispatch,
|
|
/* error */ nullptr,
|
|
/* supported */ nullptr,
|
|
};
|
|
|
|
qcomtee_object* MakeCredentials(std::uint32_t uid) {
|
|
auto* c = new CredentialsObject{};
|
|
c->blob = fingerprintd::tee::BuildCredentials(uid, NowMs());
|
|
if (qcomtee_object_cb_init(&c->object, &g_credOps, g_root)) {
|
|
delete c;
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
return &c->object;
|
|
}
|
|
|
|
// ROOT op 2: hand QTEE a live credentials object and get a client env back.
|
|
// QTEE calls into the object while this invoke is outstanding, which is why
|
|
// the supplicant has to be running first.
|
|
qcomtee_object* GetClientEnv(std::uint32_t uid) {
|
|
qcomtee_object* creds = MakeCredentials(uid);
|
|
if (creds == QCOMTEE_OBJECT_NULL) {
|
|
std::println(std::cerr, "credentials object init failed");
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
qcomtee_param p[2] = {};
|
|
p[0].attr = QCOMTEE_OBJREF_INPUT;
|
|
p[0].object = creds;
|
|
p[1].attr = QCOMTEE_OBJREF_OUTPUT;
|
|
qcomtee_result_t result = 0;
|
|
if (qcomtee_object_invoke(g_root,
|
|
static_cast<qcomtee_op_t>(fingerprintd::tee::ClientEnvOp),
|
|
p, 2, &result) || result) {
|
|
std::println(std::cerr, "ROOT op {} failed, result={}",
|
|
static_cast<unsigned>(fingerprintd::tee::ClientEnvOp),
|
|
static_cast<int>(result));
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
return p[1].object;
|
|
}
|
|
|
|
// IClientEnv op 0: open a service by UID on the env.
|
|
qcomtee_object* OpenService(qcomtee_object* env, std::uint32_t uid) {
|
|
qcomtee_param p[2] = {};
|
|
p[0].attr = QCOMTEE_UBUF_INPUT;
|
|
p[0].ubuf.addr = &uid;
|
|
p[0].ubuf.size = sizeof(uid);
|
|
p[1].attr = QCOMTEE_OBJREF_OUTPUT;
|
|
qcomtee_result_t result = 0;
|
|
if (qcomtee_object_invoke(env, 0, p, 2, &result) || result) {
|
|
std::println(std::cerr, "IClientEnv.open({}) failed, result={}", uid,
|
|
static_cast<int>(result));
|
|
return QCOMTEE_OBJECT_NULL;
|
|
}
|
|
return p[1].object;
|
|
}
|
|
|
|
int Probe() {
|
|
namespace tee = fingerprintd::tee;
|
|
|
|
std::string dev(tee::DevTee);
|
|
g_root = qcomtee_object_root_init(dev.c_str(), TeeCall, nullptr, nullptr);
|
|
if (g_root == QCOMTEE_OBJECT_NULL) {
|
|
std::println(std::cerr, "root object on {}: {}", tee::DevTee,
|
|
::strerror(errno));
|
|
return 1;
|
|
}
|
|
std::println("root object on {}", tee::DevTee);
|
|
|
|
pthread_t th{};
|
|
if (pthread_create(&th, nullptr, Supplicant, nullptr) != 0) {
|
|
std::println(std::cerr, "supplicant thread failed to start");
|
|
return 1;
|
|
}
|
|
|
|
std::uint32_t uid = ::getuid();
|
|
qcomtee_object* env = GetClientEnv(uid);
|
|
if (env == QCOMTEE_OBJECT_NULL)
|
|
return 1;
|
|
std::println("client env obtained (uid {}, {}-byte credentials)", uid,
|
|
tee::BuildCredentials(uid, 0).size());
|
|
|
|
qcomtee_object* loader = OpenService(env, tee::UidQseecomCompatAppLoader);
|
|
if (loader == QCOMTEE_OBJECT_NULL)
|
|
return 1;
|
|
std::println("QSEECOM-compat app loader (UID {}) opened",
|
|
tee::UidQseecomCompatAppLoader);
|
|
|
|
std::println("\nreached QTEE. Not driving the sensor yet.");
|
|
pthread_cancel(th);
|
|
pthread_join(th, nullptr);
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
std::span<char*> args(argv, static_cast<std::size_t>(argc));
|
|
bool probe = false;
|
|
for (std::string_view a : args.subspan(1)) {
|
|
if (a == "--version") {
|
|
std::println("fingerprintd {}", Version);
|
|
return 0;
|
|
}
|
|
if (a == "--probe-tee") probe = true;
|
|
}
|
|
if (probe)
|
|
return Probe();
|
|
|
|
std::println(std::cerr,
|
|
"fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; "
|
|
"`crafter-build test` covers the core.", Version);
|
|
return 1;
|
|
}
|