From be0fcdb2ee5eb21e63cbfe185fc9bf4c7522578d Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Sat, 6 Jun 2026 13:21:50 +0530
Subject: [PATCH v20260724 7/7] Allow to resize shared buffers without restart

User interface
==============
shared_buffers is now PGC_SIGHUP instead of PGC_POSTMASTER.

When a server is running, the new value of GUC (set using ALTER SYSTEM
... SET shared_buffers = ...; followed by SELECT pg_reload_conf()) does
not come into effect immediately. Instead a function
pg_resize_shared_buffers() is used to resize the buffer pool. The
function uses the current value of GUC in the backend where it is
executed. The function also coordinates the buffer access in other
backends while resizing the buffer pool.

SHOW shared_buffers now shows the current size of the shared buffer pool
but it also shows pending size of shared buffers, if any.

A new GUC max_shared_buffers is introduced to control the maximum value
of shared_buffers that can be set. By default it is 0. When explicitly
set, it needs to be higher than 'shared_buffers'. When max_shared_buffers
is set to 0, it assumes the same value as GUC shared_buffers. This GUC
determines the size of address space reserved for future buffer pool
sizes and the size of buffer look up table.

When shrinking the shared buffers pool, each buffer in the area being
shrunk needs to be flushed if it's dirty so as not to loose the changes
to that buffer after shrinking. Also, each such buffer needs to be
removed from the buffer mapping table so that backends do not access it
after shrinking.  If a buffer being evicted is pinned, we abort the
resizing operation.  There are other alternative which are not
implemented in the current patches 1. to wait for the pinned buffer to
get unpinned, 2. the backend is killed or it itself cancels the query
or 3.  rollback the operation which is implemented currently.  Note that
option 1 and 2 would require the pinning related local and shared
records to be accessed. But we need infrastructure to do either of this
right now.

Passing current buffer pool state to a new backend
==================================================
So far the buffer pool metdata (NBuffers and the shared memory segment
address space) is saved in process local heap memory since it's static
for the life of a server. It is passed to a new backend through
Postmaster. But with buffer pool being resized while the server running,
we need Postmaster to update its buffer pool metadata as the resizing
progresses and pass it to the new backend. This has few complications:

1. Postmaster does not receive ProcSignalBarrier. So we need to signal
   it separately.
2. Postmaster's local state is inherited by the new backend when
   fork()ed. But we need more complex implementation to pass it to an
   exec()ed backend.
3. If Postmaster can not attend to its core functionality while it is
   busy responding to the resizing signal

Instead, we maintain the buffer manager state in the shared memory.
Every backend maintains its own local copy of the state. When a backend
starts, it fetches syncs its local state with the global state before it
accesses any shared buffers. During run time it updates the local state
in response to the ProcSignalBarriers. The resizing does not advance
until the recent changes to the buffer pool state have been absorbed by
all the concurrent backends. This allows the backends to continue with
their regular activity without bothering about the buffer pool state
becoming inconsistent underneath.

Discussion points
=================

Removing the evicted buffers from buffer ring
---------------------------------------------
If the buffer pool has been shrunk, the buffers in the buffer ring may
not be valid anymore. Modify GetBufferFromRing to check if the buffer is
still valid before using it. This makes GetBufferFromRing() a bit more
expensive because of additional boolean condition and masks any bug that
introduces an invalid buffer into the ring. The alternative fix is more
complex as explained below.

The strategy object is created in CurrentMemoryContext and is not
available in any global structure hence inaccessible when processing
buffer resizing barriers. We may modify GetAccessStrategy() to register
strategy in a global linked list and then arrange to deregister it once
it's no more in use. Looking at the places which use
GetAccessStrategy(), fixing all those may be some work. So defering it
to v2.

Buffer lookup table
-------------------
In order to shrink the buffer lookup table, we need to compact the hash
table directory and the hash table entries so that the free space is
moved to the end of the memory allocated to the buffer lookup table.
This requires exclusively locking the shared hash table for a longer
duration, freezing the server for that duration. Furthermore the
compaction operation itself requires significant code.  Hence we setup
the buffer lookup table considering the maximum possible size of the
buffer pool which is MaxAvailableMemory only once at the beginning. It
is not resized even though buffer pool is resized. We will need separate
effort later to implement a hash table which can be resized without
locking it for a longer duration.

BgWriter reset
--------------
The background writer makes sure to free buffers ahead of the clock
hand. For this it keeps track of the clock hand and jump forward if it
fells behind the clock hand. The position of clock hand is maintained as
a couple (size of buffer pool, position of next victim buffer). When
buffer pool size changes, the position of clock hand is adjusted
according to the new size. The background's knowledge of clock hand goes
out of sync when resize happens. Hence when resize happens the
background writer resets its knowledge of clock hand and jumps to the
clock hand's position. In case the background writer was ahead of clock
hand when resize happens, it will loose that advantage causing a
momentary glitch which may not be noticeable. It may be possible to
compare background writer's past knowledge of clock hand with the
current position by adjusting the previous according to the new size of
the buffer pool and avoid reset.  But it requires more investigation
into background worker and clock hand synchronization. Hence deferred it
to a future version.

Fault tolerance of pg_resize_shared_buffers()
---------------------------------------------
If the backend executing pg_resize_shared_buffers() is interrupted
because of an ERROR, query cancellation, terminate signal, timeout etc.
the server is restarted to avoid leaving the buffer pool in an
inconsistent state. The server restarts with the buffer pool setup with
the new size. If the chances of such interruption are very low, this
solution might work for the first version. However, the restart can be
avoided by in following ways:
1. roll back the resize operation, if ERRORs are recoverable
2. In case of timeout and query cancellation, leave the resizing
   operation with the buffer pool in degenerate but functioning state.
   Re-attempting the resize would complete the operation or roll it
   back.
3. In case of non-recoverable errors or terminate signal, it's better to
   restart the operation.
But this needs more thought and implementation.

GUC type of shared_buffers
--------------------------
With the current code, on the platforms where have_resizable_shmem is
OFF, the users will be able to load new value of shared_buffers but not
resize the buffer pool. Probably we should change that set the type of
GUC to be POSTMASTER on those platforms.  However, that still leaves the
server running one of the platforms which usually supports resizable
shared memory structure, but can not do so at run time because the
shared memory type does not support it, we can not do the same. Will
tackle this as the patches get finalized.

Calling BufferManagerInitProc()
-------------------------------
This function gets called twice in a backend startup sequence. Once so that
BufferManagerInitalizeAccess can access the buffer pool and second time after
ProcSignalInit() for the reasons mentioned there. I think we need a better place
so that we avoid calling it twice and set it only once properly. Also it's not
clear whether the function has been called at all the right places.

I am actually not sure why don't we call ProcSignalInit() right after
InitProcess() in a backend startup sequence. If that happens, we don't need to
worry about it.

Updating local activeNBuffers when allocating new buffer
--------------------------------------------------------
Local activeNBuffers is updated in response to ProcSignalBarrier. It is also
updated by ClockSweepTick() when choosing a victim buffer for the reasons
mentioned there. The GetStrategyBuffer() function which allocates a new buffer
from the buffer ring doesn't update the local activeNBuffers. I think it should
be harmless to just rely on the ProcSignalBarrier to update the local
activeNBuffers and everyone uses the same value. But it might interfere with the
way Clock hand is wrapped.

Barrier optimization
--------------------
Each resize operation uses three barriers to synchronize the buffer pool state
across all the backends. As noted at a few places in the code, we may be able to
use lesser number of barriers to reduce waiting time. However, we need to be
sure that we are not introducing any hazard by doing so. Hence we will keep the
current implementation for now and optimize it after we have enough tests to
cover all the scenarios.

Testing
-------
We have added a new test suite to test the buffer pool resizing. There is a
stress test exercising the buffer pool resizing with pgbench. There are also
some white box tests which use injection points to trigger specific scenarios.
However, we require more tests to cover all the scenarios.

Author: Ashutosh Bapat
Initial patches by: Dmitry Dolgov <9erthalion6@gmail.com>,
Inspired by patches from: Haoyu Huang <haoyu.huang.68@gmail.com>
Author of some tests: Palak Chaturvedi <chaturvedipalak1911@gmail.com>
Reviewed-by: Tomas Vondra
---
 contrib/pg_buffercache/pg_buffercache_pages.c |   7 +
 contrib/pg_prewarm/autoprewarm.c              |   5 +
 doc/src/sgml/config.sgml                      |  62 +-
 doc/src/sgml/func/func-admin.sgml             |  69 ++
 src/backend/bootstrap/bootstrap.c             |   2 +
 src/backend/postmaster/auxprocess.c           |   7 +
 src/backend/postmaster/postmaster.c           |   5 +
 src/backend/storage/buffer/Makefile           |   3 +-
 src/backend/storage/buffer/README             |  62 +-
 src/backend/storage/buffer/buf_init.c         | 299 ++++++-
 src/backend/storage/buffer/buf_resize.c       | 523 +++++++++++
 src/backend/storage/buffer/buf_table.c        |  27 +-
 src/backend/storage/buffer/bufmgr.c           | 176 +++-
 src/backend/storage/buffer/freelist.c         | 150 +++-
 src/backend/storage/buffer/meson.build        |   1 +
 src/backend/storage/ipc/procsignal.c          |  10 +
 src/backend/storage/lmgr/proc.c               |  31 +
 src/backend/tcop/postgres.c                   |   3 +
 src/backend/utils/init/globals.c              |   2 +
 src/backend/utils/init/postinit.c             |  77 +-
 src/backend/utils/misc/guc.c                  |   2 +-
 src/backend/utils/misc/guc_parameters.dat     |  15 +-
 src/include/catalog/pg_proc.dat               |  15 +
 src/include/miscadmin.h                       |   3 +
 src/include/storage/buf_internals.h           |  48 +-
 src/include/storage/bufmgr.h                  |  15 +
 src/include/storage/procsignal.h              |   5 +
 src/include/utils/guc.h                       |   2 +
 src/include/utils/guc_hooks.h                 |   2 +
 src/test/Makefile                             |   3 +-
 src/test/README                               |   3 +
 src/test/buffermgr/Makefile                   |  35 +
 src/test/buffermgr/README                     |  26 +
 src/test/buffermgr/buffermgr_test.conf        |  11 +
 src/test/buffermgr/expected/buffer_resize.out | 290 ++++++
 src/test/buffermgr/meson.build                |  25 +
 src/test/buffermgr/sql/buffer_resize.sql      | 106 +++
 src/test/buffermgr/t/001_resize_buffer.pl     | 193 ++++
 .../buffermgr/t/003_resize_fault_tolerance.pl | 839 ++++++++++++++++++
 .../t/004_client_join_buffer_resize.pl        | 221 +++++
 src/test/buffermgr/t/005_resize_failures.pl   | 171 ++++
 .../buffermgr/t/006_resize_with_syslogger.pl  |  58 ++
 src/test/meson.build                          |   1 +
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    |  76 ++
 src/tools/pgindent/typedefs.list              |   1 +
 45 files changed, 3564 insertions(+), 123 deletions(-)
 create mode 100644 src/backend/storage/buffer/buf_resize.c
 create mode 100644 src/test/buffermgr/Makefile
 create mode 100644 src/test/buffermgr/README
 create mode 100644 src/test/buffermgr/buffermgr_test.conf
 create mode 100644 src/test/buffermgr/expected/buffer_resize.out
 create mode 100644 src/test/buffermgr/meson.build
 create mode 100644 src/test/buffermgr/sql/buffer_resize.sql
 create mode 100644 src/test/buffermgr/t/001_resize_buffer.pl
 create mode 100644 src/test/buffermgr/t/003_resize_fault_tolerance.pl
 create mode 100644 src/test/buffermgr/t/004_client_join_buffer_resize.pl
 create mode 100644 src/test/buffermgr/t/005_resize_failures.pl
 create mode 100644 src/test/buffermgr/t/006_resize_with_syslogger.pl

diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 9512f1efa2f..312343fd7bf 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -289,6 +289,13 @@ pg_buffercache_os_pages_internal(FunctionCallInfo fcinfo, bool include_numa)
 	HeapTuple	tuple;
 	Datum		result;
 
+	/*
+	 * TODO: This allocates memory using NBuffers which may change while this
+	 * function is executed. We need to change this function so that it
+	 * doesn't rely on NBuffers being static throughout the execution of this
+	 * function.
+	 */
+
 	if (SRF_IS_FIRSTCALL())
 	{
 		int			i,
diff --git a/contrib/pg_prewarm/autoprewarm.c b/contrib/pg_prewarm/autoprewarm.c
index deb4c2671b5..e12323fb7c6 100644
--- a/contrib/pg_prewarm/autoprewarm.c
+++ b/contrib/pg_prewarm/autoprewarm.c
@@ -702,6 +702,11 @@ apw_dump_now(bool is_bgworker, bool dump_unlogged)
 		return 0;
 	}
 
+	/*
+	 * TODO: we need to modify this function to not rely on NBuffers being
+	 * constant.
+	 */
+
 	/*
 	 * With sufficiently large shared_buffers, allocation will exceed 1GB, so
 	 * allow for a huge allocation to prevent outright failure.
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 5b6cc870093..7be9378bde8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1802,7 +1802,6 @@ include_dir 'conf.d'
         that is <symbol>BLCKSZ</symbol> bytes, typically 8kB.
         (Non-default values of <symbol>BLCKSZ</symbol> change the minimum
         value.)
-        This parameter can only be set at server start.
        </para>
 
        <para>
@@ -1825,6 +1824,67 @@ include_dir 'conf.d'
         appropriate, so as to leave adequate space for the operating system.
        </para>
 
+       <para>
+        The shared memory consumed by the buffer pool is allocated and
+        initialized according to the value of the GUC at the time of starting
+        the server. A desired new value of GUC can be loaded while the server is
+        running using <systemitem>SIGHUP</systemitem>. But the buffer pool will
+        not be resized immediately. Use
+        <function>pg_resize_shared_buffers()</function> to dynamically resize
+        the shared buffer pool (see <xref linkend="functions-admin"/> for
+        details).  If the running server has
+        <varname>have_resizable_shmem</varname> set to OFF,
+        <function>pg_resize_shared_buffers()</function> throws an error since
+        resizing a shared memory structure is not supported on that server. In
+        such a case the new value of <varname>shared_buffers</varname> gets
+        loaded using <function>pg_reload_conf</function> but the new size of the
+        pool takes effect only after restarting the server.  <command>SHOW
+        shared_buffers</command> shows the currently effective value and any
+        pending value of the GUC. Please note that when the GUC is changed, the
+        other GUCS which use this GUCs value to set their defaults will not be
+        changed. They may still require a server restart to consider new value.
+       </para>
+
+       <para>
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-max-shared-buffers" xreflabel="max_shared_buffers">
+      <term><varname>max_shared_buffers</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>max_shared_buffers</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Sets the upper limit for the <varname>shared_buffers</varname> value.
+        The default value is <literal>0</literal>,
+        which means no explicit limit is set and <varname>max_shared_buffers</varname>
+        will be automatically set to the value of <varname>shared_buffers</varname>
+        at server startup.
+        If this value is specified without units, it is taken as blocks,
+        that is <symbol>BLCKSZ</symbol> bytes, typically 8kB.
+        This parameter can only be set at server start.
+       </para>
+
+       <para>
+        This parameter determines the amount of memory address space to reserve
+        in each backend for expanding the buffer pool in future. While the
+        memory for buffer pool is allocated on demand as it is resized, the
+        memory required for the buffer lookup table and the array used to sort
+        buffers during a checkpoint is allocated at the server start
+        considering the largest buffer pool size allowed by this parameter.
+        <!-- TODO: Provide a numeric example of how much extra memory say max_shared_buffers = 1GB consume. -->
+       </para>
+
+       <para>
+        When <varname>have_resizable_shmem</varname> is OFF, this parameter does
+        not have any effect except limiting the maximum value of
+        <varname>shared_buffers</varname>. It does not decide the address space
+        to be reserved or the size of the buffer lookup table and the array
+        used to sort buffers during a checkpoint.
+       </para>
       </listitem>
      </varlistentry>
 
diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml
index 0eae1c1f616..2a621fff060 100644
--- a/doc/src/sgml/func/func-admin.sgml
+++ b/doc/src/sgml/func/func-admin.sgml
@@ -99,6 +99,75 @@
         <returnvalue>off</returnvalue>
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm>
+         <primary>pg_resize_shared_buffers</primary>
+        </indexterm>
+        <function>pg_resize_shared_buffers</function> ()
+        <returnvalue>boolean</returnvalue>
+       </para>
+       <para>
+        Dynamically resizes the shared buffer pool to match the current value of
+        the <varname>shared_buffers</varname> parameter in the client backend
+        where it is run. This function implements a coordinated resize process
+        that ensures all backend processes continue to operate without causing
+        any hazard. The resize happens in multiple phases to maintain data
+        consistency and system stability. Returns <literal>true</literal> if the
+        resize was successful, otherwise <literal>false</literal>. In the latter
+        case, the buffer pool is left at its previous size; this can happen if a
+        buffer being evicted during shrink could not be released, or if the
+        operating system could not supply enough shared memory to expand the
+        pool. Consult the server log for the underlying reason. This function
+        can only be called by superusers.
+       </para>
+       <para>
+        To resize shared buffers, first update the <varname>shared_buffers</varname>
+        setting and reload the configuration, then verify the new value is loaded
+        before calling this function. For example:
+<programlisting>
+postgres=# ALTER SYSTEM SET shared_buffers = '256MB'; -- Step 1
+ALTER SYSTEM
+postgres=# SELECT pg_reload_conf(); -- Step 2
+ pg_reload_conf
+----------------
+ t
+(1 row)
+
+postgres=# SHOW shared_buffers; -- Step 3
+     shared_buffers
+-------------------------
+ 128MB (pending: 256MB)
+(1 row)
+
+postgres=# SELECT pg_resize_shared_buffers(); -- Step 4
+ pg_resize_shared_buffers
+--------------------------
+ t
+(1 row)
+
+postgres=# SHOW shared_buffers; -- Step 5 (verification)
+ shared_buffers
+----------------
+ 256MB
+(1 row)
+</programlisting>
+        The <command>SHOW shared_buffers</command> at Step 3 is important to
+        verify that the configuration reload was successful and the new value is
+        available to the current session before attempting the resize. The
+        output shows both the current and pending values when the GUC change is
+        pending to be applied.
+       </para>
+       <para>
+        <!-- TODO: Document behaviour when the function is called on platforms that do not support resizable shared memory -->
+    linkend="functions-admin-signal-table"/> send control signals to
+    other server processes.  Use of these functions is restricted to
+    superusers by default but access may be granted to others using
+    <command>GRANT</command>, with noted exceptions.
+   </para>
+       </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index a678f345230..fa9a01a7ec3 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -376,6 +376,8 @@ BootstrapModeMain(int argc, char *argv[], bool check_only)
 
 	InitializeFastPathLocks();
 
+	InitializeMaxNBuffers();
+
 	ShmemCallRequestCallbacks();
 	CreateSharedMemoryAndSemaphores();
 
diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c
index 07a3b5c5923..a09d376d333 100644
--- a/src/backend/postmaster/auxprocess.c
+++ b/src/backend/postmaster/auxprocess.c
@@ -19,6 +19,7 @@
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/auxprocess.h"
+#include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/proc.h"
@@ -106,6 +107,12 @@ AuxiliaryProcessMainCommon(void)
 	 */
 	ShmemReprotectResizableStructs();
 
+	/*
+	 * Update buffer manager's local state, which might have been changed by
+	 * an online resize after startup.
+	 */
+	BufferManagerInitProc();
+
 	RESUME_INTERRUPTS();
 
 	/*
diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index 90c7c4528e8..9a9e60e5fb1 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -959,6 +959,11 @@ PostmasterMain(int argc, char *argv[])
 	 */
 	InitializeFastPathLocks();
 
