From 7e39f16e0f75cff59dd9199b14d54f867ccaa55b Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 21 Oct 2026 17:07:11 +0200 Subject: [PATCH] Fix allocation size overflow, zero-size OOM and debug category parsing --- lib/debug_config.c | 29 +++---- lib/debug_config.h | 4 +- lib/mem.c | 21 +++-- tests/Makefile.am | 5 ++ tests/test_mem.c | 130 ++++++++++++++++++++++++++++ tests/test_memory_pool_and_config.c | 101 +++++++++++---------- 6 files changed, 217 insertions(+), 73 deletions(-) create mode 100644 tests/test_mem.c diff --git a/lib/debug_config.c b/lib/debug_config.c index ffa9f791..9f30f206 100644 --- a/lib/debug_config.c +++ b/lib/debug_config.c @@ -460,7 +460,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) { g_log_hook = hook; } -/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */ +/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */ int debug_parse_config(const char* config_string) { if (!config_string || !*config_string) { return 0; @@ -468,12 +468,10 @@ int debug_parse_config(const char* config_string) { char* str = u_strdup(config_string); if (!str) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy"); return -1; } - debug_category_t categories = 0; - debug_level_t level = DEBUG_LEVEL_NONE; - char* token = strtok(str, ","); while (token) { /* Trim leading spaces */ @@ -481,15 +479,16 @@ int debug_parse_config(const char* config_string) { token++; } - char* colon = strchr(token, ':'); - if (!colon) { + char* separator = strpbrk(token, "=:"); + if (!separator) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token); u_free(str); return -1; } - *colon = '\0'; + *separator = '\0'; char* category_name = token; - char* level_name = colon + 1; + char* level_name = separator + 1; /* Trim leading spaces in level_name */ while (isspace(*level_name)) { @@ -497,22 +496,16 @@ int debug_parse_config(const char* config_string) { } debug_category_t cat = get_category_by_name(category_name); - if (cat == DEBUG_CATEGORY_NONE) { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name); + if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name); u_free(str); return -1; } debug_level_t lev = debug_level_from_name(level_name); - /* Set per-category level */ - int idx = 0; - while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) { - idx++; - } - if (idx < DEBUG_CATEGORY_COUNT) { - g_debug_config.category_levels[idx] = lev; - } + debug_set_category_level(cat, lev); + DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev); token = strtok(NULL, ","); } diff --git a/lib/debug_config.h b/lib/debug_config.h index cc1a7237..a2fdcb93 100644 --- a/lib/debug_config.h +++ b/lib/debug_config.h @@ -173,7 +173,9 @@ void debug_output(debug_level_t level, debug_category_t category_idx, typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message); void debug_set_log_hook(debug_log_hook_fn hook); -/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */ +/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '='). + * NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке; + * настройки до ошибочного элемента остаются применёнными. */ int debug_parse_config(const char* config_string); /* Convenience macros for debug output */ diff --git a/lib/mem.c b/lib/mem.c index e149cd50..69156134 100644 --- a/lib/mem.c +++ b/lib/mem.c @@ -13,6 +13,7 @@ #define CANARY 0xDEADBEEF #define PADDING_FILL 0xAA #define METADATA_SIZE 128 +#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE) #define POINTER_SIZE sizeof(void*) #define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2) #define PREV_OFFSET LOCATION_MAX @@ -89,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) { uint8_t* prefix_start = base + METADATA_SIZE; uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE); uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4); - uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE; + size_t total = (size_t)size + ALLOCATION_OVERHEAD; uint8_t* user = (uint8_t*)ptr; bool error = false; for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) { @@ -117,7 +118,7 @@ void u_check(void* ptr, const char* text, const char* location) { } if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc); - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base); hex_dump(base, total); exit(EXIT_FAILURE); @@ -125,13 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) { } void* u_malloc_impl(uint32_t size, const char* location) { + size_t total = size; + if (total > SIZE_MAX - ALLOCATION_OVERHEAD) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location); + return NULL; + } if (size >= LARGE_ALLOC_LOG_THRESHOLD) { DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s", size, (double)size / (1024.0 * 1024.0), location); } - uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE; + total += ALLOCATION_OVERHEAD; uint8_t* base = (uint8_t*)malloc(total); - if (!base && size > 0) return NULL; + if (!base) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location); + return NULL; + } memset(base, PADDING_FILL, total); // Set metadata size_t loc_len = strlen(location); @@ -209,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size); - DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE); + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count); DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8)); hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8); @@ -231,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) { allocated_count--; frees_count++; UNLOCK(); - uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE; + size_t total = (size_t)size + ALLOCATION_OVERHEAD; memset(base, 0, total); free(base); } diff --git a/tests/Makefile.am b/tests/Makefile.am index 4b05fcaa..6f573023 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -96,6 +96,7 @@ check_PROGRAMS = \ if OS_LINUX check_PROGRAMS += \ + test_mem \ test_stun_nat \ test_reality_fallback \ test_etcp_padding \ @@ -290,6 +291,10 @@ test_pool_corruption_log_SOURCES = test_pool_corruption_log.c test_pool_corruption_log_LDADD = $(COMMON_LIBS) # Linux/ELF: faults внедряются линкером, тестовых веток в библиотеках нет. +test_mem_SOURCES = test_mem.c +test_mem_LDADD = $(COMMON_LIBS) +test_mem_LDFLAGS = -Wl,--wrap=malloc + async_regression_sources = async_regression.c async_regression.h async_regression_ldflags = -Wl,--wrap=u_malloc_impl -Wl,--wrap=u_calloc_impl -Wl,--wrap=u_realloc_impl \ -Wl,--wrap=gettimeofday -Wl,--wrap=epoll_wait -Wl,--wrap=epoll_ctl -Wl,--wrap=eventfd -Wl,--wrap=epoll_create1 diff --git a/tests/test_mem.c b/tests/test_mem.c new file mode 100644 index 00000000..dccec67b --- /dev/null +++ b/tests/test_mem.c @@ -0,0 +1,130 @@ +/* Проверяем размеры реальных malloc и OOM без выделения гигабайт памяти. */ +#include +#include +#include +#include +#include + +#include "debug_config.h" +#include "mem.h" + +#define CHECK(expr) do { \ + if (!(expr)) { \ + fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \ + exit(EXIT_FAILURE); \ + } \ +} while (0) + +static bool fail_malloc; +static size_t malloc_calls; +static size_t malloc_size; + +void* __real_malloc(size_t size); + +void* __wrap_malloc(size_t size) { + malloc_calls++; + malloc_size = size; + if (fail_malloc) { + fail_malloc = false; + return NULL; + } + return __real_malloc(size); +} + +static void test_small_allocations(void) { + const uint32_t sizes[] = {0, 1, 3, 4, 255, 256, 257, 1024}; + size_t count = u_get_allocated_count(); + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + uint8_t* ptr = u_malloc(sizes[i]); + CHECK(ptr != NULL); + CHECK(u_get_allocated_count() == count + 1); + memset(ptr, 0x5a, sizes[i]); + u_check(ptr, "small allocation", LOCATION); + u_free(ptr); + CHECK(u_get_allocated_count() == count); + } + puts("PASS: zero, odd and ordinary allocation sizes"); +} + +static void test_oom(void) { + size_t count = u_get_allocated_count(); + fail_malloc = true; + CHECK(u_malloc(0) == NULL); + CHECK(!fail_malloc); + fail_malloc = true; + CHECK(u_calloc(0, UINT32_MAX) == NULL); + CHECK(!fail_malloc); + fail_malloc = true; + CHECK(u_calloc(UINT32_MAX, 0) == NULL); + CHECK(!fail_malloc); + fail_malloc = true; + CHECK(u_realloc(NULL, 0) == NULL); + CHECK(!fail_malloc); + fail_malloc = true; + CHECK(u_malloc(17) == NULL); + CHECK(!fail_malloc); + CHECK(u_get_allocated_count() == count); + + char* ptr = u_strdup("preserve on OOM"); + CHECK(ptr != NULL); + fail_malloc = true; + CHECK(u_realloc(ptr, 128) == NULL); + CHECK(!fail_malloc); + CHECK(strcmp(ptr, "preserve on OOM") == 0); + CHECK(u_get_allocated_count() == count + 1); + u_free(ptr); + CHECK(u_get_allocated_count() == count); + puts("PASS: OOM for zero/nonzero sizes and realloc preserves its old block"); +} + +static void test_large_sizes(void) { + /* Измеряем служебный размер по запросу malloc для пустого блока. */ + void* empty = u_malloc(0); + CHECK(empty != NULL); + size_t overhead = malloc_size; + u_free(empty); + CHECK(overhead > 0 && overhead <= 1024); + const uint32_t sizes[] = { + UINT32_MAX - 1024, UINT32_MAX - (uint32_t)overhead, UINT32_MAX - (uint32_t)overhead + 1, UINT32_MAX - 1, UINT32_MAX + }; + size_t count = u_get_allocated_count(); + char* old = u_strdup("preserve on large OOM"); + CHECK(old != NULL); + for (size_t i = 0; i < sizeof(sizes) / sizeof(sizes[0]); i++) { + for (int api = 0; api < 3; api++) { + size_t calls = malloc_calls; + fail_malloc = true; + void* ptr; + if (api == 0) ptr = u_malloc(sizes[i]); + else if (api == 1) ptr = u_calloc(sizes[i], 1); + else ptr = u_realloc(old, sizes[i]); + CHECK(ptr == NULL); + if ((uint64_t)sizes[i] + overhead > SIZE_MAX) { + CHECK(malloc_calls == calls); + CHECK(fail_malloc); + fail_malloc = false; + } else { + CHECK(malloc_calls == calls + 1); + CHECK(!fail_malloc); + CHECK(malloc_size == (uint64_t)sizes[i] + overhead); + } + CHECK(u_get_allocated_count() == count + 1); + CHECK(strcmp(old, "preserve on large OOM") == 0); + } + } + u_free(old); + CHECK(u_get_allocated_count() == count); + puts("PASS: sizes near UINT32_MAX reach malloc intact or are rejected before overflow"); +} + +int main(void) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_ERROR); + debug_set_category_level(DEBUG_CATEGORY_SYS, DEBUG_LEVEL_WARN); + test_small_allocations(); + test_oom(); + test_large_sizes(); + CHECK(u_get_allocated_count() == 0); + puts("PASS: mem regression tests"); + return 0; +} diff --git a/tests/test_memory_pool_and_config.c b/tests/test_memory_pool_and_config.c index e90cfecb..a9c58984 100644 --- a/tests/test_memory_pool_and_config.c +++ b/tests/test_memory_pool_and_config.c @@ -1,13 +1,11 @@ /** - * Test for memory pool optimization and configuration file support + * Проверка пулов памяти и настройки категорий отладки из строки. */ -#include "test_utils.h" #include #include #include #include -#include #include "../lib/ll_queue.h" #include "../lib/memory_pool.h" @@ -22,12 +20,60 @@ static void test_waiter_callback(struct ll_queue* q, void* arg) { test_callback_count++; } +static int test_debug_config(void) { + const char* configs[] = { + "sys=debug,etcp=error,crypto=disabled,discovery=trace", + "sys:debug,etcp:error,crypto:disabled,discovery:trace" + }; + for (size_t i = 0; i < sizeof(configs) / sizeof(configs[0]); i++) { + debug_config_init(); + debug_set_level(DEBUG_LEVEL_WARN); + if (debug_parse_config(configs[i]) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse configuration '%s'", configs[i]); + return 1; + } + for (int category = 0; category < DEBUG_CATEGORY_COUNT; category++) { + debug_level_t expected = DEBUG_LEVEL_NONE; + if (category == DEBUG_CATEGORY_SYS) expected = DEBUG_LEVEL_DEBUG; + else if (category == DEBUG_CATEGORY_ETCP) expected = DEBUG_LEVEL_ERROR; + else if (category == DEBUG_CATEGORY_CRYPTO) expected = DEBUG_LEVEL_DISABLED; + else if (category == DEBUG_CATEGORY_DISCOVERY) expected = DEBUG_LEVEL_TRACE; + if (g_debug_config.category_levels[category] != expected) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Category %d: expected level=%d, got=%d for '%s'", + category, expected, g_debug_config.category_levels[category], configs[i]); + return 1; + } + debug_level_t effective = expected == DEBUG_LEVEL_NONE ? DEBUG_LEVEL_WARN : expected; + for (debug_level_t level = DEBUG_LEVEL_ERROR; level <= DEBUG_LEVEL_TRACE; level++) { + int enabled = effective != DEBUG_LEVEL_DISABLED && level <= effective; + if (debug_should_output(level, category) != enabled) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Category %d, level=%d: expected output=%d for '%s'", + category, level, enabled, configs[i]); + return 1; + } + } + } + DEBUG_INFO(DEBUG_CATEGORY_SYS, "Configuration '%s': category indices and output filters PASS", configs[i]); + } + const char* invalid[] = {"sys", "unknown=debug", "all=debug"}; + for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { + debug_level_t previous[DEBUG_CATEGORY_COUNT]; + memcpy(previous, g_debug_config.category_levels, sizeof(previous)); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "Expecting rejection of invalid configuration '%s'", invalid[i]); + if (debug_parse_config(invalid[i]) != -1 || memcmp(previous, g_debug_config.category_levels, sizeof(previous)) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid configuration '%s' was accepted or changed category levels", invalid[i]); + return 1; + } + } + DEBUG_INFO(DEBUG_CATEGORY_SYS, "Invalid debug configurations rejected: PASS"); + return 0; +} + int main() { debug_config_init(); debug_set_level(DEBUG_LEVEL_TRACE); - debug_set_categories(DEBUG_CATEGORY_ALL); - DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== Memory Pool and Config File Test ==="); + DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== Memory Pool and Debug Configuration Test ==="); /* Любой размер блока, включая невыровненный хвост, допустим для malloc/realloc/free. */ for (uint32_t size = 1; size <= 17; size++) { unsigned char* p = u_malloc(size); @@ -97,49 +143,8 @@ int main() { DEBUG_INFO(DEBUG_CATEGORY_SYS, "Memory pool test: PASS"); - // Test 2: Configuration file support - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Test 2: Configuration file support..."); - - // Create a test configuration file - const char* config_file = "/tmp/debug_test.conf"; - FILE* f = fopen(config_file, "w"); - if (f) { - fprintf(f, "# Test debug configuration file\n"); - fprintf(f, "ll_queue:debug\n"); - fprintf(f, "uasync:info\n"); - fprintf(f, "memory:warn\n"); - fprintf(f, "# This is a comment\n"); - fprintf(f, "etcp:error\n"); - fclose(f); - - // Parse the configuration string (we don't have file parsing, so use string parsing) - const char* config_string = "ll_queue:debug,uasync:info,memory:warn,etcp:error"; - if (debug_parse_config(config_string) == 0) { - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Configuration parsed successfully"); - - // Test that configuration was applied - if (debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_SYS)) { - DEBUG_INFO(DEBUG_CATEGORY_SYS, "LL_QUEUE debug level correctly set"); - } - if (debug_should_output(DEBUG_LEVEL_INFO, DEBUG_CATEGORY_SYS)) { - DEBUG_INFO(DEBUG_CATEGORY_SYS, "UASYNC info level correctly set"); - } - if (!debug_should_output(DEBUG_LEVEL_DEBUG, DEBUG_CATEGORY_ETCP)) { - DEBUG_INFO(DEBUG_CATEGORY_ETCP, "ETCP correctly limited to error level"); - } - - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Note: Hot reload functionality not implemented in current debug system"); - - } else { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse configuration"); - } - - test_unlink(config_file); - } else { - DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to create test configuration file"); - } - - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Configuration file test: PASS"); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Test 2: Debug configuration strings..."); + if (test_debug_config() != 0) return 1; DEBUG_INFO(DEBUG_CATEGORY_SYS, "=== All tests completed successfully ==="); return 0;