519 lines
18 KiB
C
519 lines
18 KiB
C
/*
|
|
* stunnel TLS offloading and load-balancing proxy
|
|
* Copyright (C) 1998-2017 Michal Trojnara <Michal.Trojnara@stunnel.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
* See the GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, see <http://www.gnu.org/licenses>.
|
|
*
|
|
* Linking stunnel statically or dynamically with other modules is making
|
|
* a combined work based on stunnel. Thus, the terms and conditions of
|
|
* the GNU General Public License cover the whole combination.
|
|
*
|
|
* In addition, as a special exception, the copyright holder of stunnel
|
|
* gives you permission to combine stunnel with free software programs or
|
|
* libraries that are released under the GNU LGPL and with code included
|
|
* in the standard release of OpenSSL under the OpenSSL License (or
|
|
* modified versions of such code, with unchanged license). You may copy
|
|
* and distribute such a system following the terms of the GNU GPL for
|
|
* stunnel and the licenses of the other code concerned.
|
|
*
|
|
* Note that people who make modified versions of stunnel are not obligated
|
|
* to grant this special exception for their modified versions; it is their
|
|
* choice whether to do so. The GNU General Public License gives permission
|
|
* to release a modified version without this exception; this exception
|
|
* also makes it possible to release a modified version which carries
|
|
* forward this exception.
|
|
*/
|
|
|
|
#include "common.h"
|
|
#include "prototypes.h"
|
|
|
|
/* reportedly, malloc does not always return 16-byte aligned addresses
|
|
* for 64-bit targets as specified by
|
|
* https://msdn.microsoft.com/en-us/library/6ewkz86d.aspx */
|
|
#ifdef USE_WIN32
|
|
#define system_malloc(n) _aligned_malloc((n),16)
|
|
#define system_realloc(p,n) _aligned_realloc((p),(n),16)
|
|
#define system_free(p) _aligned_free(p)
|
|
#else
|
|
#define system_malloc(n) malloc(n)
|
|
#define system_realloc(p,n) realloc((p),(n))
|
|
#define system_free(p) free(p)
|
|
#endif
|
|
|
|
#define CANARY_INITIALIZED 0x0000c0ded0000000LL
|
|
#define CANARY_UNINTIALIZED 0x0000abadbabe0000LL
|
|
#define MAGIC_ALLOCATED 0x0000a110c8ed0000LL
|
|
#define MAGIC_DEALLOCATED 0x0000defec8ed0000LL
|
|
|
|
/* most platforms require allocations to be aligned */
|
|
#ifdef _MSC_VER
|
|
__declspec(align(16))
|
|
#endif
|
|
struct alloc_list_struct {
|
|
ALLOC_LIST *prev, *next;
|
|
TLS_DATA *tls;
|
|
size_t size;
|
|
const char *alloc_file, *free_file;
|
|
int alloc_line, free_line;
|
|
uint64_t valid_canary, magic;
|
|
#ifdef __GNUC__
|
|
} __attribute__((aligned(16)));
|
|
#else
|
|
#ifndef MSC_VER
|
|
uint64_t :0; /* align the structure */
|
|
#endif
|
|
};
|
|
#endif
|
|
|
|
#define LEAK_TABLE_SIZE 997
|
|
typedef struct {
|
|
const char *alloc_file;
|
|
int alloc_line;
|
|
int num, max;
|
|
} LEAK_ENTRY;
|
|
NOEXPORT LEAK_ENTRY leak_hash_table[LEAK_TABLE_SIZE],
|
|
*leak_results[LEAK_TABLE_SIZE];
|
|
NOEXPORT volatile int leak_result_num=0;
|
|
|
|
#ifdef USE_WIN32
|
|
NOEXPORT LPTSTR str_vtprintf(LPCTSTR, va_list);
|
|
#endif /* USE_WIN32 */
|
|
|
|
NOEXPORT void *str_realloc_internal_debug(void *, size_t, const char *, int);
|
|
|
|
NOEXPORT ALLOC_LIST *get_alloc_list_ptr(void *, const char *, int);
|
|
NOEXPORT void str_leak_debug(const ALLOC_LIST *, int);
|
|
|
|
NOEXPORT LEAK_ENTRY *leak_search(const ALLOC_LIST *);
|
|
NOEXPORT void leak_report();
|
|
NOEXPORT long leak_threshold();
|
|
|
|
TLS_DATA *ui_tls;
|
|
NOEXPORT uint8_t canary[10]; /* 80-bit canary value */
|
|
NOEXPORT volatile uint64_t canary_initialized=CANARY_UNINTIALIZED;
|
|
|
|
/**************************************** string manipulation functions */
|
|
|
|
#ifndef va_copy
|
|
#ifdef __va_copy
|
|
#define va_copy(dst, src) __va_copy((dst), (src))
|
|
#else /* __va_copy */
|
|
#define va_copy(dst, src) memcpy(&(dst), &(src), sizeof(va_list))
|
|
#endif /* __va_copy */
|
|
#endif /* va_copy */
|
|
|
|
char *str_dup_debug(const char *str, const char *file, int line) {
|
|
char *retval;
|
|
|
|
retval=str_alloc_debug(strlen(str)+1, file, line);
|
|
strcpy(retval, str);
|
|
return retval;
|
|
}
|
|
|
|
char *str_printf(const char *format, ...) {
|
|
char *txt;
|
|
va_list arglist;
|
|
|
|
va_start(arglist, format);
|
|
txt=str_vprintf(format, arglist);
|
|
va_end(arglist);
|
|
return txt;
|
|
}
|
|
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
|
|
#endif /* __GNUC__ */
|
|
char *str_vprintf(const char *format, va_list start_ap) {
|
|
int n;
|
|
size_t size=32;
|
|
char *p;
|
|
va_list ap;
|
|
|
|
p=str_alloc(size);
|
|
for(;;) {
|
|
va_copy(ap, start_ap);
|
|
n=vsnprintf(p, size, format, ap);
|
|
if(n>-1 && n<(int)size)
|
|
return p;
|
|
if(n>-1) /* glibc 2.1 */
|
|
size=(size_t)n+1; /* precisely what is needed */
|
|
else /* glibc 2.0, WIN32, etc. */
|
|
size*=2; /* twice the old size */
|
|
p=str_realloc(p, size);
|
|
}
|
|
}
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic pop
|
|
#endif /* __GNUC__ */
|
|
|
|
#ifdef USE_WIN32
|
|
|
|
LPTSTR str_tprintf(LPCTSTR format, ...) {
|
|
LPTSTR txt;
|
|
va_list arglist;
|
|
|
|
va_start(arglist, format);
|
|
txt=str_vtprintf(format, arglist);
|
|
va_end(arglist);
|
|
return txt;
|
|
}
|
|
|
|
NOEXPORT LPTSTR str_vtprintf(LPCTSTR format, va_list start_ap) {
|
|
int n;
|
|
size_t size=32;
|
|
LPTSTR p;
|
|
va_list ap;
|
|
|
|
p=str_alloc(size*sizeof(TCHAR));
|
|
for(;;) {
|
|
va_copy(ap, start_ap);
|
|
n=_vsntprintf(p, size, format, ap);
|
|
if(n>-1 && n<(int)size)
|
|
return p;
|
|
size*=2;
|
|
p=str_realloc(p, size*sizeof(TCHAR));
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
/**************************************** memory allocation wrappers */
|
|
|
|
void str_init(TLS_DATA *tls_data) {
|
|
tls_data->alloc_head=NULL;
|
|
tls_data->alloc_bytes=tls_data->alloc_blocks=0;
|
|
}
|
|
|
|
void str_cleanup(TLS_DATA *tls_data) {
|
|
/* free all attached allocations */
|
|
while(tls_data->alloc_head) /* str_free macro requires an lvalue */
|
|
str_free_expression(tls_data->alloc_head+1);
|
|
}
|
|
|
|
void str_canary_init() {
|
|
if(canary_initialized!=CANARY_UNINTIALIZED)
|
|
return; /* prevent double initialization on config reload */
|
|
RAND_bytes(canary, (int)sizeof canary);
|
|
/* an error would reduce the effectiveness of canaries */
|
|
/* this is nothing critical, so the return value is ignored here */
|
|
canary_initialized=CANARY_INITIALIZED; /* after RAND_bytes */
|
|
}
|
|
|
|
void str_stats() {
|
|
TLS_DATA *tls_data;
|
|
ALLOC_LIST *alloc_list;
|
|
int i=0;
|
|
|
|
if(!tls_initialized)
|
|
fatal("str not initialized");
|
|
leak_report();
|
|
tls_data=tls_get();
|
|
if(!tls_data || (!tls_data->alloc_blocks && !tls_data->alloc_bytes))
|
|
return; /* skip if no data is allocated */
|
|
s_log(LOG_DEBUG, "str_stats: %lu block(s), "
|
|
"%lu data byte(s), %lu control byte(s)",
|
|
(unsigned long)tls_data->alloc_blocks,
|
|
(unsigned long)tls_data->alloc_bytes,
|
|
(unsigned long)(tls_data->alloc_blocks*
|
|
(sizeof(ALLOC_LIST)+sizeof canary)));
|
|
for(alloc_list=tls_data->alloc_head; alloc_list; alloc_list=alloc_list->next) {
|
|
if(++i>10) /* limit the number of results */
|
|
break;
|
|
s_log(LOG_DEBUG, "str_stats: %lu byte(s) at %s:%d",
|
|
(unsigned long)alloc_list->size,
|
|
alloc_list->alloc_file, alloc_list->alloc_line);
|
|
}
|
|
}
|
|
|
|
void *str_alloc_debug(size_t size, const char *file, int line) {
|
|
TLS_DATA *tls_data;
|
|
ALLOC_LIST *alloc_list;
|
|
|
|
if(!tls_initialized)
|
|
fatal_debug("str not initialized", file, line);
|
|
tls_data=tls_get();
|
|
if(!tls_data) {
|
|
tls_data=tls_alloc(NULL, NULL, "alloc");
|
|
s_log(LOG_ERR, "INTERNAL ERROR: Uninitialized TLS at %s, line %d",
|
|
file, line);
|
|
}
|
|
|
|
alloc_list=(ALLOC_LIST *)str_alloc_detached_debug(size, file, line)-1;
|
|
alloc_list->prev=NULL;
|
|
alloc_list->next=tls_data->alloc_head;
|
|
alloc_list->tls=tls_data;
|
|
if(tls_data->alloc_head)
|
|
tls_data->alloc_head->prev=alloc_list;
|
|
tls_data->alloc_head=alloc_list;
|
|
tls_data->alloc_bytes+=size;
|
|
tls_data->alloc_blocks++;
|
|
|
|
return alloc_list+1;
|
|
}
|
|
|
|
void *str_alloc_detached_debug(size_t size, const char *file, int line) {
|
|
ALLOC_LIST *alloc_list;
|
|
|
|
#if 0
|
|
printf("allocating %lu bytes at %s:%d\n", (unsigned long)size, file, line);
|
|
#endif
|
|
alloc_list=system_malloc(sizeof(ALLOC_LIST)+size+sizeof canary);
|
|
if(!alloc_list)
|
|
fatal_debug("Out of memory", file, line);
|
|
memset(alloc_list, 0, sizeof(ALLOC_LIST)+size+sizeof canary);
|
|
alloc_list->prev=NULL; /* for debugging */
|
|
alloc_list->next=NULL; /* for debugging */
|
|
alloc_list->tls=NULL;
|
|
alloc_list->size=size;
|
|
alloc_list->alloc_file=file;
|
|
alloc_list->alloc_line=line;
|
|
alloc_list->free_file="none";
|
|
alloc_list->free_line=0;
|
|
alloc_list->valid_canary=canary_initialized; /* before memcpy */
|
|
memcpy((uint8_t *)(alloc_list+1)+size, canary, sizeof canary);
|
|
alloc_list->magic=MAGIC_ALLOCATED;
|
|
str_leak_debug(alloc_list, 1);
|
|
|
|
return alloc_list+1;
|
|
}
|
|
|
|
void *str_realloc_debug(void *ptr, size_t size, const char *file, int line) {
|
|
if(ptr)
|
|
return str_realloc_internal_debug(ptr, size, file, line);
|
|
else
|
|
return str_alloc_debug(size, file, line);
|
|
}
|
|
|
|
void *str_realloc_detached_debug(void *ptr, size_t size, const char *file, int line) {
|
|
if(ptr)
|
|
return str_realloc_internal_debug(ptr, size, file, line);
|
|
else
|
|
return str_alloc_detached_debug(size, file, line);
|
|
}
|
|
|
|
NOEXPORT void *str_realloc_internal_debug(void *ptr, size_t size, const char *file, int line) {
|
|
ALLOC_LIST *prev_alloc_list, *alloc_list;
|
|
|
|
prev_alloc_list=get_alloc_list_ptr(ptr, file, line);
|
|
str_leak_debug(prev_alloc_list, -1);
|
|
if(prev_alloc_list->size>size) /* shrinking the allocation */
|
|
memset((uint8_t *)ptr+size, 0, prev_alloc_list->size-size); /* paranoia */
|
|
alloc_list=system_realloc(prev_alloc_list, sizeof(ALLOC_LIST)+size+sizeof canary);
|
|
if(!alloc_list)
|
|
fatal_debug("Out of memory", file, line);
|
|
ptr=alloc_list+1;
|
|
if(size>alloc_list->size) /* growing the allocation */
|
|
memset((uint8_t *)ptr+alloc_list->size, 0, size-alloc_list->size);
|
|
if(alloc_list->tls) { /* not detached */
|
|
/* refresh possibly invalidated linked list pointers */
|
|
if(alloc_list->tls->alloc_head==prev_alloc_list)
|
|
alloc_list->tls->alloc_head=alloc_list;
|
|
if(alloc_list->next)
|
|
alloc_list->next->prev=alloc_list;
|
|
if(alloc_list->prev)
|
|
alloc_list->prev->next=alloc_list;
|
|
/* update statistics while the old size is still available */
|
|
alloc_list->tls->alloc_bytes+=size-alloc_list->size;
|
|
}
|
|
alloc_list->size=size;
|
|
alloc_list->alloc_file=file;
|
|
alloc_list->alloc_line=line;
|
|
alloc_list->free_file="none";
|
|
alloc_list->free_line=0;
|
|
alloc_list->valid_canary=canary_initialized; /* before memcpy */
|
|
memcpy((uint8_t *)ptr+size, canary, sizeof canary);
|
|
str_leak_debug(alloc_list, 1);
|
|
return ptr;
|
|
}
|
|
|
|
/* detach from thread automatic deallocation list */
|
|
/* it has no effect if the allocation is already detached */
|
|
void str_detach_debug(void *ptr, const char *file, int line) {
|
|
ALLOC_LIST *alloc_list;
|
|
|
|
if(!ptr) /* do not attempt to free null pointers */
|
|
return;
|
|
alloc_list=get_alloc_list_ptr(ptr, file, line);
|
|
if(alloc_list->tls) { /* not detached */
|
|
/* remove from linked list */
|
|
if(alloc_list->tls->alloc_head==alloc_list)
|
|
alloc_list->tls->alloc_head=alloc_list->next;
|
|
if(alloc_list->next)
|
|
alloc_list->next->prev=alloc_list->prev;
|
|
if(alloc_list->prev)
|
|
alloc_list->prev->next=alloc_list->next;
|
|
/* update statistics */
|
|
alloc_list->tls->alloc_bytes-=alloc_list->size;
|
|
alloc_list->tls->alloc_blocks--;
|
|
/* clear pointers */
|
|
alloc_list->next=NULL;
|
|
alloc_list->prev=NULL;
|
|
alloc_list->tls=NULL;
|
|
}
|
|
}
|
|
|
|
void str_free_debug(void *ptr, const char *file, int line) {
|
|
ALLOC_LIST *alloc_list;
|
|
|
|
if(!ptr) /* do not attempt to free null pointers */
|
|
return;
|
|
alloc_list=(ALLOC_LIST *)ptr-1;
|
|
if(alloc_list->magic==MAGIC_DEALLOCATED) { /* double free */
|
|
/* this may (unlikely) log garbage instead of file names */
|
|
s_log(LOG_CRIT,
|
|
"Double free attempt: ptr=%p alloc=%s:%d free#1=%s:%d free#2=%s:%d",
|
|
ptr,
|
|
alloc_list->alloc_file, alloc_list->alloc_line,
|
|
alloc_list->free_file, alloc_list->free_line,
|
|
file, line);
|
|
return;
|
|
}
|
|
str_detach_debug(ptr, file, line);
|
|
str_leak_debug(alloc_list, -1);
|
|
alloc_list->free_file=file;
|
|
alloc_list->free_line=line;
|
|
alloc_list->magic=MAGIC_DEALLOCATED; /* detect double free attempts */
|
|
memset(ptr, 0, alloc_list->size+sizeof canary); /* paranoia */
|
|
system_free(alloc_list);
|
|
}
|
|
|
|
NOEXPORT ALLOC_LIST *get_alloc_list_ptr(void *ptr, const char *file, int line) {
|
|
ALLOC_LIST *alloc_list;
|
|
|
|
if(!tls_initialized)
|
|
fatal_debug("str not initialized", file, line);
|
|
alloc_list=(ALLOC_LIST *)ptr-1;
|
|
if(alloc_list->magic!=MAGIC_ALLOCATED) /* not allocated by str_alloc() */
|
|
fatal_debug("Bad magic", file, line); /* LOL */
|
|
if(alloc_list->tls /* not detached */ && alloc_list->tls!=tls_get())
|
|
fatal_debug("Memory allocated in a different thread", file, line);
|
|
if(alloc_list->valid_canary!=CANARY_UNINTIALIZED &&
|
|
safe_memcmp((uint8_t *)ptr+alloc_list->size, canary, sizeof canary))
|
|
fatal_debug("Dead canary", file, line); /* LOL */
|
|
return alloc_list;
|
|
}
|
|
|
|
/**************************************** memory leak detection */
|
|
|
|
NOEXPORT void str_leak_debug(const ALLOC_LIST *alloc_list, int change) {
|
|
static size_t entries=0;
|
|
LEAK_ENTRY *entry;
|
|
int new_entry, allocations;
|
|
|
|
#if defined(USE_PTHREAD) || defined(USE_WIN32)
|
|
if(!&stunnel_locks[STUNNEL_LOCKS-1]) /* threads not initialized */
|
|
return;
|
|
#endif /* defined(USE_PTHREAD) || defined(USE_WIN32) */
|
|
if(!number_of_sections) /* configuration file not initialized */
|
|
return;
|
|
|
|
entry=leak_search(alloc_list);
|
|
/* the race condition may lead to false positives, which is handled later */
|
|
new_entry=entry->alloc_line!=alloc_list->alloc_line ||
|
|
entry->alloc_file!=alloc_list->alloc_file;
|
|
|
|
if(new_entry) { /* the file:line pair was encountered for the first time */
|
|
stunnel_write_lock(&stunnel_locks[LOCK_LEAK_HASH]);
|
|
entry=leak_search(alloc_list); /* the list may have changed */
|
|
if(entry->alloc_line==0) {
|
|
if(entries>LEAK_TABLE_SIZE-100) { /* this should never happen */
|
|
stunnel_write_unlock(&stunnel_locks[LOCK_LEAK_HASH]);
|
|
return;
|
|
}
|
|
entries++;
|
|
entry->alloc_line=alloc_list->alloc_line;
|
|
entry->alloc_file=alloc_list->alloc_file;
|
|
}
|
|
stunnel_write_unlock(&stunnel_locks[LOCK_LEAK_HASH]);
|
|
}
|
|
|
|
#ifdef PRECISE_LEAK_ALLOCATON_COUNTERS
|
|
/* this is *really* slow in OpenSSL < 1.1.0 */
|
|
CRYPTO_atomic_add(&entry->num, change, &allocations,
|
|
&stunnel_locks[LOCK_LEAK_HASH]);
|
|
#else
|
|
allocations=(entry->num+=change); /* we just need an estimate... */
|
|
#endif
|
|
|
|
if(allocations<=leak_threshold()) /* leak not detected */
|
|
return;
|
|
if(allocations<=entry->max) /* not the biggest leak for this entry */
|
|
return;
|
|
if(entry->max) { /* not the first time we found a leak for this entry */
|
|
entry->max=allocations; /* just update the value */
|
|
return;
|
|
}
|
|
/* we *may* need to allocate a new leak_results entry */
|
|
/* locking is slow, so we try to avoid it if possible */
|
|
stunnel_write_lock(&stunnel_locks[LOCK_LEAK_RESULTS]);
|
|
if(entry->max==0) { /* the table may have changed */
|
|
leak_results[leak_result_num]=entry;
|
|
entry->max=allocations;
|
|
++leak_result_num; /* at the end to avoid a lock in leak_report() */
|
|
} else { /* gracefully handle the race condition */
|
|
entry->max=allocations;
|
|
}
|
|
stunnel_write_unlock(&stunnel_locks[LOCK_LEAK_RESULTS]);
|
|
}
|
|
|
|
/* O(1) hash table lookup */
|
|
NOEXPORT LEAK_ENTRY *leak_search(const ALLOC_LIST *alloc_list) {
|
|
int i=alloc_list->alloc_line%LEAK_TABLE_SIZE;
|
|
|
|
while(!(leak_hash_table[i].alloc_line==0 ||
|
|
(leak_hash_table[i].alloc_line==alloc_list->alloc_line &&
|
|
leak_hash_table[i].alloc_file==alloc_list->alloc_file)))
|
|
i=(i+1)%LEAK_TABLE_SIZE;
|
|
return leak_hash_table+i;
|
|
}
|
|
|
|
/* report identified leaks */
|
|
NOEXPORT void leak_report() {
|
|
int i;
|
|
long limit;
|
|
|
|
limit=leak_threshold();
|
|
for(i=0; i<leak_result_num; ++i)
|
|
if(leak_results[i] /* an officious compiler could reorder code */ &&
|
|
leak_results[i]->max>limit /* the limit could have changed */)
|
|
s_log(LOG_WARNING, "Possible memory leak at %s:%d: %d allocations",
|
|
leak_results[i]->alloc_file, leak_results[i]->alloc_line,
|
|
leak_results[i]->max);
|
|
}
|
|
|
|
NOEXPORT long leak_threshold() {
|
|
long limit;
|
|
|
|
limit=10000*((int)number_of_sections+1);
|
|
#ifndef USE_FORK
|
|
limit+=100*num_clients;
|
|
#endif
|
|
return limit;
|
|
}
|
|
|
|
/**************************************** memcmp() replacement */
|
|
|
|
/* a version of memcmp() with execution time not dependent on data values */
|
|
/* it does *not* allow testing whether s1 is greater or lesser than s2 */
|
|
int safe_memcmp(const void *s1, const void *s2, size_t n) {
|
|
uint8_t *p1=(uint8_t *)s1, *p2=(uint8_t *)s2;
|
|
int r=0;
|
|
while(n--)
|
|
r|=(*p1++)^(*p2++);
|
|
return r;
|
|
}
|
|
|
|
/* end of str.c */
|