Skip to content

Fix transform_cypher_param to pass agtype_access_operator a VARIADIC array - #2489

Open
vividy163 wants to merge 2 commits into
apache:masterfrom
vividy163:fix/transform-cypher-param-variadic
Open

Fix transform_cypher_param to pass agtype_access_operator a VARIADIC array#2489
vividy163 wants to merge 2 commits into
apache:masterfrom
vividy163:fix/transform-cypher-param-variadic

Conversation

@vividy163

@vividy163 vividy163 commented Jul 31, 2026

Copy link
Copy Markdown

Root cause

agtype_access_operator is declared VARIADIC agtype[]. 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 (see transform_cypher_map_projection, transform_cypher_indirection).

transform_cypher_param() — the path that turns a Cypher $param reference into an AGE function call — was the only exception. It passed the bound Param and the key-name Const directly as separate FuncExpr arguments without wrapping them and without setting funcvariadic.

With the Simple Query protocol PostgreSQL implicitly assembles the variadic array from separate arguments at execution time, which masked the bug. Under the Extended Query protocol (Parse/Bind/Execute prepared statements) the executor does not perform that implicit assembly, so agtype_access_operator read past its single agtype argument and treated 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", and control bytes like 0x01). Drivers that UTF-8 decode the server error then raised:

UnicodeDecodeError: 'utf-8' codec can't decode byte 0x9e in position 51: invalid start byte

The bug reproduces with any Open/asyncpg/JDBC driver that uses prepared statements with bound agtype parameters against a Cypher query that references $param (for example MERGE (n:base {entity_id: $entity_id}) or MATCH (s:base {id: $src_id}), (t:base {id: $tgt_id}) CREATE (s)-[r:DIRECTED {…}]->(t), the shape used by LightRAG). The error content is unrelated to the payload — edges with pure-ASCII property values failed identically.

Fix

Pack the two arguments in make_agtype_array_expr() and set func_expr->funcvariadic = true, exactly matching every other call site.

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;

Regression test

Added p_create_edge to regress/sql/cypher_create.sql, which PREPAREs a MATCH … {id: $src_id}, … {id: $tgt_id} CREATE ()-[:e {id: $edge_id}]->() statement (the exact shape used by LightRAG/asyncpg) and EXECUTEs it repeatedly.

LightRAG Contributors and others added 2 commits July 31, 2026 12:45
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant