Subject: Re: A stack allocation API

From: Tom Lane <[email-masked]>
Date: Fri, 27 Feb 2026 10:35:39 -0500

Thomas Munro <[email-masked]> writes:
> In the locale code we often use a 1KB array for copies of strings
> where we need a NUL-terminated or transcoded version to give a library
> function, with a fallback to palloc() + pfree() if we need more space
> than that, but:

Yeah, I think there are some other use-cases too.

> I also wondered if we might have a reasonable case for using alloca(),
> where available.  It's pretty much the thing we are emulating, but
> keeps the stack nice and compact without big holes to step over for
> the following call to strcoll_l() or whatever it might be.

+1 for investigating alloca().  The one disadvantage I can see
to making this coding pattern more common is that it'll result in
increased stack usage, which is not great now and will become
considerably less great in our hypothetical multithreaded future.
If we can fix it so the typical stack consumption is a good deal
less than 1KB, that worry would be alleviated.

			regards, tom lane

---

From: Andres Freund <[email-masked]>
Date: Fri, 27 Feb 2026 13:02:30 -0500

Hi,

> considerably less great in our hypothetical multithreaded future.

Yea, that's what I immediately was thinking about too. IIRC, on linux, the
stack for the "main" thread is allocated on-demand, but the stack for threads
is mapped entirely upon creation (I think because it'd be hard to ensure
there's space for the stack otherwise). So there's more benefit in keeping the
stack small-ish with threads than there is in a process based model.

That said, I've thought about accellerating a few things with an
'on-stack-if-small-palloc-otherwise' approach as well.  Particularly things
like small StringInfos could really benefit from it - but it'd be a nontrivial
conversion, due code calling pfree on the memory.  I guess we could introduce
a memory context that'd do nothing for pfree(), which could be used when using
the stack version, but IDK, that seems mighty ugly.

However, I'm pretty unconvinced of this argument

> in the past we've forgotten to pfree() large allocations and had to fix leaks

because we'll continue to rely on calling something to free anyway (due to
large objects) and using a different path for smaller objects just will make
it harder to find those.

I wish msvc implemented something akin to gcc/clang's
attribute(cleanup(cleanup_function)), but it doesn't look like it
does. Obviously it would if we were to compile with C++, but I don't think
anybody has appetite for the work it'd need to get there.

Greetings,

Andres Freund

---

From: Thomas Munro <[email-masked]>
Date: Wed, 11 Mar 2026 13:05:22 +1300

> anybody has appetite for the work it'd need to get there.

Well, it does have __try/__finally, but the future of the idea for C2y
looks like Go or Zig:

    #include <stddefer.h>

    p = malloc();
    defer free(p);

It's already available in bleeding edge GCC and Clang 22, based on the
TS 25755 draft[1].  Unfortunately for the real world, it's annoyingly
difficult to come up with *nice* looking macros that can expand to the
dialects we'd need to cover even if we were OK with abandoning the
notion of supporting C < 2y compilers other than the 3 we talk about,
due to syntactic structure and semantic differences, presumably:

1. C2y defer
2. C++ RAII
3. GCC/Clang cleanup attributes
4. MSVC __try/__finally

But with big honking framing macros BEGIN_MAGIC_PIXIE_DUST(), ...
pg_defer_pfree(p) (assuming we'd want a style with a pathway to the
future standard?); ... END_MAGIC_PIXIE_DUST(), I think it's probably
doable...  There are lots of people trying to suffer through
portable-enough-for-me and just-enough-functionality-for-my-project,
maybe some inspiration[2]...

[1] https://www.open-std.org/JTC1/SC22/WG14/www/docs/n3734.pdf
[2] https://antonz.org/defer-in-c/

---

From: Thomas Munro <[email-masked]>
Date: Thu, 12 Mar 2026 14:52:16 +1300

> less than 1KB, that worry would be alleviated.

Yeah.  I thought about all that quite a bit, and the dangers of
longjmp(), and various portability arcana, and came up with a v2.

Safety:

* DECLARE_STACK_BUFFER() is intended for general use, and gives you
only 128 bytes with the array implementation, enough for lots of
values/nulls arrays and such, and there is also
DECLARE_STACK_BUFFER_LARGE() for the pre-existing locales code that
already uses 1024-byte arrays.  To be really paranoid, I suppose we
could instead always use palloc() if you don't have alloca() support,
except when you ask for _LARGE, but I suspect 128 might be OK: it's in
the territory of plain-old-stack-frame-with-a-few-variables.  But I
don't think it matters terribly much either way, as no modern system
would really use array mode.

* If using using alloca(), as far as I can see we don't have to be so
cautious *if* we additionally impose a total stack size limit.  Here I
chose halfway to check_stack_depth()'s trigger point, but much less
than that would also be fine I think.  You can see what that compiles
to at the end.  Thoughts?

* I came up with a way to implement
static_assert(!pg_in_lexical_scope(PG_{TRY,CATCH,FINALLY})).
setlongjmp()ing around inside the same stack frame obviously breaks
this stuff fundamentally.  I think it might be possible that some very
restricted patterns could made to work: if you allocated before (but
not inside) PG_TRY, either using volatile variables (or perhaps if
PG_TRY contained a compiler barrier?) then it freeing might be OK in
PG_CATCH, for example if plpy_exec.c moves its allocations up.  I'm
not too sure about that, though, and for now all stack buffer
operations are banned in all three scopes.  Better implementation
techniques for the resulting caveman meta-programming exercise are
welcome... it took me quite some time to make it out of the valley of
shadow warnings.

Portability:

* There are no general purpose systems left that have stacks that grow
upwards AFAICS, right?  So I propose that we define PG_STACK_DIRECTION
to -1 in pg_config_manual.h, but continue to write code that should
work correctly if you set it to +1.  That simplifies the macrology and
de-branchifies the code.  It doesn't make sense to tolerate
non-existent computers by running zillions of runtime tests around the
world every day.

* I guess I should probably actually test it on PA-RISC (the last
stack-grows-up system?) under Qemu or something.  The way I approached
this was to write expressions in terms of PG_STACK_DIRECTION, instead
of chunks of #ifdef'd code that would never be seen by a living
compiler and thus inevitably bit-rot.  We could still assert that
reality matches expectations in a few spots, and then worry about
configure-time, #ifdef or template file control if a new platform ever
appears with an upward stack.

* PostgreSQL already wouldn't work with "split stack" code (it would
bogusly error out of check_stack_depth() when it meets a far away
stack segment), or other exotic systems like spaghetti stacks.  I
don't think this is much of an additional jump in our assumptions
about how C is actually implemented, it's just assuming that there are
no systems that grow down one day and up the next.  (A number of CPU
architectures could technically do that, but their ABI firmly nails
down which way it has to be, and the answer is down).

* For estimating whether there is enough free space to call alloca(),
it prefers to use builtins to read the stack pointer (GCC, Clang 22+),
but failing that it wastes a register that initially holds a
conservative estimate (address of a local variable, itself, which is
surely deeper-or-equal to the real stack pointer), and later holds the
result of the last alloca() call, which is 100% accurate on
downward-stack systems, and potentially only out by a small amount of
alignment fuzz on non-existent upward-stack systems because I assumed
that new sp = alloca(size) + size.  (It would be more accurate if I
did TYPALIGN(X, size), but I don't know what its X is, I only know
that it's >= alignof(max_align_t), so I'd have to go and read the
relevant ABI manual for this hypothetical upward-stack system to find
out, but since it doesn't exist, all thoughts about a perfect
estimation disappear in a puff of logic.)

* When judging if a pointer came from the stack, it additionally needs
an least-deep bound, but if there is no builtin for the current stack
frame address then it uses check_stack_depth.c's stack_base_ptr.  Both
are reliable, all we need is a pointer to anywhere less deep in the
stack than anything we've alloca()'d.  The builtin might technically
give you something from a function you're inlined into, but that's OK
too.  In practice probably only MSVC uses stack_base_ptr.

* It only uses literal "alloca()" for MSVC, which turns into a builtin
and thus has a property that I'm relying on.  For Unixen it uses a
GCC/Clang builtin directly, because otherwise I'd have to worry about
that property too.  Builtins are safe in a function call argument
because the compiler implements and understands them, but the original
alloca() from old Unixen somehow moved the stack pointer without the
compiler's knowledge, which used to trash the stack if it had already
started pushing other stuff onto the stack for the function call, at
least in some circumstances.  I assume no such things exist now, but
you'd still have to figure out which header to include, and that
varies on niche systems (<alloca.h>, <stddef.h>, ...?).

* For builtin detection, I ran into CC vs CXX vs CLANG problems, so I
gave up and invented pg_has_builtin().  I don't propose it be used
except where needed though since it doesn't exist on GCC < 10 (it's an
idea from Clang).  This means that old build farm compilers would use
the array version, which I think is probably desirable for coverage.
Current distros are shipping GCC 11+, probably mostly 14 or so, so I
think it's OK to exclude 10 from a minor optimisation project.

Potential sites that can use this:

I have attached a bunch of easy candidate changes, almost all
mechanical changes for small temporary arrays of values/nulls,
scankeys, etc or temporary C-strings where no refactoring was needed
and the no-escape property was obvious to the eye.  In the case of the
planner, I worked a bit harder to find a couple of things that have
pass-down-but-never-capture semantics, based on hunches...  FWIW I do
see a small planner speed-up in the join-order-optimisation test David
mentioned the other day in simple tests, though I haven't studied it
seriously enough to report on yet.  I'm actually not too sure yet how
to evaluate this stuff at a higher level, ie the effects of tiny
micro-optimisations scattered all over the tree, and only in the
arbitrary places that didn't need refactoring.  So for now I'm
reporting only on a toy C-stringification microbenchmark: see end.

Ideas for further development:

* I wanted this to be useful for SIMD, so I made sure that
stack_buffer_alloc_object(T) would handle strict alignment
automatically based on alignof(T).  palloc_object(T) et al should get
the same treatment, as I wrote in another thread about the funky __128
situation (and I have basic patches, for another day).

* I see how to implement _realloc(): if ptr == sp, then you have the
most recent allocation, so if you (somehow) know the original size you
can allocate the difference and memmove().  (This condition would
never be met for stack-upwards, but that's OK).  In all other cases
you'd probably fall straight back to palloc() I think.  I don't know
if there'd be much call for it though.

* Presumably this could benefit from a sprinkle of VALGRIND and sanitizer clues.

* It would be nice to figure out how to make at least one compiler
complain about very obvious escapes, as they do if you return a
pointer to a normal stack variable, but I guess the main problem isn't
the obvious ones.

* Many uses of temporary C strings wouldn't be needed if we accepted
known-length strings in more APIs; it may be unavoidable for libc
functions, but our own reader etc should probably ideally be able to
cope... I have a separate project investigating a centralised string
iterator so that we can make all of our string processing behave the
same way and will think about that...

* There are quite a few places that form or copy temporary
non-escaping index tuples, tuples, tupledescs, values/nulls extracted
from arrays etc that could probably live on the stack quite happily if
we split a few functions into "how much do you need?" and "in place"
pairs.  That's been done in a few ad hoc spots for parallel query eg
tuple descriptors.

* Independently of this project, but really the same sort of thing, I
have long wanted to look into all the places that form a tuple, copy
it and then free it (hashing, sorting, materialising) when they should
form into caller-supplied destination, be it the stack, a hash table,
a tuple queue or whatever else...

* Something like "defer" for magic automatic pfree() on scope exit if
we can figure out how to do it without waiting for the year 202y + 10,
as mentioned in reply to Andres...

* Even without that, I suppose GCC cleanup callbacks could still be
used to scribble on the memory that goes out of scope.  I guess it
must be pretty hard to miss bugs anyway since the stack is already
scribbled to smithereens.

For a very simple micro-benchmark, I tried a trivial C-string
conversion wrapper:

   for (int i = 0; i < 10000000; ++i)
       my_strlen("hello world", 11);

Given this definition of a completely useless function that exercises
C-string construction:

size_t
my_strlen(const char *data, size_t size)
{
    char *cstr;
    size_t result;

    DECLARE_STACK_BUFFER();

    cstr = stack_buffer_strdup_with_len(data, size);
    result = strlen(cstr);
    stack_buffer_free(cstr);

    return result;
}

With STACK_BUFFER_USE_PALLOC, which just expands those macros to
palloc()/pfree(), it runs in ~216ms here, and with
STACK_BUFFER_USE_ALLOCA or STACK_BUFFER_USE_ARRAY it runs in ~155ms,
so that's a 1.39x speedup.

The generated code looks like:

   0x0000000000979323 <+19>:    mov    0x442a06(%rip),%rdx        #
0xdbbd30 <stack_soft_limit_ptr>
   0x000000000097932a <+26>:    lea    -0x400(%rsp),%rax          <--
sp - STACK_BUFFER_DEFAULT
   0x0000000000979332 <+34>:    cmp    %rdx,%rax
   0x0000000000979335 <+37>:    cmovb  %rdx,%rax                  <--
take the nearer address

%rax now holds the threshold stack address against which all
allocation attempts will be compared.  Uninteresting computation of
-(size + 1) for NUL byte:

   0x0000000000979339 <+41>:    mov    %rsi,%rdx
   0x000000000097933c <+44>:    not    %rdx

Then allocation attempt:

   0x000000000097933f <+47>:    add    %rsp,%rdx
   0x0000000000979342 <+50>:    cmp    %rdx,%rax                 <--
would it fit?
   0x0000000000979345 <+53>:    jae    0x979380 <my_strlen+112>  <--
nope, jump to slow path
   0x0000000000979347 <+55>:    lea    0x10(%rsi),%rax
   0x000000000097934b <+59>:    mov    %rsi,%rdx
   0x000000000097934e <+62>:    mov    %rdi,%rsi
   0x0000000000979351 <+65>:    and    $0xfffffffffffffff0,%rax  <--
alloca() aligns
   0x0000000000979355 <+69>:    sub    %rax,%rsp                 <--
alloca() allocates

Then it's then a straight line run through the uninteresting bits to
return the result, with stack_buffer_free() entirely elided:

   0x0000000000979358 <+72>:    mov    %rsp,%rdi
   0x000000000097935b <+75>:    call   0x4950d0 <memcpy@plt>
   0x0000000000979360 <+80>:    movb   $0x0,(%rsp,%rbx,1)
   0x0000000000979364 <+84>:    mov    %rsp,%rdi
   0x0000000000979367 <+87>:    call   0x494890 <strlen@plt>
   0x000000000097936c <+92>:    lea    -0x18(%rbp),%rsp
   0x0000000000979370 <+96>:    pop    %rbx
   0x0000000000979371 <+97>:    pop    %r12
   0x0000000000979373 <+99>:    pop    %r13
   0x0000000000979375 <+101>:   pop    %rbp
   0x0000000000979376 <+102>:   ret

If it wouldn't fit, the out-of-line implementation looks like what you
get with STACK_BUFFER_USE_PALLOC:

   0x0000000000979377 <+103>:   nopw   0x0(%rax,%rax,1)
   0x0000000000979380 <+112>:   lea    0x1(%rsi),%rdi
   0x0000000000979384 <+116>:   call   0xa252f0 <palloc>
   0x0000000000979389 <+121>:   mov    %rbx,%rdx
   0x000000000097938c <+124>:   mov    %r12,%rsi
   0x000000000097938f <+127>:   mov    %rax,%r13
   0x0000000000979392 <+130>:   mov    %rax,%rdi
   0x0000000000979395 <+133>:   call   0x4950d0 <memcpy@plt>
   0x000000000097939a <+138>:   movb   $0x0,0x0(%r13,%rbx,1)
   0x00000000009793a0 <+144>:   mov    %r13,%rdi
   0x00000000009793a3 <+147>:   call   0x494890 <strlen@plt>
   0x00000000009793a8 <+152>:   cmp    %rsp,%r13
   0x00000000009793ab <+155>:   jb     0x9793b2 <my_strlen+162>
   0x00000000009793ad <+157>:   cmp    %r13,%rbp
   0x00000000009793b0 <+160>:   jae    0x97936c <my_strlen+92>
   0x00000000009793b2 <+162>:   mov    %r13,%rdi
   0x00000000009793b5 <+165>:   mov    %rax,-0x28(%rbp)
   0x00000000009793b9 <+169>:   call   0xa255c0 <pfree>
   0x00000000009793be <+174>:   mov    -0x28(%rbp),%rax
   0x00000000009793c2 <+178>:   jmp    0x97936c <my_strlen+92>

In more complicated functions GCC's costing only moves the actual
palloc() and pfree() calls out-of-line and jumps back to the
straight-line code, while here it decided it was cheaper to duplicate
the memcpy(), strlen() calls.  Either way the straight-line code
assumes the stack will be used, which is hopefully correct most of the
time.

It became a lot more aggressive about that sort of thing when I added
the "stack_buffer_maybe_pfree" flag which seemed to flip GCC's
costing; before that it would often leave the
if-it's-not-between-%rbp-and-%rsp-then-call-pfree code in the main
code.  I'm not 100% sure about all that, but it *looks* like an
effective optimisation.

I've attached some data on allocation sizes from the regression tests
(so not hitting all the places changed here), captured with
STACK_BUFFER_USE_PALLOC_LOG.  Obviously not representative of real
usage, but that technique can be used to check real allocation sizes
and stack depths for any workload.

. o O ( I originally called it stack_buffer because I started with
"let's standardise the array trick", so it was a buffer.  Perhaps a
better name for all this stuff would be pg_stack_alloc() or such,
since it doesn't really have a buffer anymore except in a fallback
mode... )
From 7573e40d57e15abca8a29f9907d29f2d4a97c119 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Wed, 4 Mar 2026 18:12:54 +1300
Subject: [PATCH v2 01/19] Provide PG_STACK_DIRECTION configuration macro.

General purpose systems capable of running PostgreSQL with stacks that
grow upwards rather down downwards don't seem to be found in the wild
these days.

Define a pg_config_manual.h variable that can be set to -1 or 1, to
remove runtime branching from stack-measuring code.  We could always add
feature detection if a new system shows up that wants the other setting,
but runtime doesn't make sense for a fixed properly of a system.

Though the existing user of that fact is very simple and doesn't save
much effort at all, a proposed patch would also like to know this at
compile time.
---
 src/backend/utils/misc/stack_depth.c | 10 ++++++++--
 src/include/pg_config_manual.h       |  8 ++++++++
 2 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/src/backend/utils/misc/stack_depth.c b/src/backend/utils/misc/stack_depth.c
index 61a07cf824e..3cd1b6f2336 100644
--- a/src/backend/utils/misc/stack_depth.c
+++ b/src/backend/utils/misc/stack_depth.c
@@ -118,9 +118,15 @@ stack_is_too_deep(void)
 
 	/*
 	 * Take abs value, since stacks grow up on some machines, down on others
+	 * (historical).  This formulation amounts to a no-op on modern systems.
 	 */
-	if (stack_depth < 0)
-		stack_depth = -stack_depth;
+	stack_depth *= -(PG_STACK_DIRECTION);
+
+	/*
+	 * If this assertion fails, either PG_STACK_DIRECTION is wrong or this
+	 * system doesn't have a traditional stack.
+	 */
+	Assert(stack_depth >= 0);
 
 	/*
 	 * Trouble?
diff --git a/src/include/pg_config_manual.h b/src/include/pg_config_manual.h
index 521b49b8888..75035b9e796 100644
--- a/src/include/pg_config_manual.h
+++ b/src/include/pg_config_manual.h
@@ -222,6 +222,14 @@
  */
 #define PG_IO_ALIGN_SIZE		4096
 
+/*
+ * Historical systems had stacks that grew upwards in address space, but none
+ * of them remain.  Our stack depth-checking code no longer wastes cycles
+ * considering that possibility at runtime.  It assumes (but also asserts)
+ * that the stack grows in the direction defined here: -1 or +1.
+ */
+#define PG_STACK_DIRECTION -1
+
 /*
  *------------------------------------------------------------------------
  * The following symbols are for enabling debugging code, not for
-- 
2.53.0

From 2de6675b66388341ba9d71bcaa47c47e87f667b9 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Wed, 11 Mar 2026 13:26:22 +1300
Subject: [PATCH v2 02/19] Provide pg_has_builtin() macro.

The traditional approach of configure/meson problems that define
HAVE__BUILTIN_FOO doesn't work for results that vary across the CC, CXX
and CLANG compilers.  In Clang since early version, and in GCC 10+, you
can test with __has_builtin(__builtin_foo) instead.  This wrapper can be
tested in preprocessor conditions using pg_has_builtin(__builtin_foo).

This approach has a bootstrapping problem, since __has_builtin() itself
was adopted only ~6 years ago by GCC, so it will always evaluate to
false for older GCC versions.  Traditional configure probes therefore
remain useful for well established builtins where the three selected
compilers are unlikely to differ.  This macro is useful for builtins
that were more recently invented, or adopted late by one or the other of
GCC and Clang.
---
 src/include/c.h | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/include/c.h b/src/include/c.h
index 5b678283469..5c74da5004e 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -116,6 +116,16 @@ extern "C++"
 #define inline
 #endif
 
+/*
+ * Clang and GCC 10+ can test for a builtin's presence.  This can be useful
+ * for builtins that might be present for CC but not CXX or CLANG.
+ */
+#ifdef __has_builtin
+#define pg_has_builtin(x) __has_builtin(x)
+#else
+#define pg_has_builtin(x) 0
+#endif
+
 /*
  * Attribute macros
  *
-- 
2.53.0

From 3a1db8f04f661ae2cb93b2fecf6b02277fe01fd3 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Fri, 6 Mar 2026 20:53:23 +1300
Subject: [PATCH v2 03/19] Refactor check_stack_depth() mechanism.

* The check is now inline.
* The stack is now assumed to grow in PG_STACK_DIRECTION.
* A new "soft" limit is exposed, for a later patch to use.
---
 src/backend/utils/misc/stack_depth.c | 113 +++++++++------------------
 src/include/miscadmin.h              |  53 ++++++++++++-
 2 files changed, 90 insertions(+), 76 deletions(-)

diff --git a/src/backend/utils/misc/stack_depth.c b/src/backend/utils/misc/stack_depth.c
index 3cd1b6f2336..aea589e1a27 100644
--- a/src/backend/utils/misc/stack_depth.c
+++ b/src/backend/utils/misc/stack_depth.c
@@ -25,15 +25,28 @@
 /* GUC variable for maximum stack depth (measured in kilobytes) */
 int			max_stack_depth = 100;
 
-/* max_stack_depth converted to bytes for speed of checking */
-static ssize_t max_stack_depth_bytes = 100 * (ssize_t) 1024;
-
 /*
- * Stack base pointer -- initialized by set_stack_base(), which
- * should be called from main().
+ * Thresholds -- initialized by set_stack_base().  These have external linkage
+ * so they can be used by inlined code.
  */
-static char *stack_base_ptr = NULL;
+const void *stack_base_ptr = NULL;
+const void *stack_soft_limit_ptr = NULL;
+const void *stack_hard_limit_ptr = NULL;
+
+
+static void
+compute_limit_ptrs(int kb)
+{
+	ssize_t		bytes = kb * (ssize_t) 1024;
+
+	/* Advertise a soft limit halfway through the allowed size. */
+	stack_soft_limit_ptr = (const char *) stack_base_ptr +
+		(bytes / 2) * PG_STACK_DIRECTION;
 
+	/* This is the size at which check_stack_depth() will fail. */
+	stack_hard_limit_ptr = (const char *) stack_base_ptr +
+		bytes * PG_STACK_DIRECTION;
+}
 
 /*
  * set_stack_base: set up reference point for stack depth checking
@@ -48,7 +61,7 @@ set_stack_base(void)
 #endif
 	pg_stack_base_t old;
 
-	old = stack_base_ptr;
+	old = (pg_stack_base_t) stack_base_ptr;
 
 	/*
 	 * Set up reference point for stack depth checking.  On recent gcc we use
@@ -61,6 +74,8 @@ set_stack_base(void)
 	stack_base_ptr = &stack_base;
 #endif
 
+	compute_limit_ptrs(max_stack_depth);
+
 	return old;
 }
 
@@ -76,73 +91,11 @@ set_stack_base(void)
 void
 restore_stack_base(pg_stack_base_t base)
 {
-	stack_base_ptr = base;
-}
-
-
-/*
- * check_stack_depth/stack_is_too_deep: check for excessively deep recursion
- *
- * This should be called someplace in any recursive routine that might possibly
- * recurse deep enough to overflow the stack.  Most Unixen treat stack
- * overflow as an unrecoverable SIGSEGV, so we want to error out ourselves
- * before hitting the hardware limit.
- *
- * check_stack_depth() just throws an error summarily.  stack_is_too_deep()
- * can be used by code that wants to handle the error condition itself.
- */
-void
-check_stack_depth(void)
-{
-	if (stack_is_too_deep())
-	{
-		ereport(ERROR,
-				(errcode(ERRCODE_STATEMENT_TOO_COMPLEX),
-				 errmsg("stack depth limit exceeded"),
-				 errhint("Increase the configuration parameter \"max_stack_depth\" (currently %dkB), "
-						 "after ensuring the platform's stack depth limit is adequate.",
-						 max_stack_depth)));
-	}
-}
-
-bool
-stack_is_too_deep(void)
-{
-	char		stack_top_loc;
-	ssize_t		stack_depth;
-
-	/*
-	 * Compute distance from reference point to my local variables
-	 */
-	stack_depth = (ssize_t) (stack_base_ptr - &stack_top_loc);
+	stack_base_ptr = (const void *) base;
 
-	/*
-	 * Take abs value, since stacks grow up on some machines, down on others
-	 * (historical).  This formulation amounts to a no-op on modern systems.
-	 */
-	stack_depth *= -(PG_STACK_DIRECTION);
-
-	/*
-	 * If this assertion fails, either PG_STACK_DIRECTION is wrong or this
-	 * system doesn't have a traditional stack.
-	 */
-	Assert(stack_depth >= 0);
-
-	/*
-	 * Trouble?
-	 *
-	 * The test on stack_base_ptr prevents us from erroring out if called
-	 * before that's been set.  Logically it should be done first, but putting
-	 * it last avoids wasting cycles during normal cases.
-	 */
-	if (stack_depth > max_stack_depth_bytes &&
-		stack_base_ptr != NULL)
-		return true;
-
-	return false;
+	compute_limit_ptrs(max_stack_depth);
 }
 
-
 /* GUC check hook for max_stack_depth */
 bool
 check_max_stack_depth(int *newval, void **extra, GucSource source)
@@ -160,13 +113,25 @@ check_max_stack_depth(int *newval, void **extra, GucSource source)
 	return true;
 }
 
+/*
+ * Out-of-line part of check_stack_depth().
+ */
+void
+report_stack_is_too_deep(void)
+{
+	ereport(ERROR,
+			(errcode(ERRCODE_STATEMENT_TOO_COMPLEX),
+			 errmsg("stack depth limit exceeded"),
+			 errhint("Increase the configuration parameter \"max_stack_depth\" (currently %dkB), "
+					 "after ensuring the platform's stack depth limit is adequate.",
+					 max_stack_depth)));
+}
+
 /* GUC assign hook for max_stack_depth */
 void
 assign_max_stack_depth(int newval, void *extra)
 {
-	ssize_t		newval_bytes = newval * (ssize_t) 1024;
-
-	max_stack_depth_bytes = newval_bytes;
+	compute_limit_ptrs(newval);
 }
 
 /*
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index f16f35659b9..c618804e71f 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -293,15 +293,64 @@ extern PGDLLIMPORT bool VacuumCostActive;
 
 extern PGDLLIMPORT int max_stack_depth;
 
+extern PGDLLIMPORT const void *stack_base_ptr;
+extern PGDLLIMPORT const void *stack_soft_limit_ptr;
+extern PGDLLIMPORT const void *stack_hard_limit_ptr;
+
 /* Required daylight between max_stack_depth and the kernel limit, in bytes */
 #define STACK_DEPTH_SLOP (512 * 1024)
 
+/* Check if stack pointer p1 is deeper than p2. */
+static inline bool
+stack_ptr_deeper_p(const void *p1, const void *p2)
+{
+	const char *c1 = (const char *) p1;
+	const char *c2 = (const char *) p2;
+
+	if (PG_STACK_DIRECTION < 0)
+		return c1 < c2;
+	else
+		return c1 > c2;
+}
+
+/* Check if the stack has exceeded the configured hard limit. */
+static inline bool
+stack_is_too_deep(void)
+{
+#if pg_has_builtin(__builtin_stack_address)
+	const char *sp = (const char *) __builtin_stack_address();
+#else
+	char		c;
+	const char *sp = &c;
+#endif
+
+	return stack_ptr_deeper_p(sp, stack_hard_limit_ptr);
+}
+
+extern void report_stack_is_too_deep(void);
+
+/*
+ * check_stack_depth/stack_is_too_deep: check for excessively deep recursion
+ *
+ * This should be called someplace in any recursive routine that might possibly
+ * recurse deep enough to overflow the stack.  Most Unixen treat stack
+ * overflow as an unrecoverable SIGSEGV, so we want to error out ourselves
+ * before hitting the hardware limit.
+ *
+ * check_stack_depth() just throws an error summarily.  stack_is_too_deep()
+ * can be used by code that wants to handle the error condition itself.
+ */
+static inline void
+check_stack_depth(void)
+{
+	if (unlikely(stack_is_too_deep()))
+		report_stack_is_too_deep();
+}
+
 typedef char *pg_stack_base_t;
 
 extern pg_stack_base_t set_stack_base(void);
 extern void restore_stack_base(pg_stack_base_t base);
