|
From: Code F. <0xc...@gm...> - 2025-11-03 17:20:16
|
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 = {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 = mmap(NULL, PAGE_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0);
if (pool == MAP_FAILED) {
perror("mmap failed");
return -1;
}
g_allocator.pool_start = pool;
g_allocator.pool_size = 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 = 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 = 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) != 0) {
perror("munmap failed");
}
g_allocator.initialized = 0;
g_allocator.free_list = NULL;
g_allocator.pool_start = NULL;
g_allocator.pool_size = 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 = 0;
int block_size = sizeof(struct ll_node);
// allocate two blocks and make them point to each other
struct ll_node* first_block = g_allocator.pool_start;
// leave a block gap for no reason
struct ll_node* second_block = 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 = 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 = NULL;
second_block->p_next = 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=full --show-leak-kinds=all
./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=full --show-leak-kinds=all ./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 😬)
Thanks!
|