Skip to content

Commit 31d1e84

Browse files
tompngclaude
andauthored
Fix infinite loop and stack overflow on cyclic superclass chains (#1785)
Related to #1781 and #1782 Fix crash when a class has a superclass cycle. Superclass chain can have a cycle. Some cycle can be rejected in parse phase(#1784), but I think some manually crafted case aren't possible to reject in parse phase. So `ancestors` `superclasses` must handle cyclic chain for now. Even after #1784, superclass cycle can be created by: ```ruby class A < B; end class B < C; end class C < B; end ``` (Perhaps there's an option to add validation phase that detects and fixes cyclic chain in the future) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a8b46ec commit 31d1e84

4 files changed

Lines changed: 71 additions & 9 deletions

File tree

‎lib/rdoc/code_object/class_module.rb‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ def add_things(my_things, other_things) # :nodoc:
174174
# in the ancestors. The superclass, if any, comes last.
175175

176176
def ancestors
177+
included_ancestors
178+
end
179+
180+
def included_ancestors # :nodoc:
177181
includes.map { |i| i.module }.reverse
178182
end
179183

@@ -807,10 +811,17 @@ def superclass=(superclass)
807811

808812
def super_classes
809813
result = []
814+
# Degenerate input can produce a cyclic superclass chain
815+
visited = [full_name]
810816
parent = self
811817
while parent = parent.superclass
818+
if parent.is_a?(String)
819+
result << parent
820+
break
821+
end
822+
break if visited.include?(parent.full_name)
823+
visited << parent.full_name
812824
result << parent
813-
return result if parent.is_a?(String)
814825
end
815826
result
816827
end

‎lib/rdoc/code_object/normal_class.rb‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ class RDoc::NormalClass < RDoc::ClassModule
1010
# RDoc::ClassModules and Strings.
1111

1212
def ancestors
13-
if String === superclass then
14-
super << superclass
15-
elsif superclass then
16-
ancestors = super
17-
ancestors << superclass
18-
ancestors.concat superclass.ancestors
19-
else
20-
super
13+
ancestors = included_ancestors
14+
super_classes.each do |sclass|
15+
ancestors << sclass
16+
ancestors.concat sclass.included_ancestors unless String === sclass
2117
end
18+
ancestors
2219
end
2320

2421
def aref_prefix # :nodoc:

‎test/rdoc/code_object/class_module_test.rb‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,6 +1358,23 @@ def test_super_classes
13581358
assert_equal [rdoc_c3_h1, rdoc_object, "BasicObject"], @c3_h2.super_classes
13591359
end
13601360

1361+
def test_super_classes_superclass_cycle
1362+
c1 = @top_level.add_class RDoc::NormalClass, 'Cycle1'
1363+
c2 = @top_level.add_class RDoc::NormalClass, 'Cycle2'
1364+
c1.superclass = c2
1365+
c2.superclass = c1
1366+
1367+
assert_equal [c2], c1.super_classes
1368+
assert_equal [c1], c2.super_classes
1369+
end
1370+
1371+
def test_super_classes_superclass_referencing_itself
1372+
klass = @top_level.add_class RDoc::NormalClass, 'Klass'
1373+
klass.superclass = klass
1374+
1375+
assert_empty klass.super_classes
1376+
end
1377+
13611378
def test_update_aliases_class
13621379
n1 = @xref_data.add_module RDoc::NormalClass, 'N1'
13631380
n1_k2 = n1.add_module RDoc::NormalClass, 'N2'

‎test/rdoc/code_object/normal_class_test.rb‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,43 @@ def test_ancestors_multilevel
2222
assert_equal [c2, c1, @object, 'BasicObject'], c3.ancestors
2323
end
2424

25+
def test_ancestors_superclass_cycle
26+
c1 = @top_level.add_class RDoc::NormalClass, 'Cycle1'
27+
c2 = @top_level.add_class RDoc::NormalClass, 'Cycle2'
28+
c1.superclass = c2
29+
c2.superclass = c1
30+
31+
assert_equal [c2], c1.ancestors
32+
assert_equal [c1], c2.ancestors
33+
end
34+
35+
def test_ancestors_superclass_referencing_itself
36+
klass = @top_level.add_class RDoc::NormalClass, 'Klass'
37+
incl = RDoc::Include.new 'Incl', ''
38+
klass.add_include incl
39+
klass.superclass = klass
40+
41+
assert_equal [incl.name], klass.ancestors
42+
end
43+
44+
def test_ancestors_chain_ending_with_nil_superclass
45+
base = @top_level.add_class RDoc::NormalClass, 'Base'
46+
base.superclass = nil
47+
sub = @top_level.add_class RDoc::NormalClass, 'Sub', 'Base'
48+
49+
assert_equal [base], sub.ancestors
50+
end
51+
52+
def test_ancestors_superclass_is_module
53+
klass = @top_level.add_class RDoc::NormalClass, 'Klass'
54+
mod = @top_level.add_module RDoc::NormalModule, 'Mod'
55+
klass.superclass = mod
56+
57+
# A module is not registered as a class, so the superclass stays a String
58+
assert_equal 'Mod', klass.superclass
59+
assert_equal ['Mod'], klass.ancestors
60+
end
61+
2562
def test_aref
2663
assert_equal 'class-c1', @c1.aref
2764
assert_equal 'class-c2-c3', @c2_c3.aref

0 commit comments

Comments
 (0)