-extern void check_stack_depth(void);
-extern bool stack_is_too_deep(void);
 extern ssize_t get_stack_depth_rlimit(void);
 
 /* in tcop/utility.c */
-- 
2.53.0

From e09e2b0f12df3eaa55f37db55d0d00d12a92bf8a Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Wed, 4 Mar 2026 17:30:46 +1300
Subject: [PATCH v2 04/19] Provide way for macros to detect PG_TRY scope.

For code that can't work safely around setjmp()/longjmp() and thus in a
PG_TRY/CATCH/FINALLY block, provide a macro
"pg_in_lexical_scope(PG_TRY)" that can be tested at compile time.
---
 configure                  | 101 +++++++++++++++++++++++++++++++++++++
 configure.ac               |  13 +++++
 meson.build                |   9 ++++
 src/include/c.h            |  21 ++++++++
 src/include/pg_config.h.in |   6 +++
 src/include/utils/elog.h   |  36 +++++++++++++
 6 files changed, 186 insertions(+)

diff --git a/configure b/configure
index 42621ecd051..5b3a9975e9e 100755
--- a/configure
+++ b/configure
@@ -5992,6 +5992,107 @@ if test x"$pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local" = x"yes"; then
 fi

+  # We also want to make the usage of the above options available to macros.
+  NOT_THE_CFLAGS=""
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wshadow=compatible-local, for NOT_THE_CFLAGS" >&5
+$as_echo_n "checking whether ${CC} supports -Wshadow=compatible-local, for NOT_THE_CFLAGS... " >&6; }
+if ${pgac_cv_prog_CC_cflags__Wshadow_compatible_local+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  pgac_save_CFLAGS=$CFLAGS
+pgac_save_CC=$CC
+CC=${CC}
+CFLAGS="${NOT_THE_CFLAGS} -Wshadow=compatible-local"
+ac_save_c_werror_flag=$ac_c_werror_flag
+ac_c_werror_flag=yes
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main ()
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_compile "$LINENO"; then :
+  pgac_cv_prog_CC_cflags__Wshadow_compatible_local=yes
+else
+  pgac_cv_prog_CC_cflags__Wshadow_compatible_local=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+ac_c_werror_flag=$ac_save_c_werror_flag
+CFLAGS="$pgac_save_CFLAGS"
+CC="$pgac_save_CC"
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CC_cflags__Wshadow_compatible_local" >&5
+$as_echo "$pgac_cv_prog_CC_cflags__Wshadow_compatible_local" >&6; }
+if test x"$pgac_cv_prog_CC_cflags__Wshadow_compatible_local" = x"yes"; then
+  NOT_THE_CFLAGS="${NOT_THE_CFLAGS} -Wshadow=compatible-local"
+fi
+
+  if test -n "$NOT_THE_CFLAGS"; then
+
+$as_echo "#define WARNING_CC_SHADOW_COMPATIBLE_LOCAL 1" >>confdefs.h
+
+  fi
+  NOT_THE_CFLAGS=""
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CXX} supports -Wshadow=compatible-local, for NOT_THE_CFLAGS" >&5
+$as_echo_n "checking whether ${CXX} supports -Wshadow=compatible-local, for NOT_THE_CFLAGS... " >&6; }
+if ${pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  pgac_save_CXXFLAGS=$CXXFLAGS
+pgac_save_CXX=$CXX
+CXX=${CXX}
+CXXFLAGS="${NOT_THE_CFLAGS} -Wshadow=compatible-local"
+ac_save_cxx_werror_flag=$ac_cxx_werror_flag
+ac_cxx_werror_flag=yes
+ac_ext=cpp
+ac_cpp='$CXXCPP $CPPFLAGS'
+ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_cxx_compiler_gnu
+
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+int
+main ()
+{
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_cxx_try_compile "$LINENO"; then :
+  pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local=yes
+else
+  pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local=no
+fi
+rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext
+ac_ext=c
+ac_cpp='$CPP $CPPFLAGS'
+ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5'
+ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5'
+ac_compiler_gnu=$ac_cv_c_compiler_gnu
+
+ac_cxx_werror_flag=$ac_save_cxx_werror_flag
+CXXFLAGS="$pgac_save_CXXFLAGS"
+CXX="$pgac_save_CXX"
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local" >&5
+$as_echo "$pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local" >&6; }
+if test x"$pgac_cv_prog_CXX_cxxflags__Wshadow_compatible_local" = x"yes"; then
+  NOT_THE_CFLAGS="${NOT_THE_CFLAGS} -Wshadow=compatible-local"
+fi
+
+  if test -n "$NOT_THE_CFLAGS"; then
+
+$as_echo "#define WARNING_CXX_SHADOW_COMPATIBLE_LOCAL 1" >>confdefs.h
+
+  fi
   # This was included in -Wall/-Wformat in older GCC versions
 
 { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether ${CC} supports -Wformat-security, for CFLAGS" >&5
diff --git a/configure.ac b/configure.ac
index 61ec895d23c..de02fdcf058 100644
--- a/configure.ac
+++ b/configure.ac
@@ -577,6 +577,19 @@ if test "$GCC" = yes -a "$ICC" = no; then
   PGAC_PROG_CXX_CFLAGS_OPT([-Wcast-function-type])
   PGAC_PROG_CC_CFLAGS_OPT([-Wshadow=compatible-local])
   PGAC_PROG_CXX_CFLAGS_OPT([-Wshadow=compatible-local])
+  # We also want to make the usage of the above options available to macros.
+  NOT_THE_CFLAGS=""
+  PGAC_PROG_VARCC_VARFLAGS_OPT(CC, NOT_THE_CFLAGS, [-Wshadow=compatible-local])
+  if test -n "$NOT_THE_CFLAGS"; then
+    AC_DEFINE([WARNING_CC_SHADOW_COMPATIBLE_LOCAL], 1,
+              [Define to 1 if your C compiler understands -Wshadow=compatible-local])
+  fi
+  NOT_THE_CFLAGS=""
+  PGAC_PROG_VARCXX_VARFLAGS_OPT(CXX, NOT_THE_CFLAGS, [-Wshadow=compatible-local])
+  if test -n "$NOT_THE_CFLAGS"; then
+    AC_DEFINE([WARNING_CXX_SHADOW_COMPATIBLE_LOCAL], 1,
+              [Define to 1 if your C++ compiler understands -Wshadow=compatible-local])
+  fi
   # This was included in -Wall/-Wformat in older GCC versions
   PGAC_PROG_CC_CFLAGS_OPT([-Wformat-security])
   PGAC_PROG_CXX_CFLAGS_OPT([-Wformat-security])
diff --git a/meson.build b/meson.build
index 2df54409ca6..bbcc4197ccb 100644
--- a/meson.build
+++ b/meson.build
@@ -2206,6 +2206,15 @@ if have_cxx
   cxxflags_warn += cxx.get_supported_arguments(common_warning_flags)
 endif
 
+# Advertise whether -Wshadow=compatible-local is active, so that it can be
+# disabled in cases where that is expected.
+if cc.has_argument('-Wshadow=compatible-local')
+  cdata.set('WARNING_CC_SHADOW_COMPATIBLE_LOCAL', 1)
+endif
+if have_cxx and cxx.has_argument('-Wshadow=compatible-local')
+  cdata.set('WARNING_CXX_SHADOW_COMPATIBLE_LOCAL', 1)
+endif
+
 # To require fallthrough attribute annotations, use
 # -Wimplicit-fallthrough=5 with gcc and -Wimplicit-fallthrough with
 # clang.  The latter is also accepted on gcc but does not enforce
diff --git a/src/include/c.h b/src/include/c.h
index 5c74da5004e..5a524365ccc 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -559,6 +559,27 @@ typedef void (*pg_funcptr_t) (void);
 #define HAVE_PRAGMA_GCC_SYSTEM_HEADER	1
 #endif
 
+/*
+ * Sometimes it is useful to be able to disable GCC's shadow warnings for a
+ * specific declaration.
+ *
+ * -Wdeclaration-after-statement is also temporarily suppressed, because the
+ * pragma itself is treated as a statement while the purpose of these macros
+ * is to wrap a declaration.
+ */
+#if !defined(__clang__) && \
+	((defined(__cplusplus) && defined(WARNING_CXX_SHADOW_COMPATIBLE_LOCAL)) || \
+	 (!defined(__cplusplus) && defined(WARNING_CC_SHADOW_COMPATIBLE_LOCAL)))
+#define pg_begin_ignore_shadow_warning()								\
+	_Pragma("GCC diagnostic push");										\
+	_Pragma("GCC diagnostic ignored \"-Wshadow=compatible-local\"");	\
+	_Pragma("GCC diagnostic ignored \"-Wdeclaration-after-statement\"");
+#define pg_end_ignore_shadow_warning()			\
+	_Pragma("GCC diagnostic pop")
+#else
+#define pg_begin_ignore_shadow_warning()
+#define pg_end_ignore_shadow_warning()
+#endif
 
 /* ----------------------------------------------------------------
  *				Section 2:	bool, true, false
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index cb0f53fade4..c67534cd97b 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -751,6 +751,12 @@
 /* Define to 1 to build with ZSTD support. (--with-zstd) */
 #undef USE_ZSTD
 
+/* Define to 1 if your C compiler understands -Wshadow=compatible-local */
+#undef WARNING_CC_SHADOW_COMPATIBLE_LOCAL
+
+/* Define to 1 if your C++ compiler understands -Wshadow=compatible-local */
+#undef WARNING_CXX_SHADOW_COMPATIBLE_LOCAL
+
 /* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
    significant byte first (like Motorola and SPARC, unlike Intel). */
 #if defined AC_APPLE_UNIVERSAL_BUILD
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index a12b379e09a..30afcfbf453 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -316,6 +316,39 @@ typedef struct ErrorContextCallback
 extern PGDLLIMPORT ErrorContextCallback *error_context_stack;

+/* Support for testing if a macro is inside a PG_TRY/... block. */
+typedef char pg_lexical_scope_false_type;
+typedef int pg_lexical_scope_true_type;
+
+/* Declare a tag as a variable in global scope.  It has no storage. */
+#define pg_declare_lexical_scope_tag(name)			\
+	extern pg_lexical_scope_false_type \
+	lexical_scope_tag_##name pg_attribute_unused()
+
+/*
+ * Declare a tag as a local typedef that hides the name of the variable,
+ * within the current lexical scope.
+ *
+ * On its own, "shadowing" a variable name with a type name doesn't trigger
+ * GCC's -Wshadow=compatible-local warning, but we want to be able to handle
+ * nested PG_TRY blocks using the same tag name.  Disable the warning locally,
+ * if it's enabled.
+ */
+#define pg_set_lexical_scope_tag(name)									\
+	pg_begin_ignore_shadow_warning()									\
+	typedef pg_lexical_scope_true_type									\
+		lexical_scope_tag_##name pg_attribute_unused();					\
+	pg_end_ignore_shadow_warning()
+
+/* Test whether we are inside a lexical scope that has "set" the tag. */
+#define pg_in_lexical_scope_p(name)				\
+	(sizeof(lexical_scope_tag_##name) == sizeof(pg_lexical_scope_true_type))
+
+pg_declare_lexical_scope_tag(PG_TRY);
+pg_declare_lexical_scope_tag(PG_CATCH);
+pg_declare_lexical_scope_tag(PG_FINALLY);
+
+
 /*----------
  * API for catching ereport(ERROR) exits.  Use these macros like so:
  *
@@ -391,12 +424,14 @@ extern PGDLLIMPORT ErrorContextCallback *error_context_stack;
 		bool _do_rethrow##__VA_ARGS__ = false; \
 		if (sigsetjmp(_local_sigjmp_buf##__VA_ARGS__, 0) == 0) \
 		{ \
+			pg_set_lexical_scope_tag(PG_TRY); \
 			PG_exception_stack = &_local_sigjmp_buf##__VA_ARGS__
 
 #define PG_CATCH(...)	\
 		} \
 		else \
 		{ \
+			pg_set_lexical_scope_tag(PG_CATCH); \
 			PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
 			error_context_stack = _save_context_stack##__VA_ARGS__
 
@@ -405,6 +440,7 @@ extern PGDLLIMPORT ErrorContextCallback *error_context_stack;
 		else \
 			_do_rethrow##__VA_ARGS__ = true; \
 		{ \
+			pg_set_lexical_scope_tag(PG_FINALLY); \
 			PG_exception_stack = _save_exception_stack##__VA_ARGS__; \
 			error_context_stack = _save_context_stack##__VA_ARGS__
 
-- 
2.53.0

From f127592cd1b095d03a0e38f539948ee68283d5b1 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Fri, 27 Feb 2026 22:21:36 +1300
Subject: [PATCH v2 05/19] Provide stack buffer API.

Several places use a fixed-size array on the stack for non-escaping
memory of dynamic size, with fallback to palloc()/pfree().  Create a
standard API for that, and implement it with alloca() instead if
possible, while applying our traditional cap on stack growth.

Three implementations are provided:

* STACK_BUFFER_USE_ALLOCA:     use alloca() or builtins
* STACK_BUFFER_USE_ARRAY:      the traditioanl approach
* STACK_BUFFER_USE_PALLOC:     development/testing
* STACK_BUFFER_USE_PALLOC_LOG: development/testing

Later patches will adopt the API in various parts of the tree to speed
up common operations.

Reviewed-by:
Discussion:
---
 src/include/c.h                  |  10 +
 src/include/utils/stack_buffer.h | 461 +++++++++++++++++++++++++++++++
 2 files changed, 471 insertions(+)
 create mode 100644 src/include/utils/stack_buffer.h

diff --git a/src/include/c.h b/src/include/c.h
index 5a524365ccc..c1074371c40 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -923,6 +923,16 @@ typedef NameData *Name;
 /* we don't currently need wider versions of the other ALIGN macros */
 #define MAXALIGN64(LEN)			TYPEALIGN64(MAXIMUM_ALIGNOF, (LEN))
 
+/*
+ * MAXIMUM_ALIGNOF is respected by palloc() and similar.  The C standard
+ * defines max_align_t as a type to be used for similar purposes, but Visual
+ * Studio forgot to define it in <stddef.h>.  Supply the definition Clang uses
+ * on that platform, for use in contexts that refer to the standard library or
+ * compiler's behavior rather than PostgreSQL's.
+ */
+#ifdef _MSC_VER
+typedef double max_align_t;
+#endif
 
 /* ----------------------------------------------------------------
  *				Section 6:	assertions
diff --git a/src/include/utils/stack_buffer.h b/src/include/utils/stack_buffer.h
new file mode 100644
index 00000000000..e4057a452e6
--- /dev/null
+++ b/src/include/utils/stack_buffer.h
@@ -0,0 +1,461 @@
+/*-------------------------------------------------------------------------
+ *
+ * stack_buffer.h
+ *		Allocator for objects that don't escape the current lexical scope.
+ *
+ * A palloc()-like interface to alloca(), for allocating memory efficiently on
+ * the stack.  Raw alloca() is usually considered dangerous because of its
+ * inherent stack overflow risk, but this interface imposes limits on stack
+ * size and falls back to regular palloc() when they would be exceeded.
+ *
+ * Memory should still be freed explicitly with stack_buffer_free().  It is a
+ * no-op in the common case that pfree() doesn't need to be called.
+ *
+ * XXX It might be possible to use something like "defer" or equivalent
+ * compiler extensions to clean up palloc()'d memory automatically, in future
+ * work, and then stack_buffer_free() would not be necessary.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/stack_buffer.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef STACK_BUFFER_H
+#define STACK_BUFFER_H
+
+#include "utils/elog.h"
+#include "utils/palloc.h"
+#include "miscadmin.h"
+
+#include <limits.h>
+#include <unistd.h>
+
+
+/* #define STACK_BUFFER_USE_PALLOC_LOG "/tmp/stack_buffer.csv" */
+
+/* Choose which implementation to use, if not defined manually. */
+#if !defined(STACK_BUFFER_USE_ARRAY) && \
+	!defined(STACK_BUFFER_USE_ALLOC) && \
+	!defined(STACK_BUFFER_USE_PALLOC) && \
+	!defined(STACK_BUFFER_USE_PALLOC_LOG)
+#if pg_has_builtin(__builtin_alloca_with_align)
+/* Use GCC/Clang builtin with explicit alignment argument. */
+#define STACK_BUFFER_USE_ALLOCA
+#elif defined(_MSC_VER)
+/*
+ * MSVC's alloca() maps to a builtin that works in function arguments.  We can
+ * adjust for stricter-than-default alignment ourselves.
+ */
+#include <malloc.h> */
+#define STACK_BUFFER_USE_ALLOCA
+#else
+/* Traditional approach. */
+#define STACK_BUFFER_USE_ARRAY
+#endif
+#endif
+
+/*
+ * A default size of 128 bytes should be enough for many arrays of datums and
+ * null flags, without posing a danger to recursive code.
+ *
+ * If using alloca(), we don't need to be so cautious: unused capacity doesn't
+ * consume stack space, and a secondary limit is computed based on the
+ * remaining stack space.
+ */
+#define STACK_BUFFER_DEFAULT \
+	(stack_buffer_total_size_limited ? STACK_BUFFER_LARGE : 128)
+
+/*
+ * This tiny size is intended for cases that risk deep recursion, but can
+ * still often benefit from avoiding a palloc() call.
+ */
+#define STACK_BUFFER_TINY \
+	(stack_buffer_total_size_limited ? STACK_BUFFER_LARGE : 16)
+
+/*
+ * This larger size is intended only for non-recursive uses including
+ * conversions to C string format before calling standard library routines.
+ * Avoid allowing more than one buffer of this size to be active on the stack
+ * at a time.
+ */
+#define STACK_BUFFER_LARGE 1024
+
+
+/*-------------------------------------------------------------------------
+ *
+ * Public stack buffer API.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/* Declare a stack buffer of default size. */
+#define DECLARE_STACK_BUFFER() \
+	DECLARE_STACK_BUFFER_SIZE(STACK_BUFFER_DEFAULT)
+
+/* As above, using the standard "tiny" size (see notes above). */
+#define DECLARE_STACK_BUFFER_TINY() \
+	DECLARE_STACK_BUFFER_SIZE(STACK_BUFFER_TINY)
+
+/* As above, using the standard "large" size (see notes above). */
+#define DECLARE_STACK_BUFFER_LARGE() \
+	DECLARE_STACK_BUFFER_SIZE(STACK_BUFFER_LARGE)
+
+/* As above, but with a caller-specified limit on stack usage. */
+#define DECLARE_STACK_BUFFER_SIZE(size) \
+	size_t stack_buffer_let_size;		/* temp, avoids double eval */	\
+	bool stack_buffer_maybe_pfree = false;								\
+	DECLARE_STACK_BUFFER_IMPL(size)
+
+/* Allocate memory, optionally with explicit alignment. */
+#define stack_buffer_alloc(size)						\
+	stack_buffer_alloc_aligned((size), MAXIMUM_ALIGNOF)
+#define stack_buffer_alloc_aligned(size, align)							\
+	(stack_buffer_sanity_checks(),										\
+	 stack_buffer_let_size = (size),									\
+	 stack_buffer_let_size = Max(stack_buffer_let_size, 1),				\
+	 stack_buffer_alloc_aligned_impl(stack_buffer_let_size, (align)))
+
+/* As above, but also zero the memory. */
+#define stack_buffer_alloc0(size) \
+	stack_buffer_alloc0_aligned((size), MAXIMUM_ALIGNOF)
+#define stack_buffer_alloc0_aligned(size, align) \
+	(stack_buffer_sanity_checks(),										\
+	 stack_buffer_let_size = (size),									\
+	 stack_buffer_let_size = Max(stack_buffer_let_size, 1),				\
+	 memset(stack_buffer_alloc_aligned_impl(stack_buffer_let_size,		\
+											(align)),					\
+			0,															\
+			stack_buffer_let_size))
+
+/* As above, but for a given type T. */
+#define stack_buffer_alloc_object(T)			\
+	stack_buffer_alloc_array(T, 1)
+#define stack_buffer_alloc_array(T, n)								\
+	((T *) stack_buffer_alloc_aligned((n) * sizeof(T), alignof(T)))
+#define stack_buffer_alloc0_object(T)			\
+	stack_buffer_alloc0_array(T, 1)
+#define stack_buffer_alloc0_array(T, n)									\
+	((T *) stack_buffer_alloc0_aligned((n) * sizeof(T), alignof(T)))
+
+/* Copy a string. */
+#define stack_buffer_strdup(cstr)						\
+	stack_buffer_strdup_with_len((cstr), strlen(cstr))
+#define stack_buffer_strndup(cstr, n)							\
+	stack_buffer_strdup_with_len((cstr), strnlen((cstr), (n)))
+#define stack_buffer_strdup_with_len(data, size)						\
+	(stack_buffer_sanity_checks(),										\
+	 stack_buffer_let_size = (size),									\
+	 stack_buffer_strdup_with_len_impl(stack_buffer_alloc_aligned_impl(stack_buffer_let_size + 1, \
+																	   alignof(char)), \
+									   (data),							\
+									   stack_buffer_let_size))
+#define stack_buffer_text_to_cstring(text) \
+	stack_buffer_strdup_with_len(VARDATA_ANY(text), VARSIZE_ANY_EXHDR(text))
+#define stack_buffer_text_datum_to_cstring(datum) \
+	stack_buffer_text_to_cstring((text *) DatumGetPointer(datum))
+
+/*
+ * Free memory allocated with the above interfaces.  We don't expect to
+ * receive pointers allocated by palloc() directly and not via this API.  That
+ * would break the stack_buffer_maybe_pfree optimization, and might limit
+ * future implementation techniques.
+ */
+#define stack_buffer_free(ptr)											\
+	do																	\
+	{																	\
+		Assert(stack_buffer_stack_p(ptr) || stack_buffer_maybe_pfree);	\
+		if (unlikely(stack_buffer_maybe_pfree) &&						\
+			!stack_buffer_stack_p(ptr))									\
+			pfree(ptr);													\
+	}																	\
+	while (0)
+
+
+/*-------------------------------------------------------------------------
+ *
+ * Private helper code common to all implementations.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/* Cannot work across setjmp()/longjmp() due to stack cloberring. */
+#define stack_buffer_sanity_checks()									\
+	(StaticAssertExpr(!pg_in_lexical_scope_p(PG_TRY),					\
+					  "stack buffer API not allowed in PG_TRY"),		\
+	 StaticAssertExpr(!pg_in_lexical_scope_p(PG_CATCH),					\
+					  "stack buffer API not allowed in PG_CATCH"),		\
+	 StaticAssertExpr(!pg_in_lexical_scope_p(PG_FINALLY),				\
+					  "stack buffer API not allowed in PG_FINALLY"))
+
+/* Post-allocation part of stack_buffer_strdup_with_len(). */
+static inline char *
+stack_buffer_strdup_with_len_impl(char *dst, const char *data, size_t size)
+{
+	memcpy(dst, data, size);
+	dst[size] = 0;
+	return dst;
+}
+
+/*
+ * Allocate with palloc() or palloc_aligned().  We waste a register
+ * remembering if we've ever had to do this, to generate better straight-line
+ * code for the case where we don't have to free anything.
+ */
+#define stack_buffer_palloc_aligned(size, align) \
+	((stack_buffer_maybe_pfree = true),			 \
+	 ((align) > MAXIMUM_ALIGNOF ?										\
+	  palloc_aligned((size), (align), 0) :								\
+	  palloc(size)))			/* can't ask for smaller alignment */
+
+
+/*-------------------------------------------------------------------------
+ *
+ * Low-level implementations below this point supply the following macros,
+ * which should not be used directly:
+ *
+ * 1. stack_buffer_total_size_limited (true/false)
+ * 2. DECLARE_STACK_BUFFER_IMPL(size)
+ * 3. stack_buffer_alloc_aligned(size, align)
+ * 4. stack_buffer_stack_p(ptr)
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/*-------------------------------------------------------------------------
+ *
+ * Toy implementations for debugging.
+ *
+ *-------------------------------------------------------------------------
+ */
+
+/* Just use palloc. */
+#ifdef STACK_BUFFER_USE_PALLOC
+#define stack_buffer_total_size_limited false
+#define DECLARE_STACK_BUFFER_IMPL(size)
+#define stack_buffer_alloc_aligned_impl(size, align)	\
+	stack_buffer_palloc_aligned((size), (align))
+#define stack_buffer_stack_p(ptr) false
+#endif
+
+/*
+ * Same, but log "location,function,size,depth" entries to a path specified
+ * by the STACK_BUFFER_USE_PALLOC_LOG macro.
+ */
+#ifdef STACK_BUFFER_USE_PALLOC_LOG
+#define stack_buffer_total_size_limited false
+#define DECLARE_STACK_BUFFER_IMPL(size)									\
+	FILE *stack_buffer_log												\
+	__attribute__((cleanup(stack_buffer_close_log))) =					\
+	fopen(STACK_BUFFER_USE_PALLOC_LOG, "a+")
+#define stack_buffer_alloc_aligned_impl(size, align)					\
+	(fprintf(stack_buffer_log,											\
+			 "%s:%d,%s,%zu,%zu\n",										\
+			 __FILE__,													\
+			 __LINE__,													\
+			 __func__,													\
+			 (size_t) (size),											\
+			 ((const char *) stack_base_ptr -							\
+			  (const char *) __builtin_stack_address())),				\
+	 stack_buffer_palloc_aligned((size), (align)))
+#define stack_buffer_stack_p(ptr) false
+static inline void
+stack_buffer_close_log(FILE **f)
+{
+	fclose(*f);
+}
+#endif
+
+/*-------------------------------------------------------------------------
+ *
+ * Simple array-based implementation.
+ *
+ * This is entirely standard C requiring no compiler extensions, but it leaves
+ * a big hole in the stack when you call another function and has no ability
+ * to respect the total stack size limit so we have to be much more cautious
+ * about sizing when we use it.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifdef STACK_BUFFER_USE_ARRAY
+
+#define stack_buffer_total_size_limited false
+
+#define DECLARE_STACK_BUFFER_IMPL(size)									\
+	char stack_buffer_array[(size)];									\
+	char *stack_buffer_sp = stack_buffer_array + (size)
+
+#define stack_buffer_alloc_aligned_impl(size, align)				\
+	stack_buffer_alloc_aligned_from_array(&stack_buffer_array[0],	\
+										  &stack_buffer_sp,			\
+										  (size),					\
+										  (align))
+
+#define stack_buffer_stack_p(ptr)										\
+	((char *) (ptr) >= &stack_buffer_array[0] &&						\
+	 (char *) (ptr) < &stack_buffer_array[sizeof(stack_buffer_array)])
+
+static inline void *
+stack_buffer_alloc_aligned_from_array(const char *array,
+									  char **sp,
+									  size_t size,
+									  size_t align)
+{
+	if (likely(size >= (uintptr_t) *sp))	/* avoids overflow with huge size */
+	{
+		char	   *result = *sp - size;
+
+		if (align > 1)
+			result = (char *) TYPEALIGN_DOWN(align, result);
+
+		if (likely(result >= array))
+		{
+			*sp = result;
+			return result;
+		}
+	}
+
+	return stack_buffer_palloc_aligned(size, align);
+}
+
+#endif
+
+/*-------------------------------------------------------------------------
+ *
+ * alloca()-based implementation.
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifdef STACK_BUFFER_USE_ALLOCA
+
+#define stack_buffer_total_size_limited true
+
+#define DECLARE_STACK_BUFFER_IMPL(size)									\
+	stack_buffer_declare_impl											\
+	const char *stack_buffer_limit =									\
+		stack_buffer_least_deep(stack_soft_limit_ptr,					\
+								stack_buffer_sp +						\
+								(size) * PG_STACK_DIRECTION)
+
+#define stack_buffer_alloc_aligned_impl(size, align)					\
+	(likely(stack_buffer_alloca_would_fit_p((size), (align))) ?			\
+	 stack_buffer_alloca_aligned((size), (align)) :						\
+	 stack_buffer_palloc_aligned((size), (align)))
+
+#define stack_buffer_stack_p(ptr)					\
+	(!stack_ptr_deeper_p((ptr), stack_buffer_sp) &&	\
+	 !stack_ptr_deeper_p(stack_buffer_base, (ptr)))
+
+
+/* Portability details. */
+
+/*
+ * Builtins for interacting with the stack registers, if available.  For all
+ * but __builtin_frame_address, which is older and unlikely to differ across
+ * the selected C, C++ and bitcode compilers, use pg_has_builtin().
+  */
+#if HAVE__BUILTIN_FRAME_ADDRESS
+#define stack_buffer_base ((const char *) __builtin_frame_address(0))
+#endif
+#if pg_has_builtin(__builtin_stack_address)
+#define stack_buffer_sp ((const char *) __builtin_stack_address())
+#endif
+#if pg_has_builtin(__builtin_alloca_with_align)
+#define stack_buffer_alloca_aligned_impl(size, align)		\
+	__builtin_alloca_with_align((size), (align) * CHAR_BIT)
+#endif
+
+#if !defined(stack_buffer_alloca_aligned_impl)
+/* Use traditional alloca(), with adjustments for strict alignment. */
+#define stack_buffer_alloca_aligned_impl(size, align)					\
+	((align) <= stack_buffer_alignof_alloca ?							\
+	 alloca(size) :														\
+	 ((void *) TYPEALIGN((align), alloca((size) + (align) - 1))))
+#endif
+
+/*
+ * We assume alloca() and similar have alignment at least as strict as C
+ * requires, for estimation purposes.
+ *
+ * XXX In real systems it's usually higher (eg 16 bytes rather than 8).  We
+ * could estimate more accurately if we had that information.
+ */
+#define stack_buffer_alignof_alloca alignof(max_align_t)
+
+/* Do we need variables to track the bounds of the stack buffer? */
+#if defined(stack_buffer_sp) && defined(stack_buffer_base)
+/* Case 1: nothing to do, we have "base" and "sp" builtins. */
+#define stack_buffer_declare_impl
+#define stack_buffer_alloca_aligned stack_buffer_alloca_aligned_impl
+#else
+/* Cases 2 and 3: we need to track one or both of them ourselves... */
+#if defined(stack_buffer_base)
+/*
+ * Case 2: we have a "base" builtin, but not "sp", so we need a variable.  A
+ * pointer to itself is a good initial value.  The real stack pointer is
+ * probably less deep, as the compiler probably didn't allocate space for some
+ * variables.  That's OK: we err on the cautious side of the truth, and after
+ * the first allocation we'll have an accurate value.
+ */
+#define stack_buffer_declare_impl				\
+	char *stack_buffer_sp = (char *) &stack_buffer_sp;
+#else
+/*
+ * Case 3: we have neither "base" nor "sp".  As case 2, but we'll also use
+ * stack_depth.c's base pointer which is certainly less deep than anything in
+ * this stack frame.
+ *
+ * XXX If we used the address of one of our own stack variables as base, we'd
+ * need add some kind of slop factor since the compiler might not actually
+ * allocate space to it if is never spilled, and then alloca() might return an
+ * address on the wrong side of it, which would break stack_buffer_stack_p().
+ *
+ * XXX Another way to get an initial value for both variables would be to make
+ * a dummy alloca(1) call, wasting (probably) 16 bytes of stack.  It is, after
+ * all, a builtin that gives you the stack pointer, it just happens to consume
+ * space while doing so...
+ */
+#define stack_buffer_declare_impl										\
+	char *stack_buffer_sp = (char *) &stack_buffer_sp;					\
+	char *stack_buffer_base = (char *) stack_base_ptr;
+#endif
+/* Allocation wrapper for cases 2 and 3 that updates stack_buffer_sp. */
+#define stack_buffer_alloca_aligned(size, align)						\
+	((stack_buffer_sp = stack_buffer_alloca_aligned_impl((size),		\
+														 (align))),		\
+	 (PG_STACK_DIRECTION < 0 ? stack_buffer_sp : ((stack_buffer_sp +=	\
+												   (size))) - (size)))
+#endif
+
+/* Like Min(), but in the direction of stack growth. */
+#define stack_buffer_least_deep(p1, p2)				\
+	(stack_ptr_deeper_p((p1), (p2)) ? (p2) : (p1))
+
+/*
+ * Predict new end of stack after hypothetical allocation.  This might
+ * undershoot by a few bytes due stack_buffer_alignof_alloca being too low,
+ * and because we don't bother to do TYPEALIGN(stack_buffer_alignof_alloca,
+ * size), but that doesn't matter much for our purposes.
+ */
+static inline const char *
+stack_buffer_guess_new_sp(const char *sp, size_t size, size_t align)
+{
+	if (align <= stack_buffer_alignof_alloca)
+		return sp + size * PG_STACK_DIRECTION;
+
+	if (PG_STACK_DIRECTION < 0)
+		return (const char *) TYPEALIGN_DOWN(align, sp - size);
+	else
+		return (const char *) TYPEALIGN(align, sp) + size;
+}
+
+#define stack_buffer_alloca_would_fit_p(size, align)					\
+	(stack_ptr_deeper_p(stack_buffer_limit,								\
+						stack_buffer_guess_new_sp(stack_buffer_sp,		\
+												  (size),				\
+												  (align))))
+
+#endif
+
+#endif
-- 
2.53.0

From b7d7ae501e7416d29c867fc8f10e7e447af084dd Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Thu, 5 Mar 2026 21:38:46 +1300
Subject: [PATCH v2 06/19] Use stack buffer in locale code.

Use stack buffers for temporary C strings, wchar_t strings, pg_wchar
strings with the new API.  Some cases were already using open-coded
arrays, while others nearby were not but are obvious candidates, so
let's change them too.

These cases used TEXTBUFLEN (1KB), and they now inherit that same amount
of available space by using:

  DECLARE_STACK_BUFFER_LARGE();

Discussion:
---
 src/backend/utils/adt/pg_locale.c      |   6 -
 src/backend/utils/adt/pg_locale_icu.c  |  59 +++-------
 src/backend/utils/adt/pg_locale_libc.c | 150 +++++++++----------------
 3 files changed, 71 insertions(+), 144 deletions(-)

diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c
index 6c5c1019e1e..3a6190892a7 100644
--- a/src/backend/utils/adt/pg_locale.c
+++ b/src/backend/utils/adt/pg_locale.c
@@ -60,12 +60,6 @@
 #define		PGLOCALE_SUPPORT_ERROR(provider) \
 	elog(ERROR, "unsupported collprovider for %s: %c", __func__, provider)
 
-/*
- * This should be large enough that most strings will fit, but small enough
- * that we feel comfortable putting it on the stack
- */
-#define		TEXTBUFLEN			1024
-
 #define		MAX_L10N_DATA		80
 
 /* pg_locale_builtin.c */
diff --git a/src/backend/utils/adt/pg_locale_icu.c b/src/backend/utils/adt/pg_locale_icu.c
index 352b4c3885f..e64aff82738 100644
--- a/src/backend/utils/adt/pg_locale_icu.c
+++ b/src/backend/utils/adt/pg_locale_icu.c
@@ -39,16 +39,9 @@
 #include "utils/formatting.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
-/*
- * Size of stack buffer to use for string transformations, used to avoid heap
- * allocations in typical cases. This should be large enough that most strings
- * will fit, but small enough that we feel comfortable putting it on the
- * stack.
- */
-#define		TEXTBUFLEN			1024
-
 extern pg_locale_t create_pg_locale_icu(Oid collid, MemoryContext context);
 
 #ifdef USE_ICU
@@ -755,23 +748,17 @@ size_t
 strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen,
 			 pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
 	UChar	   *uchar;
 	int32_t		ulen;
-	size_t		uchar_bsize;
 	Size		result_bsize;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	init_icu_converter();
 
 	ulen = uchar_length(icu_converter, src, srclen);
 
-	uchar_bsize = (ulen + 1) * sizeof(UChar);
-
-	if (uchar_bsize > TEXTBUFLEN)
-		buf = palloc(uchar_bsize);
-
-	uchar = (UChar *) buf;
+	uchar = stack_buffer_alloc_array(UChar, ulen + 1);
 
 	ulen = uchar_convert(icu_converter, uchar, ulen + 1, src, srclen);
 
@@ -786,8 +773,7 @@ strnxfrm_icu(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	Assert(result_bsize > 0);
 	result_bsize--;
 
-	if (buf != sbuf)
-		pfree(buf);
+	stack_buffer_free(uchar);
 
 	/* if dest is defined, it should be nul-terminated */
 	Assert(result_bsize >= destsize || dest[result_bsize] == '\0');
@@ -1020,16 +1006,14 @@ static int
 strncoll_icu(const char *arg1, ssize_t len1,
 			 const char *arg2, ssize_t len2, pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
 	int32_t		ulen1;
 	int32_t		ulen2;
-	size_t		bufsize1;
-	size_t		bufsize2;
 	UChar	   *uchar1,
 			   *uchar2;
 	int			result;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	/* if encoding is UTF8, use more efficient strncoll_icu_utf8 */
 #ifdef HAVE_UCOL_STRCOLLUTF8
 	Assert(GetDatabaseEncoding() != PG_UTF8);
@@ -1040,14 +1024,8 @@ strncoll_icu(const char *arg1, ssize_t len1,
 	ulen1 = uchar_length(icu_converter, arg1, len1);
 	ulen2 = uchar_length(icu_converter, arg2, len2);
 
-	bufsize1 = (ulen1 + 1) * sizeof(UChar);
-	bufsize2 = (ulen2 + 1) * sizeof(UChar);
-
-	if (bufsize1 + bufsize2 > TEXTBUFLEN)
-		buf = palloc(bufsize1 + bufsize2);
-
-	uchar1 = (UChar *) buf;
-	uchar2 = (UChar *) (buf + bufsize1);
+	uchar1 = stack_buffer_alloc_array(UChar, ulen1 + 1);
+	uchar2 = stack_buffer_alloc_array(UChar, ulen2 + 1);
 
 	ulen1 = uchar_convert(icu_converter, uchar1, ulen1 + 1, arg1, len1);
 	ulen2 = uchar_convert(icu_converter, uchar2, ulen2 + 1, arg2, len2);
@@ -1056,8 +1034,8 @@ strncoll_icu(const char *arg1, ssize_t len1,
 						  uchar1, ulen1,
 						  uchar2, ulen2);
 
-	if (buf != sbuf)
-		pfree(buf);
+	stack_buffer_free(uchar1);
+	stack_buffer_free(uchar2);
 
 	return result;
 }
@@ -1068,16 +1046,15 @@ strnxfrm_prefix_icu(char *dest, size_t destsize,
 					const char *src, ssize_t srclen,
 					pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
 	UCharIterator iter;
 	uint32_t	state[2];
 	UErrorCode	status;
 	int32_t		ulen = -1;
 	UChar	   *uchar = NULL;
-	size_t		uchar_bsize;
 	Size		result_bsize;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	/* if encoding is UTF8, use more efficient strnxfrm_prefix_icu_utf8 */
 	Assert(GetDatabaseEncoding() != PG_UTF8);
 
@@ -1085,12 +1062,7 @@ strnxfrm_prefix_icu(char *dest, size_t destsize,
 
 	ulen = uchar_length(icu_converter, src, srclen);
 
-	uchar_bsize = (ulen + 1) * sizeof(UChar);
-
-	if (uchar_bsize > TEXTBUFLEN)
-		buf = palloc(uchar_bsize);
-
-	uchar = (UChar *) buf;
+	uchar = stack_buffer_alloc_array(UChar, ulen + 1);
 
 	ulen = uchar_convert(icu_converter, uchar, ulen + 1, src, srclen);
 
@@ -1108,8 +1080,7 @@ strnxfrm_prefix_icu(char *dest, size_t destsize,
 				(errmsg("sort key generation failed: %s",
 						u_errorName(status))));
 
-	if (buf != sbuf)
-		pfree(buf);
+	stack_buffer_free(uchar);
 
 	return result_bsize;
 }
diff --git a/src/backend/utils/adt/pg_locale_libc.c b/src/backend/utils/adt/pg_locale_libc.c
index 78f6ea161a0..6f51c32814a 100644
--- a/src/backend/utils/adt/pg_locale_libc.c
+++ b/src/backend/utils/adt/pg_locale_libc.c
@@ -23,6 +23,7 @@
 #include "utils/formatting.h"
 #include "utils/memutils.h"
 #include "utils/pg_locale.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
 #ifdef __GLIBC__
@@ -72,14 +73,6 @@
  * NB: the coding here assumes pg_wchar is an unsigned type.
  */
 
-/*
- * Size of stack buffer to use for string transformations, used to avoid heap
- * allocations in typical cases. This should be large enough that most strings
- * will fit, but small enough that we feel comfortable putting it on the
- * stack.
- */
-#define		TEXTBUFLEN			1024
-
 extern pg_locale_t create_pg_locale_libc(Oid collid, MemoryContext context);
 
 static int	strncoll_libc(const char *arg1, ssize_t len1,
@@ -502,6 +495,8 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	size_t		curr_char;
 	size_t		max_size;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	if (srclen < 0)
 		srclen = strlen(src);
 
@@ -512,7 +507,7 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 				 errmsg("out of memory")));
 
 	/* Output workspace cannot have more codes than input bytes */
-	workspace = palloc_array(wchar_t, srclen + 1);
+	workspace = stack_buffer_alloc_array(wchar_t, srclen + 1);
 
 	char2wchar(workspace, srclen + 1, src, srclen, loc);
 
@@ -523,7 +518,7 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	 * Make result large enough; case change might change number of bytes
 	 */
 	max_size = curr_char * pg_database_encoding_max_length();
-	result = palloc(max_size + 1);
+	result = stack_buffer_alloc(max_size + 1);
 
 	result_size = wchar2char(result, workspace, max_size + 1, loc);
 
@@ -533,8 +528,8 @@ strlower_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 		dest[result_size] = '\0';
 	}
 
-	pfree(workspace);
-	pfree(result);
+	stack_buffer_free(workspace);
+	stack_buffer_free(result);
 
 	return result_size;
 }
@@ -607,6 +602,8 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	size_t		curr_char;
 	size_t		max_size;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	if (srclen < 0)
 		srclen = strlen(src);
 
@@ -617,7 +614,7 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 				 errmsg("out of memory")));
 
 	/* Output workspace cannot have more codes than input bytes */
-	workspace = palloc_array(wchar_t, srclen + 1);
+	workspace = stack_buffer_alloc_array(wchar_t, srclen + 1);
 
 	char2wchar(workspace, srclen + 1, src, srclen, loc);
 
@@ -634,7 +631,7 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	 * Make result large enough; case change might change number of bytes
 	 */
 	max_size = curr_char * pg_database_encoding_max_length();
-	result = palloc(max_size + 1);
+	result = stack_buffer_alloc(max_size + 1);
 
 	result_size = wchar2char(result, workspace, max_size + 1, loc);
 
@@ -644,8 +641,8 @@ strtitle_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 		dest[result_size] = '\0';
 	}
 
