Overriding 'malloc' using the LD_PRELOAD mechanism
Asked Answered
N

6

51

I'm trying to write a simple shared library that would log malloc calls to stderr (a sort of 'mtrace' if you will).

However, this is not working. Here's what I do:

/* mtrace.c */
#include <dlfcn.h>
#include <stdio.h>

static void* (*real_malloc)(size_t);

void *malloc(size_t size)
{
    void *p = NULL;
    fprintf(stderr, "malloc(%d) = ", size);
    p = real_malloc(size);
    fprintf(stderr, "%p\n", p);
    return p;
}

static void __mtrace_init(void) __attribute__((constructor));
static void __mtrace_init(void)
{
    void *handle = NULL;
    handle = dlopen("libc.so.6", RTLD_LAZY);
    if (NULL == handle) {
        fprintf(stderr, "Error in `dlopen`: %s\n", dlerror());
        return;
    }
    real_malloc = dlsym(handle, "malloc");
    if (NULL == real_malloc) {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
        return;
    }
}

I compile this with:

gcc -shared -fPIC -o mtrace.so mtrace.c

And then when I try to execute ls:

$ LD_PRELOAD=./mtrace.so ls
malloc(352) = Segmentation fault

Now, I suspect that dlopen needs malloc, and as I am redefining it within the shared library, it uses that version with the still unassigned real_malloc.

The question is...how do I make it work?

P.S. sorry for the paucity in tags, I couldn't find appropriate tags, and I still don't have enough reputation to create new ones.

Ninety answered 21/5, 2011 at 16:3 Comment(2)
I have the same problem. It seems that the constructor function is not called always.Utoaztecan
FTR: for me it just was that printf() doesn't work with overloaded malloc, but fprintf() does.Hyperbaton
L
54

I always do it this way:

#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>

static void* (*real_malloc)(size_t)=NULL;

static void mtrace_init(void)
{
    real_malloc = dlsym(RTLD_NEXT, "malloc");
    if (NULL == real_malloc) {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
    }
}

void *malloc(size_t size)
{
    if(real_malloc==NULL) {
        mtrace_init();
    }

    void *p = NULL;
    fprintf(stderr, "malloc(%d) = ", size);
    p = real_malloc(size);
    fprintf(stderr, "%p\n", p);
    return p;
}

Don't use constructors, just initialize at first call to malloc. Use RTLD_NEXT to avoid dlopen. You can also try malloc hooks. Be aware that all those are GNU extensions, and probably wont work elsewhere.

Luhey answered 21/5, 2011 at 18:28 Comment(6)
Why is it important to call dlsym(RTLD_NEXT, "malloc") only once ?Faradism
@Faradism It's not really necessary. You can lookup malloc function on every call. It will just be a little slower.Luhey
Does this example support using functions in .so object which call malloc under the hood? For example I tried adding time print in your code (using gettimeofday, strftime) ad the code doesn’t work (is stuck).Darrondarrow
Don't use constructors, just initialize at first call to malloc Doing so results in code that is not multithread-safe. The code as currently posted also implicitly assumes fprintf() will never make a call to malloc(). Should fprintf() use malloc(), this code will spiral into infinite recursion.Ladanum
while this code works with overriding single malloc(), it fails if we override also calloc() in similar way. this answer should not be accepted as it's not a proper solution.Beefsteak
Wouldn't it be best to use __attribute__((constructor)) whenever possible instead of checking for correct initialisation at each invocation of the wrapper?Biforked
H
40

If you really want to use LD_PRELOAD with malloc and found that the code in the accepted answer still segfaults, I have a solution that seems to work.

The segfault was caused by dlsym calling calloc for 32 bytes, causing a recursion to the end of the stack.

My solution was to create a super-simple static allocator that takes care of allocations before dlsym returns the malloc function pointer.

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

char tmpbuff[1024];
unsigned long tmppos = 0;
unsigned long tmpallocs = 0;

void *memset(void*,int,size_t);
void *memmove(void *to, const void *from, size_t size);

/*=========================================================
 * interception points
 */

static void * (*myfn_calloc)(size_t nmemb, size_t size);
static void * (*myfn_malloc)(size_t size);
static void   (*myfn_free)(void *ptr);
static void * (*myfn_realloc)(void *ptr, size_t size);
static void * (*myfn_memalign)(size_t blocksize, size_t bytes);

static void init()
{
    myfn_malloc     = dlsym(RTLD_NEXT, "malloc");
    myfn_free       = dlsym(RTLD_NEXT, "free");
    myfn_calloc     = dlsym(RTLD_NEXT, "calloc");
    myfn_realloc    = dlsym(RTLD_NEXT, "realloc");
    myfn_memalign   = dlsym(RTLD_NEXT, "memalign");

    if (!myfn_malloc || !myfn_free || !myfn_calloc || !myfn_realloc || !myfn_memalign)
    {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
        exit(1);
    }
}

