Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/mcp/mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2003,6 +2003,14 @@ static bool quarantine_corrupt_store(cbm_mcp_server_t *srv, const char *project,
char *backup_out, size_t backup_out_size) {
char backup[CBM_SZ_2K];
char pending[CBM_SZ_2K];
/* #1425 belt-and-braces: an empty store path would render the backup as a
* bare relative ".corrupt.<hex>" in the process cwd. There is nothing at
* such a path worth quarantining. */
if (!path || !path[0]) {
cbm_log_error("store.auto_clean_failed", "project", project, "path", "", "reason",
"empty store path");
return false;
}
if (!reserve_unique_corrupt_pending(path, pending, sizeof(pending), backup, sizeof(backup))) {
cbm_log_error("store.auto_clean_failed", "project", project, "path", path, "reason",
"cannot reserve unique backup");
Expand Down Expand Up @@ -2090,10 +2098,16 @@ static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *pr
}

/* Open project's .db file — query-only open (no SQLITE_OPEN_CREATE) to
* prevent ghost .db file creation for unknown/unindexed projects. */
* prevent ghost .db file creation for unknown/unindexed projects.
* #1425: an invalid project name yields an empty path. SQLite opens ""
* as an anonymous temp db, which then fails the integrity check and
* quarantines a db that never existed — as a RELATIVE .corrupt.<hex>
* file in the daemon's cwd. Skip the direct open entirely; the fallback
* scan below still resolves legacy dbs whose internal name predates
* validation. */
char path[CBM_SZ_1K];
project_db_path(project, path, sizeof(path));
srv->store = cbm_store_open_path_query(path);
srv->store = path[0] ? cbm_store_open_path_query(path) : NULL;
if (srv->store) {
/* Check DB integrity — back up (never silently delete) a corrupt DB */
if (!cbm_store_check_integrity(srv->store)) {
Expand Down
48 changes: 48 additions & 0 deletions tests/test_mcp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2551,6 +2551,53 @@ TEST(tool_trace_call_path_not_found) {
PASS();
}

/* Regression for #1425: a project name that fails validation must produce a
* clean "not found" error and NOTHING else. project_db_path() yields "" for
* such names; SQLite opens "" as an anonymous temp db, its integrity check
* fails, and quarantine rendered "".corrupt.<hex> - a RELATIVE path dropped
* into the daemon's cwd on every such query. */
TEST(tool_call_invalid_project_name_leaves_no_corrupt_litter_issue1425) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/mcp-litter-XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("cbm_mkdtemp failed");
char oldcwd[CBM_SZ_1K];
if (!cbm_getcwd(oldcwd, sizeof(oldcwd)))
FAIL("getcwd failed");
if (cbm_chdir(tmpdir) != 0)
FAIL("chdir failed");

cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
char *resp =
cbm_mcp_server_handle(srv, "{\"jsonrpc\":\"2.0\",\"id\":30,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_graph\","
"\"arguments\":{\"name_pattern\":\"x\","
"\"project\":\"bad name\"}}}");
bool clean_error = resp && strstr(resp, "not found") != NULL;
free(resp);
cbm_mcp_server_free(srv);

int litter = 0;
cbm_dir_t *dir = cbm_opendir(tmpdir);
if (dir) {
cbm_dirent_t *entry;
while ((entry = cbm_readdir(dir)) != NULL) {
if (strstr(entry->name, ".corrupt.")) {
litter++;
}
}
cbm_closedir(dir);
}
if (cbm_chdir(oldcwd) != 0)
FAIL("chdir back failed");
th_rmtree(tmpdir);
if (!clean_error)
FAIL("invalid project name must produce a clean not-found error");
if (litter != 0)
FAIL("invalid project name must not quarantine an anonymous temp db into cwd (#1425)");
PASS();
}

TEST(tool_trace_missing_function_name) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);

Expand Down Expand Up @@ -10352,6 +10399,7 @@ SUITE(mcp) {

/* Tool handlers with validation */
RUN_TEST(tool_trace_call_path_not_found);
RUN_TEST(tool_call_invalid_project_name_leaves_no_corrupt_litter_issue1425);
RUN_TEST(tool_trace_missing_function_name);
RUN_TEST(tool_trace_call_path_ambiguous);
RUN_TEST(tool_trace_union_records_min_hop_across_seeds);
Expand Down
Loading