public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/ruby] f43: Backport upstream patch to update resolv to 0.7.2
@ 2026-09-10  8:38 Mamoru TASAKA
  0 siblings, 0 replies; only message in thread
From: Mamoru TASAKA @ 2026-09-10  8:38 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/ruby
            Branch : f43
            Commit : 66fb74d338b07638560671140c4d5cf6a0d17d85
            Author : Mamoru TASAKA <mtasaka@fedoraproject.org>
            Date   : 2026-09-10T17:38:45+09:00
            Stats  : +1072/-5 in 3 file(s)
            URL    : https://src.fedoraproject.org/rpms/ruby/c/66fb74d338b07638560671140c4d5cf6a0d17d85?branch=f43

            Log:
            Backport upstream patch to update resolv to 0.7.2

Resolves: CVE-2026-80212 (rhbz#2527308)
Resolves: CVE-2026-80213 (rhbz#2527310)

---
diff --git a/ruby-2.1.0-custom-rubygems-location.patch b/ruby-2.1.0-custom-rubygems-location.patch
index 2ff5594..b265644 100644
--- a/ruby-2.1.0-custom-rubygems-location.patch
+++ b/ruby-2.1.0-custom-rubygems-location.patch
@@ -15,7 +15,7 @@ diff --git a/configure.ac b/configure.ac
 index 93af30321d..bc13397e0e 100644
 --- a/configure.ac
 +++ b/configure.ac
-@@ -4401,6 +4401,10 @@ AC_ARG_WITH(vendorarchdir,
+@@ -4398,6 +4398,10 @@ AC_ARG_WITH(vendorarchdir,
              [vendorarchdir=$withval],
              [vendorarchdir=${multiarch+'${rubysitearchprefix}/vendor_ruby'${ruby_version_dir}}${multiarch-'${vendorlibdir}/${sitearch}'}])
  
@@ -26,7 +26,7 @@ index 93af30321d..bc13397e0e 100644
  AS_IF([test "${LOAD_RELATIVE+set}"], [
      AC_DEFINE_UNQUOTED(LOAD_RELATIVE, $LOAD_RELATIVE)
      RUBY_EXEC_PREFIX=''
-@@ -4425,6 +4429,7 @@ AC_SUBST(sitearchdir)dnl
+@@ -4422,6 +4426,7 @@ AC_SUBST(sitearchdir)dnl
  AC_SUBST(vendordir)dnl
  AC_SUBST(vendorlibdir)dnl
  AC_SUBST(vendorarchdir)dnl

diff --git a/ruby-3_4-pr18529-update-resolv-0_7_2.patch b/ruby-3_4-pr18529-update-resolv-0_7_2.patch
new file mode 100644
index 0000000..77bae30
--- /dev/null
+++ b/ruby-3_4-pr18529-update-resolv-0_7_2.patch
@@ -0,0 +1,1057 @@
+From 68a11b6f4400ae2f0f16df3773921253e419f294 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 3.4
+
+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                     | 101 +++++++++++++--
+ test/resolv/test_dns.rb           | 199 ++++++++++++++++++++++++++++++
+ test/resolv/test_resource.rb      |  61 +++++++++
+ test/resolv/test_resource_leak.rb | 101 +++++++++++++++
+ 4 files changed, 451 insertions(+), 11 deletions(-)
+ create mode 100644 test/resolv/test_resource_leak.rb
+
+diff --git a/lib/resolv.rb b/lib/resolv.rb
+index 9720b52c00fef3..a2fe692b38f6cf 100644
+--- a/lib/resolv.rb
++++ b/lib/resolv.rb
+@@ -35,7 +35,7 @@
+ class Resolv
+ 
+   # The version string
+-  VERSION = "0.7.1"
++  VERSION = "0.7.2"
+ 
+   ##
+   # Looks up the first IP address for +name+.
+@@ -1253,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.
+@@ -1298,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
+@@ -1420,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
+@@ -1532,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)
+@@ -1563,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
+ 
+@@ -1689,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)
+@@ -1720,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
+@@ -1907,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
+@@ -2215,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 7a01909eeb9a4b..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
+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-3.4.10.orig/lib/rubygems/vendor/resolv/lib/resolv.rb	2026-06-30 19:54:35.000000000 +0900
++++ ruby-3.4.10/lib/rubygems/vendor/resolv/lib/resolv.rb	2026-09-10 17:09:17.710957720 +0900
+@@ -4,6 +4,7 @@ require 'socket'
+ require_relative '../../timeout/lib/timeout'
+ require 'io/wait'
+ require_relative '../../../vendored_securerandom'
++require 'rbconfig'
+ 
+ # Gem::Resolv is a thread-aware DNS resolver library written in Ruby.  Gem::Resolv can
+ # handle multiple DNS requests concurrently without blocking the entire Ruby
+@@ -33,7 +34,8 @@ require_relative '../../../vendored_secu
+ 
+ class Gem::Resolv
+ 
+-  VERSION = "0.6.0"
++  # The version string
++  VERSION = "0.7.2"
+ 
+   ##
+   # Looks up the first IP address for +name+.
+@@ -177,14 +179,15 @@ class Gem::Resolv
+   # Gem::Resolv::Hosts is a hostname resolver that uses the system hosts file.
+ 
+   class Hosts
+-    if /mswin|mingw|cygwin/ =~ RUBY_PLATFORM and
++    if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/
+       begin
+-        require 'win32/resolv'
+-        DefaultFileName = Win32::Resolv.get_hosts_path || IO::NULL
++        require 'win32/resolv' unless defined?(Win32::Resolv)
++        hosts = Win32::Resolv.get_hosts_path || IO::NULL
+       rescue LoadError
+       end
+     end
+-    DefaultFileName ||= '/etc/hosts'
++    # The default file name for host names
++    DefaultFileName = hosts || '/etc/hosts'
+ 
+     ##
+     # Creates a new Gem::Resolv::Hosts, using +filename+ for its data source.
+@@ -484,13 +487,18 @@ class Gem::Resolv
+     # * 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
+     #
+@@ -522,6 +530,8 @@ class Gem::Resolv
+       }
+     end
+ 
++    # :stopdoc:
++
+     def fetch_resource(name, typeclass)
+       lazy_initialize
+       truncated = {}
+@@ -659,8 +669,20 @@ class Gem::Resolv
+       }
+     end
+ 
+-    def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+-      begin
++    case RUBY_PLATFORM
++    when *[
++      # https://www.rfc-editor.org/rfc/rfc6056.txt
++      # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations
++      /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/,
++      /darwin/, # the same as FreeBSD
++    ] then
++      def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
++        udpsock.bind(bind_host, 0)
++      end
++    else
++      # Sequential port assignment
++      def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
++        # Ephemeral port number range recommended by RFC 6056
+         port = random(1024..65535)
+         udpsock.bind(bind_host, port)
+       rescue Errno::EADDRINUSE, # POSIX
+@@ -704,7 +726,8 @@ class Gem::Resolv
+           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
+@@ -913,8 +936,11 @@ class Gem::Resolv
+         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
+ 
+@@ -983,13 +1009,13 @@ class Gem::Resolv
+             next unless keyword
+             case keyword
+             when 'nameserver'
+-              nameserver.concat(args)
++              nameserver.concat(args.each(&:freeze))
+             when 'domain'
+               next if args.empty?
+-              search = [args[0]]
++              search = [args[0].freeze]
+             when 'search'
+               next if args.empty?
+-              search = args
++              search = args.each(&:freeze)
+             when 'options'
+               args.each {|arg|
+                 case arg
+@@ -1000,22 +1026,21 @@ class Gem::Resolv
+             end
+           }
+         }
+-        return { :nameserver => nameserver, :search => search, :ndots => ndots }
++        return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze
+       end
+ 
+       def Config.default_config_hash(filename="/etc/resolv.conf")
+         if File.exist? filename
+-          config_hash = Config.parse_resolv_conf(filename)
++          Config.parse_resolv_conf(filename)
++        elsif defined?(Win32::Resolv)
++          search, nameserver = Win32::Resolv.get_resolv_info
++          config_hash = {}
++          config_hash[:nameserver] = nameserver if nameserver
++          config_hash[:search] = [search].flatten if search
++          config_hash
+         else
+-          if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
+-            require 'win32/resolv'
+-            search, nameserver = Win32::Resolv.get_resolv_info
+-            config_hash = {}
+-            config_hash[:nameserver] = nameserver if nameserver
+-            config_hash[:search] = [search].flatten if search
+-          end
++          {}
+         end
+-        config_hash || {}
+       end
+ 
+       def lazy_initialize
+@@ -1228,6 +1253,13 @@ class Gem::Resolv
+ 
+       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.
+@@ -1273,7 +1305,26 @@ class Gem::Resolv
+         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
+@@ -1395,12 +1446,24 @@ class Gem::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
+@@ -1507,8 +1570,15 @@ class Gem::Resolv
+         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)
+@@ -1538,7 +1608,17 @@ class Gem::Resolv
+         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
+ 
+@@ -1664,6 +1744,9 @@ class Gem::Resolv
+           prev_index = @index
+           save_index = nil
+           d = []
++          # 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)
+@@ -1684,13 +1767,21 @@ class Gem::Resolv
+               end
+               @index = idx
+             else
+-              d << self.get_label
++              l = self.get_label
++              d << l
++              size += 1 + l.string.bytesize
++              raise DecodeError.new("name label data exceed 255 octets") if size > 255
+             end
+           end
+         end
+ 
+         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
+@@ -1878,8 +1969,9 @@ class Gem::Resolv
+           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
+@@ -2110,7 +2202,14 @@ class Gem::Resolv
+ 
+       attr_reader :ttl
+ 
+-      ClassHash = {} # :nodoc:
++      ClassHash = Module.new do
++        module_function
++
++        def []=(type_class_value, klass)
++          type_value, class_value = type_class_value
++          Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass)
++        end
++      end
+ 
+       def encode_rdata(msg) # :nodoc:
+         raise NotImplementedError.new
+@@ -2148,7 +2247,9 @@ class Gem::Resolv
+       end
+ 
+       def self.get_class(type_value, class_value) # :nodoc:
+-        return ClassHash[[type_value, class_value]] ||
++        cache = :"Type#{type_value}_Class#{class_value}"
++
++        return (const_defined?(cache) && const_get(cache)) ||
+                Generic.create(type_value, class_value)
+       end
+ 
+@@ -2177,12 +2278,28 @@ class Gem::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
+@@ -2577,7 +2694,7 @@ class Gem::Resolv
+         end
+ 
+         ##
+-        # Flags for this proprty:
++        # Flags for this property:
+         # - Bit 0 : 0 = not critical, 1 = critical
+ 
+         attr_reader :flags
+@@ -2898,15 +3015,21 @@ class Gem::Resolv
+ 
+   class IPv4
+ 
+-    ##
+-    # Regular expression IPv4 addresses must match.
+-
+     Regex256 = /0
+                |1(?:[0-9][0-9]?)?
+                |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
+-               |[3-9][0-9]?/x
++               |[3-9][0-9]?/x # :nodoc:
++
++    ##
++    # Regular expression IPv4 addresses must match.
+     Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
+ 
++    ##
++    # Creates a new IPv4 address from +arg+ which may be:
++    #
++    # IPv4:: returns +arg+.
++    # String:: +arg+ must match the IPv4::Regex constant
++
+     def self.create(arg)
+       case arg
+       when IPv4
+@@ -3215,13 +3338,15 @@ class Gem::Resolv
+ 
+   end
+ 
+-  module LOC
++  module LOC # :nodoc:
+ 
+     ##
+     # A Gem::Resolv::LOC::Size
+ 
+     class Size
+ 
++      # Regular expression LOC size must match.
++
+       Regex = /^(\d+\.*\d*)[m]$/
+ 
+       ##
+@@ -3247,6 +3372,7 @@ class Gem::Resolv
+         end
+       end
+ 
++      # Internal use; use self.create.
+       def initialize(scalar)
+         @scalar = scalar
+       end
+@@ -3284,6 +3410,8 @@ class Gem::Resolv
+ 
+     class Coord
+ 
++      # Regular expression LOC Coord must match.
++
+       Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
+ 
+       ##
+@@ -3313,6 +3441,7 @@ class Gem::Resolv
+         end
+       end
+ 
++      # Internal use; use self.create.
+       def initialize(coordinates,orientation)
+         unless coordinates.kind_of?(String)
+           raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
+@@ -3375,6 +3504,8 @@ class Gem::Resolv
+ 
+     class Alt
+ 
++      # Regular expression LOC Alt must match.
++
+       Regex = /^([+-]*\d+\.*\d*)[m]$/
+ 
+       ##
+@@ -3400,6 +3531,7 @@ class Gem::Resolv
+         end
+       end
+ 
++      # Internal use; use self.create.
+       def initialize(altitude)
+         @altitude = altitude
+       end

