From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Paul Bunn 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(); +} +