>From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Paul Bunn <paul.bunn@icloud.com>
Date: Wed, 4 Mar 2026 00:00:00 +0000
Subject: [PATCH] Add regression test for DSA pagemap overflow in odd-sized segments

When make_new_segment() creates an odd-sized segment (the path taken when
the requested allocation exceeds what fits in a standard-sized segment),
the pagemap array was sized for usable_pages entries rather than
total_pages entries.  Because DSA uses absolute page indices starting at
zero, the last usable pages have indices of metadata_pages + usable_pages
- 1, which can exceed usable_pages - 1 and fall outside the pagemap
array.  This causes out-of-bounds pagemap reads/writes that corrupt user
data.

Add test_dsa_pagemap_overflow() to test_dsa to detect this bug directly.
With 879 usable pages, metadata occupies exactly 2 pages (8192 bytes),
placing pagemap[880] at offset 8192 -- the same offset as the first user
page.  The test allocates 879 pages, extracts the first user page offset
from the returned dsa_pointer, and verifies it lies strictly beyond
pagemap[880].  Before the fix both offsets are 8192 (overlap); after the
fix the user page is at 12288 (3 metadata pages), past pagemap[880].

---
 src/test/modules/test_dsa/expected/test_dsa.out |  6 +++
 src/test/modules/test_dsa/sql/test_dsa.sql      |  1 +
 src/test/modules/test_dsa/test_dsa--1.0.sql     |  4 ++
 src/test/modules/test_dsa/test_dsa.c            | 73 +++++++++++++++++++++++++
 4 files changed, 84 insertions(+)

diff --git a/src/test/modules/test_dsa/expected/test_dsa.out b/src/test/modules/test_dsa/expected/test_dsa.out
index 266010e77fe..ae405946dd7 100644
--- a/src/test/modules/test_dsa/expected/test_dsa.out
+++ b/src/test/modules/test_dsa/expected/test_dsa.out
@@ -11,3 +11,9 @@ SELECT test_dsa_resowners();
  
 (1 row)
 
+SELECT test_dsa_pagemap_overflow();
+ test_dsa_pagemap_overflow 
+---------------------------
+ 
+(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..2fd29093a66 100644
--- a/src/test/modules/test_dsa/sql/test_dsa.sql
+++ b/src/test/modules/test_dsa/sql/test_dsa.sql
@@ -2,3 +2,4 @@ CREATE EXTENSION test_dsa;
 
 SELECT test_dsa_basic();
 SELECT test_dsa_resowners();
+SELECT test_dsa_pagemap_overflow();
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..95513b03255 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,8 @@ CREATE FUNCTION test_dsa_basic()
 CREATE FUNCTION test_dsa_resowners()
 	RETURNS pg_catalog.void
 	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION test_dsa_pagemap_overflow()
+	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..7df34841b3c 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,75 @@ test_dsa_resowners(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
+
+
+/* DSA-internal constants not exposed in dsa.h, replicated from dsa.c. */
+#if SIZEOF_DSA_POINTER == 4
+#define TEST_DSA_OFFSET_WIDTH 27
+#else
+#define TEST_DSA_OFFSET_WIDTH 40
+#endif
+#define TEST_DSA_OFFSET_BITMASK (((dsa_pointer) 1 << TEST_DSA_OFFSET_WIDTH) - 1)
+#define TEST_DSA_EXTRACT_OFFSET(dp) ((dp) & TEST_DSA_OFFSET_BITMASK)
+
+/*
+ * Test for pagemap overflow into user data in make_new_segment's odd-sized
+ * segment path.
+ *
+ * With 879 usable pages requested, the pagemap starts at offset 1152
+ * (= MAXALIGN(dsa_segment_header) + MAXALIGN(FreePageManager) = 56 + 1096),
+ * so pagemap[880] lands at offset 1152 + 880*8 = 8192.
+ *
+ * Bug:  metadata only spans 2 pages, placing the first user page at offset
+ * 8192 as well -- aliasing pagemap[880] with user data.
+ *
+ * Fix:  metadata is padded to 3 pages, so the first user page moves to
+ * offset 12288, safely past pagemap[880] at 8192.
+ *
+ * BUG:   first user page offset == 8192 (overlaps pagemap[880]).
+ * FIXED: first user page offset == 12288 (no overlap).
+ */
+PG_FUNCTION_INFO_V1(test_dsa_pagemap_overflow);
+Datum
+test_dsa_pagemap_overflow(PG_FUNCTION_ARGS)
+{
+	/*
+	 * pagemap_start is MAXALIGN(dsa_segment_header) + MAXALIGN(FreePageManager)
+	 * = 56 + 1096 = 1152, derived from DSA-internal struct sizes.
+	 */
+	const size_t pagemap_start = 1152;
+	const size_t usable_pages = 879;
+	const size_t pagemap880_offset = pagemap_start + 880 * sizeof(dsa_pointer);
+	int		   *tranche_id;
+	bool		found;
+	dsa_area   *a;
+	dsa_pointer dp;
+	size_t		offset;
+
+	tranche_id = GetNamedDSMSegment("test_dsa", sizeof(int),
+									init_tranche, &found, NULL);
+
+	a = dsa_create(*tranche_id);
+	dp = dsa_allocate(a, usable_pages * FPM_PAGE_SIZE);
+	if (!DsaPointerIsValid(dp))
+		elog(ERROR, "test_dsa_pagemap_overflow: allocation failed");
+
+	/* Extract byte offset of the first user page from the dsa_pointer. */
+	offset = (size_t) TEST_DSA_EXTRACT_OFFSET(dp);
+
+	dsa_free(a, dp);
+	dsa_detach(a);
+
+	/*
+	 * The first user page must begin strictly after pagemap[880].  In the bug
+	 * case both are at 8192; in the fixed case the user page is at 12288.
+	 */
+	if (pagemap880_offset + sizeof(dsa_pointer) > offset)
+		elog(ERROR,
+			 "test_dsa_pagemap_overflow: pagemap[880] (offset %zu, size %zu) "
+			 "overlaps first user page (offset %zu)",
+			 pagemap880_offset, sizeof(dsa_pointer), offset);
+
+	PG_RETURN_VOID();
+}
+
