From d5393e7ecb9852c02d5b0e591fc5347e5fe9e0d7 Mon Sep 17 00:00:00 2001
From: Sami Imseih <simseih@amazon.com>
Date: Thu, 5 Mar 2026 16:13:12 -0600
Subject: [PATCH v4 1/1] Fix DSA pagemap undersizing in make_new_segment.

When make_new_segment() creates an odd-sized segment, the pagemap is
only sized for usable_pages entries.  But the segment also contains
metadata pages, and the FreePageManager uses absolute page indices
that cover the entire segment.  Accesses to pagemap entries beyond
usable_pages are out of bounds.

The normal (geometric) path correctly sizes the pagemap for all pages
in the segment.  The odd-sized path should do the same, but it works
forward from usable_pages rather than backward from total_size.

Fix by adding pagemap entries for the metadata pages after the initial
metadata_bytes calculation.  Use integer ceiling division to compute
the exact number of additional entries needed, avoiding iteration.

Add an Assert to verify the pagemap covers all pages in the segment.

Regression tests added to ensure dsa_allocate succeeds for a wide range
of allocations.

Author: Paul Bunn <paul.bunn@icloud.com>
Reported-by: Paul Bunn <paul.bunn@icloud.com>
---
 src/backend/utils/mmgr/dsa.c                  | 20 +++++++++++
 .../modules/test_dsa/expected/test_dsa.out    | 10 ++++++
 src/test/modules/test_dsa/sql/test_dsa.sql    |  5 +++
 src/test/modules/test_dsa/test_dsa--1.0.sql   |  4 +++
 src/test/modules/test_dsa/test_dsa.c          | 36 +++++++++++++++++++
 5 files changed, 75 insertions(+)

diff --git a/src/backend/utils/mmgr/dsa.c b/src/backend/utils/mmgr/dsa.c
index ce9ede4c196..222fa86d83d 100644
--- a/src/backend/utils/mmgr/dsa.c
+++ b/src/backend/utils/mmgr/dsa.c
@@ -2196,6 +2196,8 @@ make_new_segment(dsa_area *area, size_t requested_pages)
 	/* See if that is enough... */
 	if (requested_pages > usable_pages)
 	{
+		size_t		total_pages;
+
 		/*
 		 * We'll make an odd-sized segment, working forward from the requested
 		 * number of pages.
@@ -2206,10 +2208,28 @@ make_new_segment(dsa_area *area, size_t requested_pages)
 			MAXALIGN(sizeof(FreePageManager)) +
 			usable_pages * sizeof(dsa_pointer);
 
+		/*
+		 * We must also account for pagemap entries needed to cover the
+		 * metadata pages themselves.  The pagemap must track all pages in the
+		 * segment, including the pages occupied by metadata.
+		 */
+		metadata_bytes +=
+			((metadata_bytes + (FPM_PAGE_SIZE - sizeof(dsa_pointer)) - 1) /
+			 (FPM_PAGE_SIZE - sizeof(dsa_pointer))) *
+			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);
 		total_size = metadata_bytes + usable_pages * FPM_PAGE_SIZE;
+		total_pages = total_size / FPM_PAGE_SIZE;
+
+		/*
+		 * Verify we allocated enough pagemap entries for metadata and usable
+		 * pages
+		 */
+		Assert((metadata_bytes - MAXALIGN(sizeof(dsa_segment_header)) -
+				MAXALIGN(sizeof(FreePageManager))) / sizeof(dsa_pointer) >= total_pages);
 
 		/* Is that too large for dsa_pointer's addressing scheme? */
 		if (total_size > DSA_MAX_SEGMENT_SIZE)
diff --git a/src/test/modules/test_dsa/expected/test_dsa.out b/src/test/modules/test_dsa/expected/test_dsa.out
index 266010e77fe..2fd10fb349c 100644
--- a/src/test/modules/test_dsa/expected/test_dsa.out
+++ b/src/test/modules/test_dsa/expected/test_dsa.out
@@ -11,3 +11,13 @@ SELECT test_dsa_resowners();
  
 (1 row)
 
+--
+-- Test up to 10000 pages skipping 100 pages to cover a wide range of
+-- segment layouts without making the test too slow.
+--
+SELECT test_dsa_allocate(10000, 100);
+ test_dsa_allocate 
+-------------------
+ 
+(1 row)
+
diff --git a/src/test/modules/test_dsa/sql/test_dsa.sql b/src/test/modules/test_dsa/sql/test_dsa.sql
index c3d8db94372..6b10fe14ba6 100644
--- a/src/test/modules/test_dsa/sql/test_dsa.sql
+++ b/src/test/modules/test_dsa/sql/test_dsa.sql
@@ -2,3 +2,8 @@ CREATE EXTENSION test_dsa;
 
 SELECT test_dsa_basic();
 SELECT test_dsa_resowners();
+--
+-- Test up to 10000 pages skipping 100 pages to cover a wide range of
+-- segment layouts without making the test too slow.
+--
+SELECT test_dsa_allocate(10000, 100);
\ No newline at end of file
diff --git a/src/test/modules/test_dsa/test_dsa--1.0.sql b/src/test/modules/test_dsa/test_dsa--1.0.sql
index 2904cb23525..70baf27123a 100644
--- a/src/test/modules/test_dsa/test_dsa--1.0.sql
+++ b/src/test/modules/test_dsa/test_dsa--1.0.sql
@@ -10,3 +10,7 @@ CREATE FUNCTION test_dsa_basic()
 CREATE FUNCTION test_dsa_resowners()
 	RETURNS pg_catalog.void
 	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION test_dsa_allocate(int, int)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_dsa/test_dsa.c b/src/test/modules/test_dsa/test_dsa.c
index ed2a07c962f..a72871ea0d3 100644
--- a/src/test/modules/test_dsa/test_dsa.c
+++ b/src/test/modules/test_dsa/test_dsa.c
@@ -16,6 +16,7 @@
 #include "storage/dsm_registry.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
+#include "utils/freepage.h"
 #include "utils/resowner.h"
 
 PG_MODULE_MAGIC;
@@ -120,3 +121,38 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+/*
+ * test_dsa_allocate
+ *
+ * Test DSA allocation across a range of sizes to exercise the pagemap
+ * sizing logic in make_new_segment().  A fresh DSA is created for each
+ * iteration so that each allocation triggers a new segment creation,
+ * including the odd-sized segment path.
+ */
+PG_FUNCTION_INFO_V1(test_dsa_allocate);
+Datum
+test_dsa_allocate(PG_FUNCTION_ARGS)
+{
+	int			num_pages = PG_GETARG_INT32(0);
+	int			step = PG_GETARG_INT32(1);
+	size_t		usable_pages;
+	int		   *tranche_id;
+	bool		found;
+	dsa_area   *a;
+	dsa_pointer dp;
+
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found, NULL);
+
+	for (usable_pages = 1; usable_pages < num_pages; usable_pages += step)
+	{
+		a = dsa_create(*tranche_id);
+		dp = dsa_allocate(a, usable_pages * FPM_PAGE_SIZE);
+
+		dsa_free(a, dp);
+		dsa_detach(a);
+	}
+
+	PG_RETURN_VOID();
+}
-- 
2.50.1 (Apple Git-155)

