diff --git a/implementations/main.cpp b/implementations/main.cpp index 7ecb1d1..8283f97 100644 --- a/implementations/main.cpp +++ b/implementations/main.cpp @@ -55,6 +55,7 @@ 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 @@ -326,7 +327,8 @@ void ServeGpFile(std::span sb) { f.read(reinterpret_cast(sb.data() + sfs::ReadDataOff), static_cast(want)); auto got = static_cast(f.gcount()); - std::println(" read {} bytes into +0x{:03x}", got, sfs::ReadDataOff); + std::println(" -> errno=0 count={} (asked {}, capacity {})", got, + req->length, sfs::Capacity(sb, sfs::Action::Read)); sfs::WriteReply(sb, 0, got); return; } @@ -357,14 +359,19 @@ void ServeGpFile(std::span sb) { std::size_t want = std::min(req->length, sfs::Capacity(sb, sfs::Action::Write)); std::size_t done = 0; + int werr = 0; while (done < want) { // short writes are real; the reference loops ssize_t n = ::write(fd, sb.data() + sfs::WriteDataOff + done, want - done); - if (n <= 0) break; + if (n < 0) { werr = errno; break; } + if (n == 0) break; done += static_cast(n); } ::fsync(fd); ::close(fd); - sfs::WriteReply(sb, 0, static_cast(done)); + std::println(" -> errno={} count={} (asked {}, capacity {})", werr, done, + req->length, sfs::Capacity(sb, sfs::Action::Write)); + sfs::WriteReply(sb, static_cast(werr), + static_cast(done)); return; } case sfs::Action::Unlink: @@ -515,30 +522,45 @@ void ServeRpmb(std::span sb) { // 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. - auto plan = rp::PlanChunks(req->nblocks, req->blocksPerOp); + // 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. + 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; + } + auto plan = rp::PlanChunks(req->nblocks, bpo); if (!plan.exact) { std::println(" rpmb: {} blocks is not a whole number of {}-block chunks" - " -- refusing", req->nblocks, req->blocksPerOp); + " -- refusing", req->nblocks, bpo); } else { std::array rrq{}; rp::BuildResultReadRequest(rrq); - std::array result{}; rc = 0; for (std::uint32_t k = 0; k < plan.chunks && rc == 0; k++) { std::byte* chunk = frames + static_cast(k) - * req->blocksPerOp * rp::FrameSize; - std::uint32_t bytes = req->blocksPerOp - * static_cast(rp::FrameSize); + * bpo * rp::FrameSize; + std::uint32_t bytes = bpo * static_cast(rp::FrameSize); + // The RESULT FRAME GOES BACK INTO THE SHARED BUFFER, at the + // data offset -- QTEE reads it at req + req[0x0c], which is + // exactly where the request frames were. Collecting it into a + // local means QTEE never sees the device's answer and fails + // the whole transaction with an I/O error, having already + // committed the counter. + std::byte* result = frames; if (SecurityProtocolRetry(fd, false, chunk, bytes) != 0 || SecurityProtocolRetry(fd, false, rrq.data(), rp::FrameSize) != 0 || - SecurityProtocolRetry(fd, true, result.data(), rp::FrameSize) != 0) { + SecurityProtocolRetry(fd, true, result, rp::FrameSize) != 0) { rc = -1; break; } - std::uint16_t res = rp::ResultOf(result); + std::span rf(result, rp::FrameSize); + std::uint16_t res = rp::ResultOf(rf); std::println(" rpmb write chunk {}/{}: result=0x{:04x} ({}) counter={}", k + 1, plan.chunks, res, rp::ResultString(res), - rp::WriteCounterOf(result)); + rp::WriteCounterOf(rf)); // Anything non-zero aborts rather than continuing into further // chunks: the device rejected the frame and the counter state // is not what we think it is. @@ -1232,6 +1254,25 @@ int Probe() { std::println(" {}", tally.Identified() ? "FINGER IDENTIFIED" : "no match"); } + // ---- 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; + } + 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); + } + // ---- Enrolment // // The first thing here that WRITES: template containers through the gpfile @@ -1390,6 +1431,7 @@ int main(int argc, char** argv) { 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.starts_with("--log-dir=")) g_logDir = a.substr(10); if (a.starts_with("--rescan=")) g_rescan = std::stoi(std::string(a.substr(9)));