musl/src/ldso/dlerror.c
Rich Felker 36b72cd6fd fix potential deadlock in dlerror buffer handling at thread exit
ever since commit 8f11e6127f introduced
the thread list lock, this has been wrong. initially, it was wrong via
calling free from the context with the thread list lock held. commit
aa5a9d15e0 deferred the unsafe free but
added a lock, which was also unsafe. in particular, it could deadlock
if code holding freebuf_queue_lock was interrupted by a signal handler
that takes the thread list lock.

commit 4d5aa20a94 observed that there
was a lock here but failed to notice that it's invalid.

there is no easy solution to this problem with locks; any attempt at
solving it while still using locks would require the lock to be an
AS-safe one (blocking signals on each access to the dlerror buffer
list to check if there's deferred free work to be done) which would be
excessively costly, and there are also lock order considerations with
respect to how the lock would be handled at fork.

instead, just use an atomic list.
2022-10-19 14:01:32 -04:00

89 lines
2 KiB
C

#include <dlfcn.h>
#include <stdlib.h>
#include <stdarg.h>
#include "pthread_impl.h"
#include "dynlink.h"
#include "atomic.h"
#define malloc __libc_malloc
#define calloc __libc_calloc
#define realloc __libc_realloc
#define free __libc_free
char *dlerror()
{
pthread_t self = __pthread_self();
if (!self->dlerror_flag) return 0;
self->dlerror_flag = 0;
char *s = self->dlerror_buf;
if (s == (void *)-1)
return "Dynamic linker failed to allocate memory for error message";
else
return s;
}
/* Atomic singly-linked list, used to store list of thread-local dlerror
* buffers for deferred free. They cannot be freed at thread exit time
* because, by the time it's known they can be freed, the exiting thread
* is in a highly restrictive context where it cannot call (even the
* libc-internal) free. It also can't take locks; thus the atomic list. */
static void *volatile freebuf_queue;
void __dl_thread_cleanup(void)
{
pthread_t self = __pthread_self();
if (!self->dlerror_buf || self->dlerror_buf == (void *)-1)
return;
void *h;
do {
h = freebuf_queue;
*(void **)self->dlerror_buf = h;
} while (a_cas_p(&freebuf_queue, h, self->dlerror_buf) != h);
}
hidden void __dl_vseterr(const char *fmt, va_list ap)
{
void **q;
do q = freebuf_queue;
while (q && a_cas_p(&freebuf_queue, q, 0) != q);
while (q) {
void **p = *q;
free(q);
q = p;
}
va_list ap2;
va_copy(ap2, ap);
pthread_t self = __pthread_self();
if (self->dlerror_buf != (void *)-1)
free(self->dlerror_buf);
size_t len = vsnprintf(0, 0, fmt, ap2);
if (len < sizeof(void *)) len = sizeof(void *);
va_end(ap2);
char *buf = malloc(len+1);
if (buf) {
vsnprintf(buf, len+1, fmt, ap);
} else {
buf = (void *)-1;
}
self->dlerror_buf = buf;
self->dlerror_flag = 1;
}
hidden void __dl_seterr(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
__dl_vseterr(fmt, ap);
va_end(ap);
}
static int stub_invalid_handle(void *h)
{
__dl_seterr("Invalid library handle %p", (void *)h);
return 1;
}
weak_alias(stub_invalid_handle, __dl_invalid_handle);