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
