From 9cf62da97b327e69130cc9491cc970ad9692d0bd Mon Sep 17 00:00:00 2001 From: Adrianno Esnarriaga Sereno Date: Sun, 13 Sep 2026 04:10:46 -0300 Subject: [PATCH 1/3] fix(memory): preserve existing DB when sqlite3_open fails Skip remove() when sqlite3_open fails on a path that already exists. Deleting it caused silent data loss on permissions or transient I/O. Refs: #63 --- src/core/memory.c | 5 +++++ tests/test_memory.c | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/core/memory.c b/src/core/memory.c index a949f93..2f48367 100644 --- a/src/core/memory.c +++ b/src/core/memory.c @@ -131,6 +131,11 @@ int memory_init(const char *path) int recreated = 0; if (sqlite3_open(path, &g_db) != SQLITE_OK) { if (g_db) { sqlite3_close(g_db); g_db = NULL; } + /* 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\n", path); + return -1; + } remove(path); if (sqlite3_open(path, &g_db) != SQLITE_OK) { if (g_db) sqlite3_close(g_db); diff --git a/tests/test_memory.c b/tests/test_memory.c index a72ae47..9347f0f 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,31 @@ 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"; + remove(path); + ASSERT(memory_init(path) == 0); + ASSERT(memory_save("preserve", "important data", NULL) == 0); + memory_cleanup(); + ASSERT(chmod(path, 0000) == 0); + ASSERT(memory_init(path) == -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()); From c6717f3fdce01a75bd7da3209fe1d0b0a0055e50 Mon Sep 17 00:00:00 2001 From: Adrianno Esnarriaga Sereno Date: Sun, 13 Sep 2026 04:10:46 -0300 Subject: [PATCH 2/3] docs(memory): changelog preserve existing DB on open failure Refs: #63 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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`. From 5f8d4204d66f2a79c3df4c2d46cb50fad1f0084b Mon Sep 17 00:00:00 2001 From: Adrianno Esnarriaga Sereno Date: Sun, 13 Sep 2026 04:28:11 -0300 Subject: [PATCH 3/3] fix(memory): log sqlite errmsg on preserved-DB open failure Capture sqlite3_errmsg before close. Skip the chmod-000 case when sqlite3_open still succeeds (root/DAC), and restore mode 0600 if the init-fail assertion fires. Refs: #63 --- src/core/memory.c | 6 ++++-- tests/test_memory.c | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/core/memory.c b/src/core/memory.c index 2f48367..b6fc955 100644 --- a/src/core/memory.c +++ b/src/core/memory.c @@ -130,12 +130,14 @@ int memory_init(const char *path) int file_existed = path_exists(path); int recreated = 0; if (sqlite3_open(path, &g_db) != SQLITE_OK) { - if (g_db) { sqlite3_close(g_db); g_db = NULL; } /* 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\n", path); + 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) { if (g_db) sqlite3_close(g_db); diff --git a/tests/test_memory.c b/tests/test_memory.c index 9347f0f..1a61b91 100644 --- a/tests/test_memory.c +++ b/tests/test_memory.c @@ -157,12 +157,29 @@ static int test_gateway_schema_migration_v01(void) 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); - ASSERT(memory_init(path) == -1); + 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];