Greetings,

An out-of-memory error during the first LockAcquire for a lock tag leaves a
LOCALLOCK that crashes the next acquire of the same tag.  The initial
lockOwners allocation is done with maxLockOwners already set to 8 and
lockOwners still NULL; if that allocation throws, the entry survives in that
state.  The next acquire takes the existing-entry path, where the only check
is numLockOwners >= maxLockOwners (0 >= 8, false), so it skips the
allocation and GrantLockLocal() dereferences the NULL pointer.

This is not a hypothetical state.  Tom Lane hardened RemoveLocalLock()
against exactly it in ba51774d87 (2015, "per low-memory testing by Andreas
Seltenreich"): "RemoveLocalLock() must consider the possibility that
LockAcquireExtended() failed to palloc the initial space for a locallock's
lockOwners array."  That fix covered the cleanup path; the re-acquire path
still assumes lockOwners is allocated.  This closes that gap, the same way:
if lockOwners is NULL, allocate it before use.

One subtlety worth stating: a plain top-level OOM here aborts the
transaction, and RemoveLocalLock() then discards the partial entry, so no
re-acquire hits it.  The crash needs the OOM caught without a full lock
release, e.g. a PL/pgSQL EXCEPTION handler, where the subtransaction abort
does not run LockReleaseAll and does not touch an entry that never got a
resource owner.  The entry then survives to the next acquire.

The fix is one branch in lock.c.  I confirmed the crash and the fix by
forcing the allocation to fail for advisory locks; the regression suite
passes.

The original diagnosis is Mark Dilger's; I reproduced it on master and
prepared it for submission.

--
Bryan Green
EDB: https://www.enterprisedb.com
From 0f82f53518db97771e77a646c519f5eeef5a84ad Mon Sep 17 00:00:00 2001
From: Bryan Green <[email protected]>
Date: Sat, 8 Aug 2026 13:10:29 -0500
Subject: [PATCH] Fix SIGSEGV in GrantLockLocal when OOM leaves
 LOCALLOCK.lockOwners NULL

On the first LockAcquire for a lock tag, LockAcquireExtended() creates the
LOCALLOCK entry with lockOwners set to NULL, then allocates the lockOwners
array in TopMemoryContext.  If that allocation fails with out of memory,
the LOCALLOCK entry persists with lockOwners still NULL and maxLockOwners
already set to 8.

On a later acquisition of the same lock tag, the existing-entry path only
checks numLockOwners against maxLockOwners (0 >= 8 is false), skips the
allocation, and GrantLockLocal() dereferences the NULL lockOwners pointer.

Handle a NULL lockOwners in the existing-entry path by allocating the
array, matching the not-found path.

Co-authored-by: Mark Dilger <[email protected]>
---
 src/backend/storage/lmgr/lock.c | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 0608eee9eb..c32bb2d296 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -913,7 +913,15 @@ LockAcquireExtended(const LOCKTAG *locktag,
        else
        {
                /* Make sure there will be room to remember the lock */
-               if (locallock->numLockOwners >= locallock->maxLockOwners)
+               if (locallock->lockOwners == NULL)
+               {
+                       /* A prior acquisition left the array unallocated after 
OOM. */
+                       locallock->maxLockOwners = 8;
+                       locallock->lockOwners = (LOCALLOCKOWNER *)
+                               MemoryContextAlloc(TopMemoryContext,
+                                                                  
locallock->maxLockOwners * sizeof(LOCALLOCKOWNER));
+               }
+               else if (locallock->numLockOwners >= locallock->maxLockOwners)
                {
                        int                     newsize = 
locallock->maxLockOwners * 2;
 
-- 
2.49.0

Reply via email to