]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Improve our support for Valgrind's leak tracking.
authorTom Lane <tgl@sss.pgh.pa.us>
Sat, 2 Aug 2025 22:26:19 +0000 (18:26 -0400)
committerTom Lane <tgl@sss.pgh.pa.us>
Sun, 3 Aug 2025 01:59:46 +0000 (21:59 -0400)
When determining whether an allocated chunk is still reachable,
Valgrind will consider only pointers within what it believes to be
allocated chunks.  Normally, all of a block obtained from malloc()
would be considered "allocated" --- but it turns out that if we use
VALGRIND_MEMPOOL_ALLOC to designate sub-section(s) of a malloc'ed
block as allocated, all the rest of that malloc'ed block is ignored.
This leads to lots of false positives of course.  In particular,
in any multi-malloc-block context, all but the primary block were
reported as leaked.  We also had a problem with context "ident"
strings, which were reported as leaked unless there was some other
pointer to them besides the one in the context header.

To fix, we need to use VALGRIND_MEMPOOL_ALLOC to designate
a context's management structs (the context struct itself and
any per-block headers) as allocated chunks.  That forces moving
the VALGRIND_CREATE_MEMPOOL/VALGRIND_DESTROY_MEMPOOL calls into
the per-context-type code, so that the pool identifier can be
made as soon as we've allocated the initial block, but otherwise
it's fairly straightforward.  Note that in Valgrind's eyes there
is no distinction between these allocations and the allocations
that the mmgr modules hand out to user code.  That's fine for
now, but perhaps someday we'll want to do better yet.

When reading this patch, it's helpful to start with the comments
added at the head of mcxt.c.

Author: Andres Freund <andres@anarazel.de>
Co-authored-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/285483.1746756246@sss.pgh.pa.us
Discussion: https://postgr.es/m/20210317181531.7oggpqevzz6bka3g@alap3.anarazel.de

src/backend/utils/mmgr/aset.c
src/backend/utils/mmgr/bump.c
src/backend/utils/mmgr/generation.c
src/backend/utils/mmgr/mcxt.c
src/backend/utils/mmgr/slab.c
src/include/utils/memdebug.h

index 666ecd8f78d0e49caeb2ee8900a169438653e75f..9ef109ca586bd0716427df3bada8d29fb3c36dfe 100644 (file)
 
 #define ALLOC_BLOCKHDRSZ       MAXALIGN(sizeof(AllocBlockData))
 #define ALLOC_CHUNKHDRSZ       sizeof(MemoryChunk)
+#define FIRST_BLOCKHDRSZ       (MAXALIGN(sizeof(AllocSetContext)) + \
+                                                        ALLOC_BLOCKHDRSZ)
 
 typedef struct AllocBlockData *AllocBlock;     /* forward reference */
 
@@ -458,6 +460,21 @@ AllocSetContextCreateInternal(MemoryContext parent,
         * we'd leak the header/initial block if we ereport in this stretch.
         */
 
+       /* Create a vpool associated with the context */
+       VALGRIND_CREATE_MEMPOOL(set, 0, false);
+
+       /*
+        * Create a vchunk covering both the AllocSetContext struct and the keeper
+        * block's header.  (Perhaps it would be more sensible for these to be two
+        * separate vchunks, but doing that seems to tickle bugs in some versions
+        * of Valgrind.)  We must have these vchunks, and also a vchunk for each
+        * subsequently-added block header, so that Valgrind considers the
+        * pointers within them while checking for leaked memory.  Note that
+        * Valgrind doesn't distinguish between these vchunks and those created by
+        * mcxt.c for the user-accessible-data chunks we allocate.
+        */
+       VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ);
+
        /* Fill in the initial block's block header */
        block = KeeperBlock(set);
        block->aset = set;
@@ -585,6 +602,14 @@ AllocSetReset(MemoryContext context)
 #ifdef CLOBBER_FREED_MEMORY
                        wipe_mem(block, block->freeptr - ((char *) block));
 #endif
