datadog 2.7.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/ext/datadog_profiling_native_extension/clock_id.h +2 -2
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +64 -54
  5. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  6. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.h +1 -1
  7. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +16 -16
  8. data/ext/datadog_profiling_native_extension/collectors_stack.c +7 -7
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +259 -132
  10. data/ext/datadog_profiling_native_extension/extconf.rb +0 -8
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +11 -89
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +1 -1
  13. data/ext/datadog_profiling_native_extension/http_transport.c +4 -4
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -1
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +3 -1
  16. data/ext/datadog_profiling_native_extension/profiling.c +10 -8
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +8 -8
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +54 -88
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +1 -1
  20. data/ext/datadog_profiling_native_extension/time_helpers.h +1 -1
  21. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.c +47 -0
  22. data/ext/datadog_profiling_native_extension/unsafe_api_calls_check.h +31 -0
  23. data/ext/libdatadog_api/crashtracker.c +3 -0
  24. data/ext/libdatadog_extconf_helpers.rb +1 -1
  25. data/lib/datadog/appsec/assets/waf_rules/recommended.json +355 -157
  26. data/lib/datadog/appsec/assets/waf_rules/strict.json +62 -32
  27. data/lib/datadog/appsec/component.rb +1 -8
  28. data/lib/datadog/appsec/context.rb +54 -0
  29. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +73 -0
  30. data/lib/datadog/appsec/contrib/active_record/integration.rb +41 -0
  31. data/lib/datadog/appsec/contrib/active_record/patcher.rb +53 -0
  32. data/lib/datadog/appsec/contrib/devise/patcher/authenticatable_patch.rb +6 -6
  33. data/lib/datadog/appsec/contrib/devise/patcher/registration_controller_patch.rb +4 -4
  34. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +19 -28
  35. data/lib/datadog/appsec/contrib/graphql/reactive/multiplex.rb +5 -5
  36. data/lib/datadog/appsec/contrib/rack/gateway/response.rb +3 -3
  37. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +64 -96
  38. data/lib/datadog/appsec/contrib/rack/reactive/request.rb +10 -10
  39. data/lib/datadog/appsec/contrib/rack/reactive/request_body.rb +5 -5
  40. data/lib/datadog/appsec/contrib/rack/reactive/response.rb +6 -6
  41. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +10 -11
  42. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +43 -49
  43. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +21 -32
  44. data/lib/datadog/appsec/contrib/rails/patcher.rb +1 -1
  45. data/lib/datadog/appsec/contrib/rails/reactive/action.rb +6 -6
  46. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +41 -63
  47. data/lib/datadog/appsec/contrib/sinatra/patcher.rb +2 -2
  48. data/lib/datadog/appsec/contrib/sinatra/reactive/routed.rb +5 -5
  49. data/lib/datadog/appsec/event.rb +6 -6
  50. data/lib/datadog/appsec/ext.rb +3 -1
  51. data/lib/datadog/appsec/monitor/gateway/watcher.rb +22 -32
  52. data/lib/datadog/appsec/monitor/reactive/set_user.rb +5 -5
  53. data/lib/datadog/appsec/processor/context.rb +2 -2
  54. data/lib/datadog/appsec/processor/rule_loader.rb +0 -3
  55. data/lib/datadog/appsec/remote.rb +1 -3
  56. data/lib/datadog/appsec/response.rb +7 -11
  57. data/lib/datadog/appsec.rb +6 -5
  58. data/lib/datadog/auto_instrument.rb +3 -0
  59. data/lib/datadog/core/configuration/agent_settings_resolver.rb +39 -11
  60. data/lib/datadog/core/configuration/components.rb +20 -2
  61. data/lib/datadog/core/configuration/settings.rb +10 -0
  62. data/lib/datadog/core/configuration.rb +10 -2
  63. data/lib/datadog/{tracing → core}/contrib/rails/utils.rb +1 -3
  64. data/lib/datadog/core/crashtracking/component.rb +1 -3
  65. data/lib/datadog/core/remote/client/capabilities.rb +6 -0
  66. data/lib/datadog/core/remote/client.rb +65 -59
  67. data/lib/datadog/core/telemetry/component.rb +9 -3
  68. data/lib/datadog/core/telemetry/event.rb +87 -3
  69. data/lib/datadog/core/telemetry/ext.rb +1 -0
  70. data/lib/datadog/core/telemetry/logging.rb +2 -2
  71. data/lib/datadog/core/telemetry/metric.rb +22 -0
  72. data/lib/datadog/core/telemetry/worker.rb +33 -0
  73. data/lib/datadog/di/base.rb +115 -0
  74. data/lib/datadog/di/code_tracker.rb +11 -7
  75. data/lib/datadog/di/component.rb +21 -11
  76. data/lib/datadog/di/configuration/settings.rb +11 -1
  77. data/lib/datadog/di/contrib/active_record.rb +1 -0
  78. data/lib/datadog/di/contrib/railtie.rb +15 -0
  79. data/lib/datadog/di/contrib.rb +26 -0
  80. data/lib/datadog/di/error.rb +5 -0
  81. data/lib/datadog/di/instrumenter.rb +111 -20
  82. data/lib/datadog/di/preload.rb +18 -0
  83. data/lib/datadog/di/probe.rb +11 -1
  84. data/lib/datadog/di/probe_builder.rb +1 -0
  85. data/lib/datadog/di/probe_manager.rb +8 -5
  86. data/lib/datadog/di/probe_notification_builder.rb +27 -7
  87. data/lib/datadog/di/probe_notifier_worker.rb +5 -6
  88. data/lib/datadog/di/remote.rb +124 -0
  89. data/lib/datadog/di/serializer.rb +14 -7
  90. data/lib/datadog/di/transport.rb +3 -5
  91. data/lib/datadog/di/utils.rb +7 -0
  92. data/lib/datadog/di.rb +23 -62
  93. data/lib/datadog/kit/appsec/events.rb +3 -3
  94. data/lib/datadog/kit/identity.rb +4 -4
  95. data/lib/datadog/profiling/component.rb +59 -69
  96. data/lib/datadog/profiling/http_transport.rb +1 -26
  97. data/lib/datadog/tracing/configuration/settings.rb +4 -8
  98. data/lib/datadog/tracing/contrib/action_cable/integration.rb +5 -2
  99. data/lib/datadog/tracing/contrib/action_mailer/integration.rb +6 -2
  100. data/lib/datadog/tracing/contrib/action_pack/integration.rb +5 -2
  101. data/lib/datadog/tracing/contrib/action_view/integration.rb +5 -2
  102. data/lib/datadog/tracing/contrib/active_job/integration.rb +5 -2
  103. data/lib/datadog/tracing/contrib/active_record/integration.rb +6 -2
  104. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +3 -1
  105. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +3 -1
  106. data/lib/datadog/tracing/contrib/active_support/cache/redis.rb +16 -4
  107. data/lib/datadog/tracing/contrib/active_support/configuration/settings.rb +10 -0
  108. data/lib/datadog/tracing/contrib/active_support/integration.rb +5 -2
  109. data/lib/datadog/tracing/contrib/auto_instrument.rb +2 -2
  110. data/lib/datadog/tracing/contrib/aws/integration.rb +3 -0
  111. data/lib/datadog/tracing/contrib/concurrent_ruby/integration.rb +3 -0
  112. data/lib/datadog/tracing/contrib/elasticsearch/configuration/settings.rb +4 -0
  113. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +6 -1
  114. data/lib/datadog/tracing/contrib/httprb/integration.rb +3 -0
  115. data/lib/datadog/tracing/contrib/kafka/integration.rb +3 -0
  116. data/lib/datadog/tracing/contrib/mongodb/integration.rb +3 -0
  117. data/lib/datadog/tracing/contrib/opensearch/integration.rb +3 -0
  118. data/lib/datadog/tracing/contrib/presto/integration.rb +3 -0
  119. data/lib/datadog/tracing/contrib/rack/integration.rb +2 -2
  120. data/lib/datadog/tracing/contrib/rails/framework.rb +2 -2
  121. data/lib/datadog/tracing/contrib/rails/patcher.rb +1 -1
  122. data/lib/datadog/tracing/contrib/rest_client/integration.rb +3 -0
  123. data/lib/datadog/tracing/span.rb +12 -4
  124. data/lib/datadog/tracing/span_event.rb +123 -3
  125. data/lib/datadog/tracing/span_operation.rb +6 -0
  126. data/lib/datadog/tracing/transport/serializable_trace.rb +24 -6
  127. data/lib/datadog/version.rb +2 -2
  128. data/lib/datadog.rb +3 -0
  129. metadata +30 -17
  130. data/lib/datadog/appsec/processor/actions.rb +0 -49
  131. data/lib/datadog/appsec/reactive/operation.rb +0 -68
  132. data/lib/datadog/appsec/scope.rb +0 -58
  133. data/lib/datadog/core/crashtracking/agent_base_url.rb +0 -21
@@ -9,6 +9,7 @@
9
9
  #include "private_vm_api_access.h"
10
10
  #include "stack_recorder.h"
11
11
  #include "time_helpers.h"
12
+ #include "unsafe_api_calls_check.h"
12
13
 
13
14
  // Used to trigger sampling of threads, based on external "events", such as:
14
15
  // * periodic timer for cpu-time and wall-time
@@ -112,14 +113,14 @@ static uint32_t global_waiting_for_gvl_threshold_ns = MILLIS_AS_NS(10);
112
113
  typedef enum { OTEL_CONTEXT_ENABLED_FALSE, OTEL_CONTEXT_ENABLED_ONLY, OTEL_CONTEXT_ENABLED_BOTH } otel_context_enabled;
113
114
 
114
115
  // Contains state for a single ThreadContext instance
115
- struct thread_context_collector_state {
116
+ typedef struct {
116
117
  // Note: Places in this file that usually need to be changed when this struct is changed are tagged with
117
118
  // "Update this when modifying state struct"
118
119
 
119
120
  // Required by Datadog::Profiling::Collectors::Stack as a scratch buffer during sampling
120
121
  ddog_prof_Location *locations;
121
122
  uint16_t max_frames;
122
- // Hashmap <Thread Object, struct per_thread_context>
123
+ // Hashmap <Thread Object, per_thread_context>
123
124
  st_table *hash_map_per_thread_context;
124
125
  // Datadog::Profiling::StackRecorder instance
125
126
  VALUE recorder_instance;
@@ -162,10 +163,10 @@ struct thread_context_collector_state {
162
163
  long wall_time_at_previous_gc_ns; // Will be INVALID_TIME unless there's accumulated time above
163
164
  long wall_time_at_last_flushed_gc_event_ns; // Starts at 0 and then will always be valid
164
165
  } gc_tracking;
165
- };
166
+ } thread_context_collector_state;
166
167
 
167
168
  // Tracks per-thread state
168
- struct per_thread_context {
169
+ typedef struct {
169
170
  sampling_buffer *sampling_buffer;
170
171
  char thread_id[THREAD_ID_LIMIT_CHARS];
171
172
  ddog_CharSlice thread_id_char_slice;
@@ -181,21 +182,21 @@ struct per_thread_context {
181
182
  long cpu_time_at_start_ns;
182
183
  long wall_time_at_start_ns;
183
184
  } gc_tracking;
184
- };
185
+ } per_thread_context;
185
186
 
186
187
  // Used to correlate profiles with traces
187
- struct trace_identifiers {
188
+ typedef struct {
188
189
  bool valid;
189
190
  uint64_t local_root_span_id;
190
191
  uint64_t span_id;
191
192
  VALUE trace_endpoint;
192
- };
193
+ } trace_identifiers;
193
194
 
194
- struct otel_span {
195
+ typedef struct {
195
196
  VALUE span;
196
197
  VALUE span_id;
197
198
  VALUE trace_id;
198
- };
199
+ } otel_span;
199
200
 
200
201
  static void thread_context_collector_typed_data_mark(void *state_ptr);
201
202
  static void thread_context_collector_typed_data_free(void *state_ptr);
@@ -203,70 +204,77 @@ static int hash_map_per_thread_context_mark(st_data_t key_thread, st_data_t _val
203
204
  static int hash_map_per_thread_context_free_values(st_data_t _thread, st_data_t value_per_thread_context, st_data_t _argument);
204
205
  static VALUE _native_new(VALUE klass);
205
206
  static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
206
- static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE profiler_overhead_stack_thread);
207
+ static VALUE _native_sample(VALUE self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception);
207
208
  static VALUE _native_on_gc_start(VALUE self, VALUE collector_instance);
208
209
  static VALUE _native_on_gc_finish(VALUE self, VALUE collector_instance);
209
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state);
210
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state, VALUE allow_exception);
210
211
  static void update_metrics_and_sample(
211
- struct thread_context_collector_state *state,
212
+ thread_context_collector_state *state,
212
213
  VALUE thread_being_sampled,
213
214
  VALUE stack_from_thread,
214
- struct per_thread_context *thread_context,
215
+ per_thread_context *thread_context,
215
216
  sampling_buffer* sampling_buffer,
216
217
  long current_cpu_time_ns,
217
218
  long current_monotonic_wall_time_ns
218
219
  );
219
220
  static void trigger_sample_for_thread(
220
- struct thread_context_collector_state *state,
221
+ thread_context_collector_state *state,
221
222
  VALUE thread,
222
223
  VALUE stack_from_thread,
223
- struct per_thread_context *thread_context,
224
+ per_thread_context *thread_context,
224
225
  sampling_buffer* sampling_buffer,
225
226
  sample_values values,
226
227
  long current_monotonic_wall_time_ns,
227
228
  ddog_CharSlice *ruby_vm_type,
228
229
  ddog_CharSlice *class_name,
229
- bool is_gvl_waiting_state
230
+ bool is_gvl_waiting_state,
231
+ bool is_safe_to_allocate_objects
230
232
  );
231
233
  static VALUE _native_thread_list(VALUE self);
232
- static struct per_thread_context *get_or_create_context_for(VALUE thread, struct thread_context_collector_state *state);
233
- static struct per_thread_context *get_context_for(VALUE thread, struct thread_context_collector_state *state);
234
- static void initialize_context(VALUE thread, struct per_thread_context *thread_context, struct thread_context_collector_state *state);
235
- static void free_context(struct per_thread_context* thread_context);
234
+ static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state);
235
+ static per_thread_context *get_context_for(VALUE thread, thread_context_collector_state *state);
236
+ static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state);
237
+ static void free_context(per_thread_context* thread_context);
236
238
  static VALUE _native_inspect(VALUE self, VALUE collector_instance);
237
- static VALUE per_thread_context_st_table_as_ruby_hash(struct thread_context_collector_state *state);
239
+ static VALUE per_thread_context_st_table_as_ruby_hash(thread_context_collector_state *state);
238
240
  static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value_context, st_data_t result_hash);
239
- static VALUE stats_as_ruby_hash(struct thread_context_collector_state *state);
240
- static VALUE gc_tracking_as_ruby_hash(struct thread_context_collector_state *state);
241
- static void remove_context_for_dead_threads(struct thread_context_collector_state *state);
241
+ static VALUE stats_as_ruby_hash(thread_context_collector_state *state);
242
+ static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state);
243
+ static void remove_context_for_dead_threads(thread_context_collector_state *state);
242
244
  static int remove_if_dead_thread(st_data_t key_thread, st_data_t value_context, st_data_t _argument);
243
245
  static VALUE _native_per_thread_context(VALUE self, VALUE collector_instance);
244
246
  static long update_time_since_previous_sample(long *time_at_previous_sample_ns, long current_time_ns, long gc_start_time_ns, bool is_wall_time);
245
- static long cpu_time_now_ns(struct per_thread_context *thread_context);
247
+ static long cpu_time_now_ns(per_thread_context *thread_context);
246
248
  static long thread_id_for(VALUE thread);
247
249
  static VALUE _native_stats(VALUE self, VALUE collector_instance);
248
250
  static VALUE _native_gc_tracking(VALUE self, VALUE collector_instance);
249
- static void trace_identifiers_for(struct thread_context_collector_state *state, VALUE thread, struct trace_identifiers *trace_identifiers_result);
251
+ static void trace_identifiers_for(
252
+ thread_context_collector_state *state,
253
+ VALUE thread,
254
+ trace_identifiers *trace_identifiers_result,
255
+ bool is_safe_to_allocate_objects
256
+ );
250
257
  static bool should_collect_resource(VALUE root_span);