+	/*
+	 * Calculate MaxNBuffers after NBuffersGUC has been set.
+	 */
+	InitializeMaxNBuffers();
+
 	/*
 	 * Also call any legacy shmem request hooks that might've been installed
 	 * by preloaded libraries.
diff --git a/src/backend/storage/buffer/Makefile b/src/backend/storage/buffer/Makefile
index fd7c40dcb08..3bc9aee85de 100644
--- a/src/backend/storage/buffer/Makefile
+++ b/src/backend/storage/buffer/Makefile
@@ -17,6 +17,7 @@ OBJS = \
 	buf_table.o \
 	bufmgr.o \
 	freelist.o \
-	localbuf.o
+	localbuf.o \
+	buf_resize.o
 
 include $(top_srcdir)/src/backend/common.mk
diff --git a/src/backend/storage/buffer/README b/src/backend/storage/buffer/README
index b332e002ba1..9ee05d562eb 100644
--- a/src/backend/storage/buffer/README
+++ b/src/backend/storage/buffer/README
@@ -181,8 +181,11 @@ buffer header spinlock, which would have to be taken anyway to increment the
 buffer reference count, so it's nearly free.)
 
 The "clock hand" is a buffer index, nextVictimBuffer, that moves circularly
-through all the available buffers.  nextVictimBuffer is protected by the
-buffer_strategy_lock.
+through all the available buffers. Usually a victim can be chosen from the whole
+buffer pool, except when resizing the buffer pool. During resizing the victim
+can be chosen from a range of buffer pool which will not be affected by the
+resizing. See "Resizing shared buffers section" below. nextVictimBuffer is
+protected by the buffer_strategy_lock.
 
 The algorithm for a process that needs to obtain a victim buffer is:
 
@@ -275,3 +278,58 @@ As of 8.4, background writer starts during recovery mode when there is
 some form of potentially extended recovery to perform. It performs an
 identical service to normal processing, except that checkpoints it
 writes are technically restartpoints.
+
+Resizing shared buffers at runtime
+----------------------------------
+
+Before PostgreSQL 20, the size of the shared buffer pool (i.e. the number of
+shared buffers) was given by the global variable NBuffers and was fixed at
+server start time using GUC 'shared_buffers'. In order to change the size of the
+shared buffer pool, one needed to change the GUC and restart the server.
+Starting PostgreSQL 20, PostgreSQL supports resizing the buffer pool without a
+server restart. The value of the GUC 'shared_buffers' is stored in a new GUC
+variable called NBuffersGUC. The old variable NBuffers now strictly reflects the
+number of buffers in the buffer pool. The new GUC variable max_shared_buffers
+defines the maximum size of the shared buffer pool.  See configure.sgml for more
+details about these GUCs.
+
+Resizing buffer pool involves resizing the data structures used by the buffer
+manager and coordinating the resize with all the backends.
+
+The buffer manager maintains following data structures in shared memory.
+1. Buffer Descriptors: An array of BufferDesc structures, one per buffer.
+2. Buffer Blocks: An array of buffer blocks, forming the buffer pool.
+3. Buffer Lookup Table: A hash table mapping a page to the buffer containing
+   that page.
+4. IO conditional variables: An array of conditional variables, one per buffer.
+5. Checkpoint buffer ids: An array of buffer ids used during checkpointing.
+6. Buffer Control: A structure containing information about the size of the
+   buffer pool and whether it is being resized.
+
+Except for the Buffer Lookup Table, Checkpoint buffer ids and Buffer Control
+all other structures are registered as resizable structures. We use
+ShmemResizeStruct() described in doc/src/sgml/xfunc.sgml to resize them. The
+hash table contains multiple shared structures, each of which needs to be
+resized separately. Since these substructures are not registered as separate
+structures, they can not be turned into resizable structures and hence the hash
+table is not registered as a resizable structure. Instead we set it up for
+maximal buffer pool at the server startup. The Checkpoint buffer ids array is
+filled in by the checkpointer with buffers to be written out during a
+checkpoint; if it were resized alongside the buffer pool, a shrink concurrent
+with an ongoing checkpoint could discard entries the checkpointer still needs
+to process. To avoid that, it is also sized for the maximal buffer pool at
+server startup.
+
+For ease of resizing, we differentiate between the number of buffers in the
+whole buffer pool (BufferControl::currentNBuffers) and the number of buffers at
+the start of the buffer pool from which a victim can be chosen for replacement
+(BufferControl::activeNBuffers). Each backend maintains a local copy of these
+two numbers. These copies provide a local view of the buffer pool that the
+backend can rely upon without worrying about the concurrent changes happening to
+the buffer pool. These copies are updated through a barrier mechanism when a
+resize is performed.
+
+To resize shared buffers at runtime a user performs the steps mentioned in the
+description of shared_buffers GUC variable in configure.sgml. Actual resizing
+protocol is documented in the prologue of function pg_resize_shared_buffers() in
+src/backend/storage/buffer/buf_resize.c
diff --git a/src/backend/storage/buffer/buf_init.c b/src/backend/storage/buffer/buf_init.c
index 9ddf6551fcd..91671bce888 100644
--- a/src/backend/storage/buffer/buf_init.c
+++ b/src/backend/storage/buffer/buf_init.c
@@ -17,10 +17,15 @@
 #include "storage/aio.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "storage/pg_shmem.h"
 #include "storage/proclist.h"
 #include "storage/shmem.h"
 #include "storage/subsystems.h"
+#include "utils/guc.h"
+#include "utils/guc_hooks.h"
+#include "utils/injection_point.h"
 
+BufferControlBlock *BufferControl;
 BufferDescPadded *BufferDescriptors;
 char	   *BufferBlocks;
 ConditionVariableMinimallyPadded *BufferIOCVArray;
@@ -37,6 +42,31 @@ const ShmemCallbacks BufferManagerShmemCallbacks = {
 	.attach_fn = BufferManagerShmemAttach,
 };
 
+/*
+ * Resizable shared memory structures backing the buffer pool.
+ */
+static const struct
+{
+	const char *name;
+	size_t		element_size;
+	size_t		alignment;
+	void	  **ptr;
+}			BufferManagerResizableStructs[] = {
+
+	{"Buffer Descriptors",
+		sizeof(BufferDescPadded),
+		PG_CACHE_LINE_SIZE,
+	(void **) &BufferDescriptors},
+	{"Buffer Blocks",
+		BLCKSZ,
+		PG_IO_ALIGN_SIZE,
+	(void **) &BufferBlocks},
+	{"Buffer IO Condition Variables",
+		sizeof(ConditionVariableMinimallyPadded),
+		PG_CACHE_LINE_SIZE,
+	(void **) &BufferIOCVArray},
+};
+
 /*
  * Data Structures:
  *		buffers live in a freelist and a lookup data structure.
@@ -69,6 +99,27 @@ const ShmemCallbacks BufferManagerShmemCallbacks = {
  *		multiple times. Check the PrivateRefCount infrastructure in bufmgr.c.
  */
 
+/*
+ * Initialize a single buffer.
+ */
+static void
+InitializeBuffer(int buf_id)
+{
+	/*
+	 * Do not use GetBufferDescriptor here since it relies on the buffer
+	 * descriptor being initialized.
+	 */
+	BufferDesc *buf = &(BufferDescriptors[buf_id]).bufferdesc;
+
+	ClearBufferTag(&buf->tag);
+	pg_atomic_init_u64(&buf->state, 0);
+	buf->wait_backend_pgprocno = INVALID_PROC_NUMBER;
+	buf->buf_id = buf_id;
+	pgaio_wref_clear(&buf->io_wref);
+	proclist_init(&buf->lock_waiters);
+	ConditionVariableInit(BufferDescriptorGetIOCV(buf));
+}
+
 
 /*
  * Register shared memory area for the buffer pool.
@@ -76,25 +127,21 @@ const ShmemCallbacks BufferManagerShmemCallbacks = {
 static void
 BufferManagerShmemRequest(void *arg)
 {
-	ShmemRequestStruct(.name = "Buffer Descriptors",
-					   .size = NBuffersGUC * sizeof(BufferDescPadded),
-	/* Align descriptors to a cacheline boundary. */
-					   .alignment = PG_CACHE_LINE_SIZE,
-					   .ptr = (void **) &BufferDescriptors,
-		);
-
-	ShmemRequestStruct(.name = "Buffer Blocks",
-					   .size = NBuffersGUC * (Size) BLCKSZ,
-	/* Align buffer pool on IO page size boundary. */
-					   .alignment = PG_IO_ALIGN_SIZE,
-					   .ptr = (void **) &BufferBlocks,
-		);
+	/*
+	 * Fall back to fixed sized shared buffer pool if resizable shared memory
+	 * is not supported on this platform.
+	 */
+#ifdef HAVE_RESIZABLE_SHMEM
+	bool		resizable = (shared_memory_type == SHMEM_TYPE_MMAP);
+#else
+	bool		resizable = false;
+#endif
+	int			min_nbuffers = resizable ? MIN_NUM_BUFFERS : 0;
+	int			max_nbuffers = resizable ? MaxNBuffers : 0;
 
-	ShmemRequestStruct(.name = "Buffer IO Condition Variables",
-					   .size = NBuffersGUC * sizeof(ConditionVariableMinimallyPadded),
-	/* Align descriptors to a cacheline boundary. */
-					   .alignment = PG_CACHE_LINE_SIZE,
-					   .ptr = (void **) &BufferIOCVArray,
+	ShmemRequestStruct(.name = "Buffer Control",
+					   .size = sizeof(BufferControl),
+					   .ptr = (void **) &BufferControl,
 		);
 
 	/*
@@ -103,11 +150,29 @@ BufferManagerShmemRequest(void *arg)
 	 * memory at runtime. As that'd be in the middle of a checkpoint, or when
 	 * the checkpointer is restarted, memory allocation failures would be
 	 * painful.
+	 *
+	 * When the buffer pool is resizable, it is sized for MaxNBuffers up front
+	 * so that the entries filled in by the checkpointer are not freed even
+	 * when the buffer pool is shrunk.
 	 */
 	ShmemRequestStruct(.name = "Checkpoint BufferIds",
-					   .size = NBuffersGUC * sizeof(CkptSortItem),
+					   .size = (size_t) (resizable ? MaxNBuffers : NBuffersGUC) * sizeof(CkptSortItem),
+					   .alignment = PG_CACHE_LINE_SIZE,
 					   .ptr = (void **) &CkptBufferIds,
 		);
+
+	for (int i = 0; i < lengthof(BufferManagerResizableStructs); i++)
+	{
+		size_t		elem_size = BufferManagerResizableStructs[i].element_size;
+
+		ShmemRequestStruct(.name = BufferManagerResizableStructs[i].name,
+						   .minimum_size = min_nbuffers * elem_size,
+						   .size = NBuffersGUC * elem_size,
+						   .maximum_size = max_nbuffers * elem_size,
+						   .alignment = BufferManagerResizableStructs[i].alignment,
+						   .ptr = BufferManagerResizableStructs[i].ptr,
+			);
+	}
 }
 
 /*
@@ -129,34 +194,194 @@ BufferManagerShmemInit(void *arg)
 	 * Initialize all the buffer headers.
 	 */
 	for (int i = 0; i < NBuffers; i++)
+		InitializeBuffer(i);
+
+	/* Initialize BufferControl */
+	pg_atomic_init_u32(&BufferControl->currentNBuffers, NBuffersGUC);
+	pg_atomic_init_u32(&BufferControl->activeNBuffers, NBuffersGUC);
+	pg_atomic_init_u32(&BufferControl->targetNBuffers, NBuffersGUC);
+	pg_atomic_init_u32(&BufferControl->resizer_pid, 0);
+
+	/* Need to perform per backend steps in this backend too. */
+	BufferManagerShmemAttach(arg);
+}
+
+static void
+BufferManagerShmemAttach(void *arg)
+{
+	/* Initialize per-backend file flush context */
+	WritebackContextInit(&BackendWritebackContext,
+						 &backend_flush_after);
+
+	BufferManagerInitProc();
+}
+
+/*
+ * Fetch latest buffer pool sizes (NBuffers and activeNBuffers) shared state
+ * into process local globals.
+ */
+void
+BufferManagerInitProc(void)
+{
+	NBuffers = pg_atomic_read_u32(&BufferControl->currentNBuffers);
+	activeNBuffers = pg_atomic_read_u32(&BufferControl->activeNBuffers);
+
+	elog(DEBUG1, "setting process local buffer pool sizes: currentNBuffers = %d, activeNBuffers = %d", NBuffers, activeNBuffers);
+}
+
+/*
+ * Protect unused shared memory reserved address space.
+ *
+ * Protect the parts of the shared memory address space reserved by the buffer
+ * manager which are not used by current structures from being accessed by
+ * backends.
+ *
+ * Unused address spaces of all resizable shared structures, including the
+ * buffer manager ones, are protected at the server startup together using
+ * ShmemProtectResizableStructs(). We need this function only during resizing of
+ * the buffer pool when we specifically adjust protections of buffer manager
+ * structures.
+ */
+void
+BufferManagerShmemProtect(void)
+{
+	for (int i = 0; i < lengthof(BufferManagerResizableStructs); i++)
 	{
-		BufferDesc *buf = GetBufferDescriptor(i);
+		if (i == 2)
+			INJECTION_POINT("buffer-mgr-protect-struct", NULL);
+		ShmemProtectStruct(BufferManagerResizableStructs[i].name);
+	}
+}
+
+/*
+ * Resize and reinitialize shared buffer manager structures when resizing the
+ * buffer pool.
+ *
+ * Returns true if all the structures were resized successfully, false
+ * otherwise. We expect shrink to always succeed, but expansion may fail if the
+ * system is out of memory.
+ *
+ * The caller will always see all the structures resized consistently. If
+ * expanding a structure fails, all the expanded structures are shrunk back to
+ * their original sizes and no barrier is sent to the other backends.
+ */
+bool
+BufferManagerShmemResize(int currentNBuffers, int targetNBuffers)
+{
+#ifndef HAVE_RESIZABLE_SHMEM
+	ereport(ERROR,
+			errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+			errmsg("resizing shared buffer pool is not supported on this platform"));
+	pg_unreachable();
+#else
+	int			resized = 0;
 
-		ClearBufferTag(&buf->tag);
+	Assert(shared_memory_type == SHMEM_TYPE_MMAP);
 
-		pg_atomic_init_u64(&buf->state, 0);
-		buf->wait_backend_pgprocno = INVALID_PROC_NUMBER;
+	for (int i = 0; i < lengthof(BufferManagerResizableStructs); i++)
+	{
+		const char *name = BufferManagerResizableStructs[i].name;
+		size_t		elem_size = BufferManagerResizableStructs[i].element_size;
+		bool		resize_ok = true;
+
+#ifdef USE_INJECTION_POINTS
+		if (i == 2)
+		{
+			/* Injection point to simulate an interruption in this function. */
+			INJECTION_POINT("buffer-mgr-resize-struct", NULL);
 
-		buf->buf_id = i;
+			/*
+			 * Injection point to simulate a failure in resizing a structure
+			 * like memory allocation failure without actually running out of
+			 * memory.
+			 */
+			if (IS_INJECTION_POINT_ATTACHED("buffer-mgr-resize-struct-fail"))
+				resize_ok = false;
+		}
+#endif
 
-		pgaio_wref_clear(&buf->io_wref);
+		if (resize_ok)
+			resize_ok = ShmemResizeStruct(name, (size_t) targetNBuffers * elem_size);
 
-		proclist_init(&buf->lock_waiters);
-		ConditionVariableInit(BufferDescriptorGetIOCV(buf));
+		if (!resize_ok)
+		{
+			Assert(targetNBuffers > currentNBuffers);
+			for (int j = 0; j < resized; j++)
+				ShmemResizeStruct(BufferManagerResizableStructs[j].name,
+								  (size_t) currentNBuffers * BufferManagerResizableStructs[j].element_size);
+			return false;
+		}
+		resized++;
 	}
 
-	/* Initialize per-backend file flush context */
-	WritebackContextInit(&BackendWritebackContext,
-						 &backend_flush_after);
+	/* Initialize the headers for new buffers. */
+	for (int i = currentNBuffers; i < targetNBuffers; i++)
+		InitializeBuffer(i);
+
+	return true;
+#endif
 }
 
-static void
-BufferManagerShmemAttach(void *arg)
+/*
+ * check_shared_buffers
+ *		GUC check_hook for shared_buffers
+ *
+ * When reloading the configuration, shared_buffers should not be set to a value
+ * higher than max_shared_buffers fixed at the boot time.
+ */
+bool
+check_shared_buffers(int *newval, void **extra, GucSource source)
 {
-	/* Update the size of the buffer pool. */
-	NBuffers = NBuffersGUC;
+	if (finalMaxNBuffers && *newval > MaxNBuffers)
+	{
+		GUC_check_errdetail("\"shared_buffers\" must be less than \"max_shared_buffers\".");
+		return false;
+	}
+	return true;
+}
 
-	/* Initialize per-backend file flush context */
-	WritebackContextInit(&BackendWritebackContext,
-						 &backend_flush_after);
+/*
+ * show_shared_buffers
+ *		GUC show_hook for shared_buffers
+ *
+ * Shows both current and pending buffer counts with proper unit formatting.
+ */
+const char *
+show_shared_buffers(bool use_units)
+{
+	static char buffer[128];
+	int64		current_value;
+	const char *current_unit;
+	int			currentNBuffers = pg_atomic_read_u32(&BufferControl->currentNBuffers);
+
+	if (use_units)
+		convert_int_from_base_unit(currentNBuffers, GUC_UNIT_BLOCKS, &current_value, &current_unit);
+	else
+	{
+		current_unit = "";
+		current_value = currentNBuffers;
+	}
+	snprintf(buffer, sizeof(buffer), INT64_FORMAT "%s", current_value, current_unit);
+
+	if (currentNBuffers != NBuffersGUC)
+	{
+		int64		pending_value;
+		const char *pending_unit;
+
+		/*
+		 * Shared buffer pool is pending to be resized, show both current and
+		 * pending sizes.
+		 */
+		if (use_units)
+			convert_int_from_base_unit(NBuffersGUC, GUC_UNIT_BLOCKS, &pending_value, &pending_unit);
+		else
+		{
+			pending_value = NBuffersGUC;
+			pending_unit = "";
+		}
+		snprintf(buffer + strlen(buffer), sizeof(buffer) - strlen(buffer), " (pending: " INT64_FORMAT "%s)",
+				 pending_value, pending_unit);
+	}
+
+	return buffer;
 }
