Backport upstream patch to update resolv to 0.7.2
Resolves: CVE-2026-80212 (rhbz#2527308) Resolves: CVE-2026-80213 (rhbz#2527310)
This commit is contained in:
parent
11eb00c7bf
commit
fe82f3732e
2 changed files with 998 additions and 4 deletions
984
ruby-4_0-pr18528-update-resolv-0_7_2.patch
Normal file
984
ruby-4_0-pr18528-update-resolv-0_7_2.patch
Normal file
|
|
@ -0,0 +1,984 @@
|
|||
From 88ba1d4e1370f4221e060e4ed53ef191834e5838 Mon Sep 17 00:00:00 2001
|
||||
From: Hiroshi SHIBATA <hsbt@ruby-lang.org>
|
||||
Date: Thu, 27 Aug 2026 12:31:49 +0900
|
||||
Subject: [PATCH] Bump up resolv-0.7.2 for Ruby 4.0
|
||||
|
||||
Backport of the August 2026 resolv security release.
|
||||
|
||||
CVE-2026-80212
|
||||
CVE-2026-80213
|
||||
|
||||
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||||
---
|
||||
lib/resolv.rb | 114 +++++++++--
|
||||
test/resolv/test_dns.rb | 319 ++++++++++++++++++++++++++++++
|
||||
test/resolv/test_resource.rb | 61 ++++++
|
||||
test/resolv/test_resource_leak.rb | 101 ++++++++++
|
||||
4 files changed, 582 insertions(+), 13 deletions(-)
|
||||
create mode 100644 test/resolv/test_resource_leak.rb
|
||||
|
||||
diff --git a/lib/resolv.rb b/lib/resolv.rb
|
||||
index 0e62aaf8510496..a2fe692b38f6cf 100644
|
||||
--- a/lib/resolv.rb
|
||||
+++ b/lib/resolv.rb
|
||||
@@ -35,7 +35,7 @@
|
||||
class Resolv
|
||||
|
||||
# The version string
|
||||
- VERSION = "0.7.0"
|
||||
+ VERSION = "0.7.2"
|
||||
|
||||
##
|
||||
# Looks up the first IP address for +name+.
|
||||
@@ -487,13 +487,18 @@ def each_name(address)
|
||||
# * Resolv::DNS::Resource::IN::A
|
||||
# * Resolv::DNS::Resource::IN::AAAA
|
||||
# * Resolv::DNS::Resource::IN::ANY
|
||||
+ # * Resolv::DNS::Resource::IN::CAA
|
||||
# * Resolv::DNS::Resource::IN::CNAME
|
||||
# * Resolv::DNS::Resource::IN::HINFO
|
||||
+ # * Resolv::DNS::Resource::IN::HTTPS
|
||||
+ # * Resolv::DNS::Resource::IN::LOC
|
||||
# * Resolv::DNS::Resource::IN::MINFO
|
||||
# * Resolv::DNS::Resource::IN::MX
|
||||
# * Resolv::DNS::Resource::IN::NS
|
||||
# * Resolv::DNS::Resource::IN::PTR
|
||||
# * Resolv::DNS::Resource::IN::SOA
|
||||
+ # * Resolv::DNS::Resource::IN::SRV
|
||||
+ # * Resolv::DNS::Resource::IN::SVCB
|
||||
# * Resolv::DNS::Resource::IN::TXT
|
||||
# * Resolv::DNS::Resource::IN::WKS
|
||||
#
|
||||
@@ -721,7 +726,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,8 +936,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 EOFError if len_data.nil? || len_data.bytesize != 2
|
||||
+ len = len_data.unpack('n')[0]
|
||||
reply = @socks[0].read(len)
|
||||
+ raise EOFError if reply.nil? || reply.bytesize != len
|
||||
return reply, nil
|
||||
end
|
||||
|
||||
@@ -1244,6 +1253,13 @@ def self.split(arg)
|
||||
|
||||
class Str # :nodoc:
|
||||
def initialize(string)
|
||||
+ # A label is limited to 63 octets. [RFC 1035 2.3.4] Checking it here
|
||||
+ # makes it an invariant of the object: every label, however it was
|
||||
+ # built, fits in its length octet and cannot wrap it. Callers turn
|
||||
+ # this into the error their own contract promises.
|
||||
+ if string.bytesize > 63
|
||||
+ raise ArgumentError, "DNS label is too long (#{string.bytesize} bytes, max 63): #{string.inspect}"
|
||||
+ end
|
||||
@string = string
|
||||
# case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
|
||||
# This assumes @string is given in ASCII compatible encoding.
|
||||
@@ -1289,7 +1305,26 @@ def self.create(arg)
|
||||
when Name
|
||||
return arg
|
||||
when String
|
||||
- return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
|
||||
+ # A hostname is runtime data rather than a programming mistake, so
|
||||
+ # both size limits surface as ResolvError to stay rescuable alongside
|
||||
+ # the rest of name resolution. The type check below is a caller
|
||||
+ # mistake and keeps raising ArgumentError.
|
||||
+ begin
|
||||
+ labels = Label.split(arg)
|
||||
+ rescue ArgumentError => e
|
||||
+ raise ResolvError.new(e.message)
|
||||
+ end
|
||||
+ # Label::Str enforces the per-label limit. Only the total is knowable
|
||||
+ # here, and it counts the encoded form, so size starts at 1 for the
|
||||
+ # root label's terminating zero octet. [RFC 1035 2.3.4, 3.1]
|
||||
+ size = 1
|
||||
+ labels.each do |label|
|
||||
+ size += 1 + label.string.bytesize
|
||||
+ if size > 255
|
||||
+ raise ResolvError.new("DNS name is too long (#{size} octets, max 255): #{arg.inspect}")
|
||||
+ end
|
||||
+ end
|
||||
+ return Name.new(labels, /\.\z/ =~ arg ? true : false)
|
||||
else
|
||||
raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
|
||||
end
|
||||
@@ -1411,12 +1446,24 @@ def ==(other)
|
||||
@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
|
||||
@@ -1523,8 +1570,15 @@ def put_length16
|
||||
end
|
||||
|
||||
def put_string(d)
|
||||
- self.put_pack("C", d.length)
|
||||
- @data << d
|
||||
+ s = d.to_s
|
||||
+ # A character-string is prefixed by a single length octet, so it can
|
||||
+ # hold at most 255 octets. [RFC 1035 3.3] Reject anything longer to
|
||||
+ # avoid silently truncating the length to its low 8 bits (mod 256).
|
||||
+ if s.bytesize > 255
|
||||
+ raise ArgumentError, "character-string is too long (#{s.bytesize} bytes, max 255): #{s.inspect}"
|
||||
+ end
|
||||
+ self.put_pack("C", s.bytesize)
|
||||
+ @data << s
|
||||
end
|
||||
|
||||
def put_string_list(ds)
|
||||
@@ -1554,7 +1608,17 @@ def put_labels(d, compress: true)
|
||||
end
|
||||
|
||||
def put_label(d)
|
||||
- self.put_string(d.to_s)
|
||||
+ s = d.to_s
|
||||
+ # Label::Str applies this limit when a label is built, so what is left
|
||||
+ # for here is a raw string handed straight to put_labels. The two ways
|
||||
+ # an over-long label goes wrong differ: 64 to 255 octets write a length
|
||||
+ # octet in the reserved or compression pointer range, and 256 or more
|
||||
+ # wrap it mod 256. Either way the encoded name stops being the name the
|
||||
+ # caller asked for. [RFC 1035 2.3.4, 4.1.4]
|
||||
+ if s.bytesize > 63
|
||||
+ raise ArgumentError, "DNS label is too long (#{s.bytesize} bytes, max 63): #{s.inspect}"
|
||||
+ end
|
||||
+ self.put_string(s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1680,7 +1744,9 @@ def get_labels
|
||||
prev_index = @index
|
||||
save_index = nil
|
||||
d = []
|
||||
- size = -1
|
||||
+ # size counts the encoded form, so it starts at 1 for the root
|
||||
+ # label's terminating zero octet. [RFC 1035 3.1]
|
||||
+ size = 1
|
||||
while true
|
||||
raise DecodeError.new("limit exceeded") if @limit <= @index
|
||||
case @data.getbyte(@index)
|
||||
@@ -1711,6 +1777,11 @@ def get_labels
|
||||
|
||||
def get_label
|
||||
return Label::Str.new(self.get_string)
|
||||
+ rescue ArgumentError => e
|
||||
+ # A length octet of 64..191 is reserved rather than a label length,
|
||||
+ # but this decoder used to read it as one. [RFC 1035 4.1.4] Report it
|
||||
+ # the way the rest of a malformed message is reported.
|
||||
+ raise DecodeError.new(e.message)
|
||||
end
|
||||
|
||||
def get_question
|
||||
@@ -1898,8 +1969,9 @@ def self.create(key_number)
|
||||
key_name = :"key#{key_number}"
|
||||
c.const_set(:KeyName, key_name)
|
||||
c.const_set(:KeyNumber, key_number)
|
||||
- self.const_set(:"Key#{key_number}", c)
|
||||
- ClassHash[key_name] = ClassHash[key_number] = c
|
||||
+ # Not registered in a constant or in ClassHash. ClassHash creates a
|
||||
+ # class for every unknown SvcParamKey, so registering them
|
||||
+ # permanently would let a malicious response exhaust memory.
|
||||
return c
|
||||
end
|
||||
end
|
||||
@@ -2206,12 +2278,28 @@ def self.decode_rdata(msg) # :nodoc:
|
||||
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
|
||||
diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb
|
||||
index d5d2648e1bc649..34ae4aeb9a33c3 100644
|
||||
--- a/test/resolv/test_dns.rb
|
||||
+++ b/test/resolv/test_dns.rb
|
||||
@@ -636,6 +636,205 @@ def test_too_long_address
|
||||
end
|
||||
end
|
||||
|
||||
+ # A DNS label is limited to 63 octets. [RFC 1035 2.3.4] Writing a longer label
|
||||
+ # through the label path must raise instead of overflowing the length octet.
|
||||
+ def test_put_label_rejects_label_over_63_octets
|
||||
+ Resolv::DNS::Message::MessageEncoder.new {|msg|
|
||||
+ assert_nothing_raised { msg.put_label("a" * 63) }
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ msg.put_label("a" * 64)
|
||||
+ end
|
||||
+ }
|
||||
+ # put_labels drives put_label, so the same guard applies to the name path.
|
||||
+ Resolv::DNS::Message::MessageEncoder.new {|msg|
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ msg.put_labels(["a" * 64])
|
||||
+ end
|
||||
+ }
|
||||
+ end
|
||||
+
|
||||
+ # The per-label limit is an invariant of Label::Str, so no label object can
|
||||
+ # exist that would overflow its length octet. [RFC 1035 2.3.4]
|
||||
+ def test_label_str_rejects_label_over_63_octets
|
||||
+ assert_nothing_raised { Resolv::DNS::Label::Str.new("a" * 63) }
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Label::Str.new("a" * 64)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # Every way of building a name goes through Label::Str, so the paths that
|
||||
+ # skip Name.create are covered too.
|
||||
+ def test_label_length_is_enforced_on_every_construction_path
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Name.new(["a" * 64])
|
||||
+ end
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Label.split("a" * 64)
|
||||
+ end
|
||||
+ # Config#generate_candidates appends search domains with Name.new, and the
|
||||
+ # search list itself comes from Label.split, so a resolv.conf carrying an
|
||||
+ # over-long label is rejected when the config is read.
|
||||
+ config = Resolv::DNS::Config.new(nameserver: ['127.0.0.1'],
|
||||
+ search: ["a" * 64], ndots: 1)
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ config.lazy_initialize
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ def test_name_create_rejects_too_long_label
|
||||
+ assert_nothing_raised { Resolv::DNS::Name.create("a" * 63) }
|
||||
+ assert_raise_with_message(Resolv::ResolvError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Name.create("a" * 64)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ def test_name_create_rejects_too_long_name
|
||||
+ # Five 63-octet labels total 321 encoded octets, over the 255 octet limit,
|
||||
+ # while each individual label is still valid.
|
||||
+ too_long = (["a" * 63] * 5).join(".")
|
||||
+ assert_raise_with_message(Resolv::ResolvError, /DNS name is too long/) do
|
||||
+ Resolv::DNS::Name.create(too_long)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # A hostname is runtime data, so an over-long one has to stay rescuable the
|
||||
+ # way the rest of name resolution is. It reaches Name.create through
|
||||
+ # Config#generate_candidates, which runs outside Config#resolv's own rescue.
|
||||
+ def test_oversized_name_is_rescuable_as_resolv_error
|
||||
+ dns = Resolv::DNS.new(nameserver_port: [['127.0.0.1', 53]])
|
||||
+ assert_raise(Resolv::ResolvError) { dns.getaddress("a" * 64) }
|
||||
+ assert_raise(Resolv::ResolvError) { dns.getaddress((["a" * 63] * 5).join(".")) }
|
||||
+ ensure
|
||||
+ dns&.close
|
||||
+ end
|
||||
+
|
||||
+ # A length octet of 64..191 is reserved, not a label length, but this decoder
|
||||
+ # read it as one and accepted labels no encoder should ever produce.
|
||||
+ # [RFC 1035 4.1.4] Rejecting them has to look like any other malformed
|
||||
+ # message, so the caller's rescue DecodeError still covers it.
|
||||
+ def test_decode_rejects_label_over_63_octets
|
||||
+ message = ->(n) {
|
||||
+ [0, 0x8180, 1, 0, 0, 0].pack("n*") +
|
||||
+ [n].pack("C") + ("a" * n) + "\0" + [1, 1].pack("nn")
|
||||
+ }
|
||||
+ assert_nothing_raised { Resolv::DNS::Message.decode(message.call(63)) }
|
||||
+ [64, 100, 191].each do |n|
|
||||
+ assert_raise_with_message(Resolv::DNS::DecodeError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Message.decode(message.call(n))
|
||||
+ end
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # The type check is a caller mistake rather than runtime data, so it keeps
|
||||
+ # raising ArgumentError.
|
||||
+ def test_name_create_still_raises_argument_error_for_wrong_type
|
||||
+ assert_raise_with_message(ArgumentError, /cannot interpret as DNS name/) do
|
||||
+ Resolv::DNS::Name.create(123)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # The 255 octet limit counts the encoded form, including each label's length
|
||||
+ # octet and the root label's terminating zero octet. [RFC 1035 2.3.4, 3.1]
|
||||
+ # So the longest legal name encodes to exactly 255 octets.
|
||||
+ def test_name_create_total_length_boundary
|
||||
+ at_limit = (["a" * 63] * 3 + ["a" * 61]).join(".")
|
||||
+ name = Resolv::DNS::Name.create(at_limit)
|
||||
+ encoded = Resolv::DNS::Message::MessageEncoder.new {|msg| msg.put_name(name) }.to_s
|
||||
+ assert_equal(255, encoded.bytesize, "longest legal name encodes to 255 octets")
|
||||
+
|
||||
+ over_limit = (["a" * 63] * 3 + ["a" * 62]).join(".")
|
||||
+ assert_raise_with_message(Resolv::ResolvError, /DNS name is too long/) do
|
||||
+ Resolv::DNS::Name.create(over_limit)
|
||||
+ end
|
||||
+
|
||||
+ # Four 63-octet labels encode to 257 octets. Counting the presentation
|
||||
+ # form instead of the encoded form lets these two extra octets through.
|
||||
+ assert_raise_with_message(Resolv::ResolvError, /DNS name is too long/) do
|
||||
+ Resolv::DNS::Name.create((["a" * 63] * 4).join("."))
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # The decoder enforces the same limit, counted the same way.
|
||||
+ def test_get_labels_total_length_boundary
|
||||
+ encode = ->(labels) {
|
||||
+ Resolv::DNS::Message::MessageEncoder.new {|msg|
|
||||
+ msg.put_labels(labels.map {|l| Resolv::DNS::Label::Str.new(l) })
|
||||
+ }.to_s
|
||||
+ }
|
||||
+
|
||||
+ at_limit = encode.call(["a" * 63] * 3 + ["a" * 61])
|
||||
+ assert_equal(255, at_limit.bytesize)
|
||||
+ Resolv::DNS::Message::MessageDecoder.new(at_limit) {|msg|
|
||||
+ assert_equal(4, msg.get_labels.length)
|
||||
+ }
|
||||
+
|
||||
+ over_limit = encode.call(["a" * 63] * 4)
|
||||
+ assert_equal(257, over_limit.bytesize)
|
||||
+ assert_raise_with_message(Resolv::DNS::DecodeError, /name label data exceed 255 octets/) do
|
||||
+ Resolv::DNS::Message::MessageDecoder.new(over_limit) {|msg| msg.get_labels }
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # A single 262-octet label whose bytes start with "target\x03com\x00". The
|
||||
+ # old encoder wrote the length octet as 262 & 0xff == 6, so the wire bytes
|
||||
+ # decoded to the unrelated name "target.com" (query name confusion /
|
||||
+ # allowlist bypass).
|
||||
+ def test_encoder_rejects_label_length_wrap
|
||||
+ poc_label = "target".b + "\x03com\x00".b + ("a".b * 251)
|
||||
+ assert_equal(262, poc_label.bytesize)
|
||||
+ assert_equal(6, poc_label.bytesize & 0xff, "precondition: the length octet wraps to 6")
|
||||
+
|
||||
+ # The bytes the buggy encoder would have emitted really do decode to a
|
||||
+ # different name. This is the vulnerability being fixed.
|
||||
+ wrapped = [poc_label.bytesize & 0xff].pack("C") + poc_label
|
||||
+ Resolv::DNS::Message::MessageDecoder.new(wrapped) {|msg|
|
||||
+ assert_equal("target.com", msg.get_labels.map(&:to_s).join("."))
|
||||
+ }
|
||||
+
|
||||
+ # The fixed encoder refuses to emit it instead of silently wrapping, so it
|
||||
+ # can no longer produce "target.com" from this input.
|
||||
+ Resolv::DNS::Message::MessageEncoder.new {|msg|
|
||||
+ assert_raise_with_message(ArgumentError, /DNS label is too long/) do
|
||||
+ msg.put_label(poc_label)
|
||||
+ end
|
||||
+ }
|
||||
+ assert_raise_with_message(Resolv::ResolvError, /DNS label is too long/) do
|
||||
+ Resolv::DNS::Name.create(poc_label)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ # A character-string (e.g. TXT rdata) is prefixed by a single length octet and
|
||||
+ # may legitimately be up to 255 octets, so the 63 octet label limit must not
|
||||
+ # leak into put_string. [RFC 1035 3.3]
|
||||
+ def test_put_string_allows_character_string_up_to_255
|
||||
+ [64, 200, 255].each do |n|
|
||||
+ s = "a" * n
|
||||
+ m = Resolv::DNS::Message::MessageEncoder.new {|msg| msg.put_string(s) }
|
||||
+ encoded = m.to_s
|
||||
+ assert_equal(n, encoded.getbyte(0), "length octet for #{n} byte string")
|
||||
+ assert_equal(n + 1, encoded.bytesize)
|
||||
+ Resolv::DNS::Message::MessageDecoder.new(encoded) {|msg|
|
||||
+ assert_equal(s, msg.get_string)
|
||||
+ }
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ def test_txt_record_roundtrip_with_long_character_strings
|
||||
+ txt = Resolv::DNS::Resource::IN::TXT.new("a" * 255, "b" * 64)
|
||||
+ m = Resolv::DNS::Message.new(0)
|
||||
+ m.add_answer("example.com.", 3600, txt)
|
||||
+ decoded = Resolv::DNS::Message.decode(m.encode)
|
||||
+ _, _, res = decoded.answer.first
|
||||
+ assert_equal(["a" * 255, "b" * 64], res.strings)
|
||||
+ end
|
||||
+
|
||||
+ # put_string still guards against the length octet wrapping past 255 octets.
|
||||
+ def test_put_string_rejects_over_255_octets
|
||||
+ assert_raise_with_message(ArgumentError, /character-string is too long/) do
|
||||
+ Resolv::DNS::Message::MessageEncoder.new {|msg| msg.put_string("a" * 256) }
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
def assert_no_fd_leak
|
||||
socket = assert_throw(self) do |tag|
|
||||
Resolv::DNS.stub(:bind_random_port, ->(s, *) {throw(tag, s)}) do
|
||||
@@ -822,4 +1021,124 @@ 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
|
||||
+
|
||||
+ 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
|
||||
diff --git a/test/resolv/test_resource.rb b/test/resolv/test_resource.rb
|
||||
index 434380236e6721..b4e56ce4bb8ff5 100644
|
||||
--- a/test/resolv/test_resource.rb
|
||||
+++ b/test/resolv/test_resource.rb
|
||||
@@ -24,6 +24,67 @@ 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
|
||||
+
|
||||
def test_srv_no_compress
|
||||
# Domain name in SRV RDATA should not be compressed
|
||||
issue29 = 'https://github.com/ruby/resolv/issues/29'
|
||||
diff --git a/test/resolv/test_resource_leak.rb b/test/resolv/test_resource_leak.rb
|
||||
new file mode 100644
|
||||
index 00000000000000..4628a3d2857677
|
||||
--- /dev/null
|
||||
+++ b/test/resolv/test_resource_leak.rb
|
||||
@@ -0,0 +1,101 @@
|
||||
+# frozen_string_literal: false
|
||||
+require 'test/unit'
|
||||
+require 'resolv'
|
||||
+
|
||||
+# Decoding a response with unknown (type, class) pairs or unknown SvcParamKeys
|
||||
+# used to register a generated class permanently, so a malicious response could
|
||||
+# exhaust memory even after the response was discarded.
|
||||
+class TestResolvResourceLeak < Test::Unit::TestCase
|
||||
+ # Number of dynamically-registered "Type<n>_Class<n>" constants on +mod+.
|
||||
+ def type_const_count(mod)
|
||||
+ mod.constants(false).count { |c| c.to_s.match?(/\AType\d+_Class\d+\z/) }
|
||||
+ end
|
||||
+
|
||||
+ def svcparam_key_const_count
|
||||
+ Resolv::DNS::SvcParam::Generic.constants(false).count { |c| c.to_s.match?(/\AKey\d+\z/) }
|
||||
+ end
|
||||
+
|
||||
+ # A DNS response whose answer section holds +count+ RRs, each with a distinct
|
||||
+ # unknown (type, class) pair.
|
||||
+ def unknown_typeclass_response(count)
|
||||
+ body = "".b
|
||||
+ count.times do |i|
|
||||
+ type = 40000 + i
|
||||
+ klass = 60000
|
||||
+ rdata = "\x01\x02\x03".b
|
||||
+ body << "\x00".b # NAME = root
|
||||
+ body << [type, klass, 0, rdata.bytesize].pack('nnNn')
|
||||
+ body << rdata
|
||||
+ end
|
||||
+ header = "\x00\x00\x00\x00".b + [0, count, 0, 0].pack('nnnn')
|
||||
+ (header + body).b
|
||||
+ end
|
||||
+
|
||||
+ # An SVCB RR (type 64) carrying +count+ distinct unknown SvcParamKeys.
|
||||
+ def unknown_svcparam_response(count)
|
||||
+ rdata = "".b
|
||||
+ rdata << [1].pack('n') # SvcPriority
|
||||
+ rdata << "\x03foo\x07example\x03com\x00".b # TargetName
|
||||
+ count.times do |i|
|
||||
+ key = 1000 + i
|
||||
+ val = "x".b
|
||||
+ rdata << [key, val.bytesize].pack('nn') << val
|
||||
+ end
|
||||
+ header = "\x00\x00\x00\x00".b + [0, 1, 0, 0].pack('nnnn')
|
||||
+ name = "\x07example\x03com\x00".b
|
||||
+ rr = name + [64, 1, 0, rdata.bytesize].pack('nnNn') + rdata
|
||||
+ (header + rr).b
|
||||
+ end
|
||||
+
|
||||
+ def test_unknown_typeclass_does_not_leak_classes
|
||||
+ resource = Resolv::DNS::Resource
|
||||
+ generic = Resolv::DNS::Resource::Generic
|
||||
+
|
||||
+ before_resource = type_const_count(resource)
|
||||
+ before_generic = type_const_count(generic)
|
||||
+
|
||||
+ [100, 1000].each do |count|
|
||||
+ msg = unknown_typeclass_response(count)
|
||||
+ 3.times { Resolv::DNS::Message.decode(msg) }
|
||||
+ end
|
||||
+ GC.start
|
||||
+
|
||||
+ assert_equal before_resource, type_const_count(resource),
|
||||
+ 'decoding unknown (type, class) RRs must not register new Resource constants'
|
||||
+ assert_equal before_generic, type_const_count(generic),
|
||||
+ 'decoding unknown (type, class) RRs must not register new Generic constants'
|
||||
+ end
|
||||
+
|
||||
+ def test_unknown_svcparam_key_does_not_leak_classes
|
||||
+ class_hash = Resolv::DNS::SvcParam::ClassHash
|
||||
+
|
||||
+ before_consts = svcparam_key_const_count
|
||||
+ before_hash = class_hash.size
|
||||
+
|
||||
+ [100, 1000].each do |count|
|
||||
+ msg = unknown_svcparam_response(count)
|
||||
+ 3.times { Resolv::DNS::Message.decode(msg) }
|
||||
+ end
|
||||
+ GC.start
|
||||
+
|
||||
+ assert_equal before_consts, svcparam_key_const_count,
|
||||
+ 'decoding unknown SvcParamKeys must not register new Generic constants'
|
||||
+ assert_equal before_hash, class_hash.size,
|
||||
+ 'decoding unknown SvcParamKeys must not grow SvcParam::ClassHash'
|
||||
+ end
|
||||
+
|
||||
+ # Dropping the permanent registration must not break decoding of the unknown
|
||||
+ # values themselves.
|
||||
+ def test_unknown_values_still_decode
|
||||
+ msg = Resolv::DNS::Message.decode(unknown_typeclass_response(3))
|
||||
+ assert_equal 3, msg.answer.size
|
||||
+ _, _, rr = msg.answer.first
|
||||
+ assert_kind_of Resolv::DNS::Resource::Generic, rr
|
||||
+ assert_equal "\x01\x02\x03".b, rr.data
|
||||
+
|
||||
+ msg = Resolv::DNS::Message.decode(unknown_svcparam_response(3))
|
||||
+ _, _, svcb = msg.answer.first
|
||||
+ assert_equal 3, svcb.params.count
|
||||
+ assert_equal "x".b, svcb.params[:key1000].value
|
||||
+ end
|
||||
+end
|
||||
--- ruby-4.0.6.orig/lib/rubygems/vendor/resolv/lib/resolv.rb 2026-07-14 09:22:10.000000000 +0900
|
||||
+++ ruby-4.0.6/lib/rubygems/vendor/resolv/lib/resolv.rb 2026-09-03 17:16:18.543025164 +0900
|
||||
@@ -35,7 +35,7 @@
|
||||
class Gem::Resolv
|
||||
|
||||
# The version string
|
||||
- VERSION = "0.7.0"
|
||||
+ VERSION = "0.7.2"
|
||||
|
||||
##
|
||||
# Looks up the first IP address for +name+.
|
||||
@@ -487,13 +487,18 @@ def each_name(address)
|
||||
# * Gem::Resolv::DNS::Resource::IN::A
|
||||
# * Gem::Resolv::DNS::Resource::IN::AAAA
|
||||
# * Gem::Resolv::DNS::Resource::IN::ANY
|
||||
+ # * Gem::Resolv::DNS::Resource::IN::CAA
|
||||
# * Gem::Resolv::DNS::Resource::IN::CNAME
|
||||
# * Gem::Resolv::DNS::Resource::IN::HINFO
|
||||
+ # * Gem::Resolv::DNS::Resource::IN::HTTPS
|
||||
+ # * Gem::Resolv::DNS::Resource::IN::LOC
|
||||
# * Gem::Resolv::DNS::Resource::IN::MINFO
|
||||
# * Gem::Resolv::DNS::Resource::IN::MX
|
||||
# * Gem::Resolv::DNS::Resource::IN::NS
|
||||
# * Gem::Resolv::DNS::Resource::IN::PTR
|
||||
# * Gem::Resolv::DNS::Resource::IN::SOA
|
||||
+ # * Gem::Resolv::DNS::Resource::IN::SRV
|
||||
+ # * Gem::Resolv::DNS::Resource::IN::SVCB
|
||||
# * Gem::Resolv::DNS::Resource::IN::TXT
|
||||
# * Gem::Resolv::DNS::Resource::IN::WKS
|
||||
#
|
||||
@@ -721,7 +726,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,8 +936,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 EOFError if len_data.nil? || len_data.bytesize != 2
|
||||
+ len = len_data.unpack('n')[0]
|
||||
reply = @socks[0].read(len)
|
||||
+ raise EOFError if reply.nil? || reply.bytesize != len
|
||||
return reply, nil
|
||||
end
|
||||
|
||||
@@ -1244,6 +1253,13 @@ def self.split(arg)
|
||||
|
||||
class Str # :nodoc:
|
||||
def initialize(string)
|
||||
+ # A label is limited to 63 octets. [RFC 1035 2.3.4] Checking it here
|
||||
+ # makes it an invariant of the object: every label, however it was
|
||||
+ # built, fits in its length octet and cannot wrap it. Callers turn
|
||||
+ # this into the error their own contract promises.
|
||||
+ if string.bytesize > 63
|
||||
+ raise ArgumentError, "DNS label is too long (#{string.bytesize} bytes, max 63): #{string.inspect}"
|
||||
+ end
|
||||
@string = string
|
||||
# case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
|
||||
# This assumes @string is given in ASCII compatible encoding.
|
||||
@@ -1289,7 +1305,26 @@ def self.create(arg)
|
||||
when Name
|
||||
return arg
|
||||
when String
|
||||
- return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
|
||||
+ # A hostname is runtime data rather than a programming mistake, so
|
||||
+ # both size limits surface as ResolvError to stay rescuable alongside
|
||||
+ # the rest of name resolution. The type check below is a caller
|
||||
+ # mistake and keeps raising ArgumentError.
|
||||
+ begin
|
||||
+ labels = Label.split(arg)
|
||||
+ rescue ArgumentError => e
|
||||
+ raise ResolvError.new(e.message)
|
||||
+ end
|
||||
+ # Label::Str enforces the per-label limit. Only the total is knowable
|
||||
+ # here, and it counts the encoded form, so size starts at 1 for the
|
||||
+ # root label's terminating zero octet. [RFC 1035 2.3.4, 3.1]
|
||||
+ size = 1
|
||||
+ labels.each do |label|
|
||||
+ size += 1 + label.string.bytesize
|
||||
+ if size > 255
|
||||
+ raise ResolvError.new("DNS name is too long (#{size} octets, max 255): #{arg.inspect}")
|
||||
+ end
|
||||
+ end
|
||||
+ return Name.new(labels, /\.\z/ =~ arg ? true : false)
|
||||
else
|
||||
raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
|
||||
end
|
||||
@@ -1411,12 +1446,24 @@ def ==(other)
|
||||
@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
|
||||
@@ -1523,8 +1570,15 @@ def put_length16
|
||||
end
|
||||
|
||||
def put_string(d)
|
||||
- self.put_pack("C", d.length)
|
||||
- @data << d
|
||||
+ s = d.to_s
|
||||
+ # A character-string is prefixed by a single length octet, so it can
|
||||
+ # hold at most 255 octets. [RFC 1035 3.3] Reject anything longer to
|
||||
+ # avoid silently truncating the length to its low 8 bits (mod 256).
|
||||
+ if s.bytesize > 255
|
||||
+ raise ArgumentError, "character-string is too long (#{s.bytesize} bytes, max 255): #{s.inspect}"
|
||||
+ end
|
||||
+ self.put_pack("C", s.bytesize)
|
||||
+ @data << s
|
||||
end
|
||||
|
||||
def put_string_list(ds)
|
||||
@@ -1554,7 +1608,17 @@ def put_labels(d, compress: true)
|
||||
end
|
||||
|
||||
def put_label(d)
|
||||
- self.put_string(d.to_s)
|
||||
+ s = d.to_s
|
||||
+ # Label::Str applies this limit when a label is built, so what is left
|
||||
+ # for here is a raw string handed straight to put_labels. The two ways
|
||||
+ # an over-long label goes wrong differ: 64 to 255 octets write a length
|
||||
+ # octet in the reserved or compression pointer range, and 256 or more
|
||||
+ # wrap it mod 256. Either way the encoded name stops being the name the
|
||||
+ # caller asked for. [RFC 1035 2.3.4, 4.1.4]
|
||||
+ if s.bytesize > 63
|
||||
+ raise ArgumentError, "DNS label is too long (#{s.bytesize} bytes, max 63): #{s.inspect}"
|
||||
+ end
|
||||
+ self.put_string(s)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1680,7 +1744,9 @@ def get_labels
|
||||
prev_index = @index
|
||||
save_index = nil
|
||||
d = []
|
||||
- size = -1
|
||||
+ # size counts the encoded form, so it starts at 1 for the root
|
||||
+ # label's terminating zero octet. [RFC 1035 3.1]
|
||||
+ size = 1
|
||||
while true
|
||||
raise DecodeError.new("limit exceeded") if @limit <= @index
|
||||
case @data.getbyte(@index)
|
||||
@@ -1711,6 +1777,11 @@ def get_labels
|
||||
|
||||
def get_label
|
||||
return Label::Str.new(self.get_string)
|
||||
+ rescue ArgumentError => e
|
||||
+ # A length octet of 64..191 is reserved rather than a label length,
|
||||
+ # but this decoder used to read it as one. [RFC 1035 4.1.4] Report it
|
||||
+ # the way the rest of a malformed message is reported.
|
||||
+ raise DecodeError.new(e.message)
|
||||
end
|
||||
|
||||
def get_question
|
||||
@@ -1898,8 +1969,9 @@ def self.create(key_number)
|
||||
key_name = :"key#{key_number}"
|
||||
c.const_set(:KeyName, key_name)
|
||||
c.const_set(:KeyNumber, key_number)
|
||||
- self.const_set(:"Key#{key_number}", c)
|
||||
- ClassHash[key_name] = ClassHash[key_number] = c
|
||||
+ # Not registered in a constant or in ClassHash. ClassHash creates a
|
||||
+ # class for every unknown SvcParamKey, so registering them
|
||||
+ # permanently would let a malicious response exhaust memory.
|
||||
return c
|
||||
end
|
||||
end
|
||||
@@ -2206,12 +2278,28 @@ def self.decode_rdata(msg) # :nodoc:
|
||||
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
|
||||
|
||||
18
ruby.spec
18
ruby.spec
|
|
@ -41,7 +41,7 @@
|
|||
%global rubygems_net_http_version 0.7.0
|
||||
%global rubygems_net_protocol_version 0.2.2
|
||||
%global rubygems_optparse_version 0.8.0
|
||||
%global rubygems_resolv_version 0.7.0
|
||||
%global rubygems_resolv_version 0.7.2
|
||||
%global rubygems_securerandom_version 0.4.1
|
||||
%global rubygems_timeout_version 0.4.4
|
||||
%global rubygems_tsort_version 0.2.0
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
%global prettyprint_version 0.2.0
|
||||
%global prism_version 1.8.1
|
||||
%global psych_version 5.3.1
|
||||
%global resolv_version 0.7.0
|
||||
%global resolv_version 0.7.2
|
||||
%global ruby2_keywords_version 0.0.5
|
||||
%global securerandom_version 0.4.1
|
||||
%global shellwords_version 0.2.2
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
Summary: An interpreter of object-oriented scripting language
|
||||
Name: ruby
|
||||
Version: %{ruby_version}%{?development_release}
|
||||
Release: 37%{?dist}
|
||||
Release: 38%{?dist}
|
||||
# Licenses, which are likely not included in binary RPMs:
|
||||
# Apache-2.0:
|
||||
# benchmark/gc/redblack.rb
|
||||
|
|
@ -298,6 +298,10 @@ Patch8: ruby-4.0.1-Support-customizable-rustc_flags-for-rustc-builds.patch
|
|||
# Fix error with `gem install --document=rdoc,ri`
|
||||
# Fixed in rdoc 7.1.0 but not in 7.0.4
|
||||
Patch9: rdoc-pr1531-fix-mutilple-document-installation.patch
|
||||
# Backport from ruby_4_0 branch to update resolv to 0.7.2 (fixes CVE-2026-80212 CVE-2026-80213)
|
||||
# https://github.com/ruby/ruby/pull/18528
|
||||
# Also copied the patch to apply the fix also for vendored resolv
|
||||
Patch10: ruby-4_0-pr18528-update-resolv-0_7_2.patch
|
||||
|
||||
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
|
||||
%{?with_rubypick:Suggests: rubypick}
|
||||
|
|
@ -808,6 +812,7 @@ popd
|
|||
%patch 6 -p1
|
||||
%patch 7 -p1
|
||||
%patch 8 -p1
|
||||
%patch 10 -p1
|
||||
|
||||
# Provide an example of usage of the tapset:
|
||||
cp -a %{SOURCE3} .
|
||||
|
|
@ -1947,7 +1952,12 @@ make -C %{_vpath_builddir} runruby TESTRUN_SCRIPT=" \
|
|||
|
||||
|
||||
%changelog
|
||||
* Thu Jul 16 2026 Fedora Release Engineering <releng@fedoraproject.org>
|
||||
* Thu Sep 03 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 4.0.6-38
|
||||
- Backport upstream patch to update resolv to 0.7.2
|
||||
- Resolves: CVE-2026-80212 (rhbz#2527308)
|
||||
- Resolves: CVE-2026-80213 (rhbz#2527310)
|
||||
|
||||
* Thu Jul 16 2026 Fedora Release Engineering <releng@fedoraproject.org> - 4.0.6-37
|
||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_45_Mass_Rebuild
|
||||
|
||||
* Tue Jul 14 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 4.0.6-36
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue