mirror of
https://github.com/freemkv/freemkv-keysources.git
synced 2026-10-07 18:13:11 +00:00
e08a4e5fd3af084aa37ded2b57e89a954a6162ed
Round 1's parse cache keyed on `(len, modified)`, which is not a file
identity. mtime is stored at whole-second resolution on HFS+, on many
container overlays and on most network filesystems, so an external
writer — the daily-refresh job, a second freemkv process, an editor, a
sync tool — replacing keydb.cfg within one second with a file of the
same byte length left the stamp bit-identical and the source served the
superseded parse for the rest of the process's life. Retired or
since-corrected keys, reported as success. The doc claiming "an
externally replaced keydb is still picked up on the next call" was false
in exactly that case.
Two discriminators, chosen so neither costs a 62 MiB content hash (the
141 ms x 3+ loads per rip the cache removed):
1. dev + ino. Every atomic publisher (temp + fsync + rename, including
this crate's save()) creates a new inode, so the stamp differs
unconditionally, whatever the clock did.
2. A settle window. An entry is trusted only once the file has been
quiet for the mtime granularity (2 s); the proof that this closes
the residual in-place case, and the network-clock caveats, are in
CacheEntry::is_settled. Cost: re-parsing for up to 2 s after each
write of a file that is rewritten daily.
What it still does NOT catch, now stated in the doc instead of implied
away: a writer that forges the timestamp back to the cached value
(touch -t, rsync --times, a backup restore) into the same inode at the
same length. save() therefore keeps invalidating directly.
Also fixed, all in the same cache:
- The corruption warning was swallowed. ParseStats::log runs inside
KeyDb::parse and a cache hit skips parse, so a damaged keydb warned
once per process and was then served in silence to every later rip.
It is re-emitted on every hit that serves the file; an intact keydb
still says nothing.
- TOCTOU. cached_db stat'd the path and then separately loaded the
path — two resolutions of one name with no atomicity, so a rename in
the gap filed one file's stamp beside another file's keys. The file
is opened once; the stamp is an fstat on that handle and the bytes
come from it.
- Memory residency is recorded rather than quietly widened: a hit
holds every disc's key material plus host-cert private keys for the
process's life, unzeroized. Nothing in this crate zeroizes on drop
and that predates the cache; closing it means zeroize-on-drop across
KeyDb, DiscEntry and libfreemkv's aacs::types, which is cross-crate
work and not smuggled in behind a cache fix.
On KeydbHostCert's derived Debug: audited, NOT a leak. libfreemkv's
HostCert hand-writes a redacting Debug (private_key: "<redacted>",
certificate_len), so `{:?}` on a host cert prints no key bytes. But that
is an external guarantee this crate inherits silently, and the existing
redaction test only covered the outer types. It is now pinned here, for
host certs and device keys, verified red against a libfreemkv patched to
derive Debug instead.
Two KEYDB_PATH-gated tests that could not fail are fixed too:
test_decrypt_real_unit fell off the end after trying every key (a no-op
decrypt_unit passed it), and test_resolve_keys_vuk_path returned early
with zero assertions when the fixture had no qualifying entry.
CHANGELOG: entries for this and for dd9a5dc, including the previously
unrecorded Arc<str> break to KeyDb::disc_entries / DiscEntry::disc_hash.
Languages
Rust
100%