+
+                       /*
+                        * We need to free the block header's vchunk explicitly, although
+                        * the user-data vchunks within will go away in the TRIM below.
+                        * Otherwise Valgrind complains about leaked allocations.
+                        */
+                       VALGRIND_MEMPOOL_FREE(set, block);
+
                        free(block);
                }
                block = next;
@@ -592,6 +617,14 @@ AllocSetReset(MemoryContext context)
 
        Assert(context->mem_allocated == keepersize);
 
+       /*
+        * Instruct Valgrind to throw away all the vchunks associated with this
+        * context, except for the one covering the AllocSetContext and
+        * keeper-block header.  This gets rid of the vchunks for whatever user
+        * data is getting discarded by the context reset.
+        */
+       VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ);
+
        /* Reset block size allocation sequence, too */
        set->nextBlockSize = set->initBlockSize;
 }
@@ -648,6 +681,9 @@ AllocSetDelete(MemoryContext context)
                                freelist->first_free = (AllocSetContext *) oldset->header.nextchild;
                                freelist->num_free--;
 
+                               /* Destroy the context's vpool --- see notes below */
+                               VALGRIND_DESTROY_MEMPOOL(oldset);
+
                                /* All that remains is to free the header/initial block */
                                free(oldset);
                        }
@@ -675,13 +711,24 @@ AllocSetDelete(MemoryContext context)
 #endif
 
                if (!IsKeeperBlock(set, block))
+               {
+                       /* As in AllocSetReset, free block-header vchunks explicitly */
+                       VALGRIND_MEMPOOL_FREE(set, block);
                        free(block);
+               }
 
                block = next;
        }
 
        Assert(context->mem_allocated == keepersize);
 
+       /*
+        * Destroy the vpool.  We don't seem to need to explicitly free the
+        * initial block's header vchunk, nor any user-data vchunks that Valgrind
+        * still knows about; they'll all go away automatically.
+        */
+       VALGRIND_DESTROY_MEMPOOL(set);
+
        /* Finally, free the context header, including the keeper block */
        free(set);
 }
@@ -716,6 +763,9 @@ AllocSetAllocLarge(MemoryContext context, Size size, int flags)
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        block->aset = set;
@@ -922,6 +972,9 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size size, int flags,
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        block->aset = set;
@@ -1104,6 +1157,10 @@ AllocSetFree(void *pointer)
 #ifdef CLOBBER_FREED_MEMORY
                wipe_mem(block, block->freeptr - ((char *) block));
 #endif
+
+               /* As in AllocSetReset, free block-header vchunks explicitly */
+               VALGRIND_MEMPOOL_FREE(set, block);
+
                free(block);
        }
        else
@@ -1184,6 +1241,7 @@ AllocSetRealloc(void *pointer, Size size, int flags)
                 * realloc() to make the containing block bigger, or smaller, with
                 * minimum space wastage.
                 */
+               AllocBlock      newblock;
                Size            chksize;
                Size            blksize;
                Size            oldblksize;
@@ -1223,14 +1281,21 @@ AllocSetRealloc(void *pointer, Size size, int flags)
                blksize = chksize + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
                oldblksize = block->endptr - ((char *) block);
 
-               block = (AllocBlock) realloc(block, blksize);
-               if (block == NULL)
+               newblock = (AllocBlock) realloc(block, blksize);
+               if (newblock == NULL)
                {
                        /* Disallow access to the chunk header. */
                        VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOC_CHUNKHDRSZ);
                        return MemoryContextAllocationFailure(&set->header, size, flags);
                }
 
+               /*
+                * Move the block-header vchunk explicitly.  (mcxt.c will take care of
+                * moving the vchunk for the user data.)
+                */
+               VALGRIND_MEMPOOL_CHANGE(set, block, newblock, ALLOC_BLOCKHDRSZ);
+               block = newblock;
+
                /* updated separately, not to underflow when (oldblksize > blksize) */
                set->header.mem_allocated -= oldblksize;
                set->header.mem_allocated += blksize;
@@ -1294,7 +1359,7 @@ AllocSetRealloc(void *pointer, Size size, int flags)
                /* Ensure any padding bytes are marked NOACCESS. */
                VALGRIND_MAKE_MEM_NOACCESS((char *) pointer + size, chksize - size);
 
