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.
This commit is contained in:
Jorijn van der Graaf 2026-09-02 19:11:41 +02:00
commit a8c989857a

View file

@ -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<std::chrono::seconds>(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::uint32_t>(
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; "