-	pfree(workspace);
-	pfree(result);
+	stack_buffer_free(workspace);
+	stack_buffer_free(result);
 
 	return result_size;
 }
@@ -700,6 +697,8 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	size_t		curr_char;
 	size_t		max_size;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	if (srclen < 0)
 		srclen = strlen(src);
 
@@ -710,7 +709,7 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 				 errmsg("out of memory")));
 
 	/* Output workspace cannot have more codes than input bytes */
-	workspace = palloc_array(wchar_t, srclen + 1);
+	workspace = stack_buffer_alloc_array(wchar_t, srclen + 1);
 
 	char2wchar(workspace, srclen + 1, src, srclen, loc);
 
@@ -721,7 +720,7 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 	 * Make result large enough; case change might change number of bytes
 	 */
 	max_size = curr_char * pg_database_encoding_max_length();
-	result = palloc(max_size + 1);
+	result = stack_buffer_alloc(max_size + 1);
 
 	result_size = wchar2char(result, workspace, max_size + 1, loc);
 
@@ -731,8 +730,8 @@ strupper_libc_mb(char *dest, size_t destsize, const char *src, ssize_t srclen,
 		dest[result_size] = '\0';
 	}
 
-	pfree(workspace);
-	pfree(result);
+	stack_buffer_free(workspace);
+	stack_buffer_free(result);
 
 	return result_size;
 }
@@ -896,48 +895,25 @@ int
 strncoll_libc(const char *arg1, ssize_t len1, const char *arg2, ssize_t len2,
 			  pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
-	size_t		bufsize1 = (len1 == -1) ? 0 : len1 + 1;
-	size_t		bufsize2 = (len2 == -1) ? 0 : len2 + 1;
-	const char *arg1n;
-	const char *arg2n;
+	char	   *cstr1 = NULL;
+	char	   *cstr2 = NULL;
 	int			result;
 
-	if (bufsize1 + bufsize2 > TEXTBUFLEN)
-		buf = palloc(bufsize1 + bufsize2);
+	DECLARE_STACK_BUFFER_LARGE();
 
 	/* nul-terminate arguments if necessary */
-	if (len1 == -1)
-	{
-		arg1n = arg1;
-	}
-	else
-	{
-		char	   *buf1 = buf;
+	if (len1 != -1)
+		arg1 = cstr1 = stack_buffer_strdup_with_len(arg1, len1);
 
-		memcpy(buf1, arg1, len1);
-		buf1[len1] = '\0';
-		arg1n = buf1;
-	}
+	if (len2 != -1)
+		arg2 = cstr2 = stack_buffer_strdup_with_len(arg2, len2);
 
-	if (len2 == -1)
-	{
-		arg2n = arg2;
-	}
-	else
-	{
-		char	   *buf2 = buf + bufsize1;
-
-		memcpy(buf2, arg2, len2);
-		buf2[len2] = '\0';
-		arg2n = buf2;
-	}
+	result = strcoll_l(arg1, arg2, locale->lt);
 
-	result = strcoll_l(arg1n, arg2n, locale->lt);
-
-	if (buf != sbuf)
-		pfree(buf);
+	if (cstr1)
+		stack_buffer_free(cstr1);
+	if (cstr2)
+		stack_buffer_free(cstr2);
 
 	return result;
 }
@@ -953,25 +929,17 @@ size_t
 strnxfrm_libc(char *dest, size_t destsize, const char *src, ssize_t srclen,
 			  pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
-	size_t		bufsize = srclen + 1;
+	char	   *cstr;
 	size_t		result;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	if (srclen == -1)
 		return strxfrm_l(dest, src, destsize, locale->lt);
 
-	if (bufsize > TEXTBUFLEN)
-		buf = palloc(bufsize);
-
-	/* nul-terminate argument */
-	memcpy(buf, src, srclen);
-	buf[srclen] = '\0';
-
-	result = strxfrm_l(dest, buf, destsize, locale->lt);
-
-	if (buf != sbuf)
-		pfree(buf);
+	cstr = stack_buffer_strdup_with_len(src, srclen);
+	result = strxfrm_l(dest, cstr, destsize, locale->lt);
+	stack_buffer_free(cstr);
 
 	/* if dest is defined, it should be nul-terminated */
 	Assert(result >= destsize || dest[result] == '\0');
@@ -1057,15 +1025,13 @@ static int
 strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2,
 						 ssize_t len2, pg_locale_t locale)
 {
-	char		sbuf[TEXTBUFLEN];
-	char	   *buf = sbuf;
-	char	   *a1p,
-			   *a2p;
-	int			a1len;
-	int			a2len;
+	wchar_t    *w1p;
+	wchar_t    *w2p;
 	int			r;
 	int			result;
 
+	DECLARE_STACK_BUFFER_LARGE();
+
 	Assert(GetDatabaseEncoding() == PG_UTF8);
 
 	if (len1 == -1)
@@ -1073,50 +1039,42 @@ strncoll_libc_win32_utf8(const char *arg1, ssize_t len1, const char *arg2,
 	if (len2 == -1)
 		len2 = strlen(arg2);
 
-	a1len = len1 * 2 + 2;
-	a2len = len2 * 2 + 2;
-
-	if (a1len + a2len > TEXTBUFLEN)
-		buf = palloc(a1len + a2len);
-
-	a1p = buf;
-	a2p = buf + a1len;
+	w1p = stack_buffer_alloc_array(wchar_t, len1 + 1);
+	w2p = stack_buffer_alloc_array(wchar_t, len2 + 1);
 
 	/* API does not work for zero-length input */
 	if (len1 == 0)
 		r = 0;
 	else
 	{
-		r = MultiByteToWideChar(CP_UTF8, 0, arg1, len1,
-								(LPWSTR) a1p, a1len / 2);
+		r = MultiByteToWideChar(CP_UTF8, 0, arg1, len1, w1p, len1);
 		if (!r)
 			ereport(ERROR,
 					(errmsg("could not convert string to UTF-16: error code %lu",
 							GetLastError())));
 	}
-	((LPWSTR) a1p)[r] = 0;
+	w1p[r] = 0;
 
 	if (len2 == 0)
 		r = 0;
 	else
 	{
-		r = MultiByteToWideChar(CP_UTF8, 0, arg2, len2,
-								(LPWSTR) a2p, a2len / 2);
+		r = MultiByteToWideChar(CP_UTF8, 0, arg2, len2, w2p, len2);
 		if (!r)
 			ereport(ERROR,
 					(errmsg("could not convert string to UTF-16: error code %lu",
 							GetLastError())));
 	}
-	((LPWSTR) a2p)[r] = 0;
+	w2p[r] = 0;
 
 	errno = 0;
-	result = wcscoll_l((LPWSTR) a1p, (LPWSTR) a2p, locale->lt);
+	result = wcscoll_l(w1p, w2p, locale->lt);
 	if (result == 2147483647)	/* _NLSCMPERROR; missing from mingw headers */
 		ereport(ERROR,
 				(errmsg("could not compare Unicode strings: %m")));
 
-	if (buf != sbuf)
-		pfree(buf);
+	stack_buffer_free(w1p);
+	stack_buffer_free(w2p);
 
 	return result;
 }
@@ -1289,8 +1247,12 @@ char2wchar(wchar_t *to, size_t tolen, const char *from, size_t fromlen,
 	else
 #endif							/* WIN32 */
 	{
+		char	   *str;
+
+		DECLARE_STACK_BUFFER_LARGE();
+
 		/* mbstowcs requires ending '\0' */
-		char	   *str = pnstrdup(from, fromlen);
+		str = stack_buffer_strdup_with_len(from, fromlen);
 
 		if (loc == (locale_t) 0)
 		{
@@ -1303,7 +1265,7 @@ char2wchar(wchar_t *to, size_t tolen, const char *from, size_t fromlen,
 			result = mbstowcs_l(to, str, tolen, loc);
 		}
 
-		pfree(str);
+		stack_buffer_free(str);
 	}
 
 	if (result == -1)
-- 
2.53.0

From a67697f00b6ea05984544211c8a2d9cfa2fa1cb5 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 03:55:56 +1300
Subject: [PATCH v2 07/19] Use stack buffer in some executor code.

Mechanical changes to the allocation of memory used for:
* string copies
* datums, nulls, Oids, keys and similar values linked to the
  number of attributes

These are all non-escaping values that are not used in recursive
processes.
---
 src/backend/executor/execPartition.c |  7 +++++--
 src/backend/executor/execTuples.c    | 11 +++++++----
 src/backend/executor/nodeAgg.c       | 13 +++++++++----
 src/backend/executor/nodeHashjoin.c  | 14 ++++++++------
 src/backend/executor/nodeMemoize.c   |  7 +++++--
 src/backend/executor/nodeWindowAgg.c |  7 +++++--
 src/backend/executor/spi.c           | 11 +++++++----
 7 files changed, 46 insertions(+), 24 deletions(-)

diff --git a/src/backend/executor/execPartition.c b/src/backend/executor/execPartition.c
index bab294f5e91..6cc6d6ea510 100644
--- a/src/backend/executor/execPartition.c
+++ b/src/backend/executor/execPartition.c
@@ -33,6 +33,7 @@
 #include "utils/partcache.h"
 #include "utils/rls.h"
 #include "utils/ruleutils.h"
+#include "utils/stack_buffer.h"

 /*-----------------------
@@ -2518,6 +2519,8 @@ InitExecPartitionPruneContexts(PartitionPruneState *prunestate,
 	int			newidx;
 	bool		fix_subplan_map = false;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(prunestate->do_exec_prune);
 	Assert(parent_plan != NULL);
 	estate = parent_plan->state;
@@ -2535,7 +2538,7 @@ InitExecPartitionPruneContexts(PartitionPruneState *prunestate,
 		 * indexes to new ones.  For convenience of initialization, we use
 		 * 1-based indexes in this array and leave pruned items as 0.
 		 */
-		new_subplan_indexes = palloc0_array(int, n_total_subplans);
+		new_subplan_indexes = stack_buffer_alloc0_array(int, n_total_subplans);
 		newidx = 1;
 		i = -1;
 		while ((i = bms_next_member(initially_valid_subplans, i)) >= 0)
@@ -2645,7 +2648,7 @@ InitExecPartitionPruneContexts(PartitionPruneState *prunestate,
 		bms_free(prunestate->other_subplans);
 		prunestate->other_subplans = new_other_subplans;
 
-		pfree(new_subplan_indexes);
+		stack_buffer_free(new_subplan_indexes);
 	}
 }
 
diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c
index b768eae9e53..d105123e3e1 100644
--- a/src/backend/executor/execTuples.c
+++ b/src/backend/executor/execTuples.c
@@ -68,6 +68,7 @@
 #include "utils/builtins.h"
 #include "utils/expandeddatum.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 #include "utils/typcache.h"
 
 static TupleDesc ExecTypeFromTLInternal(List *targetList,
@@ -2330,8 +2331,10 @@ BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
 	int			i;
 	HeapTuple	tuple;
 
-	dvalues = (Datum *) palloc(natts * sizeof(Datum));
-	nulls = (bool *) palloc(natts * sizeof(bool));
+	DECLARE_STACK_BUFFER();
+
+	dvalues = stack_buffer_alloc_array(Datum, natts);
+	nulls = stack_buffer_alloc_array(bool, natts);
 
 	/*
 	 * Call the "in" function for each non-dropped attribute, even for nulls,
@@ -2368,8 +2371,8 @@ BuildTupleFromCStrings(AttInMetadata *attinmeta, char **values)
 	 * Release locally palloc'd space.  XXX would probably be good to pfree
 	 * values of pass-by-reference datums, as well.
 	 */
-	pfree(dvalues);
-	pfree(nulls);
+	stack_buffer_free(dvalues);
+	stack_buffer_free(nulls);
 
 	return tuple;
 }
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 7d487a165fa..ffc198f526d 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -273,6 +273,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/memutils_memorychunk.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/tuplesort.h"
 
@@ -4347,10 +4348,12 @@ build_pertrans_for_aggref(AggStatePerTrans pertrans,
 	{
 		Oid		   *ops;
 
+		DECLARE_STACK_BUFFER();
+
 		Assert(numArguments > 0);
 		Assert(list_length(aggref->aggdistinct) == numDistinctCols);
 
-		ops = palloc(numDistinctCols * sizeof(Oid));
+		ops = stack_buffer_alloc_array(Oid, numDistinctCols);
 
 		i = 0;
 		foreach(lc, aggref->aggdistinct)
@@ -4367,7 +4370,7 @@ build_pertrans_for_aggref(AggStatePerTrans pertrans,
 									   ops,
 									   pertrans->sortCollations,
 									   &aggstate->ss.ps);
-		pfree(ops);
+		stack_buffer_free(ops);
 	}
 
 	pertrans->sortstates = palloc0_array(Tuplesortstate *, numGroupingSets);
@@ -4382,11 +4385,13 @@ GetAggInitVal(Datum textInitVal, Oid transtype)
 	char	   *strInitVal;
 	Datum		initVal;
 
+	DECLARE_STACK_BUFFER();
+
 	getTypeInputInfo(transtype, &typinput, &typioparam);
-	strInitVal = TextDatumGetCString(textInitVal);
+	strInitVal = stack_buffer_text_datum_to_cstring(textInitVal);
 	initVal = OidInputFunctionCall(typinput, strInitVal,
 								   typioparam, -1);
-	pfree(strInitVal);
+	stack_buffer_free(strInitVal);
 	return initVal;
 }
 
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index 5aa8a09b265..fd7a47d8b85 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -171,6 +171,7 @@
 #include "miscadmin.h"
 #include "utils/lsyscache.h"
 #include "utils/sharedtuplestore.h"
+#include "utils/stack_buffer.h"
 #include "utils/wait_event.h"

@@ -826,6 +827,7 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		ListCell   *lc;
 		int			nkeys;
 
+		DECLARE_STACK_BUFFER();
 
 		hjstate->hj_HashTupleSlot = slot;
 
@@ -840,9 +842,9 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		 */
 		nkeys = list_length(node->hashoperators);
 
-		outer_hashfuncid = palloc_array(Oid, nkeys);
-		inner_hashfuncid = palloc_array(Oid, nkeys);
-		hash_strict = palloc_array(bool, nkeys);
+		outer_hashfuncid = stack_buffer_alloc_array(Oid, nkeys);
+		inner_hashfuncid = stack_buffer_alloc_array(Oid, nkeys);
+		hash_strict = stack_buffer_alloc_array(bool, nkeys);
 
 		/*
 		 * Determine the hash function for each side of the join for the given
@@ -905,9 +907,9 @@ ExecInitHashJoin(HashJoin *node, EState *estate, int eflags)
 		}
 
 		/* no need to keep these */
-		pfree(outer_hashfuncid);
-		pfree(inner_hashfuncid);
-		pfree(hash_strict);
+		stack_buffer_free(outer_hashfuncid);
+		stack_buffer_free(inner_hashfuncid);
+		stack_buffer_free(hash_strict);
 	}
 
 	/*
diff --git a/src/backend/executor/nodeMemoize.c b/src/backend/executor/nodeMemoize.c
index edf52efd4c3..09a5e674dd7 100644
--- a/src/backend/executor/nodeMemoize.c
+++ b/src/backend/executor/nodeMemoize.c
@@ -74,6 +74,7 @@
 #include "miscadmin.h"
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 
 /* States of the ExecMemoize state machine */
 #define MEMO_CACHE_LOOKUP			1	/* Attempt to perform a cache lookup */
@@ -958,6 +959,8 @@ ExecInitMemoize(Memoize *node, EState *estate, int eflags)
 	int			nkeys;
 	Oid		   *eqfuncoids;
 
+	DECLARE_STACK_BUFFER();
+
 	/* check for unsupported flags */
 	Assert(!(eflags & (EXEC_FLAG_BACKWARD | EXEC_FLAG_MARK)));
 
@@ -1006,7 +1009,7 @@ ExecInitMemoize(Memoize *node, EState *estate, int eflags)
 											 * data */
 	mstate->hashfunctions = (FmgrInfo *) palloc(nkeys * sizeof(FmgrInfo));
 
-	eqfuncoids = palloc(nkeys * sizeof(Oid));
+	eqfuncoids = stack_buffer_alloc_array(Oid, nkeys);
 
 	for (i = 0; i < nkeys; i++)
 	{
@@ -1033,7 +1036,7 @@ ExecInitMemoize(Memoize *node, EState *estate, int eflags)
 												   node->param_exprs,
 												   (PlanState *) mstate);
 
-	pfree(eqfuncoids);
+	stack_buffer_free(eqfuncoids);
 	mstate->mem_used = 0;
 
 	/* Limit the total memory consumed by the cache to this */
diff --git a/src/backend/executor/nodeWindowAgg.c b/src/backend/executor/nodeWindowAgg.c
index d9b64b0f465..ed3744f5493 100644
--- a/src/backend/executor/nodeWindowAgg.c
+++ b/src/backend/executor/nodeWindowAgg.c
@@ -52,6 +52,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/regproc.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "windowapi.h"
 
@@ -3163,11 +3164,13 @@ GetAggInitVal(Datum textInitVal, Oid transtype)
 	char	   *strInitVal;
 	Datum		initVal;
 
+	DECLARE_STACK_BUFFER();
+
 	getTypeInputInfo(transtype, &typinput, &typioparam);
-	strInitVal = TextDatumGetCString(textInitVal);
+	strInitVal = stack_buffer_text_datum_to_cstring(textInitVal);
 	initVal = OidInputFunctionCall(typinput, strInitVal,
 								   typioparam, -1);
-	pfree(strInitVal);
+	stack_buffer_free(strInitVal);
 	return initVal;
 }
 