251
258
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance);
252
- static VALUE thread_list(struct thread_context_collector_state *state);
259
+ static VALUE thread_list(thread_context_collector_state *state);
253
260
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object);
254
261
  static VALUE _native_new_empty_thread(VALUE self);
255
262
  static ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type);
256
263
  static void ddtrace_otel_trace_identifiers_for(
257
- struct thread_context_collector_state *state,
264
+ thread_context_collector_state *state,
258
265
  VALUE *active_trace,
259
266
  VALUE *root_span,
260
267
  VALUE *numeric_span_id,
261
268
  VALUE active_span,
262
- VALUE otel_values
269
+ VALUE otel_values,
270
+ bool is_safe_to_allocate_objects
263
271
  );
264
272
  static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples);
265
273
  static bool handle_gvl_waiting(
266
- struct thread_context_collector_state *state,
274
+ thread_context_collector_state *state,
267
275
  VALUE thread_being_sampled,
268
276
  VALUE stack_from_thread,
269
- struct per_thread_context *thread_context,
277
+ per_thread_context *thread_context,
270
278
  sampling_buffer* sampling_buffer,
271
279
  long current_cpu_time_ns
272
280
  );
@@ -276,12 +284,14 @@ static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread);
276
284
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread);
277
285
  static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns);
278
286
  static void otel_without_ddtrace_trace_identifiers_for(
279
- struct thread_context_collector_state *state,
287
+ thread_context_collector_state *state,
280
288
  VALUE thread,
281
- struct trace_identifiers *trace_identifiers_result
289
+ trace_identifiers *trace_identifiers_result,
290
+ bool is_safe_to_allocate_objects
282
291
  );
283
- static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
292
+ static otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key);
284
293
  static uint64_t otel_span_id_to_uint(VALUE otel_span_id);
294
+ static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key);
285
295
 
286
296
  void collectors_thread_context_init(VALUE profiling_module) {
287
297
  VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
@@ -302,11 +312,11 @@ void collectors_thread_context_init(VALUE profiling_module) {
302
312
  rb_define_singleton_method(collectors_thread_context_class, "_native_initialize", _native_initialize, -1);
303
313
  rb_define_singleton_method(collectors_thread_context_class, "_native_inspect", _native_inspect, 1);
304
314
  rb_define_singleton_method(collectors_thread_context_class, "_native_reset_after_fork", _native_reset_after_fork, 1);
305
- rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 2);
315
+ rb_define_singleton_method(testing_module, "_native_sample", _native_sample, 3);
306
316
  rb_define_singleton_method(testing_module, "_native_sample_allocation", _native_sample_allocation, 3);
307
317
  rb_define_singleton_method(testing_module, "_native_on_gc_start", _native_on_gc_start, 1);
308
318
  rb_define_singleton_method(testing_module, "_native_on_gc_finish", _native_on_gc_finish, 1);
309
- rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 2);
319
+ rb_define_singleton_method(testing_module, "_native_sample_after_gc", _native_sample_after_gc, 3);
310
320
  rb_define_singleton_method(testing_module, "_native_thread_list", _native_thread_list, 0);
311
321
  rb_define_singleton_method(testing_module, "_native_per_thread_context", _native_per_thread_context, 1);
312
322
  rb_define_singleton_method(testing_module, "_native_stats", _native_stats, 1);
@@ -347,7 +357,7 @@ void collectors_thread_context_init(VALUE profiling_module) {
347
357
  gc_profiling_init();
348
358
  }
349
359
 
350
- // This structure is used to define a Ruby object that stores a pointer to a struct thread_context_collector_state
360
+ // This structure is used to define a Ruby object that stores a pointer to a thread_context_collector_state
351
361
  // See also https://github.com/ruby/ruby/blob/master/doc/extension.rdoc for how this works
