From 223cb59b2558edcef39e88482c0e0b4f093ba75b Mon Sep 17 00:00:00 2001
From: Zsolt Parragi <zsolt.parragi@cancellar.hu>
Date: Wed, 28 Jan 2026 15:38:25 +0100
Subject: [PATCH 1/2] Guc prefix enforcement

This patch introduces a new guc variable, guc_prefix_enforcement.
This variable aims to enforce proper naming/structuring of guc
variables, by checking the following conditions:

* libraries should reserve at least one prefix if they define guc
  variables
* if a library defined one or more prefixes, all variables should be
  defined within those prefixes
* libraries shouldn't define variables in prefixes reserved by other
  libraries (even if they don't reserve a prefix)

It has
4 possible values:

* off, which is the existing earlier behavior, it does nothing
* warning, in which violation of any of the above conditions results in
  an appropriate warning message
* prefix, in which case the first condition is still only a warning, but
  the second and third result in an error
* strict, in which case any violation results in an error

The current patch sets the default of this value to off (or maybe it
should be warning?), and later major versions can increase it to strict,
after we are sure that most extensions follow these rules properly.
---
 src/backend/utils/fmgr/dfmgr.c                |  38 ++++
 src/backend/utils/init/miscinit.c             |   4 +
 src/backend/utils/misc/guc.c                  | 167 +++++++++++++++++-
 src/backend/utils/misc/guc_parameters.dat     |   9 +
 src/backend/utils/misc/guc_tables.c           |  13 ++
 src/include/fmgr.h                            |   1 +
 src/include/utils/guc.h                       |  17 ++
 src/include/utils/guc_tables.h                |   2 +
 src/test/modules/Makefile                     |   3 +
 src/test/modules/meson.build                  |   1 +
 .../test_guc_prefix_enforcement/Makefile      |  17 ++
 .../test_guc_prefix_enforcement/meson.build   |  80 +++++++++
 .../t/001_prefix_enforcement.pl               | 161 +++++++++++++++++
 .../test_guc_no_prefix.c                      |  45 +++++
 .../test_guc_no_reserve.c                     |  42 +++++
 .../test_guc_prefix_enforcement.c             |  44 +++++
 .../test_guc_wrong_prefix.c                   |  44 +++++
 17 files changed, 682 insertions(+), 6 deletions(-)
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/Makefile
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/meson.build
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
 create mode 100644 src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c

diff --git a/src/backend/utils/fmgr/dfmgr.c b/src/backend/utils/fmgr/dfmgr.c
index e636cc81cf8..44408f2f697 100644
--- a/src/backend/utils/fmgr/dfmgr.c
+++ b/src/backend/utils/fmgr/dfmgr.c
@@ -59,6 +59,12 @@ struct DynamicFileList
 static DynamicFileList *file_list = NULL;
 static DynamicFileList *file_tail = NULL;
 
+/*
+ * Track the library currently being loaded (during _PG_init execution).
+ * This allows GUC code to know which library is defining custom variables.
+ */
+static const char *current_loading_library_name = NULL;
+
 /* stat() call under Win32 returns an st_ino field, but it has no meaning */
 #ifndef WIN32
 #define SAME_INODE(A,B) ((A).st_ino == (B).inode && (A).st_dev == (B).device)
@@ -293,11 +299,33 @@ internal_load_library(const char *libname)
 
 		/*
 		 * If the library has a _PG_init() function, call it.
+		 *
+		 * Set current_loading_library_name so that GUC code can track which
+		 * library is defining custom variables. Use the module name from the
+		 * magic block if available, otherwise extract from the filename.
 		 */
 		PG_init = (PG_init_t) dlsym(file_scanner->handle, "_PG_init");
 		if (PG_init)
+		{
+			if (file_scanner->magic->name != NULL)
+				current_loading_library_name = file_scanner->magic->name;
+			else
+			{
+				/* Extract module name from library path */
+				const char *basename = strrchr(libname, '/');
+
+				if (basename)
+					basename++;
+				else
+					basename = libname;
+				current_loading_library_name = basename;
+			}
+
 			(*PG_init) ();
 
+			current_loading_library_name = NULL;
+		}
+
 		/* OK to link it into list */
 		if (file_list == NULL)
 			file_list = file_scanner;
@@ -746,3 +774,13 @@ RestoreLibraryState(char *start_address)
 		start_address += strlen(start_address) + 1;
 	}
 }
+
+/*
+ * Return the name of the library currently being loaded (during _PG_init),
+ * or NULL if no library is currently being loaded.
+ */
+const char *
+get_current_loading_library_name(void)
+{
+	return current_loading_library_name;
+}
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 563f20374ff..aaffe943b2b 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -1856,6 +1856,8 @@ process_shared_preload_libraries(void)
 				   false);
 	process_shared_preload_libraries_in_progress = false;
 	process_shared_preload_libraries_done = true;
+
+	check_guc_prefix_reservations();
 }
 
 /*
@@ -1870,6 +1872,8 @@ process_session_preload_libraries(void)
 	load_libraries(local_preload_libraries_string,
 				   "local_preload_libraries",
 				   true);
+
+	check_guc_prefix_reservations();
 }
 
 /*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index ae9d5f3fb70..1bd573a7e2a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -32,6 +32,7 @@
 #include "access/xact.h"
 #include "access/xlog.h"
 #include "catalog/objectaccess.h"
+#include "fmgr.h"
 #include "catalog/pg_authid.h"
 #include "catalog/pg_parameter_acl.h"
 #include "catalog/pg_type.h"
@@ -76,6 +77,12 @@
 
 static int	GUC_check_errcode_value;
 
+typedef struct ReservedGUCPrefix
+{
+	char	   *prefix;			/* the reserved prefix (e.g., "myext") */
+	char	   *library_name;	/* library that reserved this prefix, or NULL */
+} ReservedGUCPrefix;
+
 static List *reserved_class_prefix = NIL;
 
 /* global variables for check hook support */
@@ -259,6 +266,8 @@ static void replace_auto_config_value(ConfigVariable **head_p, ConfigVariable **
 static bool valid_custom_variable_name(const char *name);
 static bool assignable_custom_variable_name(const char *name, bool skip_errors,
 											int elevel);
+static ReservedGUCPrefix *find_reserved_prefix_for_variable(const char *varname);
+static bool library_has_reserved_prefix(const char *library_name);
 static void do_serialize(char **destptr, Size *maxbytes,
 						 const char *fmt,...) pg_attribute_printf(3, 4);
 static bool call_bool_check_hook(const struct config_generic *conf, bool *newval,
@@ -1022,10 +1031,10 @@ assignable_custom_variable_name(const char *name, bool skip_errors, int elevel)
 		/* ... and it must not match any previously-reserved prefix */
 		foreach(lc, reserved_class_prefix)
 		{
-			const char *rcprefix = lfirst(lc);
+			ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
 
-			if (strlen(rcprefix) == classLen &&
-				strncmp(name, rcprefix, classLen) == 0)
+			if (strlen(reservation->prefix) == classLen &&
+				strncmp(name, reservation->prefix, classLen) == 0)
 			{
 				if (!skip_errors)
 					ereport(elevel,
@@ -1033,7 +1042,7 @@ assignable_custom_variable_name(const char *name, bool skip_errors, int elevel)
 							 errmsg("invalid configuration parameter name \"%s\"",
 									name),
 							 errdetail("\"%s\" is a reserved prefix.",
-									   rcprefix)));
+									   reservation->prefix)));
 				return false;
 			}
 		}
@@ -4788,6 +4797,19 @@ init_custom_variable(const char *name,
 	gen->flags = flags;
 	gen->vartype = type;
 
+	/*
+	 * Record which library defined this variable, for GUC prefix enforcement.
+	 * This will be NULL for variables defined outside of _PG_init context.
+	 */
+	{
+		const char *library_name = get_current_loading_library_name();
+
+		if (library_name)
+			gen->library_name = guc_strdup(FATAL, library_name);
+		else
+			gen->library_name = NULL;
+	}
+
 	return gen;
 }
 
@@ -5143,6 +5165,8 @@ DefineCustomEnumVariable(const char *name,
  * and then prevents new ones from being created.
  * Extensions should call this after they've defined all of their custom
  * GUCs, to help catch misspelled config-file entries.
+ *
+ * Also records the library that reserved this prefix for enforcement purposes.
  */
 void
 MarkGUCPrefixReserved(const char *className)
@@ -5151,6 +5175,8 @@ MarkGUCPrefixReserved(const char *className)
 	HASH_SEQ_STATUS status;
 	GUCHashEntry *hentry;
 	MemoryContext oldcontext;
+	ReservedGUCPrefix *reservation;
+	const char *library_name = get_current_loading_library_name();
 
 	/*
 	 * Check for existing placeholders.  We must actually remove invalid
@@ -5183,12 +5209,141 @@ MarkGUCPrefixReserved(const char *className)
 		}
 	}
 
-	/* And remember the name so we can prevent future mistakes. */
+	/*
+	 * Remember the prefix and its associated library so we can prevent
+	 * future mistakes and enforce prefix ownership.
+	 */
 	oldcontext = MemoryContextSwitchTo(GUCMemoryContext);
-	reserved_class_prefix = lappend(reserved_class_prefix, pstrdup(className));
+	reservation = (ReservedGUCPrefix *) palloc(sizeof(ReservedGUCPrefix));
+	reservation->prefix = pstrdup(className);
+	if (library_name)
+		reservation->library_name = pstrdup(library_name);
+	else
+		reservation->library_name = NULL;
+	reserved_class_prefix = lappend(reserved_class_prefix, reservation);
 	MemoryContextSwitchTo(oldcontext);
 }
 
+static ReservedGUCPrefix *
+find_reserved_prefix_for_variable(const char *varname)
+{
+	ListCell   *lc;
+	const char *sep;
+	int			classLen;
+
+	/* Find the class (prefix) portion of the variable name */
+	sep = strchr(varname, GUC_QUALIFIER_SEPARATOR);
+	if (sep == NULL)
+		return NULL;
+
+	classLen = sep - varname;
+
+	foreach(lc, reserved_class_prefix)
+	{
+		ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
+
+		if (strlen(reservation->prefix) == classLen &&
+			strncmp(varname, reservation->prefix, classLen) == 0)
+			return reservation;
+	}
+
+	return NULL;
+}
+
+static bool
+library_has_reserved_prefix(const char *library_name)
+{
+	ListCell   *lc;
+
+	if (library_name == NULL)
+		return false;
+
+	foreach(lc, reserved_class_prefix)
+	{
+		ReservedGUCPrefix *reservation = (ReservedGUCPrefix *) lfirst(lc);
+
+		if (reservation->library_name != NULL &&
+			strcmp(reservation->library_name, library_name) == 0)
+			return true;
+	}
+
+	return false;
+}
+
+/*
+ * Check GUC prefix reservations after library loading.
+ *
+ * This function validates that:
+ * - In "prefix" mode: all custom GUCs are defined under a reserved prefix
+ * - In "strict" mode: all libraries that define custom GUCs have called
+ *   MarkGUCPrefixReserved()
+ */
+void
+check_guc_prefix_reservations(void)
+{
+	HASH_SEQ_STATUS status;
+	GUCHashEntry *hentry;
+
+	if (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_OFF)
+		return;
+
+	hash_seq_init(&status, guc_hashtab);
+	while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
+	{
+		struct config_generic *gconf = hentry->gucvar;
+		ReservedGUCPrefix *reservation;
+		int			strict_elevel;
+		int			prefix_elevel;
+		bool		has_reserved_prefix;
+
+		/* Skip placeholders and core variables */
+		if (gconf->flags & GUC_CUSTOM_PLACEHOLDER)
+			continue;
+		if (gconf->library_name == NULL)
+			continue;
+
+		strict_elevel = (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_STRICT)
+			? FATAL : WARNING;
+		prefix_elevel = (guc_prefix_enforcement == GUC_PREFIX_ENFORCEMENT_WARN)
+			? WARNING : FATAL;
+
+		has_reserved_prefix = library_has_reserved_prefix(gconf->library_name);
+		if (!has_reserved_prefix)
+		{
+			ereport(strict_elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extension \"%s\" defines GUC variables without calling MarkGUCPrefixReserved()",
+							gconf->library_name),
+					 errdetail("Variable \"%s\" was defined without prefix reservation.",
+							   gconf->name),
+					 errhint("Extensions should call MarkGUCPrefixReserved() after defining their GUC variables.")));
+		}
+
+		reservation = find_reserved_prefix_for_variable(gconf->name);
+
+		if (reservation == NULL)
+		{
+			if (has_reserved_prefix)
+			{
+				ereport(prefix_elevel,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("extension \"%s\" defines GUC variable \"%s\" outside any reserved prefix",
+								gconf->library_name, gconf->name),
+						 errhint("Extensions should call MarkGUCPrefixReserved() to reserve a prefix for their variables.")));
+			}
+		}
+		else if (reservation->library_name != NULL &&
+				 strcmp(reservation->library_name, gconf->library_name) != 0)
+		{
+			/* Variable is under a prefix reserved by a different library */
+			ereport(prefix_elevel,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("extension \"%s\" defines GUC variable \"%s\" under prefix reserved by \"%s\"",
+							gconf->library_name, gconf->name, reservation->library_name)));
+		}
+	}
+}
+
 
 /*
  * Return an array of modified GUC options to show in EXPLAIN.
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 7c60b125564..b1ea6121206 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1160,6 +1160,15 @@
   boot_val => 'false',
 },
 
+{ name => 'guc_prefix_enforcement', type => 'enum', context => 'PGC_SIGHUP', group => 'DEVELOPER_OPTIONS',
+  short_desc => 'Enforcement mode for GUC prefix reservations.',
+  long_desc => 'Controls whether violations of GUC prefix reservations generate warnings or errors.',
+  flags => 'GUC_NOT_IN_SAMPLE',
+  variable => 'guc_prefix_enforcement',
+  boot_val => 'GUC_PREFIX_ENFORCEMENT_OFF',
+  options => 'guc_prefix_enforcement_options',
+},
+
 { name => 'hash_mem_multiplier', type => 'real', context => 'PGC_USERSET', group => 'RESOURCES_MEM',
   short_desc => 'Multiple of "work_mem" to use for hash tables.',
   flags => 'GUC_EXPLAIN',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 73ff6ad0a32..0c492fd4fc9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -491,6 +491,17 @@ static const struct config_enum_entry file_copy_method_options[] = {
 	{NULL, 0, false}
 };
 
+static const struct config_enum_entry guc_prefix_enforcement_options[] = {
+	{"off", GUC_PREFIX_ENFORCEMENT_OFF, false},
+	{"warn", GUC_PREFIX_ENFORCEMENT_WARN, false},
+	{"prefix", GUC_PREFIX_ENFORCEMENT_PREFIX, false},
+	{"strict", GUC_PREFIX_ENFORCEMENT_STRICT, false},
+	{NULL, 0, false}
+};
+
+StaticAssertDecl(lengthof(guc_prefix_enforcement_options) == (GUC_PREFIX_ENFORCEMENT_STRICT + 2),
+				 "array length mismatch");
+
 /*
  * Options for enum values stored in other modules
  */
