Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0e4003a
fix(profiling): harden stack sampler against foreign SIGSEGV handlers
vlad-scherbich Jul 1, 2026
76013f3
test(profiling): drop warmup env override; add handler-ownership intr…
vlad-scherbich Jul 2, 2026
12cae40
test(profiling): update fast-copy stat test for the startup warmup
vlad-scherbich Jul 2, 2026
2c5ea5f
fix(profiling): check SIGSEGV and SIGBUS ownership; stop sampling whe…
vlad-scherbich Jul 3, 2026
96da0aa
fix lints
vlad-scherbich Jul 6, 2026
d3b315b
docs(profiling): tie foreign-handler fix to PROF-14568 findings
vlad-scherbich Jul 6, 2026
cd95947
fix(profiling): do not stomp a foreign fault handler at sampler start
vlad-scherbich Jul 7, 2026
93a7c26
chore(profiling): drop PROF-14568 repro script from PR
vlad-scherbich Jul 7, 2026
6cf6b18
style: apply clang-format 18.1.5 to stack sampler fault-handler code
vlad-scherbich Jul 7, 2026
fcdf250
stubs(profiling): add segv_handler_installed to _stack.pyi
vlad-scherbich Jul 7, 2026
d99a702
shorten release note
vlad-scherbich Jul 7, 2026
d5248bd
test(profiling): observe fast-copy upgrade via native introspection
vlad-scherbich Jul 7, 2026
f70f110
shorten comments and improve tests
vlad-scherbich Jul 7, 2026
8caced0
fix(profiling): reorder Sampler fields to satisfy clang-tidy padding …
vlad-scherbich Jul 7, 2026
263be87
fix(profiling): validate warmup setter input and skip test when stack…
vlad-scherbich Jul 7, 2026
a73661d
Thomas and bot comments addressed
vlad-scherbich Jul 8, 2026
192efca
review: apply suggestion
KowalskiThomas Jul 9, 2026
846d798
review: apply suggestion
KowalskiThomas Jul 9, 2026
b6e1d89
review: apply suggestion
KowalskiThomas Jul 9, 2026
741dd84
review: apply suggestion
KowalskiThomas Jul 9, 2026
8371811
review: apply suggestion
KowalskiThomas Jul 9, 2026
89ed7db
review: apply suggestion
KowalskiThomas Jul 9, 2026
0156162
review: apply suggestion
KowalskiThomas Jul 9, 2026
acc7363
feat(profiling): add ProfilerStats metrics for fast-copy fallback obs…
vlad-scherbich Jul 10, 2026
51c0105
fix(profiling): preserve fast-copy metadata across stats swaps
vlad-scherbich Jul 13, 2026
5bd63f3
style(profiling): clang-format sampler.hpp
vlad-scherbich Jul 13, 2026
fd5fd49
update comment and release note
vlad-scherbich Jul 13, 2026
61b81a0
fix lint
vlad-scherbich Jul 23, 2026
98c1492
Merge branch 'main' into vlad/prof-14568-handler-ownership
vlad-scherbich Jul 23, 2026
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
Comment thread
vlad-scherbich marked this conversation as resolved.
Comment thread
vlad-scherbich marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ class ProfilerStats
// Whether fast_copy_memory (safe_memcpy) is enabled; unset until the sampler starts
std::optional<bool> fast_copy_memory_enabled;

// User opted out of fast copy (env var or set_fast_copy(false)); static per process
std::optional<bool> fast_copy_memory_user_disabled;

// Whether safe_memcpy initialized at startup; static per process
std::optional<bool> fast_copy_memory_capable;

// Sticky: fell back to syscall copy (init failure, foreign handler, etc.)
std::optional<bool> fast_copy_memory_syscall_fallback;

// Number of copy_memory errors accumulated since the last profile reset (i.e. since the last upload)
size_t copy_memory_error_count = 0;

Expand Down Expand Up @@ -69,6 +78,18 @@ class ProfilerStats
void set_fast_copy_memory_enabled(bool enabled);
std::optional<bool> get_fast_copy_memory_enabled() const;

void set_fast_copy_memory_user_disabled(bool disabled);
std::optional<bool> get_fast_copy_memory_user_disabled() const;

void set_fast_copy_memory_capable(bool capable);
std::optional<bool> get_fast_copy_memory_capable() const;

void set_fast_copy_memory_syscall_fallback(bool fallback);
std::optional<bool> get_fast_copy_memory_syscall_fallback() const;

// fast_copy_memory_* are process-static; carry them across ProfilerStats swaps.
void copy_fast_copy_metadata_from(const ProfilerStats& other);

void add_copy_memory_error_count(size_t count);
size_t get_copy_memory_error_count() const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ append_to_string(std::string& s, size_t value)
s.append(buf, ptr);
}

void
append_optional_bool(std::string& s, const char* key, const std::optional<bool>& value)
{
if (!value.has_value()) {
return;
}
s += '"';
s += key;
s += "\": ";
s += *value ? "true" : "false";
s += ',';
}

} // namespace

void
Expand Down Expand Up @@ -51,7 +64,7 @@ Datadog::ProfilerStats::reset_state()
asyncio_task_count = std::nullopt;
greenlet_count = std::nullopt;
sample_capture_cpu_time_us = 0;
// fast_copy_memory_enabled is intentionally not reset: it reflects a static configuration
// fast_copy_memory_* static fields are intentionally not reset (see setters).
}