void *malloc(size_t size)
{
    static int initializing = 0;
    if (myfn_malloc == NULL)
    {
        if (!initializing)
        {
            initializing = 1;
            init();
            initializing = 0;

            fprintf(stdout, "jcheck: allocated %lu bytes of temp memory in %lu chunks during initialization\n", tmppos, tmpallocs);
        }
        else
        {
            if (tmppos + size < sizeof(tmpbuff))
            {
                void *retptr = tmpbuff + tmppos;
                tmppos += size;
                ++tmpallocs;
                return retptr;
            }
            else
            {
                fprintf(stdout, "jcheck: too much memory requested during initialisation - increase tmpbuff size\n");
                exit(1);
            }
        }
    }

    void *ptr = myfn_malloc(size);
    return ptr;
}

void free(void *ptr)
{
    // something wrong if we call free before one of the allocators!
//  if (myfn_malloc == NULL)
//      init();

    if (ptr >= (void*) tmpbuff && ptr <= (void*)(tmpbuff + tmppos))
        fprintf(stdout, "freeing temp memory\n");
    else
        myfn_free(ptr);
}

void *realloc(void *ptr, size_t size)
{
    if (myfn_malloc == NULL)
    {
        void *nptr = malloc(size);
        if (nptr && ptr)
        {
            memmove(nptr, ptr, size);
            free(ptr);
        }
        return nptr;
    }

    void *nptr = myfn_realloc(ptr, size);
    return nptr;
}

void *calloc(size_t nmemb, size_t size)
{
    if (myfn_malloc == NULL)
    {
        void *ptr = malloc(nmemb*size);
        if (ptr)
            memset(ptr, 0, nmemb*size);
        return ptr;
    }

    void *ptr = myfn_calloc(nmemb, size);
    return ptr;
}

void *memalign(size_t blocksize, size_t bytes)
{
    void *ptr = myfn_memalign(blocksize, bytes);
    return ptr;
}

Hope this helps someone.

Handley answered 4/4, 2012 at 9:17 Comment(6)
it definitely did. see the first comment on github.com/jtolds/malloc_instrumentation/blob/… though i should point out that we since found a better way github.com/jtolds/malloc_instrumentation/commit/…Cabanatuan
If you are using glibc, instead of making static allocator you can use __libc_calloc. The answer by @bdonlan mentions a better way supported by glibc, but I wanted to try dlsym. I have an example here: github.com/arhuaco/ram-is-mine/blob/master/src/ram_is_mine.c . Check void *realloc(...).Suazo
@Cabanatuan what is the reason for using __sync_fetch_and_add and __sync_fetch_and_sub? Is it to set some memory barriers?Mordred
Will the program crash if real free is called to free memory from the temp buffer?Photoelectrotype
Free from the temp buffer has no effect, but it shouldn't crash.Handley
Beware that ptr >= (void*) tmpbuff && ptr <= (void*)(tmpbuff + tmppos) is risky because the arithmetic assumes that ptr is in the same object as tmpbuff (comparison of unrelated pointers is UB). Also, why are some messages printed to stdout instead of stderr? That could really corrupt program data.Cestar
G
8

If you are using glibc, you should use its built in malloc hooking mechanism - the example in this page has an example of how to look up the original malloc. This is particularly important if you're adding additional tracking information to allocations, to ensure library functions which return malloc'd buffers are consistent with your free() implementation.

Glassy answered 22/5, 2011 at 4:52 Comment(4)
but malloc hooks are now deprecatedMordred
@Mordred : Since when? The latest version of the documentation does not mention anything about them being deprecated.Towhaired
@DanielKamilKozar here is one reference.Mordred
Also developers.redhat.com/articles/2021/08/25/…Collette
S
5

Here's an extension to the above examples which avoids segfaults in dlsym by using mmap until initialization is complete:

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <sys/mman.h>

static void* (*real_malloc)(size_t)         = NULL;
static void* (*real_realloc)(void*, size_t) = NULL;
static void* (*real_calloc)(size_t, size_t) = NULL;
static void  (*real_free)(void*)            = NULL;

static int alloc_init_pending = 0;

/* Load original allocation routines at first use */
static void alloc_init(void)
{
  alloc_init_pending = 1;
  real_malloc  = dlsym(RTLD_NEXT, "malloc");
  real_realloc = dlsym(RTLD_NEXT, "realloc");
  real_calloc  = dlsym(RTLD_NEXT, "calloc");
  real_free    = dlsym(RTLD_NEXT, "free");
  if (!real_malloc || !real_realloc || !real_calloc || !real_free) {
    fputs("alloc.so: Unable to hook allocation!\n", stderr);
    fputs(dlerror(), stderr);
    exit(1);
  } else {
    fputs("alloc.so: Successfully hooked\n", stderr);
  }
  alloc_init_pending = 0;
}

