From 9881fc247daab96549d7aaaf60243227b3274059 Mon Sep 17 00:00:00 2001 From: "masao.fujii" Date: Fri, 5 Jun 2026 10:37:12 +0900 Subject: [PATCH v2] pg_buffercache: restore rowtype verification in pg_buffercache_pages() Commit 257c8231bf9 changed pg_buffercache_pages() to materialize its output directly into a tuplestore. As a result, the function ended up trusting a caller-supplied RECORD descriptors. That could lead to crashes if the supplied row definition did not match the actual returned values, for example by passing bool Datums to tuplestore_putvalues() with an incompatible descriptor. Fix this by constructing the correct tuple descriptor for pg_buffercache_pages() and assigning it to rsinfo->setDesc after InitMaterializedSRF(). This restores the executor's tupledesc_match() verification, so incompatible caller-supplied row definitions are rejected with an error, as before commit 257c8231bf9. Bug: #19508 Reported-by: Nikita Kalinin Author: Fujii Masao Reviewed-by: Ayush Tiwari Discussion: https://postgr.es/m/19508-e5f188183279219b@postgresql.org --- .../expected/pg_buffercache.out | 8 +++ contrib/pg_buffercache/pg_buffercache_pages.c | 51 +++++++++++++++++++ contrib/pg_buffercache/sql/pg_buffercache.sql | 6 +++ 3 files changed, 65 insertions(+) diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out index 886dea770f6..c52a8491ff9 100644 --- a/contrib/pg_buffercache/expected/pg_buffercache.out +++ b/contrib/pg_buffercache/expected/pg_buffercache.out @@ -73,6 +73,14 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts(); t (1 row) +SELECT * +FROM pg_buffercache_pages() AS p + (bufferid integer, relfilenode oid, reltablespace oid, reldatabase oid, + relforknumber smallint, relblocknumber bigint, isdirty text, + usagecount smallint) +LIMIT 1; +ERROR: function return row and query-specified return row do not match +DETAIL: Returned type boolean at ordinal position 7, but query expects text. RESET role; ------ ---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c index bf2e6c97220..510455998aa 100644 --- a/contrib/pg_buffercache/pg_buffercache_pages.c +++ b/contrib/pg_buffercache/pg_buffercache_pages.c @@ -59,6 +59,8 @@ typedef struct BufferCacheOsPagesRec *record; } BufferCacheOsPagesContext; +static TupleDesc build_buffercache_pages_tupledesc(int natts); + /* * Function returning data from the shared buffer cache - buffer number, @@ -86,6 +88,8 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) { ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; TupleDesc expected_tupledesc; + TupleDesc actual_tupledesc; + MemoryContext oldcontext; int i; /* @@ -105,6 +109,21 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) InitMaterializedSRF(fcinfo, 0); + oldcontext = MemoryContextSwitchTo(rsinfo->econtext->ecxt_per_query_memory); + actual_tupledesc = build_buffercache_pages_tupledesc(expected_tupledesc->natts); + MemoryContextSwitchTo(oldcontext); + + /* + * Override the caller-supplied descriptor with the tuple descriptor that + * matches the values we actually return, so executor-side + * tupledesc_match() can verify the caller's row definition. + * + * Do not free the previous rsinfo->setDesc here: for RECORD results it + * can alias rsinfo->expectedDesc, which the executor still needs to + * reference. + */ + rsinfo->setDesc = actual_tupledesc; + /* * Scan through all the buffers, adding one row for each of the buffers to * the tuplestore. @@ -205,6 +224,38 @@ pg_buffercache_pages(PG_FUNCTION_ARGS) return (Datum) 0; } +static TupleDesc +build_buffercache_pages_tupledesc(int natts) +{ + TupleDesc tupledesc; + + tupledesc = CreateTemplateTupleDesc(natts); + TupleDescInitEntry(tupledesc, (AttrNumber) 1, "bufferid", + INT4OID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 2, "relfilenode", + OIDOID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 3, "reltablespace", + OIDOID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 4, "reldatabase", + OIDOID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 5, "relforknumber", + INT2OID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 6, "relblocknumber", + INT8OID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 7, "isdirty", + BOOLOID, -1, 0); + TupleDescInitEntry(tupledesc, (AttrNumber) 8, "usagecount", + INT2OID, -1, 0); + + if (natts == NUM_BUFFERCACHE_PAGES_ELEM) + TupleDescInitEntry(tupledesc, (AttrNumber) 9, "pinning_backends", + INT4OID, -1, 0); + + TupleDescFinalize(tupledesc); + + return BlessTupleDesc(tupledesc); +} + /* * Inquire about OS pages mappings for shared buffers, with NUMA information, * optionally. diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql index 127d604905c..be89b5f5a3a 100644 --- a/contrib/pg_buffercache/sql/pg_buffercache.sql +++ b/contrib/pg_buffercache/sql/pg_buffercache.sql @@ -34,6 +34,12 @@ SELECT count(*) > 0 FROM pg_buffercache; SELECT count(*) > 0 FROM pg_buffercache_os_pages; SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary(); SELECT count(*) > 0 FROM pg_buffercache_usage_counts(); +SELECT * +FROM pg_buffercache_pages() AS p + (bufferid integer, relfilenode oid, reltablespace oid, reldatabase oid, + relforknumber smallint, relblocknumber bigint, isdirty text, + usagecount smallint) +LIMIT 1; RESET role; -- 2.53.0