From 508fa7c2808178caf694cc61282bf4fbe2e0509f Mon Sep 17 00:00:00 2001 From: Dirkjan Bussink Date: Fri, 25 Sep 2026 13:19:44 +0200 Subject: [PATCH] Fix crash when describing a FETCH statement whose cursor is gone FetchStatementTargetList() dereferenced the portal named by a FetchStmt without checking that it still exists. With the extended query protocol, a client can Parse "FETCH n FROM c" while cursor c is open (which caches a result descriptor), then CLOSE c, and then Describe the prepared statement or a portal bound to it. Describe re-resolves the target list through the named cursor, and in non-assert builds the missing check becomes a NULL pointer dereference, crashing the backend and forcing a cluster-wide restart and crash recovery. Fix by returning NIL when the named portal is invalid, mirroring the existing guard in UtilityTupleDescriptor(): it is not this function's business to raise an error, and SendRowDescriptionMessage() then builds the RowDescription from the descriptor saved at Parse or Bind time. --- src/backend/tcop/pquery.c | 3 +- .../modules/libpq_pipeline/libpq_pipeline.c | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 9f3ea1c9a75..385772ec374 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -389,7 +389,8 @@ FetchStatementTargetList(Node *stmt) Assert(!fstmt->ismove); subportal = GetPortalByName(fstmt->portalname); - Assert(PortalIsValid(subportal)); + if (!PortalIsValid(subportal)) + return NIL; /* not our business to raise error */ return FetchPortalTargetList(subportal); } if (IsA(stmt, ExecuteStmt)) diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c index ad007038dc9..168f9a7d3ab 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -456,6 +456,55 @@ test_cancel(PGconn *conn) fprintf(stderr, "ok\n"); } +/* + * Test Describe of a prepared FETCH statement after the cursor it + * references has been closed. + */ +static void +test_describe_fetch(PGconn *conn) +{ + PGresult *res; + + res = PQexec(conn, "BEGIN"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("BEGIN failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "DECLARE fetch_cursor CURSOR FOR SELECT 1"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("DECLARE CURSOR failed: %s", PQerrorMessage(conn)); + PQclear(res); + + /* prepare while the cursor exists, so that it caches a result desc */ + res = PQprepare(conn, "fetch_one", "FETCH 1 FROM fetch_cursor", 0, NULL); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PQprepare failed: %s", PQerrorMessage(conn)); + PQclear(res); + + res = PQexec(conn, "CLOSE fetch_cursor"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("CLOSE failed: %s", PQerrorMessage(conn)); + PQclear(res); + + /* + * Describe must still work, using the cached row descriptor. (This used + * to crash the server.) + */ + res = PQdescribePrepared(conn, "fetch_one"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("PQdescribePrepared failed: %s", PQerrorMessage(conn)); + if (PQnfields(res) != 1 || PQftype(res, 0) != INT4OID) + pg_fatal("expected one int4 column, got %d fields", PQnfields(res)); + PQclear(res); + + res = PQexec(conn, "ROLLBACK"); + if (PQresultStatus(res) != PGRES_COMMAND_OK) + pg_fatal("ROLLBACK failed: %s", PQerrorMessage(conn)); + PQclear(res); + + fprintf(stderr, "ok\n"); +} + static void test_disallowed_in_pipeline(PGconn *conn) { @@ -2117,6 +2166,7 @@ static void print_test_list(void) { printf("cancel\n"); + printf("describe_fetch\n"); printf("disallowed_in_pipeline\n"); printf("multi_pipelines\n"); printf("nosync\n"); @@ -2223,6 +2273,8 @@ main(int argc, char **argv) if (strcmp(testname, "cancel") == 0) test_cancel(conn); + else if (strcmp(testname, "describe_fetch") == 0) + test_describe_fetch(conn); else if (strcmp(testname, "disallowed_in_pipeline") == 0) test_disallowed_in_pipeline(conn); else if (strcmp(testname, "multi_pipelines") == 0) -- 2.55.0