diff --git a/src/backend/storage/buffer/buf_resize.c b/src/backend/storage/buffer/buf_resize.c
new file mode 100644
index 00000000000..c758493f353
--- /dev/null
+++ b/src/backend/storage/buffer/buf_resize.c
@@ -0,0 +1,523 @@
+/*-------------------------------------------------------------------------
+ *
+ * buf_resize.c
+ *	  shared buffer pool resizing functionality
+ *
+ * This module contains the implementation of shared buffer pool resizing,
+ * including the main resize coordination function and barrier processing
+ * functions that synchronize all backends during resize operations.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/storage/buffer/buf_resize.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "fmgr.h"
+#include "funcapi.h"
+#include "miscadmin.h"
+#include "postmaster/bgwriter.h"
+#include "storage/bufmgr.h"
+#include "storage/buf_internals.h"
+#include "storage/ipc.h"
+#include "storage/pg_shmem.h"
+#include "storage/pmsignal.h"
+#include "storage/procsignal.h"
+#include "storage/shmem.h"
+#include "utils/builtins.h"
+#include "utils/injection_point.h"
+
+static volatile sig_atomic_t safe_exit = true;
+
+static bool resize_shared_buffers_internal(void);
+static void buf_resize_shmem_exit(int code, Datum arg);
+
+/*
+ * Set the new buffer allocation pool size, broadcast it to all the backends
+ * and wait for them to acknowledge it.
+ */
+static void
+buf_resize_set_new_alloc_size(int alloc_size)
+{
+	uint64		generation;
+
+	pg_atomic_write_u32(&BufferControl->activeNBuffers, alloc_size);
+	StrategyAdjustNewBufAllocSize();
+	generation = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_NEW_BUFFER_ALLOC);
+	INJECTION_POINT("pgrsb-new-buffer-alloc-barrier-sent", NULL);
+	WaitForProcSignalBarrier(generation);
+	elog(LOG, "all backends acknowledged PROCSIGNAL_BARRIER_NEW_BUFFER_ALLOC barrier");
+}
+
+/*
+ * Update the buffer pool size, broadcast it to all the backends and wait for
+ * them to acknowledge the change.
+ */
+static void
+buf_resize_set_buffer_pool_size(int new_size)
+{
+	uint64		generation;
+
+	pg_atomic_write_u32(&BufferControl->currentNBuffers, new_size);
+	generation = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_BUFFER_POOL_SIZE);
+	INJECTION_POINT("pgrsb-buffer-pool-size-barrier-sent", NULL);
+	WaitForProcSignalBarrier(generation);
+	elog(LOG, "all backends acknowledged PROCSIGNAL_BARRIER_BUFFER_POOL_SIZE barrier");
+}
+
+/*
+ * Resize the shared buffer manager structures, broadcast the change to all
+ * the backends and wait for them to acknowledge it.
+ *
+ * If memory is not available when expanding the buffer pool, this function
+ * returns false without sending the barrier. When shrinking the buffer pool, we
+ * don't expect any failure, so this function always returns true.
+ */
+static bool
+buf_resize_shmem_resize(int currentNBuffers, int targetNBuffers)
+{
+	uint64		generation;
+
+	if (!BufferManagerShmemResize(currentNBuffers, targetNBuffers))
+	{
+		Assert(targetNBuffers > currentNBuffers);
+		return false;
+	}
+
+	generation = EmitProcSignalBarrier(PROCSIGNAL_BARRIER_BUFFER_POOL_RESIZE);
+	INJECTION_POINT("pgrsb-buffer-pool-resize-barrier-sent", NULL);
+	WaitForProcSignalBarrier(generation);
+	elog(LOG, "all backends acknowledged PROCSIGNAL_BARRIER_BUFFER_POOL_RESIZE barrier");
+	return true;
+}
+
+/*
+ * C implementation of SQL interface to update the shared buffers according to
+ * the current values of shared_buffers GUC.
+ *
+ * Atomic BufferControl::resizer_pid holds the PID of the backend currently
+ * performing a resize, or 0 when no resize is in progress. Using
+ * compare-and-exchange to set and reset this field, we make sure that only one
+ * resize is in progress at a time.
+ *
+ * Shrinking the buffer pool involves the following steps:
+ * - s1: Set BufferControl::activeNBuffers to the new size of the buffer pool
+ *   and send SHBUF_NEW_BUFFER_ALLOC barrier to all backends. Every backend is
+ *   expected to update their local buffer allocation pool size and acknowledge
+ *   the barrier.
+ * - s2: Wait for all backends to acknowledge the barrier. When all backends
+ *   have acknowledged the barrier, new buffer allocations will be restricted
+ *   to the new size of the buffer pool.
+ * - s3: Evict the buffers beyond the new size. A backend which still requires
+ *   a previously allocated buffer which is being evicted, must have pinned it.
+ *   If a pinned buffer is encountered, the resize operation is rolled back and
+ *   the function returns false.
+ * - s4: If eviction succeeds, no backend should be using the buffers beyond
+ *   the new size of the buffer pool and no new buffers can be allocated in
+ *   that range. Update BufferControl::currentNBuffers to the new size of the
+ *   buffer pool and send SHBUF_BUFFER_POOL_SIZE barrier to all backends. In
+ *   response, all the backends should update their local buffer pool size and
+ *   acknowledge the barrier.
+ * - s5: Wait for all backends to acknowledge the barrier. When all backends
+ *   have acknowledged the barrier, no backend will be accessing the shared
+ *   buffer manager structures beyond the new size of the buffer pool.
+ * - s6: Resize the shared buffer manager structures to the new size (using
+ *   ShmemResizeStruct()) and send SHBUF_BUFFER_POOL_RESIZE barrier to all
+ *   backends. In response, all the backends should call ShmemProtectStruct()
+ *   to update the memory address space protection of the shared buffer
+ *   manager structures.
+ * - s7: Wait for all backends to acknowledge the barrier, before returning
+ *   true to indicate successful resizing.
+ *
+ * Expanding the buffer pool involves the following steps:
+ * - e1: Resize the shared buffer manager structures to the new size (using
+ *   ShmemResizeStruct()) and send SHBUF_BUFFER_POOL_RESIZE barrier to all the
+ *   backends. In response, all the backends should call ShmemProtectStruct()
+ *   to update the memory address space protection of the shared buffer
+ *   manager structures. If expanding the shared buffer manager structures
+ *   fails because of lack of memory, the function returns false without
+ *   sending the barrier.
+ * - e2: Wait for all backends to acknowledge the barrier. When all backends
+ *   have acknowledged the barrier, every backend will be able to access the
+ *   shared buffer manager structures beyond the old size of the buffer pool.
+ * - e3: Update BufferControl::currentNBuffers to the new size of the buffer
+ *   pool and send SHBUF_BUFFER_POOL_SIZE barrier to all backends. In response,
+ *   all the backends should update their local buffer pool size and
+ *   acknowledge the barrier.
+ * - e4: Wait for all backends to acknowledge the barrier. When all backends
+ *   have acknowledged the barrier, every backend is setup to use the new size
+ *   of the buffer pool.
+ * - e5: Update BufferControl::activeNBuffers to the new size of the buffer
+ *   pool, so that backends can start allocating from the new area of the
+ *   buffer pool. Send SHBUF_NEW_BUFFER_ALLOC barrier to all backends. In
+ *   response, all the backends should update their local buffer allocation
+ *   pool size and acknowledge the barrier.
+ * - e6: Wait for all backends to acknowledge the barrier, before returning
+ *   true to indicate successful resizing.
+ *
+ * Reason we introduce e4:
+ * Once we expand the new buffer allocation area, all the backends will start
+ * allocating buffers from the new area. Since this happens asynchronously,
+ * there is a chance that some backends may see buffers from outside their
+ * known buffer pool size. To avoid that, first set the new buffer pool size,
+ * broadcast it to all the backends and wait for them to update their
+ * knowledge of buffer pool size.  We may be able to avoid sending the barrier
+ * after the first step or avoid them altogether but it's not clear that that
+ * is completely hazard free. It feels safer this way, even though it takes
+ * longer.
+ *
+ * If a timeout happens or request to cancel query arrives while the function is
+ * being executed, we need to abort the operation immediately. The state of the
+ * buffer pool and its state as viewed by the backends may not be consistent at
+ * that point. Hence we escalate it to PANIC to restart the server and avoid
+ * inconsistent state. We may improve this situation by leaving the buffer pool
+ * in a consistent but degenerate state and allowing a subsequent resize
+ * operation to rollback or continue the operation.
+ *
+ * If an ERROR is raised while the function is being executed, we may have
+ * already entered an inconsistent state. Hence we escalate it to PANIC to
+ * restart the server and avoid inconsistent state.
+ */
+Datum
+pg_resize_shared_buffers(PG_FUNCTION_ARGS)
+{
+	bool		success = false;
+
+	/*
+	 * Register the exit hook before claiming resizer_pid, so that if we exit
+	 * after claiming resizer_pid, the hook is in place to reset it.
+	 */
+	before_shmem_exit(buf_resize_shmem_exit, 0);
+
+	PG_TRY();
+	{
+		uint32		expected_pid = 0;
+
+		if (!pg_atomic_compare_exchange_u32(&BufferControl->resizer_pid,
+											&expected_pid, MyProcPid))
+		{
+			/*
+			 * Another backend holds resizer_pid; expected_pid was updated by
+			 * the CAS to reflect its PID.
+			 */
+			elog(LOG, "shared buffer resize already in progress in backend %u",
+				 expected_pid);
+			/* No shared memory was touched, so it should be safe to exit. */
+			Assert(safe_exit);
+		}
+		else
+		{
+			INJECTION_POINT("pg-resize-shared-buffers-flag-set", NULL);
+
+			/*
+			 * We are about to make changes to the shared memory which can not
+			 * be rolled back easily since we need all the backends to
+			 * acknowledge these changes. Indicate that a sudden exit in this
+			 * state can leave the server in an inconsistent state.
+			 */
+			safe_exit = false;
+			success = resize_shared_buffers_internal();
+
+			/*
+			 * The changes to shared memory are in a consistent state across
+			 * all the backends, so it should be safe to exit.
+			 */
+			safe_exit = true;
+		}
+	}
+	PG_FINALLY();
+	{
+		uint32		expected_pid = MyProcPid;
+
+		/*
+		 * We are in the middle of resizing and caught an error. Without
+		 * knowing the reason and exact state of resizing it's not safe to
+		 * continue or to exit. Restarting the server is the safest option
+		 * here.  Emit the error to the server log and raise PANIC to restart
+		 * the server.
+		 */
+		if (!safe_exit)
+		{
+			HOLD_INTERRUPTS();
+			errcontext("during shared buffer resize");
+			EmitErrorReport();
+			ereport(PANIC,
+					errmsg("shared buffer resize caught an error when shared memory was in an inconsistent state"));
+			pg_unreachable();
+		}
+
+		/*
+		 * Reset the PID, if we set it before removing the shmem_exit hook so
+		 * as not to leave it set after the backend has exited.
+		 */
+		(void) pg_atomic_compare_exchange_u32(&BufferControl->resizer_pid,
+											  &expected_pid, 0);
+		cancel_before_shmem_exit(buf_resize_shmem_exit, 0);
+	}
+	PG_END_TRY();
+
+	if (success)
+		elog(LOG, "shared buffer resizing to %d buffers completed successfully", NBuffersGUC);
+	else
+		elog(WARNING, "shared buffer resizing to %d buffers failed", NBuffersGUC);
+
+	PG_RETURN_BOOL(success);
+}
+
+/*
+ * Workhorse function for the C implementation.
+ */
+static bool
+resize_shared_buffers_internal(void)
+{
+	int			currentNBuffers;
+	int			targetNBuffers;
+	bool		resize_success;
+
+	currentNBuffers = pg_atomic_read_u32(&BufferControl->currentNBuffers);
+	targetNBuffers = NBuffersGUC;
+	if (currentNBuffers == targetNBuffers)
+	{
+		elog(LOG, "shared buffers are already at %d, no need to resize", currentNBuffers);
+		return true;
+	}
+
+	/*
+	 * TODO: What if the NBuffersGUC value seen here is not the desired one
+	 * because somebody did a pg_reload_conf() between the last
+	 * pg_reload_conf() and execution of this function?
+	 */
+
+	pg_atomic_write_u32(&BufferControl->targetNBuffers, targetNBuffers);
+	elog(LOG, "resizing shared buffers from %d to %d", currentNBuffers, targetNBuffers);
+
+	if (targetNBuffers < currentNBuffers)
+	{
+		/*
+		 * step s1, s2: Restrict new buffer allocations to the new buffer pool
+		 * size.
+		 *
+		 * TODO: Alternate design idea by Andres (as I understand it): Set
+		 * BufferControl::activeNBuffers and send the barrier. Instead of
+		 * waiting for barrier, start evicting buffers but don't unpin the
+		 * evicted buffers so that they will not be considered for new
+		 * allocations. Once all the buffers are evicted wait for the barrier
+		 * to be acknowledged. This will reduce the time taken to shrink the
+		 * buffer pool.
+		 */
+		elog(LOG, "shrinking buffer pool, restricting allocations to %d buffers", targetNBuffers);
+		buf_resize_set_new_alloc_size(targetNBuffers);
+
+		/* Step s3: Evict buffers in the area being shrunk */
+		elog(LOG, "evicting buffers %u..%u", targetNBuffers + 1, currentNBuffers);
+		if (!EvictExtraBuffers(targetNBuffers, currentNBuffers))
+		{
+			elog(WARNING, "failed to evict extra buffers during shrinking");
+
+			/* Eviction failed, rollback the buffer resize operation. */
+			pg_atomic_write_u32(&BufferControl->targetNBuffers, currentNBuffers);
+			buf_resize_set_new_alloc_size(currentNBuffers);
+			return false;
+		}
+
+		/* Step s4, s5: Update the buffer pool size. */
+		buf_resize_set_buffer_pool_size(targetNBuffers);
+	}
+
+	/* Step s6, s7 or e1, e2: Resize the buffer manager structures. */
+	resize_success = buf_resize_shmem_resize(currentNBuffers, targetNBuffers);
+
+	if (targetNBuffers > currentNBuffers)
+	{
+		if (!resize_success)
+		{
+			elog(WARNING, "failed to expand buffer pool structures");
+
+			/* Revert any changes to the shared memory in this function. */
+			pg_atomic_write_u32(&BufferControl->targetNBuffers, currentNBuffers);
+			return false;
+		}
+
+		/* Step e3, e4: Declare new buffer pool size. */
+		buf_resize_set_buffer_pool_size(targetNBuffers);
+
+		/* Step e5, e6: Let expanded buffer pool be used by all backends. */
+		buf_resize_set_new_alloc_size(targetNBuffers);
+	}
+
+	return true;
+}
+
+/*
+ * Function to handle process exit when buffer resizing is in progress.
+ */
+static void
+buf_resize_shmem_exit(int code, Datum arg)
+{
+	uint32		expected_pid;
+
+	/*
+	 * If resizer_pid does not match our PID, either we never claimed it or we
+	 * have already released it. Nothing to do.
+	 */
+	if (pg_atomic_read_u32(&BufferControl->resizer_pid) != MyProcPid)
+		return;
+
+	/*
+	 * Resize is in progress and the process crashed. We do not know exactly
+	 * at which step of the resizing we are. Just restart the server to be
+	 * safe.
+	 *
+	 * TODO: If we can perform heavy operations in this callback like waiting
+	 * for barriers, we could set the current status of resizing in the
+	 * process local memory and use this callback to rollback every operation
+	 * that was performed, except buffer eviction.
+	 */
+	if (!safe_exit)
+		ereport(PANIC,
+				errmsg("buffer resize operation interrupted, restarting to avoid inconsistent state"));
+
+	/*
+	 * safe_exit should be set to true when new allocations are not using the
+	 * whole buffer pool, so the following condition should never happen. But
+	 * be on the safer side.
+	 */
+	if (pg_atomic_read_u32(&BufferControl->currentNBuffers) != pg_atomic_read_u32(&BufferControl->activeNBuffers))
+		ereport(PANIC,
+				(errmsg("buffer resize operation interrupted at an unexpected stage, restarting to avoid inconsistent state")));
+
+	/*
+	 * Reset targetNBuffers before releasing resizer_pid, so that a backend
+	 * claiming ownership immediately afterwards does not have its own
+	 * targetNBuffers overwritten by us.
+	 */
+	pg_atomic_write_u32(&BufferControl->targetNBuffers, pg_atomic_read_u32(&BufferControl->currentNBuffers));
+
+	expected_pid = MyProcPid;
+	(void) pg_atomic_compare_exchange_u32(&BufferControl->resizer_pid,
+										  &expected_pid, 0);
+}
+
+/*
+ * Process and acknowledge PROCSIGNAL_BARRIER_NEW_BUFFER_ALLOC.
+ */
+bool
+ProcessBarrierNewBufferAlloc(void)
+{
+	elog(DEBUG2, "processing barrier to restrict new buffer allocations to %d buffers (target = %d)",
+		 pg_atomic_read_u32(&BufferControl->activeNBuffers), pg_atomic_read_u32(&BufferControl->targetNBuffers));
+
+	INJECTION_POINT("pgrsb-handle-new-buffer-alloc-barrier", NULL);
+
+	Assert(pg_atomic_read_u32(&BufferControl->resizer_pid) != 0);
+
+	Assert(NBuffers == pg_atomic_read_u32(&BufferControl->currentNBuffers));
+	activeNBuffers = pg_atomic_read_u32(&BufferControl->activeNBuffers);
+
+	return true;
+}
+
+/*
+ * Process and acknowledge PROCSIGNAL_BARRIER_BUFFER_POOL_RESIZE.
+ */
+bool
+ProcessBarrierBufferPoolResize(void)
+{
+	elog(DEBUG2, "processing barrier to propagate resized shared buffer pool structures");
+
+	INJECTION_POINT("pgrsb-handle-buffer-pool-resize-barrier", NULL);
+
+	Assert(pg_atomic_read_u32(&BufferControl->resizer_pid) != 0);
+
+	Assert(NBuffers == pg_atomic_read_u32(&BufferControl->currentNBuffers));
+	Assert(activeNBuffers == pg_atomic_read_u32(&BufferControl->activeNBuffers));
+
+	/*
+	 * Access permissions to address range covered by a resizable structure is
+	 * maintained consistently across all the backends right from the time a
+	 * backend is started. We maintain that consistency as the buffer pool is
+	 * resized.So ideally modifying the access permissions in this backend
+	 * should not fail. But if it does, the address space accessible to this
+	 * backend may be inconsistent with the new buffer pool size and also with
+	 * the other backends. This may cause data corruption and other memory
+	 * access issues, if we let this backend continue to run and access the
+	 * buffer pool. Better to quit from the faulty backend.
+	 */
+	PG_TRY();
+	{
+		BufferManagerShmemProtect();
+	}
+	PG_CATCH();
+	{
+		/*
+		 * We don't know what caused the error and so avoid using further
+		 * resources. Emit the original error to the server log so that it's
+		 * not lost and raise a FATAL to terminate this backend.
+		 */
+		HOLD_INTERRUPTS();
+		errcontext("during shared buffer pool resize barrier");
+		EmitErrorReport();
+		ereport(FATAL,
+				(errmsg("shared buffer pool resize barrier caught an error while updating buffer pool protection")));
+		pg_unreachable();
+	}
+	PG_END_TRY();
+
+	return true;
+}
+
+/*
+ * Process and acknowledge PROCSIGNAL_BARRIER_BUFFER_POOL_SIZE.
+ */
+bool
+ProcessBarrierBufferPoolSize(void)
+{
+	elog(DEBUG2, "processing barrier to establish new size of the buffer pool to %d", pg_atomic_read_u32(&BufferControl->currentNBuffers));
+
+	INJECTION_POINT("pgrsb-handle-buffer-pool-size-barrier", NULL);
+
+	Assert(pg_atomic_read_u32(&BufferControl->resizer_pid) != 0);
+
+	Assert(activeNBuffers == pg_atomic_read_u32(&BufferControl->activeNBuffers));
+	NBuffers = pg_atomic_read_u32(&BufferControl->currentNBuffers);
+
+	return true;
+}
+
+/*
+ * SQL-callable function reporting the current shared buffer pool resize
+ * status.
+ */
+Datum
+pg_get_buffer_resize_status(PG_FUNCTION_ARGS)
+{
+#define PG_GET_BUFFER_RESIZE_STATUS_COLS	4
+	TupleDesc	tupdesc;
+	Datum		values[PG_GET_BUFFER_RESIZE_STATUS_COLS];
+	bool		nulls[PG_GET_BUFFER_RESIZE_STATUS_COLS] = {0};
+	HeapTuple	tuple;
+
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+	tupdesc = BlessTupleDesc(tupdesc);
+
+	values[0] = Int32GetDatum((int32) pg_atomic_read_u32(&BufferControl->activeNBuffers));
+	values[1] = Int32GetDatum((int32) pg_atomic_read_u32(&BufferControl->currentNBuffers));
+	values[2] = Int32GetDatum((int32) pg_atomic_read_u32(&BufferControl->targetNBuffers));
+	values[3] = Int32GetDatum((int32) pg_atomic_read_u32(&BufferControl->resizer_pid));
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
+#undef PG_GET_BUFFER_RESIZE_STATUS_COLS
+}
+
+/*
+ * TODO: add progress report facility if required.
+ */
diff --git a/src/backend/storage/buffer/buf_table.c b/src/backend/storage/buffer/buf_table.c
index c82b71deaa6..85664990b3c 100644
--- a/src/backend/storage/buffer/buf_table.c
+++ b/src/backend/storage/buffer/buf_table.c
@@ -28,6 +28,7 @@
 #include "utils/builtins.h"
 #include "storage/lwlock.h"
 #include "storage/subsystems.h"
+#include "storage/pg_shmem.h"
 
 /* entry for buffer lookup hashtable */
 typedef struct
@@ -58,15 +59,25 @@ BufTableShmemRequest(void *arg)
 	 * Request the shared buffer lookup hashtable.
 	 *
 	 * Since we can't tolerate running out of lookup table entries, we must be
-	 * sure to specify an adequate table size here.  The maximum steady-state
-	 * usage is of course as many entries as the number of buffers in the
-	 * pool, but BufferAlloc() tries to insert a new entry before deleting the
-	 * old.  In principle this could be happening in each partition
-	 * concurrently, so we could need as many as (number of buffers in the
-	 * pool) + NUM_BUFFER_PARTITIONS entries. Since we are still requesting
-	 * shared memory, use the GUC value instead of the actual size.
+	 * sure to specify an adequate table size here. The maximum number of
+	 * entries we could need is the number of buffers, but BufferAlloc() tries
+	 * to insert a new entry before deleting the old.  In principle this could
+	 * be happening in each partition concurrently, so we could need as many
+	 * as (number of buffers) + NUM_BUFFER_PARTITIONS entries. The number of
+	 * buffers can be increased upto MaxNBuffers at run time. So we need to
+	 * make sure that the table can accommodate MaxNBuffers +
+	 * NUM_BUFFER_PARTITIONS entries.
+	 *
+	 * See storage/buffer/README for reasons why we don't register the hash
+	 * table as a resizable structure.
+	 *
 	 */
