mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-12 09:47:20 +08:00
fix(cypher): grow the OPTIONAL fallback buffer in the rel cross-join
An OPTIONAL MATCH with a relationship, whose start and terminal variables are not bound by an earlier MATCH and whose start node pattern matches no node, is joined in cross_join_with_rels() with an empty candidate list. That path sized its output for a single binding, but the OPTIONAL fallback then kept one row per existing binding with a plain indexed store, so every row after the first was written past the end of the allocation. Route the fallback through binding_out_append(), the growing writer that the expansion branch of the same function and the other OPTIONAL fallbacks already use. The node-only join (cross_join_nodes) was fixed earlier by sizing its buffer for the fallback rows; the relationship variant kept the unchecked store. Add a cypher regression test that runs such a query over four bound functions and asserts one row per function with the optional variables unbound. Before this change the test runner reported a heap-buffer-overflow in cross_join_with_rels under ASan. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+4
-1
@@ -4811,9 +4811,12 @@ static void cross_join_with_rels(cbm_store_t *store, cbm_pattern_t *patn, bindin
|
||||
free(tmp);
|
||||
}
|
||||
if (opt && extra_count == 0) {
|
||||
/* OPTIONAL with no start node: keep the row, pattern vars unbound.
|
||||
* The buffer was sized for a single row when extra_count == 0, so
|
||||
* this write must grow it like every other writer here. */
|
||||
binding_t nb = {0};
|
||||
binding_copy(&nb, &(*bindings)[bi]);
|
||||
new_bindings[new_count++] = nb;
|
||||
(void)binding_out_append(&new_bindings, &new_count, &new_cap, &nb);
|
||||
}
|
||||
}
|
||||
for (int bi = 0; bi < *bind_count; bi++) {
|
||||
|
||||
@@ -715,6 +715,45 @@ TEST(cypher_exec_optional_empty_label_no_overflow) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Regression: the relationship variant of the test above. An OPTIONAL MATCH
|
||||
* with a relationship whose start AND terminal are both unbound, and whose
|
||||
* start matches zero nodes, drove cross_join_with_rels with extra_count == 0.
|
||||
* That sized its output for a single binding, and the OPTIONAL fallback then
|
||||
* wrote one row per existing binding without growing the buffer: a heap
|
||||
* buffer overflow once the first MATCH bound more than one node (ASan:
|
||||
* heap-buffer-overflow). The query text reaches this from the MCP query tool. */
|
||||
TEST(cypher_exec_optional_empty_label_rel_no_overflow) {
|
||||
cbm_store_t *s = setup_cypher_store(); /* 4 Function nodes */
|
||||
cbm_cypher_result_t r = {0};
|
||||
|
||||
int rc = cbm_cypher_execute(s,
|
||||
"MATCH (a:Function) OPTIONAL MATCH (x:NoSuchLabel)-[:CALLS]->(y) "
|
||||
"RETURN a.name, x.name, y.name",
|
||||
"test", 0, &r);
|
||||
ASSERT_EQ(rc, 0);
|
||||
ASSERT_EQ(r.col_count, 3);
|
||||
/* One row per Function, each with x and y left unbound (""). */
|
||||
ASSERT_EQ(r.row_count, 4);
|
||||
const char *want[] = {"HandleOrder", "ValidateOrder", "SubmitOrder", "LogError"};
|
||||
for (int w = 0; w < 4; w++) {
|
||||
int seen = 0;
|
||||
for (int i = 0; i < r.row_count; i++) {
|
||||
if (strcmp(r.rows[i][0], want[w]) == 0) {
|
||||
seen++;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(seen, 1);
|
||||
}
|
||||
for (int i = 0; i < r.row_count; i++) {
|
||||
ASSERT_STR_EQ(r.rows[i][1], "");
|
||||
ASSERT_STR_EQ(r.rows[i][2], "");
|
||||
}
|
||||
|
||||
cbm_cypher_result_free(&r);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Regression: expand_pattern_rels sized its OPTIONAL-expansion output buffer as
|
||||
* bind_cap*10 + 1 — room for the bounded expansion (max_new = bind_cap*10) plus
|
||||
* a SINGLE OPTIONAL fallback row. When one source saturated the expansion to
|
||||
@@ -4673,6 +4712,7 @@ SUITE(cypher) {
|
||||
RUN_TEST(cypher_deep_nesting_rejected_not_crash);
|
||||
RUN_TEST(cypher_exec_match_all_functions);
|
||||
RUN_TEST(cypher_exec_optional_empty_label_no_overflow);
|
||||
RUN_TEST(cypher_exec_optional_empty_label_rel_no_overflow);
|
||||
RUN_TEST(cypher_cross_join_alloc_rejects_overflow);
|
||||
RUN_TEST(cypher_exec_optional_rel_saturated_no_overflow);
|
||||
RUN_TEST(cypher_exec_optional_saturated_does_not_fabricate_no_match);
|
||||
|
||||
Reference in New Issue
Block a user