"""#68474 hardening: zeroed state.db detection + quarantine.""" from __future__ import annotations from pathlib import Path import pytest def test_is_zeroed_state_db_and_quarantine(tmp_path): import hermes_state as hs db = tmp_path / "state.db" db.write_bytes(bytes(1024)) assert hs.is_zeroed_state_db(db) is True q = hs.quarantine_zeroed_state_db(db) assert q is not None assert q.exists() assert not db.exists() assert q.read_bytes() == bytes(1024) def test_sessiondb_opens_fresh_after_zeroed_quarantine(tmp_path, monkeypatch): import hermes_state as hs monkeypatch.setenv("HERMES_HOME", str(tmp_path)) db = tmp_path / "state.db" db.write_bytes(bytes(4096)) sdb = hs.SessionDB(db_path=db) try: # Fresh DB should open and accept schema assert db.exists() assert not hs.is_zeroed_state_db(db) # Quarantine retained backups = list(tmp_path.glob("state.db.zeroed-*.bak")) assert len(backups) == 1 assert backups[0].stat().st_size == 4096 finally: sdb.close() def test_concurrent_quarantine_no_clobber(tmp_path): """#68805: two concurrent startups must not race on quarantine. Without the cross-process lock, the second process could move its newly-created empty DB over the first process's quarantine backup, erasing the original damaged-file evidence. With the lock, the second process re-checks under the lock, finds the file no longer zeroed (or gone), and returns without clobbering. """ import hermes_state as hs import threading import sqlite3 db = tmp_path / "state.db" db.write_bytes(bytes(4096)) # zeroed (all-NUL) 4 KB file results: list = [None, None] errors: list = [None, None] def worker(idx): try: # Each worker opens its own SessionDB on the same path. # The first one quarantines the zeroed file and creates a # fresh DB. The second one should find a valid DB (or no # file) under the lock and NOT clobber the quarantine. sdb = hs.SessionDB(db_path=db) try: results[idx] = "ok" finally: sdb.close() except Exception as exc: errors[idx] = exc t1 = threading.Thread(target=worker, args=(0,)) t2 = threading.Thread(target=worker, args=(1,)) t1.start() t2.start() t1.join(timeout=10) t2.join(timeout=10) # Both workers should complete without error assert errors[0] is None, f"Worker 0 raised: {errors[0]}" assert errors[1] is None, f"Worker 1 raised: {errors[1]}" # The quarantine backup must survive — exactly one .bak file with # the original 4096 zeroed bytes. backups = list(tmp_path.glob("state.db.zeroed-*.bak")) assert len(backups) >= 1, "At least one quarantine backup must exist" for bak in backups: assert bak.stat().st_size == 4096, ( f"Quarantine backup {bak} was clobbered: " f"expected 4096 bytes, got {bak.stat().st_size}" ) # The live state.db must be a valid (non-zeroed) SQLite database assert db.exists() assert not hs.is_zeroed_state_db(db) conn = sqlite3.connect(str(db)) conn.execute("SELECT 1") conn.close() def test_quarantine_fails_closed_when_lock_held(tmp_path): """#68805 review: when the cross-process lock cannot be acquired within the timeout, quarantine must FAIL CLOSED — return None without moving the file. A fail-open fallback would let a slow/paused startup that still owns the lock race with the fallback's re-check + rename. """ import hermes_state as hs import platform import threading db = tmp_path / "state.db" db.write_bytes(bytes(4096)) # zeroed (all-NUL) 4 KB file lock_path = db.with_name(db.name + ".quarantine.lock") lock_path.parent.mkdir(parents=True, exist_ok=True) # Hold the cross-process lock from a background thread so the main # thread's quarantine attempt cannot acquire it. lock_held = threading.Event() release_lock = threading.Event() def hold_lock(): handle = lock_path.open("a+b") try: if platform.system() == "Windows": import msvcrt handle.seek(0) msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1) else: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) lock_held.set() release_lock.wait(timeout=15) if platform.system() == "Windows": import msvcrt handle.seek(0) msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) else: import fcntl fcntl.flock(handle.fileno(), fcntl.LOCK_UN) except OSError: lock_held.clear() finally: handle.close() holder = threading.Thread(target=hold_lock) holder.start() assert lock_held.wait(timeout=5), "Background thread failed to acquire lock" # Reduce the quarantine lock timeout to keep the test fast. We patch # the deadline by calling quarantine directly — it uses a 5s timeout, # but we only need to verify it returns None without moving the file. result = hs.quarantine_zeroed_state_db(db) # Must fail closed: return None without moving the zeroed file assert result is None, ( f"quarantine_zeroed_state_db returned {result} — expected None " f"(fail-closed when lock is held)" ) assert db.exists(), "Zeroed state.db was moved despite lock being held" assert hs.is_zeroed_state_db(db), "File should still be zeroed (not moved)" # Release the lock so the background thread can exit cleanly release_lock.set() holder.join(timeout=5)