-	size = NBuffersGUC + NUM_BUFFER_PARTITIONS;
+#ifdef HAVE_RESIZABLE_SHMEM
+	size = (shared_memory_type == SHMEM_TYPE_MMAP) ? MaxNBuffers : NBuffersGUC;
+#else
+	size = NBuffersGUC;
+#endif
+	size = size + NUM_BUFFER_PARTITIONS;
 
 	ShmemRequestHash(.name = "Shared Buffer Lookup Table",
 					 .nelems = size,
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index db7f34d2274..ff45a171cfb 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -223,7 +223,10 @@ int			io_max_combine_limit = DEFAULT_IO_COMBINE_LIMIT;
 int			checkpoint_flush_after = DEFAULT_CHECKPOINT_FLUSH_AFTER;
 int			bgwriter_flush_after = DEFAULT_BGWRITER_FLUSH_AFTER;
 int			backend_flush_after = DEFAULT_BACKEND_FLUSH_AFTER;
+
 int			NBuffers = 0;		/* number of buffers in the buffer pool */
+int			activeNBuffers = 0; /* number of buffers at the start of the pool
+								 * from which new buffer allocations happnen. */
 
 /* local state for LockBufferForCleanup */
 static BufferDesc *PinCountWaitBuf = NULL;
@@ -814,6 +817,15 @@ PrefetchBuffer(Relation reln, ForkNumber forkNum, BlockNumber blockNum)
  * Compared to ReadBuffer(), this avoids a buffer mapping lookup when it's
  * successful.  Return true if the buffer is valid and still has the expected
  * tag.  In that case, the buffer is pinned and the usage count is bumped.
+ *
+ * The callers of this function should make sure that the buffer is valid even
+ * if the shared buffer pool has undergone a resize.  Buffer pool resizing waits
+ * for all backends to acknowledge the barrier before changing the buffer pool
+ * size. Hence the caller should call this function after validation without an
+ * intervening ProcSignalBarrier processing.
+ *
+ * TODO: This function could perform the validation in this function itself
+ * instead of relying on the two callers who do it currently.
  */
 bool
 ReadRecentBuffer(RelFileLocator rlocator, ForkNumber forkNum, BlockNumber blockNum,
@@ -3655,6 +3667,11 @@ BufferSync(int flags)
 
 	TRACE_POSTGRESQL_BUFFER_SYNC_START(NBuffers, num_to_scan);
 
+	/*
+	 * TODO: Test the case when buffer pool is shrunk after CkptBufferIds is
+	 * filled and num_to_scan is higher than the new NBuffers?
+	 */
+
 	/*
 	 * Sort buffers that need to be written to reduce the likelihood of random
 	 * IO. The sorting is also important for the implementation of balancing
@@ -3768,6 +3785,21 @@ BufferSync(int flags)
 		buf_id = CkptBufferIds[ts_stat->index].buf_id;
 		Assert(buf_id != -1);
 
+		/*
+		 * TODO: We need to test the scenario when the buffer pool is shrunk
+		 * after checkpointer has collected the buffer ids and one or more of
+		 * the buffer ids is out of range.
+		 */
+
+		/*
+		 * The buffer pool might have been shrunk between the time the
+		 * checkpoint collected the buffer ids and now. Ignore any buffers
+		 * that are out of range now. Those buffers must have been written
+		 * when they were evicted during resizing.
+		 */
+		if (buf_id >= NBuffers)
+			continue;
+
 		bufHdr = GetBufferDescriptor(buf_id);
 
 		num_processed++;
@@ -3843,6 +3875,13 @@ BufferSync(int flags)
 /*
  * BgBufferSync -- Write out some dirty buffers in the pool.
  *
+ * Usually new buffers are allocated from the whole buffer pool. However, when
+ * resizing the buffer pool, the new buffer allocations are restricted to some
+ * initial portion of the buffer pool. The rest of pool is either being evicted
+ * when shrinking or not utilized yet when growing, hence not interesting to the
+ * background writer. Hence the background writer always restricts its activity
+ * to the same portion of the buffer pool as new buffer allocations.
+ *
  * This is called periodically by the background writer process.
  *
  * Returns true if it's appropriate for the bgwriter process to go into
@@ -3894,6 +3933,7 @@ BgBufferSync(WritebackContext *wb_context)
 	/* Variables for final smoothed_density update */
 	long		new_strategy_delta;
 	uint32		new_recent_alloc;
+	static int	prev_activeNBuffers;
 
 	Assert(AmBackgroundWriterProcess());
 
@@ -3903,6 +3943,24 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
+	if (prev_activeNBuffers != activeNBuffers)
+	{
+		/*
+		 * Previous clock sweep position does not make sense if the size of
+		 * the active buffer pool has changed.
+		 *
+		 * TODO: actually we have added a fix in
+		 * StrategyAdjustNewBufAllocSize() to adjust the complete_passes so
+		 * that we don't have to reset the saved info here, but still the
+		 * Assert(StrategyDelta >= 0) below fails without resetting the saved
+		 * info. Resetting the saved info means we loose the current position
+		 * of the bgwriter and thus may have to unnecessarily scan already
+		 * scanned buffers and the new allocations may have to find victim
+		 * themselves. Needs further investigation.
+		 */
+		saved_info_valid = false;
+	}
+
 	/* Report buffer alloc counts to pgstat */
 	PendingBgWriterStats.buf_alloc += recent_alloc;
 
@@ -3930,7 +3988,7 @@ BgBufferSync(WritebackContext *wb_context)
 		int32		passes_delta = strategy_passes - prev_strategy_passes;
 
 		strategy_delta = strategy_buf_id - prev_strategy_buf_id;
-		strategy_delta += (long) passes_delta * NBuffers;
+		strategy_delta += (long) passes_delta * activeNBuffers;
 
 		if ((int32) (next_passes - strategy_passes) > 0)
 		{
@@ -3947,7 +4005,7 @@ BgBufferSync(WritebackContext *wb_context)
 				 next_to_clean >= strategy_buf_id)
 		{
 			/* on same pass, but ahead or at least not behind */
-			bufs_to_lap = NBuffers - (next_to_clean - strategy_buf_id);
+			bufs_to_lap = activeNBuffers - (next_to_clean - strategy_buf_id);
 #ifdef BGW_DEBUG
 			elog(DEBUG2, "bgwriter ahead: bgw %u-%u strategy %u-%u delta=%ld lap=%d",
 				 next_passes, next_to_clean,
@@ -3969,7 +4027,7 @@ BgBufferSync(WritebackContext *wb_context)
 #endif
 			next_to_clean = strategy_buf_id;
 			next_passes = strategy_passes;
-			bufs_to_lap = NBuffers;
+			bufs_to_lap = activeNBuffers;
 		}
 
 		/*
@@ -3996,12 +4054,13 @@ BgBufferSync(WritebackContext *wb_context)
 		strategy_delta = 0;
 		next_to_clean = strategy_buf_id;
 		next_passes = strategy_passes;
-		bufs_to_lap = NBuffers;
+		bufs_to_lap = activeNBuffers;
 	}
 
 	/* Update saved info for next time */
 	prev_strategy_buf_id = strategy_buf_id;
 	prev_strategy_passes = strategy_passes;
+	prev_activeNBuffers = activeNBuffers;
 	saved_info_valid = true;
 
 	/*
@@ -4022,7 +4081,7 @@ BgBufferSync(WritebackContext *wb_context)
 	 * strategy point and where we've scanned ahead to, based on the smoothed
 	 * density estimate.
 	 */
-	bufs_ahead = NBuffers - bufs_to_lap;
+	bufs_ahead = activeNBuffers - bufs_to_lap;
 	reusable_buffers_est = (float) bufs_ahead / smoothed_density;
 
 	/*
@@ -4060,7 +4119,7 @@ BgBufferSync(WritebackContext *wb_context)
 	 * the BGW will be called during the scan_whole_pool time; slice the
 	 * buffer pool into that many sections.
 	 */
-	min_scan_buffers = (int) (NBuffers / (scan_whole_pool_milliseconds / BgWriterDelay));
+	min_scan_buffers = (int) (activeNBuffers / (scan_whole_pool_milliseconds / BgWriterDelay));
 
 	if (upcoming_alloc_est < (min_scan_buffers + reusable_buffers_est))
 	{
@@ -4082,13 +4141,24 @@ BgBufferSync(WritebackContext *wb_context)
 	num_written = 0;
 	reusable_buffers = reusable_buffers_est;
 
-	/* Execute the LRU scan */
+	/*
+	 * Execute the LRU scan on the part of the buffer pool from where the new
+	 * allocations will happen.
+	 *
+	 * Note that activeNBuffers may change during this loop if buffer pool
+	 * gets resized concurrently. This may invalidate the num_to_scan count.
+	 * If the buffer pool was shrunk this would make background writing more
+	 * aggressive, which might be desired since the buffer pool is smaller. If
+	 * the buffer pool was grown this would make the background writer not
+	 * scan the newly added buffers, which should not have any dirty buffers
+	 * yet.
+	 */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
 		int			sync_state = SyncOneBuffer(next_to_clean, true,
 											   wb_context);
 
-		if (++next_to_clean >= NBuffers)
+		if (++next_to_clean >= activeNBuffers)
 		{
 			next_to_clean = 0;
 			next_passes++;
@@ -8053,8 +8123,6 @@ EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed,
 		uint64		buf_state;
 		bool		buffer_flushed;
 
-		CHECK_FOR_INTERRUPTS();
-
 		buf_state = pg_atomic_read_u64(&desc->state);
 		if (!(buf_state & BM_VALID))
 			continue;
@@ -8071,6 +8139,13 @@ EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed,
 
 		if (buffer_flushed)
 			(*buffers_flushed)++;
+
+		/*
+		 * Checking interrupt before we are done with the current buffer might
+		 * invalidate the buffer itself if a concurrent resizing shrinks
+		 * buffer pool below the current buffer id.
+		 */
+		CHECK_FOR_INTERRUPTS();
 	}
 }
 
@@ -8105,8 +8180,6 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 		uint64		buf_state = pg_atomic_read_u64(&(desc->state));
 		bool		buffer_flushed;
 
-		CHECK_FOR_INTERRUPTS();
-
 		/* An unlocked precheck should be safe and saves some cycles. */
 		if ((buf_state & BM_VALID) == 0 ||
 			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
@@ -8133,6 +8206,13 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 
 		if (buffer_flushed)
 			(*buffers_flushed)++;
+
+		/*
+		 * Checking interrupt before we are done with the current buffer might
+		 * invalidate the buffer itself if a concurrent resizing shrinks
+		 * buffer pool below the current buffer id.
+		 */
+		CHECK_FOR_INTERRUPTS();
 	}
 }
 
@@ -8250,8 +8330,6 @@ MarkDirtyRelUnpinnedBuffers(Relation rel,
 		uint64		buf_state = pg_atomic_read_u64(&(desc->state));
 		bool		buffer_already_dirty;
 
-		CHECK_FOR_INTERRUPTS();
-
 		/* An unlocked precheck should be safe and saves some cycles. */
 		if ((buf_state & BM_VALID) == 0 ||
 			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
@@ -8277,6 +8355,13 @@ MarkDirtyRelUnpinnedBuffers(Relation rel,
 			(*buffers_already_dirty)++;
 		else
 			(*buffers_skipped)++;
+
+		/*
+		 * Checking interrupt before we are done with the current buffer might
+		 * invalidate the buffer itself if a concurrent resizing shrinks
+		 * buffer pool below the current buffer id.
+		 */
+		CHECK_FOR_INTERRUPTS();
 	}
 }
 
@@ -8304,8 +8389,6 @@ MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
 		uint64		buf_state;
 		bool		buffer_already_dirty;
 
-		CHECK_FOR_INTERRUPTS();
-
 		buf_state = pg_atomic_read_u64(&desc->state);
 		if (!(buf_state & BM_VALID))
 			continue;
@@ -8321,6 +8404,13 @@ MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
 			(*buffers_already_dirty)++;
 		else
 			(*buffers_skipped)++;
+
+		/*
+		 * Checking interrupt before we are done with the current buffer might
+		 * invalidate the buffer itself if a concurrent resizing shrinks
+		 * buffer pool below the current buffer id.
+		 */
+		CHECK_FOR_INTERRUPTS();
 	}
 }
 
@@ -9017,3 +9107,57 @@ const PgAioHandleCallbacks aio_local_buffer_readv_cb = {
 	.complete_local = local_buffer_readv_complete,
 	.report = buffer_readv_report,
 };
+
+/*
+ * When shrinking shared buffers pool, evict the buffers which will not be part
+ * of the shrunk buffer pool.
+ *
+ * If this function encounters a pinned buffer towards the end of the buffer
+ * pool, we would have evicted most of the buffers and yet rollback the resize
+ * operation. If we could find pinned buffers before evicting any, we could save
+ * wasted work and avoid performance impact because of evicted buffers. But
+ * there is no guarantee that a buffer won't be pinned after we check it. So we
+ * have to check for pinned buffers while evicting and rollback if we encounter
+ * any.
+ */
+bool
+EvictExtraBuffers(int targetNBuffers, int currentNBuffers)
+{
+	bool		result = true;
+
+	for (Buffer buf = targetNBuffers + 1; buf <= currentNBuffers; buf++)
+	{
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint64		buf_state;
+		bool		buffer_flushed;
+
+		buf_state = pg_atomic_read_u64(&desc->state);
+
+		/*
+		 * Nobody is expected to allocate new buffers while resizing is going
+		 * on hence unlocked precheck should be safe and saves some cycles.
+		 */
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		/*
+		 * Now that we have locked buffer descriptor, make sure that the
+		 * buffer without valid data has been skipped above.
+		 */
+		Assert(buf_state & BM_VALID);
+
+		if (!EvictUnpinnedBufferInternal(desc, &buffer_flushed))
+		{
+			elog(WARNING, "could not remove buffer %u, it is pinned", buf);
+			result = false;
+			break;
+		}
+	}
+
+	return result;
+}
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 4d5ee52ddc0..e397c5ce47a 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -37,8 +37,9 @@ typedef struct
 	/*
 	 * clock-sweep hand: index of next buffer to consider grabbing. Note that
 	 * this isn't a concrete buffer - we only ever increase the value. So, to
-	 * get an actual buffer, it needs to be used modulo size of the buffer
-	 * pool.
+	 * get an actual buffer, it needs to be used modulo the size of the buffer
+	 *
+	 * allocation area.
 	 */
 	pg_atomic_uint32 nextVictimBuffer;
 
@@ -101,11 +102,52 @@ static BufferDesc *GetBufferFromRing(BufferAccessStrategy strategy,
 static void AddBufferToRing(BufferAccessStrategy strategy,
 							BufferDesc *buf);
 
+/*
+ * StrategyWrapAround - Wrap around the clock-sweep hand.
+ *
+ * `new_pos` is the new_pos position of the clock hand after wrap around.
+ * `num_passes` is the number of completed passes when wrapping around.
+ */
+static void
+StrategyWrapAround(uint32 new_pos, uint32 num_passes)
+{
+	bool		success = false;
+
+	while (!success)
+	{
+		uint32		wrapped;
+
+		/*
+		 * Acquire the spinlock while increasing completePasses. That allows
+		 * other readers to read nextVictimBuffer and completePasses in a
+		 * consistent manner which is required for StrategySyncStart().  In
+		 * theory delaying the increment could lead to an overflow of
+		 * nextVictimBuffers, but that's highly unlikely and wouldn't be
+		 * particularly harmful.
+		 */
+		SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
+
+		wrapped = new_pos % activeNBuffers;
+
+		success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
+												 &new_pos, wrapped);
+		if (success)
+			StrategyControl->completePasses += num_passes;
+		SpinLockRelease(&StrategyControl->buffer_strategy_lock);
+	}
+}
+
 /*
  * ClockSweepTick - Helper routine for StrategyGetBuffer()
  *
- * Move the clock hand one buffer ahead of its current position and return the
- * id of the buffer now under the hand.
+ * Move the clock hand one buffer ahead of its current position and return the id
+ * of the buffer now under the hand.
+ *
+ * We use the same value of activeNBuffers through out the function.  Hence, even
+ * if the multiple backends end up wrapping around nextVictimBuffer using
+ * different activeNBuffers, they end up increasing completePasses incrementally
+ * and consistent to the respective victims. Use the latest activeNBuffers so as
+ * to be as consistent with the other allocators as possible.
  */
 static inline uint32
 ClockSweepTick(void)
@@ -119,13 +161,14 @@ ClockSweepTick(void)
 	 */
 	victim =
 		pg_atomic_fetch_add_u32(&StrategyControl->nextVictimBuffer, 1);
+	activeNBuffers = pg_atomic_read_u32(&BufferControl->activeNBuffers);
 
-	if (victim >= NBuffers)
+	if (victim >= activeNBuffers)
 	{
 		uint32		originalVictim = victim;
 
 		/* always wrap what we look up in BufferDescriptors */
-		victim = victim % NBuffers;
+		victim = victim % activeNBuffers;
 
 		/*
 		 * If we're the one that just caused a wraparound, force
@@ -134,35 +177,9 @@ ClockSweepTick(void)
 		 * value consisting of nextVictimBuffer and completePasses.
 		 */
 		if (victim == 0)
-		{
-			uint32		expected;
-			uint32		wrapped;
-			bool		success = false;
-
-			expected = originalVictim + 1;
-
-			while (!success)
-			{
-				/*
-				 * Acquire the spinlock while increasing completePasses. That
-				 * allows other readers to read nextVictimBuffer and
-				 * completePasses in a consistent manner which is required for
-				 * StrategySyncStart().  In theory delaying the increment
-				 * could lead to an overflow of nextVictimBuffers, but that's
-				 * highly unlikely and wouldn't be particularly harmful.
-				 */
-				SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
-
-				wrapped = expected % NBuffers;
-
-				success = pg_atomic_compare_exchange_u32(&StrategyControl->nextVictimBuffer,
-														 &expected, wrapped);
-				if (success)
-					StrategyControl->completePasses++;
-				SpinLockRelease(&StrategyControl->buffer_strategy_lock);
-			}
-		}
+			StrategyWrapAround(originalVictim + 1, 1);
 	}
+
 	return victim;
 }
 
@@ -180,6 +197,11 @@ ClockSweepTick(void)
  *
  *	The buffer is pinned and marked as owned, using TrackNewBufferPin(),
  *	before returning.
+ *
+ *  We do not process a ProcSignalBarrier between choosing a victim and pinning
+ *  it, so the buffer will remain valid even if the buffer pool is shrunk.  For
+ *  better safety we may want to disable interrupt handling explicitly during
+ *  this time.
  */
 BufferDesc *
 StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_ring)
@@ -238,7 +260,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/* Use the "clock sweep" algorithm to find a free buffer */
-	trycounter = NBuffers;
+	trycounter = activeNBuffers;
 	for (;;)
 	{
 		uint64		old_buf_state;
@@ -291,7 +313,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint64 *buf_state, bool *from_r
 				if (pg_atomic_compare_exchange_u64(&buf->state, &old_buf_state,
 												   local_buf_state))
 				{
-					trycounter = NBuffers;
+					trycounter = activeNBuffers;
 					break;
 				}
 			}
@@ -334,9 +356,15 @@ StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc)
 	uint32		nextVictimBuffer;
 	int			result;
 
+	/*
+	 * Update backend local activeNBuffers for the same reason as in
+	 * StrategyGetBuffer.
+	 */
+	activeNBuffers = pg_atomic_read_u32(&BufferControl->activeNBuffers);
+
 	SpinLockAcquire(&StrategyControl->buffer_strategy_lock);
 	nextVictimBuffer = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
-	result = nextVictimBuffer % NBuffers;
+	result = nextVictimBuffer % activeNBuffers;
 
 	if (complete_passes)
 	{
@@ -346,7 +374,7 @@ StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc)
 		 * Additionally add the number of wraparounds that happened before
 		 * completePasses could be incremented. C.f. ClockSweepTick().
 		 */
-		*complete_passes += nextVictimBuffer / NBuffers;
+		*complete_passes += nextVictimBuffer / activeNBuffers;
 	}
 
 	if (num_buf_alloc)
@@ -392,6 +420,29 @@ StrategyCtlShmemRequest(void *arg)
 		);
 }
 
+/*
+ * StrategyAdjustNewBufAllocSize
+ *
+ * Adjust clock hand when resizing the buffer pool.
+ */
+void
+StrategyAdjustNewBufAllocSize(void)
+{
+	int			num_passes;
+	uint32		nextVictimBuffer;
+
+	/* Should be called only when resizing is in progress. */
+	Assert(pg_atomic_read_u32(&BufferControl->resizer_pid) != 0);
+
+	activeNBuffers = pg_atomic_read_u32(&BufferControl->activeNBuffers);
+
+	/* Consistently wrap around the clock sweep hand, if necessary. */
+	nextVictimBuffer = pg_atomic_read_u32(&StrategyControl->nextVictimBuffer);
+	num_passes = nextVictimBuffer / activeNBuffers;
+	if (num_passes > 0)
+		StrategyWrapAround(nextVictimBuffer, num_passes);
+}
+
 /*
  * StrategyCtlShmemInit -- initialize the buffer cache replacement strategy.
  */
@@ -634,12 +685,27 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint64 *buf_state)
 		strategy->current = 0;
 
 	/*
-	 * If the slot hasn't been filled yet, tell the caller to allocate a new
-	 * buffer with the normal allocation strategy.  He will then fill this
-	 * slot by calling AddBufferToRing with the new buffer.
+	 * If the slot hasn't been filled yet or the buffer in the slot is outside
+	 * the buffer allocation area tell the caller to allocate a new buffer
+	 * with the normal allocation strategy.  He will then fill this slot by
+	 * calling AddBufferToRing with the new buffer. Usually the buffers in the
+	 * ring will be within the buffer allocation area, but if the buffer pool
+	 * has been shrunk since the last time the ring was filled, some of the
+	 * buffers in the ring may be outside the new buffer allocation area.
+	 *
+	 * TODO: buffer ids in the ring will never be greater than the size of
+	 * buffer pool, except maybe the first time ring is accessed after
+	 * shrinking the buffer pool. Checking the upper bound on buffer id always
+	 * may mask a bug bugs that introduces buffer ids higher than the size of
+	 * buffer pool in the ring. But performing that check only once after
+	 * shrinking seems impossible.  The BufferAccessStrategy objects are not
+	 * accessible outside the ScanState. Hence we can not purge the buffers
+	 * while evicting the buffers.  After the resizing is finished, it's not
+	 * possible to notice when we touch the first of those objects and the
+	 * last of objects. See if this can fixed.
 	 */
 	bufnum = strategy->buffers[strategy->current];
-	if (bufnum == InvalidBuffer)
+	if (bufnum == InvalidBuffer || bufnum > activeNBuffers)
 		return NULL;
 
 	buf = GetBufferDescriptor(bufnum - 1);
diff --git a/src/backend/storage/buffer/meson.build b/src/backend/storage/buffer/meson.build
index ed84bf08971..f219e29d5ef 100644
--- a/src/backend/storage/buffer/meson.build
+++ b/src/backend/storage/buffer/meson.build
@@ -6,4 +6,5 @@ backend_sources += files(
   'bufmgr.c',
   'freelist.c',
   'localbuf.c',
+  'buf_resize.c',
 )
diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c
index 21a77f98c1d..f97215dae9c 100644
--- a/src/backend/storage/ipc/procsignal.c
+++ b/src/backend/storage/ipc/procsignal.c
@@ -28,6 +28,7 @@
 #include "replication/logicalworker.h"
 #include "replication/slotsync.h"
 #include "replication/walsender.h"
+#include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -598,6 +599,15 @@ ProcessProcSignalBarrier(void)
 					case PROCSIGNAL_BARRIER_CHECKSUM_OFF:
 						processed = AbsorbDataChecksumsBarrier(type);
 						break;
+					case PROCSIGNAL_BARRIER_NEW_BUFFER_ALLOC:
+						processed = ProcessBarrierNewBufferAlloc();
+						break;
+					case PROCSIGNAL_BARRIER_BUFFER_POOL_RESIZE:
+						processed = ProcessBarrierBufferPoolResize();
+						break;
+					case PROCSIGNAL_BARRIER_BUFFER_POOL_SIZE:
+						processed = ProcessBarrierBufferPoolSize();
+						break;
 				}
 
 				/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 780cdb5e9ff..8332bc0b252 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -43,6 +43,7 @@
 #include "postmaster/autovacuum.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/ipc.h"
 #include "storage/lmgr.h"
@@ -583,6 +584,21 @@ InitProcess(void)
 	 * the reasons mentioned there.
 	 */
 	ShmemReprotectResizableStructs();
+
+#ifndef EXEC_BACKEND
+
+	/*
+	 * Pick up the current buffer pool size from shared memory. Fork'ed
+	 * backends would otherwise inherit the postmaster's potentially stale
+	 * values. EXEC_BACKEND children do this via the buffer manager attach
+	 * callback above.
+	 *
+	 * We also call this function after ProcSignalInit() for the reasons
+	 * specified there, but we need it here so that InitBufferManagerAccess()
+	 * can use the current buffer pool size.
+	 */
+	BufferManagerInitProc();
+#endif
 }
 
 /*
@@ -772,6 +788,21 @@ InitAuxiliaryProcess(void)
 	 * the reasons mentioned there.
 	 */
 	ShmemReprotectResizableStructs();
+
+#ifndef EXEC_BACKEND
+
+	/*
+	 * Pick up the current buffer pool size from shared memory. Fork'ed
+	 * backends would otherwise inherit the postmaster's potentially stale
+	 * values. EXEC_BACKEND children do this via the buffer manager attach
+	 * callback above.
+	 *
+	 * We also call this function after ProcSignalInit() for the reasons
+	 * specifid there, but we need it here so that InitBufferManagerAccess()
+	 * can use the current buffer pool size.
+	 */
+	BufferManagerInitProc();
+#endif
 }
 
 /*
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index b6bdfe213fe..2586b7087ac 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -4288,6 +4288,9 @@ PostgresSingleUserMain(int argc, char *argv[],
 	/* Initialize size of fast-path lock cache. */
 	InitializeFastPathLocks();
 
+	/* Initialize MaxNBuffers for buffer pool resizing. */
+	InitializeMaxNBuffers();
+
 	/*
 	 * Also call any legacy shmem request hooks that might'be been installed
 	 * by preloaded libraries.
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index ccf845e87b9..4c5cb3c3908 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -142,6 +142,8 @@ int			max_parallel_maintenance_workers = 2;
  * register background workers.
  */
 int			NBuffersGUC = 16384;
+bool		finalMaxNBuffers = false;
+int			MaxNBuffers = 0;
 int			MaxConnections = 100;
 int			max_worker_processes = 8;
 int			max_parallel_workers = 8;
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 815b865aa38..c129d3a8a70 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -610,6 +610,55 @@ InitializeFastPathLocks(void)
 		   pg_nextpower2_32(FastPathLockGroupsPerBackend));
 }
 
+/*
+ * Initialize MaxNBuffers variable with validation.
+ *
+ * This must be called after GUCs have been loaded but before shared memory size
+ * is determined.
+ *
+ * Since MaxNBuffers limits the size of the buffer pool, it must be at least as
+ * much as NBuffersGUC. If MaxNBuffers is 0 (default), set it to
+ * NBuffersGUC. Otherwise, validate that MaxNBuffers is not less than
+ * NBuffersGUC.
+ */
+void
+InitializeMaxNBuffers(void)
+{
+	if (MaxNBuffers == 0)		/* default/boot value */
+	{
+		char		buf[32];
+
+		snprintf(buf, sizeof(buf), "%d", NBuffersGUC);
+		SetConfigOption("max_shared_buffers", buf, PGC_POSTMASTER,
+						PGC_S_DYNAMIC_DEFAULT);
+
+		/*
+		 * We prefer to report this value's source as PGC_S_DYNAMIC_DEFAULT.
+		 * However, if the DBA explicitly set max_shared_buffers = 0 in the
+		 * config file, then PGC_S_DYNAMIC_DEFAULT will fail to override that
+		 * and we must force the matter with PGC_S_OVERRIDE.
+		 */
+		if (MaxNBuffers == 0)	/* failed to apply it? */
+			SetConfigOption("max_shared_buffers", buf, PGC_POSTMASTER,
+							PGC_S_OVERRIDE);
+	}
+	else
+	{
+		if (MaxNBuffers < NBuffersGUC)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("max_shared_buffers (%d) cannot be less than current shared_buffers (%d)",
+							MaxNBuffers, NBuffersGUC),
+					 errhint("Increase max_shared_buffers or decrease shared_buffers.")));
+		}
+	}
+
+	Assert(MaxNBuffers > 0);
+	Assert(!finalMaxNBuffers);
+	finalMaxNBuffers = true;
+}
+
 /*
  * Early initialization of a backend (either standalone or under postmaster).
  * This happens even before InitPostgres.
@@ -760,32 +809,38 @@ InitPostgres(const char *in_dbname, Oid dboid,
 	SharedInvalBackendInit(false);
 
 	/*
-	 * Prevent consuming interrupts between setting ProcSignalInit and setting
-	 * the initial local data checksum value.  If a barrier is emitted, and
-	 * absorbed, before local cached state is initialized the state transition
-	 * can be invalid.
+	 * Prevent consuming interrupts between ProcSignalInit() and the
+	 * initialization of state that is kept in sync with shared memory via
+	 * procsignal-based barriers (currently the data_checksum_version cache
+	 * and the local NBuffers/activeNBuffers cache).  If a barrier is emitted,
+	 * and absorbed, before that local cached state is initialized the state
+	 * transition can be invalid.
 	 */
 	HOLD_INTERRUPTS();
 
 	ProcSignalInit(MyCancelKey, MyCancelKeyLength);
 
 	/*
-	 * Initialize a local cache of the data_checksum_version, to be updated by
-	 * the procsignal-based barriers.
+	 * Initialize the per-backend caches that are kept in sync via
+	 * procsignal-based barriers: currently the local data_checksum_version
+	 * and the local copy of the buffer pool size (NBuffers/activeNBuffers).
 	 *
-	 * This intentionally happens after initializing the procsignal, otherwise
-	 * we might miss a state change. This means we can get a barrier for the
-	 * state we've just initialized.
+	 * These initializations intentionally happen after ProcSignalInit(),
+	 * otherwise we might miss a state change.  This means we may also receive
+	 * a barrier for the state we've just initialized.
 	 *
 	 * The postmaster (which is what gets forked into the new child process)
 	 * does not handle barriers, therefore it may not have the current value
-	 * of LocalDataChecksumState value (it'll have the value read from the
-	 * control file, which may be arbitrarily old).
+	 * of LocalDataChecksumState (it'll have the value read from the control
+	 * file, which may be arbitrarily old) or NBuffers/activeNBuffers (which
+	 * may have been changed by an online resize after the postmaster
+	 * started).
 	 *
 	 * NB: Even if the postmaster handled barriers, the value might still be
 	 * stale, as it might have changed after this process forked.
 	 */
 	InitLocalDataChecksumState();
+	BufferManagerInitProc();
 
 	/*
 	 * Refresh per-backend protections for resizable shmem structures. Usually
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 1a5a168bc1a..5f1dd338f56 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2631,7 +2631,7 @@ convert_to_base_unit(double value, const char *unit,
  * the value without loss.  For example, if the base unit is GUC_UNIT_KB, 1024
  * is converted to 1 MB, but 1025 is represented as 1025 kB.
  */
-static void
+void
 convert_int_from_base_unit(int64 base_value, int base_unit,
 						   int64 *value, const char **unit)
 {
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 2cbec1981c5..d5099fa9659 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -2146,6 +2146,15 @@
   max => 'MAX_BACKENDS /* XXX? */',
 },
 
