From 43c380baad25d7ff2813315a08ff5146129017a1 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 16 Jul 2026 13:19:15 -0700 Subject: [PATCH 1/2] Avoid dispatch in ERB initialization guard The guard for methods that eval ERB source compared @_init with self.class.singleton_class by calling equal? on @_init. A marshal-loaded object can control @_init, so make the comparison through BasicObject#equal? directly instead of dispatching to the restored object. This keeps normal ERB instances working while preserving the ArgumentError for marshal-loaded instances. --- lib/erb.rb | 12 ++++++++++-- test/erb/test_erb.rb | 24 ++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index fbd146e..c04f52f 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -778,6 +778,9 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB + IDENTITY_METHOD = BasicObject.instance_method(:equal?) + private_constant :IDENTITY_METHOD + # :markup: markdown # # :call-seq: @@ -1007,7 +1010,7 @@ def run(b=new_toplevel) # [local binding]: rdoc-ref:ERB@Local+Binding # def result(b=new_toplevel) - unless @_init.equal?(self.class.singleton_class) + unless initialized_by_new? raise ArgumentError, "not initialized" end eval(@src, b, (@filename || '(erb)'), @lineno) @@ -1061,6 +1064,11 @@ def new_toplevel(vars = nil) end private :new_toplevel + def initialized_by_new? + IDENTITY_METHOD.bind_call(@_init, self.class.singleton_class) + end + private :initialized_by_new? + # :markup: markdown # # :call-seq: @@ -1087,7 +1095,7 @@ def new_toplevel(vars = nil) # ``` # def def_method(mod, methodname, fname='(ERB)') - unless @_init.equal?(self.class.singleton_class) + unless initialized_by_new? raise ArgumentError, "not initialized" end src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n" diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index b2eaa4e..1de8925 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -85,10 +85,24 @@ def test_concurrent_default_binding end class TestERBCore < Test::Unit::TestCase + class AlwaysEqual + def equal?(_other) + true + end + end + def setup @erb = ERB end + def marshal_loaded_erb(init, src: "") + erb = ERB.allocate + erb.instance_variable_set(:@src, src) + erb.instance_variable_set(:@lineno, 1) + erb.instance_variable_set(:@_init, init) + Marshal.load(Marshal.dump(erb)) + end + def test_version assert_equal(String, @erb.version.class) end @@ -664,6 +678,11 @@ def test_prohibited_marshal_load assert_raise(ArgumentError) {erb.result} end + def test_prohibited_marshal_load_result_with_overridden_equal + erb = marshal_loaded_erb(AlwaysEqual.new, src: "raise 'unreachable'") + assert_raise(ArgumentError) {erb.result} + end + def test_prohibited_marshal_load_def_method erb = ERB.allocate erb.instance_variable_set(:@src, "") @@ -673,6 +692,11 @@ def test_prohibited_marshal_load_def_method assert_raise(ArgumentError) {erb.def_method(Class.new, 'render')} end + def test_prohibited_marshal_load_def_method_with_overridden_equal + erb = marshal_loaded_erb(AlwaysEqual.new) + assert_raise(ArgumentError) {erb.def_method(Class.new, 'render')} + end + def test_prohibited_marshal_load_def_module erb = ERB.allocate erb.instance_variable_set(:@src, "") From cb03d899e4e83cd703604ec27bdfa5ec16b9839c Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Thu, 16 Jul 2026 13:38:02 -0700 Subject: [PATCH 2/2] Hide ERB init guard internals from RDoc --- lib/erb.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index c04f52f..a4b481a 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -778,7 +778,7 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB - IDENTITY_METHOD = BasicObject.instance_method(:equal?) + IDENTITY_METHOD = BasicObject.instance_method(:equal?) # :nodoc: private_constant :IDENTITY_METHOD # :markup: markdown @@ -1064,7 +1064,7 @@ def new_toplevel(vars = nil) end private :new_toplevel - def initialized_by_new? + def initialized_by_new? # :nodoc: IDENTITY_METHOD.bind_call(@_init, self.class.singleton_class) end private :initialized_by_new?