3

I've been following an article that walks through implementing primitive versions of malloc and free. After finishing the allocator, I wanted to test it using Valgrind, so I added the following lines to my malloc and free functions, respectively:

VALGRIND_MALLOCLIKE_BLOCK(block, size, 0, 0);
VALGRIND_FREELIKE_BLOCK(block, 0);

Valgrind shows that memory allocations are being tracked correctly using VALGRIND_MALLOCLIKE_BLOCK() and VALGRIND_FREELIKE_BLOCK(). However, one block is still marked as "still reachable" at exit.

HEAP SUMMARY:
==74425==     in use at exit: 20 bytes in 1 blocks
==74425==   total heap usage: 2 allocs, 2 frees, 1,044 bytes allocated
==74425==
==74425== 20 bytes in 1 blocks are still reachable in loss record 1 of 1
==74425==
==74425== LEAK SUMMARY:
==74425==    definitely lost: 0 bytes in 0 blocks
==74425==    indirectly lost: 0 bytes in 0 blocks
==74425==      possibly lost: 0 bytes in 0 blocks
==74425==    still reachable: 20 bytes in 1 blocks
==74425==         suppressed: 0 bytes in 0 blocks

The article adds a "stub" member to the union, with a fixed size of 16, however it is unnecessary, since sizeof(header_t) = 24.

Here's my implementation of the two functions:

pthread_mutex_t global_malloc_lock;

header_t *head, *tail;

header_t *get_free_block(size_t size) {
    header_t *curr = head;

    while (curr) {
        if (curr->s.is_free && curr->s.size >= size) {
            return curr;
        }
        curr = curr->s.next;
    }

    return NULL;
}

void *my_malloc(size_t size) {
    
    size_t total_size;
    void *block;
    header_t *header;

    if (!size) {
        return NULL;
    }

    pthread_mutex_lock(&global_malloc_lock);
    header = get_free_block(size);
    
    if (header) {
        header->s.is_free = 0;
        pthread_mutex_unlock(&global_malloc_lock);
        return (void*)(header+1);
    }

    total_size = sizeof(header_t) + size;
    block = sbrk(total_size);

    if (block == (void*) -1) {
        pthread_mutex_unlock(&global_malloc_lock);
        return NULL;
    }

    header = block;
    header->s.is_free = 0;
    header->s.size = size;
    header->s.next = NULL;

    if (!head) {
        head = header;
    }

    if (tail) {
        tail->s.next = header;
    }
    tail = header;
    pthread_mutex_unlock(&global_malloc_lock);
    VALGRIND_MALLOCLIKE_BLOCK(block, size, 0, 0);

    return (void*)(header+1);
}

void my_free(void* block) {

    header_t *header, *tmp;
    void *programbreak;

    if (!block) {
        return;
    }

    pthread_mutex_lock(&global_malloc_lock);
    header = (header_t*)block - 1;

    programbreak = sbrk(0);

    if ((char*)block + header->s.size == programbreak) {
        if (head == tail) {
            head = tail = NULL;
        }
        else {
            tmp = head;
            while(tmp) {
                if (tmp->s.next == tail) {
                    tmp->s.next = NULL;
                    tail = tmp;
                }
                tmp = tmp->s.next;
            }
        }

        sbrk(0 - sizeof(header_t) - header->s.size);
        pthread_mutex_unlock(&global_malloc_lock);
        VALGRIND_FREELIKE_BLOCK(block, 0);
        return;
    }

    header->s.is_free = 1;
    pthread_mutex_unlock(&global_malloc_lock);

}

(The definition of the union as well as the signatures for the functions are in a separate header file)

Here's the main function:

int main(int argc, char const *argv[])
{
    printf("sizeof(header_t) = %zu\n", sizeof(header_t));
    int* block = my_malloc(5*sizeof(int));
    my_free(block);
    return 0;
}

My question is: why does Valgrind report memory as "still reachable" even though I call my_free() and VALGRIND_FREELIKE_BLOCK()? Is it because sbrk() isn't releasing the memory? Or is there something missing in my free logic?

The article: https://arjunsreedharan.org/post/148675821737/memory-allocators-101-write-a-simple-memory

5
  • Global vars head and tail are used before initializing them. Commented Jun 23 at 7:26
  • 2
    @Wiimm global variables are always initalized (zeroed) and they can be used without initalization Commented Jun 23 at 7:30
  • You can only return heap memory to the kernel when all of the bytes you want to hand back sit contiguously at the very top of the data segment Commented Jun 23 at 7:38
  • Nevertheless, it is really sloppy and bad practice to rely on implicit zero initialization of objects with static storage duration. It's bad practice to use globals in the first place; any variable at file scope should be static or otherwise the program design is fishy at best. Commented Jun 23 at 8:40
  • Try running with --leak-check=yes --show-leak-kinds=all to get the callstack where memory was allocated. Commented Jun 23 at 9:42

