Skip to content

Commit 3c782d0

Browse files
committed
Move runtime stuff to own file
1 parent 4e361d1 commit 3c782d0

File tree

4 files changed

+475
-321
lines changed

4 files changed

+475
-321
lines changed

ext/libdatadog_api/crashtracker.c

Lines changed: 2 additions & 313 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
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>
@@ -46,116 +47,9 @@
4647

4748
static VALUE _native_start_or_update_on_fork(int argc, VALUE *argv, DDTRACE_UNUSED VALUE _self);
4849
static 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

5651
static 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

17265
static 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

Comments
 (0)