352
362
  static const rb_data_type_t thread_context_collector_typed_data = {
353
363
  .wrap_struct_name = "Datadog::Profiling::Collectors::ThreadContext",
@@ -363,7 +373,7 @@ static const rb_data_type_t thread_context_collector_typed_data = {
363
373
  // This function is called by the Ruby GC to give us a chance to mark any Ruby objects that we're holding on to,
364
374
  // so that they don't get garbage collected
365
375
  static void thread_context_collector_typed_data_mark(void *state_ptr) {
366
- struct thread_context_collector_state *state = (struct thread_context_collector_state *) state_ptr;
376
+ thread_context_collector_state *state = (thread_context_collector_state *) state_ptr;
367
377
 
368
378
  // Update this when modifying state struct
369
379
  rb_gc_mark(state->recorder_instance);
@@ -374,7 +384,7 @@ static void thread_context_collector_typed_data_mark(void *state_ptr) {
374
384
  }
375
385
 
376
386
  static void thread_context_collector_typed_data_free(void *state_ptr) {
377
- struct thread_context_collector_state *state = (struct thread_context_collector_state *) state_ptr;
387
+ thread_context_collector_state *state = (thread_context_collector_state *) state_ptr;
378
388
 
379
389
  // Update this when modifying state struct
380
390
 
@@ -399,13 +409,13 @@ static int hash_map_per_thread_context_mark(st_data_t key_thread, DDTRACE_UNUSED
399
409
 
400
410
  // Used to clear each of the per_thread_contexts inside the hash_map_per_thread_context
401
411
  static int hash_map_per_thread_context_free_values(DDTRACE_UNUSED st_data_t _thread, st_data_t value_per_thread_context, DDTRACE_UNUSED st_data_t _argument) {
402
- struct per_thread_context *thread_context = (struct per_thread_context*) value_per_thread_context;
412
+ per_thread_context *thread_context = (per_thread_context*) value_per_thread_context;
403
413
  free_context(thread_context);
404
414
  return ST_CONTINUE;
405
415
  }
406
416
 
407
417
  static VALUE _native_new(VALUE klass) {
408
- struct thread_context_collector_state *state = ruby_xcalloc(1, sizeof(struct thread_context_collector_state));
418
+ thread_context_collector_state *state = ruby_xcalloc(1, sizeof(thread_context_collector_state));
409
419
 
410
420
  // Note: Any exceptions raised from this note until the TypedData_Wrap_Struct call will lead to the state memory
411
421
  // being leaked.
@@ -461,8 +471,8 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
461
471
  ENFORCE_BOOLEAN(timeline_enabled);
462
472
  ENFORCE_TYPE(waiting_for_gvl_threshold_ns, T_FIXNUM);
463
473
 
464
- struct thread_context_collector_state *state;
465
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
474
+ thread_context_collector_state *state;
475
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
466
476
 
467
477
  // Update this when modifying state struct
468
478
  state->max_frames = sampling_buffer_check_max_frames(NUM2INT(max_frames));
@@ -496,40 +506,63 @@ static VALUE _native_initialize(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _sel
496
506
 
497
507
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
498
508
  // It SHOULD NOT be used for other purposes.
499
- static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE profiler_overhead_stack_thread) {
509
+ static VALUE _native_sample(DDTRACE_UNUSED VALUE _self, VALUE collector_instance, VALUE profiler_overhead_stack_thread, VALUE allow_exception) {
510
+ ENFORCE_BOOLEAN(allow_exception);
511
+
500
512
  if (!is_thread_alive(profiler_overhead_stack_thread)) rb_raise(rb_eArgError, "Unexpected: profiler_overhead_stack_thread is not alive");
501
513
 
514
+ if (allow_exception == Qfalse) debug_enter_unsafe_context();
515
+
502
516
  thread_context_collector_sample(collector_instance, monotonic_wall_time_now_ns(RAISE_ON_FAILURE), profiler_overhead_stack_thread);
517
+
518
+ if (allow_exception == Qfalse) debug_leave_unsafe_context();
519
+
503
520
  return Qtrue;
504
521
  }
505
522
 
506
523
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
507
524
  // It SHOULD NOT be used for other purposes.
508
525
  static VALUE _native_on_gc_start(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
526
+ debug_enter_unsafe_context();
527
+
509
528
  thread_context_collector_on_gc_start(collector_instance);
529
+
530
+ debug_leave_unsafe_context();
531
+
510
532
  return Qtrue;
511
533
  }
512
534
 
513
535
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
514
536
  // It SHOULD NOT be used for other purposes.
515
537
  static VALUE _native_on_gc_finish(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
538
+ debug_enter_unsafe_context();
539
+
516
540
  (void) !thread_context_collector_on_gc_finish(collector_instance);
541
+
542
+ debug_leave_unsafe_context();
543
+
517
544
  return Qtrue;
518
545
  }
519
546
 
520
547
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
521
548
  // It SHOULD NOT be used for other purposes.
522
- static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state) {
549
+ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE reset_monotonic_to_system_state, VALUE allow_exception) {
523
550
  ENFORCE_BOOLEAN(reset_monotonic_to_system_state);
551
+ ENFORCE_BOOLEAN(allow_exception);
524
552
 
525
- struct thread_context_collector_state *state;
526
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
553
+ thread_context_collector_state *state;
554
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
527
555
 
528
556
  if (reset_monotonic_to_system_state == Qtrue) {
529
557
  state->time_converter_state = (monotonic_to_system_epoch_state) MONOTONIC_TO_SYSTEM_EPOCH_INITIALIZER;
530
558
  }
531
559
 
560
+ if (allow_exception == Qfalse) debug_enter_unsafe_context();
561
+
532
562
  thread_context_collector_sample_after_gc(collector_instance);
563
+
564
+ if (allow_exception == Qfalse) debug_leave_unsafe_context();
565
+
533
566
  return Qtrue;
534
567
  }
535
568
 
@@ -544,11 +577,11 @@ static VALUE _native_sample_after_gc(DDTRACE_UNUSED VALUE self, VALUE collector_
544
577
  // The `profiler_overhead_stack_thread` is used to attribute the profiler overhead to a stack borrowed from a different thread
545
578
  // (belonging to ddtrace), so that the overhead is visible in the profile rather than blamed on user code.
546
579
  void thread_context_collector_sample(VALUE self_instance, long current_monotonic_wall_time_ns, VALUE profiler_overhead_stack_thread) {
547
- struct thread_context_collector_state *state;
548
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
580
+ thread_context_collector_state *state;
581
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
549
582
 
550
583
  VALUE current_thread = rb_thread_current();
551
- struct per_thread_context *current_thread_context = get_or_create_context_for(current_thread, state);
584
+ per_thread_context *current_thread_context = get_or_create_context_for(current_thread, state);
552
585
  long cpu_time_at_sample_start_for_current_thread = cpu_time_now_ns(current_thread_context);
553
586
 
554
587
  VALUE threads = thread_list(state);
@@ -556,7 +589,7 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
556
589
  const long thread_count = RARRAY_LEN(threads);
557
590
  for (long i = 0; i < thread_count; i++) {
558
591
  VALUE thread = RARRAY_AREF(threads, i);
559
- struct per_thread_context *thread_context = get_or_create_context_for(thread, state);
592
+ per_thread_context *thread_context = get_or_create_context_for(thread, state);
560
593
 
561
594
  // We account for cpu-time for the current thread in a different way -- we use the cpu-time at sampling start, to avoid
562
595
  // blaming the time the profiler took on whatever's running on the thread right now
@@ -592,10 +625,10 @@ void thread_context_collector_sample(VALUE self_instance, long current_monotonic
592
625
  }
593
626
 
594
627
  static void update_metrics_and_sample(
595
- struct thread_context_collector_state *state,
628
+ thread_context_collector_state *state,
596
629
  VALUE thread_being_sampled,
597
630
  VALUE stack_from_thread, // This can be different when attributing profiler overhead using a different stack
598
- struct per_thread_context *thread_context,
631
+ per_thread_context *thread_context,
599
632
  sampling_buffer* sampling_buffer,
600
633
  long current_cpu_time_ns,
601
634
  long current_monotonic_wall_time_ns
@@ -647,7 +680,8 @@ static void update_metrics_and_sample(
647
680
  current_monotonic_wall_time_ns,
648
681
  NULL,
649
682
  NULL,
650
- is_gvl_waiting_state
683
+ is_gvl_waiting_state,
684
+ /* is_safe_to_allocate_objects: */ true // We called from a context that's safe to run any regular code, including allocations
651
685
  );
652
686
  }
653
687
 
@@ -662,12 +696,12 @@ static void update_metrics_and_sample(
662
696
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
663
697
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
664
698
  void thread_context_collector_on_gc_start(VALUE self_instance) {
665
- struct thread_context_collector_state *state;
699
+ thread_context_collector_state *state;
666
700
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return;
667
701
  // This should never fail the the above check passes
668
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
702
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
669
703
 
670
- struct per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
704
+ per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
671
705
 
672
706
  // If there was no previously-existing context for this thread, we won't allocate one (see safety). For now we just drop
673
707
  // the GC sample, under the assumption that "a thread that is so new that we never sampled it even once before it triggers
@@ -695,12 +729,12 @@ void thread_context_collector_on_gc_start(VALUE self_instance) {
695
729
  // Assumption 2: This function is called from the main Ractor (if Ruby has support for Ractors).
696
730
  __attribute__((warn_unused_result))
697
731
  bool thread_context_collector_on_gc_finish(VALUE self_instance) {
698
- struct thread_context_collector_state *state;
732
+ thread_context_collector_state *state;
699
733
  if (!rb_typeddata_is_kind_of(self_instance, &thread_context_collector_typed_data)) return false;
700
734
  // This should never fail the the above check passes
701
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
735
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
702
736
 
703
- struct per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
737
+ per_thread_context *thread_context = get_context_for(rb_thread_current(), state);
704
738
 
705
739
  // If there was no previously-existing context for this thread, we won't allocate one (see safety). We keep a metric for
706
740
  // how often this happens -- see on_gc_start.
@@ -773,8 +807,8 @@ bool thread_context_collector_on_gc_finish(VALUE self_instance) {
773
807
  // Assumption 3: Unlike `on_gc_start` and `on_gc_finish`, this method is allowed to allocate memory as needed.
774
808
  // Assumption 4: This function is called from the main Ractor (if Ruby has support for Ractors).
775
809
  VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
776
- struct thread_context_collector_state *state;
777
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
810
+ thread_context_collector_state *state;
811
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
778
812
 
779
813
  if (state->gc_tracking.wall_time_at_previous_gc_ns == INVALID_TIME) {
780
814
  rb_raise(rb_eRuntimeError, "BUG: Unexpected call to sample_after_gc without valid GC information available");
@@ -823,17 +857,20 @@ VALUE thread_context_collector_sample_after_gc(VALUE self_instance) {
823
857
  }
824
858
 
825
859
  static void trigger_sample_for_thread(
826
- struct thread_context_collector_state *state,
860
+ thread_context_collector_state *state,
827
861
  VALUE thread,
828
862
  VALUE stack_from_thread, // This can be different when attributing profiler overhead using a different stack
829
- struct per_thread_context *thread_context,
863
+ per_thread_context *thread_context,
830
864
  sampling_buffer* sampling_buffer,
831
865
  sample_values values,
832
866
  long current_monotonic_wall_time_ns,
833
867
  // These two labels are only used for allocation profiling; @ivoanjo: may want to refactor this at some point?
834
868
  ddog_CharSlice *ruby_vm_type,
835
869
  ddog_CharSlice *class_name,
836
- bool is_gvl_waiting_state
870
+ bool is_gvl_waiting_state,
871
+ // If the Ruby VM is at a state that can allocate objects safely, or not. Added for allocation profiling: we're not
872
+ // allowed to allocate objects (or raise exceptions) when inside the NEWOBJ tracepoint.
873
+ bool is_safe_to_allocate_objects
837
874
  ) {
838
875
  int max_label_count =
839
876
  1 + // thread id
@@ -871,12 +908,12 @@ static void trigger_sample_for_thread(
871
908
  };
872
909
  }
873
910
 
874
- struct trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
875
- trace_identifiers_for(state, thread, &trace_identifiers_result);
911
+ trace_identifiers trace_identifiers_result = {.valid = false, .trace_endpoint = Qnil};
912
+ trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
876
913
 
877
914
  if (!trace_identifiers_result.valid && state->otel_context_enabled != OTEL_CONTEXT_ENABLED_FALSE) {
878
915
  // If we couldn't get something with ddtrace, let's see if we can get some trace identifiers from opentelemetry directly
879
- otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result);
916
+ otel_without_ddtrace_trace_identifiers_for(state, thread, &trace_identifiers_result, is_safe_to_allocate_objects);
880
917
  }
881
918
 
882
919
  if (trace_identifiers_result.valid) {
@@ -970,18 +1007,24 @@ static void trigger_sample_for_thread(
970
1007
  // It SHOULD NOT be used for other purposes.
971
1008
  static VALUE _native_thread_list(DDTRACE_UNUSED VALUE _self) {
972
1009
  VALUE result = rb_ary_new();
1010
+
1011
+ debug_enter_unsafe_context();
1012
+
973
1013
  ddtrace_thread_list(result);
1014
+
1015
+ debug_leave_unsafe_context();
1016
+
974
1017
  return result;
975
1018
  }
976
1019
 
977
- static struct per_thread_context *get_or_create_context_for(VALUE thread, struct thread_context_collector_state *state) {
978
- struct per_thread_context* thread_context = NULL;
1020
+ static per_thread_context *get_or_create_context_for(VALUE thread, thread_context_collector_state *state) {
1021
+ per_thread_context* thread_context = NULL;
979
1022
  st_data_t value_context = 0;
980
1023
 
981
1024
  if (st_lookup(state->hash_map_per_thread_context, (st_data_t) thread, &value_context)) {
982
- thread_context = (struct per_thread_context*) value_context;
1025
+ thread_context = (per_thread_context*) value_context;
983
1026
  } else {
984
- thread_context = ruby_xcalloc(1, sizeof(struct per_thread_context));
1027
+ thread_context = ruby_xcalloc(1, sizeof(per_thread_context));
985
1028
  initialize_context(thread, thread_context, state);
986
1029
  st_insert(state->hash_map_per_thread_context, (st_data_t) thread, (st_data_t) thread_context);
987
1030
  }
@@ -989,12 +1032,12 @@ static struct per_thread_context *get_or_create_context_for(VALUE thread, struct
989
1032
  return thread_context;
990
1033
  }
991
1034
 
992
- static struct per_thread_context *get_context_for(VALUE thread, struct thread_context_collector_state *state) {
993
- struct per_thread_context* thread_context = NULL;
1035
+ static per_thread_context *get_context_for(VALUE thread, thread_context_collector_state *state) {
1036
+ per_thread_context* thread_context = NULL;
994
1037
  st_data_t value_context = 0;
995
1038
 
996
1039
  if (st_lookup(state->hash_map_per_thread_context, (st_data_t) thread, &value_context)) {
997
- thread_context = (struct per_thread_context*) value_context;
1040
+ thread_context = (per_thread_context*) value_context;
998
1041
  }
999
1042
 
1000
1043
  return thread_context;
@@ -1021,7 +1064,7 @@ static bool is_logging_gem_monkey_patch(VALUE invoke_file_location) {
1021
1064
  return strncmp(invoke_file + invoke_file_len - logging_gem_path_len, LOGGING_GEM_PATH, logging_gem_path_len) == 0;
1022
1065
  }
1023
1066
 
1024
- static void initialize_context(VALUE thread, struct per_thread_context *thread_context, struct thread_context_collector_state *state) {
1067
+ static void initialize_context(VALUE thread, per_thread_context *thread_context, thread_context_collector_state *state) {
1025
1068
  thread_context->sampling_buffer = sampling_buffer_new(state->max_frames, state->locations);
1026
1069
 
1027
1070
  snprintf(thread_context->thread_id, THREAD_ID_LIMIT_CHARS, "%"PRIu64" (%lu)", native_thread_id_for(thread), (unsigned long) thread_id_for(thread));
@@ -1078,14 +1121,14 @@ static void initialize_context(VALUE thread, struct per_thread_context *thread_c
1078
1121
  #endif
1079
1122
  }
1080
1123
 
1081
- static void free_context(struct per_thread_context* thread_context) {
1124
+ static void free_context(per_thread_context* thread_context) {
1082
1125
  sampling_buffer_free(thread_context->sampling_buffer);
1083
1126
  ruby_xfree(thread_context);
1084
1127
  }
1085
1128
 
1086
1129
  static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1087
- struct thread_context_collector_state *state;
1088
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1130
+ thread_context_collector_state *state;
1131
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1089
1132
 
1090
1133
  VALUE result = rb_str_new2(" (native state)");
1091
1134
 
@@ -1113,7 +1156,7 @@ static VALUE _native_inspect(DDTRACE_UNUSED VALUE _self, VALUE collector_instanc
1113
1156
  return result;
1114
1157
  }
1115
1158
 
1116
- static VALUE per_thread_context_st_table_as_ruby_hash(struct thread_context_collector_state *state) {
1159
+ static VALUE per_thread_context_st_table_as_ruby_hash(thread_context_collector_state *state) {
1117
1160
  VALUE result = rb_hash_new();
1118
1161
  st_foreach(state->hash_map_per_thread_context, per_thread_context_as_ruby_hash, result);
1119
1162
  return result;
@@ -1121,7 +1164,7 @@ static VALUE per_thread_context_st_table_as_ruby_hash(struct thread_context_coll
1121
1164
 
1122
1165
  static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value_context, st_data_t result_hash) {
1123
1166
  VALUE thread = (VALUE) key_thread;
1124
- struct per_thread_context *thread_context = (struct per_thread_context*) value_context;
1167
+ per_thread_context *thread_context = (per_thread_context*) value_context;
1125
1168
  VALUE result = (VALUE) result_hash;
1126
1169
  VALUE context_as_hash = rb_hash_new();
1127
1170
  rb_hash_aset(result, thread, context_as_hash);
@@ -1146,7 +1189,7 @@ static int per_thread_context_as_ruby_hash(st_data_t key_thread, st_data_t value
1146
1189
  return ST_CONTINUE;
1147
1190
  }
1148
1191
 
1149
- static VALUE stats_as_ruby_hash(struct thread_context_collector_state *state) {
1192
+ static VALUE stats_as_ruby_hash(thread_context_collector_state *state) {
1150
1193
  // Update this when modifying state struct (stats inner struct)
1151
1194
  VALUE stats_as_hash = rb_hash_new();
1152
1195
  VALUE arguments[] = {
@@ -1157,7 +1200,7 @@ static VALUE stats_as_ruby_hash(struct thread_context_collector_state *state) {
1157
1200
  return stats_as_hash;
1158
1201
  }
1159
1202
 
1160
- static VALUE gc_tracking_as_ruby_hash(struct thread_context_collector_state *state) {
1203
+ static VALUE gc_tracking_as_ruby_hash(thread_context_collector_state *state) {
1161
1204
  // Update this when modifying state struct (gc_tracking inner struct)
1162
1205
  VALUE result = rb_hash_new();
1163
1206
  VALUE arguments[] = {
@@ -1170,13 +1213,13 @@ static VALUE gc_tracking_as_ruby_hash(struct thread_context_collector_state *sta
1170
1213
  return result;
1171
1214
  }
1172
1215
 
1173
- static void remove_context_for_dead_threads(struct thread_context_collector_state *state) {
1216
+ static void remove_context_for_dead_threads(thread_context_collector_state *state) {
1174
1217
  st_foreach(state->hash_map_per_thread_context, remove_if_dead_thread, 0 /* unused */);
1175
1218
  }
1176
1219
 
1177
1220
  static int remove_if_dead_thread(st_data_t key_thread, st_data_t value_context, DDTRACE_UNUSED st_data_t _argument) {
1178
1221
  VALUE thread = (VALUE) key_thread;
1179
- struct per_thread_context* thread_context = (struct per_thread_context*) value_context;
1222
+ per_thread_context* thread_context = (per_thread_context*) value_context;
1180
1223
 
1181
1224
  if (is_thread_alive(thread)) return ST_CONTINUE;
1182
1225
 
@@ -1189,8 +1232,8 @@ static int remove_if_dead_thread(st_data_t key_thread, st_data_t value_context,
1189
1232
  //
1190
1233
  // Returns the whole contents of the per_thread_context structs being tracked.
1191
1234
  static VALUE _native_per_thread_context(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1192
- struct thread_context_collector_state *state;
1193
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1235
+ thread_context_collector_state *state;
1236
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1194
1237
 
1195
1238
  return per_thread_context_st_table_as_ruby_hash(state);
1196
1239
  }
@@ -1235,7 +1278,7 @@ static long update_time_since_previous_sample(long *time_at_previous_sample_ns,
1235
1278
  }
1236
1279
 
1237
1280
  // Safety: This function is assumed never to raise exceptions by callers
1238
- static long cpu_time_now_ns(struct per_thread_context *thread_context) {
1281
+ static long cpu_time_now_ns(per_thread_context *thread_context) {
1239
1282
  thread_cpu_time cpu_time = thread_cpu_time_for(thread_context->thread_cpu_time_id);
1240
1283
 
1241
1284
  if (!cpu_time.valid) {
@@ -1273,8 +1316,8 @@ VALUE enforce_thread_context_collector_instance(VALUE object) {
1273
1316
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1274
1317
  // It SHOULD NOT be used for other purposes.
1275
1318
  static VALUE _native_stats(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1276
- struct thread_context_collector_state *state;
1277
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1319
+ thread_context_collector_state *state;
1320
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1278
1321
 
1279
1322
  return stats_as_ruby_hash(state);
1280
1323
  }
@@ -1282,14 +1325,19 @@ static VALUE _native_stats(DDTRACE_UNUSED VALUE _self, VALUE collector_instance)
1282
1325
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1283
1326
  // It SHOULD NOT be used for other purposes.
1284
1327
  static VALUE _native_gc_tracking(DDTRACE_UNUSED VALUE _self, VALUE collector_instance) {
1285
- struct thread_context_collector_state *state;
1286
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1328
+ thread_context_collector_state *state;
1329
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1287
1330
 
1288
1331
  return gc_tracking_as_ruby_hash(state);
1289
1332
  }
1290
1333
 
1291
1334
  // Assumption 1: This function is called in a thread that is holding the Global VM Lock. Caller is responsible for enforcing this.
1292
- static void trace_identifiers_for(struct thread_context_collector_state *state, VALUE thread, struct trace_identifiers *trace_identifiers_result) {
1335
+ static void trace_identifiers_for(
1336
+ thread_context_collector_state *state,
1337
+ VALUE thread,
1338
+ trace_identifiers *trace_identifiers_result,
1339
+ bool is_safe_to_allocate_objects
1340
+ ) {
1293
1341
  if (state->otel_context_enabled == OTEL_CONTEXT_ENABLED_ONLY) return;
1294
1342
  if (state->tracer_context_key == MISSING_TRACER_CONTEXT_KEY) return;
1295
1343
 
@@ -1308,7 +1356,9 @@ static void trace_identifiers_for(struct thread_context_collector_state *state,
1308
1356
 
1309
1357
  VALUE numeric_span_id = Qnil;
1310
1358
 
1311
- if (otel_values != Qnil) ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values);
1359
+ if (otel_values != Qnil) {
1360
+ ddtrace_otel_trace_identifiers_for(state, &active_trace, &root_span, &numeric_span_id, active_span, otel_values, is_safe_to_allocate_objects);
1361
+ }
1312
1362
 
1313
1363
  if (root_span == Qnil || (active_span == Qnil && numeric_span_id == Qnil)) return;
1314
1364
 
@@ -1365,8 +1415,8 @@ static bool should_collect_resource(VALUE root_span) {
1365
1415
  // Assumption: This method gets called BEFORE restarting profiling -- e.g. there are no components attempting to
1366
1416
  // trigger samples at the same time.
1367
1417
  static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector_instance) {
1368
- struct thread_context_collector_state *state;
1369
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1418
+ thread_context_collector_state *state;
1419
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1370
1420
 
1371
1421
  // Release all context memory before clearing the existing context
1372
1422
  st_foreach(state->hash_map_per_thread_context, hash_map_per_thread_context_free_values, 0 /* unused */);
@@ -1380,7 +1430,7 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector
1380
1430
  return Qtrue;
1381
1431
  }
1382
1432
 
1383
- static VALUE thread_list(struct thread_context_collector_state *state) {
1433
+ static VALUE thread_list(thread_context_collector_state *state) {
1384
1434
  VALUE result = state->thread_list_buffer;
1385
1435
  rb_ary_clear(result);
1386
1436
  ddtrace_thread_list(result);
@@ -1388,8 +1438,8 @@ static VALUE thread_list(struct thread_context_collector_state *state) {
1388
1438
  }
1389
1439
 
1390
1440
  void thread_context_collector_sample_allocation(VALUE self_instance, unsigned int sample_weight, VALUE new_object) {
1391
- struct thread_context_collector_state *state;
1392
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1441
+ thread_context_collector_state *state;
1442
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1393
1443
 
1394
1444
  VALUE current_thread = rb_thread_current();
1395
1445
 
@@ -1462,7 +1512,7 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1462
1512
 
1463
1513
  track_object(state->recorder_instance, new_object, sample_weight, optional_class_name);
1464
1514
 
1465
- struct per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1515
+ per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1466
1516
 
1467
1517
  trigger_sample_for_thread(
1468
1518
  state,
@@ -1474,14 +1524,20 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
1474
1524
  INVALID_TIME, // For now we're not collecting timestamps for allocation events, as per profiling team internal discussions
1475
1525
  &ruby_vm_type,
1476
1526
  optional_class_name,
1477
- false
1527
+ /* is_gvl_waiting_state: */ false,
1528
+ /* is_safe_to_allocate_objects: */ false // Not safe to allocate further inside the NEWOBJ tracepoint
1478
1529
  );
1479
1530
  }
1480
1531
 
1481
1532
  // This method exists only to enable testing Datadog::Profiling::Collectors::ThreadContext behavior using RSpec.
1482
1533
  // It SHOULD NOT be used for other purposes.
1483
1534
  static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object) {
1535
+ debug_enter_unsafe_context();
1536
+
1484
1537
  thread_context_collector_sample_allocation(collector_instance, NUM2UINT(sample_weight), new_object);
1538
+
1539
+ debug_leave_unsafe_context();
1540
+
1485
1541
  return Qtrue;
1486
1542
  }
1487
1543
 
@@ -1529,11 +1585,18 @@ static VALUE read_otel_current_span_key_const(DDTRACE_UNUSED VALUE _unused) {
1529
1585
  return rb_const_get(trace_module, rb_intern("CURRENT_SPAN_KEY"));
1530
1586
  }
1531
1587
 
1532
- static VALUE get_otel_current_span_key(struct thread_context_collector_state *state) {
1588
+ static VALUE get_otel_current_span_key(thread_context_collector_state *state, bool is_safe_to_allocate_objects) {
1533
1589
  if (state->otel_current_span_key == Qtrue) { // Qtrue means we haven't tried to extract it yet
1590
+ if (!is_safe_to_allocate_objects) {
1591
+ // Calling read_otel_current_span_key_const below can trigger exceptions and arbitrary Ruby code running (e.g.
1592
+ // `const_missing`, etc). Not safe to call in this situation, so we just skip otel info for this sample.
1593
+ return Qnil;
1594
+ }
1595
+
1534
1596
  // If this fails, we want to fail gracefully, rather than raise an exception (e.g. if the opentelemetry gem
1535
1597
  // gets refactored, we should not fall on our face)
1536
1598
  VALUE span_key = rb_protect(read_otel_current_span_key_const, Qnil, NULL);
1599
+ rb_set_errinfo(Qnil); // **Clear any pending exception after ignoring it**
1537
1600
 
1538
1601
  // Note that this gets set to Qnil if we failed to extract the correct value, and thus we won't try to extract it again
1539
1602
  state->otel_current_span_key = span_key;
@@ -1545,12 +1608,13 @@ static VALUE get_otel_current_span_key(struct thread_context_collector_state *st
1545
1608
  // This method gets used when ddtrace is being used indirectly via the opentelemetry APIs. Information gets stored slightly
1546
1609
  // differently, and this codepath handles it.
1547
1610
  static void ddtrace_otel_trace_identifiers_for(
1548
- struct thread_context_collector_state *state,
1611
+ thread_context_collector_state *state,
1549
1612
  VALUE *active_trace,
1550
1613
  VALUE *root_span,
1551
1614
  VALUE *numeric_span_id,
1552
1615
  VALUE active_span,
1553
- VALUE otel_values
1616
+ VALUE otel_values,
1617
+ bool is_safe_to_allocate_objects
1554
1618
  ) {
1555
1619
  VALUE resolved_numeric_span_id =
1556
1620
  active_span == Qnil ?
@@ -1561,7 +1625,7 @@ static void ddtrace_otel_trace_identifiers_for(
1561
1625
 
1562
1626
  if (resolved_numeric_span_id == Qnil) return;
1563
1627
 
1564
- VALUE otel_current_span_key = get_otel_current_span_key(state);
1628
+ VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
1565
1629
  if (otel_current_span_key == Qnil) return;
1566
1630
  VALUE current_trace = *active_trace;
1567
1631
 
@@ -1569,7 +1633,7 @@ static void ddtrace_otel_trace_identifiers_for(
1569
1633
  // trace and span representing it. Each ddtrace trace is then connected to the previous otel span, forming a linked
1570
1634
  // list. The local root span is going to be the trace/span we find at the end of this linked list.
1571
1635
  while (otel_values != Qnil) {
1572
- VALUE otel_span = rb_hash_lookup(otel_values, otel_current_span_key);
1636
+ VALUE otel_span = safely_lookup_hash_without_going_into_ruby_code(otel_values, otel_current_span_key);
1573
1637
  if (otel_span == Qnil) break;
1574
1638
  VALUE next_trace = rb_ivar_get(otel_span, at_datadog_trace_id);
1575
1639
  if (next_trace == Qnil) break;
@@ -1588,8 +1652,8 @@ static void ddtrace_otel_trace_identifiers_for(
1588
1652
  }
1589
1653
 
1590
1654
  void thread_context_collector_sample_skipped_allocation_samples(VALUE self_instance, unsigned int skipped_samples) {
1591
- struct thread_context_collector_state *state;
1592
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1655
+ thread_context_collector_state *state;
1656
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1593
1657
 
1594
1658
  ddog_prof_Label labels[] = {
1595
1659
  // Providing .num = 0 should not be needed but the tracer-2.7 docker image ships a buggy gcc that complains about this
@@ -1612,7 +1676,12 @@ void thread_context_collector_sample_skipped_allocation_samples(VALUE self_insta
1612
1676
  }
1613
1677
 
1614
1678
  static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE skipped_samples) {
1679
+ debug_enter_unsafe_context();
1680
+
1615
1681
  thread_context_collector_sample_skipped_allocation_samples(collector_instance, NUM2UINT(skipped_samples));
1682
+
1683
+ debug_leave_unsafe_context();
1684
+
1616
1685
  return Qtrue;
1617
1686
  }
1618
1687
 
@@ -1638,29 +1707,30 @@ static VALUE _native_sample_skipped_allocation_samples(DDTRACE_UNUSED VALUE self
1638
1707
  // root span id.
1639
1708
  // This matches the semantics of how ddtrace tracing creates a TraceOperation and assigns a local root span to it.
1640
1709
  static void otel_without_ddtrace_trace_identifiers_for(
1641
- struct thread_context_collector_state *state,
1710
+ thread_context_collector_state *state,
1642
1711
  VALUE thread,
1643
- struct trace_identifiers *trace_identifiers_result
1712
+ trace_identifiers *trace_identifiers_result,
1713
+ bool is_safe_to_allocate_objects
1644
1714
  ) {
1645
1715
  VALUE context_storage = rb_thread_local_aref(thread, otel_context_storage_id /* __opentelemetry_context_storage__ */);
1646
1716
 
1647
1717
  // If it exists, context_storage is expected to be an Array[OpenTelemetry::Context]
1648
1718
  if (context_storage == Qnil || !RB_TYPE_P(context_storage, T_ARRAY)) return;
1649
1719
 
1650
- VALUE otel_current_span_key = get_otel_current_span_key(state);
1720
+ VALUE otel_current_span_key = get_otel_current_span_key(state, is_safe_to_allocate_objects);
1651
1721
  if (otel_current_span_key == Qnil) return;
1652
1722
 
1653
1723
  int active_context_index = RARRAY_LEN(context_storage) - 1;
1654
1724
  if (active_context_index < 0) return;
1655
1725
 
1656
- struct otel_span active_span = otel_span_from(rb_ary_entry(context_storage, active_context_index), otel_current_span_key);
1726
+ otel_span active_span = otel_span_from(rb_ary_entry(context_storage, active_context_index), otel_current_span_key);
1657
1727
  if (active_span.span == Qnil) return;
1658
1728
 
1659
- struct otel_span local_root_span = active_span;
1729
+ otel_span local_root_span = active_span;
1660
1730
 
1661
1731
  // Now find the oldest span starting from the active span that still has the same trace id as the active span
1662
1732
  for (int i = active_context_index - 1; i >= 0; i--) {
1663
- struct otel_span checking_span = otel_span_from(rb_ary_entry(context_storage, i), otel_current_span_key);
1733
+ otel_span checking_span = otel_span_from(rb_ary_entry(context_storage, i), otel_current_span_key);
1664
1734
  if (checking_span.span == Qnil) return;
1665
1735
 
1666
1736
  if (rb_str_equal(active_span.trace_id, checking_span.trace_id) == Qfalse) break;
@@ -1680,7 +1750,7 @@ static void otel_without_ddtrace_trace_identifiers_for(
1680
1750
 
1681
1751
  VALUE root_span_type = rb_ivar_get(local_root_span.span, at_kind_id /* @kind */);
1682
1752
  // We filter out spans that don't have `kind: :server`
1683
- if (root_span_type == Qnil || !RB_TYPE_P(root_span_type, T_SYMBOL) || SYM2ID(root_span_type) != server_id) return;
1753
+ if (root_span_type == Qnil || !RB_TYPE_P(root_span_type, T_SYMBOL) || !RB_STATIC_SYM_P(root_span_type) || SYM2ID(root_span_type) != server_id) return;
1684
1754
 
1685
1755
  VALUE trace_resource = rb_ivar_get(local_root_span.span, at_name_id /* @name */);
1686
1756
  if (!RB_TYPE_P(trace_resource, T_STRING)) return;
@@ -1688,8 +1758,8 @@ static void otel_without_ddtrace_trace_identifiers_for(
1688
1758
  trace_identifiers_result->trace_endpoint = trace_resource;
1689
1759
  }
1690
1760
 
1691
- static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key) {
1692
- struct otel_span failed = {.span = Qnil, .span_id = Qnil, .trace_id = Qnil};
1761
+ static otel_span otel_span_from(VALUE otel_context, VALUE otel_current_span_key) {
1762
+ otel_span failed = {.span = Qnil, .span_id = Qnil, .trace_id = Qnil};
1693
1763
 
1694
1764
  if (otel_context == Qnil) return failed;
1695
1765
 
@@ -1697,7 +1767,7 @@ static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_sp
1697
1767
  if (context_entries == Qnil || !RB_TYPE_P(context_entries, T_HASH)) return failed;
1698
1768
 
1699
1769
  // If it exists, context_entries is expected to be a Hash[OpenTelemetry::Context::Key, OpenTelemetry::Trace::Span]
1700
- VALUE span = rb_hash_lookup(context_entries, otel_current_span_key);
1770
+ VALUE span = safely_lookup_hash_without_going_into_ruby_code(context_entries, otel_current_span_key);
1701
1771
  if (span == Qnil) return failed;
1702
1772
 
1703
1773
  // If it exists, span_context is expected to be a OpenTelemetry::Trace::SpanContext (don't confuse it with OpenTelemetry::Context)
@@ -1708,7 +1778,7 @@ static struct otel_span otel_span_from(VALUE otel_context, VALUE otel_current_sp
1708
1778
  VALUE trace_id = rb_ivar_get(span_context, at_trace_id_id /* @trace_id */);
1709
1779
  if (span_id == Qnil || trace_id == Qnil || !RB_TYPE_P(span_id, T_STRING) || !RB_TYPE_P(trace_id, T_STRING)) return failed;
1710
1780
 
1711
- return (struct otel_span) {.span = span, .span_id = span_id, .trace_id = trace_id};
1781
+ return (otel_span) {.span = span, .span_id = span_id, .trace_id = trace_id};
1712
1782
  }
1713
1783
 
1714
1784
  // Otel span ids are represented as a big-endian 8-byte string
@@ -1810,8 +1880,8 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1810
1880
  // NOTE: In normal use, current_thread is expected to be == rb_thread_current(); the `current_thread` parameter only
1811
1881
  // exists to enable testing.
1812
1882
  VALUE thread_context_collector_sample_after_gvl_running(VALUE self_instance, VALUE current_thread, long current_monotonic_wall_time_ns) {
1813
- struct thread_context_collector_state *state;
1814
- TypedData_Get_Struct(self_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
1883
+ thread_context_collector_state *state;
1884
+ TypedData_Get_Struct(self_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1815
1885
 
1816
1886
  if (!state->timeline_enabled) rb_raise(rb_eRuntimeError, "GVL profiling requires timeline to be enabled");
1817
1887
 
@@ -1825,7 +1895,7 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1825
1895
  return Qfalse;
1826
1896
  }
1827
1897
 
1828
- struct per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1898
+ per_thread_context *thread_context = get_or_create_context_for(current_thread, state);
1829
1899
 
1830
1900
  // We don't actually account for cpu-time during Waiting for GVL. BUT, we may chose to push an
1831
1901
  // extra sample to represent the period prior to Waiting for GVL. To support that, we retrieve the current
@@ -1851,10 +1921,10 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1851
1921
  // need to take when sampling cpu/wall-time for a thread that's in the "Waiting for GVL" state.
1852
1922
  __attribute__((warn_unused_result))
1853
1923
  static bool handle_gvl_waiting(
1854
- struct thread_context_collector_state *state,
1924
+ thread_context_collector_state *state,
1855
1925
  VALUE thread_being_sampled,
1856
1926
  VALUE stack_from_thread,
1857
- struct per_thread_context *thread_context,
1927
+ per_thread_context *thread_context,
1858
1928
  sampling_buffer* sampling_buffer,
1859
1929
  long current_cpu_time_ns
1860
1930
  ) {
@@ -1939,7 +2009,8 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1939
2009
  gvl_waiting_started_wall_time_ns,
1940
2010
  NULL,
1941
2011
  NULL,
1942
- false // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2012
+ /* is_gvl_waiting_state: */ false, // This is the extra sample before the wait begun; only the next sample will be in the gvl waiting state
2013
+ /* is_safe_to_allocate_objects: */ true // This is similar to a regular cpu/wall sample, so it's also safe
1943
2014
  );
1944
2015
  }
1945
2016
 
@@ -1949,40 +2020,62 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1949
2020
  static VALUE _native_on_gvl_waiting(DDTRACE_UNUSED VALUE self, VALUE thread) {
1950
2021
  ENFORCE_THREAD(thread);
1951
2022
 
2023
+ debug_enter_unsafe_context();
2024
+
1952
2025
  thread_context_collector_on_gvl_waiting(thread_from_thread_object(thread));
2026
+
2027
+ debug_leave_unsafe_context();
2028
+
1953
2029
  return Qnil;
1954
2030
  }
1955
2031
 
1956
2032
  static VALUE _native_gvl_waiting_at_for(DDTRACE_UNUSED VALUE self, VALUE thread) {
1957
2033
  ENFORCE_THREAD(thread);
1958
2034
 
2035
+ debug_enter_unsafe_context();
2036
+
1959
2037
  intptr_t gvl_waiting_at = gvl_profiling_state_thread_object_get(thread);
2038
+
2039
+ debug_leave_unsafe_context();
2040
+
1960
2041
  return LONG2NUM(gvl_waiting_at);
1961
2042
  }
1962
2043
 
1963
2044
  static VALUE _native_on_gvl_running(DDTRACE_UNUSED VALUE self, VALUE thread) {
1964
2045
  ENFORCE_THREAD(thread);
1965
2046
 
1966
- return thread_context_collector_on_gvl_running(thread_from_thread_object(thread)) == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2047
+ debug_enter_unsafe_context();
2048
+
2049
+ VALUE result = thread_context_collector_on_gvl_running(thread_from_thread_object(thread)) == ON_GVL_RUNNING_SAMPLE ? Qtrue : Qfalse;
2050
+
2051
+ debug_leave_unsafe_context();
2052
+
2053
+ return result;
1967
2054
  }
1968
2055
 
1969
2056
  static VALUE _native_sample_after_gvl_running(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread) {
1970
2057
  ENFORCE_THREAD(thread);
1971
2058
 
1972
- return thread_context_collector_sample_after_gvl_running(
2059
+ debug_enter_unsafe_context();
2060
+
2061
+ VALUE result = thread_context_collector_sample_after_gvl_running(
1973
2062
  collector_instance,
1974
2063
  thread,
1975
2064
  monotonic_wall_time_now_ns(RAISE_ON_FAILURE)
1976
2065
  );
2066
+
2067
+ debug_leave_unsafe_context();
2068
+
2069
+ return result;
1977
2070
  }
1978
2071
 
1979
2072
  static VALUE _native_apply_delta_to_cpu_time_at_previous_sample_ns(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE thread, VALUE delta_ns) {
1980
2073
  ENFORCE_THREAD(thread);
1981
2074
 
1982
- struct thread_context_collector_state *state;
1983
- TypedData_Get_Struct(collector_instance, struct thread_context_collector_state, &thread_context_collector_typed_data, state);
2075
+ thread_context_collector_state *state;
2076
+ TypedData_Get_Struct(collector_instance, thread_context_collector_state, &thread_context_collector_typed_data, state);
1984
2077
 
1985
- struct per_thread_context *thread_context = get_context_for(thread, state);
2078
+ per_thread_context *thread_context = get_context_for(thread, state);
1986
2079
  if (thread_context == NULL) rb_raise(rb_eArgError, "Unexpected: This method cannot be used unless the per-thread context for the thread already exists");
1987
2080
 
1988
2081
  thread_context->cpu_time_at_previous_sample_ns += NUM2LONG(delta_ns);
@@ -1992,11 +2085,45 @@ static uint64_t otel_span_id_to_uint(VALUE otel_span_id) {
1992
2085
 
1993
2086
  #else
1994
2087
  static bool handle_gvl_waiting(
1995
- DDTRACE_UNUSED struct thread_context_collector_state *state,
2088
+ DDTRACE_UNUSED thread_context_collector_state *state,
1996
2089
  DDTRACE_UNUSED VALUE thread_being_sampled,
1997
2090
  DDTRACE_UNUSED VALUE stack_from_thread,
1998
- DDTRACE_UNUSED struct per_thread_context *thread_context,
2091
+ DDTRACE_UNUSED per_thread_context *thread_context,
1999
2092
  DDTRACE_UNUSED sampling_buffer* sampling_buffer,
2000
2093
  DDTRACE_UNUSED long current_cpu_time_ns
2001
2094
  ) { return false; }
2002
2095
  #endif // NO_GVL_INSTRUMENTATION
2096
+
2097
+ #define MAX_SAFE_LOOKUP_SIZE 16
2098
+
2099
+ typedef struct { VALUE lookup_key; VALUE result; } safe_lookup_hash_state;
2100
+
2101
+ static int safe_lookup_hash_iterate(VALUE key, VALUE value, VALUE state_ptr) {
2102
+ safe_lookup_hash_state *state = (safe_lookup_hash_state *) state_ptr;
2103
+
2104
+ if (key == state->lookup_key) {
2105
+ state->result = value;
2106
+ return ST_STOP;
2107
+ }
2108
+
2109
+ return ST_CONTINUE;
2110
+ }
2111
+
2112
+ // This method exists because we need to look up a hash during sampling, but we don't want to invoke any
2113
+ // Ruby code as a side effect. To be able to look up by hash, `rb_hash_lookup` calls `#hash` on the key,
2114
+ // which we want to avoid.
2115
+ // Thus, instead, we opt to just iterate through the hash and check if we can find what we're looking for.
2116
+ //
2117
+ // To avoid having too much overhead here we only iterate in hashes up to MAX_SAFE_LOOKUP_SIZE.
2118
+ // Note that we don't even try to iterate if the hash is bigger -- this is to avoid flaky behavior where
2119
+ // depending on the internal storage order of the hash we may or not find the key, and instead we always
2120
+ // enforce the size.
2121
+ static VALUE safely_lookup_hash_without_going_into_ruby_code(VALUE hash, VALUE key) {
2122
+ if (!RB_TYPE_P(hash, T_HASH) || RHASH_SIZE(hash) > MAX_SAFE_LOOKUP_SIZE) return Qnil;
2123
+
2124
+ safe_lookup_hash_state state = {.lookup_key = key, .result = Qnil};
2125
+
2126
+ rb_hash_foreach(hash, safe_lookup_hash_iterate, (VALUE) &state);
2127
+
2128
+ return state.result;
2129
+ }