diff --git a/src/backend/executor/spi.c b/src/backend/executor/spi.c
index 3019a3b2b97..dbbb710f877 100644
--- a/src/backend/executor/spi.c
+++ b/src/backend/executor/spi.c
@@ -31,6 +31,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 
@@ -1113,6 +1114,8 @@ SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum,
 	bool	   *n;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	if (rel == NULL || tuple == NULL || natts < 0 || attnum == NULL || Values == NULL)
 	{
 		SPI_result = SPI_ERROR_ARGUMENT;
@@ -1130,8 +1133,8 @@ SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum,
 	SPI_result = 0;
 
 	numberOfAttributes = rel->rd_att->natts;
-	v = palloc_array(Datum, numberOfAttributes);
-	n = palloc_array(bool, numberOfAttributes);
+	v = stack_buffer_alloc_array(Datum, numberOfAttributes);
+	n = stack_buffer_alloc_array(bool, numberOfAttributes);
 
 	/* fetch old values and nulls */
 	heap_deform_tuple(tuple, rel->rd_att, v, n);
@@ -1163,8 +1166,8 @@ SPI_modifytuple(Relation rel, HeapTuple tuple, int natts, int *attnum,
 		SPI_result = SPI_ERROR_NOATTRIBUTE;
 	}
 
-	pfree(v);
-	pfree(n);
+	stack_buffer_free(v);
+	stack_buffer_free(n);
 
 	MemoryContextSwitchTo(oldcxt);
 
-- 
2.53.0

From 89a7bc6e554fce3d35c01eccdd6a1a618e6608d0 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 04:08:31 +1300
Subject: [PATCH v2 08/19] Use stack buffer in some adt code.

---
 src/backend/utils/adt/arrayfuncs.c   |  39 ++++----
 src/backend/utils/adt/datum.c        |   7 +-
 src/backend/utils/adt/jsonfuncs.c    |  52 ++++++++---
 src/backend/utils/adt/like_support.c |  59 +++++++-----
 src/backend/utils/adt/numeric.c      |  15 ++-
 src/backend/utils/adt/rowtypes.c     | 133 ++++++++++++++++-----------
 src/backend/utils/adt/ruleutils.c    |  47 ++++++----
 src/backend/utils/adt/tsvector_op.c  |  10 +-
 8 files changed, 223 insertions(+), 139 deletions(-)

diff --git a/src/backend/utils/adt/arrayfuncs.c b/src/backend/utils/adt/arrayfuncs.c
index 734e5fea45e..c2706be9276 100644
--- a/src/backend/utils/adt/arrayfuncs.c
+++ b/src/backend/utils/adt/arrayfuncs.c
@@ -35,6 +35,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"
 #include "utils/typcache.h"

@@ -1051,6 +1052,8 @@ array_out(PG_FUNCTION_ARGS)
 	array_iter	iter;
 	ArrayMetaState *my_extra;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * We arrange to look up info about element type, including its output
 	 * conversion proc, only once per series of calls, assuming the element
@@ -1112,8 +1115,8 @@ array_out(PG_FUNCTION_ARGS)
 	 * any overhead such as escaping backslashes), and detect whether each
 	 * item needs double quotes.
 	 */
-	values = (char **) palloc(nitems * sizeof(char *));
-	needquotes = (bool *) palloc(nitems * sizeof(bool));
+	values = stack_buffer_alloc_array(char *, nitems);
+	needquotes = stack_buffer_alloc_array(bool, nitems);
 	overall_length = 0;
 
 	array_iter_setup(&iter, v, typlen, typbyval, typalign);
@@ -1257,8 +1260,8 @@ array_out(PG_FUNCTION_ARGS)
 	/* Assert that we calculated the string length accurately */
 	Assert(overall_length == (p - retval + 1));
 
-	pfree(values);
-	pfree(needquotes);
+	stack_buffer_free(values);
+	stack_buffer_free(needquotes);
 
 	PG_RETURN_CSTRING(retval);
 }
