Reputation: 1595
I've written a simple wrapper.so
over calloc()
and free()
to monitor memory calls and appears that pthreads_create()
is leaking memory.
After an initial allocation with calloc(17, 16)
(most of the time calloc(18, 16)
), it seems like that memory is being attempted to be free'd, but a nullptr
is passed to free()
instead.
What's happening here?
// test.cpp
#include <pthread.h>
#include <cassert>
#include <cstdlib>
void* p_dummy(void*)
{
return nullptr;
}
int main(void)
{
void* ptr = calloc(11, 11);
free(ptr);
pthread_t thread;
assert(pthread_create(&thread, nullptr, p_dummy, nullptr) == 0);
assert(pthread_join(thread, nullptr) == 0);
return 0;
}
// wrapper.cpp
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <dlfcn.h>
#include <cstdio>
static void (*real_free)(void* ptr) = NULL;
static void* (*real_calloc)(size_t nmemb, size_t size) = NULL;
static bool initializing = false;
static void init()
{
initializing = true;
fprintf(stderr,"init()\n");
real_free = (void (*)(void*))dlsym(RTLD_NEXT, "free");
real_calloc = (void* (*)(size_t, size_t))dlsym(RTLD_NEXT, "calloc");
if (!real_free or !real_calloc) {
fprintf(stderr,"Error in `dlsym()`: %s\n", dlerror());
}
initializing = false;
}
extern "C" void free(void *ptr)
{
fprintf(stderr,"free(%p)\n", ptr);
if (!real_free) {
init();
}
real_free(ptr);
}
extern "C" void* calloc(size_t nmemb, size_t size)
{
static char memory[32] { 0 }; // Hack to provide memory to dlsym()
if (initializing) {
fprintf(stderr,"calloc(%lu, %lu): %p\n", nmemb, size, &memory);
return memory;
}
if (!real_calloc) {
init();
}
void* ptr = real_calloc(nmemb, size);
fprintf(stderr,"calloc(%lu, %lu): %p\n", nmemb, size, ptr);
return ptr;
}
# Makefile
CC = g++
CFLAGS = -std=c++17 -Wall
all: test
test: test.cpp wrapper.so
$(CC) $(CFLAGS) -pthread -o test test.cpp -ldl
wrapper.o: wrapper.cpp
$(CC) $(CFLAGS) -c -fPIC -o wrapper.o wrapper.cpp
wrapper.so: wrapper.o
$(CC) $(CFLAGS) -shared -o wrapper.so wrapper.o -ldl
clean:
rm -f *.o *.so test
Output:
$ LD_PRELOAD=./wrapper.so ./test
init()
calloc(1, 32): 0x7f2a02bf1080 -- dlsym() requests memory
calloc(11, 11): 0x7fffd400d260 -- calloc(11, 11) in test.cpp
free(0x7fffd400d260) -- free() in test.cpp
calloc(17, 16): 0x7fffd400d2f0 -- pthread_create() requests memory
free((nil)) -- an attempt to free previously allocated memory?
Upvotes: 1
Views: 355
Reputation: 85541
The memory region in question is the DTV (dynamic thread vector), which can't be deallocated until program termination.
You can see it in GDB if you break on calloc
:
(gdb) bt
#0 __libc_calloc (n=17, elem_size=16) at malloc.c:3366
#1 0x00007ffff7fc52fa in calloc (nmemb=17, size=16) at wrapper.cpp:56
#2 0x00007ffff7fe39cb in allocate_dtv (result=0x7ffff7d86700) at ../elf/dl-tls.c:286
#3 __GI__dl_allocate_tls (mem=mem@entry=0x7ffff7d86700) at ../elf/dl-tls.c:532
#4 0x00007ffff7f8b323 in allocate_stack (stack=<synthetic pointer>, pdp=<synthetic pointer>, attr=0x7fffffffe300) at allocatestack.c:622
#5 __pthread_create_2_1 (newthread=<optimized out>, attr=<optimized out>, start_routine=<optimized out>, arg=<optimized out>) at pthread_create.c:660
#6 0x0000555555555273 in main () at test.cpp:19
That's by design (more details here if you're really curious).
Many bugs were reported about the "leak" in allocate_dtv
, all of which were rejected (example 1, 2).
The call to free(nullptr)
is unrelated to this, it's called from a thread cleanup function (__res_thread_freeres
).
Upvotes: 5
Reputation: 142080
I added:
extern "C" void free(void *ptr)
{
if (ptr == NULL) {
raise(SIGTRAP);
}
....
then fired up a debugger:
gdb --args env LD_PRELOAD=./wrapper.so ./test
run
it and bt
showed:
free((nil))
Thread 2 "test" received signal SIGTRAP, Trace/breakpoint trap.
[Switching to Thread 0x7ffff7a25640 (LWP 418295)]
0x00007ffff7c0a702 in raise () from /usr/lib/libpthread.so.0
(gdb) bt
#0 0x00007ffff7c0a702 in raise () from /usr/lib/libpthread.so.0
#1 0x00007ffff7fc1252 in free (ptr=0x0) at wrapper.cpp:39
#2 0x00007ffff7ab8027 in __libc_thread_freeres () from /usr/lib/libc.so.6
#3 0x00007ffff7c0027f in start_thread () from /usr/lib/libpthread.so.0
#4 0x00007ffff7b275e3 in clone () from /usr/lib/libc.so.6
(gdb)
A trivial google search resulted in __libc_thread_freeres which calls __strerror_thread_freeres which calls free(last_value)
. The last_value
is used as a thread-local value for memory allocated by strerror_l
. And it's NULL
, strerror
was never called.
Upvotes: 3