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
1 change: 1 addition & 0 deletions .github/workflows/website-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
-DSOURCEMETA_CORE_LANG_STACKTRACE:BOOL=OFF
-DSOURCEMETA_CORE_UNICODE:BOOL=OFF
-DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/website-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
-DSOURCEMETA_CORE_LANG_ERROR:BOOL=OFF
-DSOURCEMETA_CORE_LANG_OPTIONS:BOOL=OFF
-DSOURCEMETA_CORE_LANG_TEXT:BOOL=OFF
-DSOURCEMETA_CORE_LANG_STACKTRACE:BOOL=OFF
-DSOURCEMETA_CORE_UNICODE:BOOL=OFF
-DSOURCEMETA_CORE_PUNYCODE:BOOL=OFF
-DSOURCEMETA_CORE_TIME:BOOL=OFF
Expand Down
11 changes: 11 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ option(SOURCEMETA_CORE_LANG_NUMERIC "Build the Sourcemeta Core language numeric
option(SOURCEMETA_CORE_LANG_ERROR "Build the Sourcemeta Core language error library" ON)
option(SOURCEMETA_CORE_LANG_OPTIONS "Build the Sourcemeta Core Options library" ON)
option(SOURCEMETA_CORE_LANG_TEXT "Build the Sourcemeta Core language text library" ON)
option(SOURCEMETA_CORE_LANG_STACKTRACE "Build the Sourcemeta Core language stacktrace library" ON)
option(SOURCEMETA_CORE_UNICODE "Build the Sourcemeta Core Unicode library" ON)
option(SOURCEMETA_CORE_PUNYCODE "Build the Sourcemeta Core Punycode library" ON)
option(SOURCEMETA_CORE_TIME "Build the Sourcemeta Core time library" ON)
Expand Down Expand Up @@ -99,6 +100,10 @@ if(SOURCEMETA_CORE_LANG_TEXT)
add_subdirectory(src/lang/text)
endif()

if(SOURCEMETA_CORE_LANG_STACKTRACE)
add_subdirectory(src/lang/stacktrace)
endif()

if(SOURCEMETA_CORE_UNICODE)
add_subdirectory(src/core/unicode)
endif()
Expand Down Expand Up @@ -245,6 +250,12 @@ if(SOURCEMETA_CORE_TESTS)
add_subdirectory(test/text)
endif()

if(SOURCEMETA_CORE_LANG_STACKTRACE
AND NOT SOURCEMETA_CORE_ADDRESS_SANITIZER
AND NOT SOURCEMETA_CORE_UNDEFINED_SANITIZER)
add_subdirectory(test/stacktrace)
endif()

if(SOURCEMETA_CORE_UNICODE)
add_subdirectory(test/unicode)
endif()
Expand Down
3 changes: 3 additions & 0 deletions config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS)
list(APPEND SOURCEMETA_CORE_COMPONENTS error)
list(APPEND SOURCEMETA_CORE_COMPONENTS options)
list(APPEND SOURCEMETA_CORE_COMPONENTS text)
list(APPEND SOURCEMETA_CORE_COMPONENTS stacktrace)
endif()

include(CMakeFindDependencyMacro)
Expand Down Expand Up @@ -158,6 +159,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_options.cmake")
elseif(component STREQUAL "text")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake")
elseif(component STREQUAL "stacktrace")
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_stacktrace.cmake")
else()
message(FATAL_ERROR "Unknown Sourcemeta Core component: ${component}")
endif()
Expand Down
16 changes: 16 additions & 0 deletions src/lang/stacktrace/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME stacktrace
PRIVATE_HEADERS
SOURCES stacktrace.cc stacktrace_posix.h stacktrace_windows.h)

if(UNIX)
target_link_libraries(sourcemeta_core_stacktrace PRIVATE ${CMAKE_DL_LIBS})
target_link_options(sourcemeta_core_stacktrace INTERFACE
"$<$<PLATFORM_ID:Linux>:-rdynamic>"
"$<$<PLATFORM_ID:Darwin>:-rdynamic>")
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/lang/stacktrace/CMakeLists.txt:9: -rdynamic is an ELF/GNU-style flag and is often rejected or ignored by Mach-O toolchains, so enabling it for PLATFORM_ID:Darwin may break macOS linking or provide no effect.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: -rdynamic is a GCC/ELF driver-level flag. On macOS (Mach-O), the native linker equivalent is -export_dynamic. When passed via target_link_options, CMake may forward this to the linker where it could be unrecognized or silently ignored. Consider using -Wl,-export_dynamic for Darwin or guarding this to Linux only.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/stacktrace/CMakeLists.txt, line 9:

<comment>`-rdynamic` is a GCC/ELF driver-level flag. On macOS (Mach-O), the native linker equivalent is `-export_dynamic`. When passed via `target_link_options`, CMake may forward this to the linker where it could be unrecognized or silently ignored. Consider using `-Wl,-export_dynamic` for Darwin or guarding this to Linux only.</comment>

<file context>
@@ -0,0 +1,16 @@
+  target_link_libraries(sourcemeta_core_stacktrace PRIVATE ${CMAKE_DL_LIBS})
+  target_link_options(sourcemeta_core_stacktrace INTERFACE
+    "$<$<PLATFORM_ID:Linux>:-rdynamic>"
+    "$<$<PLATFORM_ID:Darwin>:-rdynamic>")
+elseif(WIN32)
+  target_link_libraries(sourcemeta_core_stacktrace PRIVATE dbghelp)
</file context>

elseif(WIN32)
target_link_libraries(sourcemeta_core_stacktrace PRIVATE dbghelp)
endif()

if(SOURCEMETA_CORE_INSTALL)
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME stacktrace)
endif()
50 changes: 50 additions & 0 deletions src/lang/stacktrace/include/sourcemeta/core/stacktrace.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#ifndef SOURCEMETA_CORE_STACKTRACE_H_
#define SOURCEMETA_CORE_STACKTRACE_H_

#ifndef SOURCEMETA_CORE_STACKTRACE_EXPORT
#include <sourcemeta/core/stacktrace_export.h>
#endif

/// @defgroup stacktrace Stacktrace
/// @brief A collection of utilities for interacting with stack traces.
///
/// This functionality is included as follows:
///
/// ```cpp
/// #include <sourcemeta/core/stacktrace.h>
/// ```

namespace sourcemeta::core {

/// @ingroup stacktrace
///
/// Install a process-wide handler that prints a stack trace on fatal signals.
/// For example:
///
/// ```cpp
/// #include <sourcemeta/core/stacktrace.h>
///
/// auto main() -> int {
/// sourcemeta::core::stacktrace_on_crash();
/// // ... rest of the program
/// }
/// ```
SOURCEMETA_CORE_STACKTRACE_EXPORT
auto stacktrace_on_crash() -> void;

/// @ingroup stacktrace
///
/// Print the current backtrace. Safe to call from any thread, but not from a
/// signal handler. For example:
///
/// ```cpp
/// #include <sourcemeta/core/stacktrace.h>
///
/// sourcemeta::core::stacktrace();
/// ```
SOURCEMETA_CORE_STACKTRACE_EXPORT
auto stacktrace() -> void;

} // namespace sourcemeta::core

#endif
9 changes: 9 additions & 0 deletions src/lang/stacktrace/stacktrace.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include <sourcemeta/core/stacktrace.h>

#if defined(_WIN32)
#include "stacktrace_windows.h"
#elif defined(__unix__) || defined(__APPLE__)
#include "stacktrace_posix.h"
#else
#error "sourcemeta::core::stacktrace has no implementation for this platform"
#endif
241 changes: 241 additions & 0 deletions src/lang/stacktrace/stacktrace_posix.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#ifndef SOURCEMETA_CORE_STACKTRACE_POSIX_H_
#define SOURCEMETA_CORE_STACKTRACE_POSIX_H_

#include <sourcemeta/core/stacktrace.h>

#include <array> // std::array
#include <atomic> // std::atomic
#include <csignal> // sigaction, struct sigaction, SIG*, raise
#include <cstddef> // std::size_t
#include <cstdint> // std::uintptr_t
#include <cstring> // std::strlen
#include <initializer_list> // std::initializer_list

#include <dlfcn.h> // dladdr, Dl_info
#include <execinfo.h> // backtrace
#include <sys/ucontext.h> // ucontext_t
#include <unistd.h> // write, getpid, STDERR_FILENO

namespace {

constexpr int maximum_frames{128};
constexpr std::size_t hex_buffer_size{2 + (sizeof(std::uintptr_t) * 2)};
constexpr std::size_t decimal_buffer_size{24};

auto raw_write(int file_descriptor, const char *data, std::size_t size)
-> void {
[[maybe_unused]] const auto result{::write(file_descriptor, data, size)};
}

auto write_text(int file_descriptor, const char *text) -> void {
raw_write(file_descriptor, text, std::strlen(text));
}

auto write_hex(int file_descriptor, std::uintptr_t value) -> void {
constexpr const char *digits{"0123456789abcdef"};
std::array<char, hex_buffer_size> buffer{{'0', 'x'}};
std::size_t index{2};
if (value == 0) {
buffer[index++] = '0';
} else {
std::array<char, sizeof(std::uintptr_t) * 2> temporary{};
std::size_t length{0};
while (value != 0) {
temporary[length++] = digits[value & 0xF];
value >>= 4;
}
while (length > 0) {
buffer[index++] = temporary[--length];
}
}
raw_write(file_descriptor, buffer.data(), index);
}

auto write_decimal(int file_descriptor, unsigned long value) -> void {
std::array<char, decimal_buffer_size> buffer{};
std::size_t length{0};
if (value == 0) {
buffer[length++] = '0';
} else {
std::array<char, decimal_buffer_size> temporary{};
std::size_t temporary_length{0};
while (value != 0) {
temporary[temporary_length++] = static_cast<char>('0' + (value % 10));
value /= 10;
}
while (temporary_length > 0) {
buffer[length++] = temporary[--temporary_length];
}
}
raw_write(file_descriptor, buffer.data(), length);
}

auto write_frame(int file_descriptor, int frame_index, void *address) -> void {
Dl_info information{};
const int resolved{::dladdr(address, &information)};

write_text(file_descriptor, "#");
write_decimal(file_descriptor, static_cast<unsigned long>(frame_index));
write_text(file_descriptor, " ");
write_hex(file_descriptor, reinterpret_cast<std::uintptr_t>(address));
write_text(file_descriptor, " ");

const char *symbol_name{(resolved != 0 && information.dli_sname != nullptr)
? information.dli_sname
: "<unknown>"};
write_text(file_descriptor, symbol_name);

if (resolved != 0 && information.dli_saddr != nullptr) {
const auto offset{static_cast<std::uintptr_t>(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This computes an offset via pointer subtraction on unrelated addresses, which is undefined behavior in C++. Use uintptr_t arithmetic for address differences.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/stacktrace/stacktrace_posix.h, line 89:

<comment>This computes an offset via pointer subtraction on unrelated addresses, which is undefined behavior in C++. Use `uintptr_t` arithmetic for address differences.</comment>

<file context>
@@ -0,0 +1,241 @@
+  write_text(file_descriptor, symbol_name);
+
+  if (resolved != 0 && information.dli_saddr != nullptr) {
+    const auto offset{static_cast<std::uintptr_t>(
+        reinterpret_cast<char *>(address) -
+        reinterpret_cast<char *>(information.dli_saddr))};
</file context>

reinterpret_cast<char *>(address) -
reinterpret_cast<char *>(information.dli_saddr))};
write_text(file_descriptor, " +");
write_hex(file_descriptor, offset);
}

if (resolved != 0 && information.dli_fname != nullptr) {
write_text(file_descriptor, "\n in ");
write_text(file_descriptor, information.dli_fname);
}
write_text(file_descriptor, "\n");
}

__attribute__((noinline)) auto write_backtrace(int file_descriptor,
int frames_to_skip,
void *crash_pc = nullptr)
-> void {
std::array<void *, maximum_frames> frames{};
const int captured{::backtrace(frames.data(), maximum_frames)};

int frame_index{0};
void *suppress_saddr{nullptr};
if (crash_pc != nullptr) {
write_frame(file_descriptor, frame_index, crash_pc);
frame_index = frame_index + 1;
Dl_info crash_information{};
if (::dladdr(crash_pc, &crash_information) != 0 &&
crash_information.dli_saddr != nullptr) {
suppress_saddr = crash_information.dli_saddr;
}
}

for (int index{frames_to_skip}; index < captured; ++index) {
void *address{frames[static_cast<std::size_t>(index)]};
if (suppress_saddr != nullptr) {
Dl_info frame_information{};
if (::dladdr(address, &frame_information) != 0 &&
frame_information.dli_saddr == suppress_saddr) {
suppress_saddr = nullptr;
continue;
}
}
write_frame(file_descriptor, frame_index, address);
frame_index = frame_index + 1;
}
}

auto extract_crash_pc(void *context) -> void * {
if (context == nullptr) {
return nullptr;
}
[[maybe_unused]] const auto *user_context{
static_cast<const ucontext_t *>(context)};
std::uintptr_t program_counter{0};
#if defined(__APPLE__) && defined(__aarch64__)
program_counter = user_context->uc_mcontext->__ss.__pc;
#elif defined(__APPLE__) && defined(__x86_64__)
program_counter = user_context->uc_mcontext->__ss.__rip;
#elif defined(__linux__) && defined(__aarch64__)
program_counter = user_context->uc_mcontext.pc;
#elif defined(__linux__) && defined(__x86_64__)
program_counter =
static_cast<std::uintptr_t>(user_context->uc_mcontext.gregs[16]);
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/lang/stacktrace/stacktrace_posix.h:151-152: Using user_context->uc_mcontext.gregs[16] hard-codes the register index and may be wrong on non-glibc libcs/ABIs, leading to an incorrect crash_pc (or a null one) in the trace.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Hardcoding gregs[16] is ABI-fragile for x86_64 context decoding; use REG_RIP to make program-counter extraction robust.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/stacktrace/stacktrace_posix.h, line 152:

<comment>Hardcoding `gregs[16]` is ABI-fragile for x86_64 context decoding; use `REG_RIP` to make program-counter extraction robust.</comment>

<file context>
@@ -0,0 +1,241 @@
+  program_counter = user_context->uc_mcontext.pc;
+#elif defined(__linux__) && defined(__x86_64__)
+  program_counter =
+      static_cast<std::uintptr_t>(user_context->uc_mcontext.gregs[16]);
+#endif
+  // NOLINTNEXTLINE(performance-no-int-to-ptr)
</file context>
Suggested change
static_cast<std::uintptr_t>(user_context->uc_mcontext.gregs[16]);
static_cast<std::uintptr_t>(user_context->uc_mcontext.gregs[REG_RIP]);

#endif
// NOLINTNEXTLINE(performance-no-int-to-ptr)
return reinterpret_cast<void *>(program_counter);
}

constexpr const char *separator{"========================================"
"========================================\n"};

std::atomic<bool> crash_handler_installed{false};

extern "C" __attribute__((visibility("default"))) auto
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: crash_handler is defined inside an anonymous namespace, giving it internal linkage per C++11 (GCC docs explicitly note that extern "C" has no effect on linkage inside unnamed namespaces). This means dladdr() will be unable to resolve its symbol name, printing <unknown> instead. The test scripts expect crash_handler to appear by name in the trace output. Move this function out of the anonymous namespace to give it external linkage so dladdr() can resolve it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/stacktrace/stacktrace_posix.h, line 163:

<comment>`crash_handler` is defined inside an anonymous namespace, giving it internal linkage per C++11 (GCC docs explicitly note that `extern "C"` has no effect on linkage inside unnamed namespaces). This means `dladdr()` will be unable to resolve its symbol name, printing `<unknown>` instead. The test scripts expect `crash_handler` to appear by name in the trace output. Move this function out of the anonymous namespace to give it external linkage so `dladdr()` can resolve it.</comment>

<file context>
@@ -0,0 +1,241 @@
+
+std::atomic<bool> crash_handler_installed{false};
+
+extern "C" __attribute__((visibility("default"))) auto
+crash_handler(int signal_number, siginfo_t * /*info*/, void *context) -> void {
+  const int file_descriptor{STDERR_FILENO};
</file context>

crash_handler(int signal_number, siginfo_t * /*info*/, void *context) -> void {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/lang/stacktrace/stacktrace_posix.h:164: Because crash_handler is declared inside an anonymous namespace, it has internal linkage and dladdr() may be unable to resolve information.dli_sname for it (printing <unknown> instead), which could make the printed trace/tests unreliable.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

const int file_descriptor{STDERR_FILENO};
write_text(file_descriptor, "\n");
write_text(file_descriptor, separator);
write_text(file_descriptor, "signal: ");
write_decimal(file_descriptor, static_cast<unsigned long>(signal_number));
write_text(file_descriptor, " (");
const char *signal_name{"UNKNOWN"};
switch (signal_number) {
case SIGSEGV:
signal_name = "SIGSEGV";
break;
case SIGABRT:
signal_name = "SIGABRT";
break;
case SIGFPE:
signal_name = "SIGFPE";
break;
case SIGBUS:
signal_name = "SIGBUS";
break;
case SIGILL:
signal_name = "SIGILL";
break;
default:
break;
}
write_text(file_descriptor, signal_name);
write_text(file_descriptor, ")\n");
write_text(file_descriptor, "pid: ");
write_decimal(file_descriptor, static_cast<unsigned long>(::getpid()));
write_text(file_descriptor, "\n\n");
write_backtrace(file_descriptor, /*frames_to_skip=*/1,
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

src/lang/stacktrace/stacktrace_posix.h:196: crash_handler calls write_backtrace() which uses backtrace()/dladdr()/std::strlen() from a signal handler; these APIs are not async-signal-safe and can deadlock or crash in some failure modes.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The crash signal handler executes non-async-signal-safe routines (backtrace, dladdr, and strlen through helper paths), which can deadlock or crash again while handling fatal signals.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lang/stacktrace/stacktrace_posix.h, line 196:

<comment>The crash signal handler executes non-async-signal-safe routines (`backtrace`, `dladdr`, and `strlen` through helper paths), which can deadlock or crash again while handling fatal signals.</comment>

<file context>
@@ -0,0 +1,241 @@
+  write_text(file_descriptor, "pid:     ");
+  write_decimal(file_descriptor, static_cast<unsigned long>(::getpid()));
+  write_text(file_descriptor, "\n\n");
+  write_backtrace(file_descriptor, /*frames_to_skip=*/1,
+                  extract_crash_pc(context));
+  write_text(file_descriptor, separator);
</file context>

extract_crash_pc(context));
write_text(file_descriptor, separator);

struct sigaction default_action{};
default_action.sa_handler = SIG_DFL;
sigemptyset(&default_action.sa_mask);
::sigaction(signal_number, &default_action, nullptr);
::raise(signal_number);
}

} // namespace

namespace sourcemeta::core {

// NOLINTNEXTLINE(misc-definitions-in-headers)
__attribute__((visibility("default"))) auto stacktrace_on_crash() -> void {
bool expected{false};
if (!crash_handler_installed.compare_exchange_strong(expected, true)) {
return;
}

struct sigaction action{};
action.sa_sigaction = &crash_handler;
action.sa_flags = static_cast<int>(SA_SIGINFO | SA_RESETHAND | SA_NODEFER);
sigemptyset(&action.sa_mask);

for (const int signal_number : {SIGSEGV, SIGABRT, SIGFPE, SIGBUS, SIGILL}) {
::sigaction(signal_number, &action, nullptr);
}
}

// NOLINTNEXTLINE(misc-definitions-in-headers)
__attribute__((noinline, visibility("default"))) auto stacktrace() -> void {
const int file_descriptor{STDERR_FILENO};
write_text(file_descriptor, separator);
write_text(file_descriptor, "pid: ");
write_decimal(file_descriptor, static_cast<unsigned long>(::getpid()));
write_text(file_descriptor, "\n\n");
write_backtrace(file_descriptor, /*frames_to_skip=*/1);
write_text(file_descriptor, separator);
}

} // namespace sourcemeta::core

#endif
Loading
Loading