fix(pipeline): prevent stack-buffer-overflow in append_args_json

A call carrying enough long arguments drove append_args_json()'s running
position past the fixed CBM_SZ_2K `props` stack buffer in
emit_normal_calls_edge(): format_call_arg() returns snprintf's *untruncated*
length, so `pos += (size_t)n` could exceed `bufsize`, after which the
trailing `buf[pos] = '\0'` (and `buf[pos++] = ']'`) wrote out of bounds. The
stack canary caught it as SIGABRT, so full-repo indexing of large TypeScript
codebases crashed the server in the parallel resolve pass
(emit_service_edge -> emit_normal_calls_edge -> finalize_and_emit ->
append_args_json). Confirmed with AddressSanitizer:
stack-buffer-overflow WRITE at pass_parallel.c:1124, 'props' (2048 B).

Fix: when an argument does not fully fit, roll back to before its separator
and stop appending (atomic field, matching append_json_string's behaviour),
so `pos` can never advance past the buffer.

Add regression test parallel_args_json_no_overflow: indexes a fixture whose
single call carries 60 long string args (args JSON well past 2 KB); under the
ASan test build it aborts without this fix and passes with it.

Signed-off-by: Andrius Skerla <1492322+rainder@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrius SkerlaandClaude Opus 4.8 committed 2026-06-16 13:26:25 +03:00
1 parent e599df1d56
commit 74d15a6064
2 files changed
+58 -2

No files matched your search

+9 -2
View File
@@ -1108,15 +1108,22 @@ static size_t append_args_json(char *buf, size_t bufsize, size_t pos, const CBMC
pos += (size_t)n;
for (int i = 0; i < call->arg_count && pos < bufsize - CBM_ARG_JSON_GUARD; i++) {
const CBMCallArg *a = &call->args[i];
size_t mark = pos; /* rollback point (before the separator) */
if (i > 0 && pos < bufsize - SKIP_ONE) {
buf[pos++] = ',';
}
char expr_buf[CBM_SZ_128];
sanitize_expr(expr_buf, a->expr);
n = format_call_arg(buf + pos, bufsize - pos, a, expr_buf);
if (n > 0) {
pos += (size_t)n;
/* snprintf returns the UNtruncated length: if the arg did not fully
* fit, advancing pos by n would push it past buf and the buf[pos]
* writes below would overflow. Drop the arg whole (atomic field —
* keeps the array valid) and stop appending. */
if (n <= 0 || (size_t)n >= bufsize - pos) {
pos = mark;
break;
}
pos += (size_t)n;
}
if (pos < bufsize - SKIP_ONE) {
buf[pos++] = ']';
+49
View File
@@ -341,6 +341,54 @@ TEST(parallel_empty_files) {
PASS();
}
/* ── Regression: args JSON must not overflow the props buffer ──────── */
/* A call with many long string arguments makes append_args_json()'s running
* position exceed the fixed CBM_SZ_2K `props` stack buffer in
* emit_normal_calls_edge(): format_call_arg() returns snprintf's UNtruncated
* length, so pos += n could run past the buffer and the trailing
* buf[pos]='\0' wrote out of bounds (stack-buffer-overflow; caught by the
* stack canary as a SIGABRT on real repos). This indexes a fixture whose
* single call carries enough long args to drive pos past 2 KB; under the
* ASan test build a regression aborts here. */
TEST(parallel_args_json_no_overflow) {
char dir[256];
snprintf(dir, sizeof(dir), "/tmp/cbm_argov_XXXXXX");
ASSERT_TRUE(cbm_mkdtemp(dir) != NULL);
char path[512];
snprintf(path, sizeof(path), "%s/app.ts", dir);
FILE *f = fopen(path, "w");
ASSERT_TRUE(f != NULL);
fputs("function sink(...xs: string[]) { return xs; }\n", f);
fputs("function caller() {\n sink(\n", f);
for (int i = 0; i < 60; i++) {
/* 100-char string literal per arg; 60 args => args JSON well past the
* 2 KB props buffer, forcing the pre-fix overshoot. */
fputs(" \"", f);
for (int j = 0; j < 100; j++)
fputc('a' + (i % 26), f);
fputs(i < 59 ? "\",\n" : "\"\n", f);
}
fputs(" );\n}\n", f);
fclose(f);
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
ASSERT_EQ(cbm_discover(dir, &opts, &files, &file_count), 0);
ASSERT_GT(file_count, 0);
cbm_gbuf_t *gbuf = run_parallel("argov-test", dir, files, file_count, 4);
ASSERT_TRUE(gbuf != NULL);
ASSERT_GT(cbm_gbuf_edge_count(gbuf), 0);
cbm_gbuf_free(gbuf);
cbm_discover_free(files, file_count);
th_rmtree(dir);
PASS();
}
/* ── Graph buffer merge tests ─────────────────────────────────────── */
TEST(gbuf_shared_ids_unique) {
@@ -680,6 +728,7 @@ SUITE(parallel) {
RUN_TEST(parallel_implements_parity);
RUN_TEST(parallel_total_edges);
RUN_TEST(parallel_empty_files);
RUN_TEST(parallel_args_json_no_overflow);
/* Cleanup shared state */
parity_teardown();