From 31d703510e1017b93a43422ea4f1055603cbe4fb Mon Sep 17 00:00:00 2001 From: LightRAG Contributors Date: Fri, 31 Jul 2026 12:45:26 +0900 Subject: [PATCH 1/2] Fix transform_cypher_param to pass agtype_access_operator a VARIADIC array agtype_access_operator is declared VARIADIC agtype[] and every other call site in cypher_expr.c packs its arguments into a single agtype[] ArrayExpr via make_agtype_array_expr() and sets funcvariadic = true on the resulting FuncExpr. transform_cypher_param() was the sole exception: it passed the Param (the bound $1 agtype argument to cypher()) and the key Const directly as separate list elements to makeFuncExpr, never wrapped them in an ArrayExpr, and never set funcvariadic. With the Simple Query protocol PostgreSQL implicitly assembles the variadic array from the separate arguments at execution time, which masked the bug. Under the Extended Query protocol (prepared statements via Parse/Bind/Execute) the executor did not perform that implicit assembly, so agtype_access_operator read past its single agtype argument treating adjacent memory as additional array elements. The resulting out-of-bounds read surfaced as corrupted relation names in 'relation does not exist' error responses (random bytes mixed with fragments of in-flight query text / property values such as 'November', 'customer', 'communication', control bytes including 0x01), which client drivers then failed to UTF-8 decode: UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9e in position 51: invalid start byte The fix matches every other call site: wrap the two arguments in make_agtype_array_expr() and set func_expr->funcvariadic = true. A regression test is added that PREPAREs a CREATE EDGE statement whose MATCH clause resolves $src_id/$tgt_id scalar parameters (the exact shape used by LightRAG / asyncpg) and EXECUTEs it repeatedly. --- regress/sql/cypher_create.sql | 25 +++++++++++++++++++++++++ src/backend/parser/cypher_expr.c | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/regress/sql/cypher_create.sql b/regress/sql/cypher_create.sql index 0093dc449..9b2a434c3 100644 --- a/regress/sql/cypher_create.sql +++ b/regress/sql/cypher_create.sql @@ -194,6 +194,31 @@ PREPARE p_2 AS SELECT * FROM cypher('cypher_create', $$CREATE (v:new_vertex {key EXECUTE p_2('{"var_name": "Hello Prepared Statements"}'); EXECUTE p_2('{"var_name": "Hello Prepared Statements 2"}'); +-- prepared statement with scalar parameters used in MATCH and CREATE EDGE. +-- Regression test: transform_cypher_param() previously called the VARIADIC +-- agtype_access_operator without building the agtype[] ArrayExpr argument +-- and without setting funcvariadic = true. Under the extended query +-- protocol this caused agtype_access_operator to read past its argument +-- at execution time, corrupting error messages with random bytes and +-- producing client-side UnicodeDecodeError / invalid-start-byte errors. +PREPARE p_create_edge(agtype) AS +SELECT * FROM cypher('cypher_create', $$ + MATCH (source:v {id: $src_id}), (target:v {id: $tgt_id}) + CREATE (source)-[r:e {id: $edge_id}]->(target) + RETURN r +$$, $1) AS (r agtype); + +-- seed vertices to match against +SELECT * FROM cypher('cypher_create', $$ + CREATE (:v {id: 'a'}), (:v {id: 'b'}) +$$) AS (a agtype); + +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge1"}'); +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge2"}'); +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge3"}'); + +DEALLOCATE p_create_edge; + -- pl/pgsql CREATE FUNCTION create_test() RETURNS TABLE(vertex agtype) diff --git a/src/backend/parser/cypher_expr.c b/src/backend/parser/cypher_expr.c index 7e4f44600..f72a2c5e8 100644 --- a/src/backend/parser/cypher_expr.c +++ b/src/backend/parser/cypher_expr.c @@ -848,6 +848,7 @@ static Node *transform_cypher_param(cypher_parsestate *cpstate, cypher_param *cp) { ParseState *pstate = (ParseState *)cpstate; + ArrayExpr *newa; Const *const_str; FuncExpr *func_expr; Oid func_access_oid; @@ -867,6 +868,30 @@ static Node *transform_cypher_param(cypher_parsestate *cpstate, func_access_oid = get_ag_func_oid("agtype_access_operator", 1, AGTYPEARRAYOID); + /* + * agtype_access_operator is declared VARIADIC agtype[] and expects a + * single agtype[] ArrayExpr with funcvariadic = true. Every other call + * site in this file (see transform_cypher_map_projection, + * transform_cypher_indirection) assembles its arguments via + * make_agtype_array_expr() and sets funcvariadic; only this function was + * passing the Param and the key Const as separate list elements directly. + * + * With the Simple Query protocol PostgreSQL implicitly forms the variadic + * array from separate arguments at execution time, masking the bug. But + * under the Extended Query protocol (prepared statements / + * Parse-Bind-Execute) the executor does not perform that implicit + * assembly, so agtype_access_operator read past its single agtype + * argument treating adjacent memory as additional array elements. The + * resulting out-of-bounds read surfaced as corrupted relation names in + * error responses (random bytes mixed with fragments of property values + * such as "November", "customer", "communication"; control bytes such as + * 0x01), which client drivers then failed to UTF-8 decode: + * + * UnicodeDecodeError: 'utf-8' ... invalid start byte + * + * The fix matches every other call site: pack the arguments into an + * agtype[] ArrayExpr and set funcvariadic = true. + */ args = lappend(args, copyObject(cpstate->params)); const_str = makeConst(AGTYPEOID, -1, InvalidOid, -1, @@ -874,8 +899,11 @@ static Node *transform_cypher_param(cypher_parsestate *cpstate, args = lappend(args, const_str); - func_expr = makeFuncExpr(func_access_oid, AGTYPEOID, args, InvalidOid, - InvalidOid, COERCE_EXPLICIT_CALL); + newa = make_agtype_array_expr(args); + + func_expr = makeFuncExpr(func_access_oid, AGTYPEOID, list_make1(newa), + InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL); + func_expr->funcvariadic = true; func_expr->location = cp->location; return (Node *)func_expr; From 1ad957c67ad4d4355f4fb720c625c74dc4c95ea4 Mon Sep 17 00:00:00 2001 From: homepage Date: Fri, 31 Jul 2026 17:54:34 +0900 Subject: [PATCH 2/2] update regress/expected/cypher_create.out --- regress/expected/cypher_create.out | 40 ++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/regress/expected/cypher_create.out b/regress/expected/cypher_create.out index 2388af8a0..fe61867f6 100644 --- a/regress/expected/cypher_create.out +++ b/regress/expected/cypher_create.out @@ -467,6 +467,46 @@ EXECUTE p_2('{"var_name": "Hello Prepared Statements 2"}'); {"id": 2533274790395908, "label": "new_vertex", "properties": {"key": "Hello Prepared Statements 2"}}::vertex (1 row) +-- prepared statement with scalar parameters used in MATCH and CREATE EDGE. +-- Regression test: transform_cypher_param() previously called the VARIADIC +-- agtype_access_operator without building the agtype[] ArrayExpr argument +-- and without setting funcvariadic = true. Under the extended query +-- protocol this caused agtype_access_operator to read past its argument +-- at execution time, corrupting error messages with random bytes and +-- producing client-side UnicodeDecodeError / invalid-start-byte errors. +PREPARE p_create_edge(agtype) AS +SELECT * FROM cypher('cypher_create', $$ + MATCH (source:v {id: $src_id}), (target:v {id: $tgt_id}) + CREATE (source)-[r:e {id: $edge_id}]->(target) + RETURN r +$$, $1) AS (r agtype); +-- seed vertices to match against +SELECT * FROM cypher('cypher_create', $$ + CREATE (:v {id: 'a'}), (:v {id: 'b'}) +$$) AS (a agtype); + a +--- +(0 rows) + +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge1"}'); + r +------------------------------------------------------------------------------------------------------------------------------------- + {"id": 1125899906842635, "label": "e", "end_id": 844424930131990, "start_id": 844424930131989, "properties": {"id": "edge1"}}::edge +(1 row) + +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge2"}'); + r +------------------------------------------------------------------------------------------------------------------------------------- + {"id": 1125899906842636, "label": "e", "end_id": 844424930131990, "start_id": 844424930131989, "properties": {"id": "edge2"}}::edge +(1 row) + +EXECUTE p_create_edge('{"src_id": "a", "tgt_id": "b", "edge_id": "edge3"}'); + r +------------------------------------------------------------------------------------------------------------------------------------- + {"id": 1125899906842637, "label": "e", "end_id": 844424930131990, "start_id": 844424930131989, "properties": {"id": "edge3"}}::edge +(1 row) + +DEALLOCATE p_create_edge; -- pl/pgsql CREATE FUNCTION create_test() RETURNS TABLE(vertex agtype)