From 4fe26956098bdb1d962e5b3ec8c435cc9cd999d4 Mon Sep 17 00:00:00 2001 From: TheRealAlexxx Date: Sat, 24 Oct 2026 09:11:53 +0100 Subject: [PATCH] Resolv: build DNS wire messages as binary strings Fixes [Bug #19697]. `Resolv::DNS::MessageEncoder` accumulates the encoded packet in a UTF-8 buffer. When a query name contains non-ASCII characters, such as an internationalized domain name, the buffer becomes a UTF-8 string holding non-ASCII bytes, and binary writes into it start failing: assigning the request id in `ConnectedUDP#sender` (`request[0,2] = ...`) raises `Encoding::CompatibilityError` (incompatible character encodings: UTF-8 and ASCII-8BIT), so resolving such a name crashes before any packet is sent. The same hazard exists mid-encode, where `put_length16` backpatches a packed length into the buffer after writing a name. The wire format is bytes, so keep the buffer binary throughout: start it as ASCII-8BIT and append the binary form of byte strings, labels and character-strings. (An empty buffer adopts the encoding of the first string appended to it, so the initial conversion alone is not enough.) `Message#encode` now consistently returns an ASCII-8BIT string, matching what the socket layer and `Message.decode` already work with. The new test resolves an internationalized domain name against a stub DNS server; without the change it raises `Encoding::CompatibilityError` in `ConnectedUDP#sender`. --- lib/resolv.rb | 6 +++--- test/resolv/test_dns.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index bac338c..e73121f 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1654,7 +1654,7 @@ def encode class MessageEncoder # :nodoc: def initialize - @data = ''.dup + @data = ''.dup.b @names = {} yield self end @@ -1664,7 +1664,7 @@ def to_s end def put_bytes(d) - @data << d + @data << d.b end def put_pack(template, *d) @@ -1689,7 +1689,7 @@ def put_string(d) raise ArgumentError, "character-string is too long (#{s.bytesize} bytes, max 255): #{s.inspect}" end self.put_pack("C", s.bytesize) - @data << s + @data << s.b end def put_string_list(ds) diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index b8ecef2..51c69c0 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -567,6 +567,33 @@ def test_no_server end end + def test_internationalized_domain_name_query + bug19697 = '[Bug #19697]' + with_udp('127.0.0.1', 0) {|u| + _, server_port, _, server_address = u.addr + client_thread = Thread.new { + Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => [], :ndots => 0) {|dns| + dns.getresources("\uC6B0\uD3B8.\uB2F7\uCEF4", Resolv::DNS::Resource::IN::MX) + } + } + server_thread = Thread.new { + msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} + query = Resolv::DNS::Message.decode(msg) + reply = Resolv::DNS::Message.new(query.id) + reply.qr = 1 + reply.rd = query.rd + reply.ra = 1 + query.each_question {|name, typeclass| + reply.add_question(name, typeclass) + } + u.send(reply.encode, 0, client_address, client_port) + } + resources = client_thread.value + server_thread.join + assert_equal([], resources, bug19697) + } + end + def test_invalid_byte_comment bug9273 = '[ruby-core:59239] [Bug #9273]' Tempfile.create('resolv_test_dns_') do |tmpfile|