-               /* Disallow access to the chunk header . */
+               /* Disallow access to the chunk header. */
                VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOC_CHUNKHDRSZ);
 
                return pointer;
index f7a37d1b3e86c0657aa1dc63bae1f8a6747609e9..2805d55a2eca91913e53edcdb7372ad9355f4f96 100644 (file)
@@ -45,7 +45,9 @@
 #include "utils/memutils_memorychunk.h"
 #include "utils/memutils_internal.h"
 
-#define Bump_BLOCKHDRSZ        MAXALIGN(sizeof(BumpBlock))
+#define Bump_BLOCKHDRSZ                MAXALIGN(sizeof(BumpBlock))
+#define FIRST_BLOCKHDRSZ       (MAXALIGN(sizeof(BumpContext)) + \
+                                                        Bump_BLOCKHDRSZ)
 
 /* No chunk header unless built with MEMORY_CONTEXT_CHECKING */
 #ifdef MEMORY_CONTEXT_CHECKING
@@ -189,6 +191,12 @@ BumpContextCreate(MemoryContext parent, const char *name, Size minContextSize,
         * Avoid writing code that can fail between here and MemoryContextCreate;
         * we'd leak the header and initial block if we ereport in this stretch.
         */
+
+       /* See comments about Valgrind interactions in aset.c */
+       VALGRIND_CREATE_MEMPOOL(set, 0, false);
+       /* This vchunk covers the BumpContext and the keeper block header */
+       VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ);
+
        dlist_init(&set->blocks);
 
        /* Fill in the initial block's block header */
@@ -262,6 +270,14 @@ BumpReset(MemoryContext context)
                        BumpBlockFree(set, block);
        }
 
+       /*
+        * Instruct Valgrind to throw away all the vchunks associated with this
+        * context, except for the one covering the BumpContext and keeper-block
+        * header.  This gets rid of the vchunks for whatever user data is getting
+        * discarded by the context reset.
+        */
+       VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ);
+
        /* Reset block size allocation sequence, too */
        set->nextBlockSize = set->initBlockSize;
 
@@ -279,6 +295,10 @@ BumpDelete(MemoryContext context)
 {
        /* Reset to release all releasable BumpBlocks */
        BumpReset(context);
+
+       /* Destroy the vpool -- see notes in aset.c */
+       VALGRIND_DESTROY_MEMPOOL(context);
+
        /* And free the context header and keeper block */
        free(context);
 }
@@ -318,6 +338,9 @@ BumpAllocLarge(MemoryContext context, Size size, int flags)
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        /* the block is completely full */
@@ -455,6 +478,9 @@ BumpAllocFromNewBlock(MemoryContext context, Size size, int flags,
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        /* initialize the new block */
@@ -606,6 +632,9 @@ BumpBlockFree(BumpContext *set, BumpBlock *block)
        wipe_mem(block, ((char *) block->endptr - (char *) block));
 #endif
 
+       /* As in aset.c, free block-header vchunks explicitly */
+       VALGRIND_MEMPOOL_FREE(set, block);
+
        free(block);
 }
 
index 18679ad4f1e41b4ac80fc33a372c092025ea205a..cfafc9bf0829de0ff8aeef369a005f5cff76b265 100644 (file)
@@ -45,6 +45,8 @@
 
 #define Generation_BLOCKHDRSZ  MAXALIGN(sizeof(GenerationBlock))
 #define Generation_CHUNKHDRSZ  sizeof(MemoryChunk)
+#define FIRST_BLOCKHDRSZ               (MAXALIGN(sizeof(GenerationContext)) + \
+                                                                Generation_BLOCKHDRSZ)
 
 #define Generation_CHUNK_FRACTION      8
 
