diff --git a/modules/tls_openssl/openssl.c b/modules/tls_openssl/openssl.c index 522b6825852..5eabe5c6962 100644 --- a/modules/tls_openssl/openssl.c +++ b/modules/tls_openssl/openssl.c @@ -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; } diff --git a/modules/tls_openssl/openssl_helpers.h b/modules/tls_openssl/openssl_helpers.h index ea4611c4839..02d2c37b30e 100644 --- a/modules/tls_openssl/openssl_helpers.h +++ b/modules/tls_openssl/openssl_helpers.h @@ -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 @@ -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)