@@ -3232,6 +3235,8 @@ array_map(Datum arrayd,
 	Datum	   *transform_source = exprstate->innermost_caseval;
 	bool	   *transform_source_isnull = exprstate->innermost_casenull;
 
+	DECLARE_STACK_BUFFER();
+
 	inpType = AARR_ELEMTYPE(v);
 	ndim = AARR_NDIM(v);
 	dim = AARR_DIMS(v);
@@ -3278,8 +3283,8 @@ array_map(Datum arrayd,
 	typalignby = typalign_to_alignby(typalign);
 
 	/* Allocate temporary arrays for new values */
-	values = (Datum *) palloc(nitems * sizeof(Datum));
-	nulls = (bool *) palloc(nitems * sizeof(bool));
+	values = stack_buffer_alloc_array(Datum, nitems);
+	nulls = stack_buffer_alloc_array(bool, nitems);
 
 	/* Loop over source data */
 	array_iter_setup(&iter, v, inp_typlen, inp_typbyval, inp_typalign);
@@ -3340,8 +3345,8 @@ array_map(Datum arrayd,
 	/*
 	 * Note: do not risk trying to pfree the results of the called expression
 	 */
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 
 	return PointerGetDatum(result);
 }
@@ -6424,6 +6429,8 @@ array_replace_internal(ArrayType *array,
 	bool		changed = false;
 	TypeCacheEntry *typentry;
 
+	DECLARE_STACK_BUFFER();
+
 	element_type = ARR_ELEMTYPE(array);
 	ndim = ARR_NDIM(array);
 	dim = ARR_DIMS(array);
@@ -6482,8 +6489,8 @@ array_replace_internal(ArrayType *array,
 							 collation, NULL, NULL);
 
 	/* Allocate temporary arrays for new values */
-	values = (Datum *) palloc(nitems * sizeof(Datum));
-	nulls = (bool *) palloc(nitems * sizeof(bool));
+	values = stack_buffer_alloc_array(Datum, nitems);
+	nulls = stack_buffer_alloc_array(bool, nitems);
 
 	/* Loop over source data */
 	arraydataptr = ARR_DATA_PTR(array);
@@ -6599,16 +6606,16 @@ array_replace_internal(ArrayType *array,
 	 */
 	if (!changed)
 	{
-		pfree(values);
-		pfree(nulls);
+		stack_buffer_free(values);
+		stack_buffer_free(nulls);
 		return array;
 	}
 
 	/* If all elements were removed return an empty array */
 	if (nresult == 0)
 	{
-		pfree(values);
-		pfree(nulls);
+		stack_buffer_free(values);
+		stack_buffer_free(nulls);
 		return construct_empty_array(element_type);
 	}
 
@@ -6643,8 +6650,8 @@ array_replace_internal(ArrayType *array,
 				 typlen, typbyval, typalign,
 				 false);
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 
 	return result;
 }
diff --git a/src/backend/utils/adt/datum.c b/src/backend/utils/adt/datum.c
index 8832785540f..8b04ba8b9d7 100644
--- a/src/backend/utils/adt/datum.c
+++ b/src/backend/utils/adt/datum.c
@@ -48,6 +48,7 @@
 #include "utils/datum.h"
 #include "utils/expandeddatum.h"
 #include "utils/fmgrprotos.h"
+#include "utils/stack_buffer.h"

 /*-------------------------------------------------------------------------
@@ -464,6 +465,8 @@ datumSerialize(Datum value, bool isnull, bool typByVal, int typLen,
 	ExpandedObjectHeader *eoh = NULL;
 	int			header;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Write header word. */
 	if (isnull)
 		header = -2;
@@ -496,13 +499,13 @@ datumSerialize(Datum value, bool isnull, bool typByVal, int typLen,
 			 * EOH_flatten_into expects the target address to be maxaligned,
 			 * so we can't store directly to *start_address.
 			 */
-			tmp = (char *) palloc(header);
+			tmp = (char *) stack_buffer_alloc(header);
 			EOH_flatten_into(eoh, tmp, header);
 			memcpy(*start_address, tmp, header);
 			*start_address += header;
 
 			/* be tidy. */
-			pfree(tmp);
+			stack_buffer_free(tmp);
 		}
 		else
 		{
diff --git a/src/backend/utils/adt/jsonfuncs.c b/src/backend/utils/adt/jsonfuncs.c
index d5b64d7fca5..f0bed51a274 100644
--- a/src/backend/utils/adt/jsonfuncs.c
+++ b/src/backend/utils/adt/jsonfuncs.c
@@ -37,6 +37,7 @@
 #include "utils/jsonfuncs.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 
@@ -1681,7 +1682,11 @@ jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
 {
 	JsonbInState state = {0};
 	JsonbIterator *it;
-	bool	   *path_nulls = palloc0_array(bool, path_len);
+	bool	   *path_nulls;
+
+	DECLARE_STACK_BUFFER();
+
+	path_nulls = stack_buffer_alloc0_array(bool, path_len);
 
 	if (newval->type == jbvArray && newval->val.array.rawScalar)
 		*newval = newval->val.array.elems[0];
@@ -1692,7 +1697,7 @@ jsonb_set_element(Jsonb *jb, const Datum *path, int path_len,
 			JB_PATH_CREATE | JB_PATH_FILL_GAPS |
 			JB_PATH_CONSISTENT_POSITION);
 
-	pfree(path_nulls);
+	stack_buffer_free(path_nulls);
 
 	PG_RETURN_JSONB_P(JsonbValueToJsonb(state.result));
 }
@@ -2921,6 +2926,8 @@ populate_array(ArrayIOData *aio,
 	int		   *lbs;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	ctx.aio = aio;
 	ctx.mcxt = mcxt;
 	ctx.acxt = CurrentMemoryContext;
@@ -2955,7 +2962,7 @@ populate_array(ArrayIOData *aio,
 
 	Assert(ctx.ndims > 0);
 
-	lbs = palloc_array(int, ctx.ndims);
+	lbs = stack_buffer_alloc_array(int, ctx.ndims);
 
 	for (i = 0; i < ctx.ndims; i++)
 		lbs[i] = 1;
@@ -2965,7 +2972,7 @@ populate_array(ArrayIOData *aio,
 
 	pfree(ctx.dims);
 	pfree(ctx.sizes);
-	pfree(lbs);
+	stack_buffer_free(lbs);
 
 	*isnull = false;
 	return result;
@@ -3125,6 +3132,9 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
 	Datum		res;
 	char	   *str = NULL;
 	const char *json = NULL;
+	bool		str_stack = false;
+
+	DECLARE_STACK_BUFFER();
 
 	if (jsv->is_json)
 	{
@@ -3149,9 +3159,8 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
 		else if (len >= 0)
 		{
 			/* create a NUL-terminated version */
-			str = palloc(len + 1);
-			memcpy(str, json, len);
-			str[len] = '\0';
+			str = stack_buffer_strndup(json, len);
+			str_stack = true;
 		}
 		else
 		{
@@ -3164,7 +3173,10 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
 		JsonbValue *jbv = jsv->val.jsonb;
 
 		if (jbv->type == jbvString && omit_quotes)
-			str = pnstrdup(jbv->val.string.val, jbv->val.string.len);
+		{
+			str = stack_buffer_strndup(jbv->val.string.val, jbv->val.string.len);
+			str_stack = true;
+		}
 		else if (typid == JSONBOID)
 		{
 			Jsonb	   *jsonb = JsonbValueToJsonb(jbv); /* directly use jsonb */
@@ -3183,9 +3195,15 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
 			str = JsonbToCString(NULL, &jsonb->root, VARSIZE(jsonb));
 		}
 		else if (jbv->type == jbvString)	/* quotes are stripped */
-			str = pnstrdup(jbv->val.string.val, jbv->val.string.len);
+		{
+			str = stack_buffer_strndup(jbv->val.string.val, jbv->val.string.len);
+			str_stack = true;
+		}
 		else if (jbv->type == jbvBool)
-			str = pstrdup(jbv->val.boolean ? "true" : "false");
+		{
+			str = stack_buffer_strdup(jbv->val.boolean ? "true" : "false");
+			str_stack = true;
+		}
 		else if (jbv->type == jbvNumeric)
 			str = DatumGetCString(DirectFunctionCall1(numeric_out,
 													  PointerGetDatum(jbv->val.numeric)));
@@ -3204,7 +3222,9 @@ populate_scalar(ScalarIOData *io, Oid typid, int32 typmod, JsValue *jsv,
 	}
 
 	/* free temporary buffer */
-	if (str != json)
+	if (str_stack)
+		stack_buffer_free(str);
+	else if (str != json)
 		pfree(str);
 
 	return res;
@@ -3528,6 +3548,8 @@ populate_record(TupleDesc tupdesc,
 	int			ncolumns = tupdesc->natts;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * if the input json is empty, we can only skip the rest if we were passed
 	 * in a non-null record, since otherwise there may be issues with domain
@@ -3552,8 +3574,8 @@ populate_record(TupleDesc tupdesc,
 		record->ncolumns = ncolumns;
 	}
 
-	values = (Datum *) palloc(ncolumns * sizeof(Datum));
-	nulls = (bool *) palloc(ncolumns * sizeof(bool));
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 
 	if (defaultval)
 	{
@@ -3618,8 +3640,8 @@ populate_record(TupleDesc tupdesc,
 
 	res = heap_form_tuple(tupdesc, values, nulls);
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 
 	return res->t_data;
 }
diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c
index 01cd6b10730..119ed842bf1 100644
--- a/src/backend/utils/adt/like_support.c
+++ b/src/backend/utils/adt/like_support.c
@@ -52,6 +52,7 @@
 #include "utils/lsyscache.h"
 #include "utils/pg_locale.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"
 #include "utils/varlena.h"

@@ -994,12 +995,14 @@ like_fixed_prefix(Const *patt_const, Const **prefix_const,
 	int			pos,
 				match_pos;
 
+	DECLARE_STACK_BUFFER();
+
 	/* the right-hand const is type text or bytea */
 	Assert(typeid == BYTEAOID || typeid == TEXTOID);
 
 	if (typeid != BYTEAOID)
 	{
-		patt = TextDatumGetCString(patt_const->constvalue);
+		patt = stack_buffer_text_datum_to_cstring(patt_const->constvalue);
 		pattlen = strlen(patt);
 	}
 	else
@@ -1007,12 +1010,12 @@ like_fixed_prefix(Const *patt_const, Const **prefix_const,
 		bytea	   *bstr = DatumGetByteaPP(patt_const->constvalue);
 
 		pattlen = VARSIZE_ANY_EXHDR(bstr);
-		patt = (char *) palloc(pattlen);
+		patt = (char *) stack_buffer_alloc(pattlen);
 		memcpy(patt, VARDATA_ANY(bstr), pattlen);
 		Assert(bstr == DatumGetPointer(patt_const->constvalue));
 	}
 
-	match = palloc(pattlen + 1);
+	match = stack_buffer_alloc(pattlen + 1);
 	match_pos = 0;
 	for (pos = 0; pos < pattlen; pos++)
 	{
@@ -1042,8 +1045,8 @@ like_fixed_prefix(Const *patt_const, Const **prefix_const,
 	if (rest_selec != NULL)
 		*rest_selec = like_selectivity(&patt[pos], pattlen - pos, false);
 
-	pfree(patt);
-	pfree(match);
+	stack_buffer_free(patt);
+	stack_buffer_free(match);
 
 	/* in LIKE, an empty pattern is an exact match! */
 	if (pos == pattlen)
@@ -1075,6 +1078,8 @@ like_fixed_prefix_ci(Const *patt_const, Oid collation, Const **prefix_const,
 	int			match_mblen;
 	pg_locale_t locale = 0;
 
+	DECLARE_STACK_BUFFER();
+
 	/* the right-hand const is type text or bytea */
 	Assert(typeid == BYTEAOID || typeid == TEXTOID);
 
@@ -1097,10 +1102,10 @@ like_fixed_prefix_ci(Const *patt_const, Oid collation, Const **prefix_const,
 
 	locale = pg_newlocale_from_collation(collation);
 
-	wpatt = palloc((nbytes + 1) * sizeof(pg_wchar));
+	wpatt = stack_buffer_alloc_array(pg_wchar, nbytes + 1);
 	wpattlen = pg_mb2wchar_with_len(VARDATA_ANY(val), wpatt, nbytes);
 
-	wmatch = palloc((nbytes + 1) * sizeof(pg_wchar));
+	wmatch = stack_buffer_alloc_array(pg_wchar, nbytes + 1);
 	for (wpos = 0; wpos < wpattlen; wpos++)
 	{
 		/* % and _ are wildcard characters in LIKE */
@@ -1128,13 +1133,13 @@ like_fixed_prefix_ci(Const *patt_const, Oid collation, Const **prefix_const,
 
 	wmatch[wmatch_pos] = '\0';
 
-	match = palloc(pg_database_encoding_max_length() * wmatch_pos + 1);
+	match = stack_buffer_alloc(pg_database_encoding_max_length() * wmatch_pos + 1);
 	match_mblen = pg_wchar2mb_with_len(wmatch, match, wmatch_pos);
 	match[match_mblen] = '\0';
-	pfree(wmatch);
+	stack_buffer_free(wmatch);
 
 	*prefix_const = string_to_const(match, TEXTOID);
-	pfree(match);
+	stack_buffer_free(match);
 
 	if (rest_selec != NULL)
 	{
@@ -1142,14 +1147,14 @@ like_fixed_prefix_ci(Const *patt_const, Oid collation, Const **prefix_const,
 		char	   *rest;
 		int			rest_mblen;
 
-		rest = palloc(pg_database_encoding_max_length() * wrestlen + 1);
+		rest = stack_buffer_alloc(pg_database_encoding_max_length() * wrestlen + 1);
 		rest_mblen = pg_wchar2mb_with_len(&wpatt[wmatch_pos], rest, wrestlen);
 
 		*rest_selec = like_selectivity(rest, rest_mblen, true);
-		pfree(rest);
+		stack_buffer_free(rest);
 	}
 
-	pfree(wpatt);
+	stack_buffer_free(wpatt);
 
 	/* in LIKE, an empty pattern is an exact match! */
 	if (wpos == wpattlen)
@@ -1169,6 +1174,8 @@ regex_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation,
 	char	   *prefix;
 	bool		exact;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Should be unnecessary, there are no bytea regex operators defined. As
 	 * such, it should be noted that the rest of this function has *not* been
@@ -1190,12 +1197,12 @@ regex_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation,
 
 		if (rest_selec != NULL)
 		{
-			char	   *patt = TextDatumGetCString(patt_const->constvalue);
+			char	   *patt = stack_buffer_text_datum_to_cstring(patt_const->constvalue);
 
 			*rest_selec = regex_selectivity(patt, strlen(patt),
 											case_insensitive,
 											0);
-			pfree(patt);
+			stack_buffer_free(patt);
 		}
 
 		return Pattern_Prefix_None;
@@ -1212,12 +1219,12 @@ regex_fixed_prefix(Const *patt_const, bool case_insensitive, Oid collation,
 		}
 		else
 		{
-			char	   *patt = TextDatumGetCString(patt_const->constvalue);
+			char	   *patt = stack_buffer_text_datum_to_cstring(patt_const->constvalue);
 
 			*rest_selec = regex_selectivity(patt, strlen(patt),
 											case_insensitive,
 											strlen(prefix));
-			pfree(patt);
+			stack_buffer_free(patt);
 		}
 	}
 
@@ -1619,6 +1626,8 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 	char	   *cmptxt = NULL;
 	mbcharacter_incrementer charinc;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Get a modifiable copy of the prefix string in C-string format, and set
 	 * up the string we will compare to as a Datum.  In C locale this can just
@@ -1630,7 +1639,7 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 		bytea	   *bstr = DatumGetByteaPP(str_const->constvalue);
 
 		len = VARSIZE_ANY_EXHDR(bstr);
-		workstr = (char *) palloc(len);
+		workstr = (char *) stack_buffer_alloc(len);
 		memcpy(workstr, VARDATA_ANY(bstr), len);
 		Assert(bstr == DatumGetPointer(str_const->constvalue));
 		cmpstr = str_const->constvalue;
@@ -1641,7 +1650,7 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 			workstr = DatumGetCString(DirectFunctionCall1(nameout,
 														  str_const->constvalue));
 		else
-			workstr = TextDatumGetCString(str_const->constvalue);
+			workstr = stack_buffer_text_datum_to_cstring(str_const->constvalue);
 		len = strlen(workstr);
 		if (len == 0 || pg_newlocale_from_collation(collation)->collate_is_c)
 			cmpstr = str_const->constvalue;
@@ -1669,7 +1678,7 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 			/* And build the string to compare to */
 			if (datatype == NAMEOID)
 			{
-				cmptxt = palloc(len + 2);
+				cmptxt = stack_buffer_alloc(len + 2);
 				memcpy(cmptxt, workstr, len);
 				cmptxt[len] = suffixchar;
 				cmptxt[len + 1] = '\0';
@@ -1677,7 +1686,7 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 			}
 			else
 			{
-				cmptxt = palloc(VARHDRSZ + len + 1);
+				cmptxt = stack_buffer_alloc(VARHDRSZ + len + 1);
 				SET_VARSIZE(cmptxt, VARHDRSZ + len + 1);
 				memcpy(VARDATA(cmptxt), workstr, len);
 				*(VARDATA(cmptxt) + len) = suffixchar;
@@ -1729,8 +1738,8 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 			{
 				/* Successfully made a string larger than cmpstr */
 				if (cmptxt)
-					pfree(cmptxt);
-				pfree(workstr);
+					stack_buffer_free(cmptxt);
+				stack_buffer_free(workstr);
 				return workstr_const;
 			}
 
@@ -1749,8 +1758,8 @@ make_greater_string(const Const *str_const, FmgrInfo *ltproc, Oid collation)
 
 	/* Failed... */
 	if (cmptxt)
-		pfree(cmptxt);
-	pfree(workstr);
+		stack_buffer_free(cmptxt);
+	stack_buffer_free(workstr);
 
 	return NULL;
 }
diff --git a/src/backend/utils/adt/numeric.c b/src/backend/utils/adt/numeric.c
index d25b8ad505d..771ea2ba6b3 100644
--- a/src/backend/utils/adt/numeric.c
+++ b/src/backend/utils/adt/numeric.c
@@ -43,6 +43,7 @@
 #include "utils/numeric.h"
 #include "utils/pg_lsn.h"
 #include "utils/sortsupport.h"
+#include "utils/stack_buffer.h"
 
 /* ----------
  * Uncomment the following to enable compilation of dump_numeric()
@@ -6755,6 +6756,8 @@ set_var_from_str(const char *str, const char *cp,
 	int			offset;
 	NumericDigit *digits;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * We first parse the string to extract decimal digits and determine the
 	 * correct decimal weight.  Then convert to NBASE representation.
@@ -6781,7 +6784,7 @@ set_var_from_str(const char *str, const char *cp,
 	if (!isdigit((unsigned char) *cp))
 		goto invalid_syntax;
 
-	decdigits = (unsigned char *) palloc(strlen(cp) + DEC_DIGITS * 2);
+	decdigits = (unsigned char *) stack_buffer_alloc(strlen(cp) + DEC_DIGITS * 2);
 
 	/* leading padding for digit alignment later */
 	memset(decdigits, 0, DEC_DIGITS);
@@ -6915,7 +6918,7 @@ set_var_from_str(const char *str, const char *cp,
 		i += DEC_DIGITS;
 	}
 
-	pfree(decdigits);
+	stack_buffer_free(decdigits);
 
 	/* Strip any leading/trailing zeroes, and normalize weight if zero */
 	strip_var(dest);
@@ -8901,6 +8904,8 @@ div_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result,
 	NumericDigit *res_digits;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * First of all division by zero check; we must not be handed an
 	 * unnormalized divisor.
@@ -9050,8 +9055,8 @@ div_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result,
 	 * zero and not counted in div_ndigitpairs, so that the main loop below
 	 * can safely read and write the (qi+1)'th digit in the approximate case.
 	 */
-	dividend = (int64 *) palloc((div_ndigitpairs + 1) * sizeof(int64) +
-								var2ndigitpairs * sizeof(int32));
+	dividend = (int64 *) stack_buffer_alloc((div_ndigitpairs + 1) * sizeof(int64) +
+											var2ndigitpairs * sizeof(int32));
 	divisor = (int32 *) (dividend + div_ndigitpairs + 1);
 
 	/* load var1 into dividend[0 .. var1ndigitpairs-1], zeroing the rest */
@@ -9389,7 +9394,7 @@ div_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result,
 	}
 	Assert(carry == 0);
 
-	pfree(dividend);
+	stack_buffer_free(dividend);
 
 	/*
 	 * Finally, round or truncate the result to the requested precision.
diff --git a/src/backend/utils/adt/rowtypes.c b/src/backend/utils/adt/rowtypes.c
index e4eb7111ee7..bcceb998da7 100644
--- a/src/backend/utils/adt/rowtypes.c
+++ b/src/backend/utils/adt/rowtypes.c
@@ -25,6 +25,7 @@
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 #include "utils/typcache.h"

@@ -89,6 +90,8 @@ record_in(PG_FUNCTION_ARGS)
 	bool	   *nulls;
 	StringInfoData buf;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/*
@@ -140,8 +143,8 @@ record_in(PG_FUNCTION_ARGS)
 		my_extra->ncolumns = ncolumns;
 	}
 
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 
 	/*
 	 * Scan the string.  We use "buf" to accumulate the de-quoted data for
@@ -310,8 +313,8 @@ record_in(PG_FUNCTION_ARGS)
 
 	heap_freetuple(tuple);
 	pfree(buf.data);
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	PG_RETURN_HEAPTUPLEHEADER(result);
@@ -341,6 +344,8 @@ record_out(PG_FUNCTION_ARGS)
 	bool	   *nulls;
 	StringInfoData buf;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from the tuple itself */
@@ -383,8 +388,8 @@ record_out(PG_FUNCTION_ARGS)
 		my_extra->ncolumns = ncolumns;
 	}
 
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 
 	/* Break down the tuple into fields */
 	heap_deform_tuple(&tuple, tupdesc, values, nulls);
@@ -466,8 +471,8 @@ record_out(PG_FUNCTION_ARGS)
 
 	appendStringInfoChar(&buf, ')');
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	PG_RETURN_CSTRING(buf.data);
@@ -493,6 +498,8 @@ record_recv(PG_FUNCTION_ARGS)
 	Datum	   *values;
 	bool	   *nulls;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/*
@@ -539,8 +546,8 @@ record_recv(PG_FUNCTION_ARGS)
 		my_extra->ncolumns = ncolumns;
 	}
 
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 
 	/* Fetch number of columns user thinks it has */
 	usercols = pq_getmsgint(buf, 4);
@@ -673,8 +680,8 @@ record_recv(PG_FUNCTION_ARGS)
 	memcpy(result, tuple->t_data, tuple->t_len);
 
 	heap_freetuple(tuple);
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	PG_RETURN_HEAPTUPLEHEADER(result);
@@ -699,6 +706,8 @@ record_send(PG_FUNCTION_ARGS)
 	bool	   *nulls;
 	StringInfoData buf;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from the tuple itself */
@@ -741,8 +750,8 @@ record_send(PG_FUNCTION_ARGS)
 		my_extra->ncolumns = ncolumns;
 	}
 
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 
 	/* Break down the tuple into fields */
 	heap_deform_tuple(&tuple, tupdesc, values, nulls);
@@ -800,8 +809,8 @@ record_send(PG_FUNCTION_ARGS)
 					 VARSIZE(outputbytes) - VARHDRSZ);
 	}
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
@@ -845,6 +854,8 @@ record_cmp(FunctionCallInfo fcinfo)
 	int			i2;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from the tuples */
@@ -901,11 +912,11 @@ record_cmp(FunctionCallInfo fcinfo)
 	}
 
 	/* Break down the tuples into fields */
-	values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
-	nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+	values1 = stack_buffer_alloc_array(Datum, ncolumns1);
+	nulls1 = stack_buffer_alloc_array(bool, ncolumns1);
 	heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
-	values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
-	nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+	values2 = stack_buffer_alloc_array(Datum, ncolumns2);
+	nulls2 = stack_buffer_alloc_array(bool, ncolumns2);
 	heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
 
 	/*
@@ -1040,10 +1051,10 @@ record_cmp(FunctionCallInfo fcinfo)
 					 errmsg("cannot compare record types with different numbers of columns")));
 	}
 
-	pfree(values1);
-	pfree(nulls1);
-	pfree(values2);
-	pfree(nulls2);
+	stack_buffer_free(values1);
+	stack_buffer_free(nulls1);
+	stack_buffer_free(values2);
+	stack_buffer_free(nulls2);
 	ReleaseTupleDesc(tupdesc1);
 	ReleaseTupleDesc(tupdesc2);
 
@@ -1089,6 +1100,8 @@ record_eq(PG_FUNCTION_ARGS)
 	int			i2;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from the tuples */
@@ -1145,11 +1158,11 @@ record_eq(PG_FUNCTION_ARGS)
 	}
 
 	/* Break down the tuples into fields */
-	values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
-	nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+	values1 = stack_buffer_alloc_array(Datum, ncolumns1);
+	nulls1 = stack_buffer_alloc_array(bool, ncolumns1);
 	heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
-	values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
-	nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+	values2 = stack_buffer_alloc_array(Datum, ncolumns2);
+	nulls2 = stack_buffer_alloc_array(bool, ncolumns2);
 	heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
 
 	/*
@@ -1265,10 +1278,10 @@ record_eq(PG_FUNCTION_ARGS)
 					 errmsg("cannot compare record types with different numbers of columns")));
 	}
 
-	pfree(values1);
-	pfree(nulls1);
-	pfree(values2);
-	pfree(nulls2);
+	stack_buffer_free(values1);
+	stack_buffer_free(nulls1);
+	stack_buffer_free(values2);
+	stack_buffer_free(nulls2);
 	ReleaseTupleDesc(tupdesc1);
 	ReleaseTupleDesc(tupdesc2);
 
@@ -1371,6 +1384,8 @@ record_image_cmp(FunctionCallInfo fcinfo)
 	int			i2;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Extract type info from the tuples */
 	tupType1 = HeapTupleHeaderGetTypeId(record1);
 	tupTypmod1 = HeapTupleHeaderGetTypMod(record1);
@@ -1425,11 +1440,11 @@ record_image_cmp(FunctionCallInfo fcinfo)
 	}
 
 	/* Break down the tuples into fields */
-	values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
-	nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+	values1 = stack_buffer_alloc_array(Datum, ncolumns1);
+	nulls1 = stack_buffer_alloc_array(bool, ncolumns1);
 	heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
-	values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
-	nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+	values2 = stack_buffer_alloc_array(Datum, ncolumns2);
+	nulls2 = stack_buffer_alloc_array(bool, ncolumns2);
 	heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
 
 	/*
@@ -1568,10 +1583,10 @@ record_image_cmp(FunctionCallInfo fcinfo)
 					 errmsg("cannot compare record types with different numbers of columns")));
 	}
 
-	pfree(values1);
-	pfree(nulls1);
-	pfree(values2);
-	pfree(nulls2);
+	stack_buffer_free(values1);
+	stack_buffer_free(nulls1);
+	stack_buffer_free(values2);
+	stack_buffer_free(nulls2);
 	ReleaseTupleDesc(tupdesc1);
 	ReleaseTupleDesc(tupdesc2);
 
@@ -1617,6 +1632,8 @@ record_image_eq(PG_FUNCTION_ARGS)
 	int			i2;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Extract type info from the tuples */
 	tupType1 = HeapTupleHeaderGetTypeId(record1);
 	tupTypmod1 = HeapTupleHeaderGetTypMod(record1);
@@ -1671,11 +1688,11 @@ record_image_eq(PG_FUNCTION_ARGS)
 	}
 
 	/* Break down the tuples into fields */
-	values1 = (Datum *) palloc(ncolumns1 * sizeof(Datum));
-	nulls1 = (bool *) palloc(ncolumns1 * sizeof(bool));
+	values1 = stack_buffer_alloc_array(Datum, ncolumns1);
+	nulls1 = stack_buffer_alloc_array(bool, ncolumns1);
 	heap_deform_tuple(&tuple1, tupdesc1, values1, nulls1);
-	values2 = (Datum *) palloc(ncolumns2 * sizeof(Datum));
-	nulls2 = (bool *) palloc(ncolumns2 * sizeof(bool));
+	values2 = stack_buffer_alloc_array(Datum, ncolumns2);
+	nulls2 = stack_buffer_alloc_array(bool, ncolumns2);
 	heap_deform_tuple(&tuple2, tupdesc2, values2, nulls2);
 
 	/*
@@ -1753,10 +1770,10 @@ record_image_eq(PG_FUNCTION_ARGS)
 					 errmsg("cannot compare record types with different numbers of columns")));
 	}
 
-	pfree(values1);
-	pfree(nulls1);
-	pfree(values2);
-	pfree(nulls2);
+	stack_buffer_free(values1);
+	stack_buffer_free(nulls1);
+	stack_buffer_free(values2);
+	stack_buffer_free(nulls2);
 	ReleaseTupleDesc(tupdesc1);
 	ReleaseTupleDesc(tupdesc2);
 
@@ -1822,6 +1839,8 @@ hash_record(PG_FUNCTION_ARGS)
 	Datum	   *values;
 	bool	   *nulls;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from tuple */
@@ -1863,8 +1882,8 @@ hash_record(PG_FUNCTION_ARGS)
 	}
 
 	/* Break down the tuple into fields */
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 	heap_deform_tuple(&tuple, tupdesc, values, nulls);
 
 	for (int i = 0; i < ncolumns; i++)
@@ -1918,8 +1937,8 @@ hash_record(PG_FUNCTION_ARGS)
 		result = (result << 5) - result + element_hash;
 	}
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	/* Avoid leaking memory when handed toasted input. */
@@ -1943,6 +1962,8 @@ hash_record_extended(PG_FUNCTION_ARGS)
 	Datum	   *values;
 	bool	   *nulls;
 
+	DECLARE_STACK_BUFFER();
+
 	check_stack_depth();		/* recurses for record-type columns */
 
 	/* Extract type info from tuple */
@@ -1984,8 +2005,8 @@ hash_record_extended(PG_FUNCTION_ARGS)
 	}
 
 	/* Break down the tuple into fields */
-	values = palloc_array(Datum, ncolumns);
-	nulls = palloc_array(bool, ncolumns);
+	values = stack_buffer_alloc_array(Datum, ncolumns);
+	nulls = stack_buffer_alloc_array(bool, ncolumns);
 	heap_deform_tuple(&tuple, tupdesc, values, nulls);
 
 	for (int i = 0; i < ncolumns; i++)
@@ -2041,8 +2062,8 @@ hash_record_extended(PG_FUNCTION_ARGS)
 		result = (result << 5) - result + element_hash;
 	}
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	ReleaseTupleDesc(tupdesc);
 
 	/* Avoid leaking memory when handed toasted input. */
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 6298a37f88e..8e87d37058d 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -67,6 +67,7 @@
 #include "utils/rel.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 #include "utils/varlena.h"
@@ -1298,6 +1299,8 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 	char	   *str;
 	char	   *sep;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Fetch the pg_index tuple by the Oid of the index
 	 */
@@ -1358,9 +1361,9 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 
 		exprsDatum = SysCacheGetAttrNotNull(INDEXRELID, ht_idx,
 											Anum_pg_index_indexprs);
-		exprsString = TextDatumGetCString(exprsDatum);
+		exprsString = stack_buffer_text_datum_to_cstring(exprsDatum);
 		indexprs = (List *) stringToNode(exprsString);
-		pfree(exprsString);
+		stack_buffer_free(exprsString);
 	}
 	else
 		indexprs = NIL;
@@ -1554,9 +1557,9 @@ pg_get_indexdef_worker(Oid indexrelid, int colno,
 			/* Convert text string to node tree */
 			predDatum = SysCacheGetAttrNotNull(INDEXRELID, ht_idx,
 											   Anum_pg_index_indpred);
-			predString = TextDatumGetCString(predDatum);
+			predString = stack_buffer_text_datum_to_cstring(predDatum);
 			node = (Node *) stringToNode(predString);
-			pfree(predString);
+			stack_buffer_free(predString);
 
 			/* Deparse */
 			str = deparse_expression_pretty(node, context, false, false,
@@ -1671,6 +1674,8 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
 	bool		has_exprs;
 	int			ncolumns;
 
+	DECLARE_STACK_BUFFER();
+
 	statexttup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(statextid));
 
 	if (!HeapTupleIsValid(statexttup))
@@ -1697,9 +1702,9 @@ pg_get_statisticsobj_worker(Oid statextid, bool columns_only, bool missing_ok)
 
 		exprsDatum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
 											Anum_pg_statistic_ext_stxexprs);
-		exprsString = TextDatumGetCString(exprsDatum);
+		exprsString = stack_buffer_text_datum_to_cstring(exprsDatum);
 		exprs = (List *) stringToNode(exprsString);
-		pfree(exprsString);
+		stack_buffer_free(exprsString);
 	}
 	else
 		exprs = NIL;
@@ -1848,6 +1853,8 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
 	char	   *tmp;
 	ArrayBuildState *astate = NULL;
 
+	DECLARE_STACK_BUFFER();
+
 	statexttup = SearchSysCache1(STATEXTOID, ObjectIdGetDatum(statextid));
 
 	if (!HeapTupleIsValid(statexttup))
@@ -1870,9 +1877,9 @@ pg_get_statisticsobjdef_expressions(PG_FUNCTION_ARGS)
 	 */
 	datum = SysCacheGetAttrNotNull(STATEXTOID, statexttup,
 								   Anum_pg_statistic_ext_stxexprs);
-	tmp = TextDatumGetCString(datum);
+	tmp = stack_buffer_text_datum_to_cstring(datum);
 	exprs = (List *) stringToNode(tmp);
-	pfree(tmp);
+	stack_buffer_free(tmp);
 
 	context = deparse_context_for(get_relation_name(statextrec->stxrelid),
 								  statextrec->stxrelid);
@@ -1950,6 +1957,8 @@ pg_get_partkeydef_worker(Oid relid, int prettyFlags,
 	char	   *str;
 	char	   *sep;
 
+	DECLARE_STACK_BUFFER();
+
 	tuple = SearchSysCache1(PARTRELID, ObjectIdGetDatum(relid));
 	if (!HeapTupleIsValid(tuple))
 	{
@@ -1984,14 +1993,14 @@ pg_get_partkeydef_worker(Oid relid, int prettyFlags,
 
 		exprsDatum = SysCacheGetAttrNotNull(PARTRELID, tuple,
 											Anum_pg_partitioned_table_partexprs);
-		exprsString = TextDatumGetCString(exprsDatum);
+		exprsString = stack_buffer_text_datum_to_cstring(exprsDatum);
 		partexprs = (List *) stringToNode(exprsString);
 
 		if (!IsA(partexprs, List))
 			elog(ERROR, "unexpected node type found in partexprs: %d",
 				 (int) nodeTag(partexprs));
 
-		pfree(exprsString);
+		stack_buffer_free(exprsString);
 	}
 	else
 		partexprs = NIL;
@@ -2717,13 +2726,15 @@ pg_get_expr_worker(text *expr, Oid relid, int prettyFlags)
 	Relation	rel = NULL;
 	char	   *str;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Convert input pg_node_tree (really TEXT) object to C string */
-	exprstr = text_to_cstring(expr);
+	exprstr = stack_buffer_text_to_cstring(expr);
 
 	/* Convert expression to node tree */
 	node = (Node *) stringToNode(exprstr);
 
-	pfree(exprstr);
+	stack_buffer_free(exprstr);
 
 	/*
 	 * Throw error if the input is a querytree rather than an expression tree.
@@ -3315,6 +3326,8 @@ print_function_arguments(StringInfo buf, HeapTuple proctup,
 	ListCell   *nextargdefault = NULL;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	numargs = get_func_arg_info(proctup,
 								&argtypes, &argnames, &argmodes);
 
@@ -3331,9 +3344,9 @@ print_function_arguments(StringInfo buf, HeapTuple proctup,
 		{
 			char	   *str;
 
-			str = TextDatumGetCString(proargdefaults);
+			str = stack_buffer_text_datum_to_cstring(proargdefaults);
 			argdefaults = castNode(List, stringToNode(str));
-			pfree(str);
+			stack_buffer_free(str);
 			nextargdefault = list_head(argdefaults);
 			/* nlackdefaults counts only *input* arguments lacking defaults */
 			nlackdefaults = proc->pronargs - list_length(argdefaults);
@@ -3506,6 +3519,8 @@ pg_get_function_arg_default(PG_FUNCTION_ARGS)
 	bool		isnull;
 	int			nth_default;
 
+	DECLARE_STACK_BUFFER();
+
 	proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcid));
 	if (!HeapTupleIsValid(proctup))
 		PG_RETURN_NULL();
@@ -3531,9 +3546,9 @@ pg_get_function_arg_default(PG_FUNCTION_ARGS)
 		PG_RETURN_NULL();
 	}
 
-	str = TextDatumGetCString(proargdefaults);
+	str = stack_buffer_text_datum_to_cstring(proargdefaults);
 	argdefaults = castNode(List, stringToNode(str));
-	pfree(str);
+	stack_buffer_free(str);
 
 	proc = (Form_pg_proc) GETSTRUCT(proctup);
 
diff --git a/src/backend/utils/adt/tsvector_op.c b/src/backend/utils/adt/tsvector_op.c
index 71c7c7d3b3c..86c29f730ba 100644
--- a/src/backend/utils/adt/tsvector_op.c
+++ b/src/backend/utils/adt/tsvector_op.c
@@ -31,6 +31,7 @@
 #include "utils/builtins.h"
 #include "utils/regproc.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"

 typedef struct
@@ -2537,6 +2538,8 @@ ts_process_call(FuncCallContext *funcctx)
 	TSVectorStat *st;
 	StatEntry  *entry;
 
+	DECLARE_STACK_BUFFER();
+
 	st = (TSVectorStat *) funcctx->user_fctx;
 
 	entry = walkStatEntryTree(st);
@@ -2549,9 +2552,8 @@ ts_process_call(FuncCallContext *funcctx)
 		char		nentry[16];
 		HeapTuple	tuple;
 
-		values[0] = palloc(entry->lenlexeme + 1);
-		memcpy(values[0], entry->lexeme, entry->lenlexeme);
-		(values[0])[entry->lenlexeme] = '\0';
+		values[0] = stack_buffer_strdup_with_len(entry->lexeme,
+												 entry->lenlexeme);
 		sprintf(ndoc, "%d", entry->ndoc);
 		values[1] = ndoc;
 		sprintf(nentry, "%d", entry->nentry);
@@ -2560,7 +2562,7 @@ ts_process_call(FuncCallContext *funcctx)
 		tuple = BuildTupleFromCStrings(funcctx->attinmeta, values);
 		result = HeapTupleGetDatum(tuple);
 
-		pfree(values[0]);
+		stack_buffer_free(values[0]);
 
 		/* mark entry as already visited */
 		entry->ndoc = 0;
-- 
2.53.0

From 943060f91d3edcb1c94837a09dfb5566bf2daded Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 04:15:05 +1300
Subject: [PATCH v2 09/19] Use stack buffer in some access method code.

Mechanical changes including:
* temporary strings
* temporary datum, null, key lists etc
* WAL construction
---
 src/backend/access/brin/brin_tuple.c          | 19 +++++++++------
 src/backend/access/common/heaptuple.c         | 21 ++++++++++-------
 src/backend/access/gin/ginscan.c              |  7 ++++--
 src/backend/access/hash/hashfunc.c            | 12 ++++++----
 src/backend/access/heap/heapam.c              | 12 ++++++----
 src/backend/access/heap/heapam_handler.c      | 11 +++++----
 src/backend/access/index/genam.c              | 23 ++++++++++++-------
 src/backend/access/nbtree/nbtpreprocesskeys.c | 23 +++++++++++--------
 src/backend/access/nbtree/nbtxlog.c           |  9 +++++---
 9 files changed, 87 insertions(+), 50 deletions(-)

diff --git a/src/backend/access/brin/brin_tuple.c b/src/backend/access/brin/brin_tuple.c
index 69c233c62eb..13ff1d3f5d6 100644
--- a/src/backend/access/brin/brin_tuple.c
+++ b/src/backend/access/brin/brin_tuple.c
@@ -40,6 +40,7 @@
 #include "access/tupmacs.h"
 #include "utils/datum.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"

 /*
@@ -117,14 +118,16 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 	int			nuntoasted = 0;
 #endif
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(brdesc->bd_totalstored > 0);
 
-	values = palloc_array(Datum, brdesc->bd_totalstored);
-	nulls = palloc0_array(bool, brdesc->bd_totalstored);
-	phony_nullbitmap = palloc_array(bits8, BITMAPLEN(brdesc->bd_totalstored));
+	values = stack_buffer_alloc_array(Datum, brdesc->bd_totalstored);
+	nulls = stack_buffer_alloc0_array(bool, brdesc->bd_totalstored);
+	phony_nullbitmap = stack_buffer_alloc_array(bits8, BITMAPLEN(brdesc->bd_totalstored));
 
 #ifdef TOAST_INDEX_HACK
-	untoasted_values = palloc_array(Datum, brdesc->bd_totalstored);
+	untoasted_values = stack_buffer_alloc_array(Datum, brdesc->bd_totalstored);
 #endif
 
 	/*
@@ -307,13 +310,15 @@ brin_form_tuple(BrinDesc *brdesc, BlockNumber blkno, BrinMemTuple *tuple,
 					phony_nullbitmap);
 
 	/* done with these */
-	pfree(values);
-	pfree(nulls);
-	pfree(phony_nullbitmap);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
+	stack_buffer_free(phony_nullbitmap);
 
 #ifdef TOAST_INDEX_HACK
 	for (i = 0; i < nuntoasted; i++)
 		pfree(DatumGetPointer(untoasted_values[i]));
+
+	stack_buffer_free(untoasted_values);
 #endif
 
 	/*
diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c
index 11bec20e82e..12f820056ba 100644
--- a/src/backend/access/common/heaptuple.c
+++ b/src/backend/access/common/heaptuple.c
@@ -65,6 +65,7 @@
 #include "utils/expandeddatum.h"
 #include "utils/hsearch.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"

 /*
@@ -1219,6 +1220,8 @@ heap_modify_tuple(HeapTuple tuple,
 	bool	   *isnull;
 	HeapTuple	newTuple;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * allocate and fill values and isnull arrays from either the tuple or the
 	 * repl information, as appropriate.
@@ -1230,8 +1233,8 @@ heap_modify_tuple(HeapTuple tuple,
 	 * O(N^2) if there are many non-replaced columns, so it seems better to
 	 * err on the side of linear cost.
 	 */
-	values = palloc_array(Datum, numberOfAttributes);
-	isnull = palloc_array(bool, numberOfAttributes);
+	values = stack_buffer_alloc_array(Datum, numberOfAttributes);
+	isnull = stack_buffer_alloc_array(bool, numberOfAttributes);
 
 	heap_deform_tuple(tuple, tupleDesc, values, isnull);
 
@@ -1249,8 +1252,8 @@ heap_modify_tuple(HeapTuple tuple,
 	 */
 	newTuple = heap_form_tuple(tupleDesc, values, isnull);
 
-	pfree(values);
-	pfree(isnull);
+	stack_buffer_free(values);
+	stack_buffer_free(isnull);
 
 	/*
 	 * copy the identification info of the old tuple: t_ctid, t_self
@@ -1288,12 +1291,14 @@ heap_modify_tuple_by_cols(HeapTuple tuple,
 	HeapTuple	newTuple;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * allocate and fill values and isnull arrays from the tuple, then replace
 	 * selected columns from the input arrays.
 	 */
-	values = palloc_array(Datum, numberOfAttributes);
-	isnull = palloc_array(bool, numberOfAttributes);
+	values = stack_buffer_alloc_array(Datum, numberOfAttributes);
+	isnull = stack_buffer_alloc_array(bool, numberOfAttributes);
 
 	heap_deform_tuple(tuple, tupleDesc, values, isnull);
 
@@ -1312,8 +1317,8 @@ heap_modify_tuple_by_cols(HeapTuple tuple,
 	 */
 	newTuple = heap_form_tuple(tupleDesc, values, isnull);
 
-	pfree(values);
-	pfree(isnull);
+	stack_buffer_free(values);
+	stack_buffer_free(isnull);
 
 	/*
 	 * copy the identification info of the old tuple: t_ctid, t_self
diff --git a/src/backend/access/gin/ginscan.c b/src/backend/access/gin/ginscan.c
index fb929761ab7..6d3d2737080 100644
--- a/src/backend/access/gin/ginscan.c
+++ b/src/backend/access/gin/ginscan.c
@@ -20,6 +20,7 @@
 #include "pgstat.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"

 IndexScanDesc
@@ -420,10 +421,12 @@ ginNewScanKey(IndexScanDesc scan)
 		int			iNormalKey;
 		int			iExcludeOnly;
 
+		DECLARE_STACK_BUFFER();
+
 		/* We'd better have made at least one normal key */
 		Assert(numExcludeOnly < so->nkeys);
 		/* Make a temporary array to hold the re-ordered scan keys */
-		tmpkeys = (GinScanKey) palloc(so->nkeys * sizeof(GinScanKeyData));
+		tmpkeys = stack_buffer_alloc_array(GinScanKeyData, so->nkeys);
 		/* Re-order the keys ... */
 		iNormalKey = 0;
 		iExcludeOnly = so->nkeys - numExcludeOnly;
@@ -446,7 +449,7 @@ ginNewScanKey(IndexScanDesc scan)
 		Assert(iExcludeOnly == so->nkeys);
 		/* ... and copy them back to so->keys[] */
 		memcpy(so->keys, tmpkeys, so->nkeys * sizeof(GinScanKeyData));
-		pfree(tmpkeys);
+		stack_buffer_free(tmpkeys);
 	}
 
 	/*
diff --git a/src/backend/access/hash/hashfunc.c b/src/backend/access/hash/hashfunc.c
index 575342a21b6..cda9ce28780 100644
--- a/src/backend/access/hash/hashfunc.c
+++ b/src/backend/access/hash/hashfunc.c
@@ -31,6 +31,7 @@
 #include "utils/float.h"
 #include "utils/fmgrprotos.h"
 #include "utils/pg_locale.h"
+#include "utils/stack_buffer.h"
 #include "varatt.h"
 
 /*
@@ -295,9 +296,10 @@ hashtext(PG_FUNCTION_ARGS)
 		const char *keydata = VARDATA_ANY(key);
 		size_t		keylen = VARSIZE_ANY_EXHDR(key);
 
+		DECLARE_STACK_BUFFER();
 
 		bsize = pg_strnxfrm(NULL, 0, keydata, keylen, mylocale);
-		buf = palloc(bsize + 1);
+		buf = stack_buffer_alloc(bsize + 1);
 
 		rsize = pg_strnxfrm(buf, bsize + 1, keydata, keylen, mylocale);
 
@@ -312,7 +314,7 @@ hashtext(PG_FUNCTION_ARGS)
 		 */
 		result = hash_any((uint8_t *) buf, bsize + 1);
 
-		pfree(buf);
+		stack_buffer_free(buf);
 	}
 
 	/* Avoid leaking memory for toasted inputs */
@@ -351,8 +353,10 @@ hashtextextended(PG_FUNCTION_ARGS)
 		const char *keydata = VARDATA_ANY(key);
 		size_t		keylen = VARSIZE_ANY_EXHDR(key);
 
+		DECLARE_STACK_BUFFER();
+
 		bsize = pg_strnxfrm(NULL, 0, keydata, keylen, mylocale);
-		buf = palloc(bsize + 1);
+		buf = stack_buffer_alloc(bsize + 1);
 
 		rsize = pg_strnxfrm(buf, bsize + 1, keydata, keylen, mylocale);
 
@@ -368,7 +372,7 @@ hashtextextended(PG_FUNCTION_ARGS)
 		result = hash_any_extended((uint8_t *) buf, bsize + 1,
 								   PG_GETARG_INT64(1));
 
-		pfree(buf);
+		stack_buffer_free(buf);
 	}
 
 	PG_FREE_IF_COPY(key, 0);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 8f1c11a9350..13171df183c 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -53,6 +53,7 @@
 #include "utils/injection_point.h"
 #include "utils/inval.h"
 #include "utils/spccache.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"

@@ -8768,11 +8769,13 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate)
 	int			ncopied = 0;
 	int			nblocksfavorable = 0;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(delstate->bottomup);
 	Assert(delstate->ndeltids > 0);
 
 	/* Calculate per-heap-block count of TIDs */
-	blockgroups = palloc_array(IndexDeleteCounts, delstate->ndeltids);
+	blockgroups = stack_buffer_alloc_array(IndexDeleteCounts, delstate->ndeltids);
 	for (int i = 0; i < delstate->ndeltids; i++)
 	{
 		TM_IndexDelete *ideltid = &delstate->deltids[i];
@@ -8845,7 +8848,8 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate)
 	/* Sort groups and rearrange caller's deltids array */
 	qsort(blockgroups, nblockgroups, sizeof(IndexDeleteCounts),
 		  bottomup_sort_and_shrink_cmp);
-	reordereddeltids = palloc(delstate->ndeltids * sizeof(TM_IndexDelete));
+	reordereddeltids = stack_buffer_alloc_array(TM_IndexDelete,
+												delstate->ndeltids);
 
 	nblockgroups = Min(BOTTOMUP_MAX_NBLOCKS, nblockgroups);
 	/* Determine number of favorable blocks at the start of final deltids */
@@ -8867,8 +8871,8 @@ bottomup_sort_and_shrink(TM_IndexDeleteOp *delstate)
 		   sizeof(TM_IndexDelete) * ncopied);
 	delstate->ndeltids = ncopied;
 
