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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -96,19 +96,17 @@ pub fn build(b: *std.Build) void {
// Enable GNU extensions (sched_getcpu, CPU_ZERO, pthread_setaffinity_np, etc.)
sanitizer_flag_buf[sanitizer_flag_count] = "-D_GNU_SOURCE";
sanitizer_flag_count += 1;
sanitizer_flag_buf[sanitizer_flag_count] = "-g";
sanitizer_flag_count += 1;
if (sanitize) {
sanitizer_flag_buf[sanitizer_flag_count] = "-fsanitize=address,undefined";
sanitizer_flag_count += 1;
sanitizer_flag_buf[sanitizer_flag_count] = "-fno-omit-frame-pointer";
sanitizer_flag_count += 1;
sanitizer_flag_buf[sanitizer_flag_count] = "-g";
sanitizer_flag_count += 1;
}
if (tsan) {
sanitizer_flag_buf[sanitizer_flag_count] = "-fsanitize=thread";
sanitizer_flag_count += 1;
sanitizer_flag_buf[sanitizer_flag_count] = "-g";
sanitizer_flag_count += 1;
}
if (no_gen_checks) {
sanitizer_flag_buf[sanitizer_flag_count] = "-DRUN_NO_GEN_CHECKS";
Expand Down Expand Up @@ -311,6 +309,11 @@ pub fn build(b: *std.Build) void {

const run_runtime_tests = b.addRunArtifact(runtime_test_exe);
run_runtime_tests.step.dependOn(&runtime_test_exe.step);
if (target_info.os.tag == .macos) {
const runtime_tests_dsym = b.addSystemCommand(&.{"dsymutil"});
runtime_tests_dsym.addArtifactArg(runtime_test_exe);
run_runtime_tests.step.dependOn(&runtime_tests_dsym.step);
}
const runtime_test_step = b.step("test-runtime", "Run runtime C tests");
runtime_test_step.dependOn(&run_runtime_tests.step);

Expand Down
204 changes: 204 additions & 0 deletions src/runtime/run_stacktrace.c
Original file line number Diff line number Diff line change
@@ -1,12 +1,215 @@
#include "run_stacktrace.h"

#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined(__APPLE__) || defined(__linux__)
#define UNW_LOCAL_ONLY
#include <dlfcn.h>
#include <fcntl.h>
#include <libunwind.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

#if defined(__linux__)
/* dl_iterate_phdr lives in <link.h> and needs _GNU_SOURCE, which build.zig
* already defines globally for runtime sources. */
#include <link.h>
#include <stdint.h>

typedef struct {
uintptr_t ip;
unsigned long long elf_vma;
int found;
} run_phdr_lookup_t;

/* Convert a runtime IP into the ELF VMA the linker assigned to that
* instruction — the address `addr2line` expects. dlpi_addr is the module's
* "load slide" (zero for ET_EXEC, the ASLR offset for ET_DYN/PIE), so
* `ip - dlpi_addr` is the ELF VMA. dladdr's dli_fbase isn't usable for this
* because glibc reports it as l_map_start (the lowest mapped address), which
* for an ET_EXEC binary with text at 0x1000000 is 0x1000000 — subtracting it
* yields a file-relative offset rather than the ELF VMA addr2line wants. */
static int run_phdr_lookup_cb(struct dl_phdr_info *info, size_t size, void *data) {
(void)size;
run_phdr_lookup_t *q = (run_phdr_lookup_t *)data;
for (uint16_t i = 0; i < info->dlpi_phnum; i++) {
const ElfW(Phdr) *ph = &info->dlpi_phdr[i];
if (ph->p_type != PT_LOAD)
continue;
uintptr_t seg_start = (uintptr_t)info->dlpi_addr + (uintptr_t)ph->p_vaddr;
uintptr_t seg_end = seg_start + (uintptr_t)ph->p_memsz;
if (q->ip >= seg_start && q->ip < seg_end) {
q->elf_vma = (unsigned long long)(q->ip - (uintptr_t)info->dlpi_addr);
q->found = 1;
return 1; /* stop iteration */
}
}
return 0;
}
#endif

#if defined(__APPLE__) || defined(__linux__)
static void run_stacktrace_apply_file_line(run_stack_entry_t *entry, char *text) {
if (!entry || !text)
return;

text[strcspn(text, "\r\n")] = '\0';
char *colon = strrchr(text, ':');
if (!colon)
return;

char *line_start = colon + 1;
char *line_end = line_start;
while (*line_end >= '0' && *line_end <= '9') {
line_end++;
}
if (line_end == line_start)
return;

char saved = *line_end;
*line_end = '\0';
long line = strtol(line_start, NULL, 10);
*line_end = saved;
if (line <= 0)
return;

char *file_start = text;
char *open = NULL;
for (char *p = text; p < colon; p++) {
if (*p == '(') {
open = p;
}
}
if (open) {
file_start = open + 1;
}
while (*file_start == ' ' || *file_start == '\t') {
file_start++;
}
*colon = '\0';

if (*file_start != '\0') {
strncpy(entry->file, file_start, sizeof(entry->file) - 1);
entry->file[sizeof(entry->file) - 1] = '\0';
}
entry->line = (int64_t)line;
}

/* Spawn argv[0] with the given argv, capturing stdout into out_buf and stderr
* into /dev/null. Replaces popen(3) so we don't invoke a command processor
* (cert-env33-c) and don't have to escape the binary path into a shell string.
* Returns the number of bytes read into out_buf (0 on any failure). */
static size_t run_stacktrace_spawn_capture(char *const argv[], char *out_buf, size_t out_cap) {
if (out_cap == 0)
return 0;
out_buf[0] = '\0';

int pipefd[2];
if (pipe(pipefd) != 0)
return 0;

pid_t pid = fork();
if (pid < 0) {
close(pipefd[0]);
close(pipefd[1]);
return 0;
}
if (pid == 0) {
/* Child: only async-signal-safe calls between fork and exec. */
close(pipefd[0]);
if (dup2(pipefd[1], STDOUT_FILENO) < 0)
_exit(127);
if (pipefd[1] != STDOUT_FILENO)
close(pipefd[1]);
int devnull = open("/dev/null", O_WRONLY);
if (devnull >= 0) {
(void)dup2(devnull, STDERR_FILENO);
if (devnull != STDERR_FILENO)
close(devnull);
}
execvp(argv[0], argv);
_exit(127);
}

close(pipefd[1]);
size_t total = 0;
for (;;) {
if (total + 1 >= out_cap)
break;
ssize_t n = read(pipefd[0], out_buf + total, out_cap - 1 - total);
if (n > 0) {
total += (size_t)n;
continue;
}
if (n == 0)
break;
if (errno == EINTR)
continue;
break;
}
/* Drain anything still buffered so the child doesn't get SIGPIPE. */
char drain[256];
while (read(pipefd[0], drain, sizeof(drain)) > 0) {
}
close(pipefd[0]);
out_buf[total] = '\0';

int status;
while (waitpid(pid, &status, 0) < 0 && errno == EINTR) {
}
return total;
}

static void run_stacktrace_symbolize_source(run_stack_entry_t *entry, const Dl_info *dl,
unw_word_t ip) {
if (!entry || !dl || !dl->dli_fname || !dl->dli_fbase)
return;

char addr_buf[32];
#if defined(__APPLE__)
char load_buf[32];
snprintf(load_buf, sizeof(load_buf), "0x%llx", (unsigned long long)(uintptr_t)dl->dli_fbase);
snprintf(addr_buf, sizeof(addr_buf), "0x%llx", (unsigned long long)ip);
char *argv[] = {(char *)"atos", (char *)"-o", (char *)dl->dli_fname, (char *)"-l",
load_buf, addr_buf, (char *)NULL};
#else
run_phdr_lookup_t q = {.ip = (uintptr_t)ip, .elf_vma = 0, .found = 0};
dl_iterate_phdr(run_phdr_lookup_cb, &q);
if (!q.found)
return;
snprintf(addr_buf, sizeof(addr_buf), "0x%llx", q.elf_vma);
char *argv[] = {(char *)"addr2line", (char *)"-f", (char *)"-C", (char *)"-e",
(char *)dl->dli_fname, addr_buf, (char *)NULL};
#endif

char output[1024];
if (run_stacktrace_spawn_capture(argv, output, sizeof(output)) == 0)
return;

#if defined(__linux__)
/* addr2line prints function name first, then file:line. Skip the first
* line and apply the second. */
char *nl = strchr(output, '\n');
if (!nl)
return;
char *file_line = nl + 1;
char *end = strchr(file_line, '\n');
if (end)
*end = '\0';
run_stacktrace_apply_file_line(entry, file_line);
#else
/* atos prints a single line. */
char *end = strchr(output, '\n');
if (end)
*end = '\0';
run_stacktrace_apply_file_line(entry, output);
#endif
}
#endif

size_t run_stacktrace_capture(run_stack_entry_t *out, size_t max_count, size_t skip) {
Expand Down Expand Up @@ -59,6 +262,7 @@ size_t run_stacktrace_capture(run_stack_entry_t *out, size_t max_count, size_t s
strncpy(e->function, dl.dli_sname, sizeof(e->function) - 1);
e->function[sizeof(e->function) - 1] = '\0';
}
run_stacktrace_symbolize_source(e, &dl, ip);
}

count++;
Expand Down
3 changes: 1 addition & 2 deletions src/runtime/run_stacktrace.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
*
* Walks the current thread's call stack using libunwind where available
* and symbolizes each frame with function name, binary path, and line
* number (line numbers require DWARF parsing and are currently 0 — see
* #409 follow-up).
* number when platform symbolication tools can resolve DWARF line tables.
*
* Supported platforms: macOS and Linux (both link against libunwind).
* On unsupported platforms these functions return 0/empty and callers
Expand Down
6 changes: 3 additions & 3 deletions src/runtime/tests/test_debug_api.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "test_framework.h"
#include "../run_debug_api.h"
#include "../run_string.h"
#include "test_framework.h"

#include <stdbool.h>
#include <string.h>
Expand All @@ -21,6 +21,7 @@ static void test_debug_stack_trace(void) {
run_stack_frame_t *top = (run_stack_frame_t *)frames.ptr;
RUN_ASSERT(top->function.len > 0);
RUN_ASSERT(top->file.len > 0);
RUN_ASSERT(top->line > 0);

/* At least one frame must resolve to the exported suite dispatcher —
* dladdr on Linux only sees the dynamic symbol table, so static test
Expand All @@ -29,8 +30,7 @@ static void test_debug_stack_trace(void) {
for (size_t i = 0; i < frames.len; i++) {
run_stack_frame_t *f =
(run_stack_frame_t *)((char *)frames.ptr + i * sizeof(run_stack_frame_t));
if (f->function.len > 0 &&
strstr(f->function.ptr, "run_test_debug_api") != NULL) {
if (f->function.len > 0 && strstr(f->function.ptr, "run_test_debug_api") != NULL) {
found_dispatcher = true;
break;
}
Expand Down
7 changes: 4 additions & 3 deletions src/runtime/tests/test_runtime_api.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include "test_framework.h"
#include "../run_runtime_api.h"
#include "../run_alloc.h"
#include "../run_runtime_api.h"
#include "test_framework.h"

#include <string.h>

Expand Down Expand Up @@ -65,7 +65,7 @@ static void test_runtime_caller(void) {
/* On macOS and Linux, libunwind should resolve the caller. */
RUN_ASSERT(info.ok);
RUN_ASSERT(info.file.len > 0);
RUN_ASSERT(info.line >= 0);
RUN_ASSERT(info.line > 0);
}

static void test_runtime_stack(void) {
Expand All @@ -76,6 +76,7 @@ static void test_runtime_stack(void) {
* only resolves symbols in the dynamic table, so static test functions
* show as <unknown> — but run_test_runtime_api is exported. */
RUN_ASSERT(strstr(s.ptr, "run_test_runtime_api") != NULL);
RUN_ASSERT(strstr(s.ptr, "test_runtime_api.c:") != NULL);
}

void run_test_runtime_api(void) {
Expand Down
Loading