From e0bc02332fb6a21e740c537529ec1c1b308ba276 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 22:10:51 +0200 Subject: [PATCH] Become a daemon: a held session, a worker, and net.reactivated.Fprint The probe becomes the thing the plan was for. Three threads: the supplicant services QTEE's callbacks; the worker owns the sensor rail, the QTEE session and the trustlet and is the only thread that ever invokes it, so every enrolment and authentication is serialised by construction; the main thread runs the GLib loop and speaks fprintd's own D-Bus interface, never touching the trustlet directly. Session is the bring-up from a cold /dev/tee0 to a calibrated sensor, plus the enrol and verify loops as methods that take a cancel flag and progress callbacks. Worker is a job queue on a pthread with an 8 MiB stack -- musl's default is 128 KiB and the session keeps request buffers on the stack. Results come back through g_idle_add so signals are emitted on the thread that owns the connection. net.reactivated.Fprint is implemented rather than wrapped: Manager with GetDevices/GetDefaultDevice, Device with Claim/Release, EnrollStart/Stop, VerifyStart/Stop, ListEnrolledFingers and the three Delete variants, the three signals, and the five properties. Owning fprintd's name is what lets pam_fprintd, the Plasma KCM and fprintd-enroll work unmodified. Two honest limits. Authorisation is the conservative rule -- you may act on your own prints, root on anyone's -- because polkit is not in this milestone. And DeleteEnrolledFingers removes the finger's NAME only: FF_CMD_TA_REMOVE exists but its payload is not reverse-engineered, and guessing at a command that writes to the store is exactly how an index got invalidated earlier today. A deleted finger loses its name and stops being offered; its template still occupies a slot in the group. Logged as such. The finger-name map is written per user under the state directory, tmp-file and rename. An enrolment records the fid the trustlet reported in the touch event's response; if none was reported the finger cannot be named yet, and the daemon says so rather than inventing one. Verified on the phone as a systemd unit: owns the bus name, init chain complete, floor calibrated, ready. --- implementations/main.cpp | 1447 +++++++++++++++++-------- packaging/net.reactivated.Fprint.conf | 21 + project.cpp | 44 +- 3 files changed, 1047 insertions(+), 465 deletions(-) create mode 100644 packaging/net.reactivated.Fprint.conf diff --git a/implementations/main.cpp b/implementations/main.cpp index 8283f97..642caef 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -7,14 +7,24 @@ 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. +fingerprintd-core, which is tested without a phone. -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. +Three threads: + + * the SUPPLICANT services QTEE's callbacks — the storage listeners and the + credentials object. Nothing QTEE asks of us happens without it. + * the WORKER owns the sensor rail, the QTEE session and the trustlet, and is + the ONLY thread that invokes the trustlet. Every enrolment and every + authentication runs here, serialised by construction. + * the MAIN thread runs the GLib loop and speaks net.reactivated.Fprint. It + never touches the trustlet; it posts jobs to the worker and receives + results back through g_idle_add. + +Why one long-lived process: a listener registration is held for the life of +the process and QTEE's listener table is global to the boot; one sensor reset +buys exactly one trustlet init; and fprintd's clients expect a device that is +already open when they ask. So the process that powers the sensor is the +process that holds the session and owns the bus name. */ // 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 @@ -32,56 +42,49 @@ extern "C" { #include } +#include +#include + #include #include #include #include #include +#include +#include #include #include #include #include #include -#include import std; import Fingerprintd; namespace { -constexpr const char* Version = "0.0.3"; +// 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.1.0"; bool g_verbose = false; -bool g_listeners = false; -bool g_auth = false; -bool g_enrol = false; -bool g_calSave = false; -int g_frames = 40; int g_frameGapMs = 500; int g_samples = 10; // common.max_enrolling_samples, as shipped std::string g_logDir = "/var/log/fingerprintd"; +std::string g_stateDir = "/var/lib/fingerprintd"; int g_rescan = -1; // -1 = leave the config's value alone // The namespace key the trustlet hashes into the SFS group's directory name. -// Defaults to Android's, because that is where the store this device already -// holds was written and it is what an existing template can be read under. -// -// A DIFFERENT path is a different group directory, i.e. complete isolation -// from the Android groups. That matters for enrolment: SAVE_DATA rewrites the -// group's index container, and an index QTEE later fails to verify takes every -// template listed in it with it. Enrolling into our own namespace cannot -// damage a store we did not write. +// It defaults to Android's because that is what this device's existing store +// was written under. It does NOT isolate anything -- SET_ACTIVE_GROUP's path +// selects the group to read, but SAVE_DATA writes into the Android group +// regardless. Isolation comes from the SFS root, not from this. std::string g_groupPath{fingerprintd::ta::GroupNamespacePath}; -std::uint32_t g_gid = 0; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; 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. -// // Every run writes its own timestamped transcript. Not a convenience: a run // whose result nobody recorded is a run that has to be repeated on a human's // finger. And a SINGLE shared log path is worse than none -- the next run, @@ -304,7 +307,8 @@ void ServeGpFile(std::span sb) { if (req->op == sfs::OpConfigPathInit) { // Asked first, with an empty frame. The answer is LATCHED for the // whole boot, so an experiment on its value needs a fresh boot. - std::println(" gpfile op 12 (path init) -> {}", sfs::ConfigPathInitReply); + if (g_verbose) + std::println(" gpfile op 12 (path init) -> {}", sfs::ConfigPathInitReply); sfs::WriteConfigPathInitReply(sb); return; } @@ -318,7 +322,8 @@ void ServeGpFile(std::span sb) { switch (req->action) { case sfs::Action::Read: { - std::println(" gpfile READ {} off={} len={}", *full, req->offset, req->length); + if (g_verbose) + std::println(" gpfile READ {} off={} len={}", *full, req->offset, req->length); std::ifstream f(*full, std::ios::binary); if (!f) { sfs::WriteReply(sb, ENOENT, 0); return; } if (req->offset > 0) f.seekg(req->offset); @@ -327,8 +332,9 @@ void ServeGpFile(std::span sb) { f.read(reinterpret_cast(sb.data() + sfs::ReadDataOff), static_cast(want)); auto got = static_cast(f.gcount()); - std::println(" -> errno=0 count={} (asked {}, capacity {})", got, - req->length, sfs::Capacity(sb, sfs::Action::Read)); + if (g_verbose) + std::println(" -> errno=0 count={} (asked {}, capacity {})", got, + req->length, sfs::Capacity(sb, sfs::Action::Read)); sfs::WriteReply(sb, 0, got); return; } @@ -368,8 +374,7 @@ void ServeGpFile(std::span sb) { } ::fsync(fd); ::close(fd); - std::println(" -> errno={} count={} (asked {}, capacity {})", werr, done, - req->length, sfs::Capacity(sb, sfs::Action::Write)); + std::println(" -> errno={} count={}", werr, done); sfs::WriteReply(sb, static_cast(werr), static_cast(done)); return; @@ -397,7 +402,7 @@ void ServeGpFile(std::span sb) { // // The anti-rollback half. QTEE will not trust a container until it has read // its counter record out of the UFS device's replay-protected area, and it -// cannot reach the device itself. This serves that read. +// cannot reach the device itself. This serves that read, and the write. // // A WRITE advances a monotonic counter that can never be moved back, so it is // refused unless explicitly enabled. Key programming is refused ALWAYS -- the @@ -445,9 +450,10 @@ int SecurityProtocol(int fd, bool isIn, std::byte* buf, std::uint32_t len) { } if (io.driver_status || io.transport_status || io.device_status) { unsigned key = sense[2] & 0x0F; - std::println(" SP{} status drv={} trans={} dev={} sense key={} asc=0x{:02x}/{:02x}", - isIn ? "I" : "O", io.driver_status, io.transport_status, - io.device_status, key, sense[12], sense[13]); + if (g_verbose) + std::println(" SP{} status drv={} trans={} dev={} sense key={} asc=0x{:02x}/{:02x}", + isIn ? "I" : "O", io.driver_status, io.transport_status, + io.device_status, key, sense[12], sense[13]); // The RPMB LUN raises UNIT ATTENTION on the first command after a // reset and clears it by reporting it once. Retryable, not an error. return key == fingerprintd::rpmb::SenseKeyUnitAttention ? 1 : -1; @@ -459,7 +465,6 @@ int SecurityProtocolRetry(int fd, bool isIn, std::byte* buf, std::uint32_t len) for (int t = 0; t < 4; t++) { int rc = SecurityProtocol(fd, isIn, buf, len); if (rc != 1) return rc; - std::println(" (unit attention cleared, retrying)"); } return -1; } @@ -519,18 +524,13 @@ void ServeRpmb(std::span sb) { // The authenticated write sequence, per chunk: the data frames out, a // Result Read Request out, the result frame back. // - // A remainder is refused rather than partially committed. The - // reference silently drops one, which would leave the store - // inconsistent with a counter that cannot be moved back. // req+0x14 is the chunk size, but it is not always usable: the // reference falls back to the whole block count when it is zero or - // larger than nblocks. + // larger than nblocks. A remainder is refused rather than partially + // committed -- a partial authenticated write leaves the store + // inconsistent with a counter that cannot be moved back. std::uint32_t bpo = req->blocksPerOp; - if (bpo == 0 || bpo > req->nblocks) { - std::println(" rpmb: chunk size {} unusable, using nblocks={}", bpo, - req->nblocks); - bpo = req->nblocks; - } + if (bpo == 0 || bpo > req->nblocks) bpo = req->nblocks; auto plan = rp::PlanChunks(req->nblocks, bpo); if (!plan.exact) { std::println(" rpmb: {} blocks is not a whole number of {}-block chunks" @@ -574,11 +574,6 @@ void ServeRpmb(std::span sb) { rp::WriteReply(sb, rp::StatusRefused, 0); return; } - if (g_verbose) - std::println(" rpmb read ok: resp=0x{:04x} result=0x{:04x} counter={}", - rp::ReqRespOf(std::span(frames, rp::FrameSize)), - rp::ResultOf(std::span(frames, rp::FrameSize)), - rp::WriteCounterOf(std::span(frames, rp::FrameSize))); // +0x08 is an OUT parameter QTEE checks against what it expected to be // transferred; leaving the request's frame size there fails every // transaction. +0x0c is left exactly as the request supplied it. @@ -588,9 +583,7 @@ void ServeRpmb(std::span sb) { qcomtee_result_t ListenerDispatch(qcomtee_object* object, qcomtee_op_t op, qcomtee_param* params, int num) { auto* self = reinterpret_cast(object); - if (g_verbose) - std::println(" *** QTEE called listener 0x{:x} op={} params={}", self->id, - static_cast(op), num); + (void)op; for (int i = 0; i < num; i++) { switch (params[i].attr) { @@ -626,8 +619,6 @@ qcomtee_result_t ListenerDispatch(qcomtee_object* object, qcomtee_op_t op, ServeGpFile(sb); else if (self->id == 0x2000) ServeRpmb(sb); - else - std::println(" (listener 0x{:x}: no handler yet)", self->id); } return QCOMTEE_OK; } @@ -861,12 +852,15 @@ struct CommandResult { std::uint32_t gid = 0; std::uint32_t fid = 0; std::int32_t samplesRemaining = -1; + + bool Ok() const { return invoked && result == 0 && rc == 0; } }; CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, std::span payload) { namespace ta = fingerprintd::ta; namespace tee = fingerprintd::tee; + // Only ever called from the worker thread, hence static. static std::vector req(8192), rsp(16384), reqOut(8192), rspOut(16384); std::ranges::fill(rsp, std::byte{0}); std::ranges::fill(reqOut, std::byte{0}); @@ -901,8 +895,6 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, // -201: it reads an output-buffer pointer out of payload+0x00, and QTEE // only patches an address in there if we name the location in // embeddedBufOffsets (IB2) and hand it the region in an object slot. - // Without that the pointer is NULL. This is the whole difference between a - // flat metric and a real scan. // // Two traps: the offsets array applies to EVERY command in a run, so it is // scoped to this one command -- patching a pointer into SYNC_CONFIG's @@ -915,12 +907,8 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, std::println(std::cerr, " memory region alloc failed"); region = QCOMTEE_OBJECT_NULL; } else { - void* addr = qcomtee_memory_object_addr(region); - std::size_t sz = qcomtee_memory_object_size(region); - if (g_verbose) - std::println(" region: addr={} size={} offsets=[0x{:x}] slot=IO0", - addr, sz, offsets); - std::memset(addr, 0, sz); + std::memset(qcomtee_memory_object_addr(region), 0, + qcomtee_memory_object_size(region)); p[2].ubuf.addr = &offsets; p[2].ubuf.size = sizeof(offsets); p[6].object = region; @@ -941,13 +929,6 @@ CommandResult SendCommand(qcomtee_object* app, fingerprintd::ta::Cmd cmd, out.fid = ta::MatchedFid(reqOut); out.samplesRemaining = ta::SamplesRemaining(reqOut); } - if (g_verbose && cmd == ta::Cmd::CaptureImage) { - std::string hex; - for (std::size_t i = 0; i < 0x30; i++) - hex += std::format("{:02x}{}", std::to_integer(reqOut[i]), - (i % 16 == 15) ? "\n " : " "); - std::println(" reqOut[0x00..0x2f]:\n {}", hex); - } return out; } @@ -968,445 +949,981 @@ void Report(fingerprintd::ta::Cmd cmd, const CommandResult& r) { static_cast(r.result), r.rc, ta::StrError(r.rc)); } -int Probe() { - namespace tee = fingerprintd::tee; +// ============================================================================= +// The session: everything from a cold /dev/tee0 to a calibrated sensor, and +// the enrol and verify loops that run against it. WORKER THREAD ONLY. +// ============================================================================= +class Session { +public: + // The whole bring-up. Fails loudly at the first step that does not + // answer; nothing after a failure is attempted. + bool Start() { + namespace tee = fingerprintd::tee; + namespace ta = fingerprintd::ta; - 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); + 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 false; + } + 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; - } + if (pthread_create(&supplicant_, nullptr, Supplicant, nullptr) != 0) { + std::println(std::cerr, "supplicant thread failed to start"); + return false; + } - 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()); + std::uint32_t uid = ::getuid(); + env_ = GetClientEnv(uid); + if (env_ == QCOMTEE_OBJECT_NULL) return false; + std::println("client env obtained (uid {})", uid); - // Register the storage listeners BEFORE loading the trustlet, so any - // storage QTEE wants during init has somewhere to go. - if (g_listeners) { + // Register the storage listeners BEFORE loading the trustlet, so any + // storage QTEE wants during init has somewhere to go. for (const auto& l : tee::Listeners) { if (l.id == 10) continue; // never called on the fingerprint path - RegisterListener(env, l.id, l.bufferSize); + if (!RegisterListener(env_, l.id, l.bufferSize)) return false; } - std::println("SFS root {} ({})", g_sfsRoot, - g_sfsReadOnly ? "READ-ONLY" : "writable"); - } + std::println("SFS root {} ({})", g_sfsRoot, g_sfsReadOnly ? "READ-ONLY" : "writable"); - qcomtee_object* loader = OpenService(env, tee::UidQseecomCompatAppLoader); - if (loader == QCOMTEE_OBJECT_NULL) - return 1; - std::println("QSEECOM-compat app loader (UID {}) opened", - tee::UidQseecomCompatAppLoader); + qcomtee_object* loader = OpenService(env_, tee::UidQseecomCompatAppLoader); + if (loader == QCOMTEE_OBJECT_NULL) return false; + app_ = LoadTrustlet(loader, g_taPath); + if (app_ == QCOMTEE_OBJECT_NULL) return false; - qcomtee_object* app = LoadTrustlet(loader, g_taPath); - if (app == QCOMTEE_OBJECT_NULL) - return 1; + if (!SyncConfig()) return false; - // SYNC_CONFIG first, always. The trustlet reads its whole configuration - // from this one JSON payload, and two keys in it are load-bearing: - // algorithm.enrolling_overlap_intervals must be PRESENT (its default is - // the empty string, which faults the trustlet's own sscanf), and - // device.preferred_device_id selects the chip driver. - std::ifstream cf(g_cfgPath); - if (!cf) { - std::println(std::cerr, "cannot open config {}", g_cfgPath); - return 1; - } - std::string json((std::istreambuf_iterator(cf)), - std::istreambuf_iterator()); - - // common.max_authentication_rescan_times bounds how many frames the - // matcher may answer "not identified yet" before it has to produce a - // verdict. At the stock default a whole run can end undecided, which is - // the right shipping behaviour and useless as a measurement: a - // wrong-finger control that never reaches a verdict has not demonstrated - // a rejection. Setting it to 0 forces every frame terminal. - // - // MEASUREMENT ONLY. A rate measured this way is a per-frame figure taken - // with the retry mechanism disabled and is not a shipping reject rate. - if (g_rescan >= 0) { - auto at = json.find("\"common\":{"); - if (at == std::string::npos) at = json.find("\"common\": {"); - if (at == std::string::npos) { - std::println(std::cerr, "config has no \"common\" object to patch"); - return 1; + // The sensor, and the init chain that needs it powered. + if (!sensor_.Open()) { + std::println(std::cerr, "sensor lines unavailable"); + return false; } - auto brace = json.find('{', at); - json.insert(brace + 1, - std::format("\"max_authentication_rescan_times\":{},", g_rescan)); - std::println("forcing max_authentication_rescan_times={} (MEASUREMENT ONLY)", - g_rescan); - } - // The trustlet wants the terminating NUL counted. - std::vector cfg(json.size() + 1, std::byte{0}); - for (std::size_t i = 0; i < json.size(); i++) - cfg[i] = static_cast(json[i]); - std::println("config {}: {} bytes", g_cfgPath, cfg.size()); - - auto r = SendCommand(app, fingerprintd::ta::Cmd::SyncConfig, cfg); - Report(fingerprintd::ta::Cmd::SyncConfig, r); - if (!r.invoked || r.result != 0 || r.rc != 0) { - std::println(std::cerr, "SYNC_CONFIG did not succeed; stopping here"); - return 1; - } - - - // ---- The sensor, and the init chain that needs it powered - Sensor sensor; - if (!sensor.Open()) { - std::println(std::cerr, "sensor lines unavailable; stopping before init"); - return 1; - } - if (!sensor.PowerOn()) { - std::println(std::cerr, "sensor power-up failed"); - return 1; - } - auto irq = sensor.ReadIrq(); - std::println("sensor powered, reset released, irq={}", - irq ? std::to_string(*irq) : std::string("?")); - - // The chain, in order. Every step answers rc=0 on a healthy sensor and the - // last one is not optional: without SYNC_STATISTICS the trustlet's - // g_statistics stays NULL and the first enrol frame that gets far enough - // writes through it. - // - // One reset buys one init. If this fails, the rail has to go down and come - // back up -- re-running the chain answers -205. - bool ok = true; - for (fingerprintd::ta::Cmd c : fingerprintd::ta::InitChain) { - std::vector payload; - if (c == fingerprintd::ta::Cmd::WorkMode) { - // WORK_MODE takes a u32 mode; 1 = WAIT_TOUCH. - payload.assign(0x10, std::byte{0}); - payload[0] = static_cast( - static_cast(fingerprintd::ta::WorkMode::WaitTouch)); - } else if (c == fingerprintd::ta::Cmd::SyncStatistics) { - payload.assign(fingerprintd::ta::SyncStatisticsPayloadSize, std::byte{0}); + if (!sensor_.PowerOn()) { + std::println(std::cerr, "sensor power-up failed"); + return false; } - auto ir = SendCommand(app, c, payload); - Report(c, ir); - if (!ir.invoked || ir.result != 0 || ir.rc != 0) { - ok = false; - if (ir.rc == fingerprintd::sensor::RcDeviceNotFound) - std::println(std::cerr, - " -205: a second init in one power cycle. " - "Power-cycle the rail, do not retry."); - break; - } - } - if (!ok) { - std::println(std::cerr, "init chain did not complete"); - return 1; - } + std::println("sensor powered, reset released"); - // NOW the store can be read. A template reload needs the device init - // chain to have run first: the per-slot enroll-template array is allocated - // by that chain, and without it FtInitEnrollTplData writes through a NULL - // the moment a template becomes reachable. Running SET_ACTIVE_GROUP before - // the chain answers -2 and loads nothing, which reads like a missing - // container and is an ordering bug. - if (g_listeners) { - auto sag = fingerprintd::ta::BuildSetActiveGroup(g_gid, g_groupPath); - std::println("\nSET_ACTIVE_GROUP gid={} path='{}'", g_gid, g_groupPath); - auto g = SendCommand(app, fingerprintd::ta::Cmd::SetActiveGroup, sag); - Report(fingerprintd::ta::Cmd::SetActiveGroup, g); - - auto e = SendCommand(app, fingerprintd::ta::Cmd::Enumerate, {}); - Report(fingerprintd::ta::Cmd::Enumerate, e); - std::println(" templates loaded: {}", e.rc); - } - - // With the sensor initialised and a region supplied, a capture returns a - // real metric. No finger is needed to establish the idle floor, and the - // floor is the only meaningful reference: the metric is per frame and - // drifts, so a fixed threshold is wrong by construction. - fingerprintd::engine::Baseline baseline; - std::println("calibrating the idle floor ({} samples)", - fingerprintd::engine::Baseline::DefaultSamples); - for (std::size_t i = 0; i < fingerprintd::engine::Baseline::DefaultSamples; i++) { - std::vector cap(fingerprintd::ta::CaptureDeclaredLen); - fingerprintd::ta::BuildCapturePayload(cap); - auto c = SendCommand(app, fingerprintd::ta::Cmd::CaptureImage, cap); - if (!c.invoked || c.result != 0) { - Report(fingerprintd::ta::Cmd::CaptureImage, c); - std::println(std::cerr, "capture failed during calibration"); - return 1; - } - std::println(" idle {}/{}: rc={} metric={}", i + 1, - fingerprintd::engine::Baseline::DefaultSamples, c.rc, c.metric); - baseline.Observe(c.metric); - } - if (!baseline.Ready()) { - std::println(std::cerr, "baseline did not calibrate (floor stayed 0)"); - return 1; - } - std::println("idle floor = {}, finger threshold = {}", baseline.Floor(), - baseline.Threshold()); - - // ---- Authentication - // - // Needs no writes of any kind: no SAVE_DATA, no RPMB write, no SFS write. - // So it runs safely against an existing template with the store read-only, - // which is what makes it the right thing to try before enrolment. - if (g_auth) { - namespace ta = fingerprintd::ta; - namespace en = fingerprintd::engine; - - // AUTHENTICATE arms the scan session. Its gid must match the one - // SET_ACTIVE_GROUP used or the trustlet answers -200. - std::vector au(ta::AuthPayloadSize); - ta::BuildAuthPayload(au, 1, g_gid); - std::println("\nAUTHENTICATE gid={}", g_gid); - auto a = SendCommand(app, ta::Cmd::Authenticate, au); - Report(ta::Cmd::Authenticate, a); - if (!a.invoked || a.result != 0 || a.rc != 0) { - std::println(std::cerr, "could not arm authentication"); - return 1; + // Every step answers rc=0 on a healthy sensor and the last one is not + // optional: without SYNC_STATISTICS the trustlet's g_statistics stays + // NULL and the first enrol frame that gets far enough writes through + // it. One reset buys one init: if this fails the rail has to go down + // and come back up, re-running the chain answers -205. + for (ta::Cmd c : ta::InitChain) { + std::vector payload; + if (c == ta::Cmd::WorkMode) { + payload.assign(0x10, std::byte{0}); + payload[0] = static_cast( + static_cast(ta::WorkMode::WaitTouch)); + } else if (c == ta::Cmd::SyncStatistics) { + payload.assign(ta::SyncStatisticsPayloadSize, std::byte{0}); + } + auto r = SendCommand(app_, c, payload); + Report(c, r); + if (!r.Ok()) { + if (r.rc == fingerprintd::sensor::RcDeviceNotFound) + std::println(std::cerr, " -205: a second init in one power cycle"); + return false; + } } - for (int c = 3; c > 0; c--) { - std::println("*** press and lift your finger in {}... ***", c); - std::fflush(stdout); - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - std::println("\n*** GO -- {} frames, about {} seconds ***\n", g_frames, - (g_frames * g_frameGapMs) / 1000); - en::TouchTracker tracker; - en::AuthTally tally; - - // The reference frame loop is {QUERY, CAPTURE, REPORT, QUERY, REPORT}. - // QUERY_EVENT_STATUS returns its answer in rc -- 5 while an event is - // pending, 0 once REPORT_EVENT has consumed it -- and the trailing - // query is not decoration: without it the trustlet's event state is - // never acknowledged, and after the first verdict every later frame - // answers "not identified yet" forever. - for (int i = 0; i < g_frames; i++) { - std::vector q(0x10, std::byte{0}); - auto q0 = SendCommand(app, ta::Cmd::QueryEventStatus, q); - + // With the sensor initialised and a region supplied, a capture returns + // a real metric. The idle floor is the only meaningful reference: the + // metric is per frame and drifts, so a fixed threshold is wrong by + // construction. + for (std::size_t i = 0; i < fingerprintd::engine::Baseline::DefaultSamples; i++) { std::vector cap(ta::CaptureDeclaredLen); ta::BuildCapturePayload(cap); - auto c = SendCommand(app, ta::Cmd::CaptureImage, cap); - bool finger = baseline.IsFinger(c.metric); - - auto events = tracker.Observe(finger, en::Mode::Authenticate); - std::string verdicts; - for (ta::Event ev : events) { - std::vector evbuf(ta::EventContextSize); - ta::BuildEventContext(evbuf, { .event = ev }); - // Poison the fid field before the call. A zero-initialised - // buffer cannot tell "the matcher never ran" from "the matcher - // ran and rejected the finger" -- the failure path writes zero - // there too, so zero is ambiguous and 0xAAAAAAAA is not. - // PoisonFid already writes at RespFidOff within the PAYLOAD. - // Handing it a span that is itself already offset by the - // payload offset double-counts and poisons payload+0x20, so - // the real fid field stays zero -- and a released finger then - // classifies as a REJECTION, inventing failures that never - // happened. - ta::PoisonFid(evbuf); - - auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf); - if (!r.invoked) continue; - - ta::Verdict v = ta::Classify(r.rc, r.fid); - tally.Observe(v, finger); - verdicts += std::format(" {}", [&] { - switch (v) { - case ta::Verdict::Match: - return std::format("*** MATCH *** gid={} fid={}", r.gid, r.fid); - case ta::Verdict::Rejected: return std::string("REJECTED"); - case ta::Verdict::NotIdentifiedYet: return std::string("not identified yet"); - case ta::Verdict::MatcherNeverRan: return std::string("released"); - } - return std::string("?"); - }()); + auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); + if (!c.invoked || c.result != 0) { + Report(ta::Cmd::CaptureImage, c); + std::println(std::cerr, "capture failed during calibration"); + return false; } - // Acknowledge the event state before the next frame. - auto q1 = SendCommand(app, ta::Cmd::QueryEventStatus, q); - - std::println(" frame {:2}/{}: metric={:<4}{} evst {}->{}{}", i + 1, g_frames, - c.metric, finger ? " FINGER" : " ", - q0.invoked ? q0.rc : -999, q1.invoked ? q1.rc : -999, verdicts); - std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); + baseline_.Observe(c.metric); } - // Only a terminal verdict is an attempt. Counting rescan frames as - // rejections invents failures that never happened. - std::println("\n=== {} MATCH / {} REJECTED over {} terminal frames ===", - tally.Matches(), tally.Rejections(), tally.TerminalFrames()); - std::println(" ({} answered 'not identified yet', {} never reached the matcher)", - tally.NotIdentifiedYet(), tally.NeverRan()); - if (tally.Presses() > 0) - std::println(" presses: {} total, {} reached a verdict, {} matched", - tally.Presses(), tally.PressesDecided(), tally.PressesMatched()); - std::println(" {}", tally.Identified() ? "FINGER IDENTIFIED" : "no match"); + if (!baseline_.Ready()) { + std::println(std::cerr, "baseline did not calibrate (floor stayed 0)"); + return false; + } + std::println("idle floor = {}, finger threshold = {}", baseline_.Floor(), + baseline_.Threshold()); + return true; } - // ---- Calibration save - // - // SAVE_DATA with bit 30 CLEAR takes the calibration path, which writes a - // real container through the whole storage stack and needs NO FINGER. That - // makes it the way to debug the write path without a person present. - if (g_calSave) { - namespace ta = fingerprintd::ta; - if (g_sfsReadOnly) { - std::println(std::cerr, "a calibration save writes; pass --sfs-writable"); - return 1; + void Stop() { + sensor_.PowerOff(); + if (supplicant_) { + pthread_cancel(supplicant_); + pthread_join(supplicant_, nullptr); + supplicant_ = 0; } - std::vector sd(0x10, std::byte{0}); - for (std::size_t k = 0; k < 4; k++) - sd[k] = static_cast((ta::SaveMaskCalibration >> (8 * k)) & 0xFF); - std::println("\n=== SAVE_DATA (calibration, no finger needed) ==="); - auto sv = SendCommand(app, ta::Cmd::SaveData, sd); - Report(ta::Cmd::SaveData, sv); + } + + // Select a group and count what loads. A template reload needs the init + // chain to have run first -- Start() guarantees that. + int SetActiveGroup(std::uint32_t gid) { + namespace ta = fingerprintd::ta; + auto sag = ta::BuildSetActiveGroup(gid, g_groupPath); + std::println("SET_ACTIVE_GROUP gid={}", gid); + auto g = SendCommand(app_, ta::Cmd::SetActiveGroup, sag); + Report(ta::Cmd::SetActiveGroup, g); + auto e = SendCommand(app_, ta::Cmd::Enumerate, {}); + Report(ta::Cmd::Enumerate, e); + std::println(" templates loaded: {}", e.rc); + gid_ = gid; + return e.invoked ? e.rc : -1; } // ---- Enrolment // - // The first thing here that WRITES: template containers through the gpfile - // listener and counter records through RPMB. Both are gated behind - // explicit flags, and RPMB writes cannot be undone. - if (g_enrol) { + // One sample per PRESS: touch on the rising edge, release on the falling + // one, nothing in between. `onStage(accepted, total)` fires on each + // accepted sample; `onRetry()` when a press yielded none. + struct EnrolOutcome { + bool completed = false; + bool cancelled = false; + bool saved = false; + std::uint32_t fid = 0; + std::string why; + }; + + EnrolOutcome Enrol(std::uint32_t gid, std::atomic& cancel, + const std::function& onStage, + const std::function& onRetry, + int maxFrames) { namespace ta = fingerprintd::ta; namespace en = fingerprintd::engine; + EnrolOutcome out; if (g_sfsReadOnly || !g_rpmbWrite) { - std::println(std::cerr, - "enrolment needs --sfs-writable and --rpmb-write; refusing"); - return 1; + out.why = "store is read-only or RPMB writes disabled"; + return out; } + if (gid != gid_) SetActiveGroup(gid); // Stock's opening sequence. AUTHENTICATE is what arms the capture // session; CANCEL and RESET_LOCKOUT bracket it. std::vector au(ta::AuthPayloadSize); ta::BuildAuthPayload(au, 1, 0); - std::println("\n=== enrol pre-sequence ==="); - SendCommand(app, ta::Cmd::Cancel, {}); - SendCommand(app, ta::Cmd::ResetLockout, {}); - SendCommand(app, ta::Cmd::Authenticate, au); - SendCommand(app, ta::Cmd::Cancel, {}); - SendCommand(app, ta::Cmd::ResetLockout, {}); - - auto pe = SendCommand(app, ta::Cmd::PreEnroll, {}); - Report(ta::Cmd::PreEnroll, pe); - - SendCommand(app, ta::Cmd::Authenticate, au); - SendCommand(app, ta::Cmd::Cancel, {}); + SendCommand(app_, ta::Cmd::Cancel, {}); + SendCommand(app_, ta::Cmd::ResetLockout, {}); + SendCommand(app_, ta::Cmd::Authenticate, au); + SendCommand(app_, ta::Cmd::Cancel, {}); + SendCommand(app_, ta::Cmd::ResetLockout, {}); + SendCommand(app_, ta::Cmd::PreEnroll, {}); + SendCommand(app_, ta::Cmd::Authenticate, au); + SendCommand(app_, ta::Cmd::Cancel, {}); // The token is all zero: with trustlet.enable_trusted_enrollment false // the trustlet skips the version check, the challenge compare and the - // HMAC verify outright, which is why pmOS needs no Gatekeeper. The u32 - // at +69 is the GID this enrolment lands under. + // HMAC verify outright. The u32 at +69 is the GID this enrolment lands + // under. std::vector tok(ta::EnrollPayloadSize); - ta::BuildEnrollPayload(tok, g_gid); - std::println("\n=== ENROLL gid={} ===", g_gid); - auto er = SendCommand(app, ta::Cmd::Enroll, tok); + ta::BuildEnrollPayload(tok, gid); + auto er = SendCommand(app_, ta::Cmd::Enroll, tok); Report(ta::Cmd::Enroll, er); - if (!er.invoked || er.result != 0 || er.rc != 0) { - std::println(std::cerr, "ENROLL refused; nothing written"); - return 1; - } + if (!er.Ok()) { out.why = "ENROLL refused"; return out; } + std::println("enrolling gid={}", gid); - for (int c = 3; c > 0; c--) { - std::println("*** press and LIFT, repeatedly, in {}... ***", c); - std::fflush(stdout); - std::this_thread::sleep_for(std::chrono::seconds(1)); - } - std::println("\n*** GO -- press, hold briefly, lift, and move the finger " - "slightly each time ***\n"); - - // Enrolment takes ONE sample per PRESS. Stock sends touch on the - // rising edge and release on the falling one and nothing in between; - // its whole enrolment trace contains no image-ready event. Feeding - // every held frame instead gives the algorithm near-duplicate images - // from a single press. en::TouchTracker tracker; en::EnrolSession enrol(g_samples); - int heldFrames = 0; - for (int i = 0; i < g_frames && !enrol.Complete(); i++) { + int lastAccepted = 0; + bool pressHadTouch = false; + for (int i = 0; i < maxFrames && !enrol.Complete() && !cancel; i++) { std::vector q(0x10, std::byte{0}); - SendCommand(app, ta::Cmd::QueryEventStatus, q); + SendCommand(app_, ta::Cmd::QueryEventStatus, q); std::vector cap(ta::CaptureDeclaredLen); ta::BuildCapturePayload(cap); - auto c = SendCommand(app, ta::Cmd::CaptureImage, cap); - bool finger = baseline.IsFinger(c.metric); + auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); + bool finger = baseline_.IsFinger(c.metric); - // A sample is taken on the RISING edge only. Holding the finger - // down produces no further touch events however long it stays, so - // a run where the finger is never lifted collects exactly one - // sample -- which is what a first attempt at this did, 55 finger - // frames and three touches. - heldFrames = finger ? heldFrames + 1 : 0; - - std::string note; for (ta::Event ev : tracker.Observe(finger, en::Mode::Enrol)) { std::vector evbuf(ta::EventContextSize); ta::BuildEventContext(evbuf, { .event = ev }); - auto r = SendCommand(app, ta::Cmd::ReportEvent, evbuf); + // Poisoned so an unwritten fid can be told from a zero one. + ta::PoisonFid(evbuf); + auto r = SendCommand(app_, ta::Cmd::ReportEvent, evbuf); if (!r.invoked) continue; - // Samples remaining rides in the response on the common path, - // whether or not the sample was accepted -- which matters - // because the trustlet's log starves exactly when one is. - // Only the event that runs the enrol path reports a real - // count. A release leaves the field at 0, which reads exactly - // like "finished". - enrol.Observe(r.samplesRemaining, ev == ta::Event::FingerTouched); - note += std::format(" ev{} rem={}", static_cast(ev), - r.samplesRemaining); + if (ev == ta::Event::FingerTouched) { + pressHadTouch = true; + // Only the event that runs the enrol path reports a real + // count. A release leaves the field at 0, which reads + // exactly like "finished". + enrol.Observe(r.samplesRemaining, true); + if (r.fid != 0 && r.fid != ta::FidPoison) out.fid = r.fid; + std::println(" touch: rem={} fid={:#x}", r.samplesRemaining, r.fid); + } + if (ev == ta::Event::FingerReleased && pressHadTouch) { + // The press is over. Did it move the count? + if (enrol.Accepted() > lastAccepted) { + lastAccepted = enrol.Accepted(); + onStage(enrol.Accepted(), enrol.Total()); + } else { + onRetry(); + } + pressHadTouch = false; + } } - SendCommand(app, ta::Cmd::QueryEventStatus, q); - - std::println(" [{:2}/{}] {:<28} metric={:<4}{}{}", - enrol.Accepted(), enrol.Total(), - enrol.Started() - ? (finger ? "hold... then LIFT" : "LIFT -- now press again") - : "press your finger", - c.metric, finger ? " FINGER" : " ", note); - if (heldFrames == 4) - std::println(" *** still held -- LIFT the finger, a sample is only " - "taken when you press again ***"); + SendCommand(app_, ta::Cmd::QueryEventStatus, q); std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); } + // A final press that completed the count has no release yet. + if (enrol.Complete() && enrol.Accepted() > lastAccepted) + onStage(enrol.Accepted(), enrol.Total()); - std::println("\nsamples: {} of {} accepted", enrol.Accepted(), enrol.Total()); - if (!enrol.Complete()) { - std::println(std::cerr, - "enrolment did not complete -- NOT saving a partial template"); - return 1; + if (cancel) { + SendCommand(app_, ta::Cmd::Cancel, {}); + out.cancelled = true; + out.why = "cancelled"; + return out; } + std::println("samples: {} of {} accepted", enrol.Accepted(), enrol.Total()); + if (!enrol.Complete()) { + out.why = "ran out of frames"; + return out; + } + out.completed = true; - auto po = SendCommand(app, ta::Cmd::PostEnroll, {}); - Report(ta::Cmd::PostEnroll, po); - - // Bit 30 set is the template path; clear is calibration. + SendCommand(app_, ta::Cmd::PostEnroll, {}); std::vector sd(0x10, std::byte{0}); for (std::size_t k = 0; k < 4; k++) sd[k] = static_cast((ta::SaveMaskTemplate >> (8 * k)) & 0xFF); - std::println("\n=== SAVE_DATA (template) ==="); - auto sv = SendCommand(app, ta::Cmd::SaveData, sd); + auto sv = SendCommand(app_, ta::Cmd::SaveData, sd); Report(ta::Cmd::SaveData, sv); - - auto en2 = SendCommand(app, ta::Cmd::Enumerate, {}); - Report(ta::Cmd::Enumerate, en2); - std::println(" templates now in group {}: {}", g_gid, en2.rc); + out.saved = sv.Ok(); + if (!out.saved) out.why = std::format("SAVE_DATA rc={}", sv.rc); + return out; } - std::println("\ntrustlet initialised against a powered sensor."); - pthread_cancel(th); - pthread_join(th, nullptr); + // ---- Verification + // + // Runs until the first terminal verdict or a cancel. A frame is one of + // three things and only the third is a verdict: release (poison intact), + // rescan (rc=-11), or match/reject. + struct VerifyOutcome { + bool decided = false; + bool matched = false; + bool cancelled = false; + std::uint32_t fid = 0; + }; + + VerifyOutcome Verify(std::uint32_t gid, std::atomic& cancel, int maxFrames) { + namespace ta = fingerprintd::ta; + namespace en = fingerprintd::engine; + VerifyOutcome out; + + if (gid != gid_) SetActiveGroup(gid); + + // AUTHENTICATE arms the scan session. Its gid must match the active + // group or the trustlet answers -200. + std::vector au(ta::AuthPayloadSize); + ta::BuildAuthPayload(au, 1, gid); + auto a = SendCommand(app_, ta::Cmd::Authenticate, au); + Report(ta::Cmd::Authenticate, a); + if (!a.Ok()) return out; + + en::TouchTracker tracker; + // The reference frame loop is {QUERY, CAPTURE, REPORT, QUERY, REPORT}. + // The trailing query acknowledges the trustlet's event state; without + // it, after the first verdict every later frame answers "not + // identified yet" forever. + for (int i = 0; i < maxFrames && !cancel && !out.decided; i++) { + std::vector q(0x10, std::byte{0}); + SendCommand(app_, ta::Cmd::QueryEventStatus, q); + + std::vector cap(ta::CaptureDeclaredLen); + ta::BuildCapturePayload(cap); + auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap); + bool finger = baseline_.IsFinger(c.metric); + fingerPresent_.store(finger); + + for (ta::Event ev : tracker.Observe(finger, en::Mode::Authenticate)) { + std::vector evbuf(ta::EventContextSize); + ta::BuildEventContext(evbuf, { .event = ev }); + // A zero-initialised buffer cannot tell "the matcher never + // ran" from "the matcher ran and rejected" -- the failure path + // writes zero there too. + ta::PoisonFid(evbuf); + auto r = SendCommand(app_, ta::Cmd::ReportEvent, evbuf); + if (!r.invoked) continue; + ta::Verdict v = ta::Classify(r.rc, r.fid); + if (ta::IsTerminal(v)) { + out.decided = true; + out.matched = (v == ta::Verdict::Match); + out.fid = out.matched ? r.fid : 0; + std::println(" verdict: {} fid={}", out.matched ? "MATCH" : "REJECTED", out.fid); + break; + } + } + SendCommand(app_, ta::Cmd::QueryEventStatus, q); + std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs)); + } + fingerPresent_.store(false); + if (cancel) { + SendCommand(app_, ta::Cmd::Cancel, {}); + out.cancelled = true; + } + return out; + } + + bool FingerPresent() const { return fingerPresent_.load(); } + int EnrolStages() const { return g_samples; } + qcomtee_object* App() const { return app_; } + +private: + bool SyncConfig() { + namespace ta = fingerprintd::ta; + std::ifstream cf(g_cfgPath); + if (!cf) { + std::println(std::cerr, "cannot open config {}", g_cfgPath); + return false; + } + std::string json((std::istreambuf_iterator(cf)), std::istreambuf_iterator()); + + // common.max_authentication_rescan_times bounds how many frames the + // matcher may answer "not identified yet" before it must produce a + // verdict. MEASUREMENT ONLY: a rate taken with 0 is a per-frame figure + // with the retry mechanism disabled, not a shipping reject rate. + if (g_rescan >= 0) { + auto at = json.find("\"common\":{"); + if (at == std::string::npos) at = json.find("\"common\": {"); + if (at != std::string::npos) { + auto brace = json.find('{', at); + json.insert(brace + 1, + std::format("\"max_authentication_rescan_times\":{},", g_rescan)); + std::println("forcing max_authentication_rescan_times={} (MEASUREMENT ONLY)", + g_rescan); + } + } + // The trustlet wants the terminating NUL counted. + std::vector cfg(json.size() + 1, std::byte{0}); + for (std::size_t i = 0; i < json.size(); i++) + cfg[i] = static_cast(json[i]); + auto r = SendCommand(app_, ta::Cmd::SyncConfig, cfg); + Report(ta::Cmd::SyncConfig, r); + return r.Ok(); + } + + qcomtee_object* env_ = QCOMTEE_OBJECT_NULL; + qcomtee_object* app_ = QCOMTEE_OBJECT_NULL; + pthread_t supplicant_ = 0; + Sensor sensor_; + fingerprintd::engine::Baseline baseline_; + std::uint32_t gid_ = 0xFFFFFFFF; + std::atomic fingerPresent_{false}; +}; + +// ============================================================================= +// Worker: the one thread that talks to the trustlet. The main thread posts +// jobs; results and progress come back through the GLib main loop. +// ============================================================================= +struct Job { + enum class Kind { Claim, Enroll, Verify } kind; + std::uint32_t uid = 0; + std::string finger; + GDBusMethodInvocation* invocation = nullptr; // Claim replies asynchronously +}; + +// Everything the worker sends back to the main thread. Delivered by g_idle_add +// so the D-Bus emission happens on the thread that owns the connection. +struct Event { + enum class Kind { Ready, StartFailed, ClaimDone, EnrollStatus, VerifyStatus, OpFinished } kind; + bool ok = false; + bool done = false; + std::string status; + std::uint32_t fid = 0; + int templates = 0; + GDBusMethodInvocation* invocation = nullptr; +}; + +void PostEvent(std::unique_ptr ev); // defined with the D-Bus code + +class Worker { +public: + void Start() { + // musl's default thread stack is 128 KiB; the session holds request + // buffers on the stack. 8 MiB, as imsd does. + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, 8 * 1024 * 1024); + pthread_create(&tid_, &attr, [](void* self) -> void* { + static_cast(self)->Run(); + return nullptr; + }, this); + pthread_attr_destroy(&attr); + } + + void Post(Job j) { + std::lock_guard lk(mu_); + jobs_.push_back(std::move(j)); + cv_.notify_one(); + } + + // Stops a running enrol/verify at its next frame. The op finishes on the + // worker and reports OpFinished. + void CancelOp() { cancel_.store(true); } + + void Quit() { + { + std::lock_guard lk(mu_); + quit_ = true; + } + cancel_.store(true); + cv_.notify_one(); + if (tid_) pthread_join(tid_, nullptr); + } + + Session& TheSession() { return session_; } + +private: + void Run() { + if (!session_.Start()) { + PostEvent(std::make_unique(Event{ .kind = Event::Kind::StartFailed })); + return; + } + PostEvent(std::make_unique(Event{ .kind = Event::Kind::Ready })); + + for (;;) { + Job j; + { + std::unique_lock lk(mu_); + cv_.wait(lk, [&] { return quit_ || !jobs_.empty(); }); + if (quit_) break; + j = std::move(jobs_.front()); + jobs_.pop_front(); + } + cancel_.store(false); + switch (j.kind) { + case Job::Kind::Claim: { + int n = session_.SetActiveGroup(j.uid); + auto ev = std::make_unique(Event{ .kind = Event::Kind::ClaimDone }); + ev->ok = n >= 0; + ev->templates = n; + ev->invocation = j.invocation; + PostEvent(std::move(ev)); + break; + } + case Job::Kind::Enroll: { + auto o = session_.Enrol( + j.uid, cancel_, + [&](int accepted, int total) { + (void)accepted; (void)total; + auto ev = std::make_unique(Event{ .kind = Event::Kind::EnrollStatus }); + ev->status = "enroll-stage-passed"; + PostEvent(std::move(ev)); + }, + [&] { + auto ev = std::make_unique(Event{ .kind = Event::Kind::EnrollStatus }); + ev->status = "enroll-retry-scan"; + PostEvent(std::move(ev)); + }, + /*maxFrames*/ 600); + auto ev = std::make_unique(Event{ .kind = Event::Kind::EnrollStatus }); + ev->done = true; + ev->ok = o.saved; + ev->fid = o.fid; + if (o.cancelled) { ev->status = ""; ev->done = false; } + else if (o.saved) ev->status = "enroll-completed"; + else ev->status = "enroll-failed"; + if (!o.why.empty()) std::println("enrolment: {}", o.why); + PostEvent(std::move(ev)); + PostEvent(std::make_unique(Event{ .kind = Event::Kind::OpFinished })); + break; + } + case Job::Kind::Verify: { + auto o = session_.Verify(j.uid, cancel_, /*maxFrames*/ 600); + auto ev = std::make_unique(Event{ .kind = Event::Kind::VerifyStatus }); + ev->done = true; + ev->fid = o.fid; + if (o.cancelled) { ev->status = ""; ev->done = false; } + else if (!o.decided) ev->status = "verify-unknown-error"; + else if (o.matched) ev->status = "verify-match"; + else ev->status = "verify-no-match"; + PostEvent(std::move(ev)); + PostEvent(std::make_unique(Event{ .kind = Event::Kind::OpFinished })); + break; + } + } + } + session_.Stop(); + } + + Session session_; + pthread_t tid_ = 0; + std::mutex mu_; + std::condition_variable cv_; + std::deque jobs_; + bool quit_ = false; + std::atomic cancel_{false}; +}; + +// ============================================================================= +// net.reactivated.Fprint -- fprintd's interface, so pam_fprintd, the Plasma +// KCM and fprintd-enroll work against this daemon unmodified. MAIN THREAD. +// ============================================================================= +constexpr const char* BusName = "net.reactivated.Fprint"; +constexpr const char* ManagerPath = "/net/reactivated/Fprint/Manager"; +constexpr const char* DevicePath = "/net/reactivated/Fprint/Device/0"; +constexpr const char* ManagerIface = "net.reactivated.Fprint.Manager"; +constexpr const char* DeviceIface = "net.reactivated.Fprint.Device"; +constexpr const char* DeviceName = "FocalTech FT9391 (QTEE)"; + +constexpr const char* IntrospectionXml = R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +)xml"; + +GDBusConnection* g_conn = nullptr; +GMainLoop* g_loop = nullptr; +Worker* g_worker = nullptr; +bool g_ready = false; + +// Claim state. fprintd's model: one client holds the device at a time, on +// behalf of one user, and every operation is against that user's prints. +enum class Op { None, Enroll, Verify }; +struct Claim { + bool held = false; + std::string sender; // the bus name that holds it + std::string user; + std::uint32_t uid = 0; + Op op = Op::None; + std::string finger; // for the op in progress + fingerprintd::store::Map fingers; +}; +Claim g_claim; +GDBusMethodInvocation* g_pendingClaim = nullptr; + +std::string MapPath(std::uint32_t uid) { + return fingerprintd::store::PathForUid(g_stateDir, uid); +} + +fingerprintd::store::Map LoadMap(std::uint32_t uid) { + std::ifstream f(MapPath(uid)); + if (!f) return {}; + std::string text((std::istreambuf_iterator(f)), std::istreambuf_iterator()); + return fingerprintd::store::Map::Decode(text); +} + +void SaveMap(std::uint32_t uid, const fingerprintd::store::Map& m) { + std::error_code ec; + std::filesystem::create_directories(g_stateDir, ec); + std::string path = MapPath(uid); + std::string tmp = path + ".tmp"; + { + std::ofstream f(tmp, std::ios::trunc); + f << m.Encode(); + } + ::chmod(tmp.c_str(), 0600); + std::filesystem::rename(tmp, path, ec); +} + +void EmitDevice(const char* signal, GVariant* params) { + if (!g_conn) return; + g_dbus_connection_emit_signal(g_conn, nullptr, DevicePath, DeviceIface, signal, params, nullptr); +} + +void ReturnError(GDBusMethodInvocation* inv, const char* name, const std::string& msg) { + g_dbus_method_invocation_return_dbus_error(inv, std::format("net.reactivated.Fprint.Error.{}", name).c_str(), msg.c_str()); +} + +// Who is calling. Used for the one authorisation rule this daemon enforces. +std::optional CallerUid(GDBusMethodInvocation* inv) { + const gchar* sender = g_dbus_method_invocation_get_sender(inv); + GError* err = nullptr; + GVariant* r = g_dbus_connection_call_sync( + g_conn, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", + "GetConnectionUnixUser", g_variant_new("(s)", sender), G_VARIANT_TYPE("(u)"), + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &err); + if (!r) { if (err) g_error_free(err); return std::nullopt; } + guint32 uid = 0; + g_variant_get(r, "(u)", &uid); + g_variant_unref(r); + return uid; +} + +std::optional> ResolveUser(const std::string& name, + GDBusMethodInvocation* inv) { + if (name.empty()) { + // fprintd: an empty username means the caller. + auto uid = CallerUid(inv); + if (!uid) return std::nullopt; + passwd* pw = ::getpwuid(*uid); + return std::make_pair(pw ? std::string(pw->pw_name) : std::to_string(*uid), *uid); + } + passwd* pw = ::getpwnam(name.c_str()); + if (!pw) return std::nullopt; + return std::make_pair(name, static_cast(pw->pw_uid)); +} + +// The authorisation rule. fprintd uses polkit for this; polkit integration is +// not in this milestone, so the rule is the obvious conservative one: you may +// act on your own prints, and root may act on anyone's. +bool Authorised(GDBusMethodInvocation* inv, std::uint32_t targetUid) { + auto caller = CallerUid(inv); + return caller && (*caller == 0 || *caller == targetUid); +} + +void PostEvent(std::unique_ptr ev) { + g_idle_add([](gpointer data) -> gboolean { + std::unique_ptr ev(static_cast(data)); + switch (ev->kind) { + case Event::Kind::Ready: + g_ready = true; + std::println("fingerprintd: ready"); + break; + case Event::Kind::StartFailed: + std::println(std::cerr, "fingerprintd: session bring-up failed -- exiting for systemd"); + g_main_loop_quit(g_loop); + break; + case Event::Kind::ClaimDone: + if (ev->invocation) { + if (ev->ok) { + std::println("claimed by {} for {} (uid {}, {} template(s) loaded)", + g_claim.sender, g_claim.user, g_claim.uid, ev->templates); + g_dbus_method_invocation_return_value(ev->invocation, nullptr); + } else { + g_claim = {}; + ReturnError(ev->invocation, "Internal", "could not select the user's group"); + } + g_pendingClaim = nullptr; + } + break; + case Event::Kind::EnrollStatus: + if (!ev->status.empty()) + EmitDevice("EnrollStatus", g_variant_new("(sb)", ev->status.c_str(), ev->done ? TRUE : FALSE)); + if (ev->done && ev->ok) { + auto f = fingerprintd::store::FingerFromName(g_claim.finger); + if (f && ev->fid != 0) { + g_claim.fingers.Add(*f, ev->fid); + SaveMap(g_claim.uid, g_claim.fingers); + std::println("enrolled {} for uid {} as fid {}", g_claim.finger, g_claim.uid, ev->fid); + } else { + std::println(std::cerr, + "enrolled, but the trustlet reported no fid -- the finger cannot be " + "named until it is seen in a verification"); + } + } + break; + case Event::Kind::VerifyStatus: + if (!ev->status.empty()) + EmitDevice("VerifyStatus", g_variant_new("(sb)", ev->status.c_str(), ev->done ? TRUE : FALSE)); + if (ev->done && ev->status == "verify-match") + std::println("verified fid {} for uid {}", ev->fid, g_claim.uid); + break; + case Event::Kind::OpFinished: + g_claim.op = Op::None; + g_claim.finger.clear(); + break; + } + return G_SOURCE_REMOVE; + }, ev.release()); +} + +void HandleManager(GDBusMethodInvocation* inv, std::string_view method) { + if (method == "GetDevices") { + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("ao")); + g_variant_builder_add(&b, "o", DevicePath); + g_dbus_method_invocation_return_value(inv, g_variant_new("(ao)", &b)); + return; + } + if (method == "GetDefaultDevice") { + g_dbus_method_invocation_return_value(inv, g_variant_new("(o)", DevicePath)); + return; + } + g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); +} + +void HandleDevice(GDBusMethodInvocation* inv, std::string_view method, GVariant* params) { + namespace store = fingerprintd::store; + const gchar* sender = g_dbus_method_invocation_get_sender(inv); + + if (!g_ready) { + ReturnError(inv, "Internal", "device is still starting"); + return; + } + + if (method == "Claim") { + const gchar* username = nullptr; + g_variant_get(params, "(&s)", &username); + if (g_claim.held) { + ReturnError(inv, g_claim.sender == sender ? "AlreadyInUse" : "AlreadyInUse", + "device is already claimed"); + return; + } + auto who = ResolveUser(username ? username : "", inv); + if (!who) { ReturnError(inv, "Internal", "no such user"); return; } + if (!Authorised(inv, who->second)) { + ReturnError(inv, "PermissionDenied", "not allowed to act for that user"); + return; + } + g_claim = {}; + g_claim.held = true; + g_claim.sender = sender; + g_claim.user = who->first; + g_claim.uid = who->second; + g_claim.fingers = LoadMap(g_claim.uid); + g_pendingClaim = inv; + g_worker->Post(Job{ .kind = Job::Kind::Claim, .uid = g_claim.uid, .invocation = inv }); + return; + } + if (method == "Release") { + if (!g_claim.held || g_claim.sender != sender) { + ReturnError(inv, "ClaimDevice", "device is not claimed by you"); + return; + } + if (g_claim.op != Op::None) g_worker->CancelOp(); + g_claim = {}; + g_dbus_method_invocation_return_value(inv, nullptr); + return; + } + if (method == "ListEnrolledFingers") { + const gchar* username = nullptr; + g_variant_get(params, "(&s)", &username); + auto who = ResolveUser(username ? username : "", inv); + if (!who) { ReturnError(inv, "Internal", "no such user"); return; } + auto m = LoadMap(who->second); + if (m.Size() == 0) { ReturnError(inv, "NoEnrolledPrints", "no fingers enrolled"); return; } + GVariantBuilder b; + g_variant_builder_init(&b, G_VARIANT_TYPE("as")); + for (const auto& e : m.Entries()) + g_variant_builder_add(&b, "s", std::string(store::NameOf(e.finger)).c_str()); + g_dbus_method_invocation_return_value(inv, g_variant_new("(as)", &b)); + return; + } + if (method == "DeleteEnrolledFingers" || method == "DeleteEnrolledFingers2" + || method == "DeleteEnrolledFinger") { + std::uint32_t uid = 0; + std::optional one; + if (method == "DeleteEnrolledFingers") { + const gchar* username = nullptr; + g_variant_get(params, "(&s)", &username); + auto who = ResolveUser(username ? username : "", inv); + if (!who) { ReturnError(inv, "Internal", "no such user"); return; } + if (!Authorised(inv, who->second)) { ReturnError(inv, "PermissionDenied", "not allowed"); return; } + uid = who->second; + } else { + if (!g_claim.held || g_claim.sender != sender) { + ReturnError(inv, "ClaimDevice", "device is not claimed by you"); + return; + } + uid = g_claim.uid; + if (method == "DeleteEnrolledFinger") { + const gchar* fname = nullptr; + g_variant_get(params, "(&s)", &fname); + one = store::FingerFromName(fname ? fname : ""); + if (!one) { ReturnError(inv, "InvalidFingername", "unknown finger"); return; } + } + } + auto m = LoadMap(uid); + if (one) m.Remove(*one); else m.Clear(); + SaveMap(uid, m); + if (g_claim.held && g_claim.uid == uid) g_claim.fingers = m; + // The trustlet-side template is NOT removed. FF_CMD_TA_REMOVE (0x2006) + // exists but its payload has not been reverse-engineered, and guessing + // at a command that writes to the store is how an index gets + // invalidated. Until it is, a deleted finger loses its name and stops + // being offered, but its template still occupies a slot in the group. + std::println("deleted finger name(s) for uid {} -- trustlet template(s) NOT removed " + "(FF_CMD_TA_REMOVE not yet implemented)", uid); + g_dbus_method_invocation_return_value(inv, nullptr); + return; + } + if (method == "EnrollStart" || method == "VerifyStart") { + if (!g_claim.held || g_claim.sender != sender) { + ReturnError(inv, "ClaimDevice", "device is not claimed by you"); + return; + } + if (g_claim.op != Op::None) { + ReturnError(inv, "AlreadyInUse", "an operation is already in progress"); + return; + } + const gchar* fname = nullptr; + g_variant_get(params, "(&s)", &fname); + std::string finger = fname ? fname : ""; + bool enroll = (method == "EnrollStart"); + if (enroll) { + if (!store::FingerFromName(finger)) { + ReturnError(inv, "InvalidFingername", "unknown finger"); + return; + } + if (g_claim.fingers.Full() && !g_claim.fingers.Has(*store::FingerFromName(finger))) { + EmitDevice("EnrollStatus", g_variant_new("(sb)", "enroll-data-full", TRUE)); + g_dbus_method_invocation_return_value(inv, nullptr); + return; + } + } else { + if (finger != store::AnyFinger && !store::FingerFromName(finger)) { + ReturnError(inv, "InvalidFingername", "unknown finger"); + return; + } + if (g_claim.fingers.Size() == 0) { + ReturnError(inv, "NoEnrolledPrints", "no fingers enrolled for this user"); + return; + } + } + g_claim.op = enroll ? Op::Enroll : Op::Verify; + g_claim.finger = finger; + g_dbus_method_invocation_return_value(inv, nullptr); + if (!enroll) { + // The trustlet identifies against every template in the group, so + // the finger it will pick is whichever matches. Report what the + // client asked for. + std::string sel = finger == store::AnyFinger && g_claim.fingers.Size() > 0 + ? std::string(store::NameOf(g_claim.fingers.Entries().front().finger)) : finger; + EmitDevice("VerifyFingerSelected", g_variant_new("(s)", sel.c_str())); + } + g_worker->Post(Job{ .kind = enroll ? Job::Kind::Enroll : Job::Kind::Verify, + .uid = g_claim.uid, .finger = finger }); + return; + } + if (method == "EnrollStop" || method == "VerifyStop") { + if (!g_claim.held || g_claim.sender != sender) { + ReturnError(inv, "ClaimDevice", "device is not claimed by you"); + return; + } + Op want = (method == "EnrollStop") ? Op::Enroll : Op::Verify; + if (g_claim.op != want) { + ReturnError(inv, "NoActionInProgress", "no such operation in progress"); + return; + } + g_worker->CancelOp(); + g_dbus_method_invocation_return_value(inv, nullptr); + return; + } + g_dbus_method_invocation_return_dbus_error(inv, "org.freedesktop.DBus.Error.UnknownMethod", "no such method"); +} + +void OnMethodCall(GDBusConnection*, const gchar*, const gchar* path, const gchar*, + const gchar* method, GVariant* params, GDBusMethodInvocation* inv, gpointer) { + if (std::string_view(path) == ManagerPath) HandleManager(inv, method); + else HandleDevice(inv, method, params); +} + +GVariant* OnGetProperty(GDBusConnection*, const gchar*, const gchar*, const gchar*, + const gchar* prop, GError**, gpointer) { + std::string_view p = prop; + if (p == "name") return g_variant_new_string(DeviceName); + if (p == "num-enroll-stages") return g_variant_new_int32(g_worker ? g_worker->TheSession().EnrolStages() : 10); + if (p == "scan-type") return g_variant_new_string("press"); + if (p == "finger-present") return g_variant_new_boolean(g_worker && g_worker->TheSession().FingerPresent()); + if (p == "finger-needed") return g_variant_new_boolean(g_claim.op != Op::None); + return nullptr; +} + +const GDBusInterfaceVTable g_vtable = { OnMethodCall, OnGetProperty, nullptr, {} }; + +void OnBusAcquired(GDBusConnection* conn, const gchar*, gpointer) { + g_conn = conn; + GDBusNodeInfo* node = g_dbus_node_info_new_for_xml(IntrospectionXml, nullptr); + g_dbus_connection_register_object(conn, ManagerPath, node->interfaces[0], &g_vtable, nullptr, nullptr, nullptr); + g_dbus_connection_register_object(conn, DevicePath, node->interfaces[1], &g_vtable, nullptr, nullptr, nullptr); + g_dbus_node_info_unref(node); + std::println("objects registered at {} and {}", ManagerPath, DevicePath); +} + +gboolean OnTerm(gpointer) { + std::println("fingerprintd: stopping"); + g_main_loop_quit(g_loop); + return G_SOURCE_REMOVE; +} + +int RunDaemon() { + if (::geteuid() != 0) { + std::println(std::cerr, "fingerprintd: must run as root (/dev/tee0, gpio, RPMB)"); + return 1; + } + Worker worker; + g_worker = &worker; + g_loop = g_main_loop_new(nullptr, FALSE); + + guint owner = g_bus_own_name( + G_BUS_TYPE_SYSTEM, BusName, G_BUS_NAME_OWNER_FLAGS_NONE, OnBusAcquired, + [](GDBusConnection*, const gchar* name, gpointer) { std::println("owning {}", name); }, + [](GDBusConnection*, const gchar* name, gpointer) { + std::println(std::cerr, "lost {} -- is fprintd running? exiting", name); + g_main_loop_quit(g_loop); + }, + nullptr, nullptr); + + g_unix_signal_add(SIGTERM, OnTerm, nullptr); + g_unix_signal_add(SIGINT, OnTerm, nullptr); + + // The session comes up on the worker; the bus answers "still starting" + // until it posts Ready. + worker.Start(); + std::println("fingerprintd {} starting on the system bus", Version); + g_main_loop_run(g_loop); + + worker.Quit(); + g_bus_unown_name(owner); + g_main_loop_unref(g_loop); + return 0; +} + +// ----------------------------------------------------------------------------- +// Diagnostic modes: the probe flow, kept for a phone with no bus client at hand. +// ----------------------------------------------------------------------------- +int RunProbe(bool doAuth, bool doEnrol, bool doCalSave, std::uint32_t gid, int frames) { + namespace ta = fingerprintd::ta; + Session s; + if (!s.Start()) return 1; + int n = s.SetActiveGroup(gid); + (void)n; + + if (doCalSave) { + if (g_sfsReadOnly) { std::println(std::cerr, "a calibration save writes; pass --sfs-writable"); return 1; } + std::vector sd(0x10, std::byte{0}); + for (std::size_t k = 0; k < 4; k++) + sd[k] = static_cast((ta::SaveMaskCalibration >> (8 * k)) & 0xFF); + std::println("\n=== SAVE_DATA (calibration, no finger needed) ==="); + Report(ta::Cmd::SaveData, SendCommand(s.App(), ta::Cmd::SaveData, sd)); + } + std::atomic cancel{false}; + if (doEnrol) { + for (int c = 3; c > 0; c--) { std::println("*** press and LIFT, repeatedly, in {}... ***", c); std::this_thread::sleep_for(std::chrono::seconds(1)); } + auto o = s.Enrol(gid, cancel, + [](int a, int t) { std::println(" [{}/{}] accepted -- LIFT, then press again", a, t); }, + [] { std::println(" press rejected -- LIFT, shift the finger, press again"); }, + frames); + std::println("enrol: completed={} saved={} fid={} {}", o.completed, o.saved, o.fid, o.why); + } + if (doAuth) { + for (int c = 3; c > 0; c--) { std::println("*** press your finger in {}... ***", c); std::this_thread::sleep_for(std::chrono::seconds(1)); } + auto o = s.Verify(gid, cancel, frames); + std::println("verify: decided={} matched={} fid={}", o.decided, o.matched, o.fid); + } + s.Stop(); return 0; } @@ -1414,40 +1931,42 @@ int Probe() { int main(int argc, char** argv) { std::span args(argv, static_cast(argc)); - bool probe = false; + bool probe = false, daemon = false, doAuth = false, doEnrol = false, doCalSave = false; + std::uint32_t gid = 0; + int frames = 120; for (std::string_view a : args.subspan(1)) { - if (a == "--version") { - std::println("fingerprintd {}", Version); - return 0; - } + if (a == "--version") { std::println("fingerprintd {}", Version); return 0; } + if (a == "--daemon") daemon = true; if (a == "--probe-tee") probe = true; if (a.starts_with("--ta=")) g_taPath = a.substr(5); if (a.starts_with("--config=")) g_cfgPath = a.substr(9); if (a == "--verbose") g_verbose = true; - if (a == "--listeners") g_listeners = true; // Serving the store writable lets QTEE UNLINK a container it rejects, // which destroys an enrolled template. Opt in explicitly. if (a == "--sfs-writable") g_sfsReadOnly = false; if (a == "--rpmb-write") g_rpmbWrite = true; - if (a == "--auth") { g_auth = true; g_listeners = true; } - if (a == "--enrol") { g_enrol = true; g_listeners = true; } - if (a == "--cal-save") { g_calSave = true; g_listeners = true; g_verbose = true; } - if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9))); + if (a == "--auth") { doAuth = true; probe = true; } + if (a == "--enrol") { doEnrol = true; probe = true; } + if (a == "--cal-save") { doCalSave = true; probe = true; g_verbose = true; } + if (a.starts_with("--frames=")) frames = std::stoi(std::string(a.substr(9))); if (a.starts_with("--log-dir=")) g_logDir = a.substr(10); + if (a.starts_with("--state-dir=")) g_stateDir = a.substr(12); if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9))); if (a.starts_with("--group-path=")) g_groupPath = a.substr(13); if (a.starts_with("--samples=")) g_samples = std::stoi(std::string(a.substr(10))); if (a.starts_with("--sfs-root=")) g_sfsRoot = a.substr(11); - if (a.starts_with("--gid=")) g_gid = static_cast( - std::stoul(std::string(a.substr(6)))); - } - if (probe) { - StartTranscript(g_logDir); - return Probe(); + if (a.starts_with("--gid=")) gid = static_cast(std::stoul(std::string(a.substr(6)))); } + StartTranscript(g_logDir); + if (daemon) return RunDaemon(); + if (probe) return RunProbe(doAuth, doEnrol, doCalSave, gid, frames); std::println(std::cerr, - "fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; " - "`crafter-build test` covers the core.", Version); + "fingerprintd {}\n" + " --daemon own net.reactivated.Fprint on the system bus\n" + " --probe-tee [--gid=N] bring the session up and report\n" + " --auth | --enrol | --cal-save diagnostic loops (see README)\n" + " --sfs-root=DIR --sfs-writable --rpmb-write storage policy", + Version); return 1; } diff --git a/packaging/net.reactivated.Fprint.conf b/packaging/net.reactivated.Fprint.conf new file mode 100644 index 0000000..028ba7a --- /dev/null +++ b/packaging/net.reactivated.Fprint.conf @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/project.cpp b/project.cpp index a6219ac..eb7702d 100644 --- a/project.cpp +++ b/project.cpp @@ -36,6 +36,47 @@ static void ApplyQcomteeFlags(Configuration& cfg) { cfg.linkFlags.push_back((root / "libqcomtee.a").string()); } +// pkg-config wrapper (std-only: no popen in import std). Returns the flags +// split on whitespace, or empty if pkg-config is unavailable. +static std::vector PkgConfig(std::string_view args) { + fs::path tmp = fs::temp_directory_path() / + std::format("fingerprintd-pkgconfig-{}.txt", std::hash{}(args)); + std::string cmd = std::format("pkg-config {} > {} 2>/dev/null", args, tmp.string()); + std::vector flags; + if (std::system(cmd.c_str()) == 0) { + std::ifstream f(tmp); + std::string flag; + while (f >> flag) + flags.push_back(flag); + } + std::error_code ec; + fs::remove(tmp, ec); + return flags; +} + +// GDBus (gio-2.0) for net.reactivated.Fprint. Same choice imsd made, for the +// same reason: the platform stack is GLib, so a GMainLoop is wanted regardless. +static void ApplyGioFlags(Configuration& cfg) { + if (!cfg.sysroot.empty()) { + // Cross build: the host's pkg-config would answer for the wrong + // architecture. glib's include layout is stable; `-I=` resolves + // inside the sysroot. + cfg.compileFlags.push_back("-I=/usr/include/glib-2.0"); + cfg.compileFlags.push_back("-I=/usr/lib/glib-2.0/include"); + for (const char* l : { "-lgio-2.0", "-lgobject-2.0", "-lglib-2.0" }) + cfg.linkFlags.push_back(l); + } else { + for (std::string& f : PkgConfig("--cflags gio-2.0")) + if (f.starts_with("-I") || f.starts_with("-D")) cfg.compileFlags.push_back(std::move(f)); + std::vector libs; + for (std::string& f : PkgConfig("--libs gio-2.0")) + if (f.starts_with("-l") || f.starts_with("-L")) libs.push_back(std::move(f)); + if (libs.empty()) libs = { "-lgio-2.0", "-lgobject-2.0", "-lglib-2.0" }; + for (std::string& f : libs) + cfg.linkFlags.push_back(std::move(f)); + } +} + extern "C" Configuration CrafterBuildProject(std::span args) { // fingerprintd-core — the wire formats and state machines as a static // library of pure C++ modules. Deliberately free of GLib, libqcomtee and @@ -79,7 +120,8 @@ extern "C" Configuration CrafterBuildProject(std::span a } ApplyQcomteeFlags(cfg); - cfg.linkFlags.push_back("-lpthread"); // the supplicant thread + ApplyGioFlags(cfg); + cfg.linkFlags.push_back("-lpthread"); // the supplicant and worker threads cfg.AddTest("Sfs").Dependencies({ Core.get() }); cfg.AddTest("Rpmb").Dependencies({ Core.get() });