-	pfree(reordereddeltids);
-	pfree(blockgroups);
+	stack_buffer_free(reordereddeltids);
+	stack_buffer_free(blockgroups);
 
 	return nblocksfavorable;
 }
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 5137d2510ea..cfb06ab82ac 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -45,6 +45,7 @@
 #include "storage/smgr.h"
 #include "utils/builtins.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"
 
 static void reform_and_rewrite_tuple(HeapTuple tuple,
 									 Relation OldHeap, Relation NewHeap,
@@ -706,6 +707,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	BufferHeapTupleTableSlot *hslot;
 	BlockNumber prev_cblock = InvalidBlockNumber;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Remember if it's a system catalog */
 	is_system_catalog = IsSystemRelation(OldHeap);
 
@@ -717,8 +720,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 
 	/* Preallocate values/isnull arrays */
 	natts = newTupDesc->natts;
-	values = palloc_array(Datum, natts);
-	isnull = palloc_array(bool, natts);
+	values = stack_buffer_alloc_array(Datum, natts);
+	isnull = stack_buffer_alloc_array(bool, natts);
 
 	/* Initialize the rewrite operation */
 	rwstate = begin_heap_rewrite(OldHeap, NewHeap, OldestXmin, *xid_cutoff,
@@ -1002,8 +1005,8 @@ heapam_relation_copy_for_cluster(Relation OldHeap, Relation NewHeap,
 	end_heap_rewrite(rwstate);
 
 	/* Clean up */
-	pfree(values);
-	pfree(isnull);
+	stack_buffer_free(values);
+	stack_buffer_free(isnull);
 }
 
 /*
diff --git a/src/backend/access/index/genam.c b/src/backend/access/index/genam.c
index 5e89b86a62c..30df0b8819b 100644
--- a/src/backend/access/index/genam.c
+++ b/src/backend/access/index/genam.c
@@ -36,6 +36,7 @@
 #include "utils/rls.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"

 /* ----------------------------------------------------------------
@@ -303,6 +304,8 @@ index_compute_xid_horizon_for_tuples(Relation irel,
 	Page		ipage = BufferGetPage(ibuf);
 	IndexTuple	itup;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(nitems > 0);
 
 	delstate.irel = irel;
@@ -310,8 +313,8 @@ index_compute_xid_horizon_for_tuples(Relation irel,
 	delstate.bottomup = false;
 	delstate.bottomupfreespace = 0;
 	delstate.ndeltids = 0;
-	delstate.deltids = palloc_array(TM_IndexDelete, nitems);
-	delstate.status = palloc_array(TM_IndexStatus, nitems);
+	delstate.deltids = stack_buffer_alloc_array(TM_IndexDelete, nitems);
+	delstate.status = stack_buffer_alloc_array(TM_IndexStatus, nitems);
 
 	/* identify what the index tuples about to be deleted point to */
 	for (int i = 0; i < nitems; i++)
@@ -340,8 +343,8 @@ index_compute_xid_horizon_for_tuples(Relation irel,
 	/* assert tableam agrees that all items are deletable */
 	Assert(delstate.ndeltids == nitems);
 
-	pfree(delstate.deltids);
-	pfree(delstate.status);
+	stack_buffer_free(delstate.deltids);
+	stack_buffer_free(delstate.status);
 
 	return snapshotConflictHorizon;
 }
@@ -433,7 +436,9 @@ systable_beginscan(Relation heapRelation,
 		int			i;
 		ScanKey		idxkey;
 
-		idxkey = palloc_array(ScanKeyData, nkeys);
+		DECLARE_STACK_BUFFER();
+
+		idxkey = stack_buffer_alloc_array(ScanKeyData, nkeys);
 
 		/* Convert attribute numbers to be index column numbers. */
 		for (i = 0; i < nkeys; i++)
@@ -459,7 +464,7 @@ systable_beginscan(Relation heapRelation,
 		index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0);
 		sysscan->scan = NULL;
 
-		pfree(idxkey);
+		stack_buffer_free(idxkey);
 	}
 	else
 	{
@@ -656,6 +661,8 @@ systable_beginscan_ordered(Relation heapRelation,
 	int			i;
 	ScanKey		idxkey;
 
+	DECLARE_STACK_BUFFER();
+
 	/* REINDEX can probably be a hard error here ... */
 	if (ReindexIsProcessingIndex(RelationGetRelid(indexRelation)))
 		ereport(ERROR,
@@ -686,7 +693,7 @@ systable_beginscan_ordered(Relation heapRelation,
 		sysscan->snapshot = NULL;
 	}
 
-	idxkey = palloc_array(ScanKeyData, nkeys);
+	idxkey = stack_buffer_alloc_array(ScanKeyData, nkeys);
 
 	/* Convert attribute numbers to be index column numbers. */
 	for (i = 0; i < nkeys; i++)
@@ -720,7 +727,7 @@ systable_beginscan_ordered(Relation heapRelation,
 	index_rescan(sysscan->iscan, idxkey, nkeys, NULL, 0);
 	sysscan->scan = NULL;
 
-	pfree(idxkey);
+	stack_buffer_free(idxkey);
 
 	return sysscan;
 }
diff --git a/src/backend/access/nbtree/nbtpreprocesskeys.c b/src/backend/access/nbtree/nbtpreprocesskeys.c
index 39c0a5d610f..b62e6892345 100644
--- a/src/backend/access/nbtree/nbtpreprocesskeys.c
+++ b/src/backend/access/nbtree/nbtpreprocesskeys.c
@@ -23,6 +23,7 @@
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"
 
 typedef struct BTScanKeyPreproc
 {
@@ -1557,6 +1558,8 @@ _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap)
 				haveReqForward,
 				haveReqBackward;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Do an initial pass over so->keyData[] that determines which keys to
 	 * keep as required.  We expect so->keyData[] to still be in attribute
@@ -1568,7 +1571,7 @@ _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap)
 	 * Any requiredness markings that we might leave on later keys/attributes
 	 * are predicated on there being required = keys on all prior columns.
 	 */
-	unmarkikey = palloc0(so->numberOfKeys * sizeof(bool));
+	unmarkikey = stack_buffer_alloc0_array(bool, so->numberOfKeys);
 	nunmark = 0;
 
 	/* Set things up for first key's attribute */
@@ -1653,14 +1656,14 @@ _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap)
 	 * Next, allocate temp arrays: one for required keys that'll remain
 	 * required, the other for all remaining keys
 	 */
-	unmarkKeys = palloc(nunmark * sizeof(ScanKeyData));
-	keepKeys = palloc((so->numberOfKeys - nunmark) * sizeof(ScanKeyData));
+	unmarkKeys = stack_buffer_alloc_array(ScanKeyData, nunmark);
+	keepKeys = stack_buffer_alloc_array(ScanKeyData, so->numberOfKeys - nunmark);
 	nunmarked = 0;
 	nkept = 0;
 	if (so->numArrayKeys)
 	{
-		unmarkOrderProcs = palloc(nunmark * sizeof(FmgrInfo));
-		keepOrderProcs = palloc((so->numberOfKeys - nunmark) * sizeof(FmgrInfo));
+		unmarkOrderProcs = stack_buffer_alloc_array(FmgrInfo, nunmark);
+		keepOrderProcs = stack_buffer_alloc_array(FmgrInfo, so->numberOfKeys - nunmark);
 	}
 
 	/*
@@ -1751,9 +1754,9 @@ _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap)
 	memcpy(so->keyData + nkept, unmarkKeys, sizeof(ScanKeyData) * nunmarked);
 
 	/* Done with temp arrays */
-	pfree(unmarkikey);
-	pfree(keepKeys);
-	pfree(unmarkKeys);
+	stack_buffer_free(unmarkikey);
+	stack_buffer_free(keepKeys);
+	stack_buffer_free(unmarkKeys);
 
 	/*
 	 * Now copy so->orderProcs[] temp entries needed by scans with = array
@@ -1781,8 +1784,8 @@ _bt_unmark_keys(IndexScanDesc scan, int *keyDataMap)
 			  _bt_reorder_array_cmp);
 
 		/* Done with temp arrays */
-		pfree(unmarkOrderProcs);
-		pfree(keepOrderProcs);
+		stack_buffer_free(unmarkOrderProcs);
+		stack_buffer_free(keepOrderProcs);
 	}
 }
 
diff --git a/src/backend/access/nbtree/nbtxlog.c b/src/backend/access/nbtree/nbtxlog.c
index dff7d286fc8..14bd0fd2fd3 100644
--- a/src/backend/access/nbtree/nbtxlog.c
+++ b/src/backend/access/nbtree/nbtxlog.c
@@ -21,6 +21,7 @@
 #include "access/xlogutils.h"
 #include "storage/standby.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"
 
 static MemoryContext opCtx;		/* working memory for operations */
 
@@ -551,13 +552,15 @@ btree_xlog_updates(Page page, OffsetNumber *updatedoffsets,
 	ItemId		itemid;
 	Size		itemsz;
 
+	DECLARE_STACK_BUFFER();
+
 	for (int i = 0; i < nupdated; i++)
 	{
 		itemid = PageGetItemId(page, updatedoffsets[i]);
 		origtuple = (IndexTuple) PageGetItem(page, itemid);
 
-		vacposting = palloc(offsetof(BTVacuumPostingData, deletetids) +
-							updates->ndeletedtids * sizeof(uint16));
+		vacposting = stack_buffer_alloc(offsetof(BTVacuumPostingData, deletetids) +
+										updates->ndeletedtids * sizeof(uint16));
 		vacposting->updatedoffset = updatedoffsets[i];
 		vacposting->itup = origtuple;
 		vacposting->ndeletedtids = updates->ndeletedtids;
@@ -573,7 +576,7 @@ btree_xlog_updates(Page page, OffsetNumber *updatedoffsets,
 			elog(PANIC, "failed to update partially dead item");
 
 		pfree(vacposting->itup);
-		pfree(vacposting);
+		stack_buffer_free(vacposting);
 
 		/* advance to next xl_btree_update from array */
 		updates = (xl_btree_update *)
-- 
2.53.0

From ced3e5fd1809e38986e769384f13c2b689cd5333 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Tue, 10 Mar 2026 17:35:17 +1300
Subject: [PATCH v2 10/19] Use stack_buffer in LLVM code.

---
 src/backend/jit/llvm/llvmjit.c      | 21 ++++++++++++++-------
 src/backend/jit/llvm/llvmjit_expr.c |  7 +++++--
 2 files changed, 19 insertions(+), 9 deletions(-)

diff --git a/src/backend/jit/llvm/llvmjit.c b/src/backend/jit/llvm/llvmjit.c
index 2e8aa4749db..957241c64b5 100644
--- a/src/backend/jit/llvm/llvmjit.c
+++ b/src/backend/jit/llvm/llvmjit.c
@@ -41,6 +41,7 @@
 #include "storage/ipc.h"
 #include "utils/memutils.h"
 #include "utils/resowner.h"
+#include "utils/stack_buffer.h"
 
 #define LLVMJIT_LLVM_CONTEXT_REUSE_MAX 100
 
@@ -495,18 +496,20 @@ llvm_copy_attributes_at_index(LLVMValueRef v_from, LLVMValueRef v_to, uint32 ind
 	int			num_attributes;
 	LLVMAttributeRef *attrs;
 
+	DECLARE_STACK_BUFFER();
+
 	num_attributes = LLVMGetAttributeCountAtIndex(v_from, index);
 
 	if (num_attributes == 0)
 		return;
 
-	attrs = palloc_array(LLVMAttributeRef, num_attributes);
+	attrs = stack_buffer_alloc_array(LLVMAttributeRef, num_attributes);
 	LLVMGetAttributesAtIndex(v_from, index, attrs);
 
 	for (int attno = 0; attno < num_attributes; attno++)
 		LLVMAddAttributeAtIndex(v_to, index, attrs[attno]);
 
-	pfree(attrs);
+	stack_buffer_free(attrs);
 }
 
 /*
@@ -1126,13 +1129,17 @@ llvm_resolve_symbols(LLVMOrcDefinitionGeneratorRef GeneratorObj, void *Ctx,
 					 LLVMOrcJITDylibRef JD, LLVMOrcJITDylibLookupFlags JDLookupFlags,
 					 LLVMOrcCLookupSet LookupSet, size_t LookupSetSize)
 {
+	LLVMErrorRef error;
+	LLVMOrcMaterializationUnitRef mu;
+	LLVMOrcCSymbolMapPairs symbols;
+
+	DECLARE_STACK_BUFFER();
+
 #if LLVM_VERSION_MAJOR > 14
-	LLVMOrcCSymbolMapPairs symbols = palloc0_array(LLVMOrcCSymbolMapPair, LookupSetSize);
+	symbols = stack_buffer_alloc0_array(LLVMOrcCSymbolMapPair, LookupSetSize);
 #else
-	LLVMOrcCSymbolMapPairs symbols = palloc0_array(LLVMJITCSymbolMapPair, LookupSetSize);
+	symbols = stack_buffer_alloc0_array(LLVMJITCSymbolMapPair, LookupSetSize);
 #endif
-	LLVMErrorRef error;
-	LLVMOrcMaterializationUnitRef mu;
 
 	for (int i = 0; i < LookupSetSize; i++)
 	{
@@ -1149,7 +1156,7 @@ llvm_resolve_symbols(LLVMOrcDefinitionGeneratorRef GeneratorObj, void *Ctx,
 	if (error != LLVMErrorSuccess)
 		LLVMOrcDisposeMaterializationUnit(mu);
 
-	pfree(symbols);
+	stack_buffer_free(symbols);
 
 	return error;
 }
diff --git a/src/backend/jit/llvm/llvmjit_expr.c b/src/backend/jit/llvm/llvmjit_expr.c
index 650f1d42a93..411e4746ee9 100644
--- a/src/backend/jit/llvm/llvmjit_expr.c
+++ b/src/backend/jit/llvm/llvmjit_expr.c
@@ -41,6 +41,7 @@
 #include "utils/fmgrtab.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"
 #include "utils/timestamp.h"
 #include "utils/typcache.h"
 #include "utils/xml.h"
@@ -3063,12 +3064,14 @@ build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, const char *funcname,
 	int			argno = 0;
 	LLVMValueRef v_ret;
 
+	DECLARE_STACK_BUFFER();
+
 	/* cheap pre-check as llvm just asserts out */
 	if (LLVMCountParams(v_fn) != (nargs + 2))
 		elog(ERROR, "parameter mismatch: %s expects %d passed %d",
 			 funcname, LLVMCountParams(v_fn), nargs + 2);
 
-	params = palloc_array(LLVMValueRef, (2 + nargs));
+	params = stack_buffer_alloc_array(LLVMValueRef, (2 + nargs));
 
 	params[argno++] = v_state;
 	params[argno++] = l_ptr_const(op, l_ptr(StructExprEvalStep));
@@ -3078,7 +3081,7 @@ build_EvalXFuncInt(LLVMBuilderRef b, LLVMModuleRef mod, const char *funcname,
 
 	v_ret = l_call(b, LLVMGetFunctionType(v_fn), v_fn, params, argno, "");
 
-	pfree(params);
+	stack_buffer_free(params);
 
 	return v_ret;
 }
-- 
2.53.0

From 4a3c54cadd459d4ac6b65e7b323775d84714649b Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Tue, 10 Mar 2026 17:37:29 +1300
Subject: [PATCH v2 11/19] Use stack buffer in transam/xact.c.

---
 src/backend/access/transam/xact.c | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index aafc53e0164..91adced5fb9 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -69,6 +69,7 @@
 #include "utils/memutils.h"
 #include "utils/relmapper.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"
 #include "utils/timeout.h"
 #include "utils/timestamp.h"
 #include "utils/typcache.h"
@@ -640,6 +641,8 @@ AssignTransactionId(TransactionState s)
 	ResourceOwner currentOwner;
 	bool		log_unknown_top = false;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Assert that caller didn't screw up */
 	Assert(!FullTransactionIdIsValid(s->fullTransactionId));
 	Assert(s->state == TRANS_INPROGRESS);
@@ -665,7 +668,7 @@ AssignTransactionId(TransactionState s)
 		TransactionState *parents;
 		size_t		parentOffset = 0;
 
-		parents = palloc_array(TransactionState, s->nestingLevel);
+		parents = stack_buffer_alloc_array(TransactionState, s->nestingLevel);
 		while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
 		{
 			parents[parentOffset++] = p;
@@ -679,7 +682,7 @@ AssignTransactionId(TransactionState s)
 		while (parentOffset != 0)
 			AssignTransactionId(parents[--parentOffset]);
 
-		pfree(parents);
+		stack_buffer_free(parents);
 	}
 
 	/*
@@ -5568,6 +5571,8 @@ SerializeTransactionState(Size maxsize, char *start_address)
 	TransactionId *workspace;
 	SerializedTransactionState *result;
 
+	DECLARE_STACK_BUFFER();
+
 	result = (SerializedTransactionState *) start_address;
 
 	result->xactIsoLevel = XactIsoLevel;
@@ -5604,7 +5609,7 @@ SerializeTransactionState(Size maxsize, char *start_address)
 		   <= maxsize);
 
 	/* Copy them to our scratch space. */
-	workspace = palloc(nxids * sizeof(TransactionId));
+	workspace = stack_buffer_alloc_array(TransactionId, nxids);
 	for (s = CurrentTransactionState; s != NULL; s = s->parent)
 	{
 		if (FullTransactionIdIsValid(s->fullTransactionId))
@@ -5623,6 +5628,8 @@ SerializeTransactionState(Size maxsize, char *start_address)
 	result->nParallelCurrentXids = nxids;
 	memcpy(&result->parallelCurrentXids[0], workspace,
 		   nxids * sizeof(TransactionId));
+
+	stack_buffer_free(workspace);
 }
 
 /*
-- 
2.53.0

From e07593473fad042cfa9da6f5b565443728af7656 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Tue, 10 Mar 2026 17:41:49 +1300
Subject: [PATCH v2 12/19] Use stack buffer in a few places in the parser.

Mechanical C string conversions and a small temporary array.
---
 src/backend/parser/parse_clause.c | 7 +++++--
 src/backend/parser/parse_func.c   | 7 +++++--
 src/backend/parser/parse_type.c   | 7 +++++--
 3 files changed, 15 insertions(+), 6 deletions(-)

diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c
index 06b65d4a605..7360da2b04b 100644
--- a/src/backend/parser/parse_clause.c
+++ b/src/backend/parser/parse_clause.c
@@ -45,6 +45,7 @@
 #include "utils/catcache.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"

@@ -693,6 +694,8 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf)
 	char	  **names;
 	int			colno;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Currently we only support XMLTABLE here.  See transformJsonTable() for
 	 * JSON_TABLE support.
@@ -733,7 +736,7 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf)
 	tf->ordinalitycol = -1;
 
 	/* Process column specs */
-	names = palloc_array(char *, list_length(rtf->columns));
+	names = stack_buffer_alloc_array(char *, list_length(rtf->columns));
 
 	colno = 0;
 	foreach(col, rtf->columns)
@@ -825,7 +828,7 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf)
 
 		colno++;
 	}
-	pfree(names);
+	stack_buffer_free(names);
 
 	/* Namespaces, if any, also need to be transformed */
 	if (rtf->namespaces != NIL)
diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c
index 24f6745923b..2f63ce55a23 100644
--- a/src/backend/parser/parse_func.c
+++ b/src/backend/parser/parse_func.c
@@ -32,6 +32,7 @@
 #include "parser/parse_type.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"

@@ -1759,15 +1760,17 @@ func_get_detail(List *funcname,
 			char	   *str;
 			List	   *defaults;
 
+			DECLARE_STACK_BUFFER();
+
 			/* shouldn't happen, FuncnameGetCandidates messed up */
 			if (best_candidate->ndargs > pform->pronargdefaults)
 				elog(ERROR, "not enough default arguments");
 
 			proargdefaults = SysCacheGetAttrNotNull(PROCOID, ftup,
 													Anum_pg_proc_proargdefaults);
-			str = TextDatumGetCString(proargdefaults);
+			str = stack_buffer_text_datum_to_cstring(proargdefaults);
 			defaults = castNode(List, stringToNode(str));
-			pfree(str);
+			stack_buffer_free(str);
 
 			/* Delete any unused defaults from the returned list */
 			if (best_candidate->argnumbers != NULL)
diff --git a/src/backend/parser/parse_type.c b/src/backend/parser/parse_type.c
index bb7eccde9fd..35234331070 100644
--- a/src/backend/parser/parse_type.c
+++ b/src/backend/parser/parse_type.c
@@ -24,6 +24,7 @@
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
 static int32 typenameTypeMod(ParseState *pstate, const TypeName *typeName,
@@ -339,6 +340,8 @@ typenameTypeMod(ParseState *pstate, const TypeName *typeName, Type typ)
 	ArrayType  *arrtypmod;
 	ParseCallbackState pcbstate;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Return prespecified typmod if no typmod expressions */
 	if (typeName->typmods == NIL)
 		return typeName->typemod;
@@ -369,7 +372,7 @@ typenameTypeMod(ParseState *pstate, const TypeName *typeName, Type typ)
 	 * Currently, we allow simple numeric constants, string literals, and
 	 * identifiers; possibly this list could be extended.
 	 */
-	datums = palloc_array(Datum, list_length(typeName->typmods));
+	datums = stack_buffer_alloc_array(Datum, list_length(typeName->typmods));
 	n = 0;
 	foreach(l, typeName->typmods)
 	{
@@ -421,7 +424,7 @@ typenameTypeMod(ParseState *pstate, const TypeName *typeName, Type typ)
 
 	cancel_parser_errposition_callback(&pcbstate);
 
-	pfree(datums);
+	stack_buffer_free(datums);
 	pfree(arrtypmod);
 
 	return result;
-- 
2.53.0

From 674b354dc120e3fd938c2b9f6f8a8dbb364c8ed7 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Tue, 10 Mar 2026 17:42:46 +1300
Subject: [PATCH v2 13/19] Use stack buffer in fmgr.c.

Mechanical C string conversions.
---
 src/backend/utils/fmgr/fmgr.c | 17 +++++++++++------
 1 file changed, 11 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/fmgr/fmgr.c b/src/backend/utils/fmgr/fmgr.c
index 4e26df7c63a..1614a683877 100644
--- a/src/backend/utils/fmgr/fmgr.c
+++ b/src/backend/utils/fmgr/fmgr.c
@@ -32,6 +32,7 @@
 #include "utils/fmgrtab.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
 /*
@@ -154,6 +155,8 @@ fmgr_info_cxt_security(Oid functionId, FmgrInfo *finfo, MemoryContext mcxt,
 	Datum		prosrcdatum;
 	char	   *prosrc;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * fn_oid *must* be filled in last.  Some code assumes that if fn_oid is
 	 * valid, the whole struct is valid.  Some FmgrInfo struct's do survive
@@ -229,14 +232,14 @@ fmgr_info_cxt_security(Oid functionId, FmgrInfo *finfo, MemoryContext mcxt,
 			 */
 			prosrcdatum = SysCacheGetAttrNotNull(PROCOID, procedureTuple,
 												 Anum_pg_proc_prosrc);
-			prosrc = TextDatumGetCString(prosrcdatum);
+			prosrc = stack_buffer_text_datum_to_cstring(prosrcdatum);
 			fbp = fmgr_lookupByName(prosrc);
 			if (fbp == NULL)
 				ereport(ERROR,
 						(errcode(ERRCODE_UNDEFINED_FUNCTION),
 						 errmsg("internal function \"%s\" is not in internal lookup table",
 								prosrc)));
-			pfree(prosrc);
+			stack_buffer_free(prosrc);
 			/* Should we check that nargs, strict, retset match the table? */
 			finfo->fn_addr = fbp->func;
 			/* note this policy is also assumed in fast path above */
@@ -370,6 +373,8 @@ fmgr_info_C_lang(Oid functionId, FmgrInfo *finfo, HeapTuple procedureTuple)
 				   *probinstring;
 		void	   *libraryhandle;
 
+		DECLARE_STACK_BUFFER();
+
 		/*
 		 * Get prosrc and probin strings (link symbol and library filename).
 		 * While in general these columns might be null, that's not allowed
@@ -377,11 +382,11 @@ fmgr_info_C_lang(Oid functionId, FmgrInfo *finfo, HeapTuple procedureTuple)
 		 */
 		prosrcattr = SysCacheGetAttrNotNull(PROCOID, procedureTuple,
 											Anum_pg_proc_prosrc);
-		prosrcstring = TextDatumGetCString(prosrcattr);
+		prosrcstring = stack_buffer_text_datum_to_cstring(prosrcattr);
 
 		probinattr = SysCacheGetAttrNotNull(PROCOID, procedureTuple,
 											Anum_pg_proc_probin);
-		probinstring = TextDatumGetCString(probinattr);
+		probinstring = stack_buffer_text_datum_to_cstring(probinattr);
 
 		/* Look up the function itself */
 		user_fn = load_external_function(probinstring, prosrcstring, true,
@@ -393,8 +398,8 @@ fmgr_info_C_lang(Oid functionId, FmgrInfo *finfo, HeapTuple procedureTuple)
 		/* Cache the addresses for later calls */
 		record_C_func(procedureTuple, user_fn, inforec);
 
-		pfree(prosrcstring);
-		pfree(probinstring);
+		stack_buffer_free(prosrcstring);
+		stack_buffer_free(probinstring);
 	}
 
 	switch (inforec->api_version)
-- 
2.53.0

From b94fed5ea689c680329a6b436e5e16171e0a5c7b Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 04:19:54 +1300
Subject: [PATCH v2 14/19] Use stack buffer in logical replication.

Mechanical changes to temporary datum and null arrays.
---
 src/backend/replication/logical/reorderbuffer.c | 16 +++++++++-------
 src/backend/replication/logical/worker.c        | 10 ++++++++--
 2 files changed, 17 insertions(+), 9 deletions(-)

diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 4c230bcc8e4..5f0888fc159 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -113,6 +113,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/relfilenumbermap.h"
+#include "utils/stack_buffer.h"
 #include "utils/wait_event.h"
 
 /*
@@ -5092,6 +5093,8 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 	HeapTuple	newtup;
 	Size		old_size;
 
+	DECLARE_STACK_BUFFER();
+
 	/* no toast tuples changed */
 	if (txn->toast_hash == NULL)
 		return;
@@ -5122,10 +5125,9 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 
 	toast_desc = RelationGetDescr(toast_rel);
 
-	/* should we allocate from stack instead? */
-	attrs = palloc0_array(Datum, desc->natts);
-	isnull = palloc0_array(bool, desc->natts);
-	free = palloc0_array(bool, desc->natts);
+	attrs = stack_buffer_alloc0_array(Datum, desc->natts);
+	isnull = stack_buffer_alloc0_array(bool, desc->natts);
+	free = stack_buffer_alloc0_array(bool, desc->natts);
 
 	newtup = change->data.tp.newtuple;
 
@@ -5247,9 +5249,9 @@ ReorderBufferToastReplace(ReorderBuffer *rb, ReorderBufferTXN *txn,
 		if (free[natt])
 			pfree(DatumGetPointer(attrs[natt]));
 	}
-	pfree(attrs);
-	pfree(free);
-	pfree(isnull);
+	stack_buffer_free(attrs);
+	stack_buffer_free(free);
+	stack_buffer_free(isnull);
 
 	MemoryContextSwitchTo(oldcontext);
 
diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c
index 033858752d9..54828cb7e0e 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -294,6 +294,7 @@
 #include "utils/rel.h"
 #include "utils/rls.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/usercontext.h"
 #include "utils/wait_event.h"
@@ -972,14 +973,16 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate,
 	ExprState **defexprs;
 	ExprContext *econtext;
 
+	DECLARE_STACK_BUFFER();
+
 	econtext = GetPerTupleExprContext(estate);
 
 	/* We got all the data via replication, no need to evaluate anything. */
 	if (num_phys_attrs == rel->remoterel.natts)
 		return;
 
-	defmap = palloc_array(int, num_phys_attrs);
-	defexprs = palloc_array(ExprState *, num_phys_attrs);
+	defmap = stack_buffer_alloc_array(int, num_phys_attrs);
+	defexprs = stack_buffer_alloc_array(ExprState *, num_phys_attrs);
 
 	Assert(rel->attrmap->maplen == num_phys_attrs);
 	for (attnum = 0; attnum < num_phys_attrs; attnum++)
@@ -1010,6 +1013,9 @@ slot_fill_defaults(LogicalRepRelMapEntry *rel, EState *estate,
 	for (i = 0; i < num_defaults; i++)
 		slot->tts_values[defmap[i]] =
 			ExecEvalExpr(defexprs[i], econtext, &slot->tts_isnull[defmap[i]]);
+
+	stack_buffer_free(defmap);
+	stack_buffer_free(defexprs);
 }
 
 /*
-- 
2.53.0

From dcc2eb693c70936627e8c9af32b93bcc35d6cd5e Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 04:21:02 +1300
Subject: [PATCH v2 15/19] Use stack buffer in some FDW code.

Mechanical changes to temporary datum and null arrays.
---
 contrib/file_fdw/file_fdw.c         | 11 +++++++----
 contrib/postgres_fdw/postgres_fdw.c |  9 +++++++--
 2 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/contrib/file_fdw/file_fdw.c b/contrib/file_fdw/file_fdw.c
index 33a37d832ce..a6958d6a4c0 100644
--- a/contrib/file_fdw/file_fdw.c
+++ b/contrib/file_fdw/file_fdw.c
@@ -40,6 +40,7 @@
 #include "utils/memutils.h"
 #include "utils/rel.h"
 #include "utils/sampling.h"
+#include "utils/stack_buffer.h"
 #include "utils/varlena.h"
 
 PG_MODULE_MAGIC_EXT(
@@ -1202,12 +1203,14 @@ file_acquire_sample_rows(Relation onerel, int elevel,
 	MemoryContext oldcontext = CurrentMemoryContext;
 	MemoryContext tupcontext;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(onerel);
 	Assert(targrows > 0);
 
 	tupDesc = RelationGetDescr(onerel);
-	values = (Datum *) palloc(tupDesc->natts * sizeof(Datum));
-	nulls = (bool *) palloc(tupDesc->natts * sizeof(bool));
+	values = stack_buffer_alloc_array(Datum, tupDesc->natts);
+	nulls = stack_buffer_alloc_array(bool, tupDesc->natts);
 
 	/* Fetch options of foreign table */
 	fileGetOptions(RelationGetRelid(onerel), &filename, &is_program, &options);
@@ -1324,8 +1327,8 @@ file_acquire_sample_rows(Relation onerel, int elevel,
 
 	EndCopyFrom(cstate);
 
-	pfree(values);
-	pfree(nulls);
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 
 	/*
 	 * Emit some interesting relation info
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 60d90329a65..c817b718616 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -48,6 +48,7 @@
 #include "utils/rel.h"
 #include "utils/sampling.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"
 
 PG_MODULE_MAGIC_EXT(
 					.name = "postgres_fdw",
@@ -7507,6 +7508,8 @@ make_tuple_from_result_row(PGresult *res,
 	ListCell   *lc;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(row < PQntuples(res));
 
 	/*
@@ -7528,8 +7531,8 @@ make_tuple_from_result_row(PGresult *res,
 		tupdesc = fsstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor;
 	}
 
-	values = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
-	nulls = (bool *) palloc(tupdesc->natts * sizeof(bool));
+	values = stack_buffer_alloc0_array(Datum, tupdesc->natts);
+	nulls = stack_buffer_alloc_array(bool, tupdesc->natts);
 	/* Initialize to nulls for any columns not present in result */
 	memset(nulls, true, tupdesc->natts * sizeof(bool));
 
@@ -7631,6 +7634,8 @@ make_tuple_from_result_row(PGresult *res,
 	HeapTupleHeaderSetCmin(tuple->t_data, InvalidTransactionId);
 
 	/* Clean up */
+	stack_buffer_free(values);
+	stack_buffer_free(nulls);
 	MemoryContextReset(temp_context);
 
 	return tuple;
-- 
2.53.0

From 6e8968cbadeec651963ed9b310551685799bf364 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 04:44:33 +1300
Subject: [PATCH v2 16/19] Use stack buffer in partition pruning.

---
 src/backend/partitioning/partbounds.c | 60 +++++++++++++++++----------
 src/backend/partitioning/partprune.c  | 17 +++++---
 2 files changed, 49 insertions(+), 28 deletions(-)

diff --git a/src/backend/partitioning/partbounds.c b/src/backend/partitioning/partbounds.c
index 0ca312ac27d..fc0ae489aaa 100644
--- a/src/backend/partitioning/partbounds.c
+++ b/src/backend/partitioning/partbounds.c
@@ -39,6 +39,7 @@
 #include "utils/partcache.h"
 #include "utils/ruleutils.h"
 #include "utils/snapmgr.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
 /*
@@ -354,13 +355,15 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			greatest_modulus;
 	Datum	   *boundDatums;
 
+	DECLARE_STACK_BUFFER();
+
 	boundinfo = palloc0_object(PartitionBoundInfoData);
 	boundinfo->strategy = key->strategy;
 	/* No special hash partitions. */
 	boundinfo->null_index = -1;
 	boundinfo->default_index = -1;
 
-	hbounds = palloc_array(PartitionHashBound, nparts);
+	hbounds = stack_buffer_alloc_array(PartitionHashBound, nparts);
 
 	/* Convert from node to the internal representation */
 	for (i = 0; i < nparts; i++)
@@ -422,7 +425,7 @@ create_hash_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 		(*mapping)[hbounds[i].index] = i;
 	}
-	pfree(hbounds);
+	stack_buffer_free(hbounds);
 
 	return boundinfo;
 }
@@ -471,6 +474,8 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	int			null_index = -1;
 	Datum	   *boundDatums;
 
+	DECLARE_STACK_BUFFER();
+
 	boundinfo = palloc0_object(PartitionBoundInfoData);
 	boundinfo->strategy = key->strategy;
 	/* Will be set correctly below. */
@@ -479,7 +484,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 
 	ndatums = get_non_null_list_datum_count(boundspecs, nparts);
 	all_values = (PartitionListValue *)
-		palloc(ndatums * sizeof(PartitionListValue));
+		stack_buffer_alloc_array(PartitionListValue, ndatums);
 
 	/* Create a unified list of non-null values across all partitions. */
 	for (j = 0, i = 0; i < nparts; i++)
@@ -566,7 +571,7 @@ create_list_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		boundinfo->indexes[i] = (*mapping)[orig_index];
 	}
 
-	pfree(all_values);
+	stack_buffer_free(all_values);
 
 	/*
 	 * Set the canonical value for null_index, if any.
@@ -688,6 +693,8 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	Datum	   *boundDatums;
 	PartitionRangeDatumKind *boundKinds;
 
+	DECLARE_STACK_BUFFER();
+
 	boundinfo = palloc0_object(PartitionBoundInfoData);
 	boundinfo->strategy = key->strategy;
 	/* There is no special null-accepting range partition. */
@@ -695,7 +702,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 	/* Will be set correctly below. */
 	boundinfo->default_index = -1;
 
-	all_bounds = palloc0_array(PartitionRangeBound *, 2 * nparts);
+	all_bounds = stack_buffer_alloc0_array(PartitionRangeBound *, 2 * nparts);
 
 	/* Create a unified list of range bounds across all the partitions. */
 	ndatums = 0;
@@ -735,8 +742,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 			  key);
 
 	/* Save distinct bounds from all_bounds into rbounds. */
-	rbounds = (PartitionRangeBound **)
-		palloc(ndatums * sizeof(PartitionRangeBound *));
+	rbounds = stack_buffer_alloc_array(PartitionRangeBound *, ndatums);
 	k = 0;
 	prev = NULL;
 	for (i = 0; i < ndatums; i++)
@@ -785,7 +791,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		prev = cur;
 	}
 
-	pfree(all_bounds);
+	stack_buffer_free(all_bounds);
 
 	/* Update ndatums to hold the count of distinct datums. */
 	ndatums = k;
@@ -858,7 +864,7 @@ create_range_bounds(PartitionBoundSpec **boundspecs, int nparts,
 		}
 	}
 
-	pfree(rbounds);
+	stack_buffer_free(rbounds);
 
 	/* Set the canonical value for default_index, if any. */
 	if (default_index != -1)
@@ -2384,9 +2390,11 @@ fix_merged_indexes(PartitionMap *outer_map, PartitionMap *inner_map,
 	int			i;
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(nmerged > 0);
 
-	new_indexes = palloc_array(int, nmerged);
+	new_indexes = stack_buffer_alloc_array(int, nmerged);
 	for (i = 0; i < nmerged; i++)
 		new_indexes[i] = -1;
 
@@ -2419,7 +2427,7 @@ fix_merged_indexes(PartitionMap *outer_map, PartitionMap *inner_map,
 			lfirst_int(lc) = new_indexes[merged_index];
 	}
 
-	pfree(new_indexes);
+	stack_buffer_free(new_indexes);
 }
 
 /*
@@ -2442,12 +2450,14 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 	int			max_nparts;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(nmerged > 0);
 	Assert(*outer_parts == NIL);
 	Assert(*inner_parts == NIL);
 
-	outer_indexes = palloc_array(int, nmerged);
-	inner_indexes = palloc_array(int, nmerged);
+	outer_indexes = stack_buffer_alloc_array(int, nmerged);
+	inner_indexes = stack_buffer_alloc_array(int, nmerged);
 	for (i = 0; i < nmerged; i++)
 		outer_indexes[i] = inner_indexes[i] = -1;
 
@@ -2500,8 +2510,8 @@ generate_matching_part_pairs(RelOptInfo *outer_rel, RelOptInfo *inner_rel,
 							   inner_rel->part_rels[inner_index] : NULL);
 	}
 
-	pfree(outer_indexes);
-	pfree(inner_indexes);
+	stack_buffer_free(outer_indexes);
+	stack_buffer_free(inner_indexes);
 }
 
 /*
@@ -5104,6 +5114,8 @@ calculate_partition_bound_for_merge(Relation parent,
 	PartitionKey key = RelationGetPartitionKey(parent);
 	PartitionBoundSpec *bound;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(!spec->is_default);
 
 	switch (key->strategy)
@@ -5115,7 +5127,7 @@ calculate_partition_bound_for_merge(Relation parent,
 				int			nparts = list_length(partOids);
 				List	   *bounds = NIL;
 
-				lower_bounds = palloc0_array(PartitionRangeBound *, nparts);
+				lower_bounds = stack_buffer_alloc0_array(PartitionRangeBound *, nparts);
 
 				/*
 				 * Create an array of lower bounds and a list of
@@ -5164,7 +5176,7 @@ calculate_partition_bound_for_merge(Relation parent,
 				spec->upperdatums =
 					((PartitionBoundSpec *) list_nth(bounds, lower_bounds[nparts - 1]->index))->upperdatums;
 
-				pfree(lower_bounds);
+				stack_buffer_free(lower_bounds);
 				list_free(bounds);
 				break;
 			}
@@ -5741,6 +5753,8 @@ check_partitions_for_split(Relation parent,
 	 */
 	int			nparts = 0;
 
+	DECLARE_STACK_BUFFER();
+
 	key = RelationGetPartitionKey(parent);
 	strategy = get_partition_strategy(key);
 
@@ -5754,7 +5768,7 @@ check_partitions_for_split(Relation parent,
 	 * Make an array new_parts with new partitions except the DEFAULT
 	 * partition.
 	 */
-	new_parts = palloc0_array(SinglePartitionSpec *, list_length(partlist));
+	new_parts = stack_buffer_alloc0_array(SinglePartitionSpec *, list_length(partlist));
 
 	/* isSplitPartDefault flag: is split partition a DEFAULT partition? */
 	isSplitPartDefault = (defaultPartOid == splitPartOid);
@@ -5784,7 +5798,7 @@ check_partitions_for_split(Relation parent,
 		 * all partitions in ascending order of their bounds (we compare the
 		 * lower bound only).
 		 */
-		lower_bounds = palloc0_array(PartitionRangeBound *, nparts);
+		lower_bounds = stack_buffer_alloc0_array(PartitionRangeBound *, nparts);
 
 		/* Create an array of lower bounds. */
 		for (i = 0; i < nparts; i++)
@@ -5799,12 +5813,12 @@ check_partitions_for_split(Relation parent,
 
 		/* Reorder the array of partitions. */
 		tmp_new_parts = new_parts;
-		new_parts = palloc0_array(SinglePartitionSpec *, nparts);
+		new_parts = stack_buffer_alloc0_array(SinglePartitionSpec *, nparts);
 		for (i = 0; i < nparts; i++)
 			new_parts[i] = tmp_new_parts[lower_bounds[i]->index];
 
-		pfree(tmp_new_parts);
-		pfree(lower_bounds);
+		stack_buffer_free(tmp_new_parts);
+		stack_buffer_free(lower_bounds);
 	}
 
 	for (i = 0; i < nparts; i++)
@@ -5871,5 +5885,5 @@ check_partitions_for_split(Relation parent,
 												  new_parts, nparts, pstate);
 	}
 
-	pfree(new_parts);
+	stack_buffer_free(new_parts);
 }
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 6d979a08fd3..8c4bf1d657e 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -54,6 +54,7 @@
 #include "partitioning/partprune.h"
 #include "utils/array.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"

 /*
@@ -233,6 +234,8 @@ make_partition_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	ListCell   *lc;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Scan the subpaths to see which ones are scans of partition child
 	 * relations, and identify their parent partitioned rels.  (Note: we must
@@ -246,7 +249,8 @@ make_partition_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	 * that zero can represent an un-filled array entry.
 	 */
 	allpartrelids = NIL;
-	relid_subplan_map = palloc0_array(int, root->simple_rel_array_size);
+	relid_subplan_map = stack_buffer_alloc0_array(int,
+												  root->simple_rel_array_size);
 
 	i = 1;
 	foreach(lc, subpaths)
@@ -327,7 +331,7 @@ make_partition_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 		}
 	}
 
