From 96dc3d15fea8b0661fc8ec29818ca76749eb8ec2 Mon Sep 17 00:00:00 2001 From: Jean-Samuel Aubry-Guzzi Date: Wed, 29 Oct 2025 08:31:26 -0400 Subject: [PATCH 1/2] Fix TCP Requester #recv_reply --- lib/resolv.rb | 5 +++- test/resolv/test_dns.rb | 59 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 73410f1..03b8123 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -929,8 +929,11 @@ def initialize(host, port=Port) end def recv_reply(readable_socks) - len = readable_socks[0].read(2).unpack('n')[0] + len_data = readable_socks[0].read(2) + raise Errno::ECONNRESET if len_data.nil? + len = len_data.unpack('n')[0] reply = @socks[0].read(len) + raise Errno::ECONNRESET if reply.nil? return reply, nil end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 2bb8061..7764f68 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -822,4 +822,63 @@ def test_multiple_servers_with_timeout_and_truncated_tcp_fallback end end end + + def test_tcp_connection_closed_before_length + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end + + def test_tcp_connection_closed_after_length + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.send([100].pack('n'), 0) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end end From 9c640bdc4a01729b404dddc26e28e2abc60ed3bc Mon Sep 17 00:00:00 2001 From: Jean-Samuel Aubry-Guzzi Date: Mon, 8 Dec 2025 11:22:55 -0500 Subject: [PATCH 2/2] Handle TCP Requester #recv_reply incomplete data --- lib/resolv.rb | 7 +++-- test/resolv/test_dns.rb | 61 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 03b8123..c84c172 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -720,7 +720,8 @@ def request(sender, tout) begin reply, from = recv_reply(select_result[0]) rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD - Errno::ECONNRESET # Windows + Errno::ECONNRESET, # Windows + EOFError # No name server running on the server? # Don't wait anymore. raise ResolvTimeout @@ -930,10 +931,10 @@ def initialize(host, port=Port) def recv_reply(readable_socks) len_data = readable_socks[0].read(2) - raise Errno::ECONNRESET if len_data.nil? + raise EOFError if len_data.nil? || len_data.bytesize != 2 len = len_data.unpack('n')[0] reply = @socks[0].read(len) - raise Errno::ECONNRESET if reply.nil? + raise EOFError if reply.nil? || reply.bytesize != len return reply, nil end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 7764f68..a91800b 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -881,4 +881,65 @@ def test_tcp_connection_closed_after_length client_thread.join end end + + def test_tcp_connection_closed_with_partial_length_prefix + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.write "A" # 1 byte + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end + + def test_tcp_connection_closed_with_partial_message_body + with_tcp('127.0.0.1', 0) do |t| + _, server_port, _, server_address = t.addr + + server_thread = Thread.new do + ct = t.accept + ct.recv(512) + ct.write([10].pack('n')) # length 10 + ct.write "12345" # 5 bytes (partial) + ct.close + end + + client_thread = Thread.new do + requester = Resolv::DNS::Requester::TCP.new(server_address, server_port) + begin + msg = Resolv::DNS::Message.new + msg.add_question('example.org', Resolv::DNS::Resource::IN::A) + sender = requester.sender(msg, msg) + assert_raise(Resolv::ResolvTimeout) do + requester.request(sender, 2) + end + ensure + requester.close + end + end + + server_thread.join + client_thread.join + end + end end