From 23f000967da6daed15bc93b5e6e6fe644bd82f2f Mon Sep 17 00:00:00 2001
From: Hiroshi SHIBATA <hsbt@ruby-lang.org>
Date: Wed, 26 Aug 2026 16:03:50 +0900
Subject: [PATCH] Do not register on-demand classes for unknown DNS types and
SvcParamKeys
Decoding an unknown (type, class) pair or an unknown SvcParamKey
registered the generated class in a constant and in ClassHash
permanently, so a malicious response could exhaust memory.
Dropping the registration means a fresh class per decode, which breaks
the class identity that Resource#== and Message#== relied on, so both now
compare through Resource::Generic.type_class_equal?.
Fixes CVE-2026-80212.
Reference:https://github.com/ruby/resolv/commit/23f000967da6daed15bc93b5e6e6fe644bd82f2f
Conflict:(1)don't change def self.create(key_number) function, This function was introduced in
https://github.com/ruby/resolv/commit/b3ced7f039
(2)don't addd test/resolv/test_resource_leak.rb file, it tests SvcParamKeys.
lib/resolv.rb | 34 +++++++++-
test/resolv/test_resource.rb | 61 ++++++++++++++++++
2 files changed, 92 insertions(+), 3 deletions(-)
@@ -1372,12 +1372,24 @@ class Resolv
@rd == other.rd &&
@ra == other.ra &&
@rcode == other.rcode &&
- @question == other.question &&
+ question_equal?(other.question) &&
@answer == other.answer &&
@authority == other.authority &&
@additional == other.additional
end
+ # A question holds the resource class itself, and decoding creates a fresh
+ # class for each unknown type, so the classes cannot be compared by
+ # identity alone.
+ private def question_equal?(other_question) # :nodoc:
+ return false unless @question.length == other_question.length
+ @question.zip(other_question) {|(name, typeclass), (o_name, o_typeclass)|
+ return false unless name == o_name &&
+ Resource::Generic.type_class_equal?(typeclass, o_typeclass)
+ }
+ return true
+ end
+
def add_question(name, typeclass)
@question << [Name.create(name), typeclass]
end
@@ -1777,12 +1789,28 @@ class Resolv
return self.new(msg.get_bytes)
end
+ # create makes a fresh class for each decoded resource, so the type and
+ # class values have to be compared instead of the class itself.
+ def self.type_class_equal?(klass, other) # :nodoc:
+ return true if klass.equal?(other)
+ Generic > klass && Generic > other &&
+ klass::TypeValue == other::TypeValue &&
+ klass::ClassValue == other::ClassValue
+ end
+
+ def ==(other) # :nodoc:
+ return other.is_a?(Generic) &&
+ Generic.type_class_equal?(self.class, other.class) &&
+ @data == other.data
+ end
+
def self.create(type_value, class_value) # :nodoc:
c = Class.new(Generic)
c.const_set(:TypeValue, type_value)
c.const_set(:ClassValue, class_value)
- Generic.const_set("Type#{type_value}_Class#{class_value}", c)
- ClassHash[[type_value, class_value]] = c
+ # Not registered in a constant or in ClassHash. get_class creates a
+ # class for every unknown (type, class) pair, so registering them
+ # permanently would let a malicious response exhaust memory.
return c
end
end
@@ -23,4 +23,65 @@ class TestResolvResource < Test::Unit::TestCase
def test_coord
Resolv::LOC::Coord.create('1 2 1.1 N')
end
+
+ # Decoding an unknown (type, class) pair builds a fresh class every time, so
+ # equality must not rest on the class identity.
+ def test_generic_equality
+ wire = generic_answer(40000, "\x01\x02\x03")
+ rr1 = decode_generic(wire)
+ rr2 = decode_generic(wire)
+
+ assert_not_same rr1.class, rr2.class
+ assert_equal rr1, rr2
+ assert rr1.eql?(rr2)
+ assert_equal rr1.hash, rr2.hash
+ assert_equal Resolv::DNS::Message.decode(wire), Resolv::DNS::Message.decode(wire)
+ end
+
+ # Any descendant counts, not just a class create returned.
+ def test_generic_equality_between_descendants
+ generic = Resolv::DNS::Resource::Generic
+ direct = generic.create(40000, 60000)
+ descendant = Class.new(generic.create(40000, 60000))
+
+ assert_equal direct.new("\x01\x02\x03"), descendant.new("\x01\x02\x03")
+ assert_equal descendant.new("\x01\x02\x03"), direct.new("\x01\x02\x03")
+ assert_equal generic.new("\x01\x02\x03"), generic.new("\x01\x02\x03")
+ assert_not_equal direct.new("\x01\x02\x03"),
+ Class.new(generic.create(40001, 60000)).new("\x01\x02\x03")
+ end
+
+ def test_generic_inequality
+ rr = decode_generic(generic_answer(40000, "\x01\x02\x03"))
+
+ assert_not_equal rr, decode_generic(generic_answer(40001, "\x01\x02\x03"))
+ assert_not_equal rr, decode_generic(generic_answer(40000, "\x09\x09\x09"))
+ assert_not_equal rr, Resolv::DNS::Resource::IN::A.new("192.168.0.1")
+ end
+
+ # A question holds the resource class itself, so it needs the same treatment.
+ def test_generic_question_equality
+ wire = generic_question(40000)
+
+ assert_equal Resolv::DNS::Message.decode(wire), Resolv::DNS::Message.decode(wire)
+ assert_not_equal Resolv::DNS::Message.decode(wire),
+ Resolv::DNS::Message.decode(generic_question(40001))
+ end
+
+ private def header(qdcount, ancount)
+ "\x00\x00\x00\x00".b + [qdcount, ancount, 0, 0].pack('nnnn')
+ end
+
+ private def generic_answer(type, rdata)
+ rdata = rdata.b
+ (header(0, 1) + "\x00".b + [type, 60000, 0, rdata.bytesize].pack('nnNn') + rdata).b
+ end
+
+ private def generic_question(type)
+ (header(1, 0) + "\x07example\x03com\x00".b + [type, 60000].pack('nn')).b
+ end
+
+ private def decode_generic(wire)
+ Resolv::DNS::Message.decode(wire).answer.first[2]
+ end
end
--
2.43.0