-	pfree(relid_subplan_map);
+	stack_buffer_free(relid_subplan_map);
 
 	/*
 	 * If none of the partition hierarchies had any useful run-time pruning
@@ -457,6 +461,8 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	int			rti;
 	int			i;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Examine each partitioned rel, constructing a temporary array to map
 	 * from planner relids to index of the partitioned rel, and building a
@@ -465,7 +471,8 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	 * In this phase we discover whether runtime pruning is needed at all; if
 	 * not, we can avoid doing further work.
 	 */
-	relid_subpart_map = palloc0_array(int, root->simple_rel_array_size);
+	relid_subpart_map = stack_buffer_alloc0_array(int,
+												  root->simple_rel_array_size);
 
 	i = 1;
 	rti = -1;
@@ -624,7 +631,7 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	if (!doruntimeprune)
 	{
 		/* No run-time pruning required. */
-		pfree(relid_subpart_map);
+		stack_buffer_free(relid_subpart_map);
 		return NIL;
 	}
 
@@ -719,7 +726,7 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 		pinfo->leafpart_rti_map = leafpart_rti_map;
 	}
 
-	pfree(relid_subpart_map);
+	stack_buffer_free(relid_subpart_map);
 
 	*matchedsubplans = subplansfound;
 
-- 
2.53.0

From 5cef70069ca8e0b0e5a2e45bdd7cf177404a882d Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 03:39:11 +1300
Subject: [PATCH v2 17/19] Use stack buffer for simple arrays in planner.

---
 src/backend/optimizer/path/allpaths.c     |  7 +++--
 src/backend/optimizer/path/equivclass.c   |  9 ++++--
 src/backend/optimizer/path/indxpath.c     | 14 ++++++---
 src/backend/optimizer/path/pathkeys.c     | 15 +++++----
 src/backend/optimizer/plan/analyzejoins.c |  7 ++++-
 src/backend/optimizer/plan/planner.c      | 37 ++++++++++++++---------
 src/backend/optimizer/prep/prepagg.c      |  7 +++--
 src/backend/optimizer/prep/prepunion.c    |  7 +++--
 src/backend/optimizer/util/clauses.c      | 17 +++++++----
 src/backend/optimizer/util/plancat.c      |  7 +++--
 10 files changed, 84 insertions(+), 43 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 5eceb321828..1c8efc5e3cd 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -49,6 +49,7 @@
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"

 /* Bitmask flags for pushdown_safety_info.unsafeFlags */
@@ -1021,6 +1022,8 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 	int			nattrs;
 	ListCell   *l;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Guard against stack overflow due to overly deep inheritance tree. */
 	check_stack_depth();
 
@@ -1065,7 +1068,7 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 	parent_rows = 0;
 	parent_size = 0;
 	nattrs = rel->max_attr - rel->min_attr + 1;
-	parent_attrsizes = (double *) palloc0(nattrs * sizeof(double));
+	parent_attrsizes = stack_buffer_alloc0_array(double, nattrs);
 
 	foreach(l, root->append_rel_list)
 	{
@@ -1296,7 +1299,7 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
 		set_dummy_rel_pathlist(rel);
 	}
 
-	pfree(parent_attrsizes);
+	stack_buffer_free(parent_attrsizes);
 }
 
 /*
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index e3697df51a2..28a3162b98e 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -32,6 +32,7 @@
 #include "optimizer/restrictinfo.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"

 static EquivalenceMember *make_eq_member(EquivalenceClass *ec,
@@ -1373,6 +1374,8 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
 	EquivalenceMember **prev_ems;
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * We scan the EC members once and track the last-seen member for each
 	 * base relation.  When we see another member of the same base relation,
@@ -1381,8 +1384,8 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
 	 * ordering would succeed.  XXX FIXME: use a UNION-FIND algorithm similar
 	 * to the way we build merged ECs.  (Use a list-of-lists for each rel.)
 	 */
-	prev_ems = (EquivalenceMember **)
-		palloc0(root->simple_rel_array_size * sizeof(EquivalenceMember *));
+	prev_ems = stack_buffer_alloc0_array(EquivalenceMember *,
+										 root->simple_rel_array_size);
 
 	/* We don't expect any children yet */
 	Assert(ec->ec_childmembers == NULL);
@@ -1444,7 +1447,7 @@ generate_base_implied_equalities_no_const(PlannerInfo *root,
 		prev_ems[relid] = cur_em;
 	}
 
-	pfree(prev_ems);
+	stack_buffer_free(prev_ems);
 
 	/*
 	 * We also have to make sure that all the Vars used in the member clauses
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index 67d9dc35f44..b01c48650c8 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -34,6 +34,7 @@
 #include "optimizer/restrictinfo.h"
 #include "utils/lsyscache.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"

 /* XXX see PartCollMatchesExprColl */
@@ -1281,6 +1282,8 @@ group_similar_or_args(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo)
 	List	   *result = NIL;
 	Index		relid = rel->relid;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(IsA(rinfo->orclause, BoolExpr));
 	orargs = ((BoolExpr *) rinfo->orclause)->args;
 	n = list_length(orargs);
@@ -1291,7 +1294,7 @@ group_similar_or_args(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo)
 	 * which will be used to sort these arguments at the next step.
 	 */
 	i = -1;
-	matches = palloc_array(OrArgIndexMatch, n);
+	matches = stack_buffer_alloc_array(OrArgIndexMatch, n);
 	foreach(lc, orargs)
 	{
 		Node	   *arg = lfirst(lc);
@@ -1421,7 +1424,7 @@ group_similar_or_args(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo)
 	 */
 	if (!matched)
 	{
-		pfree(matches);
+		stack_buffer_free(matches);
 		return orargs;
 	}
 
@@ -1526,7 +1529,7 @@ group_similar_or_args(PlannerInfo *root, RelOptInfo *rel, RestrictInfo *rinfo)
 			group_start = i;
 		}
 	}
-	pfree(matches);
+	stack_buffer_free(matches);
 	return result;
 }
 
@@ -1794,6 +1797,8 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths)
 				j;
 	ListCell   *l;
 
+	DECLARE_STACK_BUFFER();
+
 	Assert(npaths > 0);			/* else caller error */
 	if (npaths == 1)
 		return (Path *) linitial(paths);	/* easy case */
@@ -1853,7 +1858,7 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths)
 	 * same set of clauses; keep only the cheapest-to-scan of any such groups.
 	 * The surviving paths are put into an array for qsort'ing.
 	 */
-	pathinfoarray = palloc_array(PathClauseUsage *, npaths);
+	pathinfoarray = stack_buffer_alloc_array(PathClauseUsage *, npaths);
 	clauselist = NIL;
 	npaths = 0;
 	foreach(l, paths)
@@ -1979,6 +1984,7 @@ choose_bitmap_and(PlannerInfo *root, RelOptInfo *rel, List *paths)
 		/* some easy cleanup (we don't try real hard though) */
 		list_free(qualsofar);
 	}
+	stack_buffer_free(pathinfoarray);
 
 	if (list_length(bestpaths) == 1)
 		return (Path *) linitial(bestpaths);	/* no need for AND */
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index 5eb71635d15..a0e3194f6fd 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -26,6 +26,7 @@
 #include "optimizer/paths.h"
 #include "partitioning/partbounds.h"
 #include "rewrite/rewriteManip.h"
+#include "utils/stack_buffer.h"
 #include "utils/lsyscache.h"
 
 /* Consider reordering of GROUP BY keys? */
@@ -1668,6 +1669,8 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
 	ListCell   *lc;
 	int			j;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Might have no mergeclauses */
 	if (nClauses == 0)
 		return NIL;
@@ -1676,8 +1679,8 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
 	 * Make arrays of the ECs used by the mergeclauses (dropping any
 	 * duplicates) and their "popularity" scores.
 	 */
-	ecs = (EquivalenceClass **) palloc(nClauses * sizeof(EquivalenceClass *));
-	scores = (int *) palloc(nClauses * sizeof(int));
+	ecs = stack_buffer_alloc_array(EquivalenceClass *, nClauses);
+	scores = stack_buffer_alloc_array(int, nClauses);
 	necs = 0;
 
 	foreach(lc, mergeclauses)
@@ -1784,8 +1787,8 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
 			pathkeys = list_copy_head(root->query_pathkeys, matches);
 
 			/* we have all of the join pathkeys, so nothing more to do */
-			pfree(ecs);
-			pfree(scores);
+			stack_buffer_free(ecs);
+			stack_buffer_free(scores);
 
 			return pathkeys;
 		}
@@ -1827,8 +1830,8 @@ select_outer_pathkeys_for_merge(PlannerInfo *root,
 		pathkeys = lappend(pathkeys, pathkey);
 	}
 
-	pfree(ecs);
-	pfree(scores);
+	stack_buffer_free(ecs);
+	stack_buffer_free(scores);
 
 	return pathkeys;
 }
diff --git a/src/backend/optimizer/plan/analyzejoins.c b/src/backend/optimizer/plan/analyzejoins.c
index 12e9ed0d0c7..e3d462af416 100644
--- a/src/backend/optimizer/plan/analyzejoins.c
+++ b/src/backend/optimizer/plan/analyzejoins.c
@@ -34,6 +34,7 @@
 #include "parser/parse_agg.h"
 #include "rewrite/rewriteManip.h"
 #include "utils/lsyscache.h"
+#include "utils/stack_buffer.h"
 
 /*
  * Utility structure.  A sorting procedure is needed to simplify the search
@@ -2314,6 +2315,8 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 	int			j;
 	int			numRels;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Collect indexes of base relations of the join tree */
 	foreach(jl, joinlist)
 	{
@@ -2363,7 +2366,7 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 	 * In order to find relations with the same oid we first build an array of
 	 * candidates and then sort it by oid.
 	 */
-	candidates = palloc_array(SelfJoinCandidate, numRels);
+	candidates = stack_buffer_alloc_array(SelfJoinCandidate, numRels);
 	i = -1;
 	j = 0;
 	while ((i = bms_next_member(relids, i)) >= 0)
@@ -2431,6 +2434,8 @@ remove_self_joins_recurse(PlannerInfo *root, List *joinlist, Relids toRemove)
 
 	Assert(bms_is_empty(relids));
 
+	stack_buffer_free(candidates);
+
 	return toRemove;
 }
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 42604a0f75c..24ad41d4ec6 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -63,6 +63,7 @@
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"
 
 /* GUC parameters */
 double		cursor_tuple_fraction = DEFAULT_CURSOR_TUPLE_FRACTION;
@@ -3020,6 +3021,8 @@ extract_rollup_sets(List *groupingSets)
 	ListCell   *lc1 = list_head(groupingSets);
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Start by stripping out empty sets.  The algorithm doesn't require this,
 	 * but the planner currently needs all empty sets to be returned in the
@@ -3053,10 +3056,10 @@ extract_rollup_sets(List *groupingSets)
 	 * to leave 0 free for the NIL node in the graph algorithm.
 	 *----------
 	 */
-	orig_sets = palloc0((num_sets_raw + 1) * sizeof(List *));
-	set_masks = palloc0((num_sets_raw + 1) * sizeof(Bitmapset *));
-	adjacency = palloc0((num_sets_raw + 1) * sizeof(short *));
-	adjacency_buf = palloc((num_sets_raw + 1) * sizeof(short));
+	orig_sets = stack_buffer_alloc0_array(List *, num_sets_raw + 1);
+	set_masks = stack_buffer_alloc0_array(Bitmapset *, num_sets_raw + 1);
+	adjacency = stack_buffer_alloc0_array(short *, num_sets_raw + 1);
+	adjacency_buf = stack_buffer_alloc_array(short, num_sets_raw + 1);
 
 	j_size = 0;
 	j = 0;
@@ -3141,7 +3144,7 @@ extract_rollup_sets(List *groupingSets)
 	 * pair_vu[v] = u (both will be true, but we check both so that we can do
 	 * it in one pass)
 	 */
-	chains = palloc0((num_sets + 1) * sizeof(int));
+	chains = stack_buffer_alloc0_array(int, num_sets + 1);
 
 	for (i = 1; i <= num_sets; ++i)
 	{
@@ -3157,7 +3160,7 @@ extract_rollup_sets(List *groupingSets)
 	}
 
 	/* build result lists. */
-	results = palloc0((num_chains + 1) * sizeof(List *));
+	results = stack_buffer_alloc0_array(List *, (num_chains + 1));
 
 	for (i = 1; i <= num_sets; ++i)
 	{
@@ -3183,17 +3186,17 @@ extract_rollup_sets(List *groupingSets)
 	 * tied up a nontrivial amount of memory.)
 	 */
 	BipartiteMatchFree(state);
-	pfree(results);
-	pfree(chains);
+	stack_buffer_free(results);
+	stack_buffer_free(chains);
 	for (i = 1; i <= num_sets; ++i)
 		if (adjacency[i])
 			pfree(adjacency[i]);
-	pfree(adjacency);
-	pfree(adjacency_buf);
-	pfree(orig_sets);
+	stack_buffer_free(adjacency);
+	stack_buffer_free(adjacency_buf);
+	stack_buffer_free(orig_sets);
 	for (i = 1; i <= num_sets; ++i)
 		bms_free(set_masks[i]);
-	pfree(set_masks);
+	stack_buffer_free(set_masks);
 
 	return result;
 }
@@ -6033,8 +6036,12 @@ select_active_windows(PlannerInfo *root, WindowFuncLists *wflists)
 	List	   *result = NIL;
 	ListCell   *lc;
 	int			nActive = 0;
-	WindowClauseSortData *actives = palloc_array(WindowClauseSortData,
-												 list_length(windowClause));
+	WindowClauseSortData *actives;
+
+	DECLARE_STACK_BUFFER();
+
+	actives = stack_buffer_alloc_array(WindowClauseSortData,
+									   list_length(windowClause));
 
 	/* First, construct an array of the active windows */
 	foreach(lc, windowClause)
@@ -6093,7 +6100,7 @@ select_active_windows(PlannerInfo *root, WindowFuncLists *wflists)
 	for (int i = 0; i < nActive; i++)
 		result = lappend(result, actives[i].wc);
 
-	pfree(actives);
+	stack_buffer_free(actives);
 
 	return result;
 }
diff --git a/src/backend/optimizer/prep/prepagg.c b/src/backend/optimizer/prep/prepagg.c
index 3737cc15ba1..f1d0466504a 100644
--- a/src/backend/optimizer/prep/prepagg.c
+++ b/src/backend/optimizer/prep/prepagg.c
@@ -49,6 +49,7 @@
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 
 static bool preprocess_aggrefs_walker(Node *node, PlannerInfo *root);
@@ -525,11 +526,13 @@ GetAggInitVal(Datum textInitVal, Oid transtype)
 	char	   *strInitVal;
 	Datum		initVal;
 
+	DECLARE_STACK_BUFFER();
+
 	getTypeInputInfo(transtype, &typinput, &typioparam);
-	strInitVal = TextDatumGetCString(textInitVal);
+	strInitVal = stack_buffer_text_datum_to_cstring(textInitVal);
 	initVal = OidInputFunctionCall(typinput, strInitVal,
 								   typioparam, -1);
-	pfree(strInitVal);
+	stack_buffer_free(strInitVal);
 	return initVal;
 }
 
diff --git a/src/backend/optimizer/prep/prepunion.c b/src/backend/optimizer/prep/prepunion.c
index f50c296e3d9..9816e315bab 100644
--- a/src/backend/optimizer/prep/prepunion.c
+++ b/src/backend/optimizer/prep/prepunion.c
@@ -38,6 +38,7 @@
 #include "optimizer/tlist.h"
 #include "parser/parse_coerce.h"
 #include "utils/selfuncs.h"
