ascii-chat 0.11.33
Video chat in your terminal
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 <ascii-chat/atomic.h>
22
23#ifndef _WIN32
24#include <unistd.h>
25#else
26#include <windows.h>
27#endif
28
32#include <ascii-chat/common.h>
33#include <ascii-chat/uthash.h>
39#include <ascii-chat/log/log.h>
40
41// ============================================================================
42// Constants
43// ============================================================================
44
45// Sentinel string for failed allocations (replaces NULL in middle of array)
46#define NULL_SENTINEL "[NULL]"
47
48// ============================================================================
49// Symbolizer Type Selection
50// ============================================================================
51
52typedef enum {
53 SYMBOLIZER_NONE = 0, // No symbolizer available, use raw addresses
54 SYMBOLIZER_LLVM = 1, // llvm-symbolizer (preferred on all platforms)
55 SYMBOLIZER_ADDR2LINE = 2, // addr2line (fallback)
57
58static symbolizer_type_t g_symbolizer_type = SYMBOLIZER_NONE;
59static atomic_t g_symbolizer_detected = {0};
60static atomic_t g_llvm_symbolizer_available = {0};
61static char g_llvm_symbolizer_cmd[PLATFORM_MAX_PATH_LENGTH];
62static atomic_t g_addr2line_available = {0};
63static char g_addr2line_cmd[PLATFORM_MAX_PATH_LENGTH];
64// Lifecycle for symbolizer detection (replaces double-checked locking with mutex)
65static lifecycle_t g_llvm_symbolizer_lc = LIFECYCLE_INIT;
66static lifecycle_t g_addr2line_lc = LIFECYCLE_INIT;
67
68// ============================================================================
69// Cache State
70// ============================================================================
71
108typedef struct {
110 void *addr;
112 char *symbol;
114 UT_hash_handle hh;
116
117static symbol_entry_t *g_symbol_cache = NULL; // uthash uses structure pointer as head
118static rwlock_t g_symbol_cache_lock = {0}; // External locking for thread safety
119static lifecycle_t g_symbol_cache_lc = LIFECYCLE_INIT;
120
121// Statistics
122static atomic_t g_cache_hits = {0};
123static atomic_t g_cache_misses = {0};
124
125// ============================================================================
126// Helper Functions
127// ============================================================================
128
133static bool symbolizer_path_is_executable(const char *path) {
134#ifdef _WIN32
135 if (!path || path[0] == '\0') {
136 return false;
137 }
138
139 DWORD attrs = GetFileAttributesA(path);
140 if (attrs == INVALID_FILE_ATTRIBUTES) {
141 return false;
142 }
143 if (attrs & FILE_ATTRIBUTE_DIRECTORY) {
144 return false;
145 }
146
147 HANDLE handle = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
148 FILE_ATTRIBUTE_NORMAL, NULL);
149 if (handle == INVALID_HANDLE_VALUE) {
150 return false;
151 }
152
153 CloseHandle(handle);
154 return true;
155#else
156 return (path && path[0] != '\0' && access(path, X_OK) == 0);
157#endif
158}
159
160static const char *get_llvm_symbolizer_command(void) {
161 // Check if already initialized
162 if (lifecycle_is_initialized(&g_llvm_symbolizer_lc)) {
163 goto check_available;
164 }
165
166 // Try to win the initialization race
167 if (!lifecycle_init(&g_llvm_symbolizer_lc, "llvm_symbolizer")) {
168 // Lost race - wait for winner to complete
169 while (!lifecycle_is_initialized(&g_llvm_symbolizer_lc)) {
170 // Spin-wait
171 }
172 goto check_available;
173 }
174
175 // Winner: do the initialization
176 {
177 const char *env_path = SAFE_GETENV("LLVM_SYMBOLIZER_PATH");
178 bool available = false;
179
180 g_llvm_symbolizer_cmd[0] = '\0';
181
182 if (env_path && env_path[0] != '\0') {
183 if (symbolizer_path_is_executable(env_path)) {
184 SAFE_STRNCPY(g_llvm_symbolizer_cmd, env_path, sizeof(g_llvm_symbolizer_cmd));
185 available = true;
186 log_dev("Using llvm-symbolizer from LLVM_SYMBOLIZER_PATH: %s", env_path);
187 } else {
188 log_warn("LLVM_SYMBOLIZER_PATH is set but not executable: %s", env_path);
189 }
190 }
191
193 available = true;
194 g_llvm_symbolizer_cmd[0] = '\0'; // Use binary name from PATH
195 log_dev("Found %s in PATH", LLVM_SYMBOLIZER_BIN);
196 }
197
198 atomic_store_bool(&g_llvm_symbolizer_available, available);
199 }
200
201check_available:
202
203 if (!atomic_load_bool(&g_llvm_symbolizer_available)) {
204 return NULL;
205 }
206
207 if (g_llvm_symbolizer_cmd[0] != '\0') {
208 return g_llvm_symbolizer_cmd;
209 }
210
211 return LLVM_SYMBOLIZER_BIN;
212}
213
214static const char *get_addr2line_command(void) {
215 // Check if already initialized
216 if (lifecycle_is_initialized(&g_addr2line_lc)) {
217 goto check_available;
218 }
219
220 // Try to win the initialization race
221 if (!lifecycle_init(&g_addr2line_lc, "addr2line")) {
222 // Lost race - wait for winner to complete
223 while (!lifecycle_is_initialized(&g_addr2line_lc)) {
224 // Spin-wait
225 }
226 goto check_available;
227 }
228
229 // Winner: do the initialization
230 {
231 const char *env_path = SAFE_GETENV("ADDR2LINE_PATH");
232 bool available = false;
233
234 g_addr2line_cmd[0] = '\0';
235
236 if (env_path && env_path[0] != '\0') {
237 if (symbolizer_path_is_executable(env_path)) {
238 SAFE_STRNCPY(g_addr2line_cmd, env_path, sizeof(g_addr2line_cmd));
239 available = true;
240 log_dev("Using addr2line from ADDR2LINE_PATH: %s", env_path);
241 } else {
242 log_warn("ADDR2LINE_PATH is set but not executable: %s", env_path);
243 }
244 }
245
246 if (!available && platform_is_binary_in_path(ADDR2LINE_BIN)) {
247 available = true;
248 g_addr2line_cmd[0] = '\0'; // Use binary name from PATH
249 log_dev("Found %s in PATH", ADDR2LINE_BIN);
250 }
251
252 atomic_store_bool(&g_addr2line_available, available);
253 }
254
255check_available:
256
257 if (!atomic_load_bool(&g_addr2line_available)) {
258 return NULL;
259 }
260
261 if (g_addr2line_cmd[0] != '\0') {
262 return g_addr2line_cmd;
263 }
264
265 return ADDR2LINE_BIN;
266}
267
268static symbolizer_type_t detect_symbolizer(void) {
269 // Prefer llvm-symbolizer on all platforms (including macOS)
270 // We handle ASLR ourselves using dyld APIs on macOS
271 const char *llvm_symbolizer = get_llvm_symbolizer_command();
272 if (llvm_symbolizer) {
273 log_dev("Using llvm-symbolizer for symbol resolution");
274 return SYMBOLIZER_LLVM;
275 }
276
277 const char *addr2line_cmd = get_addr2line_command();
278 if (addr2line_cmd) {
279 log_debug("Using addr2line command: %s", addr2line_cmd);
281 }
282
283 log_warn("No symbolizer found in PATH (tried %s, %s) - using native backend", LLVM_SYMBOLIZER_BIN, ADDR2LINE_BIN);
284 return SYMBOLIZER_NONE;
285}
286
287// ============================================================================
288// Public API Implementation
289// ============================================================================
290
292 if (!lifecycle_init(&g_symbol_cache_lc, "symbol_cache")) {
293 return 0; // Already initialized
294 }
295
296 // NOTE: Symbolizer detection is deferred to lazy initialization (symbol_cache_resolve_batch)
297 // to avoid large stack allocations during early initialization in musl static-pie builds.
298 // This prevents __stack_chk_fail() in check_binary_in_path_uncached() which allocates ~4.6KB on stack.
299
300 // Initialize rwlock for thread safety (uthash requires external locking)
301 if (rwlock_init(&g_symbol_cache_lock, "symbol_cache") != 0) {
302 lifecycle_init_abort(&g_symbol_cache_lc);
303 return SET_ERRNO(ERROR_THREAD, "Failed to initialize symbol cache rwlock");
304 }
305
306 // Initialize uthash head to NULL (required)
307 g_symbol_cache = NULL;
308
309 atomic_store_u64(&g_cache_hits, 0);
310 atomic_store_u64(&g_cache_misses, 0);
311
312 log_dev("Symbol cache initialized");
313 return 0;
314}
315
317 if (!lifecycle_shutdown(&g_symbol_cache_lc)) {
318 return;
319 }
320
321 // Mark as uninitialized FIRST to prevent new inserts during cleanup
322
323 // Acquire write lock to prevent any concurrent operations
324 rwlock_wrlock(&g_symbol_cache_lock);
325
326 // Count entries before freeing for debugging
327 size_t entry_count = HASH_COUNT(g_symbol_cache);
328
329 // Free all symbol entries using HASH_ITER
330 symbol_entry_t *entry = NULL, *tmp = NULL;
331 size_t freed_count = 0;
332 HASH_ITER(hh, g_symbol_cache, entry, tmp) {
333 if (entry) {
334 HASH_DEL(g_symbol_cache, entry);
335 if (entry->symbol) {
336 free(entry->symbol);
337 }
338 free(entry);
339 freed_count++;
340 }
341 }
342
343 // Release lock and destroy rwlock
344 rwlock_wrunlock(&g_symbol_cache_lock);
345 rwlock_destroy(&g_symbol_cache_lock);
346
347 g_symbol_cache = NULL;
348
349 log_dev("Symbol cache cleaned up: %zu entries counted, %zu entries freed (hits=%llu, misses=%llu)", entry_count,
350 freed_count, (unsigned long long)atomic_load_u64(&g_cache_hits),
351 (unsigned long long)atomic_load_u64(&g_cache_misses));
352}
353
354const char *symbol_cache_lookup(void *addr) {
355 if (!lifecycle_is_initialized(&g_symbol_cache_lc) || !addr) {
356 return NULL;
357 }
358
359 rwlock_rdlock(&g_symbol_cache_lock);
360
361 symbol_entry_t *entry = NULL;
362 HASH_FIND_PTR(g_symbol_cache, &addr, entry);
363
364 const char *result = NULL;
365 if (entry) {
366 // Copy string while holding lock to prevent use-after-free
367 result = platform_strdup(entry->symbol);
368 atomic_fetch_add_u64(&g_cache_hits, 1);
369 } else {
370 atomic_fetch_add_u64(&g_cache_misses, 1);
371 }
372
373 rwlock_rdunlock(&g_symbol_cache_lock);
374 return result;
375}
376
377bool symbol_cache_insert(void *addr, const char *symbol) {
378 if (!lifecycle_is_initialized(&g_symbol_cache_lc) || !addr || !symbol) {
379 return false;
380 }
381
382 // Acquire write lock to make the entire operation atomic
383 rwlock_wrlock(&g_symbol_cache_lock);
384
385 // Double-check cache is still initialized after acquiring lock
386 // (cleanup might have marked it uninitialized between our check and lock acquisition)
387 if (!lifecycle_is_initialized(&g_symbol_cache_lc)) {
388 rwlock_wrunlock(&g_symbol_cache_lock);
389 return false;
390 }
391
392 // Check if entry already exists
393 symbol_entry_t *existing = NULL;
394 HASH_FIND_PTR(g_symbol_cache, &addr, existing);
395
396 if (existing) {
397 // Entry exists - update symbol if different
398 if (existing->symbol && strcmp(existing->symbol, symbol) != 0) {
399 // Free old symbol and allocate new one
400 free(existing->symbol);
401 existing->symbol = platform_strdup(symbol);
402 if (!existing->symbol) {
403 rwlock_wrunlock(&g_symbol_cache_lock);
404 return false;
405 }
406 }
407 rwlock_wrunlock(&g_symbol_cache_lock);
408 return true;
409 }
410
411 // Create new entry
412 symbol_entry_t *entry = (symbol_entry_t *)malloc(sizeof(symbol_entry_t));
413 if (!entry) {
414 rwlock_wrunlock(&g_symbol_cache_lock);
415 return false;
416 }
417
418 entry->addr = addr;
419 entry->symbol = platform_strdup(symbol);
420 if (!entry->symbol) {
421 free(entry);
422 rwlock_wrunlock(&g_symbol_cache_lock);
423 return false;
424 }
425
426 // Add to hash table
427 HASH_ADD_PTR(g_symbol_cache, addr, entry);
428
429 // Release lock
430 rwlock_wrunlock(&g_symbol_cache_lock);
431
432 return true;
433}
434
435void symbol_cache_get_stats(uint64_t *hits_out, uint64_t *misses_out, size_t *entries_out) {
436 if (hits_out) {
437 *hits_out = atomic_load_u64(&g_cache_hits);
438 }
439 if (misses_out) {
440 *misses_out = atomic_load_u64(&g_cache_misses);
441 }
442 if (entries_out) {
443 rwlock_rdlock(&g_symbol_cache_lock);
444 *entries_out = HASH_COUNT(g_symbol_cache);
445 rwlock_rdunlock(&g_symbol_cache_lock);
446 }
447}
448
450 uint64_t hits = atomic_load_u64(&g_cache_hits);
451 uint64_t misses = atomic_load_u64(&g_cache_misses);
452
453 rwlock_rdlock(&g_symbol_cache_lock);
454 size_t entries = HASH_COUNT(g_symbol_cache);
455 rwlock_rdunlock(&g_symbol_cache_lock);
456
457 uint64_t total = hits + misses;
458 double hit_rate = total > 0 ? (100.0 * (double)hits / (double)total) : 0.0;
459
460 log_debug("Symbol Cache Stats: %zu entries, %llu hits, %llu misses (%.1f%% hit rate)", entries,
461 (unsigned long long)hits, (unsigned long long)misses, hit_rate);
462}
463
464// ============================================================================
465// Batch Resolution with llvm-symbolizer and addr2line
466// ============================================================================
467
474static char *parse_llvm_symbolizer_result(FILE *fp, void *addr) {
475 char func_name[512] = "??";
476 char file_location[512] = "??:0";
477 char blank_line[8];
478
479 // Read function name line
480 if (fgets(func_name, sizeof(func_name), fp) == NULL) {
481 return NULL;
482 }
483 func_name[strcspn(func_name, "\n")] = '\0';
484
485 // Read file location line
486 if (fgets(file_location, sizeof(file_location), fp) == NULL) {
487 return NULL;
488 }
489 file_location[strcspn(file_location, "\n")] = '\0';
490
491 // Read blank separator line (and discard it)
492 if (fgets(blank_line, sizeof(blank_line), fp) == NULL) {
493 // End of output - not an error
494 }
495
496 // Remove column number (last :N) from file_location
497 char *last_colon = strrchr(file_location, ':');
498 if (last_colon) {
499 *last_colon = '\0';
500 }
501
502 // Extract relative path and COPY IT IMMEDIATELY
503 // IMPORTANT: extract_project_relative_path uses a static buffer internally.
504 // SAFE_MALLOC's debug memory tracking also calls extract_project_relative_path,
505 // which would overwrite the static buffer. So we must copy before allocating.
506 const char *rel_path_tmp = extract_project_relative_path(file_location);
507 char rel_path[512];
508 SAFE_STRNCPY(rel_path, rel_path_tmp, sizeof(rel_path));
509
510 // Allocate result buffer (AFTER copying rel_path to local storage)
511 char *result = SAFE_MALLOC(1024, char *);
512 if (!result) {
513 return NULL;
514 }
515
516 // Format symbol
517 bool has_func = (strcmp(func_name, "??") != 0 && strlen(func_name) > 0);
518 bool has_file =
519 (strcmp(file_location, "??:0") != 0 && strcmp(file_location, "??:?") != 0 && strcmp(file_location, "??") != 0);
520
521 // Remove () from function name if present (llvm-symbolizer includes them)
522 char clean_func[512];
523 SAFE_STRNCPY(clean_func, func_name, sizeof(clean_func));
524 char *paren = strstr(clean_func, "()");
525 if (paren) {
526 *paren = '\0';
527 }
528
529 if (!has_func && !has_file) {
530 SAFE_SNPRINTF(result, 1024, "%p", addr);
531 } else if (has_func && has_file) {
532 SAFE_SNPRINTF(result, 1024, "%s() (%s)", clean_func, rel_path);
533 } else if (has_func) {
534 SAFE_SNPRINTF(result, 1024, "%s()", clean_func);
535 } else {
536 SAFE_SNPRINTF(result, 1024, "%s (unknown function)", rel_path);
537 }
538
539 return result;
540}
541
551static char **run_llvm_symbolizer_batch(void *const *buffer, int size) {
552 if (size <= 0 || !buffer) {
553 return NULL;
554 }
555
556 const char *symbolizer_cmd = get_llvm_symbolizer_command();
557 if (!symbolizer_cmd) {
558 log_debug("llvm-symbolizer not available - skipping symbolization");
559 return NULL;
560 }
561
562 // Allocate result array
563 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
564 if (!result) {
565 return NULL;
566 }
567
568 // ============================================================================
569 // Unified platform-agnostic implementation using get_binary_file_address_offsets()
570 // ============================================================================
571
572 // Group addresses by binary using platform-independent interface
573 // Works identically on Linux, macOS, and Windows
574 typedef struct {
575 char binary_path[PLATFORM_MAX_PATH_LENGTH];
576 uintptr_t file_offsets[64]; // Max addresses per binary
577 int original_indices[64];
578 int count;
579 } binary_group_t;
580
581 binary_group_t groups[8]; // Support up to 8 different binaries
582 int num_groups = 0;
583
584 // First pass: Call get_binary_file_address_offsets() for each address
585 // (same function works on all platforms - zero #ifdefs needed!)
586 for (int i = 0; i < size; i++) {
588 if (get_binary_file_address_offsets(buffer[i], &match, 1) == 0) {
589 // Could not find binary - use raw address as fallback
590 result[i] = SAFE_MALLOC(32, char *);
591 if (result[i]) {
592 SAFE_SNPRINTF(result[i], 32, "%p", buffer[i]);
593 }
594 continue;
595 }
596
597 // Find or create group for this binary
598 int group_idx = -1;
599 for (int g = 0; g < num_groups; g++) {
600 if (strcmp(groups[g].binary_path, match.path) == 0) {
601 group_idx = g;
602 break;
603 }
604 }
605
606 if (group_idx < 0 && num_groups < 8) {
607 group_idx = num_groups++;
608 SAFE_STRNCPY(groups[group_idx].binary_path, match.path, sizeof(groups[group_idx].binary_path));
609 groups[group_idx].count = 0;
610 }
611
612 if (group_idx >= 0 && groups[group_idx].count < 64) {
613 int idx = groups[group_idx].count++;
614 groups[group_idx].file_offsets[idx] = match.file_offset;
615 groups[group_idx].original_indices[idx] = i;
616 }
617 }
618
619 // Second pass: batch symbolize each group
620 for (int g = 0; g < num_groups; g++) {
621 if (groups[g].count == 0) {
622 continue;
623 }
624
625 // Escape binary path for shell
626 char escaped_binary_path[PLATFORM_MAX_PATH_LENGTH * 2];
627 if (!escape_path_for_shell(groups[g].binary_path, escaped_binary_path, sizeof(escaped_binary_path))) {
628 // On error, fill with fallback addresses
629 for (int j = 0; j < groups[g].count; j++) {
630 int orig_idx = groups[g].original_indices[j];
631 if (!result[orig_idx]) {
632 result[orig_idx] = SAFE_MALLOC(32, char *);
633 if (result[orig_idx]) {
634 SAFE_SNPRINTF(result[orig_idx], 32, "%p", buffer[orig_idx]);
635 }
636 }
637 }
638 continue;
639 }
640
641 // Build command with all addresses for this binary
642 char cmd[8192];
643 int offset = safe_snprintf(cmd, sizeof(cmd), "%s --demangle --output-style=LLVM --relativenames -e %s ",
644 symbolizer_cmd, escaped_binary_path);
645
646 for (int j = 0; j < groups[g].count && offset < (int)sizeof(cmd) - 32; j++) {
647 int n =
648 safe_snprintf(cmd + offset, sizeof(cmd) - (size_t)offset, "0x%lx ", (unsigned long)groups[g].file_offsets[j]);
649 if (n > 0) {
650 offset += n;
651 }
652 }
653
654 // Suppress stderr
655 safe_snprintf(cmd + offset, sizeof(cmd) - (size_t)offset, "2>/dev/null");
656
657 // Note: No LOG_IO here because output is read and consumed by this function
658 FILE *fp = NULL;
659 if (platform_popen("llvm-symbolizer", cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
660 // popen() failed - fill with fallback addresses instead of leaving NULL
661 for (int j = 0; j < groups[g].count; j++) {
662 int orig_idx = groups[g].original_indices[j];
663 if (!result[orig_idx]) {
664 result[orig_idx] = SAFE_MALLOC(32, char *);
665 if (result[orig_idx]) {
666 SAFE_SNPRINTF(result[orig_idx], 32, "%p", buffer[orig_idx]);
667 }
668 }
669 }
670 continue;
671 }
672
673 // Extract binary name (basename) for display
674 const char *binary_name = strrchr(groups[g].binary_path, '/');
675 if (!binary_name) {
676 binary_name = strrchr(groups[g].binary_path, '\\');
677 }
678 binary_name = binary_name ? binary_name + 1 : groups[g].binary_path;
679
680 // Parse results in order
681 for (int j = 0; j < groups[g].count; j++) {
682 int orig_idx = groups[g].original_indices[j];
683 char *parsed = parse_llvm_symbolizer_result(fp, buffer[orig_idx]);
684
685 if (parsed) {
686 // Prepend binary name in brackets
687 char *with_binary = SAFE_MALLOC(1024, char *);
688 if (with_binary) {
689 SAFE_SNPRINTF(with_binary, 1024, "[%s] %s", binary_name, parsed);
690 SAFE_FREE(parsed);
691 result[orig_idx] = with_binary;
692 } else {
693 result[orig_idx] = parsed;
694 }
695 } else {
696 // Fallback: show raw address with binary name
697 result[orig_idx] = SAFE_MALLOC(256, char *);
698 if (result[orig_idx]) {
699 SAFE_SNPRINTF(result[orig_idx], 256, "[%s] %p", binary_name, buffer[orig_idx]);
700 }
701 }
702 }
703
704 platform_pclose(&fp);
705 }
706
707 return result;
708}
709
716static char **run_addr2line_batch(void *const *buffer, int size) {
717 if (size <= 0 || !buffer) {
718 log_error("Invalid parameters: buffer=%p, size=%d", (void *)buffer, size);
719 return NULL;
720 }
721
722 const char *addr2line_cmd = get_addr2line_command();
723 if (!addr2line_cmd) {
724 log_debug("addr2line not available - skipping symbolization");
725 return NULL;
726 }
727
728 // Get executable path
729 char exe_path[PLATFORM_MAX_PATH_LENGTH];
730 if (!platform_get_executable_path(exe_path, sizeof(exe_path))) {
731 return NULL;
732 }
733
734 // SECURITY: Validate executable path to prevent command injection
735 // Paths from system APIs should be safe, but validate to be thorough
736 if (!validate_shell_safe(exe_path, ".-/\\:")) {
737 log_error("Invalid executable path - contains unsafe characters: %s", exe_path);
738 return NULL;
739 }
740
741 // Escape exe_path for shell (auto-detects platform and quoting needs)
742 char escaped_exe_path_buf[PLATFORM_MAX_PATH_LENGTH * 2];
743 if (!escape_path_for_shell(exe_path, escaped_exe_path_buf, sizeof(escaped_exe_path_buf))) {
744 log_error("Failed to escape executable path for shell command");
745 return NULL;
746 }
747 const char *escaped_exe_path = escaped_exe_path_buf;
748
749 if (!validate_shell_safe(addr2line_cmd, ".-/\\:_")) {
750 log_warn("addr2line path contains unsafe characters: %s", addr2line_cmd);
751 return NULL;
752 }
753
754 // Escape addr2line command for shell (auto-detects platform and quoting needs)
755 char escaped_addr2line_buf[PLATFORM_MAX_PATH_LENGTH * 2];
756 if (!escape_path_for_shell(addr2line_cmd, escaped_addr2line_buf, sizeof(escaped_addr2line_buf))) {
757 log_error("Failed to escape addr2line path for shell command");
758 return NULL;
759 }
760 const char *escaped_addr2line_cmd = escaped_addr2line_buf;
761
762 // Build addr2line command
763 char cmd[4096];
764 int offset = safe_snprintf(cmd, sizeof(cmd), "%s -e %s -f -C -i ", escaped_addr2line_cmd, escaped_exe_path);
765 if (offset <= 0 || offset >= (int)sizeof(cmd)) {
766 log_error("Failed to build addr2line command");
767 return NULL;
768 }
769
770 // Use explicit hex format with 0x prefix since Windows %p doesn't include it
771 for (int i = 0; i < size; i++) {
772 int n = safe_snprintf(cmd + offset, sizeof(cmd) - (size_t)offset, "0x%llx ", (unsigned long long)buffer[i]);
773 if (n <= 0 || offset + n >= (int)sizeof(cmd)) {
774 log_error("Failed to build addr2line command");
775 break;
776 }
777 offset += n;
778 }
779
780 // Allocate result array BEFORE executing addr2line (to avoid leak on popen failure)
781 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
782 if (!result) {
783 return NULL;
784 }
785
786 // Execute addr2line
787 // Note: No LOG_IO here because output is read and consumed by this function
788 FILE *fp = NULL;
789 if (platform_popen("addr2line", cmd, "r", &fp) != ASCIICHAT_OK || !fp) {
790 log_error("Failed to execute addr2line command");
791 // Fill result array with fallback values instead of leaving NULL entries
792 for (int i = 0; i < size; i++) {
793 result[i] = SAFE_MALLOC(32, char *);
794 if (result[i]) {
795 SAFE_SNPRINTF(result[i], 32, "%p", buffer[i]);
796 }
797 }
798 result[size] = NULL;
799 return result;
800 }
801
802 // Parse output
803 for (int i = 0; i < size; i++) {
804 char func_name[256];
805 char file_line[512];
806
807 if (fgets(func_name, sizeof(func_name), fp) == NULL) {
808 break;
809 }
810 if (fgets(file_line, sizeof(file_line), fp) == NULL) {
811 break;
812 }
813
814 // Remove newlines
815 func_name[strcspn(func_name, "\n")] = '\0';
816 file_line[strcspn(file_line, "\n")] = '\0';
817
818 // Extract relative path and COPY IT IMMEDIATELY
819 // IMPORTANT: extract_project_relative_path uses a static buffer internally.
820 // SAFE_MALLOC's debug memory tracking also calls extract_project_relative_path,
821 // which would overwrite the static buffer. So we must copy before allocating.
822 const char *rel_path_tmp = extract_project_relative_path(file_line);
823 char rel_path[512];
824 SAFE_STRNCPY(rel_path, rel_path_tmp, sizeof(rel_path));
825
826 // Allocate result buffer (AFTER copying rel_path to local storage)
827 result[i] = SAFE_MALLOC(1024, char *);
828
829 // Format symbol
830 bool has_func = (strcmp(func_name, "??") != 0);
831 bool has_file = (strcmp(file_line, "??:0") != 0 && strcmp(file_line, "??:?") != 0);
832
833 if (!has_func && !has_file) {
834 // Complete unknown - show raw address
835 SAFE_SNPRINTF(result[i], 1024, "%p", buffer[i]);
836 } else if (has_func && has_file) {
837 // Best case - both function and file:line known
838 if (strstr(rel_path, ":") != NULL) {
839 SAFE_SNPRINTF(result[i], 1024, "%s in %s()", rel_path, func_name);
840 } else {
841 SAFE_SNPRINTF(result[i], 1024, "%s() at %s", func_name, rel_path);
842 }
843 } else if (has_func) {
844 // Function known but file unknown (common for library functions)
845 SAFE_SNPRINTF(result[i], 1024, "%s() at %p", func_name, buffer[i]);
846 } else {
847 // File known but function unknown (rare)
848 SAFE_SNPRINTF(result[i], 1024, "%s (unknown function)", rel_path);
849 }
850 }
851
852 platform_pclose(&fp);
853 return result;
854}
855
856char **symbol_cache_resolve_batch(void *const *buffer, int size) {
857 if (size <= 0 || !buffer) {
858 log_error("Invalid parameters: buffer=%p, size=%d", (void *)buffer, size);
859 return NULL;
860 }
861
862 // DO NOT auto-initialize here - causes circular dependency during lock_debug_init()
863 // The cache must be initialized explicitly by platform_init() before use
864 if (!lifecycle_is_initialized(&g_symbol_cache_lc)) {
865 // Cache not initialized - fall back to uncached resolution
866 // This happens during early initialization before platform_init() completes
867 char **result = run_llvm_symbolizer_batch(buffer, size);
868 if (!result) {
869 result = run_addr2line_batch(buffer, size);
870 }
871 return result;
872 }
873
874 // Lazy initialize symbolizer detection on first use (not during symbol_cache_init)
875 // This avoids large stack allocations in musl static-pie during early initialization
876 bool expected = false;
877 if (atomic_cas_bool(&g_symbolizer_detected, &expected, true)) {
878 g_symbolizer_type = detect_symbolizer();
879 }
880
881 // Allocate result array (size + 1 for NULL terminator)
882 // CALLOC zeros the memory, so result[size] is already NULL
883 char **result = SAFE_CALLOC((size_t)(size + 1), sizeof(char *), char **);
884 if (!result) {
885 return NULL;
886 }
887
888 // Ensure NULL terminator is explicitly set (CALLOC already did this, but be explicit)
889 result[size] = NULL;
890
891 // First pass: check cache for all addresses
892 int uncached_count = 0;
893
894 // Allocate arrays for uncached addresses on heap instead of stack
895 // VLAs can cause stack overflow with large backtrace sizes
896 void **uncached_addrs = SAFE_MALLOC((size_t)size * sizeof(void *), void **);
897 int *uncached_indices = SAFE_MALLOC((size_t)size * sizeof(int), int *);
898
899 if (!uncached_addrs || !uncached_indices) {
900 SAFE_FREE(uncached_addrs);
901 SAFE_FREE(uncached_indices);
902 // Return result array with raw addresses as fallback
903 for (int i = 0; i < size; i++) {
904 result[i] = SAFE_MALLOC(32, char *);
905 if (result[i]) {
906 SAFE_SNPRINTF(result[i], 32, "%p", buffer[i]);
907 }
908 }
909 return result;
910 }
911
912 for (int i = 0; i < size; i++) {
913 const char *cached = symbol_cache_lookup(buffer[i]);
914 if (cached) {
915 // Cache hit - symbol_cache_lookup already duplicated the string
916 result[i] = (char *)cached;
917 } else {
918 // Cache miss - track for batch resolution
919 uncached_addrs[uncached_count] = buffer[i];
920 uncached_indices[uncached_count] = i;
921 uncached_count++;
922 }
923 }
924
925 // Second pass: resolve uncached addresses with selected symbolizer
926 if (uncached_count > 0) {
927 char **resolved = NULL;
928
929 // Use the detected symbolizer type
930 switch (g_symbolizer_type) {
931 case SYMBOLIZER_LLVM:
932 resolved = run_llvm_symbolizer_batch(uncached_addrs, uncached_count);
933 break;
935 resolved = run_addr2line_batch(uncached_addrs, uncached_count);
936 break;
937 case SYMBOLIZER_NONE:
938 default:
939 // No symbolizer available - will fall through to raw address handling
940 resolved = NULL;
941 break;
942 }
943
944 if (resolved) {
945 for (int i = 0; i < uncached_count; i++) {
946 int orig_idx = uncached_indices[i];
947 if (resolved[i]) {
948 result[orig_idx] = platform_strdup(resolved[i]);
949 // If strdup failed, use sentinel string instead of NULL
950 if (!result[orig_idx]) {
951 log_error("Failed to duplicate string for result[%d]", orig_idx);
952 result[orig_idx] = platform_strdup(NULL_SENTINEL);
953 }
954 // Only insert into cache if strdup succeeded (and it's not the sentinel)
955 if (result[orig_idx] && strcmp(result[orig_idx], NULL_SENTINEL) != 0) {
956 if (!symbol_cache_insert(uncached_addrs[i], resolved[i])) {
957 log_error("Failed to insert symbol into cache for result[%d]", orig_idx);
958 }
959 }
960 SAFE_FREE(resolved[i]);
961 } else {
962 // resolved[i] is NULL - use sentinel string
963 if (!result[orig_idx]) {
964 result[orig_idx] = platform_strdup(NULL_SENTINEL);
965 }
966 }
967 if (!result[orig_idx]) {
968 log_error("Failed to allocate memory for result[%d]", orig_idx);
969 }
970 }
971
972 SAFE_FREE(resolved);
973
974 } else {
975 // addr2line failed - fill uncached entries with raw addresses or sentinel
976 for (int i = 0; i < uncached_count; i++) {
977 int orig_idx = uncached_indices[i];
978 if (!result[orig_idx]) {
979 result[orig_idx] = SAFE_MALLOC(32, char *);
980 if (result[orig_idx]) {
981 SAFE_SNPRINTF(result[orig_idx], 32, "%p", uncached_addrs[i]);
982 } else {
983 result[orig_idx] = platform_strdup(NULL_SENTINEL);
984 }
985 }
986 if (!result[orig_idx]) {
987 log_error("Failed to allocate memory for result[%d]", orig_idx);
988 }
989 }
990 }
991 }
992
993 // Clean up heap-allocated arrays
994 SAFE_FREE(uncached_addrs);
995 SAFE_FREE(uncached_indices);
996
997 return result;
998}
999
1000void symbol_cache_free_symbols(char **symbols) {
1001 if (!symbols) {
1002 return;
1003 }
1004
1005 // The array is NULL-terminated (allocated with size+1, with result[size] = NULL)
1006 // The terminator is a SINGLE NULL at index 'size'
1007 // Failed allocations use the NULL_SENTINEL string "[NULL]" instead of NULL,
1008 // so there are no NULL entries in the middle - only at the terminator
1009 // This makes iteration safe: we can iterate until we find the first NULL (the terminator)
1010
1011 // Iterate through entries, freeing all non-NULL entries until we hit the NULL terminator
1012 for (int i = 0; i < 64; i++) { // Reasonable limit to prevent infinite loop
1013 if (symbols[i] == NULL) {
1014 // Found NULL - this is the terminator, stop here
1015 break;
1016 }
1017
1018 // Found a non-NULL entry - check if it's the sentinel string
1019 // Both regular strings and sentinel strings are allocated (with strdup),
1020 // so we free them all
1021 SAFE_FREE(symbols[i]);
1022 symbols[i] = NULL; // Clear pointer after freeing
1023 }
1024
1025 // Free the array itself
1026 SAFE_FREE(symbols);
1027}
uint64_t atomic_fetch_add_u64(atomic_t *a, uint64_t delta)
Atomically add to a uint64_t and return the previous value.
Definition atomic.c:248
void atomic_store_bool(atomic_t *a, bool value)
Atomically store a boolean value.
Definition atomic.c:177
bool atomic_load_bool(atomic_t *a)
Atomically load a boolean value.
Definition atomic.c:169
void atomic_store_u64(atomic_t *a, uint64_t value)
Atomically store a uint64_t value.
Definition atomic.c:241
uint64_t atomic_load_u64(atomic_t *a)
Atomically load a uint64_t value.
Definition atomic.c:233
bool atomic_cas_bool(atomic_t *a, bool *expected, bool new_value)
Atomically compare-and-swap a boolean.
Definition atomic.c:184
⚛️ Atomic operations abstraction layer with debug tracking
⚙️ Common definitions, error codes, macros, and types shared throughout the application
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:414
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
#define SAFE_GETENV(name)
Definition common.h:434
#define SAFE_SNPRINTF(buffer, buffer_size,...)
Definition common.h:492
#define SAFE_CALLOC(count, size, cast)
Definition common.h:274
unsigned long long uint64_t
Definition common.h:59
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_THREAD
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_dev(...)
Log a DEV message (most verbose, development only)
Definition log/log.h:534
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
asciichat_error_t platform_popen(const char *name, const char *command, const char *mode, FILE **out_stream)
Execute a command and return a file stream for reading/writing.
bool platform_get_executable_path(char *exe_path, size_t path_size)
Get the path to the current executable.
Definition filesystem.c:191
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:349
void symbol_cache_get_stats(uint64_t *hits_out, uint64_t *misses_out, size_t *entries_out)
Get cache statistics.
Definition symbols.c:435
bool symbol_cache_insert(void *addr, const char *symbol)
Insert a symbol into the cache.
Definition symbols.c:377
int safe_snprintf(char *buffer, size_t buffer_size, const char *format,...)
Safe formatted string printing to buffer.
Definition system.c:148
int rwlock_init(rwlock_t *rwlock, const char *name)
Initialize a read-write lock with a name.
Definition threading.c:65
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
void symbol_cache_print_stats(void)
Print cache statistics to logging system.
Definition symbols.c:449
bool platform_is_binary_in_path(const char *bin_name)
Check if a binary is available in the system PATH.
Definition filesystem.c:133
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)
char ** symbol_cache_resolve_batch(void *const *buffer, int size)
Resolve multiple addresses using addr2line and cache results.
Definition symbols.c:856
const char * symbol_cache_lookup(void *addr)
Look up a symbol for a given address.
Definition symbols.c:354
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:313
asciichat_error_t symbol_cache_init(void)
Initialize the symbol cache.
Definition symbols.c:291
void symbol_cache_destroy(void)
Clean up the symbol cache and free all resources.
Definition symbols.c:316
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.
asciichat_error_t platform_pclose(FILE **stream_ptr)
Close a process stream opened with platform_popen()
void symbol_cache_free_symbols(char **symbols)
Free symbol array returned by symbol_cache_resolve_batch.
Definition symbols.c:1000
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331
bool validate_shell_safe(const char *str, const char *allowed_chars)
Validate that a string contains only safe characters for shell commands.
Definition util/string.c:62
bool escape_path_for_shell(const char *path, char *out_buffer, size_t out_buffer_size)
Escape a path for safe use in shell commands (auto-platform)
const char * extract_project_relative_path(const char *file)
Extract relative path from an absolute path.
Definition path.c:456
Platform initialization and static synchronization helpers.
void lifecycle_init_abort(lifecycle_t *lc)
Definition lifecycle.c:99
bool lifecycle_is_initialized(const lifecycle_t *lc)
Definition lifecycle.c:155
bool lifecycle_init(lifecycle_t *lc, const char *name)
Definition lifecycle.c:26
bool lifecycle_shutdown(lifecycle_t *lc)
Definition lifecycle.c:108
Lock-free module lifecycle state machine using pure stdatomic.
#define LIFECYCLE_INIT
Static initializer for module-global lifecycle variables (no sync primitive)
Definition lifecycle.h:68
📝 Logging API with multiple log levels and terminal output control
📂 Path Manipulation Utilities
Cross-platform process types and execution utilities.
Cross-platform read-write lock interface for ascii-chat.
Atomic value wrapper for integral/boolean types.
Definition atomic.h:76
Find which binary(ies) contain a given address.
Definition system.h:777
uintptr_t file_offset
Definition system.h:781
Read-write lock type (POSIX: pthread_rwlock_t with debug tracking)
Definition rwlock.h:61
Symbol cache entry structure for address-to-symbol mapping.
Definition symbols.c:108
UT_hash_handle hh
uthash handle (required for hash table operations)
Definition symbols.c:114
char * symbol
Resolved symbol string (allocated, owned by cache)
Definition symbols.c:112
void * addr
Memory address key (used for hashtable lookup)
Definition symbols.c:110
#define LLVM_SYMBOLIZER_BIN
Definition symbols.c:14
#define NULL_SENTINEL
Definition symbols.c:46
#define ADDR2LINE_BIN
Definition symbols.c:15
symbolizer_type_t
Definition symbols.c:52
@ SYMBOLIZER_NONE
Definition symbols.c:53
@ SYMBOLIZER_LLVM
Definition symbols.c:54
@ SYMBOLIZER_ADDR2LINE
Definition symbols.c:55
Symbol Resolution Cache for Backtrace Addresses.
#define PLATFORM_MAX_PATH_LENGTH
Definition system.c:69
Cross-platform system functions interface for ascii-chat.
🔤 String Manipulation and Shell Escaping Utilities