fix(state): quarantine 0-byte truncated state.db and record store provenance (#97568)
This commit is contained in:
@@ -449,15 +449,17 @@ def is_zeroed_sqlite_file(
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
return False
|
||||
if size <= 0:
|
||||
if size < 0:
|
||||
return False
|
||||
from hermes_cli.sqlite_safe_read import read_header_bytes_preopen
|
||||
|
||||
head = read_header_bytes_preopen(
|
||||
path, length=max(16, probe_bytes), force=force
|
||||
)
|
||||
if not head:
|
||||
if head is None:
|
||||
return False
|
||||
if len(head) == 0:
|
||||
return True
|
||||
if head.startswith(b"SQLite format 3"):
|
||||
return False
|
||||
return all(byte == 0 for byte in head)
|
||||
|
||||
+6
-2
@@ -3952,14 +3952,18 @@ def is_zeroed_state_db(
|
||||
size = path.stat().st_size
|
||||
except OSError:
|
||||
return False
|
||||
if size <= 0:
|
||||
if size < 0:
|
||||
return False
|
||||
from hermes_cli.sqlite_safe_read import read_header_bytes_preopen
|
||||
|
||||
head = read_header_bytes_preopen(
|
||||
path, length=max(16, probe_bytes), force=force
|
||||
)
|
||||
if not head or head.startswith(b"SQLite format 3"):
|
||||
if head is None:
|
||||
return False
|
||||
if len(head) == 0:
|
||||
return True
|
||||
if head.startswith(b"SQLite format 3"):
|
||||
return False
|
||||
return all(byte == 0 for byte in head)
|
||||
|
||||
|
||||
@@ -8,12 +8,15 @@ own; methods access the host's attributes (``self._conn``, ``self.db_path``,
|
||||
module-level constants live in hermes_state_common.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Optional, Sequence
|
||||
|
||||
|
||||
from hermes_constants import get_hermes_home
|
||||
from hermes_state_common import (
|
||||
DEFERRED_INDEX_SQL,
|
||||
@@ -1032,6 +1035,17 @@ class SessionSchemaMixin:
|
||||
"INSERT INTO schema_version (version) VALUES (?)",
|
||||
(SCHEMA_VERSION,),
|
||||
)
|
||||
# Record store provenance on creation so fresh vs wiped stores are distinguishable (#97568)
|
||||
now_iso = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
instance_id = str(uuid.uuid4())
|
||||
cursor.executemany(
|
||||
"INSERT OR IGNORE INTO state_meta (key, value) VALUES (?, ?)",
|
||||
[
|
||||
("store_instance_id", instance_id),
|
||||
("store_created_at_utc", now_iso),
|
||||
],
|
||||
)
|
||||
|
||||
else:
|
||||
current_version = row["version"] if isinstance(row, sqlite3.Row) else row[0]
|
||||
# Data migrations that can't be expressed declaratively (row
|
||||
|
||||
@@ -41,6 +41,38 @@ def test_sessiondb_opens_fresh_after_zeroed_quarantine(tmp_path, monkeypatch):
|
||||
sdb.close()
|
||||
|
||||
|
||||
def test_is_zeroed_state_db_zero_byte_quarantine(tmp_path, monkeypatch):
|
||||
"""#97568: a 0-byte file must be detected as zeroed and quarantined."""
|
||||
import hermes_state as hs
|
||||
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
db = tmp_path / "state.db"
|
||||
db.write_bytes(b"") # 0-byte truncated file
|
||||
assert hs.is_zeroed_state_db(db) is True
|
||||
|
||||
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 for the 0-byte file
|
||||
backups = list(tmp_path.glob("state.db.zeroed-*.bak"))
|
||||
assert len(backups) == 1
|
||||
assert backups[0].stat().st_size == 0
|
||||
# Check store provenance was recorded in state_meta
|
||||
row_instance = sdb._conn.execute(
|
||||
"SELECT value FROM state_meta WHERE key = 'store_instance_id'"
|
||||
).fetchone()
|
||||
row_created = sdb._conn.execute(
|
||||
"SELECT value FROM state_meta WHERE key = 'store_created_at_utc'"
|
||||
).fetchone()
|
||||
assert row_instance is not None and row_instance[0]
|
||||
assert row_created is not None and row_created[0]
|
||||
finally:
|
||||
sdb.close()
|
||||
|
||||
|
||||
|
||||
def test_concurrent_quarantine_no_clobber(tmp_path):
|
||||
"""#68805: two concurrent startups must not race on quarantine.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user