Skip to content
Merged
3 changes: 2 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,9 @@ elseif(DD_CRASH_MODE STREQUAL "inprocess")
message(FATAL_ERROR "In-process crash handler not supported on this platform")
endif()

# In-process implementation needs to write crash report files
# In-process implementation needs to write crash report and context files
target_sources(dd_native PRIVATE
datadog/impl/platform/crash_context_write.cpp
datadog/impl/platform/crash_report_write.cpp
datadog/impl/platform/crash_handler_buildid_cache.cpp
)
Expand Down
5 changes: 5 additions & 0 deletions src/datadog/impl/core/feature_types/rum.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ struct RumFeatureContext {
UUID session_id; // UUID::Zero if no active session
UUID view_id; // UUID::Zero if no active view
UUID action_id; // UUID::Zero if no active action

bool operator==(const RumFeatureContext& other) const {
return application_id == other.application_id && session_id == other.session_id &&
view_id == other.view_id && action_id == other.action_id;
}
};

DATADOG_STRING_ENUM(
Expand Down
25 changes: 25 additions & 0 deletions src/datadog/impl/features/crash_reporting/crash_reporting.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

#include "datadog/impl/features/crash_reporting/crash_reporting.hpp"

#include <memory>

#include "datadog/impl/assert.hpp"
#include "datadog/impl/core/feature_message.hpp"
#include "datadog/impl/core/writer.hpp"

namespace datadog::impl {
Expand All @@ -24,6 +27,27 @@ std::optional<Report> CrashReporting::UploadThread_PrepareReport(
return std::nullopt;
}

std::optional<std::function<void(const FeatureMessage&)>>
CrashReporting::MakeMessageHandler() {
const auto weak_self = weak_from_this();
return [weak_self](const FeatureMessage& msg) {
const auto* context_changed = std::get_if<ContextChangedMessage>(&msg);
if (!context_changed || !context_changed->context.rum) {
return;
}
auto self = std::static_pointer_cast<CrashReporting>(weak_self.lock());
if (!self || !self->_crash_handler) {
return;
Comment on lines +39 to +40

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 Badge Skip RUM context writes when crash handler init failed

This handler only checks _crash_handler for non-null, but Start() keeps _crash_handler even when Initialize() returns false. In that failure mode, context-change messages still invoke SetRumContext; for the in-process backend this attempts file writes despite failed setup (for example after .crashes creation/open failure), causing repeated failed I/O and possible stray temp-file behavior. Track initialization success (or clear _crash_handler on failure) before forwarding context updates.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed with 9dd9597; impl::CrashReporting now discards and destroys its _crash_handler reference if ICrashHandler::Initialize() fails. Once the feature is started, it either has a valid, initialized handler or it has no handler; there is no longer an inbetween state.

}
const auto& rum_ctx = *context_changed->context.rum;
if (self->_last_rum_ctx == rum_ctx) {
return;
}
self->_last_rum_ctx = rum_ctx;
self->_crash_handler->SetRumContext(rum_ctx);
};
}

void CrashReporting::Start() {
DATADOG_ASSERT(_scope, "CrashReporting::Start called without valid FeatureScope");

Expand All @@ -46,6 +70,7 @@ void CrashReporting::Start() {
_scope->diagnostic_logger.Status("Crash handler initialized");
} else {
_scope->diagnostic_logger.Error("Crash handler initialization failed");
_crash_handler.reset();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include <string_view>

#include "datadog/impl/core/feature.hpp"
#include "datadog/impl/core/feature_types/rum.hpp"
#include "datadog/impl/platform/clock.hpp"
#include "datadog/impl/platform/crash_handler.hpp"

Expand All @@ -34,6 +35,9 @@ class CrashReporting final : public Feature {
const HttpContext& context, BatchReader& reader
) override;

std::optional<std::function<void(const FeatureMessage&)>>
MakeMessageHandler() override;

protected:
/**
* Responds to SDK start by initializing the Crashpad crash handler process.
Expand All @@ -53,6 +57,10 @@ class CrashReporting final : public Feature {

// Platform-specific crash handler implementation
std::unique_ptr<platform::ICrashHandler> _crash_handler;

// Last RUM context forwarded to the crash handler; used to suppress redundant
// SetRumContext calls when the context hasn't actually changed
std::optional<RumFeatureContext> _last_rum_ctx;
};

} // namespace datadog::impl
30 changes: 30 additions & 0 deletions src/datadog/impl/platform/crash_context.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
//
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

#pragma once

#include <cinttypes>

/**
* Example layout:
*
* 0x0000: <CrashContextHeaderMagic>
* 0x0008: version
* 0x0010: rum_application_id
* 0x0020: rum_session_id
* 0x0030: rum_view_id
* 0x0040: rum_action_id
* 0x0050: <CrashContextFooterMagic>
*/

namespace datadog::platform {

static const uint64_t CrashContextHeaderMagic = 0xdc01;
static const uint64_t CrashContextFileVersion = 1;

static const uint64_t CrashContextFooterMagic = 0xdcff;

} // namespace datadog::platform
114 changes: 114 additions & 0 deletions src/datadog/impl/platform/crash_context_write.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
//
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

#include "datadog/impl/platform/crash_context_write.hpp"

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <windows.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif

#include <cstddef>
#include <cstdint>
#include <cstdio>

#include "datadog/impl/core/feature_types/rum.hpp"
#include "datadog/impl/platform/crash_context.hpp"

namespace datadog::platform {

// NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
// NOLINTBEGIN(cppcoreguidelines-pro-type-vararg)

#ifdef _WIN32
using FileHandle = HANDLE;
static const FileHandle kInvalidHandle = INVALID_HANDLE_VALUE;
#else
using FileHandle = int;
static const FileHandle kInvalidHandle = -1;
#endif

static void write_bytes(FileHandle fd, const void* data, size_t size) {
#ifdef _WIN32
DWORD written = 0;
WriteFile(fd, data, static_cast<DWORD>(size), &written, nullptr);
(void)written;
#else
ssize_t result = write(fd, data, size);
(void)result;
#endif
}

static void write_uint64(FileHandle fd, uint64_t value) {
write_bytes(fd, &value, sizeof(value));
}

void WriteCrashContext(const char* filename, const impl::RumFeatureContext& rum_ctx) {
// Write to a .tmp file first, then atomically rename to the final path. This
// ensures readers on the next launch never observe a partially-written file.
char tmp[512];
#ifdef _WIN32
_snprintf_s(tmp, sizeof(tmp), _TRUNCATE, "%s.tmp", filename);
#else
snprintf(tmp, sizeof(tmp), "%s.tmp", filename);
#endif

#ifdef _WIN32
FileHandle fd = CreateFileA(
tmp, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr
);
#else
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
FileHandle fd = open(tmp, O_CREAT | O_WRONLY | O_TRUNC, 0644);
#endif

if (fd == kInvalidHandle) {
return;
}

write_uint64(fd, CrashContextHeaderMagic);
write_uint64(fd, CrashContextFileVersion);
write_bytes(fd, rum_ctx.application_id.bytes.data(), 16);
write_bytes(fd, rum_ctx.session_id.bytes.data(), 16);
write_bytes(fd, rum_ctx.view_id.bytes.data(), 16);
write_bytes(fd, rum_ctx.action_id.bytes.data(), 16);
write_uint64(fd, CrashContextFooterMagic);

#ifdef _WIN32
CloseHandle(fd);
// MoveFileExA with MOVEFILE_REPLACE_EXISTING is the closest Windows equivalent
// to an atomic rename; it replaces the destination in a single operation
MoveFileExA(tmp, filename, MOVEFILE_REPLACE_EXISTING);
#else
close(fd);
// rename() is atomic on POSIX when src and dst are on the same filesystem,
// which is guaranteed here since both paths share the same .crashes/ directory
rename(tmp, filename);
#endif
}

void DeleteCrashContext(const char* filename) {
char tmp[512];
#ifdef _WIN32
DeleteFileA(filename);
_snprintf_s(tmp, sizeof(tmp), _TRUNCATE, "%s.tmp", filename);
DeleteFileA(tmp);
#else
unlink(filename);
snprintf(tmp, sizeof(tmp), "%s.tmp", filename);
unlink(tmp);
#endif
}

// NOLINTEND(cppcoreguidelines-pro-type-vararg)
// NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay)

} // namespace datadog::platform
30 changes: 30 additions & 0 deletions src/datadog/impl/platform/crash_context_write.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
//
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2025-Present Datadog, Inc.

#pragma once

namespace datadog::impl {
struct RumFeatureContext;
}

namespace datadog::platform {

/**
* Creates or overwrites a crash context file at `filename`, writing the current RUM
* session identifiers in binary format.
*
* The file persists across crashes so the next SDK launch can recover the session
* context and attach it to any crash report found on disk. Call `DeleteCrashContext` on
* clean shutdown to remove it.
*/
void WriteCrashContext(const char* filename, const impl::RumFeatureContext& rum_ctx);

/**
* Deletes the crash context file at `filename`, if any such file exists.
*/
void DeleteCrashContext(const char* filename);

} // namespace datadog::platform
8 changes: 8 additions & 0 deletions src/datadog/impl/platform/crash_handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace datadog::impl {
class DiagnosticLogger;
struct RumFeatureContext;
} // namespace datadog::impl

namespace datadog::platform {
Expand Down Expand Up @@ -86,6 +87,13 @@ class ICrashHandler {
* like `virtual bool IsProcessGlobal()` or `IsReversible()` etc.
*/
virtual void Shutdown() = 0;

/**
* Persists `rum_ctx` so that it is available if a crash occurs before the next
* normal SDK shutdown. Only called when the RUM context has actually changed since
* the last call.
*/
virtual void SetRumContext(const impl::RumFeatureContext& rum_ctx) = 0;
};

namespace CrashHandler {
Expand Down
67 changes: 62 additions & 5 deletions src/datadog/impl/platform/crash_handler_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "client/crashpad_client.h"
#include "client/settings.h"

#include "datadog/impl/core/feature_types/rum.hpp"
#include "datadog/impl/diagnostics.hpp"
#include "datadog/impl/platform/crash_handler.hpp"

Expand All @@ -31,8 +32,10 @@
// them safe to read during a crash. The Crashpad handler will automatically resolve
// these values and include them as annotations when the crash dump is uploaded.
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
static crashpad::StringAnnotation<64> s_test_annotation1("test_annotation1");
static crashpad::StringAnnotation<64> s_test_annotation2("test_annotation2");
static crashpad::StringAnnotation<37> s_rum_application_id("rum.application_id");
static crashpad::StringAnnotation<37> s_rum_session_id("rum.session_id");
static crashpad::StringAnnotation<37> s_rum_view_id("rum.view_id");
static crashpad::StringAnnotation<37> s_rum_action_id("rum.action_id");
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)

/**
Expand Down Expand Up @@ -159,9 +162,11 @@ class CrashpadCrashHandler final : public ICrashHandler {
return false;
}

// Set initial annotation values as a test
s_test_annotation1.Set("foo");
s_test_annotation2.Set("bar");
// Clear annotation values, if any were set previously
s_rum_application_id.Set("");
s_rum_session_id.Set("");
s_rum_view_id.Set("");
s_rum_action_id.Set("");

// When the Crashpad client is first initialized, it populates the configured
// database directory with configuration metadata and other state. By default, a
Expand Down Expand Up @@ -225,9 +230,61 @@ class CrashpadCrashHandler final : public ICrashHandler {
// and continue running after SDK shutdown. We don't forcibly terminate it here.
}

void SetRumContext(const impl::RumFeatureContext& rum_ctx) override {
// NOLINTBEGIN(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
char buf[37] = {0};

if (rum_ctx.application_id != _rum_application_id) {
_rum_application_id = rum_ctx.application_id;
if (_rum_application_id == UUID::Zero) {
s_rum_application_id.Set("");
} else {
_rum_application_id.ToBytes(buf, std::size(buf));
s_rum_application_id.Set(buf);
Comment on lines +242 to +243

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 Badge NUL-terminate UUID buffer before setting Crashpad annotations

UUID::ToBytes serializes exactly 36 characters without a terminator, but this buffer is passed directly to StringAnnotation::Set as a C-string. When buf[36] is non-zero, annotation writes can include trailing stack garbage (and potentially read past intended bounds), so crash uploads may contain corrupted RUM IDs. Ensure the buffer is explicitly terminated (or use a length-aware setter) before calling Set.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed with 73a2a76; buffer is now zero-filled to ensure null-termination.

}
}

if (rum_ctx.session_id != _rum_session_id) {
_rum_session_id = rum_ctx.session_id;
if (_rum_session_id == UUID::Zero) {
s_rum_session_id.Set("");
} else {
_rum_session_id.ToBytes(buf, std::size(buf));
s_rum_session_id.Set(buf);
}
}

if (rum_ctx.view_id != _rum_view_id) {
_rum_view_id = rum_ctx.view_id;
if (_rum_view_id == UUID::Zero) {
s_rum_view_id.Set("");
} else {
_rum_view_id.ToBytes(buf, std::size(buf));
s_rum_view_id.Set(buf);
}
}

if (rum_ctx.action_id != _rum_action_id) {
_rum_action_id = rum_ctx.action_id;
if (_rum_action_id == UUID::Zero) {
s_rum_action_id.Set("");
} else {
_rum_action_id.ToBytes(buf, std::size(buf));
s_rum_action_id.Set(buf);
}
}
// NOLINTEND(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
};

private:
impl::DiagnosticLogger _logger;
std::string _handler_exe_path;

// Values cached on last call to SetRumContext
UUID _rum_application_id;
UUID _rum_session_id;
UUID _rum_view_id;
UUID _rum_action_id;
};

std::unique_ptr<ICrashHandler> CrashHandler::Init(
Expand Down
Loading