Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w6BBl-00422e-1B for pgsql-bugs@arkaria.postgresql.org; Fri, 27 Mar 2026 17:41:41 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1w6BBj-00B7Op-1A for pgsql-bugs@arkaria.postgresql.org; Fri, 27 Mar 2026 17:41:39 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1w6BBj-00B7Oe-0G for pgsql-bugs@lists.postgresql.org; Fri, 27 Mar 2026 17:41:39 +0000 Received: from sss.pgh.pa.us ([68.162.161.243]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1w6BBg-00000001WHt-20zh for pgsql-bugs@lists.postgresql.org; Fri, 27 Mar 2026 17:41:39 +0000 Received: from sss1.sss.pgh.pa.us (localhost [127.0.0.1]) by sss.pgh.pa.us (8.15.2/8.15.2) with ESMTP id 62RHfTgO1338825; Fri, 27 Mar 2026 13:41:29 -0400 From: Tom Lane To: David Rowley cc: kuzmin.db4@gmail.com, pgsql-bugs@lists.postgresql.org Subject: Re: BUG #19438: segfault with temp_file_limit inside cursor In-reply-to: <1106026.1774573371@sss.pgh.pa.us> References: <19438-9d37b179c56d43aa@postgresql.org> <1106026.1774573371@sss.pgh.pa.us> Comments: In-reply-to Tom Lane message dated "Thu, 26 Mar 2026 21:02:51 -0400" MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" Content-ID: <1338712.1774633223.0@sss.pgh.pa.us> Date: Fri, 27 Mar 2026 13:41:29 -0400 Message-ID: <1338824.1774633289@sss.pgh.pa.us> List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk ------- =_aaaaaaaaaa0 Content-Type: text/plain; charset="us-ascii" Content-ID: <1338712.1774633223.1@sss.pgh.pa.us> I wrote: > Somehow, we are not crashing on a > double free with the new memory chunk header infrastructure. In fact, we are not. AllocSetFree does not change the "hdrmask" field of a freed chunk. So if we try to free it again, we end up right back at AllocSetFree, and the outcome is there's no detected problem but the corresponding freelist is now corrupt because the chunk got linked into it twice. In this example that doesn't cause any visible misbehavior, because we'll free the holdStore's context before doing very much more with it (and AllocSetCheck won't notice this type of corruption). Other cases could lead to very hard-to-diagnose problems that manifest somewhere far removed from the actual bug. In MEMORY_CONTEXT_CHECKING builds, we can cheaply detect double frees by using the existing behavior that requested_size is set to InvalidAllocSize during AllocSetFree. Another plausible idea is to change a freed chunk's MemoryContextMethodID to something invalid, which'd permit detection of double frees even in non-MEMORY_CONTEXT_CHECKING builds. I made draft patches showing how to do it both ways. (Both patches pass check-world and are able to detect the bug in v17.) The methodid-change way seems like the better alternative to me, but it is more invasive and does add a cycle or two when freeing or reusing a chunk. The other mcxt modules need to be looked at too, but I thought I'd try to get agreement on the solution approach before going further. regards, tom lane ------- =_aaaaaaaaaa0 Content-Type: text/x-diff; name="detect-double-free-with-requested_size.patch"; charset="us-ascii" Content-ID: <1338712.1774633223.2@sss.pgh.pa.us> Content-Description: detect-double-free-with-requested_size.patch diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c index 161c2e2d3df..ebf237bf575 100644 --- a/src/backend/utils/mmgr/aset.c +++ b/src/backend/utils/mmgr/aset.c @@ -1175,6 +1175,10 @@ AllocSetFree(void *pointer) link = GetFreeListLink(chunk); #ifdef MEMORY_CONTEXT_CHECKING + /* Test for previously-freed chunk */ + if (unlikely(chunk->requested_size == InvalidAllocSize)) + elog(WARNING, "detected double pfree in %s %p", + set->header.name, chunk); /* Test for someone scribbling on unused space in chunk */ if (chunk->requested_size < GetChunkSizeFromFreeListIdx(fidx)) if (!sentinel_ok(pointer, chunk->requested_size)) ------- =_aaaaaaaaaa0 Content-Type: text/x-diff; name="detect-double-free-with-methodid.patch"; charset="us-ascii" Content-ID: <1338712.1774633223.3@sss.pgh.pa.us> Content-Description: detect-double-free-with-methodid.patch Content-Transfer-Encoding: quoted-printable diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c index 161c2e2d3df..82ce25133ff 100644 --- a/src/backend/utils/mmgr/aset.c +++ b/src/backend/utils/mmgr/aset.c @@ -765,7 +765,7 @@ AllocSetAllocLarge(MemoryContext context, Size size, i= nt flags) = chunk =3D (MemoryChunk *) (((char *) block) + ALLOC_BLOCKHDRSZ); = - /* mark the MemoryChunk as externally managed */ + /* mark the MemoryChunk as externally managed and valid */ MemoryChunkSetHdrMaskExternal(chunk, MCTX_ASET_ID); = #ifdef MEMORY_CONTEXT_CHECKING @@ -826,7 +826,7 @@ AllocSetAllocChunkFromBlock(MemoryContext context, All= ocBlock block, block->freeptr +=3D (chunk_size + ALLOC_CHUNKHDRSZ); Assert(block->freeptr <=3D block->endptr); = - /* store the free list index in the value field */ + /* store the free list index in the value field, and mark chunk valid */ MemoryChunkSetHdrMask(chunk, block, fidx, MCTX_ASET_ID); = #ifdef MEMORY_CONTEXT_CHECKING @@ -910,8 +910,8 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size = size, int flags, block->freeptr +=3D (availchunk + ALLOC_CHUNKHDRSZ); availspace -=3D (availchunk + ALLOC_CHUNKHDRSZ); = - /* store the freelist index in the value field */ - MemoryChunkSetHdrMask(chunk, block, a_fidx, MCTX_ASET_ID); + /* store the freelist index in the value field, but mark chunk free */ + MemoryChunkSetHdrMask(chunk, block, a_fidx, MCTX_UNUSED_CHUNK_ID); #ifdef MEMORY_CONTEXT_CHECKING chunk->requested_size =3D InvalidAllocSize; /* mark it free */ #endif @@ -1058,6 +1058,9 @@ AllocSetAlloc(MemoryContext context, Size size, int = flags) set->freelist[fidx] =3D link->next; VALGRIND_MAKE_MEM_NOACCESS(link, sizeof(AllocFreeListLink)); = + /* mark chunk valid */ + MemoryChunkSetMethodID(chunk, MCTX_ASET_ID); + #ifdef MEMORY_CONTEXT_CHECKING chunk->requested_size =3D size; /* set mark to catch clobber of "unused" space */ @@ -1185,6 +1188,10 @@ AllocSetFree(void *pointer) #ifdef CLOBBER_FREED_MEMORY wipe_mem(pointer, GetChunkSizeFromFreeListIdx(fidx)); #endif + + /* mark chunk free */ + MemoryChunkSetMethodID(chunk, MCTX_UNUSED_CHUNK_ID); + /* push this chunk onto the top of the free list */ VALGRIND_MAKE_MEM_DEFINED(link, sizeof(AllocFreeListLink)); link->next =3D set->freelist[fidx]; diff --git a/src/include/utils/memutils_internal.h b/src/include/utils/mem= utils_internal.h index 475e91b336b..9fa877332ee 100644 --- a/src/include/utils/memutils_internal.h +++ b/src/include/utils/memutils_internal.h @@ -138,6 +138,9 @@ typedef enum MemoryContextMethodID MCTX_15_RESERVED_WIPEDMEM_ID /* 1111 occurs in wipe_mem'd memory */ } MemoryContextMethodID; = +/* This MemoryContextMethodID is recommended to put in pfree'd chunks */ +#define MCTX_UNUSED_CHUNK_ID MCTX_0_RESERVED_UNUSEDMEM_ID + /* * The number of bits that 8-byte memory chunk headers can use to encode = the * MemoryContextMethodID. diff --git a/src/include/utils/memutils_memorychunk.h b/src/include/utils/= memutils_memorychunk.h index bda9912182d..b876940d5e0 100644 --- a/src/include/utils/memutils_memorychunk.h +++ b/src/include/utils/memutils_memorychunk.h @@ -58,6 +58,9 @@ * MemoryChunkSetHdrMaskExternal: * Used to set up an externally managed MemoryChunk. * + * MemoryChunkSetMethodID: + * Used to change a MemoryChunk's MethodID while preserving other fields= . + * * MemoryChunkIsExternal: * Determine if the given MemoryChunk is externally managed, i.e. * MemoryChunkSetHdrMaskExternal() was called on the chunk. @@ -196,6 +199,21 @@ MemoryChunkSetHdrMaskExternal(MemoryChunk *chunk, methodid; } = +/* + * MemoryChunkSetMethodID: + * Set the methodid without changing any other chunk header fields. + * This is typically used while marking a chunk as free or not-free. + */ +static inline void +MemoryChunkSetMethodID(MemoryChunk *chunk, + MemoryContextMethodID methodid) +{ + Assert((int) methodid <=3D MEMORY_CONTEXT_METHODID_MASK); + + chunk->hdrmask =3D (chunk->hdrmask & ~MEMORY_CONTEXT_METHODID_MASK) | + methodid; +} + /* * MemoryChunkIsExternal * Return true if 'chunk' is marked as external. ------- =_aaaaaaaaaa0--