mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-15 05:00:18 +08:00
fix(cypher): free a partially parsed MATCH pattern
parse_match_pattern allocated the pattern's node and relationship arrays and returned on a parse error without freeing them. Its callers count a pattern only after it parsed, so cbm_query_free never saw it: every query whose pattern failed partway leaked, including every named path (`MATCH p = (...)`, not supported, fails at the first node). The element whose parse failed leaked too (its variable, label or properties), and parse_props freed only the array, not the entries it had already parsed. The fuzz job found it on its first run, from a checked-in seed: `MATCH p = (a:Class)-[:INHERITS*1..3]->(b:Class) RETURN a.name, length(p)` (LeakSanitizer: 288 bytes in 2 allocations). parse_match_pattern now frees and zeroes the pattern on every failure, counting the failed element (parse_node and parse_rel zero it first, so only what they allocated is freed). parse_props frees its parsed entries on every failure through one helper. New test cypher_parse_error_frees_partial_pattern drives five failure points: a named path, a property list after one entry, a cut-off relationship, a missing label after a relationship, and a second MATCH clause's properties. Checked with the fuzz binary (clang 18 ASan/LSan, arm64 container) on the five queries: before this change the first four leak (288, 306, 339 and 348 bytes); after it all five are clean, and scripts/fuzz.sh all --runs 20000 --seed 1 passes for extract, cypher and config. The fifth goes through the same parse_props failure as the second. macOS: the cypher suite passes, 217 tests. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+33
-17
@@ -505,6 +505,15 @@ static const cbm_token_t *expect(parser_t *p, cbm_token_type_t type) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Free the first n entries of a property array and the array itself. */
|
||||
static void free_prop_entries(cbm_prop_filter_t *arr, int n) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
safe_str_free(&arr[i].key);
|
||||
safe_str_free(&arr[i].value);
|
||||
}
|
||||
free(arr);
|
||||
}
|
||||
|
||||
/* Parse inline properties: {key: "value", ...} */
|
||||
static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
|
||||
*out = NULL;
|
||||
@@ -521,18 +530,19 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
|
||||
}
|
||||
|
||||
while (!check(p, TOK_RBRACE) && !check(p, TOK_EOF)) {
|
||||
/* Every failure frees the entries already parsed, not only the array. */
|
||||
const cbm_token_t *key = expect(p, TOK_IDENT);
|
||||
if (!key) {
|
||||
free(arr);
|
||||
free_prop_entries(arr, n);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
if (!expect(p, TOK_COLON)) {
|
||||
free(arr);
|
||||
free_prop_entries(arr, n);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
const cbm_token_t *val = expect(p, TOK_STRING);
|
||||
if (!val) {
|
||||
free(arr);
|
||||
free_prop_entries(arr, n);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
@@ -540,11 +550,7 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
|
||||
int new_cap = cap * PAIR_LEN;
|
||||
void *tmp = realloc(arr, new_cap * sizeof(cbm_prop_filter_t));
|
||||
if (!tmp) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
safe_str_free(&arr[i].key);
|
||||
safe_str_free(&arr[i].value);
|
||||
}
|
||||
free(arr);
|
||||
free_prop_entries(arr, n);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
arr = tmp;
|
||||
@@ -555,11 +561,7 @@ static int parse_props(parser_t *p, cbm_prop_filter_t **out, int *count) {
|
||||
if (!new_key || !new_val) {
|
||||
safe_str_free(&new_key);
|
||||
safe_str_free(&new_val);
|
||||
for (int i = 0; i < n; i++) {
|
||||
safe_str_free(&arr[i].key);
|
||||
safe_str_free(&arr[i].value);
|
||||
}
|
||||
free(arr);
|
||||
free_prop_entries(arr, n);
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
arr[n].key = new_key;
|
||||
@@ -1864,7 +1866,11 @@ static int parse_return(parser_t *p, cbm_return_clause_t **out) {
|
||||
return parse_return_or_with(p, out, false);
|
||||
}
|
||||
|
||||
/* Parse a single MATCH pattern into pat */
|
||||
static void free_pattern(cbm_pattern_t *pat);
|
||||
|
||||
/* Parse a single MATCH pattern into pat. On failure pat is freed and zeroed:
|
||||
* the caller counts a pattern only after it parsed, so a partial one would
|
||||
* otherwise leak (a named path "p = (...)" failed this way on every query). */
|
||||
static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
|
||||
memset(pat, 0, sizeof(*pat));
|
||||
int node_cap = CYP_INIT_CAP4;
|
||||
@@ -1872,8 +1878,11 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
|
||||
pat->nodes = malloc(node_cap * sizeof(cbm_node_pattern_t));
|
||||
pat->rels = calloc(rel_cap, sizeof(cbm_rel_pattern_t));
|
||||
|
||||
/* A failed parse_node / parse_rel leaves its element zeroed except for
|
||||
* what it allocated, so counting that element frees its partial state. */
|
||||
if (parse_node(p, &pat->nodes[0]) < 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
pat->node_count = SKIP_ONE;
|
||||
goto fail;
|
||||
}
|
||||
pat->node_count = SKIP_ONE;
|
||||
|
||||
@@ -1883,7 +1892,8 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
|
||||
pat->rels = safe_realloc(pat->rels, rel_cap * sizeof(cbm_rel_pattern_t));
|
||||
}
|
||||
if (parse_rel(p, &pat->rels[pat->rel_count]) < 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
pat->rel_count++;
|
||||
goto fail;
|
||||
}
|
||||
pat->rel_count++;
|
||||
|
||||
@@ -1892,11 +1902,17 @@ static int parse_match_pattern(parser_t *p, cbm_pattern_t *pat) {
|
||||
pat->nodes = safe_realloc(pat->nodes, node_cap * sizeof(cbm_node_pattern_t));
|
||||
}
|
||||
if (parse_node(p, &pat->nodes[pat->node_count]) < 0) {
|
||||
return CBM_NOT_FOUND;
|
||||
pat->node_count++;
|
||||
goto fail;
|
||||
}
|
||||
pat->node_count++;
|
||||
}
|
||||
return 0;
|
||||
|
||||
fail:
|
||||
free_pattern(pat);
|
||||
memset(pat, 0, sizeof(*pat));
|
||||
return CBM_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Parse UNWIND [...] AS var clause into query */
|
||||
|
||||
@@ -567,6 +567,35 @@ TEST(cypher_parse_error) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A pattern that fails partway frees what it already parsed. Each query
|
||||
* fails at a different point; under LeakSanitizer the first four leaked
|
||||
* before the fix (the first is the fuzz seed that found it), and the fifth
|
||||
* reaches the same property-list failure through a second MATCH. */
|
||||
TEST(cypher_parse_error_frees_partial_pattern) {
|
||||
static const char *const queries[] = {
|
||||
/* named path: unsupported, fails at the first node */
|
||||
"MATCH p = (a:Class)-[:INHERITS*1..3]->(b:Class) RETURN a.name, length(p)",
|
||||
/* property list fails after one parsed entry */
|
||||
"MATCH (a:Function {name: \"x\", 7}) RETURN a",
|
||||
/* relationship cut off after its variable */
|
||||
"MATCH (a:Function)-[r:CALLS",
|
||||
/* second node's label missing, after a parsed relationship */
|
||||
"MATCH (a:Function)-[:CALLS]->(b:",
|
||||
/* a second MATCH clause's pattern fails inside its properties */
|
||||
"MATCH (a:Function) MATCH (b:Function {name: \"y\" 5}) RETURN a",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(queries) / sizeof(queries[0]); i++) {
|
||||
cbm_query_t *q = NULL;
|
||||
char *err = NULL;
|
||||
int rc = cbm_cypher_parse(queries[i], &q, &err);
|
||||
ASSERT_EQ(rc, -1);
|
||||
ASSERT_NOT_NULL(err);
|
||||
free(err);
|
||||
cbm_query_free(q);
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════════
|
||||
* EXECUTION TESTS (end-to-end against store)
|
||||
* ══════════════════════════════════════════════════════════════════ */
|
||||
@@ -5116,6 +5145,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_parse_return_distinct);
|
||||
RUN_TEST(cypher_parse_inline_props);
|
||||
RUN_TEST(cypher_parse_error);
|
||||
RUN_TEST(cypher_parse_error_frees_partial_pattern);
|
||||
/* Execution */
|
||||
RUN_TEST(cypher_exec_deadline_aborts_runaway_query_issue601);
|
||||
RUN_TEST(cypher_exec_deadline_allows_normal_query_issue601);
|
||||
|
||||
Reference in New Issue
Block a user