From 6b6106fa936de96eb7d6e2432e79303f003bacb2 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Wed, 21 Oct 2026 13:28:53 +0900 Subject: [PATCH 1/4] refactor(search): run POSIX unscoped scan as argv stages, no shell (#2634) Signed-off-by: tahodev <85001874+tahodev@users.noreply.github.com> --- src/mcp/mcp.c | 327 ++++++++++++++++++++++++++++++++++++++++++++++- tests/test_mcp.c | 138 ++++++++++++++++++++ 2 files changed, 461 insertions(+), 4 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 5bd02e4e3..6b79ba94f 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -16474,6 +16474,28 @@ static mcp_scan_cause_t mcp_run_shell_command_cancellable_bounded( uint64_t deadline_ms, bool deadline_enabled, bool deadline_latched, bool exit_one_is_no_match, bool git_child, cbm_proc_result_t *result_out); +#ifndef _WIN32 +/* POSIX-only: supervised run of one argv stage (no shell). Same latch and + * containment contract as the shell runner; the measured file's size plus + * output_base counts toward output_limit so chained stages share one budget. + * tolerate_exit_one maps a plain exit-1 (grep's no-match) to SUCCESS. */ +static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( + cbm_mcp_server_t *srv, const char *bin, const char *const *argv, + const char *const *env_overrides, const char *stdout_capture, uint64_t output_base, + size_t output_limit, uint64_t deadline_ms, bool deadline_enabled, bool tolerate_exit_one, + bool *cancellation_latched, bool *deadline_latched, bool *output_limit_latched, + cbm_proc_result_t *result_out); + +/* POSIX-only: the unscoped search_code scan as find/sort/grep argv stages. */ +static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, + const char *file_pattern, + const char *pattern_file, const char *filelist, + const char *root_path, + char output_path[CBM_SZ_2K], size_t output_limit, + uint64_t deadline_ms, bool deadline_latched, + cbm_proc_result_t *result_out); +#endif + static char *search_code_timeout_result(void) { static const char message[] = "search_code scan exceeded its execution deadline"; static const char fallback[] = @@ -16873,10 +16895,22 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { scan_cause = MCP_SCAN_COMMAND_FAILURE; } } else { - scan_cause = mcp_run_shell_command_cancellable_bounded( - srv, scan_command, output_path, scan_output_limit, scan_deadline_ms, true, - scan_deadline_latched, /*exit_one_is_no_match=*/false, /*git_child=*/false, - &scan_result); +#ifndef _WIN32 + if (!srv->search_scan_command_override) { + /* POSIX unscoped scans run as find/sort/grep argv stages: no + * shell parses search scopes or file patterns anymore (#2634). + * The command override keeps the shell runner for tests. */ + scan_cause = mcp_run_search_scan_argv( + srv, use_regex, file_pattern, tmpfile, filelist, root_path, output_path, + scan_output_limit, scan_deadline_ms, scan_deadline_latched, &scan_result); + } else +#endif + { + scan_cause = mcp_run_shell_command_cancellable_bounded( + srv, scan_command, output_path, scan_output_limit, scan_deadline_ms, true, + scan_deadline_latched, /*exit_one_is_no_match=*/false, /*git_child=*/false, + &scan_result); + } } /* Both POSIX commands wrap grep and map its no-match status to 0, so any * non-zero exit (a failed find/sort, an unreadable operand, a broken @@ -17395,6 +17429,291 @@ static int mcp_run_shell_command_cancellable(cbm_mcp_server_t *srv, const char * return cause == MCP_SCAN_COMMAND_FAILURE || cause == MCP_SCAN_SUPERVISION_FAILURE ? -1 : 0; } +#ifndef _WIN32 +/* Latch poll shared by argv stages: request-cancellation, wall-clock deadline, + * and the cumulative measured-file output budget. Mirrors the latch updates of + * the shell runner so both paths fail the same way. */ +static void mcp_argv_stage_poll_latches(cbm_mcp_server_t *srv, const char *measured_path, + uint64_t output_base, size_t output_limit, + uint64_t deadline_ms, bool deadline_enabled, + bool *cancellation_latched, bool *deadline_latched, + bool *output_limit_latched) { + *cancellation_latched = *cancellation_latched || mcp_request_cancelled(srv); + *deadline_latched = *deadline_latched || (deadline_enabled && cbm_now_ms() >= deadline_ms); + if (!*output_limit_latched && output_limit > 0 && measured_path && measured_path[0]) { + int64_t measured_size = cbm_file_size(measured_path); + *output_limit_latched = + measured_size > 0 && output_base + (uint64_t)measured_size > output_limit; + } +} + +static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( + cbm_mcp_server_t *srv, const char *bin, const char *const *argv, + const char *const *env_overrides, const char *stdout_capture, uint64_t output_base, + size_t output_limit, uint64_t deadline_ms, bool deadline_enabled, bool tolerate_exit_one, + bool *cancellation_latched, bool *deadline_latched, bool *output_limit_latched, + cbm_proc_result_t *result_out) { + memset(result_out, 0, sizeof(*result_out)); + mcp_argv_stage_poll_latches(srv, stdout_capture, output_base, output_limit, deadline_ms, + deadline_enabled, cancellation_latched, deadline_latched, + output_limit_latched); + if (*cancellation_latched || *deadline_latched || *output_limit_latched) { + result_out->tree_quiesced = true; /* no child was spawned */ + return *cancellation_latched ? MCP_SCAN_CANCELLED + : *deadline_latched ? MCP_SCAN_DEADLINE + : MCP_SCAN_OUTPUT_LIMIT; + } + cbm_proc_opts_t options = { + .bin = bin, + .argv = argv, + .env_overrides = env_overrides, + .log_file = NULL, + .quiet_timeout_ms = 0, + .cancel_grace_ms = CBM_SUBPROCESS_DEFAULT_CANCEL_GRACE_MS, + .delete_log_on_exit = false, + .strip_git_repo_env = false, + .stdout_file = stdout_capture, + }; + cbm_subprocess_t *process = NULL; + if (cbm_subprocess_spawn(&options, &process) != 0) { + result_out->tree_quiesced = true; + return MCP_SCAN_COMMAND_FAILURE; + } + cbm_proc_poll_t state; + for (;;) { + mcp_argv_stage_poll_latches(srv, stdout_capture, output_base, output_limit, deadline_ms, + deadline_enabled, cancellation_latched, deadline_latched, + output_limit_latched); + if (*cancellation_latched || *deadline_latched || *output_limit_latched) { + (void)cbm_subprocess_request_cancel(process); + } + state = cbm_subprocess_poll(process, result_out); + if (state != CBM_PROC_POLL_RUNNING) { + break; + } + cbm_usleep(10000); + } + bool contained = state == CBM_PROC_POLL_TERMINAL && result_out->tree_quiesced && + !result_out->supervision_failed; + cbm_subprocess_destroy(process); + mcp_argv_stage_poll_latches(srv, stdout_capture, output_base, output_limit, deadline_ms, + deadline_enabled, cancellation_latched, deadline_latched, + output_limit_latched); + if (!contained) { + return MCP_SCAN_SUPERVISION_FAILURE; + } + if (*cancellation_latched) { + return MCP_SCAN_CANCELLED; + } + if (*deadline_latched) { + return MCP_SCAN_DEADLINE; + } + if (*output_limit_latched) { + return MCP_SCAN_OUTPUT_LIMIT; + } + if (tolerate_exit_one && result_out->outcome == CBM_PROC_EXIT_NONZERO && + result_out->exit_code == 1) { + return MCP_SCAN_SUCCESS; /* ordinary grep no-match */ + } + return result_out->outcome == CBM_PROC_CLEAN ? MCP_SCAN_SUCCESS + : MCP_SCAN_CONTAINED_COMMAND_FAILURE; +} + +/* Number of file operands per grep spawn. Bounded so argv stays far below + * ARG_MAX even with long paths, like the xargs batching it replaces. */ +#define MCP_ARGV_SCAN_GREP_CHUNK 256 + +/* The unscoped (unindexed or not-fully-indexed root) search_code scan as three + * argv stages, with no shell anywhere: find writes the NUL-delimited candidate + * list, LC_ALL=C sort orders it in place, and grep scans it in chunks. Search + * scopes, file patterns and the pattern file now reach the tools as plain + * arguments instead of shell-string interpolations (#2634). Exit codes keep + * the old pipeline's contract: find/sort failures fail closed, grep's exit 1 + * is an ordinary no-match, and an empty list is a successful empty scan. */ +static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, + const char *file_pattern, + const char *pattern_file, const char *filelist, + const char *root_path, + char output_path[CBM_SZ_2K], size_t output_limit, + uint64_t deadline_ms, bool deadline_latched, + cbm_proc_result_t *result_out) { + bool cancellation_latched = false; + bool output_limit_latched = false; + + const char *find_argv[9]; + int fa = 0; + find_argv[fa++] = "find"; + find_argv[fa++] = root_path; + find_argv[fa++] = "-type"; + find_argv[fa++] = "f"; + if (file_pattern) { + find_argv[fa++] = "-name"; + find_argv[fa++] = file_pattern; + } + find_argv[fa++] = "-print0"; + find_argv[fa] = NULL; + mcp_scan_cause_t cause = mcp_run_argv_stage_cancellable_bounded( + srv, "find", find_argv, NULL, filelist, 0, 0, deadline_ms, true, false, + &cancellation_latched, &deadline_latched, &output_limit_latched, result_out); + if (cause != MCP_SCAN_SUCCESS) { + return cause; + } + + const char *sort_argv[] = {"sort", "-z", "-o", filelist, filelist, NULL}; + const char *sort_env[] = {"LC_ALL=C", NULL}; + cause = mcp_run_argv_stage_cancellable_bounded( + srv, "sort", sort_argv, sort_env, NULL, 0, 0, deadline_ms, true, false, + &cancellation_latched, &deadline_latched, &output_limit_latched, result_out); + if (cause != MCP_SCAN_SUCCESS) { + return cause; + } + + if (!mcp_command_output_path(output_path)) { + return MCP_SCAN_COMMAND_FAILURE; + } + FILE *out = cbm_fopen(output_path, "wb"); + if (!out) { + (void)cbm_unlink(output_path); + output_path[0] = '\0'; + return MCP_SCAN_COMMAND_FAILURE; + } + FILE *list = cbm_fopen(filelist, "rb"); + if (!list) { + (void)fclose(out); + (void)cbm_unlink(output_path); + output_path[0] = '\0'; + return MCP_SCAN_COMMAND_FAILURE; + } + + const char *grep_flag = use_regex ? "-E" : "-F"; + const char **chunk_argv = + malloc(((size_t)MCP_ARGV_SCAN_GREP_CHUNK + 7) * sizeof(*chunk_argv)); + char **chunk_paths = malloc((size_t)MCP_ARGV_SCAN_GREP_CHUNK * sizeof(*chunk_paths)); + char *pathbuf = malloc(CBM_SZ_4K); + if (!chunk_argv || !chunk_paths || !pathbuf) { + free(chunk_argv); + free(chunk_paths); + free(pathbuf); + (void)fclose(list); + (void)fclose(out); + (void)cbm_unlink(output_path); + output_path[0] = '\0'; + return MCP_SCAN_COMMAND_FAILURE; + } + size_t pathcap = CBM_SZ_4K; + size_t pathlen = 0; + uint64_t output_bytes = 0; + size_t seq = 0; + cause = MCP_SCAN_SUCCESS; + bool io_error = false; + while (cause == MCP_SCAN_SUCCESS) { + size_t n = 0; + while (n < MCP_ARGV_SCAN_GREP_CHUNK) { + int c = fgetc(list); + if (c == EOF) { + if (ferror(list)) { + io_error = true; + } + break; + } + if (c == '\0') { + if (pathlen > 0) { + pathbuf[pathlen] = '\0'; + chunk_paths[n] = cbm_strdup(pathbuf); + if (!chunk_paths[n]) { + io_error = true; + break; + } + n++; + pathlen = 0; + } + continue; + } + if (pathlen + 1 >= pathcap) { + size_t newcap = pathcap * 2; + char *grown = realloc(pathbuf, newcap); + if (!grown) { + io_error = true; + break; + } + pathbuf = grown; + pathcap = newcap; + } + pathbuf[pathlen++] = (char)c; + } + if (io_error || n == 0) { + for (size_t i = 0; i < n; i++) { + free(chunk_paths[i]); + } + break; + } + + char chunk_path[CBM_SZ_2K]; + int written = snprintf(chunk_path, sizeof(chunk_path), "%s.grep%zu", output_path, seq++); + if (written <= 0 || (size_t)written >= sizeof(chunk_path)) { + io_error = true; + } else { + chunk_argv[0] = "grep"; + chunk_argv[1] = "-Hn"; + chunk_argv[2] = grep_flag; + chunk_argv[3] = "-f"; + chunk_argv[4] = pattern_file; + chunk_argv[5] = "--"; + for (size_t i = 0; i < n; i++) { + chunk_argv[6 + i] = chunk_paths[i]; + } + chunk_argv[6 + n] = NULL; + cause = mcp_run_argv_stage_cancellable_bounded( + srv, "grep", chunk_argv, NULL, chunk_path, output_bytes, output_limit, + deadline_ms, true, true, &cancellation_latched, &deadline_latched, + &output_limit_latched, result_out); + if (cause == MCP_SCAN_SUCCESS) { + FILE *chunk = cbm_fopen(chunk_path, "rb"); + if (!chunk) { + cause = MCP_SCAN_COMMAND_FAILURE; + } else { + char copybuf[CBM_SZ_16K]; + size_t got; + while ((got = fread(copybuf, 1, sizeof(copybuf), chunk)) > 0) { + if (fwrite(copybuf, 1, got, out) != got) { + cause = MCP_SCAN_COMMAND_FAILURE; + break; + } + output_bytes += got; + } + if (ferror(chunk) && cause == MCP_SCAN_SUCCESS) { + cause = MCP_SCAN_COMMAND_FAILURE; + } + (void)fclose(chunk); + } + } + (void)cbm_unlink(chunk_path); + } + for (size_t i = 0; i < n; i++) { + free(chunk_paths[i]); + } + } + free(chunk_argv); + free(chunk_paths); + free(pathbuf); + (void)fclose(list); + if (fclose(out) != 0 && cause == MCP_SCAN_SUCCESS) { + cause = MCP_SCAN_COMMAND_FAILURE; + } + if (io_error && cause == MCP_SCAN_SUCCESS) { + cause = MCP_SCAN_COMMAND_FAILURE; + } + if (cause == MCP_SCAN_SUCCESS && output_limit > 0 && output_bytes > output_limit) { + cause = MCP_SCAN_OUTPUT_LIMIT; + } + if (cause != MCP_SCAN_SUCCESS) { + (void)cbm_unlink(output_path); + output_path[0] = '\0'; + } + return cause; +} +#endif + /* Does `node`'s line range overlap any recorded hunk for `file`? Used to scope * seed detection to the actually-changed lines rather than the whole file. * Non-static (declared in mcp_internal.h) so tests can exercise the overlap diff --git a/tests/test_mcp.c b/tests/test_mcp.c index 80a320ca5..57262d8a6 100644 --- a/tests/test_mcp.c +++ b/tests/test_mcp.c @@ -12420,6 +12420,141 @@ TEST(search_code_output_limit_fails_closed_and_cleans_scan) { PASS(); } +/* #2634: the POSIX unscoped scan runs find/sort/grep as argv stages with no + * shell, so file names carrying shell metacharacters are plain operands, not + * command text. Pin that such files are discovered and scanned. */ +TEST(search_code_unscoped_argv_scans_metachar_filenames_issue2634) { +#ifdef _WIN32 + SKIP_PLATFORM("argv scan stages are POSIX-only"); +#else + char tmp[CBM_SZ_512]; + snprintf(tmp, sizeof(tmp), "%s/cbm-search-argv-meta-XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + + char hostile[CBM_SZ_1K]; + snprintf(hostile, sizeof(hostile), "%s/we'ird ;na`me$.go", tmp); + ASSERT_EQ(th_write_file(hostile, + "package main\n\nfunc HandleHostile2634() error {\n\treturn nil\n}\n"), + 0); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *store = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(store); + const char *project = "search-argv-metachar"; + cbm_mcp_server_set_project(srv, project); + ASSERT_EQ(cbm_store_upsert_project(store, project, tmp), CBM_STORE_OK); + + char *resp = cbm_mcp_handle_tool(srv, "search_code", + "{\"pattern\":\"HandleHostile2634\"," + "\"project\":\"search-argv-metachar\",\"format\":\"json\"}"); + ASSERT_NOT_NULL(resp); + ASSERT_TRUE(strstr(resp, "\"isError\":true") == NULL); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "we'ird ;na`me$.go")); + ASSERT_NOT_NULL(strstr(inner, "\"total_grep_matches\":1")); + free(inner); + free(resp); + cbm_mcp_server_free(srv); + ASSERT_EQ(th_rmtree(tmp), 0); + PASS(); +#endif +} + +/* #2634: grep's exit 1 (no match in a chunk) is an ordinary empty result, not + * a scan failure — the same contract the retired xargs wrapper encoded. */ +TEST(search_code_unscoped_argv_no_match_is_success_issue2634) { +#ifdef _WIN32 + SKIP_PLATFORM("argv scan stages are POSIX-only"); +#else + char tmp[CBM_SZ_512]; + snprintf(tmp, sizeof(tmp), "%s/cbm-search-argv-nomatch-XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + + char src_path[CBM_SZ_1K]; + snprintf(src_path, sizeof(src_path), "%s/handler.go", tmp); + ASSERT_EQ(th_write_file(src_path, + "package main\n\nfunc HandlePresent2634() error {\n\treturn nil\n}\n"), + 0); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *store = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(store); + const char *project = "search-argv-nomatch"; + cbm_mcp_server_set_project(srv, project); + ASSERT_EQ(cbm_store_upsert_project(store, project, tmp), CBM_STORE_OK); + + char *resp = cbm_mcp_handle_tool(srv, "search_code", + "{\"pattern\":\"NoSuchSymbol2634\"," + "\"project\":\"search-argv-nomatch\",\"format\":\"json\"}"); + ASSERT_NOT_NULL(resp); + ASSERT_TRUE(strstr(resp, "\"isError\":true") == NULL); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "\"total_grep_matches\":0")); + ASSERT_NOT_NULL(strstr(inner, "\"total_relation\":\"eq\"")); + free(inner); + free(resp); + cbm_mcp_server_free(srv); + ASSERT_EQ(th_rmtree(tmp), 0); + PASS(); +#endif +} + +/* #2634: the unscoped file_pattern reaches find as a -name operand, so a + * non-matching extension is excluded from the scan without any shell glob. */ +TEST(search_code_unscoped_argv_honours_file_pattern_issue2634) { +#ifdef _WIN32 + SKIP_PLATFORM("argv scan stages are POSIX-only"); +#else + char tmp[CBM_SZ_512]; + snprintf(tmp, sizeof(tmp), "%s/cbm-search-argv-glob-XXXXXX", cbm_tmpdir()); + ASSERT_NOT_NULL(cbm_mkdtemp(tmp)); + + char py[CBM_SZ_1K]; + snprintf(py, sizeof(py), "%s/handler.py", tmp); + ASSERT_EQ(th_write_file(py, "def handle_python2634():\n return 1\n"), 0); + + cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL); + ASSERT_NOT_NULL(srv); + cbm_store_t *store = cbm_mcp_server_store(srv); + ASSERT_NOT_NULL(store); + const char *project = "search-argv-glob"; + cbm_mcp_server_set_project(srv, project); + ASSERT_EQ(cbm_store_upsert_project(store, project, tmp), CBM_STORE_OK); + + char *resp = cbm_mcp_handle_tool(srv, "search_code", + "{\"pattern\":\"handle_python2634\"," + "\"project\":\"search-argv-glob\"," + "\"file_pattern\":\"*.go\",\"format\":\"json\"}"); + ASSERT_NOT_NULL(resp); + ASSERT_TRUE(strstr(resp, "\"isError\":true") == NULL); + char *inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_TRUE(strstr(inner, "handler.py") == NULL); + ASSERT_NOT_NULL(strstr(inner, "\"total_grep_matches\":0")); + free(inner); + free(resp); + + resp = cbm_mcp_handle_tool(srv, "search_code", + "{\"pattern\":\"handle_python2634\"," + "\"project\":\"search-argv-glob\",\"format\":\"json\"}"); + ASSERT_NOT_NULL(resp); + ASSERT_TRUE(strstr(resp, "\"isError\":true") == NULL); + inner = extract_text_content(resp); + ASSERT_NOT_NULL(inner); + ASSERT_NOT_NULL(strstr(inner, "handler.py")); + ASSERT_NOT_NULL(strstr(inner, "\"total_grep_matches\":1")); + free(inner); + free(resp); + cbm_mcp_server_free(srv); + ASSERT_EQ(th_rmtree(tmp), 0); + PASS(); +#endif +} + TEST(search_code_scan_deadline_fails_closed_and_resets) { mcp_search_cache_t cache; ASSERT_TRUE(mcp_search_cache_open(&cache, "cbm-search-deadline")); @@ -25224,6 +25359,9 @@ SUITE(mcp) { RUN_TEST(search_code_file_pattern_prefilter_boundaries); RUN_TEST(search_code_windows_scope_prefilter_removes_pipeline_filter); RUN_TEST(search_code_cancel_cleans_supervised_scan); + RUN_TEST(search_code_unscoped_argv_scans_metachar_filenames_issue2634); + RUN_TEST(search_code_unscoped_argv_no_match_is_success_issue2634); + RUN_TEST(search_code_unscoped_argv_honours_file_pattern_issue2634); RUN_TEST(search_code_output_limit_fails_closed_and_cleans_scan); RUN_TEST(search_code_scan_deadline_fails_closed_and_resets); RUN_TEST(search_code_scan_deadline_override_is_per_server); From eaf5b865d5af9cc6938d6076052937474a2654f5 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:27:48 +0900 Subject: [PATCH 2/4] Feed the unscoped search file list to search_scan_in_process instead of chunked grep Signed-off-by: tahodev <85001874+tahodev@users.noreply.github.com> --- src/mcp/mcp.c | 165 ++++++-------------------------------------------- 1 file changed, 20 insertions(+), 145 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 504e1d83a..5c1ee41e1 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -17326,10 +17326,11 @@ static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( bool *cancellation_latched, bool *deadline_latched, bool *output_limit_latched, cbm_proc_result_t *result_out); -/* POSIX-only: the unscoped search_code scan as find/sort/grep argv stages. */ +/* POSIX-only: the unscoped search_code scan as find/sort argv stages feeding the + * in-process scanner. */ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, const char *file_pattern, - const char *pattern_file, const char *filelist, + const char *pattern, const char *filelist, const char *root_path, char output_path[CBM_SZ_2K], size_t output_limit, uint64_t deadline_ms, bool deadline_latched, @@ -17741,7 +17742,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) { * shell parses search scopes or file patterns anymore (#2634). * The command override keeps the shell runner for tests. */ scan_cause = mcp_run_search_scan_argv( - srv, use_regex, file_pattern, tmpfile, filelist, root_path, output_path, + srv, use_regex, file_pattern, pattern, filelist, root_path, output_path, scan_output_limit, scan_deadline_ms, scan_deadline_latched, &scan_result); } else #endif @@ -18360,20 +18361,16 @@ static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( : MCP_SCAN_CONTAINED_COMMAND_FAILURE; } -/* Number of file operands per grep spawn. Bounded so argv stays far below - * ARG_MAX even with long paths, like the xargs batching it replaces. */ -#define MCP_ARGV_SCAN_GREP_CHUNK 256 - -/* The unscoped (unindexed or not-fully-indexed root) search_code scan as three - * argv stages, with no shell anywhere: find writes the NUL-delimited candidate - * list, LC_ALL=C sort orders it in place, and grep scans it in chunks. Search - * scopes, file patterns and the pattern file now reach the tools as plain - * arguments instead of shell-string interpolations (#2634). Exit codes keep - * the old pipeline's contract: find/sort failures fail closed, grep's exit 1 - * is an ordinary no-match, and an empty list is a successful empty scan. */ +/* The unscoped (unindexed or not-fully-indexed root) search_code scan with no + * shell anywhere: find writes the NUL-delimited candidate list and LC_ALL=C + * sort orders it in place, both as argv stages, then the list feeds the + * in-process scanner (search_scan_in_process). Search scopes and file patterns + * reach find as plain arguments instead of shell-string interpolations (#2634). + * find/sort failures fail closed, and an empty list is a successful empty + * scan. */ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, const char *file_pattern, - const char *pattern_file, const char *filelist, + const char *pattern, const char *filelist, const char *root_path, char output_path[CBM_SZ_2K], size_t output_limit, uint64_t deadline_ms, bool deadline_latched, @@ -18418,140 +18415,18 @@ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use output_path[0] = '\0'; return MCP_SCAN_COMMAND_FAILURE; } - FILE *list = cbm_fopen(filelist, "rb"); - if (!list) { - (void)fclose(out); - (void)cbm_unlink(output_path); - output_path[0] = '\0'; - return MCP_SCAN_COMMAND_FAILURE; + /* The sorted, NUL-separated file list feeds the same in-process scanner an + * indexed project uses, so matching has one implementation. */ + mcp_scan_cause_t scan_cause = search_scan_in_process(srv, filelist, pattern, use_regex, out, + output_limit, deadline_ms); + if (fclose(out) != 0 && scan_cause == MCP_SCAN_SUCCESS) { + scan_cause = MCP_SCAN_COMMAND_FAILURE; } - - const char *grep_flag = use_regex ? "-E" : "-F"; - const char **chunk_argv = - malloc(((size_t)MCP_ARGV_SCAN_GREP_CHUNK + 7) * sizeof(*chunk_argv)); - char **chunk_paths = malloc((size_t)MCP_ARGV_SCAN_GREP_CHUNK * sizeof(*chunk_paths)); - char *pathbuf = malloc(CBM_SZ_4K); - if (!chunk_argv || !chunk_paths || !pathbuf) { - free(chunk_argv); - free(chunk_paths); - free(pathbuf); - (void)fclose(list); - (void)fclose(out); + if (scan_cause != MCP_SCAN_SUCCESS) { (void)cbm_unlink(output_path); output_path[0] = '\0'; - return MCP_SCAN_COMMAND_FAILURE; } - size_t pathcap = CBM_SZ_4K; - size_t pathlen = 0; - uint64_t output_bytes = 0; - size_t seq = 0; - cause = MCP_SCAN_SUCCESS; - bool io_error = false; - while (cause == MCP_SCAN_SUCCESS) { - size_t n = 0; - while (n < MCP_ARGV_SCAN_GREP_CHUNK) { - int c = fgetc(list); - if (c == EOF) { - if (ferror(list)) { - io_error = true; - } - break; - } - if (c == '\0') { - if (pathlen > 0) { - pathbuf[pathlen] = '\0'; - chunk_paths[n] = cbm_strdup(pathbuf); - if (!chunk_paths[n]) { - io_error = true; - break; - } - n++; - pathlen = 0; - } - continue; - } - if (pathlen + 1 >= pathcap) { - size_t newcap = pathcap * 2; - char *grown = realloc(pathbuf, newcap); - if (!grown) { - io_error = true; - break; - } - pathbuf = grown; - pathcap = newcap; - } - pathbuf[pathlen++] = (char)c; - } - if (io_error || n == 0) { - for (size_t i = 0; i < n; i++) { - free(chunk_paths[i]); - } - break; - } - - char chunk_path[CBM_SZ_2K]; - int written = snprintf(chunk_path, sizeof(chunk_path), "%s.grep%zu", output_path, seq++); - if (written <= 0 || (size_t)written >= sizeof(chunk_path)) { - io_error = true; - } else { - chunk_argv[0] = "grep"; - chunk_argv[1] = "-Hn"; - chunk_argv[2] = grep_flag; - chunk_argv[3] = "-f"; - chunk_argv[4] = pattern_file; - chunk_argv[5] = "--"; - for (size_t i = 0; i < n; i++) { - chunk_argv[6 + i] = chunk_paths[i]; - } - chunk_argv[6 + n] = NULL; - cause = mcp_run_argv_stage_cancellable_bounded( - srv, "grep", chunk_argv, NULL, chunk_path, output_bytes, output_limit, - deadline_ms, true, true, &cancellation_latched, &deadline_latched, - &output_limit_latched, result_out); - if (cause == MCP_SCAN_SUCCESS) { - FILE *chunk = cbm_fopen(chunk_path, "rb"); - if (!chunk) { - cause = MCP_SCAN_COMMAND_FAILURE; - } else { - char copybuf[CBM_SZ_16K]; - size_t got; - while ((got = fread(copybuf, 1, sizeof(copybuf), chunk)) > 0) { - if (fwrite(copybuf, 1, got, out) != got) { - cause = MCP_SCAN_COMMAND_FAILURE; - break; - } - output_bytes += got; - } - if (ferror(chunk) && cause == MCP_SCAN_SUCCESS) { - cause = MCP_SCAN_COMMAND_FAILURE; - } - (void)fclose(chunk); - } - } - (void)cbm_unlink(chunk_path); - } - for (size_t i = 0; i < n; i++) { - free(chunk_paths[i]); - } - } - free(chunk_argv); - free(chunk_paths); - free(pathbuf); - (void)fclose(list); - if (fclose(out) != 0 && cause == MCP_SCAN_SUCCESS) { - cause = MCP_SCAN_COMMAND_FAILURE; - } - if (io_error && cause == MCP_SCAN_SUCCESS) { - cause = MCP_SCAN_COMMAND_FAILURE; - } - if (cause == MCP_SCAN_SUCCESS && output_limit > 0 && output_bytes > output_limit) { - cause = MCP_SCAN_OUTPUT_LIMIT; - } - if (cause != MCP_SCAN_SUCCESS) { - (void)cbm_unlink(output_path); - output_path[0] = '\0'; - } - return cause; + return scan_cause; } #endif From 9a0742ed4521156575063692bade8b088bd79037 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Mon, 2 Nov 2026 05:29:05 +0900 Subject: [PATCH 3/4] Lower the memory-core baseline for src/mcp/mcp.c Signed-off-by: tahodev <85001874+tahodev@users.noreply.github.com> --- scripts/memory-core-baseline.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/memory-core-baseline.txt b/scripts/memory-core-baseline.txt index d9f632e7c..7e979160d 100644 --- a/scripts/memory-core-baseline.txt +++ b/scripts/memory-core-baseline.txt @@ -51,7 +51,7 @@ src/git/git_context.c 18 src/main.c 32 src/mcp/compact_out.c 39 src/mcp/index_supervisor.c 8 -src/mcp/mcp.c 769 +src/mcp/mcp.c 765 src/pipeline/artifact.c 38 src/pipeline/fqn.c 23 src/pipeline/lsp_resolve.h 4 From 55200a6cfd9da24acdad9f18207efdbdb37b9b9b Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:25:32 +0900 Subject: [PATCH 4/4] Format the new search scan hunks for clang-format-20 Signed-off-by: tahodev <85001874+tahodev@users.noreply.github.com> --- src/mcp/mcp.c | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/mcp/mcp.c b/src/mcp/mcp.c index 5c1ee41e1..463aa8d80 100644 --- a/src/mcp/mcp.c +++ b/src/mcp/mcp.c @@ -17329,9 +17329,8 @@ static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( /* POSIX-only: the unscoped search_code scan as find/sort argv stages feeding the * in-process scanner. */ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, - const char *file_pattern, - const char *pattern, const char *filelist, - const char *root_path, + const char *file_pattern, const char *pattern, + const char *filelist, const char *root_path, char output_path[CBM_SZ_2K], size_t output_limit, uint64_t deadline_ms, bool deadline_latched, cbm_proc_result_t *result_out); @@ -18301,9 +18300,10 @@ static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( output_limit_latched); if (*cancellation_latched || *deadline_latched || *output_limit_latched) { result_out->tree_quiesced = true; /* no child was spawned */ - return *cancellation_latched ? MCP_SCAN_CANCELLED - : *deadline_latched ? MCP_SCAN_DEADLINE - : MCP_SCAN_OUTPUT_LIMIT; + if (*cancellation_latched) { + return MCP_SCAN_CANCELLED; + } + return *deadline_latched ? MCP_SCAN_DEADLINE : MCP_SCAN_OUTPUT_LIMIT; } cbm_proc_opts_t options = { .bin = bin, @@ -18369,9 +18369,8 @@ static mcp_scan_cause_t mcp_run_argv_stage_cancellable_bounded( * find/sort failures fail closed, and an empty list is a successful empty * scan. */ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use_regex, - const char *file_pattern, - const char *pattern, const char *filelist, - const char *root_path, + const char *file_pattern, const char *pattern, + const char *filelist, const char *root_path, char output_path[CBM_SZ_2K], size_t output_limit, uint64_t deadline_ms, bool deadline_latched, cbm_proc_result_t *result_out) { @@ -18417,8 +18416,8 @@ static mcp_scan_cause_t mcp_run_search_scan_argv(cbm_mcp_server_t *srv, bool use } /* The sorted, NUL-separated file list feeds the same in-process scanner an * indexed project uses, so matching has one implementation. */ - mcp_scan_cause_t scan_cause = search_scan_in_process(srv, filelist, pattern, use_regex, out, - output_limit, deadline_ms); + mcp_scan_cause_t scan_cause = + search_scan_in_process(srv, filelist, pattern, use_regex, out, output_limit, deadline_ms); if (fclose(out) != 0 && scan_cause == MCP_SCAN_SUCCESS) { scan_cause = MCP_SCAN_COMMAND_FAILURE; }