#define ZALLOC_MAX 1024
static void* zalloc_list[ZALLOC_MAX];
static size_t zalloc_cnt = 0;

/* dlsym needs dynamic memory before we can resolve the real memory 
 * allocator routines. To support this, we offer simple mmap-based 
 * allocation during alloc_init_pending. 
 * We support a max. of ZALLOC_MAX allocations.
 * 
 * On the tested Ubuntu 16.04 with glibc-2.23, this happens only once.
 */
void* zalloc_internal(size_t size)
{
  fputs("alloc.so: zalloc_internal called", stderr);
  if (zalloc_cnt >= ZALLOC_MAX-1) {
    fputs("alloc.so: Out of internal memory\n", stderr);
    return NULL;
  }
  /* Anonymous mapping ensures that pages are zero'd */
  void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
  if (MAP_FAILED == ptr) {
    perror("alloc.so: zalloc_internal mmap failed");
    return NULL;
  }
  zalloc_list[zalloc_cnt++] = ptr; /* keep track for later calls to free */
  return ptr;
}

void free(void* ptr)
{
  if (alloc_init_pending) {
    fputs("alloc.so: free internal\n", stderr);
    /* Ignore 'free' during initialization and ignore potential mem leaks 
     * On the tested system, this did not happen
     */
    return;
  }
  if(!real_malloc) {
    alloc_init();
  }
  for (size_t i = 0; i < zalloc_cnt; i++) {
    if (zalloc_list[i] == ptr) {
      /* If dlsym cleans up its dynamic memory allocated with zalloc_internal,
       * we intercept and ignore it, as well as the resulting mem leaks.
       * On the tested system, this did not happen
       */
      return;
    }
  }
  real_free(ptr);
}

void *malloc(size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: malloc internal\n", stderr);
    return zalloc_internal(size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  void* result = real_malloc(size);
  //fprintf(stderr, "alloc.so: malloc(0x%zx) = %p\n", size, result);
  return result;
}

void *realloc(void* ptr, size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: realloc internal\n", stderr);
    if (ptr) {
      fputs("alloc.so: realloc resizing not supported\n", stderr);
      exit(1);
    }
    return zalloc_internal(size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  return real_realloc(ptr, size);
}

void *calloc(size_t nmemb, size_t size)
{
  if (alloc_init_pending) {
    fputs("alloc.so: calloc internal\n", stderr);
    /* Be aware of integer overflow in nmemb*size.
     * Can only be triggered by dlsym */
    return zalloc_internal(nmemb * size);
  }
  if(!real_malloc) {
    alloc_init();
  }
  return real_calloc(nmemb, size);
}
Strickle answered 17/6, 2019 at 11:18 Comment(0)
H
4

Here is the simplest example for malloc and free hooking.

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

static void* (*real_malloc)(size_t size);
static void  (*real_free)(void *ptr);

__attribute__((constructor))
static void init()
{
        real_malloc = dlsym(RTLD_NEXT, "malloc");
        real_free   = dlsym(RTLD_NEXT, "free");
        fprintf(stderr, "init\n");
}

void *malloc(size_t size)
{
        void *ptr = real_malloc(size);
        fprintf(stderr, "malloc(%zd) = %p\n", size, ptr);
        return ptr;
}

void free(void *ptr)
{
        real_free(ptr);
        fprintf(stderr, "free(%p)\n", ptr);
}
Hairball answered 19/3, 2019 at 5:49 Comment(2)
How would you use this? Put this code in front of main and call init?Ostensive
It's quite late, but this program can be compiled as a shared library for example and preloaded with LD_PRELOAD when running the target program.Hairball
H
0

For c++ compiler there could be a need to extern directly overridden function like this

#include <stdio.h>
#include <dlfcn.h>

extern "C" void *malloc(size_t size);

static void* (*real_malloc)(size_t)=NULL;

static void mtrace_init(void)
{
    real_malloc = dlsym(RTLD_NEXT, "malloc");
    if (NULL == real_malloc) {
        fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
    }
}

void *malloc(size_t size)
{
    if(real_malloc==NULL) {
        mtrace_init();
    }

    void *p = NULL;
    fprintf(stderr, "malloc(%d) = ", size);
    p = real_malloc(size);
    fprintf(stderr, "%p\n", p);
    return p;
}
Hereford answered 16/2 at 8:26 Comment(2)
For c++ compiler there could be a need to extern directly overridden function: What is your reference for this?Sweetening
copyprogramming.com/howto/… + my analysis that without it nothing was overridden (g++ case).Hereford

© 2022 - 2024 — McMap. All rights reserved.