@@ -221,6 +223,12 @@ GenerationContextCreate(MemoryContext parent,
         * Avoid writing code that can fail between here and MemoryContextCreate;
         * we'd leak the header if we ereport in this stretch.
         */
+
+       /* See comments about Valgrind interactions in aset.c */
+       VALGRIND_CREATE_MEMPOOL(set, 0, false);
+       /* This vchunk covers the GenerationContext and the keeper block header */
+       VALGRIND_MEMPOOL_ALLOC(set, set, FIRST_BLOCKHDRSZ);
+
        dlist_init(&set->blocks);
 
        /* Fill in the initial block's block header */
@@ -309,6 +317,14 @@ GenerationReset(MemoryContext context)
                        GenerationBlockFree(set, block);
        }
 
+       /*
+        * Instruct Valgrind to throw away all the vchunks associated with this
+        * context, except for the one covering the GenerationContext and
+        * keeper-block header.  This gets rid of the vchunks for whatever user
+        * data is getting discarded by the context reset.
+        */
+       VALGRIND_MEMPOOL_TRIM(set, set, FIRST_BLOCKHDRSZ);
+
        /* set it so new allocations to make use of the keeper block */
        set->block = KeeperBlock(set);
 
@@ -329,6 +345,10 @@ GenerationDelete(MemoryContext context)
 {
        /* Reset to release all releasable GenerationBlocks */
        GenerationReset(context);
+
+       /* Destroy the vpool -- see notes in aset.c */
+       VALGRIND_DESTROY_MEMPOOL(context);
+
        /* And free the context header and keeper block */
        free(context);
 }
@@ -365,6 +385,9 @@ GenerationAllocLarge(MemoryContext context, Size size, int flags)
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        /* block with a single (used) chunk */
@@ -487,6 +510,9 @@ GenerationAllocFromNewBlock(MemoryContext context, Size size, int flags,
        if (block == NULL)
                return MemoryContextAllocationFailure(context, size, flags);
 
+       /* Make a vchunk covering the new block's header */
+       VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ);
+
        context->mem_allocated += blksize;
 
        /* initialize the new block */
@@ -677,6 +703,9 @@ GenerationBlockFree(GenerationContext *set, GenerationBlock *block)
        wipe_mem(block, block->blksize);
 #endif
 
+       /* As in aset.c, free block-header vchunks explicitly */
+       VALGRIND_MEMPOOL_FREE(set, block);
+
        free(block);
 }
 
index ce01dce9861dae2dc038cba842f73f8cd86fe9f4..073aa6c4fc5b32137efc2a4b7665aad0f2abcfbf 100644 (file)
@@ -8,6 +8,23 @@
  * context-type-specific operations via the function pointers in a
  * context's MemoryContextMethods struct.
  *
+ * A note about Valgrind support: when USE_VALGRIND is defined, we provide
+ * support for memory leak tracking at the allocation-unit level.  Valgrind
+ * does leak detection by tracking allocated "chunks", which can be grouped
+ * into "pools".  The "chunk" terminology is overloaded, since we use that
+ * word for our allocation units, and it's sometimes important to distinguish
+ * those from the Valgrind objects that describe them.  To reduce confusion,
+ * let's use the terms "vchunk" and "vpool" for the Valgrind objects.
+ *
+ * We use a separate vpool for each memory context.  The context-type-specific
+ * code is responsible for creating and deleting the vpools, and also for
+ * creating vchunks to cover its management data structures such as block
+ * headers.  (There must be a vchunk that includes every pointer we want
+ * Valgrind to consider for leak-tracking purposes.)  This module creates
+ * and deletes the vchunks that cover the caller-visible allocated chunks.
+ * However, the context-type-specific code must handle cleaning up those
+ * vchunks too during memory context reset operations.
+ *
  *
  * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -418,8 +435,6 @@ MemoryContextResetOnly(MemoryContext context)
 
                context->methods->reset(context);
                context->isReset = true;
-               VALGRIND_DESTROY_MEMPOOL(context);
-               VALGRIND_CREATE_MEMPOOL(context, 0, false);
        }
 }
 
@@ -526,8 +541,6 @@ MemoryContextDeleteOnly(MemoryContext context)
        context->ident = NULL;
 
        context->methods->delete_context(context);
