3434
3535#include <datadog/crashtracker.h>
3636#include "datadog_ruby_common.h"
37+ #include "datadog_runtime_stack.h"
3738#include <sys/mman.h>
3839#include <unistd.h>
3940#include <errno.h>
4647
4748static VALUE _native_start_or_update_on_fork (int argc , VALUE * argv , DDTRACE_UNUSED VALUE _self );
4849static VALUE _native_stop (DDTRACE_UNUSED VALUE _self );
49- static VALUE _native_register_runtime_stack_callback (VALUE _self , VALUE callback_type );
50- static VALUE _native_is_runtime_callback_registered (DDTRACE_UNUSED VALUE _self );
51-
52- static void ruby_runtime_stack_callback (
53- void (* emit_frame )(const ddog_crasht_RuntimeStackFrame * )
54- );
5550
5651static bool first_init = true;
5752
58- static bool is_pointer_readable (const void * ptr , size_t size ) {
59- if (!ptr ) return false;
60-
61- // This is signal-safe and doesn't allocate memory
62- size_t page_size = getpagesize ();
63- void * aligned_ptr = (void * )((uintptr_t )ptr & ~(page_size - 1 ));
64- size_t pages = ((char * )ptr + size - (char * )aligned_ptr + page_size - 1 ) / page_size ;
65-
66- // Stack-allocate a small buffer for mincore results.. should be safe?
67- unsigned char vec [16 ];
68- if (pages > 16 ) return false; // Too big to check safely
69-
70- return mincore (aligned_ptr , pages * page_size , vec ) == 0 ;
71- }
72-
73- static bool is_safe_string_encoding (const char * ptr , long len ) {
74- if (!ptr || len <= 0 ) return false;
75-
76- // Should we scan it all?
77- for (long i = 0 ; i < len && i < 128 ; i ++ ) {
78- unsigned char c = (unsigned char )ptr [i ];
79-
80- if (c == 0 && i < len - 1 ) return false;
81-
82- // Control characters (except tab, newline, return) is sus
83- if (c < 0x20 && c != 0x09 && c != 0x0A && c != 0x0D ) return false;
84-
85- // High bytes
86- if (c >= 0xF8 ) return false;
87- }
88-
89- return true;
90- }
91-
92- static bool is_reasonable_string_size (VALUE str ) {
93- if (str == Qnil ) return false;
94- if (!RB_TYPE_P (str , T_STRING )) return false;
95-
96- if (!is_pointer_readable (& str , sizeof (VALUE ))) return false;
97-
98- long len = RSTRING_LEN (str );
99-
100- if (len < 0 ) return false; // Negative length, probably corrupted
101- if (len > 2048 ) return false; // > 2KB path/function name, sus
102-
103- if (len > 0 ) {
104- if (!is_pointer_readable (RSTRING (str ), sizeof (struct RString ))) return false;
105- }
106-
107- return true;
108- }
109-
110- static const char * safe_string_ptr (VALUE str ) {
111- if (str == Qnil ) return "<nil>" ;
112- if (!RB_TYPE_P (str , T_STRING )) return "<not_string>" ;
113-
114- long len = RSTRING_LEN (str );
115- if (!is_reasonable_string_size (str )) return "<corrupted>" ;
116-
117- // Use Ruby's standard string pointer access (handles different representations internally)
118- const char * ptr = RSTRING_PTR (str );
119-
120- if (!ptr ) return "<null>" ;
121-
122- if (!is_pointer_readable (ptr , len > 0 ? len : 1 )) return "<unreadable>" ;
123- if (!is_safe_string_encoding (ptr , len )) return "<unsafe_encoding>" ;
124-
125- return ptr ;
126- }
127-
128- static bool is_valid_control_frame (const rb_control_frame_t * cfp ,
129- const rb_execution_context_t * ec ) {
130- if (!cfp ) return false;
131-
132- void * stack_start = ec -> vm_stack ;
133- void * stack_end = (char * )stack_start + ec -> vm_stack_size * sizeof (VALUE );
134- if ((void * )cfp < stack_start || (void * )cfp >= stack_end ) {
135- return false;
136- }
137-
138- if (!is_pointer_readable (cfp , sizeof (rb_control_frame_t ))) {
139- return false;
140- }
141-
142- return true;
143- }
144-
145- static bool is_valid_iseq (const rb_iseq_t * iseq ) {
146- if (!iseq ) return false;
147- if (!is_pointer_readable (iseq , sizeof (rb_iseq_t ))) return false;
148-
149- // Check iseq body
150- if (!iseq -> body ) return false;
151- if (!is_pointer_readable (iseq -> body , sizeof (* iseq -> body ))) return false;
152-
153- // Validate iseq size
154- if (iseq -> body -> iseq_size > 100000 ) return false; // > 100K instructions, suspicious
155-
156- return true;
157- }
158-
15953// Used to report Ruby VM crashes.
16054// Once initialized, segfaults will be reported automatically using libdatadog.
16155
@@ -165,8 +59,7 @@ void crashtracker_init(VALUE core_module) {
16559
16660 rb_define_singleton_method (crashtracker_class , "_native_start_or_update_on_fork" , _native_start_or_update_on_fork , -1 );
16761 rb_define_singleton_method (crashtracker_class , "_native_stop" , _native_stop , 0 );
168- rb_define_singleton_method (crashtracker_class , "_native_register_runtime_stack_callback" , _native_register_runtime_stack_callback , 1 );
169- rb_define_singleton_method (crashtracker_class , "_native_is_runtime_callback_registered" , _native_is_runtime_callback_registered , 0 );
62+ datadog_runtime_stack_init (crashtracker_class );
17063}
17164
17265static VALUE _native_start_or_update_on_fork (int argc , VALUE * argv , DDTRACE_UNUSED VALUE _self ) {
@@ -274,207 +167,3 @@ static VALUE _native_stop(DDTRACE_UNUSED VALUE _self) {
274167 return Qtrue ;
275168}
276169
277- static void ruby_runtime_stack_callback (
278- void (* emit_frame )(const ddog_crasht_RuntimeStackFrame * )
279- ) {
280-
281- VALUE current_thread = rb_thread_current ();
282- if (current_thread == Qnil ) return ;
283-
284- // Get thread struct carefully
285- static const rb_data_type_t * thread_data_type = NULL ;
286- if (thread_data_type == NULL ) {
287- thread_data_type = RTYPEDDATA_TYPE (current_thread );
288- if (!thread_data_type ) return ;
289- }
290-
291- rb_thread_t * th = (rb_thread_t * ) rb_check_typeddata (current_thread , thread_data_type );
292- if (!th ) return ;
293-
294- const rb_execution_context_t * ec = th -> ec ;
295- if (!ec ) return ;
296-
297- if (th -> status == THREAD_KILLED ) return ;
298- if (!ec -> vm_stack || ec -> vm_stack_size == 0 ) return ;
299-
300- const rb_control_frame_t * cfp = ec -> cfp ;
301- const rb_control_frame_t * end_cfp = RUBY_VM_END_CONTROL_FRAME (ec );
302-
303- if (!cfp || !end_cfp ) return ;
304-
305- // Skip dummy frame, `thread_profile_frames` does this too
306- end_cfp = RUBY_VM_NEXT_CONTROL_FRAME (end_cfp );
307- if (end_cfp <= cfp ) return ;
308-
309- end_cfp = RUBY_VM_NEXT_CONTROL_FRAME (end_cfp );
310-
311- int frame_count = 0 ;
312- const int MAX_FRAMES = 400 ;
313-
314- // Traverse from current frame backwards to older frames, so that we get the crash point at the top
315- for (; frame_count < MAX_FRAMES && cfp != end_cfp ; cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME (cfp )) {
316- if (!is_valid_control_frame (cfp , ec )) {
317- continue ;
318- }
319-
320- if (cfp -> iseq && !cfp -> pc ) {
321- continue ;
322- }
323-
324- if (VM_FRAME_RUBYFRAME_P (cfp ) && cfp -> iseq ) {
325- // Handle Ruby frames
326- const rb_iseq_t * iseq = cfp -> iseq ;
327-
328- if (!is_valid_iseq (iseq )) {
329- continue ;
330- }
331-
332- VALUE name = rb_iseq_base_label (iseq );
333- const char * function_name = "<unknown>" ;
334- if (name != Qnil ) {
335- function_name = safe_string_ptr (name );
336- }
337-
338- VALUE filename = rb_iseq_path (iseq );
339- const char * file_name = "<unknown>" ;
340- if (filename != Qnil ) {
341- file_name = safe_string_ptr (filename );
342- }
343-
344- int line_no = 0 ;
345- if (iseq && iseq -> body ) {
346- if (!cfp -> pc ) {
347- // Handle case where PC is NULL - use first line number like private_vm_api_access.c
348- if (iseq -> body -> type == ISEQ_TYPE_TOP ) {
349- // For TOP type iseqs, line number should be 0
350- line_no = 0 ;
351- } else {
352- // Use first line number for other types
353- # ifndef NO_INT_FIRST_LINENO // Ruby 3.2+
354- line_no = iseq -> body -> location .first_lineno ;
355- # else
356- line_no = FIX2INT (iseq -> body -> location .first_lineno );
357- #endif
358- }
359- } else {
360- // Handle case where PC is available - mirror calc_pos logic
361- if (is_pointer_readable (iseq -> body -> iseq_encoded , iseq -> body -> iseq_size * sizeof (* iseq -> body -> iseq_encoded )) &&
362- iseq -> body -> iseq_size > 0 ) {
363- ptrdiff_t pc_offset = cfp -> pc - iseq -> body -> iseq_encoded ;
364-
365- // bounds checking like private_vm_api_access.c PROF-11475 fix
366- if (pc_offset >= 0 && pc_offset <= iseq -> body -> iseq_size ) {
367- size_t pos = (size_t )pc_offset ;
368- if (pos > 0 ) {
369- // Use pos-1 because PC points to next instruction
370- pos -- ;
371- }
372- line_no = rb_iseq_line_no (iseq , pos );
373- }
374- }
375- }
376- }
377-
378- ddog_crasht_RuntimeStackFrame frame = {
379- .type_name = char_slice_from_cstr (NULL ),
380- .function = char_slice_from_cstr (function_name ),
381- .file = char_slice_from_cstr (file_name ),
382- .line = line_no ,
383- .column = 0
384- };
385-
386- emit_frame (& frame );
387- frame_count ++ ;
388- } else if (VM_FRAME_CFRAME_P (cfp )) {
389- const char * function_name = "<C method>" ;
390- const char * file_name = "<C extension>" ;
391-
392- // Try to get method entry information
393- const rb_callable_method_entry_t * me = rb_vm_frame_method_entry (cfp );
394- if (me && is_pointer_readable (me , sizeof (rb_callable_method_entry_t ))) {
395- if (me -> def && is_pointer_readable (me -> def , sizeof (* me -> def ))) {
396- if (me -> def -> original_id ) {
397- const char * method_name = rb_id2name (me -> def -> original_id );
398- if (method_name && is_pointer_readable (method_name , strlen (method_name ))) {
399- // Sanity check for method name length
400- size_t method_name_len = strlen (method_name );
401- if (method_name_len > 0 && method_name_len < 256 ) {
402- function_name = method_name ;
403- }
404- }
405- }
406-
407- if (me -> def -> type == VM_METHOD_TYPE_CFUNC && me -> owner ) {
408- // Try to get the full class/module path
409- VALUE owner_name = Qnil ;
410- VALUE actual_owner = me -> owner ;
411-
412- // If this is a singleton class (like Fiddle's singleton class for module methods),
413- // try to get the attached object which should be the actual module or else we will
414- // just get `Module` which is not that useful lol
415- if (RB_TYPE_P (me -> owner , T_CLASS ) && FL_TEST (me -> owner , FL_SINGLETON )) {
416- VALUE attached = rb_ivar_get (me -> owner , rb_intern ("__attached__" ));
417- if (attached != Qnil ) {
418- actual_owner = attached ;
419- }
420- }
421-
422- // Get the class/module path
423- if (RB_TYPE_P (actual_owner , T_CLASS ) || RB_TYPE_P (actual_owner , T_MODULE )) {
424- owner_name = rb_class_path (actual_owner );
425- }
426-
427- // Fallback to rb_class_name if rb_class_path fails
428- if (owner_name == Qnil ) {
429- owner_name = rb_class_name (actual_owner );
430- }
431-
432- if (owner_name != Qnil ) {
433- const char * owner_str = safe_string_ptr (owner_name );
434- static char file_buffer [256 ];
435- snprintf (file_buffer , sizeof (file_buffer ), "<%s (C extension)>" , owner_str );
436- file_name = file_buffer ;
437- }
438- }
439- }
440- }
441-
442- ddog_crasht_RuntimeStackFrame frame = {
443- .type_name = char_slice_from_cstr (NULL ),
444- .function = char_slice_from_cstr (function_name ),
445- .file = char_slice_from_cstr (file_name ),
446- .line = 0 ,
447- .column = 0
448- };
449-
450- emit_frame (& frame );
451- frame_count ++ ;
452- }
453- }
454- }
455-
456- static VALUE _native_register_runtime_stack_callback (DDTRACE_UNUSED VALUE _self , VALUE callback_type ) {
457- ENFORCE_TYPE (callback_type , T_SYMBOL );
458-
459- VALUE frame_symbol = ID2SYM (rb_intern ("frame" ));
460- if (callback_type != frame_symbol ) {
461- rb_raise (rb_eArgError , "Invalid callback_type. Only :frame is supported" );
462- }
463-
464- enum ddog_crasht_CallbackResult result = ddog_crasht_register_runtime_frame_callback (
465- ruby_runtime_stack_callback
466- );
467-
468- switch (result ) {
469- case DDOG_CRASHT_CALLBACK_RESULT_OK :
470- return Qtrue ;
471- default :
472- return Qfalse ;
473- }
474-
475- return Qfalse ;
476- }
477-
478- static VALUE _native_is_runtime_callback_registered (DDTRACE_UNUSED VALUE _self ) {
479- return ddog_crasht_is_runtime_callback_registered () ? Qtrue : Qfalse ;
480- }
0 commit comments