ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
symbols.c
Go to the documentation of this file.
1
7// Platform-specific binary names
8#ifdef _WIN32
9#define LLVM_SYMBOLIZER_BIN "llvm-symbolizer.exe"
10#define ADDR2LINE_BIN "addr2line.exe"
11#define popen _popen
12#define pclose _pclose
13#else
14#define LLVM_SYMBOLIZER_BIN "llvm-symbolizer"
15#define ADDR2LINE_BIN "addr2line"
16#endif
17
18#include <stdlib.h>
19#include <string.h>
20#include <stdio.h>
21#include <stdatomic.h>
22
23#ifndef _WIN32
24#include <unistd.h>
25#else
26#include <windows.h>
27#endif
28
29#include <ascii-chat/platform/symbols.h>
30#include <ascii-chat/platform/system.h>
31#include <ascii-chat/common.h>
32#include <ascii-chat/uthash/uthash.h>
33#include <ascii-chat/platform/rwlock.h>
34#include <ascii-chat/platform/init.h>
35#include <ascii-chat/util/path.h>
36#include <ascii-chat/util/string.h>
37
38// ============================================================================
39// Constants
40// ============================================================================
41
42// Sentinel string for failed allocations (replaces NULL in middle of array)
43#define NULL_SENTINEL "[NULL]"
44
45// ============================================================================
46// Symbolizer Type Selection
47// ============================================================================
48
49typedef enum {
50 SYMBOLIZER_NONE = 0, // No symbolizer available, use raw addresses
51 SYMBOLIZER_LLVM = 1, // llvm-symbolizer (preferred on all platforms)
52 SYMBOLIZER_ADDR2LINE = 2, // addr2line (fallback)
54
55static symbolizer_type_t g_symbolizer_type = SYMBOLIZER_NONE;
56static atomic_bool g_symbolizer_detected = false;
57static atomic_bool g_llvm_symbolizer_checked = false;
58static atomic_bool g_llvm_symbolizer_available = false;
59static char g_llvm_symbolizer_cmd[PLATFORM_MAX_PATH_LENGTH];
60static atomic_bool g_addr2line_checked = false;
61static atomic_bool g_addr2line_available = false;
62static char g_addr2line_cmd[PLATFORM_MAX_PATH_LENGTH];
63static static_mutex_t g_symbolizer_detection_mutex = STATIC_MUTEX_INIT;
64
65// ============================================================================
66// Cache State
67// ============================================================================
68
105typedef struct {
107 void *addr;
109 char *symbol;
111 UT_hash_handle hh;
113
114static symbol_entry_t *g_symbol_cache = NULL; // uthash uses structure pointer as head
115static rwlock_t g_symbol_cache_lock = {0}; // External locking for thread safety
116static atomic_bool g_symbol_cache_initialized = false;
117
118// Statistics
119static atomic_uint_fast64_t g_cache_hits = 0;
120static atomic_uint_fast64_t g_cache_misses = 0;
121
122// ============================================================================
123// Helper Functions
124// ============================================================================
125
130static bool symbolizer_path_is_executable(const char *path) {
131#ifdef _WIN32
132 if (!path || path[0] == '\0') {
133 return false;
134 }
135
136 DWORD attrs = GetFileAttributesA(path);
137 if (attrs == INVALID_FILE_ATTRIBUTES) {
138 return false;
139 }
140 if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
141 return false;
142 }
143
144 HANDLE handle = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
145 FILE_ATTRIBUTE_NORMAL, NULL);
146 if (handle == INVALID_HANDLE_VALUE) {
147 return false;
148 }
149
150 CloseHandle(handle);
151 return true;
152#else
153 return (path && path[0] != '\0' && access(path, X_OK) == 0);
154#endif
155}
156
157static const char *get_llvm_symbolizer_command(void) {
158 if (!atomic_load(&g_llvm_symbolizer_checked)) {
159 static_mutex_lock(&g_symbolizer_detection_mutex);
160 if (!atomic_load(&g_llvm_symbolizer_checked)) {
161 const char *env_path = SAFE_GETENV("LLVM_SYMBOLIZER_PATH");
162 bool available = false;
163
164 g_llvm_symbolizer_cmd[0] = '\0';
165
166 if (env_path && env_path[0] != '\0') {
167 if (symbolizer_path_is_executable(env_path)) {
168 SAFE_STRNCPY(g_llvm_symbolizer_cmd, env_path, sizeof(g_llvm_symbolizer_cmd));
169 available = true;
170 log_dev("Using llvm-symbolizer from LLVM_SYMBOLIZER_PATH: %s", env_path);
171 } else {
172 log_warn("LLVM_SYMBOLIZER_PATH is set but not executable: %s", env_path);
173 }
174 }
175
177 available = true;
178 g_llvm_symbolizer_cmd[0] = '\0'; // Use binary name from PATH
179 log_dev("Found %s in PATH", LLVM_SYMBOLIZER_BIN);
180 }
181
182 atomic_store(&g_llvm_symbolizer_available, available);
183 atomic_store(&g_llvm_symbolizer_checked, true);
184 }
185 static_mutex_unlock(&g_symbolizer_detection_mutex);
186 }
187
188 if (!atomic_load(&g_llvm_symbolizer_available)) {
189 return NULL;
190 }
191
192 if (g_llvm_symbolizer_cmd[0] != '\0') {
193 return g_llvm_symbolizer_cmd;
194 }
195
196 return LLVM_SYMBOLIZER_BIN;
197}
198
199static const char *get_addr2line_command(void) {
200 if (!atomic_load(&g_addr2line_checked)) {
201 static_mutex_lock(&g_symbolizer_detection_mutex);
202 if (!atomic_load(&g_addr2line_checked)) {
203 const char *env_path = SAFE_GETENV("ADDR2LINE_PATH");
204 bool available = false;
205
206 g_addr2line_cmd[0] = '\0';
207
208 if (env_path && env_path[0] != '\0') {
209 if (symbolizer_path_is_executable(env_path)) {
210 SAFE_STRNCPY(g_addr2line_cmd, env_path, sizeof(g_addr2line_cmd));
211 available = true;
212 log_dev("Using addr2line from ADDR2LINE_PATH: %s", env_path);
213 } else {
214 log_warn("ADDR2LINE_PATH is set but not executable: %s", env_path);
215 }
216 }
217
218 if (!available && platform_is_binary_in_path(ADDR2LINE_BIN)) {
219 available = true;
220 g_addr2line_cmd[0] = '\0'; // Use binary name from PATH
221 log_dev("Found %s in PATH", ADDR2LINE_BIN);
222 }
223
224 atomic_store(&g_addr2line_available, available);
225 atomic_store(&g_addr2line_checked, true);
226 }
227 static_mutex_unlock(&g_symbolizer_detection_mutex);
228 }
229
230 if (!atomic_load(&g_addr2line_available)) {
231 return NULL;
232 }
233
234 if (g_addr2line_cmd[0] != '\0') {
235 return g_addr2line_cmd;
236 }
237
238 return ADDR2LINE_BIN;
239}
240
241static symbolizer_type_t detect_symbolizer(void) {
242 // Prefer llvm-symbolizer on all platforms (including macOS)
243 // We handle ASLR ourselves using dyld APIs on macOS
244 const char *llvm_symbolizer = get_llvm_symbolizer_command();
245 if (llvm_symbolizer) {
246 log_dev("Using llvm-symbolizer for symbol resolution");
247 return SYMBOLIZER_LLVM;
248 }
249
250 const char *addr2line_cmd = get_addr2line_command();
251 if (addr2line_cmd) {
252 log_debug("Using addr2line command: %s", addr2line_cmd);
254 }
255
256 log_warn("No symbolizer found in PATH (tried %s, %s) - using native backend", LLVM_SYMBOLIZER_BIN, ADDR2LINE_BIN);
257 return SYMBOLIZER_NONE;
258}
259
260// ============================================================================
261// Public API Implementation
262// ============================================================================
263
264asciichat_error_t symbol_cache_init(void) {
265 bool expected = false;
266 if (!atomic_compare_exchange_strong(&g_symbol_cache_initialized, &expected, true)) {
267 return 0; // Already initialized
268 }
269
270 // Detect which symbolizer is available (once at init)
271 expected = false;
272 if (atomic_compare_exchange_strong(&g_symbolizer_detected, &expected, true)) {
273 g_symbolizer_type = detect_symbolizer();
274 }
275
276 // Initialize rwlock for thread safety (uthash requires external locking)
277 if (rwlock_init(&g_symbol_cache_lock) != 0) {
278 atomic_store(&g_symbol_cache_initialized, false);
279 return SET_ERRNO(ERROR_THREAD, "Failed to initialize symbol cache rwlock");
280 }
281
282 // Initialize uthash head to NULL (required)
283 g_symbol_cache = NULL;
284
285 atomic_store(&g_cache_hits, 0);
286 atomic_store(&g_cache_misses, 0);
287
288 log_dev("Symbol cache initialized");
289 return 0;
290}
291
293 if (!atomic_load(&g_symbol_cache_initialized)) {
294 return;
295 }
296
297 // Mark as uninitialized FIRST to prevent new inserts during cleanup
298 atomic_store(&g_symbol_cache_initialized, false);
299
300 // Acquire write lock to prevent any concurrent operations
301 rwlock_wrlock(&g_symbol_cache_lock);
302
303 // Count entries before freeing for debugging
304 size_t entry_count = HASH_COUNT(g_symbol_cache);
305
306 // Free all symbol entries using HASH_ITER
307 symbol_entry_t *entry = NULL, *tmp = NULL;
308 size_t freed_count = 0;
309 HASH_ITER(hh, g_symbol_cache, entry, tmp) {
310 if (entry) {
311 HASH_DEL(g_symbol_cache, entry);
312 if (entry->symbol) {
313 free(entry->symbol);
314 }
315 free(entry);
316 freed_count++;
317 }
318 }
319
320 // Release lock and destroy rwlock
321 rwlock_wrunlock(&g_symbol_cache_lock);
322 rwlock_destroy(&g_symbol_cache_lock);
323
324 g_symbol_cache = NULL;
325
326 log_dev("Symbol cache cleaned up: %zu entries counted, %zu entries freed (hits=%llu, misses=%llu)", entry_count,
327 freed_count, (unsigned long long)atomic_load(&g_cache_hits),
328 (unsigned long long)atomic_load(&g_cache_misses));
329}
330
331const char *symbol_cache_lookup(void *addr) {
332 if (!atomic_load(&g_symbol_cache_initialized) || !addr) {
333 return NULL;
334 }
335
336 rwlock_rdlock(&g_symbol_cache_lock);
337
338 symbol_entry_t *entry = NULL;
339 HASH_FIND_PTR(g_symbol_cache, &addr, entry);
340
341 const char *result = NULL;
342 if (entry) {
343 // Copy string while holding lock to prevent use-after-free
344 result = platform_strdup(entry->symbol);
345 atomic_fetch_add(&g_cache_hits, 1);
346 } else {
347 atomic_fetch_add(&g_cache_misses, 1);
348 }
349
350 rwlock_rdunlock(&g_symbol_cache_lock);
351 return result;
352}
353
354bool symbol_cache_insert(void *addr, const char *symbol) {
355 if (!atomic_load(&g_symbol_cache_initialized) || !addr || !symbol) {
356 return false;
357 }
358
359 // Acquire write lock to make the entire operation atomic
360 rwlock_wrlock(&g_symbol_cache_lock);
361
362 // Double-check cache is still initialized after acquiring lock
363 // (cleanup might have marked it uninitialized between our check and lock acquisition)
364 if (!atomic_load(&g_symbol_cache_initialized)) {
365 rwlock_wrunlock(&g_symbol_cache_lock);
366 return false;
367 }
368
369 // Check if entry already exists
370 symbol_entry_t *existing = NULL;
371 HASH_FIND_PTR(g_symbol_cache, &addr, existing);
372
373 if (existing) {
374 // Entry exists - update symbol if different
375 if (existing->symbol && strcmp(existing->symbol, symbol) != 0) {
376 // Free old symbol and allocate new one
377 free(existing->symbol);
378 existing->symbol = platform_strdup(symbol);
379 if (!existing->symbol) {
380 rwlock_wrunlock(&g_symbol_cache_lock);
381 return false;
382 }
383 }
384 rwlock_wrunlock(&g_symbol_cache_lock);
385 return true;
386 }
387
388 // Create new entry
389 symbol_entry_t *entry = (symbol_entry_t *)malloc(sizeof(symbol_entry_t));
390 if (!entry) {
391 rwlock_wrunlock(&g_symbol_cache_lock);
392 return false;
393 }
394
395 entry->addr = addr;
396 entry->symbol = platform_strdup(symbol);
397 if (!entry->symbol) {
398 free(entry);
399 rwlock_wrunlock(&g_symbol_cache_lock);
400 return false;
401 }
402
403 // Add to hash table
404 HASH_ADD_PTR(g_symbol_cache, addr, entry);
405
406 // Release lock
407 rwlock_wrunlock(&g_symbol_cache_lock);
408
409 return true;
410}
411
412void symbol_cache_get_stats(uint64_t *hits_out, uint64_t *misses_out, size_t *entries_out) {
413 if (hits_out) {
414 *hits_out = atomic_load(&g_cache_hits);
415 }
416 if (misses_out) {
417 *misses_out = atomic_load(&g_cache_misses);
418 }
419 if (entries_out) {
420 rwlock_rdlock(&g_symbol_cache_lock);
421 *entries_out = HASH_COUNT(g_symbol_cache);
422 rwlock_rdunlock(&g_symbol_cache_lock);
423 }
424}
425
427 uint64_t hits = atomic_load(&g_cache_hits);
428 uint64_t misses = atomic_load(&g_cache_misses);
429
430 rwlock_rdlock(&g_symbol_cache_lock);
431 size_t entries = HASH_COUNT(g_symbol_cache);
432 rwlock_rdunlock(&g_symbol_cache_lock);
433
434 uint64_t total = hits + misses;
435 double hit_rate = total > 0 ? (100.0 * (double)hits / (double)total) : 0.0;
436
437 log_debug("Symbol Cache Stats: %zu entries, %llu hits, %llu misses (%.1f%% hit rate)", entries,
438 (unsigned long long)hits, (unsigned long long)misses, hit_rate);
439}
440
441// ============================================================================
442// Batch Resolution with llvm-symbolizer and addr2line
443// ============================================================================
444
451static char *parse_llvm_symbolizer_result(FILE *fp, void *addr) {
452 char func_name[512] = "??";
453 char file_location[512] = "??:0";
454 char blank_line[8];
455
456 // Read function name line
457 if (fgets(func_name, sizeof(func_name), fp) == NULL) {
458 return NULL;
459 }
460 func_name[strcspn(func_name, "\n")] = '\0';
461
462 // Read file location line
463 if (fgets(file_location, sizeof(file_location), fp) == NULL) {
464 return NULL;
465 }
466 file_location[strcspn(file_location, "\n")] = '\0';
467
468 // Read blank separator line (and discard it)
469 if (fgets(blank_line, sizeof(blank_line), fp) == NULL) {
470 // End of output - not an error
471 }
472
473 // Remove column number (last :N) from file_location
474 char *last_colon = strrchr(file_location, ':');
475 if (last_colon) {
476 *last_colon = '\0';
477 }
478
479 // Extract relative path and COPY IT IMMEDIATELY
480 // IMPORTANT: extract_project_relative_path uses a static buffer internally.
481 // SAFE_MALLOC's debug memory tracking also calls extract_project_relative_path,
482 // which would overwrite the static buffer. So we must copy before allocating.
483 const char *rel_path_tmp = extract_project_relative_path(file_location);
484 char rel_path[512];
485 SAFE_STRNCPY(rel_path, rel_path_tmp, sizeof(rel_path));
486
487 // Allocate result buffer (AFTER copying rel_path to local storage)
488 char *result = SAFE_MALLOC(1024, char *);
489 if (!result) {
490 return NULL;
491 }
492
493 // Format symbol
494 bool has_func = (strcmp(func_name, "??") != 0 && strlen(func_name) > 0);
495 bool has_file =
496 (strcmp(file_location, "??:0") != 0 && strcmp(file_location, "??:?") != 0 && strcmp(file_location, "??") != 0);
497
498 // Remove () from function name if present (llvm-symbolizer includes them)
499 char clean_func[512];
500 SAFE_STRNCPY(clean_func, func_name, sizeof(clean_func));
501 char *paren = strstr(clean_func, "()");
502 if (paren) {
503 *paren = '\0';
504 }
505
506 if (!has_func && !has_file) {
507 SAFE_SNPRINTF(result, 1024, "%p", addr);
508 } else if (has_func && has_file) {
509 SAFE_SNPRINTF(result, 1024, "%s() (%s)", clean_func, rel_path);
510 } else if (has_func) {
511 SAFE_SNPRINTF(result, 1024, "%s()", clean_func);
512 } else {
513 SAFE_SNPRINTF(result, 1024, "%s (unknown function)", rel_path);
514 }
515
516 return result;
517}
518
528static char **run_llvm_symbolizer_batch(void *const *buffer, int size) {
529 if (size <= 0 || !buffer) {
530 return NULL;
531 }
532
533 const char *symbolizer_cmd = get_llvm_symbolizer_command();
534 if (!symbolizer_cmd) {
535 log_debug("llvm-symbolizer not available - skipping symbolization");
536 return NULL;
537 }
538
539 // Allocate result array
540 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
541 if (!result) {
542 return NULL;
543 }
544
545 // ============================================================================
546 // Unified platform-agnostic implementation using get_binary_file_address_offsets()
547 // ============================================================================
548
549 // Group addresses by binary using platform-independent interface
550 // Works identically on Linux, macOS, and Windows
551 typedef struct {
552 char binary_path[PLATFORM_MAX_PATH_LENGTH];
553 uintptr_t file_offsets[64]; // Max addresses per binary
554 int original_indices[64];
555 int count;
556 } binary_group_t;
557
558 binary_group_t groups[8]; // Support up to 8 different binaries
559 int num_groups = 0;
560
561 // First pass: Call get_binary_file_address_offsets() for each address
562 // (same function works on all platforms - zero #ifdefs needed!)
563 for (int i = 0; i < size; i++) {
564 platform_binary_match_t match;
565 if (get_binary_file_address_offsets(buffer[i], &match, 1) == 0) {
566 // Could not find binary - use raw address as fallback
567 result[i] = SAFE_MALLOC(32, char *);
568 if (result[i]) {
569 SAFE_SNPRINTF(result[i], 32, "%p", buffer[i]);
570 }
571 continue;
572 }
573
574 // Find or create group for this binary
575 int group_idx = -1;
576 for (int g = 0; g < num_groups; g++) {
577 if (strcmp(groups[g].binary_path, match.path) == 0) {
578 group_idx = g;
579 break;
580 }
581 }
582
583 if (group_idx < 0 && num_groups < 8) {
584 group_idx = num_groups++;
585 SAFE_STRNCPY(groups[group_idx].binary_path, match.path, sizeof(groups[group_idx].binary_path));
586 groups[group_idx].count = 0;
587 }
588
589 if (group_idx >= 0 && groups[group_idx].count < 64) {
590 int idx = groups[group_idx].count++;
591 groups[group_idx].file_offsets[idx] = match.file_offset;
592 groups[group_idx].original_indices[idx] = i;
593 }
594 }
595
596 // Second pass: batch symbolize each group
597 for (int g = 0; g < num_groups; g++) {
598 if (groups[g].count == 0) {
599 continue;
600 }
601
602 // Escape binary path for shell
603 char escaped_binary_path[PLATFORM_MAX_PATH_LENGTH * 2];
604 if (!escape_path_for_shell(groups[g].binary_path, escaped_binary_path, sizeof(escaped_binary_path))) {
605 continue;
606 }
607
608 // Build command with all addresses for this binary
609 char cmd[8192];
610 int offset = safe_snprintf(cmd, sizeof(cmd), "%s --demangle --output-style=LLVM --relativenames -e %s ",
611 symbolizer_cmd, escaped_binary_path);
612
613 for (int j = 0; j < groups[g].count && offset < (int)sizeof(cmd) - 32; j++) {
614 int n =
615 safe_snprintf(cmd + offset, sizeof(cmd) - (size_t)offset, "0x%lx ", (unsigned long)groups[g].file_offsets[j]);
616 if (n > 0) {
617 offset += n;
618 }
619 }
620
621 // Suppress stderr
622 strncat(cmd, "2>/dev/null", sizeof(cmd) - strlen(cmd) - 1);
623
624 FILE *fp = popen(cmd, "r");
625 if (!fp) {
626 continue;
627 }
628
629 // Extract binary name (basename) for display
630 const char *binary_name = strrchr(groups[g].binary_path, '/');
631 if (!binary_name) {
632 binary_name = strrchr(groups[g].binary_path, '\\');
633 }
634 binary_name = binary_name ? binary_name + 1 : groups[g].binary_path;
635
636 // Parse results in order
637 for (int j = 0; j < groups[g].count; j++) {
638 int orig_idx = groups[g].original_indices[j];
639 char *parsed = parse_llvm_symbolizer_result(fp, buffer[orig_idx]);
640
641 if (parsed) {
642 // Prepend binary name in brackets
643 char *with_binary = SAFE_MALLOC(1024, char *);
644 if (with_binary) {
645 SAFE_SNPRINTF(with_binary, 1024, "[%s] %s", binary_name, parsed);
646 SAFE_FREE(parsed);
647 result[orig_idx] = with_binary;
648 } else {
649 result[orig_idx] = parsed;
650 }
651 } else {
652 // Fallback: show raw address with binary name
653 result[orig_idx] = SAFE_MALLOC(256, char *);
654 if (result[orig_idx]) {
655 SAFE_SNPRINTF(result[orig_idx], 256, "[%s] %p", binary_name, buffer[orig_idx]);
656 }
657 }
658 }
659
660 pclose(fp);
661 }
662
663 return result;
664}
665
672static char **run_addr2line_batch(void *const *buffer, int size) {
673 if (size <= 0 || !buffer) {
674 log_error("Invalid parameters: buffer=%p, size=%d", (void *)buffer, size);
675 return NULL;
676 }
677
678 const char *addr2line_cmd = get_addr2line_command();
679 if (!addr2line_cmd) {
680 log_debug("addr2line not available - skipping symbolization");
681 return NULL;
682 }
683
684 // Get executable path
685 char exe_path[PLATFORM_MAX_PATH_LENGTH];
686 if (!platform_get_executable_path(exe_path, sizeof(exe_path))) {
687 return NULL;
688 }
689
690 // SECURITY: Validate executable path to prevent command injection
691 // Paths from system APIs should be safe, but validate to be thorough
692 if (!validate_shell_safe(exe_path, ".-/\\:")) {
693 log_error("Invalid executable path - contains unsafe characters: %s", exe_path);
694 return NULL;
695 }
696
697 // Escape exe_path for shell (auto-detects platform and quoting needs)
698 char escaped_exe_path_buf[PLATFORM_MAX_PATH_LENGTH * 2];
699 if (!escape_path_for_shell(exe_path, escaped_exe_path_buf, sizeof(escaped_exe_path_buf))) {
700 log_error("Failed to escape executable path for shell command");
701 return NULL;
702 }
703 const char *escaped_exe_path = escaped_exe_path_buf;
704
705 if (!validate_shell_safe(addr2line_cmd, ".-/\\:_")) {
706 log_warn("addr2line path contains unsafe characters: %s", addr2line_cmd);
707 return NULL;
708 }
709
710 // Escape addr2line command for shell (auto-detects platform and quoting needs)
711 char escaped_addr2line_buf[PLATFORM_MAX_PATH_LENGTH * 2];
712 if (!escape_path_for_shell(addr2line_cmd, escaped_addr2line_buf, sizeof(escaped_addr2line_buf))) {
713 log_error("Failed to escape addr2line path for shell command");
714 return NULL;
715 }
716 const char *escaped_addr2line_cmd = escaped_addr2line_buf;
717
718 // Build addr2line command
719 char cmd[4096];
720 int offset = safe_snprintf(cmd, sizeof(cmd), "%s -e %s -f -C -i ", escaped_addr2line_cmd, escaped_exe_path);
721 if (offset <= 0 || offset >= (int)sizeof(cmd)) {
722 log_error("Failed to build addr2line command");
723 return NULL;
724 }
725
726 // Use explicit hex format with 0x prefix since Windows %p doesn't include it
727 for (int i = 0; i < size; i++) {
728 int n = safe_snprintf(cmd + offset, sizeof(cmd) - (size_t)offset, "0x%llx ", (unsigned long long)buffer[i]);
729 if (n <= 0 || offset + n >= (int)sizeof(cmd)) {
730 log_error("Failed to build addr2line command");
731 break;
732 }
733 offset += n;
734 }
735
736 // Execute addr2line
737 FILE *fp = popen(cmd, "r");
738 if (!fp) {
739 log_error("Failed to execute addr2line command");
740 return NULL;
741 }
742
743 // Allocate result array
744 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
745 if (!result) {
746 pclose(fp);
747 return NULL;
748 }
749
750 // Parse output
751 for (int i = 0; i < size; i++) {
752 char func_name[256];
753 char file_line[512];
754
755 if (fgets(func_name, sizeof(func_name), fp) == NULL) {
756 break;
757 }
758 if (fgets(file_line, sizeof(file_line), fp) == NULL) {
759 break;
760 }
761
762 // Remove newlines
763 func_name[strcspn(func_name, "\n")] = '\0';
764 file_line[strcspn(file_line, "\n")] = '\0';
765
766 // Extract relative path and COPY IT IMMEDIATELY
767 // IMPORTANT: extract_project_relative_path uses a static buffer internally.
768 // SAFE_MALLOC's debug memory tracking also calls extract_project_relative_path,
769 // which would overwrite the static buffer. So we must copy before allocating.
770 const char *rel_path_tmp = extract_project_relative_path(file_line);
771 char rel_path[512];
772 SAFE_STRNCPY(rel_path, rel_path_tmp, sizeof(rel_path));
773
774 // Allocate result buffer (AFTER copying rel_path to local storage)
775 result[i] = SAFE_MALLOC(1024, char *);
776
777 // Format symbol
778 bool has_func = (strcmp(func_name, "??") != 0);
779 bool has_file = (strcmp(file_line, "??:0") != 0 && strcmp(file_line, "??:?") != 0);
780
781 if (!has_func && !has_file) {
782 // Complete unknown - show raw address
783 SAFE_SNPRINTF(result[i], 1024, "%p", buffer[i]);
784 } else if (has_func && has_file) {
785 // Best case - both function and file:line known
786 if (strstr(rel_path, ":") != NULL) {
787 SAFE_SNPRINTF(result[i], 1024, "%s in %s()", rel_path, func_name);
788 } else {
789 SAFE_SNPRINTF(result[i], 1024, "%s() at %s", func_name, rel_path);
790 }
791 } else if (has_func) {
792 // Function known but file unknown (common for library functions)
793 SAFE_SNPRINTF(result[i], 1024, "%s() at %p", func_name, buffer[i]);
794 } else {
795 // File known but function unknown (rare)
796 SAFE_SNPRINTF(result[i], 1024, "%s (unknown function)", rel_path);
797 }
798 }
799
800 pclose(fp);
801 return result;
802}
803
804char **symbol_cache_resolve_batch(void *const *buffer, int size) {
805 if (size <= 0 || !buffer) {
806 log_error("Invalid parameters: buffer=%p, size=%d", (void *)buffer, size);
807 return NULL;
808 }
809
810 // DO NOT auto-initialize here - causes circular dependency during lock_debug_init()
811 // The cache must be initialized explicitly by platform_init() before use
812 if (!atomic_load(&g_symbol_cache_initialized)) {
813 // Cache not initialized - fall back to uncached resolution
814 // This happens during early initialization before platform_init() completes
815 char **result = run_llvm_symbolizer_batch(buffer, size);
816 if (!result) {
817 result = run_addr2line_batch(buffer, size);
818 }
819 return result;
820 }
821
822 // Allocate result array (size + 1 for NULL terminator)
823 // CALLOC zeros the memory, so result[size] is already NULL
824 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
825 if (!result) {
826 return NULL;
827 }
828
829 // Ensure NULL terminator is explicitly set (CALLOC already did this, but be explicit)
830 result[size] = NULL;
831
832 // First pass: check cache for all addresses
833 int uncached_count = 0;
834 void *uncached_addrs[size];
835 int uncached_indices[size];
836
837 for (int i = 0; i < size; i++) {
838 const char *cached = symbol_cache_lookup(buffer[i]);
839 if (cached) {
840 // Cache hit - symbol_cache_lookup already duplicated the string
841 result[i] = (char *)cached;
842 } else {
843 // Cache miss - track for batch resolution
844 uncached_addrs[uncached_count] = buffer[i];
845 uncached_indices[uncached_count] = i;
846 uncached_count++;
847 }
848 }
849
850 // Second pass: resolve uncached addresses with selected symbolizer
851 if (uncached_count > 0) {
852 char **resolved = NULL;
853
854 // Use the detected symbolizer type
855 switch (g_symbolizer_type) {
856 case SYMBOLIZER_LLVM:
857 resolved = run_llvm_symbolizer_batch(uncached_addrs, uncached_count);
858 break;
860 resolved = run_addr2line_batch(uncached_addrs, uncached_count);
861 break;
862 case SYMBOLIZER_NONE:
863 default:
864 // No symbolizer available - will fall through to raw address handling
865 resolved = NULL;
866 break;
867 }
868
869 if (resolved) {
870 for (int i = 0; i < uncached_count; i++) {
871 int orig_idx = uncached_indices[i];
872 if (resolved[i]) {
873 result[orig_idx] = platform_strdup(resolved[i]);
874 // If strdup failed, use sentinel string instead of NULL
875 if (!result[orig_idx]) {
876 log_error("Failed to duplicate string for result[%d]", orig_idx);
877 result[orig_idx] = platform_strdup(NULL_SENTINEL);
878 }
879 // Only insert into cache if strdup succeeded (and it's not the sentinel)
880 if (result[orig_idx] && strcmp(result[orig_idx], NULL_SENTINEL) != 0) {
881 if (!symbol_cache_insert(uncached_addrs[i], resolved[i])) {
882 log_error("Failed to insert symbol into cache for result[%d]", orig_idx);
883 }
884 }
885 SAFE_FREE(resolved[i]);
886 } else {
887 // resolved[i] is NULL - use sentinel string
888 if (!result[orig_idx]) {
889 result[orig_idx] = platform_strdup(NULL_SENTINEL);
890 }
891 }
892 if (!result[orig_idx]) {
893 log_error("Failed to allocate memory for result[%d]", orig_idx);
894 }
895 }
896
897 SAFE_FREE(resolved);
898
899 } else {
900 // addr2line failed - fill uncached entries with raw addresses or sentinel
901 for (int i = 0; i < uncached_count; i++) {
902 int orig_idx = uncached_indices[i];
903 if (!result[orig_idx]) {
904 result[orig_idx] = SAFE_MALLOC(32, char *);
905 if (result[orig_idx]) {
906 SAFE_SNPRINTF(result[orig_idx], 32, "%p", uncached_addrs[i]);
907 } else {
908 result[orig_idx] = platform_strdup(NULL_SENTINEL);
909 }
910 }
911 if (!result[orig_idx]) {
912 log_error("Failed to allocate memory for result[%d]", orig_idx);
913 }
914 }
915 }
916 }
917
918 return result;
919}
920
921void symbol_cache_free_symbols(char **symbols) {
922 if (!symbols) {
923 return;
924 }
925
926 // The array is NULL-terminated (allocated with size+1, with result[size] = NULL)
927 // The terminator is a SINGLE NULL at index 'size'
928 // Failed allocations use the NULL_SENTINEL string "[NULL]" instead of NULL,
929 // so there are no NULL entries in the middle - only at the terminator
930 // This makes iteration safe: we can iterate until we find the first NULL (the terminator)
931
932 // Iterate through entries, freeing all non-NULL entries until we hit the NULL terminator
933 for (int i = 0; i < 64; i++) { // Reasonable limit to prevent infinite loop
934 if (symbols[i] == NULL) {
935 // Found NULL - this is the terminator, stop here
936 break;
937 }
938
939 // Found a non-NULL entry - check if it's the sentinel string
940 // Both regular strings and sentinel strings are allocated (with strdup),
941 // so we free them all
942 SAFE_FREE(symbols[i]);
943 symbols[i] = NULL; // Clear pointer after freeing
944 }
945
946 // Free the array itself
947 SAFE_FREE(symbols);
948}
int get_binary_file_address_offsets(const void *addr, platform_binary_match_t *matches, int max_matches)
Get binary that contains address on Linux via /proc/self/maps.
const char * extract_project_relative_path(const char *file)
Definition path.c:410
char * platform_strdup(const char *s)
Symbol cache entry structure for address-to-symbol mapping.
Definition symbols.c:105
UT_hash_handle hh
uthash handle (required for hash table operations)
Definition symbols.c:111
char * symbol
Resolved symbol string (allocated, owned by cache)
Definition symbols.c:109
void * addr
Memory address key (used for hashtable lookup)
Definition symbols.c:107
#define LLVM_SYMBOLIZER_BIN
Definition symbols.c:14
void symbol_cache_get_stats(uint64_t *hits_out, uint64_t *misses_out, size_t *entries_out)
Definition symbols.c:412
#define NULL_SENTINEL
Definition symbols.c:43
bool symbol_cache_insert(void *addr, const char *symbol)
Definition symbols.c:354
#define ADDR2LINE_BIN
Definition symbols.c:15
void symbol_cache_print_stats(void)
Definition symbols.c:426
char ** symbol_cache_resolve_batch(void *const *buffer, int size)
Definition symbols.c:804
symbolizer_type_t
Definition symbols.c:49
@ SYMBOLIZER_NONE
Definition symbols.c:50
@ SYMBOLIZER_LLVM
Definition symbols.c:51
@ SYMBOLIZER_ADDR2LINE
Definition symbols.c:52
const char * symbol_cache_lookup(void *addr)
Definition symbols.c:331
asciichat_error_t symbol_cache_init(void)
Definition symbols.c:264
void symbol_cache_destroy(void)
Definition symbols.c:292
void symbol_cache_free_symbols(char **symbols)
Definition symbols.c:921
bool platform_get_executable_path(char *exe_path, size_t path_size)
Get the path to the current executable.
Definition system.c:393
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:456
bool platform_is_binary_in_path(const char *bin_name)
Definition system.c:331
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:64
int rwlock_init(rwlock_t *rwlock)
Definition threading.c:63
bool validate_shell_safe(const char *str, const char *allowed_chars)
Definition util/string.c:59
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)