Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions modules/tls_openssl/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,44 @@ static int mod_load(void)
*/

LM_INFO("openssl version: %s\n", SSLeay_version(SSLEAY_VERSION));

#if OPENSSL_VERSION_NUMBER < 0x30000000L
/* OpenSSL 1.x - use custom shared memory allocator */
if (!CRYPTO_set_mem_functions(os_malloc, os_realloc, os_free)) {
LM_ERR("unable to set the memory allocation functions\n");
LM_ERR("NOTE: please make sure you are loading tls_mgm module at the"
"very beginning of your script, before any other module!\n");
return -1;
}
LM_INFO("Using custom shared memory allocator for OpenSSL\n");
#else
/*
* OpenSSL 3.x - use package memory allocator with double-free protection
*
* CRITICAL FIX for fork() safety:
* OpenSSL 3.x uses thread-local storage (TLS) extensively. When combined with
* fork(), this causes double-free issues:
* - Thread-local storage is duplicated across fork()
* - Multiple cleanup paths try to free the same buffer
* - This triggers pkg_malloc's double-free detection
*
* Solution: Use pkg_malloc wrappers with tracking headers that:
* 1. Detect and prevent double-free (mark freed blocks)
* 2. Validate pointers with magic numbers
* 3. Keep all pkg_malloc benefits (tracking, stats, debugging)
*
* Benefits:
* - OpenSSL memory tracked in pkg_stats
* - Memory leaks detectable
* - Resource limits enforced
* - Minimal overhead (12 bytes per allocation)
*/
if (!CRYPTO_set_mem_functions(os_pkg_malloc, os_pkg_realloc, os_pkg_free)) {
LM_ERR("unable to set pkg_malloc allocator for OpenSSL 3.x\n");
return -1;
}
LM_INFO("OpenSSL 3.x using pkg_malloc with double-free protection\n");
#endif

return 0;
}
Expand Down
165 changes: 165 additions & 0 deletions modules/tls_openssl/openssl_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ SSL_METHOD *ssl_methods[TLS_USE_TLSv1_2 + 1];
/*
* Wrappers around OpenSIPS shared memory functions
* (which can be macros)
*
* These are only used for OpenSSL 1.x. For OpenSSL 3.x, we use
* pkg_malloc wrappers with double-free protection (see below).
*/
#if OPENSSL_VERSION_NUMBER < 0x30000000L

#if OPENSSL_VERSION_NUMBER >= 0x10100000L
static void* os_malloc(size_t size, const char *file, int line)
#else
Expand Down Expand Up @@ -122,7 +127,167 @@ static void os_free(void *ptr)
#endif
}

#endif /* OPENSSL_VERSION_NUMBER < 0x30000000L */

#if OPENSSL_VERSION_NUMBER >= 0x30000000L
/*
* Wrappers around OpenSIPS package (private) memory functions with
* double-free protection for OpenSSL 3.x fork() safety
*
* === WHY THIS TRACKING LAYER IS NEEDED ===
*
* OpenSSL 3.x introduced extensive use of thread-local storage (TLS) for:
* - Error queues (per-thread error state)
* - RNG state (random number generator context)
* - Provider dispatch tables (crypto algorithm implementations)
* - Internal caches and buffers
*
* THE PROBLEM: OpenSSL 3.x + fork() + pkg_malloc causes double-free crashes
*
* 1. Parent process: OpenSSL allocates buffer X using pkg_malloc
* - Stores pointer to X in thread-local storage
* - May duplicate reference in multiple TLS slots
*
* 2. fork() happens:
* - Child process gets copy of all thread-local storage
* - Both parent and child have pointers to buffer X
* - Copy-on-write means both point to same physical memory initially
*
* 3. Connection cleanup (e.g., tcpconn_destroy):
* - OpenSSL walks through thread-local cleanup lists
* - Finds buffer X in multiple TLS slots (duplicated references)
* - Calls free() on X multiple times
* - pkg_malloc's debug allocator (qm_free_dbg) detects second free
* - CRITICAL ERROR: "freeing already freed pointer" -> process aborts
*
* THE SOLUTION: Thin tracking layer that:
*
* 1. Adds 12-byte header to each allocation:
* - magic number (validates pointer came from our allocator)
* - freed flag (prevents double-free)
* - size (for debugging)
*
* 2. On first free(): Set freed=1, actually call pkg_free()
* 3. On second free(): Detect freed=1, silently skip the free()
* 4. Invalid pointers: Detect wrong magic, log warning, skip free()
*
* BENEFITS:
* - Keeps pkg_malloc (memory tracking, statistics, debug, limits)
* - Prevents crash (works around OpenSSL bug)
* - Detects and logs the issue (debug visibility)
* - Minimal overhead (12 bytes per allocation)
* - Fork-safe (each process has its own tracking)
*
* ALTERNATIVE REJECTED: Using system malloc/free
* - Would hide the bug instead of fixing it
* - Loses all pkg_malloc benefits (no tracking, no stats, no limits)
* - Makes debugging harder (can't see OpenSSL memory usage)
*
* NOTE: This is a workaround for OpenSSL 3.x behavior. The real bug is in
* how OpenSSL manages thread-local storage across fork() boundaries.
*/
struct openssl_mem_hdr {
unsigned int magic; /* Magic: 0x4F53534C validates our allocation */
unsigned int freed; /* 0=allocated, 1=freed (prevents double-free) */
size_t size; /* Allocation size (for debugging/stats) */
};

