Skip to content

Commit 4d2e8cb

Browse files
committed
Exclude internal frames from backtrace
This changeset suppresses backtrace locations like `<internal:array>:211` as much as possible. Before the patch: ``` $ ruby -e '[1].fetch_values(42)' <internal:array>:211:in 'Array#fetch': index 42 outside of array bounds: -1...1 (IndexError) from <internal:array>:211:in 'block in Array#fetch_values' from <internal:array>:211:in 'Array#map!' from <internal:array>:211:in 'Array#fetch_values' from -e:1:in '<main>' ``` After the patch: ``` $ ./miniruby -e '[1].fetch_values(42)' -e:1:in 'Array#fetch_values': index 42 outside of array bounds: -1...1 (IndexError) from -e:1:in '<main>' ``` Specifically: * The special backtrace handling of BUILTIN_ATTR_C_TRACE is now always applied to frames with `<internal:...>`. * When multiple consecutive internal frames appear, all but the bottom (caller-side) frame are removed. [Misc #20968]
1 parent 895a70f commit 4d2e8cb

File tree

3 files changed

+58
-23
lines changed

3 files changed

+58
-23
lines changed

spec/ruby/core/kernel/caller_locations_spec.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
end
8484
end
8585

86-
ruby_version_is "3.4" do
86+
ruby_version_is "3.4"..."3.5" do
8787
it "includes core library methods defined in Ruby" do
8888
file, line = Kernel.instance_method(:tap).source_location
8989
file.should.start_with?('<internal:')
@@ -94,5 +94,17 @@
9494
loc.path.should.start_with? "<internal:"
9595
end
9696
end
97+
98+
ruby_version_is "3.5" do
99+
it "does not include core library methods defined in Ruby" do
100+
file, line = Kernel.instance_method(:tap).source_location
101+
file.should.start_with?('<internal:')
102+
103+
loc = nil
104+
tap { loc = caller_locations(1, 1)[0] }
105+
loc.label.should == "Kernel#tap"
106+
loc.path.should == __FILE__
107+
end
108+
end
97109
end
98110
end

spec/ruby/core/kernel/caller_spec.rb

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,26 @@
8484
end
8585

8686
guard -> { Kernel.instance_method(:tap).source_location } do
87-
it "includes core library methods defined in Ruby" do
88-
file, line = Kernel.instance_method(:tap).source_location
89-
file.should.start_with?('<internal:')
87+
ruby_version_is ""..."3.5" do
88+
it "includes core library methods defined in Ruby" do
89+
file, line = Kernel.instance_method(:tap).source_location
90+
file.should.start_with?('<internal:')
91+
92+
loc = nil
93+
tap { loc = caller(1, 1)[0] }
94+
loc.should =~ /\A<internal:.*in [`'](?:Kernel#)?tap'\z/
95+
end
96+
end
97+
98+
ruby_version_is "3.5" do
99+
it "includes core library methods defined in Ruby" do
100+
file, line = Kernel.instance_method(:tap).source_location
101+
file.should.start_with?('<internal:')
90102

91-
loc = nil
92-
tap { loc = caller(1, 1)[0] }
93-
loc.should =~ /\A<internal:.*in [`'](?:Kernel#)?tap'\z/
103+
loc = nil
104+
tap { loc = caller(1, 1)[0] }
105+
loc.should =~ /\A#{ __FILE__ }:.*in [`'](?:Kernel#)?tap'\z/
106+
end
94107
end
95108
end
96109
end

vm_backtrace.c

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,15 @@ calculate_iseq_label(VALUE owner, const rb_iseq_t *iseq)
262262
}
263263
}
264264

