From d981be821123ec79baf0cb28f6ff9008a7c099a7 Mon Sep 17 00:00:00 2001 From: Dirkjan Bussink Date: Sat, 26 Sep 2026 09:18:40 +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 raising an error when the named cursor's portal is invalid, consistent with the adjacent ExecuteStmt branch and with the error that executing the FETCH itself would raise. --- src/backend/tcop/pquery.c | 6 ++- .../modules/libpq_pipeline/libpq_pipeline.c | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/backend/tcop/pquery.c b/src/backend/tcop/pquery.c index 9f3ea1c9a75..fa2ef20f178 100644 --- a/src/backend/tcop/pquery.c +++ b/src/backend/tcop/pquery.c @@ -389,7 +389,11 @@ FetchStatementTargetList(Node *stmt) Assert(!fstmt->ismove); subportal = GetPortalByName(fstmt->portalname); - Assert(PortalIsValid(subportal)); + if (!PortalIsValid(subportal)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_CURSOR), + errmsg("cursor \"%s\" does not exist", + fstmt->portalname))); 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..4540ae847f6 100644 --- a/src/test/modules/libpq_pipeline/libpq_pipeline.c +++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c @@ -456,6 +456,53 @@ 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 fail cleanly after the cursor has been closed. (This + * used to crash the server.) + */ + res = PQdescribePrepared(conn, "fetch_one"); + if (PQresultStatus(res) != PGRES_FATAL_ERROR) + pg_fatal("expected FATAL_ERROR, got %s", PQresStatus(PQresultStatus(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 +2164,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 +2271,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