diff --git a/ruby.spec b/ruby.spec
index 94b0f22..61df8c9 100644
--- a/ruby.spec
+++ b/ruby.spec
@@ -32,7 +32,7 @@
 %global rubygems_net_http_version 0.6.0
 %global rubygems_net_protocol_version 0.2.2
 %global rubygems_optparse_version 0.6.0
-%global rubygems_resolv_version 0.6.0
+%global rubygems_resolv_version 0.7.2
 %global rubygems_securerandom_version 0.4.1
 %global rubygems_timeout_version 0.4.3
 %global rubygems_tsort_version 0.2.0
@@ -82,7 +82,7 @@
 %global pstore_version 0.2.1
 %global readline_version 0.0.4
 %global reline_version 0.6.0
-%global resolv_version 0.7.1
+%global resolv_version 0.7.2
 %global ruby2_keywords_version 0.0.5
 %global securerandom_version 0.4.1
 %global set_version 1.1.1
@@ -177,7 +177,7 @@
 Summary: An interpreter of object-oriented scripting language
 Name: ruby
 Version: %{ruby_version}%{?development_release}
-Release: 31%{?dist}
+Release: 32%{?dist}
 # Licenses, which are likely not included in binary RPMs:
 # Apache-2.0:
 #   benchmark/gc/redblack.rb
