From: Paul Bunn <paul.bunn@icloud.com>
Date: Mon, 3 Mar 2026 16:40:00 -0800
Subject: [PATCH] Fix DSA pagemap undersizing in make_new_segment

When make_new_segment creates an "odd-sized" segment (the path taken when
the requested allocation exceeds the geometric segment size), the pagemap
is sized for usable_pages entries.  However, the FreePageManager hands out
pages using absolute page indices that range up to metadata_pages +
usable_pages - 1.  Since metadata_pages >= 1 always, the last
metadata_pages page indices exceed the pagemap array bounds.

The normal (geometric) path correctly sizes the pagemap for total_pages
(= total_size / FPM_PAGE_SIZE), which includes both metadata and usable
pages.  The odd-sized path should do the same, but it works forward from
usable_pages rather than backward from total_size, creating a circular
dependency: metadata_bytes depends on total_pages which depends on
metadata_bytes.

This can be resolved in closed form.  After computing metadata_bytes for
usable_pages entries, add pagemap entries for the metadata pages
themselves.  The divisor (FPM_PAGE_SIZE - sizeof(dsa_pointer)) accounts
for the fact that each metadata page consumes one pagemap entry (of
sizeof(dsa_pointer) bytes), leaving only (FPM_PAGE_SIZE -
sizeof(dsa_pointer)) net bytes per metadata page.  The +1 absorbs the
ceiling.  This yields the exact metadata_pages in a single expression
with no iteration needed.

The bug is observable when a parallel hash join (or any operation) makes a
DSA allocation large enough to trigger the odd-sized segment path.  During
subsequent reuse of the segment, allocations landing on pages with indices
>= usable_pages cause out-of-bounds pagemap reads/writes.  On write,
span pointers are stored into the data area, corrupting allocated objects.
On read (during dsa_free), garbage is interpreted as a span pointer,
typically crashing in dsa_get_address with "dsa_area could not attach to
segment" or SIGSEGV.

The bug has been present since the DSA implementation was introduced and
affects all supported versions.

---
 src/backend/utils/mmgr/dsa.c | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/backend/utils/mmgr/dsa.c b/src/backend/utils/mmgr/dsa.c
index ce9ede4..789751f 100644
--- a/src/backend/utils/mmgr/dsa.c
+++ b/src/backend/utils/mmgr/dsa.c
@@ -2206,6 +2206,11 @@ make_new_segment(dsa_area *area, size_t requested_pages)
 			MAXALIGN(sizeof(FreePageManager)) +
 			usable_pages * sizeof(dsa_pointer);
 
+		/* Also cover the metadata pages in the pagemap (+1 for rounding). */
+		metadata_bytes +=
+			((metadata_bytes / (FPM_PAGE_SIZE - sizeof(dsa_pointer))) + 1) *
+			sizeof(dsa_pointer);
+
 		/* Add padding up to next page boundary. */
 		if (metadata_bytes % FPM_PAGE_SIZE != 0)
 			metadata_bytes += FPM_PAGE_SIZE - (metadata_bytes % FPM_PAGE_SIZE);
-- 
2.43.7