+#include "utils/stack_buffer.h"

 static RelOptInfo *recurse_set_operations(Node *setOp, PlannerInfo *root,
@@ -1628,13 +1629,15 @@ generate_append_tlist(List *colTypes, List *colCollations,
 	ListCell   *tlistl;
 	int32	   *colTypmods;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * First extract typmods to use.
 	 *
 	 * If the inputs all agree on type and typmod of a particular column, use
 	 * that typmod; else use -1.
 	 */
-	colTypmods = palloc_array(int32, list_length(colTypes));
+	colTypmods = stack_buffer_alloc_array(int32, list_length(colTypes));
 
 	foreach(tlistl, input_tlists)
 	{
@@ -1705,7 +1708,7 @@ generate_append_tlist(List *colTypes, List *colCollations,
 		tlist = lappend(tlist, tle);
 	}
 
-	pfree(colTypmods);
+	stack_buffer_free(colTypmods);
 
 	return tlist;
 }
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index f0f8e2515ec..4b50c56991d 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -59,6 +59,7 @@
 #include "utils/jsonpath.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"
 #include "utils/typcache.h"
 
@@ -5069,11 +5070,13 @@ fetch_function_defaults(HeapTuple func_tuple)
 	Datum		proargdefaults;
 	char	   *str;
 
+	DECLARE_STACK_BUFFER();
+
 	proargdefaults = SysCacheGetAttrNotNull(PROCOID, func_tuple,
 											Anum_pg_proc_proargdefaults);
-	str = TextDatumGetCString(proargdefaults);
+	str = stack_buffer_text_datum_to_cstring(proargdefaults);
 	defaults = castNode(List, stringToNode(str));
-	pfree(str);
+	stack_buffer_free(str);
 	return defaults;
 }
 
@@ -6273,10 +6276,12 @@ make_SAOP_expr(Oid oper, Node *leftexpr, Oid coltype, Oid arraycollid,
 		int			dims[1] = {list_length(exprs)};
 		int			lbs[1] = {1};
 
+		DECLARE_STACK_BUFFER();
+
 		get_typlenbyvalalign(coltype, &typlen, &typbyval, &typalign);
 
-		elems = palloc_array(Datum, list_length(exprs));
-		nulls = palloc_array(bool, list_length(exprs));
+		elems = stack_buffer_alloc_array(Datum, list_length(exprs));
+		nulls = stack_buffer_alloc_array(bool, list_length(exprs));
 		foreach_node(Const, value, exprs)
 		{
 			elems[i] = value->constvalue;
@@ -6289,8 +6294,8 @@ make_SAOP_expr(Oid oper, Node *leftexpr, Oid coltype, Oid arraycollid,
 									   -1, PointerGetDatum(arrayConst),
 									   false, false);
 
-		pfree(elems);
-		pfree(nulls);
+		stack_buffer_free(elems);
+		stack_buffer_free(nulls);
 		list_free(exprs);
 	}
 
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index b2fbd6a082b..a080ba0f241 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -51,6 +51,7 @@
 #include "utils/lsyscache.h"
 #include "utils/partcache.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"
 #include "utils/snapmgr.h"
 #include "utils/syscache.h"
 
@@ -1788,9 +1789,11 @@ get_relation_statistics(PlannerInfo *root, RelOptInfo *rel,
 			{
 				char	   *exprsString;
 
-				exprsString = TextDatumGetCString(datum);
+				DECLARE_STACK_BUFFER();
+
+				exprsString = stack_buffer_text_datum_to_cstring(datum);
 				exprs = (List *) stringToNode(exprsString);
-				pfree(exprsString);
+				stack_buffer_free(exprsString);
 
 				/*
 				 * Modify the copies we obtain from the relcache to have the
-- 
2.53.0

From 9a8d0bd1806694651b02cc994810bc973dcfc026 Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 02:35:03 +1300
Subject: [PATCH v2 18/19] Use stack buffer for tlist indexes in planner.

---
 src/backend/optimizer/plan/setrefs.c | 86 +++++++++++++++++++---------
 1 file changed, 58 insertions(+), 28 deletions(-)

diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index 1b5b9b5ed9c..bc048c397a2 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -28,6 +28,7 @@
 #include "parser/parse_relation.h"
 #include "rewrite/rewriteManip.h"
 #include "tcop/utility.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"

@@ -166,7 +167,15 @@ static void set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset);
 static void set_param_references(PlannerInfo *root, Plan *plan);
 static Node *convert_combining_aggrefs(Node *node, void *context);
 static void set_dummy_tlist_references(Plan *plan, int rtoffset);
-static indexed_tlist *build_tlist_index(List *tlist);
+static size_t tlist_index_size(List *tlist);
+static indexed_tlist *fill_tlist_index(indexed_tlist *itlist, List *tlist);
+#define build_tlist_index(tlist) \
+	fill_tlist_index(stack_buffer_alloc(tlist_index_size(tlist)), (tlist))
+#define build_tlist_index_other_vars(tlist, ignore_rel) \
+	fill_tlist_index_other_vars(stack_buffer_alloc(tlist_index_size(tlist)), \
+								(tlist), (ignore_rel))
+#define free_tlist_index(itlist) \
+	stack_buffer_free(itlist)
 static Var *search_indexed_tlist_for_var(Var *var,
 										 indexed_tlist *itlist,
 										 int newvarno,
@@ -643,6 +652,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 {
 	ListCell   *l;
 
+	DECLARE_STACK_BUFFER();
+
 	if (plan == NULL)
 		return NULL;
 
@@ -1182,7 +1193,7 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset)
 									  linitial_int(splan->resultRelations),
 									  rtoffset, NRM_EQUAL, NUM_EXEC_QUAL(plan));
 
-					pfree(itlist);
+					free_tlist_index(itlist);
 
 					splan->exclRelTlist =
 						fix_scan_list(root, splan->exclRelTlist, rtoffset, 1);
@@ -1383,6 +1394,8 @@ set_indexonlyscan_references(PlannerInfo *root,
 	List	   *stripped_indextlist;
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Vars in the plan node's targetlist, qual, and recheckqual must only
 	 * reference columns that the index AM can actually return.  To ensure
@@ -1436,7 +1449,7 @@ set_indexonlyscan_references(PlannerInfo *root,
 	plan->indextlist = fix_scan_list(root, plan->indextlist,
 									 rtoffset, NUM_EXEC_TLIST((Plan *) plan));
 
-	pfree(index_itlist);
+	free_tlist_index(index_itlist);
 
 	return (Plan *) plan;
 }
@@ -1643,6 +1656,8 @@ set_foreignscan_references(PlannerInfo *root,
 						   ForeignScan *fscan,
 						   int rtoffset)
 {
+	DECLARE_STACK_BUFFER();
+
 	/* Adjust scanrelid if it's valid */
 	if (fscan->scan.scanrelid > 0)
 		fscan->scan.scanrelid += rtoffset;
@@ -1687,7 +1702,7 @@ set_foreignscan_references(PlannerInfo *root,
 						   rtoffset,
 						   NRM_EQUAL,
 						   NUM_EXEC_QUAL((Plan *) fscan));
-		pfree(itlist);
+		free_tlist_index(itlist);
 		/* fdw_scan_tlist itself just needs fix_scan_list() adjustments */
 		fscan->fdw_scan_tlist =
 			fix_scan_list(root, fscan->fdw_scan_tlist,
@@ -1732,6 +1747,8 @@ set_customscan_references(PlannerInfo *root,
 {
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Adjust scanrelid if it's valid */
 	if (cscan->scan.scanrelid > 0)
 		cscan->scan.scanrelid += rtoffset;
@@ -1765,7 +1782,7 @@ set_customscan_references(PlannerInfo *root,
 						   rtoffset,
 						   NRM_EQUAL,
 						   NUM_EXEC_QUAL((Plan *) cscan));
-		pfree(itlist);
+		free_tlist_index(itlist);
 		/* custom_scan_tlist itself just needs fix_scan_list() adjustments */
 		cscan->custom_scan_tlist =
 			fix_scan_list(root, cscan->custom_scan_tlist,
@@ -2028,6 +2045,8 @@ set_hash_references(PlannerInfo *root, Plan *plan, int rtoffset)
 	Plan	   *outer_plan = plan->lefttree;
 	indexed_tlist *outer_itlist;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * Hash's hashkeys are used when feeding tuples into the hashtable,
 	 * therefore have them reference Hash's outer plan (which itself is the
@@ -2042,6 +2061,7 @@ set_hash_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   rtoffset,
 					   NRM_EQUAL,
 					   NUM_EXEC_QUAL(plan));
+	free_tlist_index(outer_itlist);
 
 	/* Hash doesn't project */
 	set_dummy_tlist_references(plan, rtoffset);
@@ -2408,6 +2428,8 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 	indexed_tlist *outer_itlist;
 	indexed_tlist *inner_itlist;
 
+	DECLARE_STACK_BUFFER();
+
 	outer_itlist = build_tlist_index(outer_plan->targetlist);
 	inner_itlist = build_tlist_index(inner_plan->targetlist);
 
@@ -2527,8 +2549,8 @@ set_join_references(PlannerInfo *root, Join *join, int rtoffset)
 									(join->jointype == JOIN_INNER ? NRM_EQUAL : NRM_SUPERSET),
 									NUM_EXEC_QUAL((Plan *) join));
 
-	pfree(outer_itlist);
-	pfree(inner_itlist);
+	free_tlist_index(outer_itlist);
+	free_tlist_index(inner_itlist);
 }
 
 /*
@@ -2557,6 +2579,8 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 	List	   *output_targetlist;
 	ListCell   *l;
 
+	DECLARE_STACK_BUFFER();
+
 	subplan_itlist = build_tlist_index(subplan->targetlist);
 
 	/*
@@ -2627,7 +2651,7 @@ set_upper_references(PlannerInfo *root, Plan *plan, int rtoffset)
 					   NRM_EQUAL,
 					   NUM_EXEC_QUAL(plan));
 
-	pfree(subplan_itlist);
+	free_tlist_index(subplan_itlist);
 }
 
 /*
@@ -2815,7 +2839,19 @@ set_dummy_tlist_references(Plan *plan, int rtoffset)

 /*
- * build_tlist_index --- build an index data structure for a child tlist
+ * tlist_index_size --- compute memory required to an indexed_tlist.
+ */
+static size_t
+tlist_index_size(List *tlist)
+{
+	/* Require data structure with enough slots for all tlist entries */
+	return offsetof(indexed_tlist, vars) +
+		list_length(tlist) * sizeof(tlist_vinfo);
+}
+
+
+/*
+ * fill_tlist_index --- build an index data structure for a child tlist
  *
  * In most cases, subplan tlists will be "flat" tlists with only Vars,
  * so we try to optimize that case by extracting information about Vars
@@ -2825,20 +2861,16 @@ set_dummy_tlist_references(Plan *plan, int rtoffset)
  *
  * The result of this function is an indexed_tlist struct to pass to
  * search_indexed_tlist_for_var() and siblings.
- * When done, the indexed_tlist may be freed with a single pfree().
+ *
+ * The macro build_tlist_index() can be used to allocate memory in a stack
+ * buffer.  When done, the indexed_tlist may be freed with free_tlist_index().
  */
 static indexed_tlist *
-build_tlist_index(List *tlist)
+fill_tlist_index(indexed_tlist *itlist, List *tlist)
 {
-	indexed_tlist *itlist;
 	tlist_vinfo *vinfo;
 	ListCell   *l;
 
-	/* Create data structure with enough slots for all tlist entries */
-	itlist = (indexed_tlist *)
-		palloc(offsetof(indexed_tlist, vars) +
-			   list_length(tlist) * sizeof(tlist_vinfo));
-
 	itlist->tlist = tlist;
 	itlist->has_ph_vars = false;
 	itlist->has_non_vars = false;
@@ -2871,25 +2903,19 @@ build_tlist_index(List *tlist)
 }
 
 /*
- * build_tlist_index_other_vars --- build a restricted tlist index
+ * fill_tlist_index_other_vars --- build a restricted tlist index
  *
- * This is like build_tlist_index, but we only index tlist entries that
+ * This is like fill_tlist_index, but we only index tlist entries that
  * are Vars belonging to some rel other than the one specified.  We will set
  * has_ph_vars (allowing PlaceHolderVars to be matched), but not has_non_vars
  * (so nothing other than Vars and PlaceHolderVars can be matched).
  */
 static indexed_tlist *
-build_tlist_index_other_vars(List *tlist, int ignore_rel)
+fill_tlist_index_other_vars(indexed_tlist *itlist, List *tlist, int ignore_rel)
 {
-	indexed_tlist *itlist;
 	tlist_vinfo *vinfo;
 	ListCell   *l;
 
-	/* Create data structure with enough slots for all tlist entries */
-	itlist = (indexed_tlist *)
-		palloc(offsetof(indexed_tlist, vars) +
-			   list_length(tlist) * sizeof(tlist_vinfo));
-
 	itlist->tlist = tlist;
 	itlist->has_ph_vars = false;
 	itlist->has_non_vars = false;
@@ -3476,6 +3502,8 @@ set_returning_clause_references(PlannerInfo *root,
 {
 	indexed_tlist *itlist;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * We can perform the desired Var fixup by abusing the fix_join_expr
 	 * machinery that formerly handled inner indexscan fixup.  We search the
@@ -3501,7 +3529,7 @@ set_returning_clause_references(PlannerInfo *root,
 						  NRM_EQUAL,
 						  NUM_EXEC_TLIST(topplan));
 
-	pfree(itlist);
+	free_tlist_index(itlist);
 
 	return rlist;
 }
@@ -3570,11 +3598,13 @@ set_windowagg_runcondition_references(PlannerInfo *root,
 	List	   *newlist;
 	indexed_tlist *itlist;
 
+	DECLARE_STACK_BUFFER();
+
 	itlist = build_tlist_index(plan->targetlist);
 
 	newlist = fix_windowagg_condition_expr(root, runcondition, itlist);
 
-	pfree(itlist);
+	free_tlist_index(itlist);
 
 	return newlist;
 }
-- 
2.53.0

From 5e889d2f52c9f4b5f4a76f6f6d89afbe091d4f1a Mon Sep 17 00:00:00 2001
From: Thomas Munro <[email-masked]>
Date: Mon, 2 Mar 2026 11:24:34 +1300
Subject: [PATCH v2 19/19] Use stack buffer for AppendRelInfos in planner.

---
 src/backend/optimizer/path/joinrels.c   | 18 ++++++---
 src/backend/optimizer/plan/planner.c    | 23 ++++++++----
 src/backend/optimizer/util/appendinfo.c | 32 ++++++++++------
 src/backend/partitioning/partprune.c    | 13 ++++---
 src/include/optimizer/appendinfo.h      | 49 ++++++++++++++++++++++++-
 src/tools/pgindent/typedefs.list        |  1 +
 6 files changed, 105 insertions(+), 31 deletions(-)

diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index 443e2dca7c0..c54d771eda6 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -23,6 +23,7 @@
 #include "optimizer/planner.h"
 #include "partitioning/partbounds.h"
 #include "utils/memutils.h"
+#include "utils/stack_buffer.h"

 static void make_rels_by_clause_joins(PlannerInfo *root,
@@ -1619,6 +1620,9 @@ try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 	ListCell   *lcr1 = NULL;
 	ListCell   *lcr2 = NULL;
 	int			cnt_parts;
+	AppendRelInfoBuffer appinfos_buffer = {0};
+
+	DECLARE_STACK_BUFFER();
 
 	/* Guard against stack overflow due to overly deep partition hierarchy. */
 	check_stack_depth();
@@ -1774,8 +1778,10 @@ try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 
 		/* Find the AppendRelInfo structures */
 		child_relids = bms_union(child_rel1->relids, child_rel2->relids);
-		appinfos = find_appinfos_by_relids(root, child_relids,
-										   &nappinfos);
+		appinfos = find_appinfos_by_relids_with_buffer(&appinfos_buffer,
+													   root,
+													   child_relids,
+													   &nappinfos);
 
 		/*
 		 * Construct restrictions applicable to the child join from those
@@ -1820,10 +1826,10 @@ try_partitionwise_join(PlannerInfo *root, RelOptInfo *rel1, RelOptInfo *rel2,
 		 * are only needed within the loop.  Free these local objects eagerly
 		 * at the end of each iteration.
 		 */
-		pfree(appinfos);
 		bms_free(child_relids);
 		free_child_join_sjinfo(child_sjinfo, parent_sjinfo);
 	}
+	free_appinfos_buffer(&appinfos_buffer);
 }
 
 /*
@@ -1844,6 +1850,8 @@ build_child_join_sjinfo(PlannerInfo *root, SpecialJoinInfo *parent_sjinfo,
 	AppendRelInfo **right_appinfos;
 	int			right_nappinfos;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Dummy SpecialJoinInfos can be created without any translation. */
 	if (parent_sjinfo->jointype == JOIN_INNER)
 	{
@@ -1874,8 +1882,8 @@ build_child_join_sjinfo(PlannerInfo *root, SpecialJoinInfo *parent_sjinfo,
 															 right_nappinfos,
 															 right_appinfos);
 
-	pfree(left_appinfos);
-	pfree(right_appinfos);
+	free_appinfos(left_appinfos);
+	free_appinfos(right_appinfos);
 
 	return sjinfo;
 }
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 24ad41d4ec6..64b4e98638f 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -7966,6 +7966,8 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
 	PathTarget *scanjoin_target;
 	ListCell   *lc;
 
+	DECLARE_STACK_BUFFER();
+
 	/* This recurses, so be paranoid. */
 	check_stack_depth();
 
@@ -8111,6 +8113,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
 	{
 		List	   *live_children = NIL;
 		int			i;
+		AppendRelInfoBuffer appinfos_buffer = {0};
 
 		/* Adjust each partition. */
 		i = -1;
@@ -8128,8 +8131,10 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
 				continue;
 
 			/* Translate scan/join targets for this child. */
-			appinfos = find_appinfos_by_relids(root, child_rel->relids,
-											   &nappinfos);
+			appinfos = find_appinfos_by_relids_with_buffer(&appinfos_buffer,
+														   root,
+														   child_rel->relids,
+														   &nappinfos);
 			foreach(lc, scanjoin_targets)
 			{
 				PathTarget *target = lfirst_node(PathTarget, lc);
@@ -8142,7 +8147,6 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
 				child_scanjoin_targets = lappend(child_scanjoin_targets,
 												 target);
 			}
-			pfree(appinfos);
 
 			/* Recursion does the real work. */
 			apply_scanjoin_target_to_paths(root, child_rel,
@@ -8155,6 +8159,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root,
 			if (!IS_DUMMY_REL(child_rel))
 				live_children = lappend(live_children, child_rel);
 		}
+		free_appinfos_buffer(&appinfos_buffer);
 
 		/* Build new paths for this relation by appending child paths. */
 		add_paths_to_append_rel(root, rel, live_children);
@@ -8210,6 +8215,9 @@ create_partitionwise_grouping_paths(PlannerInfo *root,
 	PathTarget *target = grouped_rel->reltarget;
 	bool		partial_grouping_valid = true;
 	int			i;
+	AppendRelInfoBuffer appinfos_buffer = {0};
+
+	DECLARE_STACK_BUFFER();
 
 	Assert(patype != PARTITIONWISE_AGGREGATE_NONE);
 	Assert(patype != PARTITIONWISE_AGGREGATE_PARTIAL ||
@@ -8241,8 +8249,10 @@ create_partitionwise_grouping_paths(PlannerInfo *root,
 		 */
 		memcpy(&child_extra, extra, sizeof(child_extra));
 
-		appinfos = find_appinfos_by_relids(root, child_input_rel->relids,
-										   &nappinfos);
+		appinfos = find_appinfos_by_relids_with_buffer(&appinfos_buffer,
+													   root,
+													   child_input_rel->relids,
+													   &nappinfos);
 
 		child_target->exprs = (List *)
 			adjust_appendrel_attrs(root,
@@ -8296,9 +8306,8 @@ create_partitionwise_grouping_paths(PlannerInfo *root,
 			grouped_live_children = lappend(grouped_live_children,
 											child_grouped_rel);
 		}
-
-		pfree(appinfos);
 	}
+	free_appinfos_buffer(&appinfos_buffer);
 
 	/*
 	 * Try to create append paths for partially grouped children. For full
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
index eadecd0bb92..10760235697 100644
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -25,6 +25,7 @@
 #include "parser/parsetree.h"
 #include "utils/lsyscache.h"
 #include "utils/rel.h"
+#include "utils/stack_buffer.h"
 #include "utils/syscache.h"

@@ -600,6 +601,8 @@ adjust_appendrel_attrs_multilevel(PlannerInfo *root, Node *node,
 	AppendRelInfo **appinfos;
 	int			nappinfos;
 
+	DECLARE_STACK_BUFFER();
+
 	/* Recurse if immediate parent is not the top parent. */
 	if (childrel->parent != parentrel)
 	{
@@ -612,11 +615,12 @@ adjust_appendrel_attrs_multilevel(PlannerInfo *root, Node *node,
 	}
 
 	/* Now translate for this child. */
-	appinfos = find_appinfos_by_relids(root, childrel->relids, &nappinfos);
+	appinfos = find_appinfos_by_relids(root, childrel->relids,
+									   &nappinfos);
 
 	node = adjust_appendrel_attrs(root, node, nappinfos, appinfos);
 
-	pfree(appinfos);
+	free_appinfos(appinfos);
 
 	return node;
 }
@@ -667,6 +671,8 @@ adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
 	AppendRelInfo **appinfos;
 	int			nappinfos;
 
+	DECLARE_STACK_BUFFER();
+
 	/*
 	 * If the given relids set doesn't contain any of the parent relids, it
 	 * will remain unchanged.
@@ -686,11 +692,13 @@ adjust_child_relids_multilevel(PlannerInfo *root, Relids relids,
 	}
 
 	/* Now translate for this child. */
-	appinfos = find_appinfos_by_relids(root, childrel->relids, &nappinfos);
+	appinfos = find_appinfos_by_relids(root,
+									   childrel->relids,
+									   &nappinfos);
 
 	relids = adjust_child_relids(relids, nappinfos, appinfos);
 
-	pfree(appinfos);
+	free_appinfos(appinfos);
 
 	return relids;
 }
@@ -801,19 +809,19 @@ get_translated_update_targetlist(PlannerInfo *root, Index relid,
  * include outer-join RT indexes in addition to baserels.  We silently
  * ignore the outer joins.
  *
- * The AppendRelInfos are returned in an array, which can be pfree'd by the
- * caller. *nappinfos is set to the number of entries in the array.
+ * A caller-supplied output array must have enough space for
+ * bms_num_members(relids) AppendRelInfo pointer.  The destination address is
+ * returned.
  */
 AppendRelInfo **
-find_appinfos_by_relids(PlannerInfo *root, Relids relids, int *nappinfos)
+find_appinfos_by_relids_in_place(AppendRelInfo **appinfos,
+								 PlannerInfo *root,
+								 Relids relids,
+								 int *nappinfos)
 {
-	AppendRelInfo **appinfos;
 	int			cnt = 0;
 	int			i;
 
-	/* Allocate an array that's certainly big enough */
-	appinfos = palloc_array(AppendRelInfo *, bms_num_members(relids));
-
 	i = -1;
 	while ((i = bms_next_member(relids, i)) >= 0)
 	{
@@ -830,11 +838,11 @@ find_appinfos_by_relids(PlannerInfo *root, Relids relids, int *nappinfos)
 
 		appinfos[cnt++] = appinfo;
 	}
+
 	*nappinfos = cnt;
 	return appinfos;
 }
 
-
 /*****************************************************************************
  *
  *		ROW-IDENTITY VARIABLE MANAGEMENT
diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index 8c4bf1d657e..58478747513 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -460,6 +460,7 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 	ListCell   *lc;
 	int			rti;
 	int			i;
+	AppendRelInfoBuffer appinfos_buffer = {0};
 
 	DECLARE_STACK_BUFFER();
 
@@ -517,16 +518,17 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 			if (!bms_equal(parentrel->relids, subpart->relids))
 			{
 				int			nappinfos;
-				AppendRelInfo **appinfos = find_appinfos_by_relids(root,
-																   subpart->relids,
-																   &nappinfos);
+				AppendRelInfo **appinfos;
+
+				appinfos = find_appinfos_by_relids_with_buffer(&appinfos_buffer,
+															   root,
+															   subpart->relids,
+															   &nappinfos);
 
 				prunequal = (List *) adjust_appendrel_attrs(root, (Node *)
 															prunequal,
 															nappinfos,
 															appinfos);
-
-				pfree(appinfos);
 			}
 
 			partprunequal = prunequal;
@@ -544,6 +546,7 @@ make_partitionedrel_pruneinfo(PlannerInfo *root, RelOptInfo *parentrel,
 												  subpart,
 												  targetpart);
 		}
+		free_appinfos_buffer(&appinfos_buffer);
 
 		/*
 		 * Convert pruning qual to pruning steps.  We may need to do this
diff --git a/src/include/optimizer/appendinfo.h b/src/include/optimizer/appendinfo.h
index b59a6218853..03d44c5ba74 100644
--- a/src/include/optimizer/appendinfo.h
+++ b/src/include/optimizer/appendinfo.h
@@ -38,8 +38,10 @@ extern List *adjust_inherited_attnums_multilevel(PlannerInfo *root,
 extern void get_translated_update_targetlist(PlannerInfo *root, Index relid,
 											 List **processed_tlist,
 											 List **update_colnos);
-extern AppendRelInfo **find_appinfos_by_relids(PlannerInfo *root,
-											   Relids relids, int *nappinfos);
+extern AppendRelInfo **find_appinfos_by_relids_in_place(AppendRelInfo **appinfos,
+														PlannerInfo *root,
+														Relids relids,
+														int *nappinfos);
 extern void add_row_identity_var(PlannerInfo *root, Var *orig_var,
 								 Index rtindex, const char *rowid_name);
 extern void add_row_identity_columns(PlannerInfo *root, Index rtindex,
@@ -47,4 +49,47 @@ extern void add_row_identity_columns(PlannerInfo *root, Index rtindex,
 									 Relation target_relation);
 extern void distribute_row_identity_vars(PlannerInfo *root);
 
+typedef struct AppendRelInfoBuffer
+{
+	AppendRelInfo **appinfos;
+	int			capacity;
+	int			want;
+} AppendRelInfoBuffer;
+
+/*
+ * The following are written as a macros so they can use a stack buffer to
+ * place the results in the calling stack frame if possible.  A stack buffer
+ * must be declared in the current scope to use them.
+ */
+
+#define find_appinfos_by_relids(root, relids, nappinfos)				\
+	find_appinfos_by_relids_in_place(stack_buffer_alloc_array(AppendRelInfo *, \
+															  bms_num_members(relids)), \
+									 (root),							\
+									 (relids),							\
+									 (nappinfos))
+#define free_appinfos(appinfos)					\
+	if (appinfos)								\
+		stack_buffer_free(appinfos)
+
+#define find_appinfos_by_relids_with_buffer(buffer, root, relids, nappinfos) \
+	((buffer)->want = bms_num_members(relids),							\
+	 (buffer)->appinfos = ((buffer)->capacity >= (buffer)->want ?		\
+						   (buffer)->appinfos :							\
+						   ((buffer)->capacity = (buffer)->want,		\
+							stack_buffer_alloc_array(AppendRelInfo *,	\
+													 (buffer)->capacity))), \
+	 find_appinfos_by_relids_in_place((buffer)->appinfos,				\
+									  (root),							\
+									  (relids),							\
+									  (nappinfos)))
+
+#define free_appinfos_buffer(buffer)				\
+	if ((buffer)->appinfos)							\
+	{												\
+		stack_buffer_free((buffer)->appinfos);		\
+		(buffer)->appinfos = NULL;					\
+		(buffer)->capacity = 0;						\
+	}
+
 #endif							/* APPENDINFO_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 141b9d6e077..5b9b56d6a0e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -127,6 +127,7 @@ Append
 AppendPath
 AppendPathInput
 AppendRelInfo
+AppendRelInfoBuffer
 AppendState
 ApplyErrorCallbackArg
 ApplyExecutionData
-- 
2.53.0

---

From: Thomas Munro <[email-masked]>
Date: Tue, 17 Mar 2026 11:10:17 +1300

> since it doesn't really have a buffer anymore except in a fallback

* renamed like like
* hopefully improved comments and naming and structure
* replaced macros with inline functions where possible
* added many sanity and overflow checks
* added tests
* added assertions that pg_stack_free() stack pointers are known, not
double-freed
* added CLOBBER_FREED_MEMORY and ASAN use-after-
* abandoned __builtin_alloca_with_align(), it has weird scoping on GCC
* removed alloca(0)-avoidance, as zero-sized objects have normal stack pointers
* removed grows-upwards alloca support, too complicated and
practically untestable
* let's use alloca on all systems, we can just tell a CI task to test array mode
* simplified the default sizing, now it's just: 128 for arrays and
1024 for alloca()
* split main patch into a few incremental pieces that might hopefully
be more digestable

The stack is a dangerous place, so I tried hard to think of all the
ways that anything anywhere could be exploited to overflow
intermediate values etc, while also trying to prove useful things at
compile time.  Ideas for other things to be paranoid about welcome.

Patches:

* 0001 + 0002 are the basic API but still using arrays
* 0003 shows the locales transformation.
* 0004..0008 add alloca()
* collection of example candidate callers, not changed much since v3,
in a tarball

One thing to highlight is how 0007 decides whether a pointer should go
to pfree() or not.  I am reasonably confident that
pg_stack_upper_bound() (which is either __builtin_frame_address(0) or
stack_base_ptr) is a good authoritative bound, and I am very confident
that pg_stack_lower_bound() is reliable when it's computed as the
lowest stack address that pg_stack_alloc() has ever seen.  But it
annoys me to have that book-keeping when:

pg_stack_lower_bound() can instead use __builtin_stack_address(), if
we decree that it is an authoritative lower bound, as is the case in
this patch currently.  The descriptions in the Clang and GCC manuals
sound good, and the generated code is exactly what I expect: it's just
comparing with %rsp, and I haven't  seen it trip my assertion that
%rsp <= every alloca().  But I wonder if some weird edge case
involving storage that is actually in a register or something like
that could allow it to not decrement %rsp, and then I wonder if that
builtin could reveal that, or something like that.  I been wondering
if it might make sense to have a secondary test out-of-line that
includes a fudge factor.  That sounds kinda bogus but perhaps an
argument can be constructed from guard pages: even on a system under
extreme address space pressure, palloc() pointers can only get so
close to %rsp.  But that gets pretty system-specific if you want
something that holds up in court.  Hnggh.  Maybe I'm worrying about
nothing.  Clues welcome.

---

From: Zsolt Parragi <[email-masked]>
Date: Wed, 18 Mar 2026 19:54:00 +0000

Hello!

+ !defined(PG_STACK_USE_ALLOC) &&

There's a typo there, that should be ALLOCA?

+/* Just use palloc. */
+#ifdef PG_STACK_USE_PALLOC
+#define DECLARE_PG_STACK_IMPL(size)
+#define pg_stack_alloc_aligned_impl(size, align) \
+ pg_stack_palloc_aligned((size), (align))
+#define pg_stack_ptr_p() false
+#endif

Isn't a ptr parameter missing from this branch?

+#define pg_stack_strdup_with_len(data, size) \
+ (pg_stack_sanity_checks(1), \
+ pg_stack_let_size = (size), \
+ pg_stack_strdup_with_len_impl( \
+ pg_stack_alloc_aligned_impl(pg_stack_let_size + 1, \
+ alignof(char)),

Both seems like a not realistic edge case, but since the array macros
above have a length check, shouldn't this also guard against +1
overflowing?

+/* Populate and insert a new entry. */
+static inline void *
+pg_stack_debug_link(pg_stack_debug_node **head,
+ pg_stack_debug_node *node,
+ void *ptr,
+ size_t size)
+{
+ node->ptr = ptr;
+ node->size = size;

Again unrealistic edge case, but if the above macros have overflow
checks for typically 64 bit size_t, shouldn't this also have some
safety checks, or at least setting node->size consistently to uint32_t
max if size is bigger?

+#define pg_stack_strdup(cstr) \
+ pg_stack_strdup_with_len((cstr), strlen(cstr))
+#define pg_stack_strndup(cstr, n) \
+ pg_stack_strdup_with_len((cstr), strnlen((cstr), (n)))

+#define pg_stack_text_to_cstring(text) \
+ pg_stack_strdup_with_len(VARDATA_ANY(text), VARSIZE_ANY_EXHDR(text))
+#define pg_stack_text_datum_to_cstring(datum) \
+ pg_stack_text_to_cstring((text *) DatumGetPointer(datum))

I would at least very clearly document that these macros evaluate
their arguments twice. I can't think of any scenario that would
seriously break things with them (or at least not break the stack),
but it could be still easy to forget that these are macros and not
functions.

Or can't they work similarly to DECLARE_PG_STACK_SIZE and store the
pointer in a local variable?