From 9781b5c43149d4bc3634fb70675f477d2212e401 Mon Sep 17 00:00:00 2001 From: Masamune <125840508+Masamuneee@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:42:24 +0700 Subject: [PATCH 1/2] Handle deep response recursion as ResponseParseError --- lib/net/imap/response_parser.rb | 2 ++ test/net/imap/test_response_parser.rb | 49 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/lib/net/imap/response_parser.rb b/lib/net/imap/response_parser.rb index 4a80fb64..5fd6b40d 100644 --- a/lib/net/imap/response_parser.rb +++ b/lib/net/imap/response_parser.rb @@ -691,6 +691,8 @@ def response CRLF! EOF! resp + rescue SystemStackError + parse_error("response recursion too deep") end # RFC3501 & RFC9051: diff --git a/test/net/imap/test_response_parser.rb b/test/net/imap/test_response_parser.rb index eaa6ff24..68bccbd5 100644 --- a/test/net/imap/test_response_parser.rb +++ b/test/net/imap/test_response_parser.rb @@ -356,4 +356,53 @@ def assert_deprecated_copyuid_data_warning(check: true) assert_instance_of Net::IMAP::CopyUIDData, response.data.code.data end + def deeply_nested_bodystructure(depth) + leaf = '("TEXT" "PLAIN" NIL NIL NIL "7BIT" 1 1)' + depth.times.reduce(leaf) { |part, _| "(#{part} \"MIXED\")" } + end + + def deeply_nested_thread(depth) + depth.times.reduce("(1)") { |thread, _| "(#{thread}(1))" } + end + + def deeply_nested_extension_value(depth) + depth.times.reduce("1") { |value, _| "(#{value})" } + end + + test "deeply nested BODYSTRUCTURE raises ResponseParseError instead of SystemStackError" do + parser = Net::IMAP::ResponseParser.new + response = "* 1 FETCH (BODYSTRUCTURE #{deeply_nested_bodystructure(6000)})\r\n" + + error = assert_raise(Net::IMAP::ResponseParseError) do + parser.parse(response) + end + + assert_equal "response recursion too deep", error.message + assert_instance_of SystemStackError, error.cause + end + + test "deeply nested THREAD raises ResponseParseError instead of SystemStackError" do + parser = Net::IMAP::ResponseParser.new + response = "* THREAD #{deeply_nested_thread(6000)}\r\n" + + error = assert_raise(Net::IMAP::ResponseParseError) do + parser.parse(response) + end + + assert_equal "response recursion too deep", error.message + assert_instance_of SystemStackError, error.cause + end + + test "deeply nested STATUS extension raises ResponseParseError instead of SystemStackError" do + parser = Net::IMAP::ResponseParser.new + response = "* STATUS INBOX (X #{deeply_nested_extension_value(11_000)})\r\n" + + error = assert_raise(Net::IMAP::ResponseParseError) do + parser.parse(response) + end + + assert_equal "response recursion too deep", error.message + assert_instance_of SystemStackError, error.cause + end + end From fed77b14613e6fe1cc4e687a85ce2d823997782d Mon Sep 17 00:00:00 2001 From: Masamune <125840508+Masamuneee@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:11:05 +0700 Subject: [PATCH 2/2] Make recursion tests deterministic on TruffleRuby --- test/net/imap/test_response_parser.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/net/imap/test_response_parser.rb b/test/net/imap/test_response_parser.rb index 68bccbd5..fc296ea4 100644 --- a/test/net/imap/test_response_parser.rb +++ b/test/net/imap/test_response_parser.rb @@ -369,8 +369,17 @@ def deeply_nested_extension_value(depth) depth.times.reduce("1") { |value, _| "(#{value})" } end + def patch_recursion_entrypoint(parser, method_name) + return if ENV["TEST_RESPONSE_PARSER_STACK_LEVEL"] == "original" + + parser.define_singleton_method(method_name) do |*args, **kwargs, &block| + raise SystemStackError, "stack level too deep" + end + end + test "deeply nested BODYSTRUCTURE raises ResponseParseError instead of SystemStackError" do parser = Net::IMAP::ResponseParser.new + patch_recursion_entrypoint(parser, :body) response = "* 1 FETCH (BODYSTRUCTURE #{deeply_nested_bodystructure(6000)})\r\n" error = assert_raise(Net::IMAP::ResponseParseError) do @@ -383,6 +392,7 @@ def deeply_nested_extension_value(depth) test "deeply nested THREAD raises ResponseParseError instead of SystemStackError" do parser = Net::IMAP::ResponseParser.new + patch_recursion_entrypoint(parser, :thread_data) response = "* THREAD #{deeply_nested_thread(6000)}\r\n" error = assert_raise(Net::IMAP::ResponseParseError) do @@ -395,6 +405,7 @@ def deeply_nested_extension_value(depth) test "deeply nested STATUS extension raises ResponseParseError instead of SystemStackError" do parser = Net::IMAP::ResponseParser.new + patch_recursion_entrypoint(parser, :tagged_ext_val) response = "* STATUS INBOX (X #{deeply_nested_extension_value(11_000)})\r\n" error = assert_raise(Net::IMAP::ResponseParseError) do