Instrument custom allocator with Valgrind

Code Fade <[email protected]> Mon, 3 Nov 2025 22:49:58 +0530
Newsgroups gmane.comp.debugging.valgrind
Message-ID <CAFTjGvSy=n05RYstSSvT8hMdQ-V+_=H_-VWpn+kv865qvA=Z-A@mail.gmail.com>
--===============6349098868371395100==
Content-Type: multipart/alternative; boundary="00000000000063eeac0642b3ed6c"

--00000000000063eeac0642b3ed6c
Content-Type: text/plain; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

Hello users,
I'm having trouble making valgrind detect leaks correctly with a custom
allocator. This allocator allocates a page of memory at initialization and
subsequently allocates smaller blocks from within itself. Only a single
type of struct ( a double linked list node) of some size is allocated on
this page.

When there are two blocks, where the first block points to the second one
and the second block points to the first one. The memory is still marked as
reachable even though these two blocks were allocated on stack and the
pointers to the blocks were lost. Only if these blocks are not linked, it
shows up as a definite leak. A similar case with malloc is a definite leak,
just want to understand what I'm missing with my allocator.

I'm adding a small poc.

1. testpool.c
```
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

#ifdef USE_VALGRIND
#include <valgrind/valgrind.h>
#include <valgrind/memcheck.h>
#else
#define VALGRIND_MEMPOOL_METAPOOL 1
#define VALGRIND_FREELIKE_BLOCK(block, size) do{} while(0)
#define VALGRIND_MALLOCLIKE_BLOCK(block, size, is_zeroed, flags)  do {}
while (0)
#define VALGRIND_CREATE_MEMPOOL_EXT(pool, rzB, is_zeroed, flags) do {}
while(0)
#define VALGRIND_CREATE_MEMPOOL(pool, rzB, is_zeroed) do {} while(0)
#define VALGRIND_DESTROY_MEMPOOL(pool) do {} while(0)
#define VALGRIND_MEMPOOL_ALLOC(pool, addr, size) do {} while(0)
#define VALGRIND_MEMPOOL_FREE(pool, addr) do {} while(0)
#define VALGRIND_MAKE_MEM_NOACCESS(addr, size) do {} while(0)
#define VALGRIND_MAKE_MEM_UNDEFINED(addr, size) do {} while(0)
#define VALGRIND_MAKE_MEM_DEFINED(addr, size) do {} while(0)
#endif

// Custom allocator constants
#define PAGE_SIZE 4096
#define BLOCK_SIZE 64  // Size of each allocation block (adjust as needed)
#define BLOCKS_PER_PAGE (PAGE_SIZE / BLOCK_SIZE)

// Free list node structure
typedef struct free_block {
    struct free_block *next;
} free_block_t;
typedef struct alloc_block {
    struct alloc_block *next;
} alloc_block_t;

// Allocator state
typedef struct {
    void *pool_start;           // Start of mmap'd memory pool
    size_t pool_size;           // Total size of pool
    free_block_t *free_list;    // Head of free list
    alloc_block_t *alloc_list;    // Head of free list
    int initialized;            // Whether allocator is initialized
} allocator_t;

// Global allocator instance
static allocator_t g_allocator =3D {0};

// Initialize the custom allocator
static int allocator_init(void) {
    if (g_allocator.initialized) {
        return 0;  // Already initialized
    }

    // Allocate one page using mmap
    void *pool =3D mmap(NULL, PAGE_SIZE,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS,
                      -1, 0);

    if (pool =3D=3D MAP_FAILED) {
        perror("mmap failed");
        return -1;
    }
    g_allocator.pool_start =3D pool;
    g_allocator.pool_size =3D PAGE_SIZE;

    // Create Valgrind memory pool
    VALGRIND_CREATE_MEMPOOL(pool, 0, 0);
    printf("Created pool\n");
    VALGRIND_MAKE_MEM_UNDEFINED(g_allocator.pool_start,
g_allocator.pool_size);

    g_allocator.free_list =3D NULL;

    // Mark as no access for Valgrind, if read at runtime, caught as
invalid mem access
    VALGRIND_MAKE_MEM_NOACCESS(g_allocator.pool_start,
g_allocator.pool_size);

    g_allocator.initialized =3D 1;

    printf("Pool info: %p, end: %p\n", g_allocator.pool_start,
g_allocator.pool_start + g_allocator.pool_size);
    printf("Allocator initialized: %d blocks of %d bytes each (%d bytes
total)\n",
           BLOCKS_PER_PAGE, BLOCK_SIZE, PAGE_SIZE);

    return 0;
}

// Cleanup allocator
static void allocator_cleanup(void) {
    if (!g_allocator.initialized) {
        return;
    }

    VALGRIND_DESTROY_MEMPOOL(g_allocator.pool_start);

    // Unmap the memory
    if (munmap(g_allocator.pool_start, g_allocator.pool_size) !=3D 0) {
        perror("munmap failed");
    }

    g_allocator.initialized =3D 0;
    g_allocator.free_list =3D NULL;
    g_allocator.pool_start =3D NULL;
    g_allocator.pool_size =3D 0;

    printf("Allocator cleaned up\n");
}

// Define the struct
struct ll_node {
    struct ll_node *next;
    struct ll_node *p_next;  // previous node pointer
    int data;
};

struct node {
   struct ll_node* head;
};

void
manual_block_alloc() {
   assert(!allocator_init());

   int index =3D 0;
   int block_size =3D sizeof(struct ll_node);
   // allocate two blocks and make them point to each other
   struct ll_node* first_block =3D g_allocator.pool_start;
   // leave a block gap for no reason
   struct ll_node* second_block =3D g_allocator.pool_start + 2 * block_size=
;

   // Unlock first block but also register it with valgrind for leak
tracking
   VALGRIND_MAKE_MEM_UNDEFINED( first_block, block_size);
   first_block->next =3D second_block;
   VALGRIND_MAKE_MEM_NOACCESS( first_block, block_size);
   VALGRIND_MALLOCLIKE_BLOCK(first_block, block_size, 0, 1);
   // Allocate second block
   VALGRIND_MAKE_MEM_UNDEFINED( second_block, block_size);
   second_block->next =3D NULL;
   second_block->p_next =3D first_block;
   VALGRIND_MAKE_MEM_NOACCESS( second_block, block_size);
   VALGRIND_MALLOCLIKE_BLOCK(second_block, block_size, 0, 1);

}
int main() {
    printf("Starting custom allocator demo with Valgrind support\n\n");

    // Create three nodes
    manual_block_alloc();

    printf("\nRun with: valgrind --leak-check=3Dfull --show-leak-kinds=3Dal=
l
./a.out\n");
    printf("Or compile with -DUSE_VALGRIND for enhanced Valgrind
integration\n");

    return 0;
}
```