@@ -281,6 +281,10 @@ Patch9: ruby-3.3.0-Disable-syntax-suggest-test-case.patch
 # Fix the tests using SHA-1 Probabilistic Signature Scheme (PSS) parameters.
 # https://github.com/ruby/openssl/pull/879
 Patch10: ruby-3.4.2-openssl-Fix-SHA-1-PSS-tests.patch
+# Backport from ruby_3_4 branch to update resolv to 0.7.2 (fixes CVE-2026-80212 CVE-2026-80213)
+# https://github.com/ruby/ruby/pull/18529
+# Also copied the patch to apply the fix also for vendored resolv
+Patch11: ruby-3_4-pr18529-update-resolv-0_7_2.patch
 
 Requires: %{name}-libs%{?_isa} = %{version}-%{release}
 %{?with_rubypick:Suggests: rubypick}
@@ -775,6 +779,7 @@ analysis result in RBS format, a standard type description format for Ruby
 %patch 6 -p1
 %patch 9 -p1
 %patch 10 -p1
+%patch 11 -p1
 
 # Provide an example of usage of the tapset:
 cp -a %{SOURCE3} .
@@ -1879,6 +1884,11 @@ make -C %{_vpath_builddir} runruby TESTRUN_SCRIPT=" \
 
 
 %changelog
+* Fri Sep 04 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 3.4.10-32
+- Backport upstream patch to update resolv to 0.7.2
+- Resolves: CVE-2026-80212 (rhbz#2527308)
+- Resolves: CVE-2026-80213 (rhbz#2527310)
+
 * Wed Jul 08 2026 Mamoru TASAKA <mtasaka@fedoraproject.org> - 3.4.10-31
 - Update to Ruby 3.4.10
 - Resolves: CVE-2026-41316 (rhbz#2463216)

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-09-10  8:38 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-10  8:38 [rpms/ruby] f43: Backport upstream patch to update resolv to 0.7.2 Mamoru TASAKA

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox