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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@
All notable changes to the Tombstack Native SDK (the `tombstone_*` C ABI and
the `tombstone` library name are stable — Tombstack is the product name).

## [0.9.1] - 2026-07-20

### Hardening (production audit; no API/ABI change)

- **Native crash handler — bounds-checked ELF NOTE parsing.** `read_build_id` now validates the note
name + descriptor against the segment end before reading, and guards forward progress, so a
truncated/hostile `PT_NOTE` can't drive an out-of-bounds read at handler install.
- **Native crash handler — atomic re-entry guard.** The double-fault guard is now
`std::atomic_flag::test_and_set` (async-signal-safe AND cross-thread) instead of a
read-then-write `volatile sig_atomic_t`, so two threads faulting at once can't both write the dump.
- **TLS verification asserted.** The libcurl transport now sets `CURLOPT_SSL_VERIFYPEER`/`VERIFYHOST`
explicitly instead of inheriting the integrator's defaults.
- **Breadcrumb memory scrub on consent revoke / GDPR reset.** `BreadcrumbRing::clear()` now clears the
slot strings, not just the head/count, so pre-revoke breadcrumb text doesn't linger in process memory.
- **Version string fixed** — `tombstone_version()` now reports the real version (was stuck at `0.8.0`);
header docblock updated to describe the shipped opt-in native handler.

## [0.9.0] - 2026-07-20

### Added — in-process native crash handler (EXPERIMENTAL, opt-in; Linux/Android)
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(tombstone VERSION 0.9.0 LANGUAGES C CXX)
project(tombstone VERSION 0.9.1 LANGUAGES C CXX)

option(TOMBSTONE_BUILD_STATIC "Also build a static tombstone library" OFF)
option(TOMBSTONE_BUILD_EXAMPLES "Build the example programs" ON)
Expand Down
10 changes: 6 additions & 4 deletions include/tombstone/tombstone.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
* opaque type reserved for a future multi-instance API). Double-init and
* use-after-shutdown return result codes; they are never undefined behavior.
*
* Crash capture scope (v0.x): this SDK REPORTS crashes you hand it
* (tombstone_report_crash) and detects unclean shutdowns across launches.
* It does NOT install signal/SEH handlers or write minidumps — that arrives
* in Phase 2 via a sentry-native/Crashpad fork (see README roadmap).
* Crash capture scope: this SDK REPORTS crashes you hand it
* (tombstone_report_crash) and detects unclean shutdowns across launches. As of
* v0.9 it can ALSO install an in-process async-signal-safe native crash handler
* (opt-in via options.enable_native_crash_handler, default off; Linux/Android) —
* see that field. Full stack unwinding (Breakpad) + Windows SEH + iOS Mach
* remain on the roadmap (see README).
*
* Pre-init capture (v0.7): tombstone_add_breadcrumb / tombstone_track_event /
* tombstone_track_metric / tombstone_set_user / tombstone_set_environment /
Expand Down
7 changes: 7 additions & 0 deletions src/breadcrumb_ring.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ void BreadcrumbRing::clear() {
const std::lock_guard<std::mutex> lock(mutex_);
head_ = 0;
count_ = 0;
// Scrub slot memory too: clear() is called on consent revoke / GDPR reset, so pre-revoke
// breadcrumb text (potentially PII) must not linger in the process until later overwrites.
for (auto &slot : slots_) {
slot.ts_iso.clear();
slot.level.clear();
slot.message.clear();
}
}

} // namespace tombstone
30 changes: 21 additions & 9 deletions src/native_crash.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <signal.h>
#include <ucontext.h>
#include <unistd.h>
#include <atomic>
#include <cerrno>
#include <cstdint>
#include <cstdlib>
Expand Down Expand Up @@ -128,11 +129,15 @@ struct HandlerState {
int dump_fd{-1};
struct sigaction old_actions[kSignalCount];
bool installed{false};
volatile sig_atomic_t in_handler{0}; // re-entrance / double-fault guard
};

