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) 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;