*Command to run:* gcc -g -DUSE_VALGRIND testpool.c -o testpool &&  valgrind
--leak-check=3Dfull --show-leak-kinds=3Dall ./testpool

Is it possible to get malloc-like leak behavior with valgrind or this will
not work with the custom allocator?

(I hope this is the right mailing list =F0=9F=98=AC)

Thanks!

--00000000000063eeac0642b3ed6c
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div dir=3D"ltr"><div>Hello users,</div><div>I&#39;m havin=
g trouble making valgrind detect leaks correctly with a custom allocator. T=
his allocator allocates a page of memory at initialization and subsequently=
 allocates smaller blocks from within itself. Only a single type of struct =
( a double linked list node) of some size is allocated on this page.</div><=
div><br></div><div>When there are two blocks, where the first block points =
to the second one and the second block points to the first one. The memory =
is still marked as reachable even though these two blocks were allocated on=
 stack and the pointers to the blocks were lost. Only if these blocks are n=
ot linked, it shows up as a definite leak. A similar case with malloc is a =
definite leak, just want to understand what I&#39;m missing with my allocat=
or.</div><div><br></div><div>I&#39;m adding a small poc.</div><div><br></di=
v><div><span style=3D"font-family:monospace">1. testpool.c</span></div><div=
><span style=3D"font-family:monospace">```</span></div><div><span style=3D"=
font-family:monospace">#include &lt;stdio.h&gt;<br>#include &lt;stdlib.h&gt=
;<br>#include &lt;assert.h&gt;<br>#include &lt;sys/mman.h&gt;<br>#include &=
lt;unistd.h&gt;<br>#include &lt;string.h&gt;<br><br>#ifdef USE_VALGRIND<br>=
#include &lt;valgrind/valgrind.h&gt;<br>#include &lt;valgrind/memcheck.h&gt=
;<br>#else<br>#define VALGRIND_MEMPOOL_METAPOOL 1<br>#define VALGRIND_FREEL=
IKE_BLOCK(block, size) do{} while(0)<br>#define VALGRIND_MALLOCLIKE_BLOCK(b=
lock, size, is_zeroed, flags) =C2=A0do {} while (0)<br>#define VALGRIND_CRE=
ATE_MEMPOOL_EXT(pool, rzB, is_zeroed, flags) do {} while(0)<br>#define VALG=
RIND_CREATE_MEMPOOL(pool, rzB, is_zeroed) do {} while(0)<br>#define VALGRIN=
D_DESTROY_MEMPOOL(pool) do {} while(0)<br>#define VALGRIND_MEMPOOL_ALLOC(po=
ol, addr, size) do {} while(0)<br>#define VALGRIND_MEMPOOL_FREE(pool, addr)=
 do {} while(0)<br>#define VALGRIND_MAKE_MEM_NOACCESS(addr, size) do {} whi=
le(0)<br>#define VALGRIND_MAKE_MEM_UNDEFINED(addr, size) do {} while(0)<br>=
#define VALGRIND_MAKE_MEM_DEFINED(addr, size) do {} while(0)<br>#endif<br><=
br>// Custom allocator constants<br>#define PAGE_SIZE 4096<br>#define BLOCK=
_SIZE 64 =C2=A0// Size of each allocation block (adjust as needed)<br>#defi=
ne BLOCKS_PER_PAGE (PAGE_SIZE / BLOCK_SIZE)<br><br>// Free list node struct=
ure<br>typedef struct free_block {<br>=C2=A0 =C2=A0 struct free_block *next=
;<br>} free_block_t;<br>typedef struct alloc_block {<br>=C2=A0 =C2=A0 struc=
t alloc_block *next;<br>} alloc_block_t;<br><br>// Allocator state<br>typed=
ef struct {<br>=C2=A0 =C2=A0 void *pool_start; =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 // Start of mmap&#39;d memory pool<br>=C2=A0 =C2=A0 size_t pool_size=
; =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 // Total size of pool<br>=C2=A0 =C2=A0=
 free_block_t *free_list; =C2=A0 =C2=A0// Head of free list<br>=C2=A0 =C2=
=A0 alloc_block_t *alloc_list; =C2=A0 =C2=A0// Head of free list<br>=C2=A0 =
=C2=A0 int initialized; =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0// Whether=
 allocator is initialized<br>} allocator_t;<br><br>// Global allocator inst=
ance<br>static allocator_t g_allocator =3D {0};<br><br>// Initialize the cu=
stom allocator<br>static int allocator_init(void) {<br>=C2=A0 =C2=A0 if (g_=
allocator.initialized) {<br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 return 0; =C2=A0// =
Already initialized<br>=C2=A0 =C2=A0 }<br><br>=C2=A0 =C2=A0 // Allocate one=
 page using mmap<br>=C2=A0 =C2=A0 void *pool =3D mmap(NULL, PAGE_SIZE,<br>=
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 PROT_READ | PROT_WRITE,<br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 MAP_PRIVATE | MAP_ANONYMOUS,<br>=C2=A0 =
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 -1, 0=
);<br><br>=C2=A0 =C2=A0 if (pool =3D=3D MAP_FAILED) {<br>=C2=A0 =C2=A0 =C2=
=A0 =C2=A0 perror(&quot;mmap failed&quot;);<br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =
return -1;<br>=C2=A0 =C2=A0 }<br>=C2=A0 =C2=A0 g_allocator.pool_start =3D p=
ool;<br>=C2=A0 =C2=A0 g_allocator.pool_size =3D PAGE_SIZE;<br><br>=C2=A0 =
=C2=A0 // Create Valgrind memory pool<br>=C2=A0 =C2=A0 VALGRIND_CREATE_MEMP=
OOL(pool, 0, 0);<br>=C2=A0 =C2=A0 printf(&quot;Created pool\n&quot;);<br>=
=C2=A0 =C2=A0 VALGRIND_MAKE_MEM_UNDEFINED(g_allocator.pool_start, g_allocat=
or.pool_size);<br><br>=C2=A0 =C2=A0 g_allocator.free_list =3D NULL;<br><br>=
=C2=A0 =C2=A0 // Mark as no access for Valgrind, if read at runtime, caught=
 as invalid mem access<br>=C2=A0 =C2=A0 VALGRIND_MAKE_MEM_NOACCESS(g_alloca=
tor.pool_start, g_allocator.pool_size);<br><br>=C2=A0 =C2=A0 g_allocator.in=
itialized =3D 1;<br><br>=C2=A0 =C2=A0 printf(&quot;Pool info: %p, end: %p\n=
&quot;, g_allocator.pool_start, g_allocator.pool_start + g_allocator.pool_s=
ize);<br>=C2=A0 =C2=A0 printf(&quot;Allocator initialized: %d blocks of %d =
bytes each (%d bytes total)\n&quot;,<br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0BLOCKS_PER_PAGE, BLOCK_SIZE, PAGE_SIZE);<br><br>=C2=A0 =C2=A0 return =
0;<br>}<br><br>// Cleanup allocator<br>static void allocator_cleanup(void) =
{<br>=C2=A0 =C2=A0 if (!g_allocator.initialized) {<br>=C2=A0 =C2=A0 =C2=A0 =
=C2=A0 return;<br>=C2=A0 =C2=A0 }<br><br>=C2=A0 =C2=A0 VALGRIND_DESTROY_MEM=
POOL(g_allocator.pool_start);<br><br>=C2=A0 =C2=A0 // Unmap the memory<br>=
=C2=A0 =C2=A0 if (munmap(g_allocator.pool_start, g_allocator.pool_size) !=
=3D 0) {<br>=C2=A0 =C2=A0 =C2=A0 =C2=A0 perror(&quot;munmap failed&quot;);<=
br>=C2=A0 =C2=A0 }<br><br>=C2=A0 =C2=A0 g_allocator.initialized =3D 0;<br>=
=C2=A0 =C2=A0 g_allocator.free_list =3D NULL;<br>=C2=A0 =C2=A0 g_allocator.=
pool_start =3D NULL;<br>=C2=A0 =C2=A0 g_allocator.pool_size =3D 0;<br><br>=
=C2=A0 =C2=A0 printf(&quot;Allocator cleaned up\n&quot;);<br>}<br><br>// De=
fine the struct<br>struct ll_node {<br>=C2=A0 =C2=A0 struct ll_node *next;<=
br>=C2=A0 =C2=A0 struct ll_node *p_next; =C2=A0// previous node pointer<br>=
=C2=A0 =C2=A0 int data;<br>};<br><br>struct node {<br>=C2=A0 =C2=A0struct l=
l_node* head;<br>};<br><br>void<br>manual_block_alloc() {<br>=C2=A0 =C2=A0a=
ssert(!allocator_init());<br><br>=C2=A0 =C2=A0int index =3D 0;<br>=C2=A0 =
=C2=A0int block_size =3D sizeof(struct ll_node);<br>=C2=A0 =C2=A0// allocat=
e two blocks and make them point to each other<br>=C2=A0 =C2=A0struct ll_no=
de* first_block =3D g_allocator.pool_start;<br>=C2=A0 =C2=A0// leave a bloc=
k gap for no reason<br>=C2=A0 =C2=A0struct ll_node* second_block =3D g_allo=
cator.pool_start + 2 * block_size;<br><br>=C2=A0 =C2=A0// Unlock first bloc=
k but also register it with valgrind for leak tracking<br>=C2=A0 =C2=A0VALG=
RIND_MAKE_MEM_UNDEFINED( first_block, block_size);<br>=C2=A0 =C2=A0first_bl=
ock-&gt;next =3D second_block;<br>=C2=A0 =C2=A0VALGRIND_MAKE_MEM_NOACCESS( =
first_block, block_size);<br>=C2=A0 =C2=A0VALGRIND_MALLOCLIKE_BLOCK(first_b=
lock, block_size, 0, 1);<br>=C2=A0 =C2=A0// Allocate second block<br>=C2=A0=
 =C2=A0VALGRIND_MAKE_MEM_UNDEFINED( second_block, block_size);<br>=C2=A0 =
=C2=A0second_block-&gt;next =3D NULL;<br>=C2=A0 =C2=A0second_block-&gt;p_ne=
xt =3D first_block;<br>=C2=A0 =C2=A0VALGRIND_MAKE_MEM_NOACCESS( second_bloc=
k, block_size);<br>=C2=A0 =C2=A0VALGRIND_MALLOCLIKE_BLOCK(second_block, blo=
ck_size, 0, 1);<br>=C2=A0 =C2=A0<br>}<br>int main() {<br>=C2=A0 =C2=A0 prin=
tf(&quot;Starting custom allocator demo with Valgrind support\n\n&quot;);<b=
r><br>=C2=A0 =C2=A0 // Create three nodes<br>=C2=A0 =C2=A0 manual_block_all=
oc();<br><br>=C2=A0 =C2=A0 printf(&quot;\nRun with: valgrind --leak-check=
=3Dfull --show-leak-kinds=3Dall ./a.out\n&quot;);<br>=C2=A0 =C2=A0 printf(&=
quot;Or compile with -DUSE_VALGRIND for enhanced Valgrind integration\n&quo=
t;);<br><br>=C2=A0 =C2=A0 return 0;<br>}</span></div><div><span style=3D"fo=
nt-family:monospace">```</span></div><div><span style=3D"font-family:monosp=
ace"><br></span></div><div><span style=3D"font-family:monospace"><b>Command=
 to run:</b>=C2=A0gcc -g -DUSE_VALGRIND testpool.c -o testpool &amp;&amp; =
=C2=A0valgrind --leak-check=3Dfull --show-leak-kinds=3Dall ./testpool</span=
></div><div><span style=3D"font-family:monospace"><br></span></div><div><sp=
an style=3D"font-family:arial,sans-serif">Is it possible to get malloc-like=
 leak behavior with valgrind or this will not work with the custom allocato=
r?</span></div><div><span style=3D"font-family:monospace"><br></span></div>=
<div><span style=3D"font-family:monospace">(I hope this is the right mailin=
g list=C2=A0=F0=9F=98=AC)</span></div><div><span style=3D"font-family:monos=
pace"><br></span></div><div><span style=3D"font-family:arial,sans-serif">Th=
anks!</span></div></div>
</div>

--00000000000063eeac0642b3ed6c--


--===============6349098868371395100==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline


--===============6349098868371395100==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

_______________________________________________
Valgrind-users mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/valgrind-users

--===============6349098868371395100==--