2 Answers 2

2

As for the still reachable memory issue, I was able to fix it by changing the Valgrind free notifier from:

VALGRIND_FREELIKE_BLOCK(block, 0);

to:

VALGRIND_FREELIKE_BLOCK(header, 0);

This change ensures that Valgrind is correctly notified of the entire allocated block (including the header), allowing it to recognize the memory as properly freed:

HEAP SUMMARY:
==129253==     in use at exit: 0 bytes in 0 blocks
==129253==   total heap usage: 2 allocs, 2 frees, 1,044 bytes allocated
==129253==
==129253== All heap blocks were freed -- no leaks are possible

However, I still need to address some suppressed errors reported by Valgrind, mainly invalid writes, and conditional jumps depending on uninitialized values, which leads me to think that my pointer arithmetic might be slightly flawed. Here's a snippet of the Valgrind output for reference:

1 errors in context 1 of 9:
==129253== Conditional jump or move depends on uninitialised value(s)
==129253==    at 0x497E838: brk (brk.c:37)
==129253==    by 0x497E8F3: __sbrk (sbrk.c:74)
==129253==    by 0x497E8F3: sbrk (sbrk.c:36)
==129253==    by 0x109875: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 2 of 9:
==129253== Syscall param brk(end_data_segment) contains uninitialised byte(s)
==129253==    at 0x497E82B: brk (brk.c:36)
==129253==    by 0x497E8F3: __sbrk (sbrk.c:74)
==129253==    by 0x497E8F3: sbrk (sbrk.c:36)
==129253==    by 0x109875: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 3 of 9:
==129253== Conditional jump or move depends on uninitialised value(s)
==129253==    at 0x497E8E9: __sbrk (sbrk.c:66)
==129253==    by 0x497E8E9: sbrk (sbrk.c:36)
==129253==    by 0x109875: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 4 of 9:
==129253== Conditional jump or move depends on uninitialised value(s)
==129253==    at 0x497E8B9: __sbrk (sbrk.c:66)
==129253==    by 0x497E8B9: sbrk (sbrk.c:36)
==129253==    by 0x109875: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 5 of 9:
==129253== Conditional jump or move depends on uninitialised value(s)
==129253==    at 0x497E8B7: sbrk (sbrk.c:62)
==129253==    by 0x109875: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 6 of 9:
==129253== Conditional jump or move depends on uninitialised value(s)
==129253==    at 0x1097DE: my_free (in /home/projects/allocators/main)
==129253==    by 0x10924E: main (in /home/projects/allocators/main)
==129253==
==129253==
==129253== 1 errors in context 7 of 9:
==129253== Invalid write of size 8
==129253==    at 0x109699: my_malloc (in /home/projects/allocators/main)
==129253==    by 0x10923E: main (in /home/projects/allocators/main)
==129253==  Address 0x403c010 is in the brk data segment 0x403c000-0x403c02b
==129253==
==129253==
==129253== 1 errors in context 8 of 9:
==129253== Invalid write of size 8
==129253==    at 0x10968F: my_malloc (in /home/projects/allocators/main)
==129253==    by 0x10923E: main (in /home/projects/allocators/main)
==129253==  Address 0x403c000 is in the brk data segment 0x403c000-0x403c02b
==129253==
==129253==
==129253== 1 errors in context 9 of 9:
==129253== Invalid write of size 4
==129253==    at 0x10967A: my_malloc (in /home/projects/allocators/main)
==129253==    by 0x10923E: main (in /home/projects/allocators/main)
==129253==  Address 0x403c008 is in the brk data segment 0x403c000-0x403c02b
Sign up to request clarification or add additional context in comments.

2 Comments

So yes, for Valgrind to do its accounting correctly, the arguments to a use of VALGRIND_FREELIKE_BLOCK() must match the arguments to the corresponding use of VALGRIND_MALLOCLIKE_BLOCK(). These do not match properly in the original code.
As for the additional issues reported in this answer, that would be the subject of another question. However, if I understand correctly that you get those only after turning off suppressions provided with Valgrind then no, you do not have to address them, and you probably can't. That's why your Valgrind package comes with those suppressions. Typically, such suppressions reflect situations that a system library maintainer has validated, but which Valgrind is not clever enough to recognize as safe / correct. And anyway, you may have no recourse other than avoiding the affected function.
1

Valgrind memcheck does not operate at the level of sbrk. It does intercept calls to sbrk to check the arguments to the syscall and to maintain an internal memory map.

Memcheck operates at the level of malloc and operator new (and any user defined allocators using instrumentation as you have done). If it says that some memory hasn't been freed then it is at that level that you need to investigate and try to correct the error.

When you grow the break pointer you should be using

VALGRIND_MAKE_MEM_NOACCESS(pointer, size);

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.