Skip to content

Commit 4a7115d

Browse files
committed
Fix crash when sampling weird internal VM objects
Today I learned that sometimes Ruby allocates objects without a class! See notes in diff for more details.
1 parent 0128298 commit 4a7115d

File tree

2 files changed

+60
-6
lines changed

2 files changed

+60
-6
lines changed

ext/ddtrace_profiling_native_extension/collectors_thread_context.c

+45-6
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ static VALUE _native_reset_after_fork(DDTRACE_UNUSED VALUE self, VALUE collector
206206
static VALUE thread_list(struct thread_context_collector_state *state);
207207
static VALUE _native_sample_allocation(DDTRACE_UNUSED VALUE self, VALUE collector_instance, VALUE sample_weight, VALUE new_object);
208208
static VALUE _native_new_empty_thread(VALUE self);
209+
ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type);
209210

210211
void collectors_thread_context_init(VALUE profiling_module) {
211212
VALUE collectors_module = rb_define_module_under(profiling_module, "Collectors");
@@ -1122,13 +1123,25 @@ void thread_context_collector_sample_allocation(VALUE self_instance, unsigned in
11221123
type == RUBY_T_SYMBOL ||
11231124
type == RUBY_T_FIXNUM
11241125
) {
1125-
const char *name = rb_obj_classname(new_object);
1126-
size_t name_length = name != NULL ? strlen(name) : 0;
1127-
1128-
if (name_length > 0) {
1129-
class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1126+
VALUE klass = rb_class_of(new_object);
1127+
1128+
// Ruby sometimes plays a bit fast and loose with some of its internal objects, e.g.
1129+
// `rb_str_tmp_frozen_acquire` allocates a string with no class (klass=0).
1130+
// Thus, we need to make sure there's actually a class before getting its name.
1131+
1132+
if (klass != 0) {
1133+
const char *name = rb_obj_classname(new_object);
1134+
size_t name_length = name != NULL ? strlen(name) : 0;
1135+
1136+
if (name_length > 0) {
1137+
class_name = (ddog_CharSlice) {.ptr = name, .len = name_length};
1138+
} else {
1139+
// @ivoanjo: I'm not sure this can ever happen, but just-in-case
1140+
class_name = ruby_value_type_to_class_name(type);
1141+
}
11301142
} else {
1131-
class_name = DDOG_CHARSLICE_C("(Anonymous)");
1143+
// Fallback for objects with no class
1144+
class_name = ruby_value_type_to_class_name(type);
11321145
}
11331146
} else {
11341147
class_name = ruby_vm_type; // For internal things and, we just use the VM type
@@ -1164,3 +1177,29 @@ static VALUE new_empty_thread_inner(DDTRACE_UNUSED void *arg) { return Qnil; }
11641177
static VALUE _native_new_empty_thread(DDTRACE_UNUSED VALUE self) {
11651178
return rb_thread_create(new_empty_thread_inner, NULL);
11661179
}
1180+
1181+
ddog_CharSlice ruby_value_type_to_class_name(enum ruby_value_type type) {
1182+
switch (type) {
1183+
case(RUBY_T_OBJECT ): return DDOG_CHARSLICE_C("Object");
1184+
case(RUBY_T_CLASS ): return DDOG_CHARSLICE_C("Class");
1185+
case(RUBY_T_MODULE ): return DDOG_CHARSLICE_C("Module");
1186+
case(RUBY_T_FLOAT ): return DDOG_CHARSLICE_C("Float");
1187+
case(RUBY_T_STRING ): return DDOG_CHARSLICE_C("String");
1188+
case(RUBY_T_REGEXP ): return DDOG_CHARSLICE_C("Regexp");
1189+
case(RUBY_T_ARRAY ): return DDOG_CHARSLICE_C("Array");
1190+
case(RUBY_T_HASH ): return DDOG_CHARSLICE_C("Hash");
1191+
case(RUBY_T_STRUCT ): return DDOG_CHARSLICE_C("Struct");
1192+
case(RUBY_T_BIGNUM ): return DDOG_CHARSLICE_C("Integer");
1193+
case(RUBY_T_FILE ): return DDOG_CHARSLICE_C("File");
1194+
case(RUBY_T_DATA ): return DDOG_CHARSLICE_C("(VM Internal, T_DATA)");
1195+
case(RUBY_T_MATCH ): return DDOG_CHARSLICE_C("MatchData");
1196+
case(RUBY_T_COMPLEX ): return DDOG_CHARSLICE_C("Complex");
1197+
case(RUBY_T_RATIONAL): return DDOG_CHARSLICE_C("Rational");
1198+
case(RUBY_T_NIL ): return DDOG_CHARSLICE_C("NilClass");
1199+
case(RUBY_T_TRUE ): return DDOG_CHARSLICE_C("TrueClass");
1200+
case(RUBY_T_FALSE ): return DDOG_CHARSLICE_C("FalseClass");
1201+
case(RUBY_T_SYMBOL ): return DDOG_CHARSLICE_C("Symbol");
1202+
case(RUBY_T_FIXNUM ): return DDOG_CHARSLICE_C("Integer");
1203+
default: return DDOG_CHARSLICE_C("(VM Internal, Missing class)");
1204+
}
1205+
}

spec/datadog/profiling/collectors/cpu_and_wall_time_worker_spec.rb

+15
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,21 @@
443443
expect(allocation_sample.values).to include(:'alloc-samples' => 123)
444444
expect(allocation_sample.locations.first.lineno).to eq allocation_line
445445
end
446+
447+
context 'when sampling optimized Ruby strings' do
448+
# Regression test: Some internal Ruby classes use a `rb_str_tmp_frozen_acquire` function which allocates a
449+
# weird "intermediate" string object that has its class pointer set to 0.
450+
#
451+
# When such an object gets sampled, we need to take care not to try to resolve its class name.
452+
#
453+
# In practice, this test is actually validating behavior of the `ThreadContext` collector, but we can only
454+
# really trigger this situation when using the allocation tracepoint, which lives in the `CpuAndWallTimeWorker`.
455+
it 'does not crash' do
456+
start
457+
458+
expect(Time.new.strftime(String.new('Potato'))).to_not be nil
459+
end
460+
end
446461
end
447462

448463
context 'when allocation sampling is disabled' do

0 commit comments

Comments
 (0)