@@ -581,6 +592,8 @@ int			huge_pages = HUGE_PAGES_TRY;
 int			huge_page_size;
 int			huge_pages_status = HUGE_PAGES_UNKNOWN;
 
+int			guc_prefix_enforcement = GUC_PREFIX_ENFORCEMENT_OFF;
+
 /*
  * These variables are all dummies that don't do anything, except in some
  * cases provide the value for SHOW to display.  The real state is elsewhere
diff --git a/src/include/fmgr.h b/src/include/fmgr.h
index 22dd6526169..5a4dffc6e30 100644
--- a/src/include/fmgr.h
+++ b/src/include/fmgr.h
@@ -797,6 +797,7 @@ extern void get_loaded_module_details(DynamicFileList *dfptr,
 									  const char **module_name,
 									  const char **module_version);
 extern void **find_rendezvous_variable(const char *varName);
+extern const char *get_current_loading_library_name(void);
 extern Size EstimateLibraryStateSpace(void);
 extern void SerializeLibraryState(Size maxsize, char *start_address);
 extern void RestoreLibraryState(char *start_address);
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index bf39878c43e..ee02b9aa987 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -126,6 +126,20 @@ typedef enum
 	PGC_S_SESSION,				/* SET command */
 } GucSource;
 
+/*
+ * Enforcement modes for GUC prefix reservations.
+ *
+ * This controls how strictly we enforce that extensions call
+ * MarkGUCPrefixReserved() and only define GUCs under their reserved prefix.
+ */
+typedef enum
+{
+	GUC_PREFIX_ENFORCEMENT_OFF,		/* no enforcement (earlier behavior) */
+	GUC_PREFIX_ENFORCEMENT_WARN,	/* emit warnings on violations */
+	GUC_PREFIX_ENFORCEMENT_PREFIX,	/* ERROR if GUC defined outside reserved prefix */
+	GUC_PREFIX_ENFORCEMENT_STRICT,	/* ERROR if extension doesn't call MarkGUCPrefixReserved */
+} GucPrefixEnforcement;
+
 /*
  * Parsing the configuration file(s) will return a list of name-value pairs
  * with source location info.  We also abuse this data structure to carry
@@ -287,6 +301,8 @@ extern PGDLLIMPORT bool log_statement_stats;
 extern PGDLLIMPORT bool log_btree_build_stats;
 extern PGDLLIMPORT char *event_source;
 
+extern PGDLLIMPORT int guc_prefix_enforcement;
+
 extern PGDLLIMPORT bool check_function_bodies;
 extern PGDLLIMPORT bool current_role_is_superuser;
 
@@ -456,6 +472,7 @@ extern int	set_config_with_handle(const char *name, config_handle *handle,
 								   GucAction action, bool changeVal,
 								   int elevel, bool is_reload);
 extern config_handle *get_config_handle(const char *name);
+extern void check_guc_prefix_reservations(void);
 extern void AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt);
 extern char *GetConfigOptionByName(const char *name, const char **varname,
 								   bool missing_ok);
diff --git a/src/include/utils/guc_tables.h b/src/include/utils/guc_tables.h
index 71a80161961..1bbaa09212f 100644
--- a/src/include/utils/guc_tables.h
+++ b/src/include/utils/guc_tables.h
@@ -278,6 +278,8 @@ struct config_generic
 	char	   *sourcefile;		/* file current setting is from (NULL if not
 								 * set in config file) */
 	int			sourceline;		/* line in source file */