HandlerState g_state;

// Re-entrance / double-fault guard. `atomic_flag::test_and_set` is the ONE lock-free op guaranteed
// async-signal-safe AND atomic across threads (a plain `volatile sig_atomic_t` read-then-write is
// not a CAS — two threads faulting at once could both pass and corrupt the single dump fd).
std::atomic_flag g_in_handler = ATOMIC_FLAG_INIT;

// Fixed alternate signal stack (glibc 2.34+ made SIGSTKSZ a runtime value that
// can't size an array): 64 KiB is comfortably above SIGSTKSZ on every target.
char g_alt_stack[65536];
Expand All @@ -155,15 +160,22 @@ void read_build_id(const ElfW(Phdr) & phdr, std::uintptr_t base, ModuleEntry &en
while (reinterpret_cast<const char *>(note) + sizeof(ElfW(Nhdr)) <= end) {
const char *name = reinterpret_cast<const char *>(note) + sizeof(ElfW(Nhdr));
const char *desc = name + ((note->n_namesz + 3) & ~3u);
if (note->n_type == NT_GNU_BUILD_ID && note->n_namesz == 4 &&
// Bounds: a truncated / hostile NOTE must never drive an out-of-bounds read. `desc` past the
// segment end, or the "GNU" name / the descriptor overrunning `end`, aborts the walk.
if (desc > end) break;
if (note->n_type == NT_GNU_BUILD_ID && note->n_namesz == 4 && name + 4 <= end &&
std::memcmp(name, "GNU", 3) == 0) {
std::size_t len = note->n_descsz;
if (len > kMaxBuildIdBytes) len = kMaxBuildIdBytes;
std::memcpy(entry.build_id, desc, len);
entry.build_id_len = len;
if (desc + len <= end) {
std::memcpy(entry.build_id, desc, len);
entry.build_id_len = len;
}
return;
}
note = reinterpret_cast<const ElfW(Nhdr) *>(desc + ((note->n_descsz + 3) & ~3u));
const char *next = desc + ((note->n_descsz + 3) & ~3u);
if (next <= reinterpret_cast<const char *>(note)) break; // no forward progress → stop
note = reinterpret_cast<const ElfW(Nhdr) *>(next);
}
}

Expand Down Expand Up @@ -325,13 +337,13 @@ void chain_old(int sig, siginfo_t *info, void *ucontext) {
}

extern "C" void tombstone_signal_handler(int sig, siginfo_t *info, void *ucontext) {
// Re-entrance / double-fault guard: if a second fatal signal arrives while
// we're dumping, don't recurse — hand straight to the old handler.
if (g_state.in_handler != 0) {
// Re-entrance / double-fault guard (atomic test-and-set): a second fatal signal — including one on
// another thread while we're dumping — must not recurse or share the fd; hand straight to the old
// handler. Never cleared: a crash is terminal.
if (g_in_handler.test_and_set()) {
chain_old(sig, info, ucontext);
return;
}
g_state.in_handler = 1;

const int fd = g_state.dump_fd;
if (fd >= 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/tombstone_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

namespace {

constexpr const char *sdk_version = "0.8.0";
constexpr const char *sdk_version = "0.9.1";

// The process-wide client. Entry points snapshot the shared_ptr under the
// mutex and call outside it, so a long flush() neither blocks other calls nor
Expand Down
5 changes: 5 additions & 0 deletions src/transport_curl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ HttpResponse perform(CURL *handle, SdkLog &sdk_log) {
curl_easy_setopt(handle, CURLOPT_HEADERDATA, &retry_after);
curl_easy_setopt(handle, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 0L);
// Assert TLS verification explicitly rather than inheriting the integrator's libcurl defaults:
// this SDK ships a Bearer ingest token to a remote endpoint and must not accept an untrusted or
// MITM'd cert even if the host build flipped the defaults. (libcurl defaults to these today.)
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L);

const CURLcode code = curl_easy_perform(handle);
HttpResponse response;
Expand Down
Loading