From e63852b0b4064b59ccbc85533d63e817be303d2c Mon Sep 17 00:00:00 2001 From: Matt Rideout Date: Wed, 12 Aug 2026 19:34:21 -0400 Subject: [PATCH] Check the 255-octet name limit against the wire length, not the presentation length --- RELEASE_NOTES.md | 4 ++++ lib/dnsruby/name.rb | 5 ++++- test/tc_name.rb | 33 +++++++++++++++++++++++++++------ 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a2f44c0..c251561 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## Unreleased + +* Enforce the 255-octet name limit (RFC 1035 Section 2.3.4, RFC 2181 Section 11) against the wire length rather than the dotted presentation length: names of 256 and 257 wire octets were accepted and encoded into messages, and now raise ResolvError + ## v1.74.0 * Resolve all configured nameservers in parallel diff --git a/lib/dnsruby/name.rb b/lib/dnsruby/name.rb index e059467..7101140 100644 --- a/lib/dnsruby/name.rb +++ b/lib/dnsruby/name.rb @@ -93,7 +93,10 @@ def self.split(name) # This method should only be called internally. # Use Name::create to create a new Name def initialize(labels, absolute=true) #:nodoc: all - total_length=labels.length-1 + # The 255-octet limit (RFC 1035 section 2.3.4, RFC 2181 section 11) is + # on the wire form, so count each label's length octet, plus the root's + # zero octet. + total_length=labels.length+1 labels.each do |l| if (!l.kind_of?Label) raise ArgumentError.new("Name::new called with non-labels. Use Name::create instead?") diff --git a/test/tc_name.rb b/test/tc_name.rb index 9470020..2dbb31a 100644 --- a/test/tc_name.rb +++ b/test/tc_name.rb @@ -20,6 +20,11 @@ class TestName < Minitest::Test include Dnsruby + # Names on either side of the 255-octet wire limit: 249 octets of label text + # in 5 labels, plus a length octet each and the root's zero octet, is 255. + AT_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 59}.e." + OVER_LIMIT_NAME = "#{'a' * 63}.#{'b' * 63}.#{'c' * 63}.#{'d' * 60}.e." + def test_label_length Name::Label.set_max_length(Name::Label::MaxLabelLength) # Other tests may have changed this # Test max label length = 63 @@ -30,13 +35,29 @@ def test_label_length end end + def wire_length(name) + MessageEncoder.new { |msg| msg.put_name(name, true) }.to_s.length + end + + # Test max name length=255. The limit is on the wire form, two octets more + # than the dotted presentation form without its trailing dot. def test_name_length - # Test max name length=255 - begin - Name.create("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123.com") - assert(false, "Name of length > 255 allowed") - rescue ResolvError - end + assert_equal(255, wire_length(Name.create(AT_LIMIT_NAME))) + + error = assert_raises(ResolvError) { Name.create(OVER_LIMIT_NAME) } + assert_match(/Name length is 256,/, error.message) + end + + # An oversized name must not reach the wire through Message either, and a + # name right at the limit must still survive the round trip. + def test_name_length_in_message + assert_raises(ResolvError) { Message.new(OVER_LIMIT_NAME, "A") } + + message = Message.new(AT_LIMIT_NAME, "A") + message.add_answer(RR.create("#{AT_LIMIT_NAME} 3600 IN NS #{AT_LIMIT_NAME}")) + decoded = Message.decode(message.encode) + assert_equal(AT_LIMIT_NAME, decoded.question[0].qname.to_s + ".") + assert_equal(AT_LIMIT_NAME, decoded.answer[0].domainname.to_s + ".") end def test_absolute