+{ name => "max_shared_buffers", type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM',
+  short_desc => 'Sets the upper limit for the shared_buffers value.',
+  flags => 'GUC_UNIT_BLOCKS',
+  variable => 'MaxNBuffers',
+  boot_val => '0',
+  min => '0',
+  max => 'INT_MAX / 2',
+},
+
 { name => 'max_slot_wal_keep_size', type => 'int', context => 'PGC_SIGHUP', group => 'REPLICATION_SENDING',
   short_desc => 'Sets the maximum WAL size that can be reserved by replication slots.',
   long_desc => 'Replication slots will be marked as failed, and segments released for deletion or recycling, if this much space is occupied by WAL on disk. -1 means no maximum.',
@@ -2720,13 +2729,15 @@
 
 # We sometimes multiply the number of shared buffers by two without
 # checking for overflow, so we mustn't allow more than INT_MAX / 2.
-{ name => 'shared_buffers', type => 'int', context => 'PGC_POSTMASTER', group => 'RESOURCES_MEM',
+{ name => 'shared_buffers', type => 'int', context => 'PGC_SIGHUP', group => 'RESOURCES_MEM',
   short_desc => 'Sets the number of shared memory buffers used by the server.',
   flags => 'GUC_UNIT_BLOCKS',
   variable => 'NBuffersGUC',
   boot_val => '16384',
-  min => '16',
+  min => 'MIN_NUM_BUFFERS',
   max => 'INT_MAX / 2',
+  check_hook => 'check_shared_buffers',
+  show_hook => 'show_shared_buffers',
 },
 
 { name => 'shared_memory_initial_size', type => 'int', context => 'PGC_INTERNAL', group => 'PRESET_OPTIONS',
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 712172760b3..0487576ffd7 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12715,4 +12715,19 @@
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
 
+{ oid => '9999', descr => 'resize shared buffers according to the value of GUC `shared_buffers`',
+  proname => 'pg_resize_shared_buffers',
+  provolatile => 'v',
+  prorettype => 'bool',
+  proargtypes => '',
+  prosrc => 'pg_resize_shared_buffers'},
+{ oid => '9998', descr => 'report shared buffer pool resize status',
+  proname => 'pg_get_buffer_resize_status',
+  provolatile => 'v',
+  prorettype => 'record',
+  proargtypes => '',
+  proallargtypes => '{int4,int4,int4,int4}',
+  proargmodes => '{o,o,o,o}',
+  proargnames => '{active_nbuffers,current_nbuffers,target_nbuffers,resizer_pid}',
+  prosrc => 'pg_get_buffer_resize_status'},
 ]
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index b9449e9aced..04fb70086ad 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -176,6 +176,8 @@ extern PGDLLIMPORT char *DataDir;
 extern PGDLLIMPORT int data_directory_mode;
 
 extern PGDLLIMPORT int NBuffersGUC;
+extern PGDLLIMPORT bool finalMaxNBuffers;
+extern PGDLLIMPORT int MaxNBuffers;
 extern PGDLLIMPORT int MaxBackends;
 extern PGDLLIMPORT int MaxConnections;
 extern PGDLLIMPORT int max_worker_processes;
@@ -511,6 +513,7 @@ extern PGDLLIMPORT ProcessingMode Mode;
 extern void pg_split_opts(char **argv, int *argcp, const char *optstr);
 extern void InitializeMaxBackends(void);
 extern void InitializeFastPathLocks(void);
+extern void InitializeMaxNBuffers(void);
 extern void InitPostgres(const char *in_dbname, Oid dboid,
 						 const char *username, Oid useroid,
 						 uint32 flags,
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 678065b3de2..a4971cebde4 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -264,6 +264,33 @@ BufMappingPartitionLockByIndex(uint32 index)
 	return &MainLWLockArray[BUFFER_MAPPING_LWLOCK_OFFSET + index].lock;
 }
 
+/*
+ *	BufferControl -- shared area controlling buffer pool
+ *
+ * This structure stores information about the size of the buffer pool and
+ * whether it is being resized.
+ */
+typedef struct BufferControlBlock
+{
+	/*
+	 * size of the part of the buffer pool from where buffers are being
+	 * allocated to new requests.
+	 */
+	pg_atomic_uint32 activeNBuffers;
+
+	/* current size of the buffer pool, in number of buffers */
+	pg_atomic_uint32 currentNBuffers;
+
+	/* target size of the buffer pool, in number of buffers */
+	pg_atomic_uint32 targetNBuffers;
+
+	/*
+	 * PID of the backend currently performing a resize, or 0 when no resize
+	 * is in progress. Also acts as a lock prohibiting concurrent resizes.
+	 */
+	pg_atomic_uint32 resizer_pid;
+} BufferControlBlock;
+
 /*
  *	BufferDesc -- shared descriptor/state data for a single shared buffer.
  *
@@ -411,6 +438,7 @@ typedef struct WritebackContext
 } WritebackContext;
 
 /* in buf_init.c */
+extern PGDLLIMPORT BufferControlBlock *BufferControl;
 extern PGDLLIMPORT BufferDescPadded *BufferDescriptors;
 extern PGDLLIMPORT ConditionVariableMinimallyPadded *BufferIOCVArray;
 extern PGDLLIMPORT WritebackContext BackendWritebackContext;
@@ -422,9 +450,26 @@ extern PGDLLIMPORT BufferDesc *LocalBufferDescriptors;
 static inline BufferDesc *
 GetBufferDescriptor(int id)
 {
+	BufferDesc *bdesc;
+
 	Assert(id >= 0 && id < NBuffers);
 
-	return &(BufferDescriptors[id]).bufferdesc;
+	bdesc = &(BufferDescriptors[id]).bufferdesc;
+
+	/*
+	 * TODO: This assertion was proposed in
+	 * https://www.postgresql.org/message-id/CAExHW5uzRMYVZsXXS3HXXT0fG_sNrpUhUqwP4NorhaCqH9JDhA@mail.gmail.com,
+	 * but was ultimately removed since there was no adequate reason to keep
+	 * it in the code without shared buffer resizing. With resizing we may
+	 * write and rewrite parts of the buffer descriptor array. So it's better
+	 * to make sure that the buffer descriptor is initialized correctly. For
+	 * now just make sure that the id in the buffer descriptor is the same as
+	 * the id used to access it. Later we may want to expand the assertion to
+	 * check the BufferDesc invariants or remove this assertion.
+	 */
+	Assert(bdesc->buf_id == id);
+
+	return bdesc;
 }
 
 static inline BufferDesc *
@@ -594,6 +639,7 @@ extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
+extern void StrategyAdjustNewBufAllocSize(void);
 
 /* buf_table.c */
 extern uint32 BufTableHashCode(BufferTag *tagPtr);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f1f6e601f51..187e9b29a9e 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -14,6 +14,7 @@
 #ifndef BUFMGR_H
 #define BUFMGR_H
 
+#include "fmgr.h"
 #include "port/pg_iovec.h"
 #include "storage/aio_types.h"
 #include "storage/block.h"
@@ -159,6 +160,7 @@ typedef struct ReadBuffersOperation ReadBuffersOperation;
 typedef struct WritebackContext WritebackContext;
 
 /* in globals.c ... this duplicates miscadmin.h */
+#define MIN_NUM_BUFFERS 16
 extern PGDLLIMPORT int NBuffersGUC;
 
 /* in bufmgr.c */
@@ -167,6 +169,7 @@ extern PGDLLIMPORT int bgwriter_lru_maxpages;
 extern PGDLLIMPORT double bgwriter_lru_multiplier;
 extern PGDLLIMPORT bool track_io_timing;
 extern PGDLLIMPORT int NBuffers;
+extern PGDLLIMPORT int activeNBuffers;
 
 #define DEFAULT_EFFECTIVE_IO_CONCURRENCY 16
 #define DEFAULT_MAINTENANCE_IO_CONCURRENCY 16
@@ -371,6 +374,12 @@ extern void MarkDirtyRelUnpinnedBuffers(Relation rel,
 extern void MarkDirtyAllUnpinnedBuffers(int32 *buffers_dirtied,
 										int32 *buffers_already_dirty,
 										int32 *buffers_skipped);
+extern bool EvictExtraBuffers(int targetNBuffers, int currentNBuffers);
+
+/* in buf_init.c */
+extern bool BufferManagerShmemResize(int currentNBuffers, int targetNBuffers);
+extern void BufferManagerShmemProtect(void);
+extern void BufferManagerInitProc(void);
 
 /* in localbuf.c */
 extern void AtProcExit_LocalBuffers(void);
@@ -473,4 +482,10 @@ BufferGetPage(Buffer buffer)
 
 #endif							/* FRONTEND */
 
+/* buf_resize.c */
+extern Datum pg_resize_shared_buffers(PG_FUNCTION_ARGS);
+extern bool ProcessBarrierNewBufferAlloc(void);
+extern bool ProcessBarrierBufferPoolResize(void);
+extern bool ProcessBarrierBufferPoolSize(void);
+
 #endif							/* BUFMGR_H */
diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h
index aaa158bfd66..78dda9c8d21 100644
--- a/src/include/storage/procsignal.h
+++ b/src/include/storage/procsignal.h
@@ -54,6 +54,11 @@ typedef enum
 	PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_ON,
 	PROCSIGNAL_BARRIER_CHECKSUM_INPROGRESS_OFF,
 	PROCSIGNAL_BARRIER_CHECKSUM_ON,
+	PROCSIGNAL_BARRIER_NEW_BUFFER_ALLOC,	/* New buffer allocation pool size
+											 * changed */
+	PROCSIGNAL_BARRIER_BUFFER_POOL_RESIZE,	/* Buffer pool shared structures
+											 * resized */
+	PROCSIGNAL_BARRIER_BUFFER_POOL_SIZE,	/* Buffer pool size updated */
 } ProcSignalBarrierType;
 
 /*
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 2a6e2ed18b3..2e3dc919018 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -462,6 +462,8 @@ extern config_handle *get_config_handle(const char *name);
 extern void AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt);
 extern char *GetConfigOptionByName(const char *name, const char **varname,
 								   bool missing_ok);
+extern void convert_int_from_base_unit(int64 base_value, int base_unit,
+									   int64 *value, const char **unit);
 
 extern void TransformGUCArray(ArrayType *array, List **names,
 							  List **values);
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index df048517a0f..7e549a36530 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -181,4 +181,6 @@ extern void assign_synchronized_standby_slots(const char *newval, void *extra);
 extern bool check_log_min_messages(char **newval, void **extra, GucSource source);
 extern void assign_log_min_messages(const char *newval, void *extra);
 
+extern const char *show_shared_buffers(bool use_units);
+extern bool check_shared_buffers(int *newval, void **extra, GucSource source);
 #endif							/* GUC_HOOKS_H */
diff --git a/src/test/Makefile b/src/test/Makefile
index 3eb0a06abb4..7a0d74086c1 100644
--- a/src/test/Makefile
+++ b/src/test/Makefile
@@ -20,7 +20,8 @@ SUBDIRS = \
 	postmaster \
 	recovery \
 	regress \
-	subscription
+	subscription \
+	buffermgr
 
 ifeq ($(with_icu),yes)
 SUBDIRS += icu
diff --git a/src/test/README b/src/test/README
index afdc7676519..77f11607ff7 100644
--- a/src/test/README
+++ b/src/test/README
@@ -15,6 +15,9 @@ examples/
   Demonstration programs for libpq that double as regression tests via
   "make check"
 
+buffermgr/
+  Tests for resizing buffer pool without restarting the server
+
 isolation/
   Tests for concurrent behavior at the SQL level
 
diff --git a/src/test/buffermgr/Makefile b/src/test/buffermgr/Makefile
new file mode 100644
index 00000000000..24c245c900a
--- /dev/null
+++ b/src/test/buffermgr/Makefile
@@ -0,0 +1,35 @@
+#-------------------------------------------------------------------------
+#
+# Makefile for src/test/buffermgr
+#
+# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+# Portions Copyright (c) 1994, Regents of the University of California
+#
+# src/test/buffermgr/Makefile
+#
+#-------------------------------------------------------------------------
+
+EXTRA_INSTALL = contrib/pg_buffercache \
+				src/test/modules/injection_points \
+				src/test/modules/test_shmem
+
+REGRESS = buffer_resize
+
+# Custom configuration for buffer manager tests
+TEMP_CONFIG = $(srcdir)/buffermgr_test.conf
+
+export enable_injection_points
+
+subdir = src/test/buffermgr
+top_builddir = ../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+
+check:
+	$(prove_check)
+
+installcheck:
+	$(prove_installcheck)
+
+clean distclean:
+	rm -rf tmp_check
diff --git a/src/test/buffermgr/README b/src/test/buffermgr/README
new file mode 100644
index 00000000000..c375ad80989
--- /dev/null
+++ b/src/test/buffermgr/README
@@ -0,0 +1,26 @@
+src/test/buffermgr/README
+
+Regression tests for buffer manager
+===================================
+
+This directory contains a test suite for resizing buffer manager without restarting the server.
+
+
+Running the tests
+=================
+
+NOTE: You must have given the --enable-tap-tests argument to configure.
+
+Run
+    make check
+or
+    make installcheck
+You can use "make installcheck" if you previously did "make install".
+In that case, the code in the installation tree is tested.  With
+"make check", a temporary installation tree is built from the current
+sources and then tested.
+
+Either way, this test initializes, starts, and stops a test Postgres
+cluster.
+
+See src/test/perl/README for more info about running these tests.
diff --git a/src/test/buffermgr/buffermgr_test.conf b/src/test/buffermgr/buffermgr_test.conf
new file mode 100644
index 00000000000..a15f3e442a5
--- /dev/null
+++ b/src/test/buffermgr/buffermgr_test.conf
@@ -0,0 +1,11 @@
+# Configuration for buffer manager regression tests
+
+# Even if max_shared_buffers is set multiple times only the last one is used to
+# as the limit on shared_buffers.
+max_shared_buffers = 128kB
+# Set initial shared_buffers as expected by test
+shared_buffers = 128MB
+# Set a larger value for max_shared_buffers to allow testing resize operations
+max_shared_buffers = 300MB
+# Turn huge pages off, since that affects the size of memory segments
+huge_pages = off
diff --git a/src/test/buffermgr/expected/buffer_resize.out b/src/test/buffermgr/expected/buffer_resize.out
new file mode 100644
index 00000000000..64430b4a3ae
--- /dev/null
+++ b/src/test/buffermgr/expected/buffer_resize.out
@@ -0,0 +1,290 @@
+-- Test buffer pool resizing and shared memory allocation tracking This test
+-- resizes the buffer pool multiple times and monitors shared memory allocations
+-- related to buffer management
+-- TODOs
+-- 
+-- 1. The test sets shared_buffers values in MBs. Instead it could use values in
+-- kBs so that the test runs on very small machines.
+-- 
+-- 2. The size, minimum_size and maximum_size columns in pg_shmem_allocations
+-- for "Buffer Blocks" should be same as the value of GUC shared_buffers. We
+-- should test that.
+-- 
+-- 3. We should make sure that when the shared_buffers value is increased, the
+-- size and allocated_size for all buffer related shared memory allocations
+-- increases and when the shared_buffers value is decreased, the size and
+-- allocated_size for all buffer related shared memory allocations decreases
+-- proportionately.
+-- 
+-- 4. allocated_size for allocations should be greater than or equal to size for
+-- all buffer related shared memory allocations. Similarly reserved_space should
+-- be greater than or equal to maximum_size for all buffer related shared memory
+-- allocations. We should test these conditions as well.
+CREATE EXTENSION IF NOT EXISTS pg_buffercache;
+-- Load test_shmem for test_shmem_pagesize().
+CREATE EXTENSION IF NOT EXISTS test_shmem;
+-- Create a view for buffer-related shared memory allocations
+CREATE VIEW buffer_allocations AS
+SELECT name, size,
+       allocated_size >= size AS alloc_size_cmp,
+       allocated_size - size < 2 * test_shmem_pagesize() AS alloc_size_diff,
+       minimum_size, maximum_size, reserved_space
+FROM pg_shmem_allocations
+WHERE name IN ('Buffer Blocks', 'Buffer Descriptors', 'Buffer IO Condition Variables',
+               'Checkpoint BufferIds')
+ORDER BY name;
+-- Test 1: Default shared_buffers
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128MB
+(1 row)
+
+SHOW max_shared_buffers;
+ max_shared_buffers 
+--------------------
+ 300MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |   size    | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+-----------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 134217728 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |   1048576 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |    262144 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          |    768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        16384
+(1 row)
+
+-- Calling pg_resize_shared_buffers() without changing shared_buffers should be a no-op.
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |   size    | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+-----------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 134217728 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |   1048576 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |    262144 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          |    768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        16384
+(1 row)
+
+-- Test 2: Set to 64MB
+ALTER SYSTEM SET shared_buffers = '64MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+    shared_buffers     
+-----------------------
+ 128MB (pending: 64MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 64MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |   size   | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+----------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 67108864 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |   524288 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |   131072 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          |   768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+         8192
+(1 row)
+
+-- Test 3: Set to 256MB
+ALTER SYSTEM SET shared_buffers = '256MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+    shared_buffers     
+-----------------------
+ 64MB (pending: 256MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 256MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |   size    | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+-----------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 268435456 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |   2097152 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |    524288 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          |    768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        32768
+(1 row)
+
+-- Test 4: Set to 100MB (non-power-of-two)
+ALTER SYSTEM SET shared_buffers = '100MB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+     shared_buffers     
+------------------------
+ 256MB (pending: 100MB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 100MB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |   size    | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+-----------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 104857600 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |    819200 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |    204800 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          |    768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+        12800
+(1 row)
+
+-- Test 5: Set to minimum 128kB
+ALTER SYSTEM SET shared_buffers = '128kB';
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+     shared_buffers     
+------------------------
+ 100MB (pending: 128kB)
+(1 row)
+
+SELECT pg_resize_shared_buffers();
+ pg_resize_shared_buffers 
+--------------------------
+ t
+(1 row)
+
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128kB
+(1 row)
+
+SELECT * FROM buffer_allocations;
+             name              |  size  | alloc_size_cmp | alloc_size_diff | minimum_size | maximum_size | reserved_space 
+-------------------------------+--------+----------------+-----------------+--------------+--------------+----------------
+ Buffer Blocks                 | 131072 | t              | t               |       131072 |    314572800 |      314574336
+ Buffer Descriptors            |   1024 | t              | t               |         1024 |      2457600 |        2457600
+ Buffer IO Condition Variables |    256 | t              | t               |          256 |       614400 |         614400
+ Checkpoint BufferIds          | 768000 | t              | t               |       768000 |       768000 |         768120
+(4 rows)
+
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+ buffer_count 
+--------------
+           16
+(1 row)
+
+-- Test 6: Try to set shared_buffers higher than max_shared_buffers (should fail)
+ALTER SYSTEM SET shared_buffers = '400MB';
+ERROR:  invalid value for parameter "shared_buffers": 51200
+DETAIL:  "shared_buffers" must be less than "max_shared_buffers".
+SELECT pg_reload_conf();
+ pg_reload_conf 
+----------------
+ t
+(1 row)
+
+-- reconnect to ensure new setting is loaded
+\c
+-- This should show the old value since the configuration was rejected
+SHOW shared_buffers;
+ shared_buffers 
+----------------
+ 128kB
+(1 row)
+
+SHOW max_shared_buffers;
+ max_shared_buffers 
+--------------------
+ 300MB
+(1 row)
+
+-- TODO: Test that a non-superuser can not invoke pg_resize_shared_buffers()
+-- function. 
diff --git a/src/test/buffermgr/meson.build b/src/test/buffermgr/meson.build
new file mode 100644
index 00000000000..7a6d5e29f8d
--- /dev/null
+++ b/src/test/buffermgr/meson.build
@@ -0,0 +1,25 @@
+# Copyright (c) 2022-2025, PostgreSQL Global Development Group
+
+tests += {
+  'name': 'buffermgr',
+  'sd': meson.current_source_dir(),
+  'bd': meson.current_build_dir(),
+  'regress': {
+    'sql': [
+      'buffer_resize',
+    ],
+    'regress_args': ['--temp-config', files('buffermgr_test.conf')],
+  },
+  'tap': {
+    'env': {
+      'enable_injection_points': get_option('injection_points') ? 'yes' : 'no',
+    },
+    'tests': [
+      't/001_resize_buffer.pl',
+      't/003_resize_fault_tolerance.pl',
+      't/004_client_join_buffer_resize.pl',
+      't/005_resize_failures.pl',
+      't/006_resize_with_syslogger.pl',
+    ],
+  },
+}
diff --git a/src/test/buffermgr/sql/buffer_resize.sql b/src/test/buffermgr/sql/buffer_resize.sql
new file mode 100644
index 00000000000..4219e33a805
--- /dev/null
+++ b/src/test/buffermgr/sql/buffer_resize.sql
@@ -0,0 +1,106 @@
+-- Test buffer pool resizing and shared memory allocation tracking This test
+-- resizes the buffer pool multiple times and monitors shared memory allocations
+-- related to buffer management
+
+-- TODOs
+--
+-- 1. The test sets shared_buffers values in MBs. Instead it could use values in
+-- kBs so that the test runs on very small machines.
+--
+-- 2. The size, minimum_size and maximum_size columns in pg_shmem_allocations
+-- for "Buffer Blocks" should be same as the value of GUC shared_buffers. We
+-- should test that.
+--
+-- 3. We should make sure that when the shared_buffers value is increased, the
+-- size and allocated_size for all buffer related shared memory allocations
+-- increases and when the shared_buffers value is decreased, the size and
+-- allocated_size for all buffer related shared memory allocations decreases
+-- proportionately.
+--
+-- 4. allocated_size for allocations should be greater than or equal to size for
+-- all buffer related shared memory allocations. Similarly reserved_space should
+-- be greater than or equal to maximum_size for all buffer related shared memory
+-- allocations. We should test these conditions as well.
+
+CREATE EXTENSION IF NOT EXISTS pg_buffercache;
+
+-- Load test_shmem for test_shmem_pagesize().
+CREATE EXTENSION IF NOT EXISTS test_shmem;
+
+-- Create a view for buffer-related shared memory allocations
+CREATE VIEW buffer_allocations AS
+SELECT name, size,
+       allocated_size >= size AS alloc_size_cmp,
+       allocated_size - size < 2 * test_shmem_pagesize() AS alloc_size_diff,
+       minimum_size, maximum_size, reserved_space
+FROM pg_shmem_allocations
+WHERE name IN ('Buffer Blocks', 'Buffer Descriptors', 'Buffer IO Condition Variables',
+               'Checkpoint BufferIds')
+ORDER BY name;
+
+-- Test 1: Default shared_buffers
+SHOW shared_buffers;
+SHOW max_shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+-- Calling pg_resize_shared_buffers() without changing shared_buffers should be a no-op.
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 2: Set to 64MB
+ALTER SYSTEM SET shared_buffers = '64MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 3: Set to 256MB
+ALTER SYSTEM SET shared_buffers = '256MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 4: Set to 100MB (non-power-of-two)
+ALTER SYSTEM SET shared_buffers = '100MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 5: Set to minimum 128kB
+ALTER SYSTEM SET shared_buffers = '128kB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+SHOW shared_buffers;
+SELECT pg_resize_shared_buffers();
+SHOW shared_buffers;
+SELECT * FROM buffer_allocations;
+SELECT COUNT(*) AS buffer_count FROM pg_buffercache;
+
+-- Test 6: Try to set shared_buffers higher than max_shared_buffers (should fail)
+ALTER SYSTEM SET shared_buffers = '400MB';
+SELECT pg_reload_conf();
+-- reconnect to ensure new setting is loaded
+\c
+-- This should show the old value since the configuration was rejected
+SHOW shared_buffers;
+SHOW max_shared_buffers;
+
+-- TODO: Test that a non-superuser can not invoke pg_resize_shared_buffers()
+-- function.
diff --git a/src/test/buffermgr/t/001_resize_buffer.pl b/src/test/buffermgr/t/001_resize_buffer.pl
new file mode 100644
index 00000000000..fb5a42be26a
--- /dev/null
+++ b/src/test/buffermgr/t/001_resize_buffer.pl
@@ -0,0 +1,193 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Minimal test testing shared_buffer resizing under load
+
+use strict;
+use warnings;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Function to check if pgbench is still running.
+#
+# Relying on IPC::Run's pumpable status to check if pgbench is still running has
+# been proven unreliable. Instead we rely on existence of pgbench processes in
+# pg_stat_activity.  Since we use -C with pgbench, there can be a non-zero
+# chance that no pgbench process is running even thought pgbench is running. But
+# that's a very rare possibility that can be ignored.
+sub pgbench_processes_active
+{
+	my ($node, $application_name) = @_;
+
+	my $result = $node->safe_psql('postgres',
+		"SELECT count(*) FROM pg_stat_activity WHERE application_name = '$application_name';");
+	return int($result) > 0;
+}
+
+my $resize_sql_func_def = q{
+create or replace function pg_resize_shared_buffers_sql(new_size int, out num_tries int) returns int as $$
+declare
+    success boolean := false;
+    tries int := 0;
+    cur_setting text;
+    pending_pattern text;
+    target text := new_size::text;
+begin
+    -- Wait until pg_settings reports the new value as pending,
+    -- i.e. "<old value> (pending: <new value>)".
+    pending_pattern := '%(pending: ' || target || ')%';
+    loop
+        select setting into cur_setting
+        from pg_settings where name = 'shared_buffers';
+        exit when cur_setting like pending_pattern or cur_setting = target;
+        perform pg_sleep(0.1);
+        raise notice 'Current setting: %', cur_setting;
+    end loop;
+
+    -- pg_resize_shared_buffers() returns true on success; retry until it succeeds.
+    while not success loop
+        tries := tries + 1;
+        select pg_resize_shared_buffers() into success;
+        if not success then
+            perform pg_sleep(0.1);
+        end if;
+        raise notice 'pg_resize_shared_buffers() attempt %: success = %', tries, success;
+    end loop;
+
+    -- Confirm the new value is in effect (no longer pending).
+    select setting into cur_setting
+    from pg_settings where name = 'shared_buffers';
+    if cur_setting <> target then
+        raise exception 'shared_buffers resize did not take effect: expected %, got %',
+            target, cur_setting;
+    end if;
+
+    num_tries := tries;
+    return;
+end;
+$$ language plpgsql;
+};
+
+# Function to resize buffer pool and verify the change.
+sub apply_and_verify_buffer_change
+{
+	my ($node, $new_size) = @_;
+
+	# Use the new pg_resize_shared_buffers() interface which handles everything synchronously
+	$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$new_size'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+	$node->safe_psql('postgres', "SELECT pg_resize_shared_buffers_sql($new_size)");
+
+	# Any failure in resizing the buffer pool will cause the test to timeout. So
+	# if we reach here, the resize was successful. Just declare it as a
+	# successful test so that we can see progress in the test output.
+	ok(1, "Buffer pool resized to $new_size");
+}
+
+my @buffer_sizes = (128, 28, 16 * 1024, 32 * 1024, 1024, 512, 16, 24, 256, 128 * 1024, 16 * 1204);
+
+# Initialize a cluster and start pgbench in the background for concurrent load.
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Permit resizing up to 1GB for this test and let the server start with 128MB.
+$node->append_conf('postgresql.conf', qq{
+max_shared_buffers = } . (sort { $b <=> $a } @buffer_sizes)[0] . qq{
+shared_buffers = 16
+log_statement = none
+restart_after_crash = off
+});
+
+$node->start;
+$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache");
+$node->safe_psql('postgres', $resize_sql_func_def);
+
+my $pgb_scale = 10;
+my $pgb_duration = 120;
+my $pgb_num_clients = 10;
+# make it easy to identify pgbench processes in pg_stat_activity
+my $application_name = 'pgbench_buffer_resize_test';
+$node->pgbench(
+	"--initialize --init-steps=dtpvg --scale=$pgb_scale --quiet",
+	0,
+	[qr{^$}],
+	[   # stderr patterns to verify initialization stages
+		qr{dropping old tables},
+		qr{creating tables},
+		qr{done in \d+\.\d\d s }
+	],
+	"pgbench initialization (scale=$pgb_scale)"
+);
+my ($pgbench_stdin, $pgbench_stdout, $pgbench_stderr) = ('', '', '');
+# Use --exit-on-abort so that the test stops on the first server crash or error,
+# thus making it easy to debug the failure. Use -C to increase the chances of a
+# new backend being created while resizing the buffer pool.
+my $pgbench_process = IPC::Run::start(
+	[
+		'pgbench',
+		'-p', $node->port,
+		'-h', $node->host,
+		'-T', $pgb_duration,
+		'-c', $pgb_num_clients,
+		'-C',
+		'--exit-on-abort',
+		'--continue-on-error',
+		"dbname=postgres application_name=$application_name"
+	],
+	'<'  => \$pgbench_stdin,
+	'>'  => \$pgbench_stdout,
+	'2>' => \$pgbench_stderr
+);
+
+ok($pgbench_process, "pgbench started successfully");
+
+# Resize buffer pool to various sizes while pgbench is running in the
+# background. We use smaller sizes to induce frequent buffer eviction and
+# allocation.  Also smaller buffer pool means frequent wraparound in background
+# writer, default buffer allocation strategy and checkpointer.
+#
+# TODO: These are pseudo-randomly picked sizes, but we can do better.
+my $tests_completed = 0;
+
+# Reset background writer stats before starting the resize cycle
+$node->safe_psql('postgres', "SELECT pg_stat_reset_shared('bgwriter')");
+
+# Resize as many times as possible while pgbench is running.
+while (pgbench_processes_active($node, $application_name))
+{
+	for my $target_size (@buffer_sizes)
+	{
+		# Stop if pgbench finished
+		if (!pgbench_processes_active($node, $application_name))
+		{
+			last;
+		}
+
+		apply_and_verify_buffer_change($node, $target_size);
+		$tests_completed++;
+
+		# Wait for the resized buffer pool to stabilize.
+		sleep(1);
+	}
+}
+
+ok($tests_completed > scalar(@buffer_sizes), "All buffer size transitions were tested");
+note "Completed $tests_completed buffer resize operations while pgbench was running";
+
+# Check that the background writer did some work during the resize cycle
+is($node->safe_psql('postgres', "SELECT buffers_clean > 0 FROM pg_stat_bgwriter"), 't', "Background writer ran during resize cycle");
+
+# Make sure that pgbench finishes
+$pgbench_process->signal('TERM');
+ok((IPC::Run::finish $pgbench_process), "pgbench finished successfully");
+
+# Log any error output from pgbench for debugging
+diag("pgbench stderr:\n$pgbench_stderr");
+diag("pgbench stdout:\n$pgbench_stdout");
+
+# Ensure database is still functional after all the buffer changes
+$node->connect_ok("dbname=postgres",
+	"Database remains accessible after $tests_completed buffer resize operations");
+
+done_testing();
diff --git a/src/test/buffermgr/t/003_resize_fault_tolerance.pl b/src/test/buffermgr/t/003_resize_fault_tolerance.pl
new file mode 100644
index 00000000000..366929f4c45
--- /dev/null
+++ b/src/test/buffermgr/t/003_resize_fault_tolerance.pl
@@ -0,0 +1,839 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Test that only one pg_resize_shared_buffers() call succeeds when multiple
+# sessions attempt to resize buffers concurrently
+
+use strict;
+use warnings;
+use Config;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Skip this test if injection points are not supported
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# =============================================================================
+# Initialization
+# =============================================================================
+my $initial_nbuffers = 16;
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points');
+$node->append_conf('postgresql.conf', "shared_buffers = $initial_nbuffers");
+$node->append_conf('postgresql.conf', 'max_shared_buffers = 32');
+$node->append_conf('postgresql.conf', 'restart_after_crash = on');
+$node->start;
+
+# Load injection points extension for test coordination
+$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+
+# =============================================================================
+# Helper functions
+# =============================================================================
+
+# Setup resize operation to be interrupted.
+#
+# Prepare to resize the buffer pool to a target size. Start a resize session
+# through a background psql session.  Adjust GUCs for the mode of interruption.
+# If injection point is provided, setup it up with the injection point and wait
+# for the resize session to reach the injection point.  The resize session is
+# returned to the caller.
+sub start_resize_session
+{
+	my ($target_nbuffers, $mode, $injection_point) = @_;
+
+	$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target_nbuffers'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+
+	my $session = $node->background_psql('postgres', on_error_stop => 0);
+
+	my $injection_action;
+	if (defined $injection_point)
+	{
+		$injection_action = ($mode eq 'error') ? 'error' : 'wait';
+		$session->query_safe('SELECT injection_points_set_local()', verbose => 0);
+		$session->query_safe(
+			"SELECT injection_points_attach('$injection_point', '$injection_action')",
+			verbose => 0);
+	}
+
+	apply_session_gucs_for_mode($session, $mode);
+
+	$session->query_until(
+		qr/starting_resize/,
+		q(
+			\echo starting_resize
+			SELECT pg_resize_shared_buffers();
+		));
+
+	# Wait for the pg_resize_shared_buffers to start waiting at the injection
+	# point.
+	if (defined $injection_point && $injection_action eq 'wait')
+	{
+		my $resize_pid = $session->{backend_pid};
+		$node->poll_query_until('postgres',
+			"SELECT wait_event = '$injection_point' FROM pg_stat_activity WHERE pid = $resize_pid")
+		  or die "timed out waiting for resize backend $resize_pid at $injection_point";
+	}
+
+	return $session;
+}
+
+# Start a backend which can be used to test the barrier handler fault tolerance.
+# We use a long pg_sleep() to simulate a load that checks for interrupts
+# regularly. The given injection point is attached to the peer backend locally
+# to induce a fault in barrier handler.
+sub start_peer_session_with_injection_point
+{
+	my ($injection_point, $action) = @_;
+
+	my $session = $node->background_psql('postgres', on_error_stop => 0);
+
+	$session->query_safe('SELECT injection_points_set_local()', verbose => 0);
+	$session->query_safe("SELECT injection_points_attach('$injection_point', '$action')",
+		verbose => 0);
+	$session->query_until(
+		qr/starting_sleep/,
+		q(
+			\echo starting_sleep
+			SELECT pg_sleep(60);
+		));
+
+	my $peer_pid = $session->{backend_pid};
+	$node->poll_query_until('postgres',
+		"SELECT wait_event = 'PgSleep' FROM pg_stat_activity WHERE pid = $peer_pid")
+	  or die "timed out waiting for peer $peer_pid to enter pg_sleep";
+
+	return $session;
+}
+
+# Apply per-mode session GUCs locally in the given session if required.
+sub apply_session_gucs_for_mode
+{
+	my ($session, $mode) = @_;
+
+	# In timeout mode, set a statement timeout long enough for the resizing
+	# session to reach the injection point and stay there but short enough that
+	# the test doesn't take too long to fail if something goes wrong.
+	if ($mode eq 'timeout')
+	{
+		$session->query_safe("SET statement_timeout = '500ms'", verbose => 0);
+	}
+
+	# Let the resize session detect a client disconnection when testing client
+	# disconnections.
+	if ($mode eq 'disconnect')
+	{
+		$session->query_safe("SET client_connection_check_interval = '100ms'",
+			verbose => 0);
+	}
+}
+
+# Administer the interrupt corresponding to $mode against a resize session
+# that is waiting to be interrupted while resizing the buffer pool.
+sub interrupt_resize_session
+{
+	my ($mode, $session) = @_;
+
+	if ($mode eq 'terminate')
+	{
+		$node->safe_psql('postgres', "SELECT pg_terminate_backend(" . $session->{backend_pid} . ")");
+	}
+	elsif ($mode eq 'cancel')
+	{
+		$node->safe_psql('postgres', "SELECT pg_cancel_backend(" . $session->{backend_pid} . ")");
+	}
+	elsif ($mode eq 'disconnect')
+	{
+		$session->{run}->kill_kill;
+	}
+	elsif ($mode eq 'timeout')
+	{
+		# Nothing to do; statement_timeout will fire from within the resize
+		# session itself.
+	}
+	elsif ($mode eq 'error')
+	{
+		# Nothing to do; the injection point raised ERROR from within the
+		# resize backend itself.
+	}
+	else
+	{
+		die "interrupt_resize_session: unknown mode '$mode'";
+	}
+}
+
+# Function to perform checks after the resize operation has been interrupted. As
+# a result of the interruption, the resize function may finish rolling back the
+# resize or the backend executing that function may exit rolling back the resize
+# or the postmaster may restart all the backends. Perform appropriate checks by
+# detecting the post-interrupt state.
+#
+#  - sentinel_session: a background psql session that is used to detect whether the
+#    postmaster restarted all backends or not.
+#  - resize_session: the background psql session that was executing the resize
+#    operation and was interrupted.
+#  - log_offset: the offset in the server log file before the resize operation was
+#    initiated.
+#  - injection_point and mode: the injection point and mode of interruption that
+#    was used to interrupt the resize operation.
+#  - orig_nbuffers and target_nbuffers: the original and target buffer sizes for
+#    the resize operation.
+#  - test_label: a label to create unique test names for different tests
+sub check_interrupted_resize
+{
+	my ($sentinel_session, $resize_session, $log_offset, $mode,
+		$injection_point, $orig_nbuffers, $target_nbuffers, $test_label) = @_;
+
+	my $resize_pid = $resize_session->{backend_pid};
+	my $sentinel_pid = $sentinel_session->{backend_pid};
+
+	# Wait until the resize backend is no longer running the resize query.
+	$node->poll_query_until('postgres',
+		"SELECT count(*) = 0 FROM pg_stat_activity "
+		  . "WHERE pid = $resize_pid AND state = 'active' "
+		  . "AND query LIKE '%pg_resize_shared_buffers%'")
+	  or die
+	  "timed out waiting for resize backend $resize_pid to finish";
+
+	# Wait for the postmaster to be ready in case it restarted the backends.
+	$node->poll_query_until('postgres', 'SELECT true')
+	  or die "timed out waiting for postmaster liveliness check";
+
+	# Confirm the resize backend was interrupted by the intended signal.
+	# Match against the log line emitted by the resize PID so we don't
+	# accidentally pick up an unrelated message.
+	my %expected_msg = (
+		terminate  => 'terminating connection due to administrator command',
+		cancel     => 'canceling statement due to user request',
+		timeout    => 'canceling statement due to statement timeout',
+		disconnect => 'connection to client lost',
+		error      => "error triggered for injection point $injection_point",
+	);
+	my $log_pattern = qr/\[$resize_pid\][^\n]*\Q$expected_msg{$mode}\E/;
+
+	$node->wait_for_log($log_pattern, $log_offset);
+	ok($node->log_contains($log_pattern, $log_offset),
+		"$test_label: server log shows expected $mode message from pid $resize_pid"
+	);
+
+	my $server_restarted = $node->safe_psql('postgres',
+		"SELECT count(*) = 0 FROM pg_stat_activity WHERE pid = $sentinel_pid"
+	) eq 't';
+
+	if ($server_restarted)
+	{
+		# Postmaster restarted all backends; sentinel and resize sessions
+		# are dead, just reap their IPC::Run handles.
+		$sentinel_session->finish;
+		$resize_session->finish;
+
+		is($node->safe_psql('postgres',
+			"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+			"$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+			"$test_label: buffer pool reflects target size after crash recovery");
+
+		is($node->safe_psql('postgres',
+			"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+			"$target_nbuffers",
+			"$test_label: pg_settings reports target size after crash recovery");
+	}
+	else
+	{
+		$sentinel_session->quit;
+		# The resize session may have exited (e.g. on FATAL or disconnect).
+		if ($node->safe_psql('postgres',
+				"SELECT count(*) = 1 FROM pg_stat_activity WHERE pid = $resize_pid") eq 't')
+		{
+			$resize_session->quit;
+		}
+		else
+		{
+			$resize_session->finish;
+		}
+
+		is($node->safe_psql('postgres',
+			"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+			"$orig_nbuffers|$orig_nbuffers|$orig_nbuffers|0",
+			"$test_label: buffer resize rolled back after $mode");
+
+		# TODO: Also check that the pg_shmem_allocations values are not changed
+
+		is($node->safe_psql('postgres',
+			"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+			"$orig_nbuffers (pending: $target_nbuffers)",
+			"$test_label: pg_settings reports pending new value after $mode");
+
+		is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"),
+			't',
+			"$test_label: resize succeeds after interrupted resize is cleaned up");
+	}
+}
+
+# =============================================================================
+# Concurrent resize test functions
+#
+# Verify that only one pg_resize_shared_buffers() call can succeed at a time
+# using injection points.
+# =============================================================================
+
+# Workhorse function:
+#
+# Make the resize session wait at the given injection point and start another
+# concurrent resize session. The concurrent resize should fail.
+sub test_concurrent_resize_at_injection_point
+{
+	my ($injection_point, $target_nbuffers, $test_label) = @_;
+
+	my $session = start_resize_session($target_nbuffers, 'concurrent_resize',
+		$injection_point);
+	my $resize_pid = $session->{backend_pid};
+
+	is($node->safe_psql('postgres',
+		"SELECT resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$resize_pid", "$test_label: resizer_pid reports resize backend");
+
+	is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"),
+		'f', "$test_label: concurrent resize fails");
+
+	$node->safe_psql('postgres',
+		"SELECT injection_points_wakeup('$injection_point')");
+
+	$session->quit;
+
+	is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+		"$test_label: buffer pool resized to target after wakeup");
+
+	is($node->safe_psql('postgres',
+		"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+		"$target_nbuffers",
+		"$test_label: pg_settings reports target size after wakeup");
+}
+
+# Driver function:
+#
+# Invoke the workhorse function for different injection points
+sub test_concurrent_resize
+{
+	my @injection_points = (
+		'pg-resize-shared-buffers-flag-set',
+		'pgrsb-new-buffer-alloc-barrier-sent',
+		'pgrsb-buffer-pool-size-barrier-sent',
+		'pgrsb-buffer-pool-resize-barrier-sent',
+	);
+
+	# Expand then shrink so the pool returns to its starting size.
+	my @directions = (['expand', 24], ['shrink', $initial_nbuffers]);
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"buffer pool size is $initial_nbuffers at start");
+
+	for my $point (@injection_points)
+	{
+		for my $dir (@directions)
+		{
+			my ($name, $target) = @$dir;
+
+			test_concurrent_resize_at_injection_point($point, $target,
+				"$name: $point");
+		}
+	}
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"buffer pool size is $initial_nbuffers at end");
+}
+
+# =============================================================================
+# Functions to test resize operation interruption
+#
+# Verify that an interruption in resize operation does not leave the buffer pool
+# in an inconsistent state.
+# =============================================================================
+
+# Workhorse function:
+#
+# Interrupt pg_resize_shared_buffers() when it is waiting on an injection point.
+# Check that the buffer pool is left in a consistent state as an aftermath.
+#
+# $mode selects how the resize session is interrupted.
+#  - 'terminate'  - SIGTERM via pg_terminate_backend() from another session.
+#  - 'cancel'     - SIGINT via pg_cancel_backend() from another session.
+#  - 'timeout'    - statement_timeout fires inside the resize session itself.
+#  - 'disconnect' - the resize session's client connection is closed abruptly.
+#  - 'error'      - the injection point itself raises ERROR from within the
+#                   resize backend.
+sub test_interrupt_resize_at_injection_point
+{
+	my ($injection_point, $target_nbuffers, $mode, $test_label) = @_;
+
+	my $orig_nbuffers = $node->safe_psql('postgres',
+		"SELECT current_nbuffers FROM pg_get_buffer_resize_status()");
+	my $log_offset = -s $node->logfile;
+
+	# Start a sentinel session that will be used to detect whether the
+	# postmaster restarted all backends or not after the resize session is
+	# interrupted.
+	my $sentinel_session = $node->background_psql('postgres', on_error_stop => 0);
+
+	my $resize_session = start_resize_session($target_nbuffers, $mode,
+		$injection_point);
+
+	interrupt_resize_session($mode, $resize_session);
+
+	check_interrupted_resize($sentinel_session, $resize_session, $log_offset,
+		$mode, $injection_point, $orig_nbuffers, $target_nbuffers,
+		$test_label);
+}
+
+# Driver function:
+#
+# Invoke the workhorse function for different injection points passing it the
+# given mode of interruption.
+sub test_interrupt_resize_session
+{
+	my ($mode) = @_;
+
+	my @injection_points = (
+		'pg-resize-shared-buffers-flag-set',
+		'pgrsb-new-buffer-alloc-barrier-sent',
+		'pgrsb-buffer-pool-size-barrier-sent',
+		'buffer-mgr-resize-struct',
+		'pgrsb-buffer-pool-resize-barrier-sent',
+	);
+
+	# Expand then shrink so the pool returns to its starting size.
+	my @directions = (['expand', 24], ['shrink', $initial_nbuffers]);
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"$mode: buffer pool size is $initial_nbuffers at start");
+
+	for my $point (@injection_points)
+	{
+		for my $dir (@directions)
+		{
+			my ($name, $target) = @$dir;
+
+			test_interrupt_resize_at_injection_point($point, $target, $mode,
+				"$mode $name: $point");
+		}
+	}
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"$mode: buffer pool size is $initial_nbuffers at end");
+}
+
+# =============================================================================
+# Functions to test error handling in barrier handler
+#
+# Verify that an error in barrier handler does not cause a resize session to
+# fail. The barrier handler may run in a peer backend or the backend which is
+# performing the resize itself.
+# =============================================================================
+
+# Workhorse function:
+#
+# Make a peer session wait at the given injection point in the barrier handler
+# and simulate an error in the handler.
+sub test_error_in_barrier_handler_at_injection_point
+{
+	my ($injection_point, $target_nbuffers, $test_label) = @_;
+
+	my $peer_session = start_peer_session_with_injection_point($injection_point, 'error');
+	my $peer_pid = $peer_session->{backend_pid};
+
+	my $log_offset = -s $node->logfile;
+
+	# Resize the buffer pool which will send a barrier to the peer backend
+	# simulating an error in the barrier handler.
+	$node->safe_psql('postgres',"ALTER SYSTEM SET shared_buffers = '$target_nbuffers'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+	is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"), 't');
+
+	# Confirm the peer raised the expected error from inside the handler.
+	my $log_pattern = qr/\[$peer_pid\][^\n]*\Qerror triggered for injection point $injection_point\E/;
+	$node->wait_for_log($log_pattern, $log_offset);
+	ok($node->log_contains($log_pattern, $log_offset),
+		"$test_label: server log shows error from peer pid $peer_pid at $injection_point"
+	);
+
+	# Check that the resize was completed as expected
+	is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+		"$test_label: buffer pool reflects target size");
+
+	is($node->safe_psql('postgres',
+		"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+		"$target_nbuffers",
+		"$test_label: pg_settings reports target size");
+
+	# pg_resize_shared_buffers() returns only after every peer has
+	# acknowledged the barrier, so by this point the erroring peer has
+	# already left procArray.  Assert that and then reap its IPC::Run handle.
+	is($node->safe_psql('postgres',
+			"SELECT count(*) FROM pg_stat_activity WHERE pid = $peer_pid"),
+		'0',
+		"$test_label: peer pid $peer_pid exited after handler error");
+
+	$peer_session->finish;
+}
+
+# Driver function:
+#
+# Simulate a failure to change the protection on the shared memory. This should
+# cause the barrier handler to raise an error. The barrier handler may run in a
+# peer backend or the backend which is performing the resize itself. The resize
+# session should still complete successfully.
+sub test_error_in_barrier_handler
+{
+	my $injection_point = 'buffer-mgr-protect-struct';
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"error-in-handler: buffer pool size is $initial_nbuffers at start");
+
+	# Test error in barrier handler in a peer backend.  Expand then shrink so
+	# the pool returns to its starting size.
+	for my $dir (['expand', 24], ['shrink', $initial_nbuffers])
+	{
+		my ($name, $target) = @$dir;
+		test_error_in_barrier_handler_at_injection_point($injection_point,
+			$target, "error-in-handler peer $name");
+	}
+
+	# Test the same error in the barrier handler in the resize backend itself.
+	# Expand then shrink so the pool returns to its starting size.
+	for my $dir (['expand', 24], ['shrink', $initial_nbuffers])
+	{
+		my ($name, $target) = @$dir;
+		test_interrupt_resize_at_injection_point($injection_point,
+			$target, 'error', "error-in-handler resize-backend $name");
+	}
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"error-in-handler: buffer pool size is $initial_nbuffers at end");
+}
+
+# =============================================================================
+# Functions to test fault tolerance of resize operation waiting for barrier
+#
+# Test that, when interrupted, a resizing operation waiting for a barrier to be
+# acknowledged doesn't leave the buffer pool in an inconsistent state.
+# =============================================================================
+
+# Workhorse function:
+#
+# We start a peer session with the given injection point in the barrier handler
+# code attached locally. Once the resize operation starts, the peer session will
+# hit the injection point and wait there. Interrupt the resize session and check
+# that the buffer pool is left in a consistent state as an aftermath.
+#
+# - injection_point: the injection point to attach to the peer session.
+# - target_nbuffers: the target buffer size for the resize operation.
+# - mode: the mode of interruption to apply to the resize session.
+# - test_label: a label to create unique test names for different tests
+sub test_fault_resize_waiting_barrier
+{
+	my ($injection_point, $target_nbuffers, $mode, $test_label) = @_;
+
+	my $orig_nbuffers = $node->safe_psql('postgres',
+		"SELECT current_nbuffers FROM pg_get_buffer_resize_status()");
+	my $log_offset = -s $node->logfile;
+
+	my $peer_session = start_peer_session_with_injection_point($injection_point, 'wait');
+	my $peer_pid = $peer_session->{backend_pid};
+
+	# Sentinel session to detect a postmaster restart.
+	my $sentinel_session = $node->background_psql('postgres', on_error_stop => 0);
+
+	my $resize_session = start_resize_session($target_nbuffers, $mode);
+	my $resize_pid = $resize_session->{backend_pid};
+
+	# Wait for the peer to reach the injection point. At this point the resize
+	# backend should be blocked in WaitForProcSignalBarrier.
+	$node->poll_query_until('postgres',
+		"SELECT wait_event = '$injection_point' FROM pg_stat_activity WHERE pid = $peer_pid")
+	  or die "$test_label: timed out waiting for peer $peer_pid at $injection_point";
+	is($node->safe_psql('postgres',
+			"SELECT wait_event FROM pg_stat_activity WHERE pid = $resize_pid"),
+		'ProcSignalBarrier',
+		"$test_label: resize $resize_pid is waiting at ProcSignalBarrier");
+
+	interrupt_resize_session($mode, $resize_session);
+
+	check_interrupted_resize($sentinel_session, $resize_session, $log_offset,
+		$mode, $injection_point, $orig_nbuffers, $target_nbuffers, $test_label);
+
+	# Cleanup peer session. If the postmaster restarted all backends, the peer
+	# backend is already gone.
+	if ($node->safe_psql('postgres',
+			"SELECT count(*) = 1 FROM pg_stat_activity WHERE pid = $peer_pid") eq 't')
+	{
+		$peer_session->quit;
+	}
+	else
+	{
+		$peer_session->finish;
+	}
+}
+
+# Driver function:
+#
+# Invoke the workhorse function for different injection points passing it the
+# given mode of interruption.
+sub test_fault_resize_waiting_barrier_for_mode
+{
+	my ($mode) = @_;
+
+	my @injection_points = (
+		'pgrsb-handle-new-buffer-alloc-barrier',
+		'pgrsb-handle-buffer-pool-size-barrier',
+		'pgrsb-handle-buffer-pool-resize-barrier',
+	);
+
+	# Expand then shrink so the pool returns to its starting size.
+	my @directions = (['expand', 24], ['shrink', $initial_nbuffers]);
+
+	is($node->safe_psql('postgres', "SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"fault-resize-on-peer $mode: buffer pool size is $initial_nbuffers at start");
+
+	for my $point (@injection_points)
+	{
+		for my $dir (@directions)
+		{
+			my ($name, $target) = @$dir;
+
+			test_fault_resize_waiting_barrier($point, $target, $mode,
+				"fault-resize-on-peer $mode $name: $point");
+		}
+	}
+
+	is($node->safe_psql('postgres', "SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"fault-resize-on-peer $mode: buffer pool size is $initial_nbuffers at end");
+}
+
+# =============================================================================
+# Functions to test server restart during a resize
+#
+# Verify that a server can be stopped and started while a resize operation is in
+# progress and the server is started with buffer pool in a consistent state that
+# reflects the target size.
+# =============================================================================
+
+# Workhorse function for fast/immediate shutdown:
+#
+# Make the resize session wait at the given injection point and restart the
+# server in the given mode.
+sub test_server_restart_during_resize_at_injection_point
+{
+	my ($injection_point, $target_nbuffers, $stop_mode, $test_label) = @_;
+
+	my $resize_session = start_resize_session($target_nbuffers,
+		'server_restart', $injection_point);
+
+	$node->stop($stop_mode);
+
+	# Cleanup resize session, the backend must have gone now.
+	$resize_session->finish;
+
+	$node->start;
+
+	is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+		"$test_label: buffer pool reflects target size after $stop_mode restart");
+
+	is($node->safe_psql('postgres',
+		"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+		"$target_nbuffers",
+		"$test_label: pg_settings reports target size after $stop_mode restart");
+}
+
+# Workhorse function for smart shutdown:
+#
+# Let the resize operation wait at the given injection point, send a smart
+# shutdown asynchronously. Once the postmaster enters smart shutdown, wakeup the
+# resize backend and let it complete. Verify that the pool reflects the target
+# size immediately and also after the restart.
+#
+# - injection_point: the injection point to park the resize backend at.
+# - target_nbuffers: the target buffer size for the resize operation.
+# - test_label: a label to create unique test names for different tests
+sub test_server_restart_smart_during_resize_at_injection_point
+{
+	my ($injection_point, $target_nbuffers, $test_label) = @_;
+
+	my $log_offset = -s $node->logfile;
+
+	my $resize_session = start_resize_session($target_nbuffers, 'server_restart', $injection_point);
+	my $resize_pid = $resize_session->{backend_pid};
+
+	# Open another session which can be used to wakeup the resize backend.
+	my $control_session = $node->background_psql('postgres', on_error_stop => 0);
+
+	# Start the process to stop the server in smart mode.
+	local %ENV = $node->_get_env();
+	my @stop_cmd = ('pg_ctl', '--pgdata' => $node->data_dir, '--mode' => 'smart', 'stop');
+	my ($stop_in, $stop_out, $stop_err) = ('', '', '');
+	my $stop_session = IPC::Run::start(\@stop_cmd,
+		\$stop_in, \$stop_out, \$stop_err);
+
+	# Confirm the postmaster entered smart shutdown.
+	$node->wait_for_log(qr/received smart shutdown request/, $log_offset);
+
+	# Make sure that the resize backend is still alive
+	is($control_session->query("SELECT wait_event FROM pg_stat_activity WHERE pid = $resize_pid"),
+		$injection_point,
+		"$test_label: resize backend alive during smart shutdown");
+
+	# Wake up the resize backnd and let it finish.
+	$control_session->query_safe("SELECT injection_points_wakeup('$injection_point')",
+		verbose => 0);
+
+	# Check that the resize finished successfully by querying from the same
+	# session. The queries won't return if the resize didn't finish. Accomodate
+	# the output 't' from pg_resize_shared_buffers() in the expected output of
+	# the first query.
+	is($resize_session->query("SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()",
+			verbose => 0),
+		"t\n$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+		"$test_label: pg_resize_shared_buffers() succeeded and pool at target during smart shutdown");
+	is($resize_session->query("SELECT setting FROM pg_settings WHERE name = 'shared_buffers'",
+			verbose => 0),
+		"$target_nbuffers",
+		"$test_label: pg_settings reports target size during smart shutdown");
+
+	$resize_session->quit;
+	$control_session->quit;
+
+	# Wait for server to stop
+	IPC::Run::finish($stop_session)
+	  or die "$test_label: pg_ctl smart stop failed: $stop_err";
+
+	# Sync Cluster.pm internal state and start the cluster back.
+	$node->{_pid} = undef;
+	$node->start;
+
+	is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$target_nbuffers|$target_nbuffers|$target_nbuffers|0",
+		"$test_label: buffer pool reflects target size after smart restart");
+
+	is($node->safe_psql('postgres',
+		"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+		"$target_nbuffers",
+		"$test_label: pg_settings reports target size after smart restart");
+}
+
+# Driver function:
+#
+# Invoke the workhorse function for every injection point in resize operation in
+# both directions for the given stop mode.
+sub test_server_restart_during_resize
+{
+	my ($stop_mode) = @_;
+
+	my @injection_points = (
+		'pg-resize-shared-buffers-flag-set',
+		'pgrsb-new-buffer-alloc-barrier-sent',
+		'pgrsb-buffer-pool-size-barrier-sent',
+		'pgrsb-buffer-pool-resize-barrier-sent',
+	);
+
+	# Expand then shrink so the pool returns to its starting size.
+	my @directions = (['expand', 24], ['shrink', $initial_nbuffers]);
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"server-restart $stop_mode: buffer pool size is $initial_nbuffers at start");
+
+	for my $point (@injection_points)
+	{
+		for my $dir (@directions)
+		{
+			my ($name, $target) = @$dir;
+			my $label = "server-restart $stop_mode $name: $point";
+
+			if ($stop_mode eq 'smart')
+			{
+				test_server_restart_smart_during_resize_at_injection_point(
+					$point, $target, $label);
+			}
+			else
+			{
+				test_server_restart_during_resize_at_injection_point($point,
+					$target, $stop_mode, $label);
+			}
+		}
+	}
+
+	is($node->safe_psql('postgres',
+			"SELECT current_nbuffers FROM pg_get_buffer_resize_status()"),
+		"$initial_nbuffers",
+		"server-restart $stop_mode: buffer pool size is $initial_nbuffers at end");
+}
+
+# =============================================================================
+# Run tests
+# =============================================================================
+test_concurrent_resize();
+test_error_in_barrier_handler();
+
+test_interrupt_resize_session('terminate');
+test_interrupt_resize_session('cancel');
+test_interrupt_resize_session('timeout');
+test_interrupt_resize_session('error');
+
+# A resize session waiting for a barrier to be acknowledged can not be
+# interrupted by an error. Hence don't test that mode.
+test_fault_resize_waiting_barrier_for_mode('terminate');
+test_fault_resize_waiting_barrier_for_mode('cancel');
+test_fault_resize_waiting_barrier_for_mode('timeout');
+
+test_server_restart_during_resize('immediate');
+test_server_restart_during_resize('fast');
+test_server_restart_during_resize('smart');
+
+# client_connection_check_interval is only effective on systems that expose
+# POLLRDHUP/EPOLLRDHUP (Linux, and a few other Unix variants).  On other
+# platforms the GUC is silently a no-op, so the disconnect test would hang.
+if ($Config::Config{osname} eq 'linux')
+{
+	test_interrupt_resize_session('disconnect');
+	test_fault_resize_waiting_barrier_for_mode('disconnect');
+}
+else
+{
+	diag("skipping disconnect interrupt test on $Config::Config{osname} "
+		  . "(requires POLLRDHUP support)");
+}
+
+done_testing();
+
+# Few more tests to add but may be somewhere else
+# TODO: test when there are backends that have not attached to the shared memory
+# TODO: test that a non-superuser cannot run pg_resize_shared_buffers()
+# TODO: the resize_sql_func_def in 001_resize_buffer may be useful in other
+#		tests (not necessarily this one). Maybe we can use it in other tests where we
+#		are looping in TAP test code.
diff --git a/src/test/buffermgr/t/004_client_join_buffer_resize.pl b/src/test/buffermgr/t/004_client_join_buffer_resize.pl
new file mode 100644
index 00000000000..fda0f01bb27
--- /dev/null
+++ b/src/test/buffermgr/t/004_client_join_buffer_resize.pl
@@ -0,0 +1,221 @@
+# Copyright (c) 2025-2025, PostgreSQL Global Development Group
+#
+# Test shared_buffer resizing coordination with client connections joining using injection points
+use strict;
+use warnings;
+use IPC::Run;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(sleep);
+
+# Skip this test if injection points are not supported
+if ($ENV{enable_injection_points} ne 'yes')
+{
+	plan skip_all => 'Injection points not supported by this build';
+}
+
+# Function to calculate the size of test table required to fill up maximum
+# buffer pool when populating it.
+sub calculate_test_sizes
+{
+	my ($node, $block_size) = @_;
+
+	# Get the maximum buffer pool size from configuration
+	my $max_shared_buffers = $node->safe_psql('postgres', "SHOW max_shared_buffers");
+	my ($max_val, $max_unit) = ($max_shared_buffers =~ /(\d+)(\w+)/);
+	my $max_size_bytes;
+	if (lc($max_unit) eq 'kb') {
+		$max_size_bytes = $max_val * 1024;
+	} elsif (lc($max_unit) eq 'mb') {
+		$max_size_bytes = $max_val * 1024 * 1024;
+	} elsif (lc($max_unit) eq 'gb') {
+		$max_size_bytes = $max_val * 1024 * 1024 * 1024;
+	} else {
+		# Default to kB if unit is not recognized
+		$max_size_bytes = $max_val * 1024;
+	}
+
+	# Fill more pages than minimally required to increase the chances of pages
+	# from the test table filling the buffer cache.
+	$max_size_bytes = $max_size_bytes;
+	my $pages_needed = int($max_size_bytes / $block_size) + 10; # Add some extra to ensure buffers are filled
+	my $rows_to_insert = $pages_needed * 100; # Assuming roughly 100 rows per page for our table structure
+	return ($max_size_bytes, $pages_needed, $rows_to_insert);
+}
+
+# Function to calculate expected buffer count from size string
+sub calculate_buffer_count
+{
+	my ($size_string, $block_size) = @_;
+	# Parse size and convert to bytes
+	my ($size_val, $unit) = ($size_string =~ /(\d+)(\w+)/);
+	my $size_bytes;
+	if (lc($unit) eq 'kb') {
+		$size_bytes = $size_val * 1024;
+	} elsif (lc($unit) eq 'mb') {
+		$size_bytes = $size_val * 1024 * 1024;
+	} elsif (lc($unit) eq 'gb') {
+		$size_bytes = $size_val * 1024 * 1024 * 1024;
+	} else {
+		# Default to kB if unit is not recognized
+		$size_bytes = $size_val * 1024;
+	}
+	return int($size_bytes / $block_size);
+}
+
+# Initialize cluster with very small buffer sizes for testing
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# Configure for buffer resizing with very small buffer pool sizes for faster tests.
+# TODO: for some reason parallel workers try to load default number of shared_buffers which doesn't work with lower max_shared_buffers. We need to fix that - somewhere it's picking default value of shared buffers. For now disable parallelism
+$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points');
+$node->append_conf('postgresql.conf', qq{
+max_shared_buffers = 512kB
+shared_buffers = 320kB
+max_parallel_workers_per_gather = 0
+});
+$node->start;
+
+# Enable injection points
+$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+
+# Get the block size (this is fixed for the binary)
+my $block_size = $node->safe_psql('postgres', "SHOW block_size");
+
+# Try to create pg_buffercache extension for buffer analysis
+eval {
+	$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache");
+};
+if ($@) {
+	$node->stop;
+	plan skip_all => 'pg_buffercache extension not available - cannot verify buffer usage';
+}
+
+# Create a small test table, and fetch its properties for later reference if required.
+$node->safe_psql('postgres', qq{
+	CREATE TABLE client_test (c1 int, data char(50));
+});
+my $table_oid = $node->safe_psql('postgres', "SELECT oid FROM pg_class WHERE relname = 'client_test'");
+my $table_relfilenode = $node->safe_psql('postgres', "SELECT relfilenode FROM pg_class WHERE relname = 'client_test'");
+note("Test table client_test: OID = $table_oid, relfilenode = $table_relfilenode");
+my ($max_size_bytes, $pages_needed, $rows_to_insert) = calculate_test_sizes($node, $block_size);
+
+# Create dedicated sessions for injection point handling and test queries,
+# so that we don't create new backends for test operations after starting
+# resize operation. Only one backend, which tests new backend synchronization
+# with resizing operation, should start after resizing has commenced.
+my $injection_session = $node->background_psql('postgres');
+my $query_session = $node->background_psql('postgres');
+my $resize_session = $node->background_psql('postgres');
+
+# Function to run a single injection point test
+sub run_injection_point_test
+{
+	my ($test_name, $injection_point, $target_size, $operation_type) = @_;
+
+	# Silence the logging of the statements we run to avoid
+	# unnecessarily bloating the test logs.  This runs before the
+	# upgrade we're testing, so the details should not be very
+	# interesting for debugging.  But if needed, you can make it more
+	# verbose by setting this.
+	my $verbose = 0;
+
+	note("Test with $test_name ($operation_type)");
+
+	# Calculate test parameters before starting resize
+	my ($max_size_bytes, $pages_needed, $rows_to_insert) = calculate_test_sizes($node, $target_size, $block_size);
+
+	# Update buffer pool size and wait for it to reflect pending state
+	$resize_session->query_safe("ALTER SYSTEM SET shared_buffers = '$target_size'", verbose => $verbose);
+	$resize_session->query_safe("SELECT pg_reload_conf()", verbose => $verbose);
+	my $pending_size_str = "pending: $target_size";
+	$resize_session->poll_query_until("SELECT substring(current_setting('shared_buffers'), '$pending_size_str')", $pending_size_str, verbose => $verbose);
+
+	# Set up injection point in injection session
+	$injection_session->query_safe("SELECT injection_points_attach('$injection_point', 'wait')", verbose => $verbose);
+
+	# Trigger resize
+	$resize_session->query_until(
+		qr/starting_resize/,
+		q(
+			\echo starting_resize
+			SELECT pg_resize_shared_buffers();
+		)
+	);
+
+	# Wait until resize actually reaches the injection point using the query session
+	$query_session->wait_for_event('client backend', $injection_point, verbose => $verbose);
+
+	# Start a client while resize is paused
+	my $client = $node->background_psql('postgres');
+	note("Background client backend PID: " . $client->query_safe("SELECT pg_backend_pid()", verbose => $verbose));
+
+	# Wake up the injection point from injection session
+	$injection_session->query_safe("SELECT injection_points_wakeup('$injection_point')", verbose => $verbose);
+
+	# Test buffer functionality immediately after waking up injection point
+	# Insert data to test buffer pool functionality during/after resize
+	$client->query_safe("INSERT INTO client_test SELECT i, 'test_data_' || i FROM generate_series(1, $rows_to_insert) i", verbose => $verbose);
+	# Verify the data was inserted correctly and can be read back
+	is($client->query_safe("SELECT COUNT(*) FROM client_test", verbose => $verbose), $rows_to_insert, "inserted $rows_to_insert during $test_name ($operation_type) successful");
+
+	# Verify table size is reasonable (should be substantial for testing)
+	ok($query_session->query_safe("SELECT pg_total_relation_size('client_test')", verbose => $verbose) >=  $max_size_bytes,"table size is large enough to overflow buffer pool in test $test_name ($operation_type)");
+
+	# Wait for the resize operation to complete. There is no direct way to do so
+	# in background_psql. Hence fire a psql command and wait for it to finish
+	$resize_session->query(q(\echo 'done'), verbose => $verbose);
+
+	# Detach injection point from injection session
+	$injection_session->query_safe("SELECT injection_points_detach('$injection_point')", verbose => $verbose);
+
+	# Verify resize completed successfully
+	is($query_session->query_safe("SELECT current_setting('shared_buffers')", verbose => $verbose), $target_size,
+		"resize completed successfully to $target_size");
+
+	# Check buffer pool size using pg_buffercache after resize completion
+	is($query_session->query_safe("SELECT COUNT(*) FROM pg_buffercache", verbose => $verbose), calculate_buffer_count($target_size, $block_size), "all buffers in the buffer pool used in $test_name ($operation_type)");
+
+	# Wait for client to complete
+	ok($client->quit, "client succeeded during $test_name ($operation_type)");
+
+	# Clean up for next test
+	$query_session->query_safe("DELETE FROM client_test", verbose => $verbose);
+}
+
+# Test new client joining during various phases of buffer resizing operation using injection points
+my @injection_tests = (
+	{
+		name => 'flag setting phase',
+		injection_point => 'pg-resize-shared-buffers-flag-set',
+	},
+	{
+		name => 'new buffer alloc barrier complete',
+		injection_point => 'pgrsb-new-buffer-alloc-barrier-sent',
+	},
+	{
+		name => 'buffer pool size barrier complete',
+		injection_point => 'pgrsb-buffer-pool-size-barrier-sent',
+	},
+	{
+		name => 'buffer pool resize barrier complete',
+		injection_point => 'pgrsb-buffer-pool-resize-barrier-sent',
+	},
+);
+
+foreach my $test (@injection_tests)
+{
+	# Test shrinking scenario
+	run_injection_point_test($test->{name}, $test->{injection_point}, '272kB', 'shrinking');
+
+	# Test expanding scenario
+	run_injection_point_test($test->{name}, $test->{injection_point}, '400kB', 'expanding');
+}
+
+$injection_session->quit;
+$query_session->quit;
+$resize_session->quit;
+
+done_testing();
diff --git a/src/test/buffermgr/t/005_resize_failures.pl b/src/test/buffermgr/t/005_resize_failures.pl
new file mode 100644
index 00000000000..820aa07f006
--- /dev/null
+++ b/src/test/buffermgr/t/005_resize_failures.pl
@@ -0,0 +1,171 @@
+# Copyright (c) 2025-2026, PostgreSQL Global Development Group
+#
+# Test that pg_resize_shared_buffers() rolls back cleanly when resize fails.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $have_injection_points = ($ENV{enable_injection_points} eq 'yes');
+
+# Start the pool large enough that there is room to shrink below a pinned
+# buffer while still satisfying the shared_buffers GUC minimum.
+my $initial_nbuffers = 24;
+my $max_nbuffers = 32;
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+if ($have_injection_points)
+{
+	$node->append_conf('postgresql.conf', 'shared_preload_libraries = injection_points');
+}
+$node->append_conf('postgresql.conf', "shared_buffers = $initial_nbuffers");
+$node->append_conf('postgresql.conf', "max_shared_buffers = $max_nbuffers");
+$node->start;
+
+# pg_buffercache lets us locate the bufferid holding a given page.
+$node->safe_psql('postgres', "CREATE EXTENSION pg_buffercache");
+if ($have_injection_points)
+{
+	$node->safe_psql('postgres', "CREATE EXTENSION injection_points");
+}
+
+# ---------------------------------------------------------------------------
+# Test the case when shrinking is aborted by a pinned buffer
+# ---------------------------------------------------------------------------
+
+my $min_nbuffers = $node->safe_psql('postgres',
+	"SELECT min_val::int FROM pg_settings WHERE name = 'shared_buffers'");
+
+# In order to reliably pin a buffer above $min_nbuffers, we create as many
+# tables $min_nbuffers + 1, open a cursor on the tables and fetch one row from
+# each cursor one at a time.  This will pin one buffer per table, guaranteeing
+# that at least one of the pinned buffers will be above $min_nbuffers.
+my $ntables = $min_nbuffers + 1;
+for my $i (1 .. $ntables)
+{
+	$node->safe_psql('postgres', "CREATE TABLE evict_target_$i AS SELECT generate_series(1, 2) AS i");
+}
+my $pinner = $node->background_psql('postgres', on_error_stop => 0);
+$pinner->query_safe("BEGIN", verbose => 0);
+my $pinned_buf = 0;
+for my $i (1 .. $ntables)
+{
+	$pinner->query_safe("DECLARE c_$i CURSOR FOR SELECT * FROM evict_target_$i", verbose => 0);
+	$pinner->query_safe("FETCH 1 FROM c_$i", verbose => 0);
+
+	my $buf = $node->safe_psql('postgres',
+    "SELECT min(bufferid) FROM pg_buffercache WHERE pinning_backends > 0 AND bufferid > $min_nbuffers");
+
+	if ($buf =~ /^\d+$/)
+	{
+		$pinned_buf = $buf;
+		last;
+	}
+}
+cmp_ok($pinned_buf, '>', $min_nbuffers, "pinned a buffer above $min_nbuffers");
+
+# Set the target so that the pinned buffer is in the range of buffers to be evicted.
+my $shrink_target = $pinned_buf - 1;
+$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$shrink_target'");
+$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+
+my $log_offset = -s $node->logfile;
+
+is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"),
+	'f',
+	"shrink returns false when a buffer to be evicted is pinned");
+ok($node->log_contains(qr/could not remove buffer $pinned_buf, it is pinned/, $log_offset),
+	"log reports the pinned buffer that blocked eviction");
+ok($node->log_contains(qr/failed to evict extra buffers during shrinking/, $log_offset),
+	"log reports the eviction failure");
+
+is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+	"$initial_nbuffers|$initial_nbuffers|$initial_nbuffers|0",
+	"pool unchanged after eviction failure");
+
+is($node->safe_psql('postgres',
+		"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+	"$initial_nbuffers (pending: $shrink_target)",
+	"pg_settings reports pending shrink target");
+
+# Releasing all pins lets the retry succeed.
+$pinner->quit;
+
+is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"),
+	't',
+	"shrink succeeds after pins released");
+
+is($node->safe_psql('postgres',
+		"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+	"$shrink_target|$shrink_target|$shrink_target|0",
+	"pool shrunk to $shrink_target after pins released");
+
+# ---------------------------------------------------------------------------
+# Test the case when memory allocation fails when expanding the buffer pool.
+# Uses an injection point to simulate the failure without exhausting real
+# memory.
+# ---------------------------------------------------------------------------
+
+SKIP:
+{
+	skip "injection points not supported by this build"
+	  unless $have_injection_points;
+
+	# The buffer manager's resizable structures whose sizes must be rolled
+	# back if any one of them fails to grow.
+	my $resizable_structs =
+	  q{('Buffer Descriptors', 'Buffer Blocks', 'Buffer IO Condition Variables', 'Checkpoint BufferIds')};
+	my $sizes_query = "SELECT name, size FROM pg_shmem_allocations WHERE name IN $resizable_structs ORDER BY name";
+	my $sizes_before = $node->safe_psql('postgres', $sizes_query);
+
+	my $resizer = $node->background_psql('postgres');
+	$resizer->query_safe("SELECT injection_points_set_local()", verbose => 0);
+	$resizer->query_safe("SELECT injection_points_attach('buffer-mgr-resize-struct-fail', 'notice')",
+		verbose => 0);
+
+	my $expand_target = $max_nbuffers;
+	$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$expand_target'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+
+	my $expand_log_offset = -s $node->logfile;
+
+	is($resizer->query("SELECT pg_resize_shared_buffers()"), 'f',
+		"expansion fails when a structure can not be expanded");
+
+	# Discard the expected WARNINGs so later query_safe calls do not die.
+	$resizer->{stderr} = '';
+
+	ok($node->log_contains(qr/failed to expand buffer pool structures/, $expand_log_offset),
+		"log reports the expansion failure");
+
+	is($node->safe_psql('postgres',
+			"SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$shrink_target|$shrink_target|$shrink_target|0",
+		"buffer pool status after expansion failure");
+
+	is($node->safe_psql('postgres',
+			"SELECT setting FROM pg_settings WHERE name = 'shared_buffers'"),
+		"$shrink_target (pending: $expand_target)",
+		"pg_settings reports pending expand target");
+
+	is($node->safe_psql('postgres', $sizes_query), $sizes_before,
+		"resizable buffer manager structures rolled back to previous sizes");
+
+	# Detach the injection point, to retry again. The retry should succeed.
+	$resizer->query_safe(
+		"SELECT injection_points_detach('buffer-mgr-resize-struct-fail')",
+		verbose => 0);
+	is($resizer->query("SELECT pg_resize_shared_buffers()"), 't',
+		"expand succeeds after the injection point is detached");
+
+	$resizer->quit;
+
+	is($node->safe_psql('postgres', "SELECT active_nbuffers, current_nbuffers, target_nbuffers, resizer_pid FROM pg_get_buffer_resize_status()"),
+		"$expand_target|$expand_target|$expand_target|0",
+		"pool expanded to $expand_target after detach");
+}
+
+done_testing();
diff --git a/src/test/buffermgr/t/006_resize_with_syslogger.pl b/src/test/buffermgr/t/006_resize_with_syslogger.pl
new file mode 100644
index 00000000000..75b047ad984
--- /dev/null
+++ b/src/test/buffermgr/t/006_resize_with_syslogger.pl
@@ -0,0 +1,58 @@
+# Copyright (c) 2026-2026, PostgreSQL Global Development Group
+#
+# Test that pg_resize_shared_buffers() works when a backend that never
+# attaches to shared memory is running.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+my $initial_nbuffers = 16;
+my $expanded_nbuffers = 24;
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+
+# When logging_collector is on, the server starts a syslogger process that never
+# attaches to the shared memory. We use that as a proxy for a backend that never
+# attaches to shared memory.
+$node->append_conf(
+	'postgresql.conf', qq{
+shared_buffers = $initial_nbuffers
+max_shared_buffers = $expanded_nbuffers
+logging_collector = on
+});
+$node->start;
+
+# Check that the syslogger is running by writing a log marker and waiting for it
+# to appear in the log file.
+sub check_syslogger_running
+{
+	my ($marker) = @_;
+
+	$node->safe_psql('postgres', "DO \$\$ BEGIN RAISE LOG '$marker'; END \$\$");
+	return $node->poll_query_until('postgres', "SELECT pg_read_file(pg_current_logfile()) ~ '$marker'");
+}
+
+check_syslogger_running('syslogger_marker_before_resize')
+  or die "syslogger is not running";
+
+# Resize the buffer pool, and check that the syslogger continues to run while
+# the resize is in progress.
+# TODO: Instead of custom markers we could use the log line that is emitted when
+# the resize is complete, when we have frozen those.
+for my $dir (['expand', $expanded_nbuffers], ['shrink', $initial_nbuffers])
+{
+	my ($name, $target) = @$dir;
+
+	$node->safe_psql('postgres', "ALTER SYSTEM SET shared_buffers = '$target'");
+	$node->safe_psql('postgres', "SELECT pg_reload_conf()");
+	is($node->safe_psql('postgres', "SELECT pg_resize_shared_buffers()"),
+		't',
+		"$name to $target succeeds with syslogger running");
+	ok(check_syslogger_running("syslogger_marker_after_$name"),
+		"syslogger drains logs after $name");
+}
+
+done_testing();
diff --git a/src/test/meson.build b/src/test/meson.build
index cd45cbf57fb..e9550933063 100644
--- a/src/test/meson.build
+++ b/src/test/meson.build
@@ -4,6 +4,7 @@ subdir('regress')
 subdir('isolation')
 
 subdir('authentication')
+subdir('buffermgr')
 subdir('postmaster')
 subdir('recovery')
 subdir('subscription')
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index 699334320d9..e02c5314f4b 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -61,6 +61,7 @@ use Config;
 use IPC::Run;
 use PostgreSQL::Test::Utils qw(pump_until);
 use Test::More;
+use Time::HiRes                      qw(usleep);
 
 =pod
 
@@ -403,4 +404,79 @@ sub set_query_timer_restart
 	return $self->{query_timer_restart};
 }
 
+=pod
+
+=item $session->poll_query_until($query [, $expected ])
+
+Run B<$query> repeatedly in this background session, until it returns the
+B<$expected> result ('t', or SQL boolean true, by default).
+Continues polling if the query returns an error result.
+Times out after a reasonable number of attempts.
+Returns 1 if successful, 0 if timed out.
+
+=cut
+
+sub poll_query_until
+{
+	my ($self, $query, $expected, %params) = @_;
+
+	$expected = 't' unless defined($expected);    # default value
+
+	my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default;
+	my $attempts = 0;
+	my ($stdout, $stderr_flag);
+
+	while ($attempts < $max_attempts)
+	{
+		($stdout, $stderr_flag) = $self->query($query, %params);
+
+		chomp($stdout);
+
+		# If query succeeded and returned expected result
+		if (!$stderr_flag && $stdout eq $expected)
+		{
+			return 1;
+		}
+
+		# Wait 0.1 second before retrying.
+		usleep(100_000);
+
+		$attempts++;
+	}
+
+	# Give up. Print the output from the last attempt, hopefully that's useful
+	# for debugging.
+	my $stderr_output = $stderr_flag ? $self->{stderr} : '';
+	diag qq(poll_query_until timed out executing this query:
+$query
+expecting this output:
+$expected
+last actual query output:
+$stdout
+with stderr:
+$stderr_output);
+	return 0;
+}
+
+=item $session->wait_for_event(backend_type, wait_event_name)
+
+Poll pg_stat_activity until backend_type reaches wait_event_name using this
+background session.
+
+=cut
+
+sub wait_for_event
+{
+	my ($self, $backend_type, $wait_event_name, %params) = @_;
+
+	$self->poll_query_until(qq[
+		SELECT count(*) > 0 FROM pg_stat_activity
+		WHERE backend_type = '$backend_type' AND wait_event = '$wait_event_name'
+	], undef, %params)
+	  or die
+	  qq(timed out when waiting for $backend_type to reach wait event '$wait_event_name');
+
+	return;
+}
+
 1;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 65f0fa4d5cb..c0323d7f170 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -359,6 +359,7 @@ BufferAccessStrategy
 BufferAccessStrategyType
 BufferCacheOsPagesContext
 BufferCacheOsPagesRec
+BufferControlBlock
 BufferDesc
 BufferDescPadded
 BufferHeapTupleTableSlot
-- 
2.34.1