#define OPENSSL_MEM_MAGIC 0x4F53534C /* "OSSL" in hex */

#if OPENSSL_VERSION_NUMBER >= 0x10100000L
static void* os_pkg_malloc(size_t size, const char *file, int line)
#else
static void* os_pkg_malloc(size_t size)
#endif
{
struct openssl_mem_hdr *hdr;

hdr = (struct openssl_mem_hdr *)pkg_malloc(sizeof(*hdr) + size);
if (!hdr)
return NULL;

hdr->magic = OPENSSL_MEM_MAGIC;
hdr->freed = 0;
hdr->size = size;

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


#if OPENSSL_VERSION_NUMBER >= 0x10100000L
static void* os_pkg_realloc(void *ptr, size_t size, const char *file, int line)
#else
static void* os_pkg_realloc(void *ptr, size_t size)
#endif
{
struct openssl_mem_hdr *old_hdr, *new_hdr;

if (!ptr)
return os_pkg_malloc(size
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
, file, line
#endif
);

old_hdr = ((struct openssl_mem_hdr *)ptr) - 1;

/* Validate magic number */
if (old_hdr->magic != OPENSSL_MEM_MAGIC) {
LM_ERR("OpenSSL realloc: invalid magic 0x%x for ptr %p\n",
old_hdr->magic, ptr);
return NULL;
}

/* Check if already freed */
if (old_hdr->freed) {
LM_ERR("OpenSSL realloc on freed pointer %p\n", ptr);
return NULL;
}

new_hdr = (struct openssl_mem_hdr *)pkg_realloc(old_hdr, sizeof(*new_hdr) + size);
if (!new_hdr)
return NULL;

new_hdr->size = size;

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


#if OPENSSL_VERSION_NUMBER >= 0x10100000L
static void os_pkg_free(void *ptr, const char *file, int line)
#else
static void os_pkg_free(void *ptr)
#endif
{
struct openssl_mem_hdr *hdr;

if (!ptr)
return;

hdr = ((struct openssl_mem_hdr *)ptr) - 1;

/* Validate magic number */
if (hdr->magic != OPENSSL_MEM_MAGIC) {
LM_WARN("OpenSSL free: invalid magic 0x%x for ptr %p, skipping free\n",
hdr->magic, ptr);
return;
}

/* Check for double-free */
if (hdr->freed) {
LM_DBG("OpenSSL double-free prevented for ptr %p (OpenSSL 3.x fork issue)\n", ptr);
return;
}

/* Mark as freed */
hdr->freed = 1;

/* Actually free the memory */
pkg_free(hdr);
}

#endif /* OPENSSL_VERSION_NUMBER >= 0x30000000L */


inline static unsigned long tls_get_id(void)
Expand Down
Loading