@@ -123,6 +123,7 @@ enum {
123123 * growing getline buffers without bound through ignored extension headers. */
124124#define MCP_MAX_MESSAGE_SIZE ((size_t)10U * 1024U * 1024U)
125125#define MCP_MAX_HEADER_SIZE ((size_t)8U * 1024U)
126+ #define MCP_SEARCH_OUTPUT_MAX ((size_t)64U * 1024U * 1024U)
126127
127128/* ── Helpers ────────────────────────────────────────────────────── */
128129
@@ -1569,6 +1570,7 @@ struct cbm_mcp_server {
15691570 void *quarantine_test_context;
15701571 cbm_mcp_command_test_hook_fn command_test_hook;
15711572 void *command_test_context;
1573+ size_t search_output_limit_override;
15721574 cbm_thread_t autoindex_tid;
15731575 bool autoindex_active; /* true if auto-index thread was started */
15741576
@@ -1867,6 +1869,12 @@ void cbm_mcp_server_set_command_test_hook(cbm_mcp_server_t *srv, cbm_mcp_command
18671869 srv->command_test_context = context;
18681870}
18691871
1872+ void cbm_mcp_server_set_search_output_limit_for_test(cbm_mcp_server_t *srv, size_t limit) {
1873+ if (srv) {
1874+ srv->search_output_limit_override = limit;
1875+ }
1876+ }
1877+
18701878/* ── Cache dir + project DB path helpers ───────────────────────── */
18711879
18721880/* Returns the cache directory. Writes to buf, returns buf for convenience. */
@@ -9686,6 +9694,32 @@ static bool compile_path_filter(const char *filter, cbm_regex_t *re) {
96869694 return cbm_regcomp(re, filter, CBM_REG_EXTENDED | CBM_REG_NOSUB) == CBM_REG_OK;
96879695}
96889696
9697+ static int mcp_run_shell_command_cancellable_bounded(cbm_mcp_server_t *srv, const char *command,
9698+ char output_path[CBM_SZ_2K],
9699+ size_t output_limit,
9700+ bool *output_limit_exceeded,
9701+ cbm_proc_result_t *result_out);
9702+
9703+ #ifdef _WIN32
9704+ static char *search_code_scan_error(search_scratch_t *scratch, const char *output_path,
9705+ bool has_path_filter, cbm_regex_t *path_regex, char *root_path,
9706+ char *pattern, char *project, char *file_pattern,
9707+ const char *message) {
9708+ if (output_path && output_path[0]) {
9709+ (void)cbm_unlink(output_path);
9710+ }
9711+ search_scratch_close(scratch);
9712+ if (has_path_filter) {
9713+ cbm_regfree(path_regex);
9714+ }
9715+ free(root_path);
9716+ free(pattern);
9717+ free(project);
9718+ free(file_pattern);
9719+ return cbm_mcp_text_result(message, true);
9720+ }
9721+ #endif
9722+
96899723static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
96909724 char *pattern = cbm_mcp_get_string_arg(args, "pattern");
96919725 char *project = get_project_arg(args);
@@ -9869,6 +9903,44 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
98699903 cbm_search_code_build_grep_cmd(cmd, sizeof(cmd), use_regex, scoped, file_pattern, tmpfile,
98709904 filelist, root_path);
98719905
9906+ #ifdef _WIN32
9907+ char output_path[CBM_SZ_2K] = {0};
9908+ cbm_proc_result_t scan_result = {0};
9909+ bool scan_output_exceeded = false;
9910+ size_t scan_output_limit = srv->search_output_limit_override
9911+ ? srv->search_output_limit_override
9912+ : MCP_SEARCH_OUTPUT_MAX;
9913+ int scan_run = mcp_run_shell_command_cancellable_bounded(
9914+ srv, cmd, output_path, scan_output_limit, &scan_output_exceeded, &scan_result);
9915+ if (scan_output_exceeded) {
9916+ char message[CBM_SZ_128];
9917+ snprintf(message, sizeof(message),
9918+ "search failed: output exceeded the %zu-byte safety limit", scan_output_limit);
9919+ return search_code_scan_error(&scratch, output_path, has_path_filter, &path_regex,
9920+ root_path, pattern, project, file_pattern, message);
9921+ }
9922+ bool scan_cancelled = scan_result.cancellation_requested || mcp_request_cancelled(srv);
9923+ if (scan_cancelled) {
9924+ return search_code_scan_error(&scratch, output_path, has_path_filter, &path_regex,
9925+ root_path, pattern, project, file_pattern,
9926+ "search_code cancelled for this request");
9927+ }
9928+ if (scan_run != 0) {
9929+ return search_code_scan_error(
9930+ &scratch, output_path, has_path_filter, &path_regex, root_path, pattern, project,
9931+ file_pattern, "search failed: the contained command could not complete");
9932+ }
9933+ FILE *fp = cbm_fopen(output_path, "rb");
9934+ if (!fp) {
9935+ return search_code_scan_error(&scratch, output_path, has_path_filter, &path_regex,
9936+ root_path, pattern, project, file_pattern,
9937+ "search failed: contained output could not be read");
9938+ }
9939+ gm = collect_grep_matches(fp, root_path, strlen(root_path), has_path_filter, &path_regex,
9940+ grep_limit, &gm_count);
9941+ (void)fclose(fp);
9942+ (void)cbm_unlink(output_path);
9943+ #else
98729944 FILE *fp = cbm_popen(cmd, "r");
98739945 if (!fp) {
98749946 search_scratch_close(&scratch);
@@ -9885,6 +9957,7 @@ static char *handle_search_code(cbm_mcp_server_t *srv, const char *args) {
98859957 gm = collect_grep_matches(fp, root_path, strlen(root_path), has_path_filter, &path_regex,
98869958 grep_limit, &gm_count);
98879959 cbm_pclose(fp);
9960+ #endif
98889961 /* Both scratch files and the private directory go here — unlike the old
98899962 * code, the file list is removed even when the scan was not scoped. */
98909963 search_scratch_close(&scratch);
@@ -10045,10 +10118,16 @@ static bool mcp_resolve_windows_cmd(char out[CBM_SZ_4K]) {
1004510118}
1004610119#endif
1004710120
10048- static int mcp_run_shell_command_cancellable (cbm_mcp_server_t * srv , const char * command ,
10049- char output_path [CBM_SZ_2K ],
10050- cbm_proc_result_t * result_out ) {
10051- if (!srv || !command || !output_path || !result_out || !mcp_command_output_path (output_path )) {
10121+ static int mcp_run_shell_command_cancellable_bounded(cbm_mcp_server_t *srv, const char *command,
10122+ char output_path[CBM_SZ_2K],
10123+ size_t output_limit,
10124+ bool *output_limit_exceeded,
10125+ cbm_proc_result_t *result_out) {
10126+ if (output_limit_exceeded) {
10127+ *output_limit_exceeded = false;
10128+ }
10129+ if (!srv || !command || !output_path || !result_out ||
10130+ (output_limit > 0 && !output_limit_exceeded) || !mcp_command_output_path(output_path)) {
1005210131 return -1;
1005310132 }
1005410133 /* Internal test seam: rejecting after output allocation exercises the same
@@ -10087,10 +10166,18 @@ static int mcp_run_shell_command_cancellable(cbm_mcp_server_t *srv, const char *
1008710166 }
1008810167
1008910168 cbm_proc_poll_t state;
10169+ bool limit_exceeded = false;
1009010170 for (;;) {
1009110171 if (mcp_request_cancelled(srv)) {
1009210172 (void)cbm_subprocess_request_cancel(process);
1009310173 }
10174+ if (!limit_exceeded && output_limit > 0) {
10175+ int64_t output_size = cbm_file_size(output_path);
10176+ if (output_size > 0 && (uint64_t)output_size > output_limit) {
10177+ limit_exceeded = true;
10178+ (void)cbm_subprocess_request_cancel(process);
10179+ }
10180+ }
1009410181 state = cbm_subprocess_poll(process, result_out);
1009510182 if (state != CBM_PROC_POLL_RUNNING) {
1009610183 break;
@@ -10100,9 +10187,23 @@ static int mcp_run_shell_command_cancellable(cbm_mcp_server_t *srv, const char *
1010010187 bool contained = state == CBM_PROC_POLL_TERMINAL && result_out->tree_quiesced &&
1010110188 !result_out->supervision_failed;
1010210189 cbm_subprocess_destroy(process);
10190+ if (!limit_exceeded && output_limit > 0) {
10191+ int64_t final_size = cbm_file_size(output_path);
10192+ limit_exceeded = final_size > 0 && (uint64_t)final_size > output_limit;
10193+ }
10194+ if (output_limit_exceeded) {
10195+ *output_limit_exceeded = limit_exceeded;
10196+ }
1010310197 return contained ? 0 : -1;
1010410198}
1010510199
10200+ static int mcp_run_shell_command_cancellable(cbm_mcp_server_t *srv, const char *command,
10201+ char output_path[CBM_SZ_2K],
10202+ cbm_proc_result_t *result_out) {
10203+ return mcp_run_shell_command_cancellable_bounded(srv, command, output_path, 0, NULL,
10204+ result_out);
10205+ }
10206+
1010610207/* Does `node`'s line range overlap any recorded hunk for `file`? Used to scope
1010710208 * seed detection to the actually-changed lines rather than the whole file.
1010810209 * Non-static (declared in mcp_internal.h) so tests can exercise the overlap
0 commit comments