265+
static bool
266+
is_internal_location(const rb_iseq_t *iseq)
267+
{
268+
static const char prefix[] = "<internal:";
269+
const size_t prefix_len = sizeof(prefix) - 1;
270+
VALUE file = rb_iseq_path(iseq);
271+
return strncmp(prefix, RSTRING_PTR(file), prefix_len) == 0;
272+
}
273+
265274
// Return true if a given location is a C method or supposed to behave like one.
266275
static inline bool
267276
location_cfunc_p(rb_backtrace_location_t *loc)
@@ -272,7 +281,7 @@ location_cfunc_p(rb_backtrace_location_t *loc)
272281
case VM_METHOD_TYPE_CFUNC:
273282
return true;
274283
case VM_METHOD_TYPE_ISEQ:
275-
return rb_iseq_attr_p(loc->cme->def->body.iseq.iseqptr, BUILTIN_ATTR_C_TRACE);
284+
return is_internal_location(loc->cme->def->body.iseq.iseqptr);
276285
default:
277286
return false;
278287
}
@@ -604,15 +613,6 @@ backtrace_size(const rb_execution_context_t *ec)
604613
return start_cfp - last_cfp + 1;
605614
}
606615

607-
static bool
608-
is_internal_location(const rb_control_frame_t *cfp)
609-
{
610-
static const char prefix[] = "<internal:";
611-
const size_t prefix_len = sizeof(prefix) - 1;
612-
VALUE file = rb_iseq_path(cfp->iseq);
613-
return strncmp(prefix, RSTRING_PTR(file), prefix_len) == 0;
614-
}
615-
616616
static bool
617617
is_rescue_or_ensure_frame(const rb_control_frame_t *cfp)
618618
{
@@ -691,16 +691,26 @@ rb_ec_partial_backtrace_object(const rb_execution_context_t *ec, long start_fram
691691
if (start_frame > 0) {
692692
start_frame--;
693693
}
694-
else if (!(skip_internal && is_internal_location(cfp))) {
694+
else {
695+
bool internal = is_internal_location(cfp->iseq);
696+
if (skip_internal && internal) continue;
695697
if (!skip_next_frame) {
696698
const rb_iseq_t *iseq = cfp->iseq;
697699
const VALUE *pc = cfp->pc;
700+
if (internal && backpatch_counter > 0) {
701+
// To keep only one internal frame, discard the previous backpatch frames
702+
bt->backtrace_size -= backpatch_counter;
703+
backpatch_counter = 0;
704+
}
698705
loc = &bt->backtrace[bt->backtrace_size++];
699706
RB_OBJ_WRITE(btobj, &loc->cme, rb_vm_frame_method_entry(cfp));
700-
// Ruby methods with `Primitive.attr! :c_trace` should behave like C methods
701-
if (rb_iseq_attr_p(cfp->iseq, BUILTIN_ATTR_C_TRACE)) {
702-
loc->iseq = NULL;
703-
loc->pc = NULL;
707+
// internal frames (`<internal:...>`) should behave like C methods
708+
if (internal) {
709+
// Typically, these iseq and pc are not needed because they will be backpatched later.
710+
// But when the call stack starts with an internal frame (i.e., prelude.rb),
711+
// they will be used to show the `<internal:...>` location.
712+
RB_OBJ_WRITE(btobj, &loc->iseq, iseq);
713+
loc->pc = pc;
704714
backpatch_counter++;
705715
}
706716
else {
@@ -736,7 +746,7 @@ rb_ec_partial_backtrace_object(const rb_execution_context_t *ec, long start_fram
736746
// is the one of the caller Ruby frame, so if the last entry is a C frame we find the caller Ruby frame here.
737747
if (backpatch_counter > 0) {
738748
for (; cfp != end_cfp; cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp)) {
739-
if (cfp->iseq && cfp->pc && !(skip_internal && is_internal_location(cfp))) {
749+
if (cfp->iseq && cfp->pc && !(skip_internal && is_internal_location(cfp->iseq))) {
740750
VM_ASSERT(!skip_next_frame); // ISEQ_TYPE_RESCUE/ISEQ_TYPE_ENSURE should have a caller Ruby ISEQ, not a cfunc
741751
bt_backpatch_loc(backpatch_counter, loc, cfp->iseq, cfp->pc);
742752
RB_OBJ_WRITTEN(btobj, Qundef, cfp->iseq);

0 commit comments

Comments
 (0)