diff --git a/CHANGELOG.md b/CHANGELOG.md index bc52426..955f8d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha ## [Unreleased] ### Fixed +- `memory_init` no longer deletes an existing SQLite DB when `sqlite3_open` fails (permissions or transient I/O). - Anthropic `content` parse fails closed when growing the text buffer or `tool_use` array cannot `realloc`, instead of copying against an inflated cap. - HTTP 200 JSON-RPC results with a malformed ASAP envelope no longer double-free the duplicated request id. - Inbound ASAP `mcp.tool_call` and `state.query` now hold `agent_lock()` around tool execute and SQLite `g_db` reads, matching `task.request`. diff --git a/src/core/memory.c b/src/core/memory.c index a949f93..b6fc955 100644 --- a/src/core/memory.c +++ b/src/core/memory.c @@ -130,6 +130,13 @@ int memory_init(const char *path) int file_existed = path_exists(path); int recreated = 0; if (sqlite3_open(path, &g_db) != SQLITE_OK) { + /* Never delete an existing DB on open failure (permissions, transient I/O). */ + if (file_existed) { + fprintf(stderr, "Error: cannot open existing memory DB at %s: %s\n", + path, g_db ? sqlite3_errmsg(g_db) : "unknown"); + if (g_db) { sqlite3_close(g_db); g_db = NULL; } + return -1; + } if (g_db) { sqlite3_close(g_db); g_db = NULL; } remove(path); if (sqlite3_open(path, &g_db) != SQLITE_OK) { diff --git a/tests/test_memory.c b/tests/test_memory.c index a72ae47..1a61b91 100644 --- a/tests/test_memory.c +++ b/tests/test_memory.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "sqlite3.h" @@ -153,11 +154,48 @@ static int test_gateway_schema_migration_v01(void) return 0; } +static int test_existing_db_preserved_on_open_failure(void) +{ + const char *path = "/tmp/shellclaw_test_open_fail.db"; + sqlite3 *probe = NULL; + int open_rc; + remove(path); + ASSERT(memory_init(path) == 0); + ASSERT(memory_save("preserve", "important data", NULL) == 0); + memory_cleanup(); + ASSERT(chmod(path, 0000) == 0); + open_rc = sqlite3_open(path, &probe); + if (probe) + sqlite3_close(probe); + if (open_rc == SQLITE_OK) { + /* Root / DAC override: mode 000 still opens. Do not delete. */ + (void)chmod(path, 0600); + remove(path); + return 0; + } + if (memory_init(path) != -1) { + fprintf(stderr, "FAIL: %s:%d memory_init(path) == -1\n", __FILE__, __LINE__); + (void)chmod(path, 0600); + memory_cleanup(); + remove(path); + return 1; + } + ASSERT(chmod(path, 0600) == 0); + ASSERT(memory_init(path) == 0); + char buf[256]; + ASSERT(memory_recall("important", buf, sizeof(buf), 5) == 0); + ASSERT(strstr(buf, "important data") != NULL); + memory_cleanup(); + remove(path); + return 0; +} + int main(void) { RUN(test_schema_and_fts5()); RUN(test_save_overwrite()); RUN(test_corrupted_db_recreated()); + RUN(test_existing_db_preserved_on_open_failure()); RUN(test_session_list()); RUN(test_session_crud()); RUN(test_gateway_schema_new_db());