void
Expand All @@ -66,6 +79,59 @@ Datadog::ProfilerStats::get_fast_copy_memory_enabled() const
return fast_copy_memory_enabled;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_user_disabled(bool disabled)
{
fast_copy_memory_user_disabled = disabled;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_user_disabled() const
{
return fast_copy_memory_user_disabled;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_capable(bool capable)
{
fast_copy_memory_capable = capable;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_capable() const
{
return fast_copy_memory_capable;
}

void
Datadog::ProfilerStats::set_fast_copy_memory_syscall_fallback(bool fallback)
{
fast_copy_memory_syscall_fallback = fallback;
}

std::optional<bool>
Datadog::ProfilerStats::get_fast_copy_memory_syscall_fallback() const
{
return fast_copy_memory_syscall_fallback;
}

void
Datadog::ProfilerStats::copy_fast_copy_metadata_from(const ProfilerStats& other)
{
if (auto value = other.get_fast_copy_memory_user_disabled()) {
set_fast_copy_memory_user_disabled(*value);
}
if (auto value = other.get_fast_copy_memory_capable()) {
set_fast_copy_memory_capable(*value);
}
if (auto value = other.get_fast_copy_memory_syscall_fallback()) {
set_fast_copy_memory_syscall_fallback(*value);
}
if (auto value = other.get_fast_copy_memory_enabled()) {
set_fast_copy_memory_enabled(*value);
}
}

void
Datadog::ProfilerStats::add_copy_memory_error_count(size_t count)
{
Expand Down Expand Up @@ -203,6 +269,11 @@ Datadog::ProfilerStats::get_internal_metadata_json()
internal_metadata_json += ",";
}

append_optional_bool(internal_metadata_json, "fast_copy_memory_user_disabled", fast_copy_memory_user_disabled);
append_optional_bool(internal_metadata_json, "fast_copy_memory_capable", fast_copy_memory_capable);
append_optional_bool(
internal_metadata_json, "fast_copy_memory_syscall_fallback", fast_copy_memory_syscall_fallback);

auto maybe_heap_tracker_count = get_heap_tracker_size();
if (maybe_heap_tracker_count) {
internal_metadata_json += R"("heap_tracker_count": )";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ Datadog::UploaderBuilder::build()
// Swap the ProfilerStats (which replaces the one being written to with an empty state).
// We do this first as we still want to reset ProfilerStats if the serialization fails.
std::swap(stats, borrowed.stats());
borrowed.stats().copy_fast_copy_metadata_from(stats);

// Try to encode the Profile (which will also reset it)
encoded = ddog_prof_Profile_serialize(&borrowed.profile(), nullptr, nullptr);
Expand Down
12 changes: 12 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def set_interval(new_interval: float) -> None: ...
# Memory copy strategy
def set_fast_copy(enabled: bool) -> None: ...
def is_safe_copy_failed() -> bool: ...
def fast_copy_memory_active() -> bool: ... # test introspection: is safe_memcpy active?

# _set_fast_copy_warmup_seconds is test-only; accessed via _stack (import * skips it).

def uninstall_segv_handler() -> None: ...
def reinstall_segv_handler() -> None:
"""Reinstall SIGSEGV/SIGBUS handlers after another component overwrites them.
Expand All @@ -52,6 +56,14 @@ def reinstall_segv_handler() -> None:
"""
...

def segv_handler_installed() -> bool:
"""Return True if our handler is the installed disposition for SIGSEGV and SIGBUS.

Primarily test introspection: it queries the live disposition via sigaction(2)
for both signals on every call, so it is not free. Do not call it on hot paths.
"""
...
Comment thread
vlad-scherbich marked this conversation as resolved.

# Pause/resume sampling
def pause_sampling() -> bool | None:
"""Pause the sampling thread and wait for any in-flight sample to complete.
Expand Down
9 changes: 9 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/_stack.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ def set_interval(new_interval: float) -> None: ...
# Memory copy strategy
def set_fast_copy(enabled: bool) -> None: ...
def is_safe_copy_failed() -> bool: ...
def fast_copy_memory_active() -> bool: ... # test introspection: is safe_memcpy active?
def _set_fast_copy_warmup_seconds(seconds: float) -> None: ... # test-only; before start
def segv_handler_installed() -> bool:
"""Return True if our handler is the installed disposition for SIGSEGV and SIGBUS.

Primarily test introspection: it queries the live disposition via sigaction(2)
for both signals on every call, so it is costly. Do not call it on hot paths.
"""
...

# span <-> profile association
def link_span(
Expand Down
Comment thread
vlad-scherbich marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ init_segv_catcher();
void
uninstall_segv_handler();

// Returns true only if our signal handler owns both SIGSEGV and SIGBUS; false on any error.
bool
segv_handler_installed();

#if defined PL_LINUX
ssize_t
safe_memcpy_wrapper(pid_t,
Expand Down
15 changes: 13 additions & 2 deletions ddtrace/internal/datadog/profiling/stack/echion/echion/vm.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,19 @@ inline kern_return_t (*safe_copy)(vm_map_read_t,
// Whether safe_copy is currently set to the memcpy-based wrapper.
inline bool fast_copy_active = false;

// Whether init_segv_catcher succeeded at constructor time. Persists even if
// fast_copy_active is later toggled off by set_fast_copy_enabled.
// User opted out via _DD_PROFILING_STACK_FAST_COPY or set_fast_copy(false).
inline bool fast_copy_user_disabled = false;

// Sticky: fell back to syscall copy (init failure, foreign handler, warmup miss).
inline bool fast_copy_syscall_fallback = false;

inline void
mark_fast_copy_syscall_fallback()
{
fast_copy_syscall_fallback = true;
}

// Set at init; survives toggling fast_copy_active.
inline bool safe_memcpy_initialized = false;

#if defined PL_LINUX
Expand Down
13 changes: 10 additions & 3 deletions ddtrace/internal/datadog/profiling/stack/include/sampler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ class Sampler
// The mutex + condition variable pair is used to avoid the "lost wake-up" race condition
// where stop() could miss the notification and hang forever (or until timeout).
std::atomic<bool> thread_running{ false };
std::mutex thread_exit_mutex;
std::condition_variable thread_exit_cv;

// Whether the sampler is currently active. Unlike thread_running (which tracks
// the thread's actual lifecycle) this is set synchronously in start/stop, so
// it is not subject to the sampling-thread startup race.
Expand All @@ -56,6 +53,8 @@ class Sampler
// prefork reads this to decide whether to restart the sampler after fork,
// ensuring a sampler that stopped due to an error is not silently restarted.
std::atomic<bool> sampler_active_{ false };
std::mutex thread_exit_mutex;
std::condition_variable thread_exit_cv;

// Pause synchronization — allows the faulthandler wrapper to temporarily
// suspend sampling so signal handlers can be safely swapped without racing
Expand Down Expand Up @@ -101,6 +100,9 @@ class Sampler
// Percentile (0..1) used for p_stable; configurable, default p95.
double p_stable_percentile_frac = 0.95;

// Fast-copy startup warmup in seconds.
double fast_copy_warmup_seconds = 15.0;

// Rolling window duration in seconds; controls the ring buffer capacity.
uint32_t p_stable_window_s = 600;

Expand Down Expand Up @@ -164,6 +166,8 @@ class Sampler
// Set the percentile (0–100) used to compute p_stable from the rolling window.
void set_p_stable_percentile(double percentile) { p_stable_percentile_frac = percentile / 100.0; }

void set_fast_copy_warmup_seconds(double value) { fast_copy_warmup_seconds = value; }

// Delegates to the StackRenderer to clear its caches after fork
void postfork_child();

Expand All @@ -178,4 +182,7 @@ class Sampler
bool restart_after_fork();
};

void
seed_fast_copy_profiler_stats();

} // namespace Datadog
18 changes: 18 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/src/echion/danger.cc
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,24 @@ init_segv_catcher()
return 0;
}

bool
segv_handler_installed()
{
// Recovery needs our handler to own BOTH SIGSEGV and SIGBUS
// (a copy fault can arrive as either); anything else means we can't recover.
const int signals[] = { SIGSEGV, SIGBUS };
for (int signo : signals) {
struct sigaction current;
if (sigaction(signo, nullptr, &current) != 0) {
return false;
}
if (current.sa_sigaction != segv_handler || (current.sa_flags & SA_SIGINFO) == 0) {
return false;
}
}
return true;
}

void
uninstall_segv_handler()
{
Expand Down
5 changes: 5 additions & 0 deletions ddtrace/internal/datadog/profiling/stack/src/echion/vm.cc
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ init_safe_copy()
// Honor the fast-copy opt-out: when disabled via env var, skip installing
// the SIGSEGV/SIGBUS handlers and alt stack entirely.
if (fast_copy_env_disabled()) {
fast_copy_user_disabled = true;
if (process_vm_readv_available) {
safe_copy = process_vm_readv;
} else {
Expand All @@ -60,6 +61,7 @@ init_safe_copy()
fprintf(stderr, "Failed to initialize segv catcher. Trying process_vm_readv.\n");
if (process_vm_readv_available) {
safe_copy = process_vm_readv;
mark_fast_copy_syscall_fallback();
} else {
fprintf(stderr, "Failed to initialize safe copy interface\n");
failed_safe_copy = true;
Expand All @@ -72,6 +74,7 @@ init_safe_copy()
{
// Honor the fast-copy opt-out: skip installing signal handlers when disabled.
if (fast_copy_env_disabled()) {
fast_copy_user_disabled = true;
return;
}

Expand All @@ -84,6 +87,7 @@ init_safe_copy()

// std::cerr might not be fully initialized at constructor time.
fprintf(stderr, "Failed to initialize segv catcher. Using mach_vm_read_overwrite instead.\n");
mark_fast_copy_syscall_fallback();
}
#endif // PL_DARWIN

Expand All @@ -99,6 +103,7 @@ set_fast_copy_enabled(bool enabled)
}
fprintf(stderr,
"Warning: fast copy requested but safe_memcpy was not initialized; falling back to process_vm_readv\n");
mark_fast_copy_syscall_fallback();

// Fall through to process_vm_readv.
}
Expand Down
Loading
Loading