From 359f37cd71676b2097ade34548a4abefb1a4880b Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 9 Jun 2026 23:54:25 +0200 Subject: [PATCH] Escape control bytes in properties-JSON array items (raw newlines from multi-line C params) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The array appenders (param_types/param_names/decorators/base_classes) in both props-builder twins escaped only quote and backslash, so items sliced from multi-line C parameter declarations carried raw newline/tab bytes — invalid inside JSON strings (the remaining 118 malformed kernel rows after the truncation fix; e.g. param_types:["struct\n\t\t..."]). Array items now go through the full escape function, and both escapers degrade any other raw control byte (e.g. form feed) to a space. Reproduced via the prod CLI on a multi-line-param fixture (nl_fn json_valid=0 pre-fix); the sweep regression test gained that fixture (61st case). --- src/pipeline/pass_definitions.c | 15 +++++++-------- src/pipeline/pass_parallel.c | 15 +++++++-------- tests/test_pipeline.c | 19 ++++++++++++++++++- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index b83cf5b80..676f1b169 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -103,7 +103,9 @@ static int def_json_escape_char(char *buf, size_t avail, char ch) { break; default: if (avail >= SKIP_ONE) { - buf[0] = ch; + /* Any other raw control byte (e.g. form feed) is invalid inside a + * JSON string — degrade to a space. */ + buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch; } return SKIP_ONE; } @@ -194,14 +196,11 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; } + /* Full escaping (not just quote/backslash): items like C param types + * sliced from multi-line declarations carry raw \n/\t bytes, which are + * invalid inside JSON strings. */ for (const char *s = arr[i]; *s && p < bufsize - PD_ESC_SPACE; s++) { - if (*s == '"' || *s == '\\') { - buf[p++] = '\\'; - if (p >= bufsize - PD_ESC_SPACE) { - break; - } - } - buf[p++] = *s; + p += (size_t)def_json_escape_char(buf + p, bufsize - p - PD_ESC_SPACE, *s); } if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 4713b77c6..91dd5601f 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -145,7 +145,9 @@ static int json_escape_char(char *buf, size_t avail, char ch) { break; default: if (avail >= SKIP_ONE) { - buf[0] = ch; + /* Any other raw control byte (e.g. form feed) is invalid inside a + * JSON string — degrade to a space. */ + buf[0] = ((unsigned char)ch < 0x20) ? ' ' : ch; } return SKIP_ONE; } @@ -237,14 +239,11 @@ static void append_json_str_array(char *buf, size_t bufsize, size_t *pos, const if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; } + /* Full escaping (not just quote/backslash): items like C param types + * sliced from multi-line declarations carry raw \n/\t bytes, which are + * invalid inside JSON strings. */ for (const char *s = arr[i]; *s && p < bufsize - PP_ESC_SPACE; s++) { - if (*s == '"' || *s == '\\') { - buf[p++] = '\\'; - if (p >= bufsize - PP_ESC_SPACE) { - break; - } - } - buf[p++] = *s; + p += (size_t)json_escape_char(buf + p, bufsize - p - PP_ESC_SPACE, *s); } if (p < bufsize - SKIP_ONE) { buf[p++] = '"'; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index d2ed468fd..333865229 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -506,6 +506,23 @@ TEST(pipeline_def_props_valid_json_when_oversized) { fclose(f); } + /* Multi-line parameter declarations: param_types items then carry raw + * newline/tab bytes from the source slice. The array appender's inline + * escape loop only handled quote/backslash, so the raw control bytes made + * the JSON invalid (118 Linux-kernel rows of this shape). */ + { + char path[512]; + snprintf(path, sizeof(path), "%s/sweep_nl.c", g_tmpdir); + FILE *f = fopen(path, "w"); + if (!f) { + teardown_test_repo(); + FAIL("failed to write sweep_nl.c"); + } + fprintf(f, "int sweep_fn_nl(struct\n\t\t\t\treally_long_struct_name *a,\n" + " enum\n\tweird_enum b) { return 0; }\n"); + fclose(f); + } + char db_path[512]; snprintf(db_path, sizeof(db_path), "%s/test_huge_props.db", g_tmpdir); cbm_pipeline_t *p = cbm_pipeline_new(g_tmpdir, db_path, CBM_MODE_FULL); @@ -539,7 +556,7 @@ TEST(pipeline_def_props_valid_json_when_oversized) { yyjson_doc_free(doc); checked++; } - ASSERT_EQ(checked, 60); /* all sweep functions present */ + ASSERT_EQ(checked, 61); /* all sweep functions present (60 sizes + nl case) */ cbm_store_free_nodes(funcs, func_count); cbm_store_close(s);