From a8c989857a3519c7d717ccbcb432e0985ac837eb Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Wed, 2 Sep 2026 19:11:41 +0200 Subject: [PATCH] Write a timestamped transcript per run A run whose result nobody recorded is a run that has to be repeated on a human's finger, and this daemon printed to stdout and nowhere else. Two real authentication runs -- an enrolled finger and a wrong-finger control -- were performed and their output is simply gone. A single shared log path would be worse than none: the next run, including a quick control, overwrites the interesting one. That very nearly cost this project the transcript of its first successful authentication, which is why the harness moved to per-run files. Teeing is done at the file-descriptor level rather than by wrapping a stream. std::println writes to stdout through C stdio, so an ostream wrapper captures nothing; routing fd 1 through tee catches every line, including whatever the library prints. --- implementations/main.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/implementations/main.cpp b/implementations/main.cpp index 0b72bff..f8df4b6 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -56,6 +56,7 @@ bool g_listeners = false; bool g_auth = false; int g_frames = 40; int g_frameGapMs = 500; +std::string g_logDir = "/var/log/fingerprintd"; std::uint32_t g_gid = 0; std::string g_taPath = "/lib/firmware/focal64.mbn"; std::string g_cfgPath = "/lib/firmware/fingerprintd.json"; @@ -66,6 +67,30 @@ qcomtee_object* g_root = QCOMTEE_OBJECT_NULL; // 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, +// including a quick control, destroys the interesting one, which is how the +// first successful authentication in this project was very nearly lost. +// +// Done at the file-descriptor level rather than by wrapping a stream, because +// std::println writes to stdout through C stdio: an ostream wrapper would +// capture nothing. Routing fd 1 through tee catches every line including the +// ones libqcomtee prints. +bool StartTranscript(const std::string& dir) { + std::error_code ec; + std::filesystem::create_directories(dir, ec); + auto now = std::chrono::system_clock::now(); + std::string path = std::format("{}/{:%Y%m%d-%H%M%S}.log", dir, + std::chrono::floor(now)); + FILE* t = ::popen(std::format("tee {}", path).c_str(), "w"); + if (!t) return false; + ::dup2(::fileno(t), 1); + ::setvbuf(stdout, nullptr, _IOLBF, 0); + std::println("transcript: {}", path); + return true; +} + // 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 @@ -1136,12 +1161,15 @@ int main(int argc, char** argv) { if (a == "--rpmb-write") g_rpmbWrite = true; if (a == "--auth") { g_auth = true; g_listeners = true; } if (a.starts_with("--frames=")) g_frames = std::stoi(std::string(a.substr(9))); + if (a.starts_with("--log-dir=")) g_logDir = 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) + if (probe) { + StartTranscript(g_logDir); return Probe(); + } std::println(std::cerr, "fingerprintd {}: no runtime yet. --probe-tee reaches QTEE; "