+	const char *library_name;	/* library that defined this variable, or NULL
+								 * for core variables */
 
 	/* fields for specific variable types */
 	union
diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile
index 4c6d56d97d8..bf811e2ca7e 100644
--- a/src/test/modules/Makefile
+++ b/src/test/modules/Makefile
@@ -29,6 +29,9 @@ SUBDIRS = \
 		  test_escape \
 		  test_extensions \
 		  test_ginpostinglist \
+		  test_guc_prefix_enforcement \
+		  test_hba_guc \
+		  test_hba_guc_contexts \
 		  test_int128 \
 		  test_integerset \
 		  test_json_parser \
diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build
index 1b31c5b98d6..da545219a92 100644
--- a/src/test/modules/meson.build
+++ b/src/test/modules/meson.build
@@ -29,6 +29,7 @@ subdir('test_dsm_registry')
 subdir('test_escape')
 subdir('test_extensions')
 subdir('test_ginpostinglist')
+subdir('test_guc_prefix_enforcement')
 subdir('test_int128')
 subdir('test_integerset')
 subdir('test_json_parser')
diff --git a/src/test/modules/test_guc_prefix_enforcement/Makefile b/src/test/modules/test_guc_prefix_enforcement/Makefile
new file mode 100644
index 00000000000..5265c6ee37d
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/Makefile
@@ -0,0 +1,17 @@
+# src/test/modules/test_guc_prefix_enforcement/Makefile
+
+MODULES = test_guc_prefix_enforcement test_guc_no_reserve test_guc_wrong_prefix test_guc_no_prefix
+PGFILEDESC = "test_guc_prefix_enforcement - test module for GUC prefix reservation enforcement"
+
+TAP_TESTS = 1
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/test_guc_prefix_enforcement
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/src/test/modules/test_guc_prefix_enforcement/meson.build b/src/test/modules/test_guc_prefix_enforcement/meson.build
new file mode 100644
index 00000000000..ccf59f5330a
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/meson.build
@@ -0,0 +1,80 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Main test module (good behavior)
+test_guc_prefix_enforcement_sources = files(
+  'test_guc_prefix_enforcement.c',
+)
+
+if host_system == 'windows'
+  test_guc_prefix_enforcement_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_prefix_enforcement',
+    '--FILEDESC', 'test_guc_prefix_enforcement - test module for GUC prefix reservation enforcement',])
+endif
+
+test_guc_prefix_enforcement = shared_module('test_guc_prefix_enforcement',
+  test_guc_prefix_enforcement_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_prefix_enforcement
+
+# Test module without reservation
+test_guc_no_reserve_sources = files(
+  'test_guc_no_reserve.c',
+)
+
+if host_system == 'windows'
+  test_guc_no_reserve_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_no_reserve',
+    '--FILEDESC', 'test_guc_no_reserve - test module without prefix reservation',])
+endif
+
+test_guc_no_reserve = shared_module('test_guc_no_reserve',
+  test_guc_no_reserve_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_no_reserve
+
+# Test module with wrong prefix
+test_guc_wrong_prefix_sources = files(
+  'test_guc_wrong_prefix.c',
+)
+
+if host_system == 'windows'
+  test_guc_wrong_prefix_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_wrong_prefix',
+    '--FILEDESC', 'test_guc_wrong_prefix - test module with wrong prefix reservation',])
+endif
+
+test_guc_wrong_prefix = shared_module('test_guc_wrong_prefix',
+  test_guc_wrong_prefix_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_wrong_prefix
+
+# Test module with variable without prefix (no dot)
+test_guc_no_prefix_sources = files(
+  'test_guc_no_prefix.c',
+)
+
+if host_system == 'windows'
+  test_guc_no_prefix_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'test_guc_no_prefix',
+    '--FILEDESC', 'test_guc_no_prefix - test module with variable without prefix',])
+endif
+
+test_guc_no_prefix = shared_module('test_guc_no_prefix',
+  test_guc_no_prefix_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += test_guc_no_prefix
+
+tests += {
+  'name': 'test_guc_prefix_enforcement',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'tap': {
+    'tests': [
+      't/001_prefix_enforcement.pl',
+    ],
+  },
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl b/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
new file mode 100644
index 00000000000..273f9b18fa1
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/t/001_prefix_enforcement.pl
@@ -0,0 +1,161 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test GUC prefix reservation enforcement modes
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+#
+# Test 1: Default mode (off) - all extensions should load without errors
+#
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+my $result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads with default enforcement (off)');
+
+$node->stop;
+
+#
+# Test 2: warn mode with good extension - should load with no warnings
+#
+$node = PostgreSQL::Test::Cluster->new('warn_good');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads with warn enforcement');
+
+# Verify no warnings were emitted
+my $log = $node->logfile;
+my $log_contents = slurp_file($log);
+unlike($log_contents, qr/WARNING.*MarkGUCPrefixReserved/,
+	 'no warning about MarkGUCPrefixReserved for good extension');
+unlike($log_contents, qr/WARNING.*reserved prefix/,
+	 'no warning about reserved prefix for good extension');
+
+$node->stop;
+
+#
+# Test 3: warn mode with no_reserve extension - should load with warning
+#
+$node = PostgreSQL::Test::Cluster->new('warn_no_reserve');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_reserve'");
+
+# Start should succeed but log warnings
+$node->start;
+$result = $node->safe_psql('postgres', 'SHOW test_guc_no_reserve.some_var');
+is($result, 'default', 'no_reserve extension loads with warn enforcement');
+
+# Check log for warning
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/WARNING.*without calling MarkGUCPrefixReserved/,
+	 'warn mode emits warning for extension without prefix reservation');
+
+$node->stop;
+
+#
+# Test 4: strict mode with no_reserve extension - should fail to start
+#
+$node = PostgreSQL::Test::Cluster->new('strict_no_reserve');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'strict'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_reserve'");
+
+# Start should fail
+my $ret = $node->start(fail_ok => 1);
+is($ret, 0, 'strict mode prevents startup with non-compliant extension');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*without calling MarkGUCPrefixReserved/,
+	 'strict mode emits error for extension without prefix reservation');
+
+#
+# Test 5: prefix mode with wrong_prefix extension - should fail to start
+#
+$node = PostgreSQL::Test::Cluster->new('prefix_wrong');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'prefix'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_wrong_prefix'");
+
+# Start should fail
+$ret = $node->start(fail_ok => 1);
+is($ret, 0, 'prefix mode prevents startup with wrong prefix extension');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*outside any reserved prefix/,
+	 'prefix mode emits error for extension defining GUC outside reserved prefix');
+
+#
+# Test 6: warn mode with no_prefix extension - should load with warning
+# (extension reserves a prefix but defines variable without any prefix)
+#
+$node = PostgreSQL::Test::Cluster->new('warn_no_prefix');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'warn'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_prefix'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_no_prefix_var');
+is($result, 'default', 'no_prefix extension loads with warn enforcement');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/WARNING.*outside any reserved prefix/,
+	 'warn mode emits warning for extension defining GUC without prefix');
+
+$node->stop;
+
+#
+# Test 7: prefix mode with no_prefix extension - should fail to start
+# (extension reserves a prefix but defines variable without any prefix)
+#
+$node = PostgreSQL::Test::Cluster->new('prefix_no_prefix');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'prefix'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_no_prefix'");
+
+# Start should fail
+$ret = $node->start(fail_ok => 1);
+is($ret, 0, 'prefix mode prevents startup with unprefixed variable');
+
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+like($log_contents, qr/FATAL.*outside any reserved prefix/,
+	 'prefix mode emits error for extension defining GUC without prefix');
+
+#
+# Test 8: verify good extension works in strict mode
+#
+$node = PostgreSQL::Test::Cluster->new('strict_good');
+$node->init;
+$node->append_conf('postgresql.conf', "guc_prefix_enforcement = 'strict'");
+$node->append_conf('postgresql.conf', "shared_preload_libraries = 'test_guc_prefix_enforcement'");
+$node->start;
+
+$result = $node->safe_psql('postgres', 'SHOW test_guc_prefix_enforcement.test_var');
+is($result, 'default', 'good extension loads successfully in strict mode');
+
+# Verify no warnings or errors were emitted
+$log = $node->logfile;
+$log_contents = slurp_file($log);
+unlike($log_contents, qr/(WARNING|FATAL).*MarkGUCPrefixReserved/,
+	 'no warning/error about MarkGUCPrefixReserved for good extension in strict mode');
+unlike($log_contents, qr/(WARNING|FATAL).*reserved prefix/,
+	 'no warning/error about reserved prefix for good extension in strict mode');
+
+$node->stop;
+
+done_testing();
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
new file mode 100644
index 00000000000..610c9e15430
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
@@ -0,0 +1,45 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_no_prefix.c
+ *		Test extension that defines a GUC variable without a prefix.
+ *
+ * This extension reserves a prefix but also defines a variable without
+ * a prefix, which should be flagged by prefix enforcement.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_no_prefix.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_no_prefix",
+	.version = PG_VERSION,
+);
+
+static char *test_var = NULL;
+
+void
+_PG_init(void)
+{
+	/* Define a variable without any prefix */
+	DefineCustomStringVariable("test_guc_no_prefix_var",
+							   "Test variable without prefix",
+							   NULL,
+							   &test_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_no_prefix");
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
new file mode 100644
index 00000000000..c57887ba88c
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
@@ -0,0 +1,42 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_no_reserve.c
+ *		Test module that defines GUCs without calling MarkGUCPrefixReserved
+ *
+ * This module intentionally does NOT call MarkGUCPrefixReserved() after
+ * defining its custom GUC variables.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_no_reserve.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_no_reserve",
+	.version = PG_VERSION,
+);
+
+static char *no_reserve_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_no_reserve.some_var",
+							   "A variable without prefix reservation",
+							   NULL,
+							   &no_reserve_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
new file mode 100644
index 00000000000..1ccbbb1fb8e
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_prefix_enforcement.c
+ *		Test extension that properly reserves its GUC prefix.
+ *
+ * This extension defines a GUC variable and calls MarkGUCPrefixReserved(),
+ * demonstrating correct behavior.
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_prefix_enforcement.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_prefix_enforcement",
+	.version = PG_VERSION,
+);
+
+static char *test_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_prefix_enforcement.test_var",
+							   "Test variable",
+							   NULL,
+							   &test_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_prefix_enforcement");
+}
diff --git a/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c b/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
new file mode 100644
index 00000000000..4e94b367bb9
--- /dev/null
+++ b/src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
@@ -0,0 +1,44 @@
+/*-------------------------------------------------------------------------
+ *
+ * test_guc_wrong_prefix.c
+ *		Test module that reserves one prefix but defines GUCs under another
+ *
+ * This module reserves the prefix "test_guc_wrong" but defines a variable
+ * under "test_guc_other".
+ *
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/test_guc_prefix_enforcement/test_guc_wrong_prefix.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "utils/guc.h"
+
+PG_MODULE_MAGIC_EXT(
+	.name = "test_guc_wrong_prefix",
+	.version = PG_VERSION,
+);
+
+static char *wrong_prefix_var = NULL;
+
+void
+_PG_init(void)
+{
+	DefineCustomStringVariable("test_guc_other.some_var",
+							   "A variable under a different prefix than reserved",
+							   NULL,
+							   &wrong_prefix_var,
+							   "default",
+							   PGC_SUSET,
+							   0,
+							   NULL,
+							   NULL,
+							   NULL);
+
+	MarkGUCPrefixReserved("test_guc_wrong");
+}
-- 
2.43.0