-
-       VALGRIND_DESTROY_MEMPOOL(context);
 }
 
 /*
@@ -1170,8 +1183,6 @@ MemoryContextCreate(MemoryContext node,
                node->nextchild = NULL;
                node->allowInCritSection = false;
        }
-
-       VALGRIND_CREATE_MEMPOOL(node, 0, false);
 }
 
 /*
index d32c0d318fbf4a23713e1760e5b40c097723bb9b..0e35abcf5a055c839873a65de6951b396542d75b 100644 (file)
@@ -377,6 +377,11 @@ SlabContextCreate(MemoryContext parent,
         * we'd leak the header if we ereport in this stretch.
         */
 
+       /* See comments about Valgrind interactions in aset.c */
+       VALGRIND_CREATE_MEMPOOL(slab, 0, false);
+       /* This vchunk covers the SlabContext only */
+       VALGRIND_MEMPOOL_ALLOC(slab, slab, sizeof(SlabContext));
+
        /* Fill in SlabContext-specific header fields */
        slab->chunkSize = (uint32) chunkSize;
        slab->fullChunkSize = (uint32) fullChunkSize;
@@ -451,6 +456,10 @@ SlabReset(MemoryContext context)
 #ifdef CLOBBER_FREED_MEMORY
                wipe_mem(block, slab->blockSize);
 #endif
+
+               /* As in aset.c, free block-header vchunks explicitly */
+               VALGRIND_MEMPOOL_FREE(slab, block);
+
                free(block);
                context->mem_allocated -= slab->blockSize;
        }
@@ -467,11 +476,23 @@ SlabReset(MemoryContext context)
 #ifdef CLOBBER_FREED_MEMORY
                        wipe_mem(block, slab->blockSize);
 #endif
+
+                       /* As in aset.c, free block-header vchunks explicitly */
+                       VALGRIND_MEMPOOL_FREE(slab, block);
+
                        free(block);
                        context->mem_allocated -= slab->blockSize;
                }
        }
 
+       /*
+        * Instruct Valgrind to throw away all the vchunks associated with this
+        * context, except for the one covering the SlabContext.  This gets rid of
+        * the vchunks for whatever user data is getting discarded by the context
+        * reset.
+        */
+       VALGRIND_MEMPOOL_TRIM(slab, slab, sizeof(SlabContext));
+
        slab->curBlocklistIndex = 0;
 
        Assert(context->mem_allocated == 0);
@@ -486,6 +507,10 @@ SlabDelete(MemoryContext context)
 {
        /* Reset to release all the SlabBlocks */
        SlabReset(context);
+
+       /* Destroy the vpool -- see notes in aset.c */
+       VALGRIND_DESTROY_MEMPOOL(context);
+
        /* And free the context header */
        free(context);
 }
@@ -567,6 +592,9 @@ SlabAllocFromNewBlock(MemoryContext context, Size size, int flags)
                if (unlikely(block == NULL))
                        return MemoryContextAllocationFailure(context, size, flags);
 
+               /* Make a vchunk covering the new block's header */
+               VALGRIND_MEMPOOL_ALLOC(slab, block, Slab_BLOCKHDRSZ);
+
                block->slab = slab;
                context->mem_allocated += slab->blockSize;
 
@@ -795,6 +823,10 @@ SlabFree(void *pointer)
 #ifdef CLOBBER_FREED_MEMORY
                        wipe_mem(block, slab->blockSize);
 #endif
+
+                       /* As in aset.c, free block-header vchunks explicitly */
+                       VALGRIND_MEMPOOL_FREE(slab, block);
+
                        free(block);
                        slab->header.mem_allocated -= slab->blockSize;
                }
index 7309271834b9fd96a7c8c0b3a22d1cbc05b2f8c8..80692dcef9382c19b9fc1710d8f320ed7a77cc59 100644 (file)
@@ -29,6 +29,7 @@
 #define VALGRIND_MEMPOOL_ALLOC(context, addr, size)                    do {} while (0)
 #define VALGRIND_MEMPOOL_FREE(context, addr)                           do {} while (0)
 #define VALGRIND_MEMPOOL_CHANGE(context, optr, nptr, size)     do {} while (0)
+#define VALGRIND_MEMPOOL_TRIM(context, addr, size)                     do {} while (0)
 #endif