From 1c664d80245a22b8d81242e85c3d6fe277fcde83 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 26 Aug 2026 15:08:44 +0900 Subject: [PATCH 01/19] Defer unloading box-local extension DLLs on Windows box_entry_free could run before other T_DATA objects are finalized in rb_objspace_call_finalizer, and FreeLibrary there unmapped the rb_data_type_t and dfree functions that later finalization still dereferences, crashing any boxed process that leaves extension objects alive at exit. Keep the copied DLLs loaded until ruby_vm_destruct, then unload and delete them. Co-Authored-By: Claude Fable 5 --- box.c | 51 +++++++++++++++++++++++++++++++++++--------------- internal/box.h | 2 ++ load.c | 1 + vm.c | 2 ++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/box.c b/box.c index e72caa4ccefda0..9121ba003d6ada 100644 --- a/box.c +++ b/box.c @@ -65,7 +65,6 @@ bool ruby_box_crashed = false; // extern, changed only in vm.c VALUE rb_resolve_feature_path(VALUE klass, VALUE fname); static VALUE rb_box_inspect(VALUE obj); -static void cleanup_all_local_extensions(VALUE libmap); void rb_box_set_gem_flags(rb_box_gem_flags_t *flags) @@ -303,8 +302,6 @@ box_entry_free(void *ptr) st_foreach(box->classext_cow_classes, free_classext_for_box, (st_data_t)box); } - cleanup_all_local_extensions(box->ruby_dln_libmap); - free_box_st_tables(ptr); SIZED_FREE(box); } @@ -808,24 +805,48 @@ rb_box_cleanup_local_extension(VALUE cleanup) (void)p; } -static int -cleanup_local_extension_i(VALUE key, VALUE value, VALUE arg) -{ #if defined(_WIN32) - HMODULE h = (HMODULE)NUM2PTR(value); - WCHAR module_path[MAXPATHLEN]; - DWORD len = GetModuleFileNameW(h, module_path, numberof(module_path)); +struct box_local_ext_list { + struct box_local_ext_list *next; + HMODULE handle; +}; +static struct box_local_ext_list *box_local_exts; +#endif - FreeLibrary(h); - if (len > 0 && len < numberof(module_path)) DeleteFileW(module_path); +void +rb_box_defer_unload_local_extension(void *handle) +{ +#if defined(_WIN32) + /* A box-local copy of an extension DLL must stay loaded as long as + * objects created by it can be finalized; unloading is deferred to + * rb_box_unload_local_extensions after the objspace is destructed. + * The loaded copy cannot be deleted on Windows, so the file is also + * removed there instead of in box_ext_cleanup_free. */ + struct box_local_ext_list *ext = malloc(sizeof(struct box_local_ext_list)); + if (!ext) return; + ext->handle = (HMODULE)handle; + ext->next = box_local_exts; + box_local_exts = ext; #endif - return ST_DELETE; } -static void -cleanup_all_local_extensions(VALUE libmap) +void +rb_box_unload_local_extensions(void) { - rb_hash_foreach(libmap, cleanup_local_extension_i, 0); +#if defined(_WIN32) + struct box_local_ext_list *ext = box_local_exts; + box_local_exts = NULL; + while (ext) { + struct box_local_ext_list *next = ext->next; + WCHAR module_path[MAXPATHLEN]; + DWORD len = GetModuleFileNameW(ext->handle, module_path, numberof(module_path)); + + FreeLibrary(ext->handle); + if (len > 0 && len < numberof(module_path)) DeleteFileW(module_path); + free(ext); + ext = next; + } +#endif } VALUE diff --git a/internal/box.h b/internal/box.h index 1e7d4fbdef5169..fa01a47307ed66 100644 --- a/internal/box.h +++ b/internal/box.h @@ -90,6 +90,8 @@ VALUE rb_get_box_object(rb_box_t *ns); VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path, VALUE *cleanup); void rb_box_cleanup_local_extension(VALUE cleanup); +void rb_box_defer_unload_local_extension(void *handle); +void rb_box_unload_local_extensions(void); void rb_initialize_mandatory_boxes(void); void rb_box_init_done(void); diff --git a/load.c b/load.c index 069617397affee..c10de25a45e3b6 100644 --- a/load.c +++ b/load.c @@ -1224,6 +1224,7 @@ load_ext(VALUE path, VALUE fname) void *handle = dln_load_feature(RSTRING_PTR(loaded), RSTRING_PTR(fname)); if (cleanup) { rb_box_cleanup_local_extension(cleanup); + rb_box_defer_unload_local_extension(handle); } RB_GC_GUARD(loaded); RB_GC_GUARD(fname); diff --git a/vm.c b/vm.c index 97d2731002fe72..d4fac088004a5c 100644 --- a/vm.c +++ b/vm.c @@ -3584,6 +3584,8 @@ ruby_vm_destruct(rb_vm_t *vm) rb_yjit_free_at_exit(); #endif } + + rb_box_unload_local_extensions(); } RUBY_FREE_LEAVE("vm"); return 0; From efa874431ab7236d6935808273bdc0bc971bbc95 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 26 Aug 2026 15:08:59 +0900 Subject: [PATCH 02/19] Revert "[ruby/rubygems] Skip the RUBY_BOX gem CLI canary on Windows" This reverts commit 86817cf6ef9c1bcf7ef0c876ac1ead5b51c0b029. --- test/rubygems/test_gem_command.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/rubygems/test_gem_command.rb b/test/rubygems/test_gem_command.rb index 44e695f567389e..6043723aad1ef0 100644 --- a/test/rubygems/test_gem_command.rb +++ b/test/rubygems/test_gem_command.rb @@ -403,8 +403,6 @@ def test_show_lookup_failure_suggestions_remote def test_gem_cli_runs_under_ruby_box omit "Ruby::Box is not available" unless defined?(Ruby::Box) - # A boxed subprocess crashes with SIGSEGV during finalization on Windows - omit "Ruby::Box is unstable on Windows" if Gem.win_platform? # Ruby 4.0 resolves Gem::NameTuple from the root box and fails autoload omit "Ruby::Box is too unstable before 4.1" if Gem.ruby_version < Gem::Version.new("4.1.0.a") From 04c367a7e2b383460579a065662f91a215774f40 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 11:32:34 +0900 Subject: [PATCH 03/19] [ruby/resolv] Enforce DNS label and name size limits when encoding A label longer than 255 octets wrapped its length octet mod 256 while the data was written unchanged, so the wire bytes decoded to a different name than the caller asked for. A hostname that passed an application's allowlist could be sent as a query for an unrelated domain. The 63 octet limit goes on the label path. put_string also writes character-strings, which may legitimately be 255 octets, so it keeps the wider limit that its single length octet can represent. https://github.com/ruby/resolv/commit/137df11fc0 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 40 ++++++++++++-- test/resolv/test_dns.rb | 117 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 03e5e9a75dfa72..c61425804cdbbc 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1360,7 +1360,24 @@ def self.create(arg) when Name return arg when String - return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false) + labels = Label.split(arg) + # Enforce the DNS size limits at construction time so an attacker + # controlled hostname cannot reach the encoder with a label that + # overflows its length octet. [RFC 1035 2.3.4, 3.1] size counts the + # encoded form, so it starts at 1 for the root label's terminating + # zero octet. + size = 1 + labels.each do |label| + len = label.string.bytesize + if len > 63 + raise ArgumentError, "DNS label is too long (#{len} bytes): #{label.string.inspect}" + end + size += 1 + len + if size > 255 + raise ArgumentError, "DNS name is too long (exceeds 255 octets): #{arg.inspect}" + end + end + return Name.new(labels, /\.\z/ =~ arg ? true : false) else raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}") end @@ -1594,8 +1611,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): #{s.inspect}" + end + self.put_pack("C", s.bytesize) + @data << s end def put_string_list(ds) @@ -1625,7 +1649,15 @@ def put_labels(d, compress: true) end def put_label(d) - self.put_string(d.to_s) + s = d.to_s + # A DNS label is limited to 63 octets. [RFC 1035 2.3.4] A longer label + # would overflow the single length octet and be written with the top + # bits of the length set, which a decoder reads as a compression + # pointer or reserved value, silently changing the encoded name. + if s.bytesize > 63 + raise ArgumentError, "DNS label is too long (#{s.bytesize} bytes): #{s.inspect}" + end + self.put_string(s) end end diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index b4ef92b63845f9..b276b52fc5ed32 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -636,6 +636,123 @@ 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 + + # Name.create is the entry point for application supplied hostnames, so the + # size limits are enforced there before an attacker controlled name reaches + # the encoder. [RFC 1035 2.3.4, 3.1] + def test_name_create_rejects_too_long_label + assert_nothing_raised { Resolv::DNS::Name.create("a" * 63) } + assert_raise_with_message(ArgumentError, /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(ArgumentError, /DNS name is too long/) do + Resolv::DNS::Name.create(too_long) + 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(ArgumentError, /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(ArgumentError, /DNS name is too long/) do + Resolv::DNS::Name.create((["a" * 63] * 4).join(".")) + 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(ArgumentError, /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 From f681ca8ade0cd98daf5537d225f740fb82fb706a Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 11:32:57 +0900 Subject: [PATCH 04/19] [ruby/resolv] Count the encoded form when limiting DNS name length The counter started at -1, which measures the presentation form, so a name encoding to 257 octets passed the 255 octet limit of RFC 1035 section 3.1. The encoded form includes each label's length octet and the root label's terminating zero octet. https://github.com/ruby/resolv/commit/0a5ef9b498 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 4 +++- test/resolv/test_dns.rb | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index c61425804cdbbc..1e5b0b02a317bc 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1783,7 +1783,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) diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index b276b52fc5ed32..9955f4c7af9d26 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -693,6 +693,27 @@ def test_name_create_total_length_boundary 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 / From 5bf9c3cea78cd3f3bf75039a334479e62563aa77 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 11:42:45 +0900 Subject: [PATCH 05/19] [ruby/resolv] Raise ResolvError for an oversized name A hostname is runtime data rather than a programming mistake, and callers already wrap name resolution in rescue Resolv::ResolvError. Rejecting an oversized name with ArgumentError escaped that rescue, since Config#generate_candidates runs outside Config#resolv's own handler. No new exception class: resolv raises ResolvError directly in nine other places and subclasses it only where a caller has to tell cases apart. https://github.com/ruby/resolv/commit/346416498a Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 7 +++++-- test/resolv/test_dns.rb | 29 ++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 1e5b0b02a317bc..7fbba3e67835fa 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1366,15 +1366,18 @@ def self.create(arg) # overflows its length octet. [RFC 1035 2.3.4, 3.1] size counts the # encoded form, so it starts at 1 for the root label's terminating # zero octet. + # A hostname is runtime data rather than a programming mistake, so + # these raise ResolvError to stay rescuable alongside the rest of + # name resolution. The type check below keeps raising ArgumentError. size = 1 labels.each do |label| len = label.string.bytesize if len > 63 - raise ArgumentError, "DNS label is too long (#{len} bytes): #{label.string.inspect}" + raise ResolvError.new("DNS label is too long (#{len} bytes, max 63): #{label.string.inspect}") end size += 1 + len if size > 255 - raise ArgumentError, "DNS name is too long (exceeds 255 octets): #{arg.inspect}" + raise ResolvError.new("DNS name is too long (#{size} octets, max 255): #{arg.inspect}") end end return Name.new(labels, /\.\z/ =~ arg ? true : false) diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 9955f4c7af9d26..832f1147c5af41 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -658,7 +658,7 @@ def test_put_label_rejects_label_over_63_octets # the encoder. [RFC 1035 2.3.4, 3.1] def test_name_create_rejects_too_long_label assert_nothing_raised { Resolv::DNS::Name.create("a" * 63) } - assert_raise_with_message(ArgumentError, /DNS label is too long/) do + assert_raise_with_message(Resolv::ResolvError, /DNS label is too long/) do Resolv::DNS::Name.create("a" * 64) end end @@ -667,11 +667,30 @@ 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(ArgumentError, /DNS name is too long/) do + 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 + + # 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. @@ -682,13 +701,13 @@ def test_name_create_total_length_boundary assert_equal(255, encoded.bytesize, "longest legal name encodes to 255 octets") over_limit = (["a" * 63] * 3 + ["a" * 62]).join(".") - assert_raise_with_message(ArgumentError, /DNS name is too long/) do + 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(ArgumentError, /DNS name is too long/) do + assert_raise_with_message(Resolv::ResolvError, /DNS name is too long/) do Resolv::DNS::Name.create((["a" * 63] * 4).join(".")) end end @@ -737,7 +756,7 @@ def test_encoder_rejects_label_length_wrap msg.put_label(poc_label) end } - assert_raise_with_message(ArgumentError, /DNS label is too long/) do + assert_raise_with_message(Resolv::ResolvError, /DNS label is too long/) do Resolv::DNS::Name.create(poc_label) end end From d17b92162e27e2aa7911877bb0b4ee4e33a5d3e0 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 12:23:14 +0900 Subject: [PATCH 06/19] [ruby/resolv] Make the 63 octet label limit an invariant of Label::Str Checking in Name.create left two ways to build an over-long label: Name.new, which Config#generate_candidates uses to append search domains, and the search list itself, which Config splits without going through Name.create. Label::Str is the one place every label is built, so enforcing it there covers both, and the encoder guard becomes a backstop rather than the only line of defence. Callers keep the error their own contract promises. Name.create still reports ResolvError, and get_label reports DecodeError, which also means a length octet in the reserved 64..191 range is no longer read as a label. https://github.com/ruby/resolv/commit/07f3b7d098 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 37 ++++++++++++++++++++----------- test/resolv/test_dns.rb | 48 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 7fbba3e67835fa..0e25a567f23e99 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1315,6 +1315,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. @@ -1360,22 +1367,21 @@ def self.create(arg) when Name return arg when String - labels = Label.split(arg) - # Enforce the DNS size limits at construction time so an attacker - # controlled hostname cannot reach the encoder with a label that - # overflows its length octet. [RFC 1035 2.3.4, 3.1] size counts the - # encoded form, so it starts at 1 for the root label's terminating - # zero octet. # A hostname is runtime data rather than a programming mistake, so - # these raise ResolvError to stay rescuable alongside the rest of - # name resolution. The type check below keeps raising ArgumentError. + # 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| - len = label.string.bytesize - if len > 63 - raise ResolvError.new("DNS label is too long (#{len} bytes, max 63): #{label.string.inspect}") - end - size += 1 + len + size += 1 + label.string.bytesize if size > 255 raise ResolvError.new("DNS name is too long (#{size} octets, max 255): #{arg.inspect}") end @@ -1819,6 +1825,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 diff --git a/test/resolv/test_dns.rb b/test/resolv/test_dns.rb index 832f1147c5af41..f2649ac7a75800 100644 --- a/test/resolv/test_dns.rb +++ b/test/resolv/test_dns.rb @@ -653,9 +653,34 @@ def test_put_label_rejects_label_over_63_octets } end - # Name.create is the entry point for application supplied hostnames, so the - # size limits are enforced there before an attacker controlled name reaches - # the encoder. [RFC 1035 2.3.4, 3.1] + # 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 @@ -683,6 +708,23 @@ def test_oversized_name_is_rescuable_as_resolv_error 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 From eafd4471897d76b165f361245fec07144e87f1ae Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 12:33:21 +0900 Subject: [PATCH 07/19] [ruby/resolv] Describe the label limit the way the encoder now applies it The comment predates the check moving into Label::Str and read as though a label of 64 octets already overflowed its length octet. It does not: 64 to 255 write a reserved or compression pointer value, and only 256 or more wrap. Say which is which, and say that this guard now only catches a raw string passed straight to put_labels. Both messages also name the limit they enforce, as the other size checks do. https://github.com/ruby/resolv/commit/972ec472e3 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 0e25a567f23e99..8349c1b2ecb07e 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1625,7 +1625,7 @@ def put_string(d) # 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): #{s.inspect}" + raise ArgumentError, "character-string is too long (#{s.bytesize} bytes, max 255): #{s.inspect}" end self.put_pack("C", s.bytesize) @data << s @@ -1659,12 +1659,14 @@ def put_labels(d, compress: true) def put_label(d) s = d.to_s - # A DNS label is limited to 63 octets. [RFC 1035 2.3.4] A longer label - # would overflow the single length octet and be written with the top - # bits of the length set, which a decoder reads as a compression - # pointer or reserved value, silently changing the encoded name. + # 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): #{s.inspect}" + raise ArgumentError, "DNS label is too long (#{s.bytesize} bytes, max 63): #{s.inspect}" end self.put_string(s) end From 55f73130b8f92cb6849a8c6fea8216c3886d0456 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 16 Jul 2026 16:49:58 +0900 Subject: [PATCH 08/19] [ruby/resolv] Do not register on-demand classes for unknown DNS types and SvcParamKeys Decoding a response with unknown (type, class) pairs or unknown SvcParamKeys generated an anonymous class per value and registered it in a constant and in ClassHash permanently, so a spoofed or malicious response carrying many unknown values could exhaust memory even after the response was discarded and GC ran (a denial of service). Generate the classes on demand without registering them so they stay collectable; the default RR and SvcParam classes are still registered at require time. https://github.com/ruby/resolv/commit/fa5e689c46 Co-Authored-By: Claude Opus 4.8 --- lib/resolv.rb | 16 +++-- test/resolv/test_resource_leak.rb | 106 ++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 test/resolv/test_resource_leak.rb diff --git a/lib/resolv.rb b/lib/resolv.rb index 8349c1b2ecb07e..976dd03c77fefe 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -2019,8 +2019,12 @@ 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 + # Do not register the generated class in a constant or ClassHash: + # ClassHash's default block calls create for every unknown SvcParamKey + # in a decoded message, so permanently registering each one would let a + # malicious or spoofed response leak classes, constants, and symbols + # without bound (memory-exhaustion DoS). Returning a fresh, GC-able + # class each time keeps decoding of unknown keys stateless. return c end end @@ -2331,8 +2335,12 @@ 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 + # Do not register the generated class in a constant or ClassHash: + # get_class is called for every unknown (type, class) pair in a + # decoded message, so permanently registering each one would let a + # malicious or spoofed response leak classes, constants, and symbols + # without bound (memory-exhaustion DoS). Returning a fresh, GC-able + # class each time keeps decoding of unknown RRs stateless. return c end end diff --git a/test/resolv/test_resource_leak.rb b/test/resolv/test_resource_leak.rb new file mode 100644 index 00000000000000..c64e4cc5bce3aa --- /dev/null +++ b/test/resolv/test_resource_leak.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: false +require 'test/unit' +require 'resolv' + +# Regression tests for the unbounded class/constant/symbol growth that used to +# happen when decoding DNS responses containing unknown (type, class) pairs or +# unknown SvcParamKeys. Each unknown value made Resolv generate and PERMANENTLY +# register a fresh anonymous class, along with a constant and a symbol, so a +# malicious or spoofed response holding many unknown values could exhaust memory +# even after the response was discarded and GC ran (a denial of service). The +# generated classes are now left unregistered and garbage-collectable. +# [HackerOne #3769501] +class TestResolvResourceLeak < Test::Unit::TestCase + # Number of dynamically-registered "Type_Class" 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 From 1745c36e4ad532180a7bd8da7e6148bcb648fc5f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 11:04:00 +0900 Subject: [PATCH 09/19] [ruby/resolv] Shorten the comments added in the previous commit https://github.com/ruby/resolv/commit/95e0a6664c Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 18 ++++++------------ test/resolv/test_resource_leak.rb | 11 +++-------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index 976dd03c77fefe..b5e377478c00e4 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -2019,12 +2019,9 @@ def self.create(key_number) key_name = :"key#{key_number}" c.const_set(:KeyName, key_name) c.const_set(:KeyNumber, key_number) - # Do not register the generated class in a constant or ClassHash: - # ClassHash's default block calls create for every unknown SvcParamKey - # in a decoded message, so permanently registering each one would let a - # malicious or spoofed response leak classes, constants, and symbols - # without bound (memory-exhaustion DoS). Returning a fresh, GC-able - # class each time keeps decoding of unknown keys stateless. + # 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 @@ -2335,12 +2332,9 @@ def self.create(type_value, class_value) # :nodoc: c = Class.new(Generic) c.const_set(:TypeValue, type_value) c.const_set(:ClassValue, class_value) - # Do not register the generated class in a constant or ClassHash: - # get_class is called for every unknown (type, class) pair in a - # decoded message, so permanently registering each one would let a - # malicious or spoofed response leak classes, constants, and symbols - # without bound (memory-exhaustion DoS). Returning a fresh, GC-able - # class each time keeps decoding of unknown RRs stateless. + # 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_resource_leak.rb b/test/resolv/test_resource_leak.rb index c64e4cc5bce3aa..4628a3d2857677 100644 --- a/test/resolv/test_resource_leak.rb +++ b/test/resolv/test_resource_leak.rb @@ -2,14 +2,9 @@ require 'test/unit' require 'resolv' -# Regression tests for the unbounded class/constant/symbol growth that used to -# happen when decoding DNS responses containing unknown (type, class) pairs or -# unknown SvcParamKeys. Each unknown value made Resolv generate and PERMANENTLY -# register a fresh anonymous class, along with a constant and a symbol, so a -# malicious or spoofed response holding many unknown values could exhaust memory -# even after the response was discarded and GC ran (a denial of service). The -# generated classes are now left unregistered and garbage-collectable. -# [HackerOne #3769501] +# 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_Class" constants on +mod+. def type_const_count(mod) From c77b5944c3a44f6db0c267805557725c1b78df3b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 11:19:53 +0900 Subject: [PATCH 10/19] [ruby/resolv] Compare generic resources by their type and class values Dropping the class cache made every decode build a fresh class for an unknown type, so comparing the classes by identity reported two otherwise identical resources, questions, and messages as different. https://github.com/ruby/resolv/commit/05185b4322 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 32 +++++++++++++++++++++++- test/resolv/test_resource.rb | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index b5e377478c00e4..b2b52eebfe520c 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1508,12 +1508,29 @@ 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).all? {|(name, typeclass), (o_name, o_typeclass)| + name == o_name && typeclass_equal?(typeclass, o_typeclass) + } + end + + private def typeclass_equal?(typeclass, other) # :nodoc: + return true if typeclass.equal?(other) + Resource::Generic > typeclass && Resource::Generic > other && + typeclass::TypeValue == other::TypeValue && + typeclass::ClassValue == other::ClassValue + end + def add_question(name, typeclass) @question << [Name.create(name), typeclass] end @@ -2328,6 +2345,19 @@ 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 ==(other) # :nodoc: + return false unless other.is_a?(Generic) + unless self.class.equal?(other.class) + return false unless self.class.superclass.equal?(Generic) && + other.class.superclass.equal?(Generic) && + self.class::TypeValue == other.class::TypeValue && + self.class::ClassValue == other.class::ClassValue + end + return @data == other.data + end + def self.create(type_value, class_value) # :nodoc: c = Class.new(Generic) c.const_set(:TypeValue, type_value) diff --git a/test/resolv/test_resource.rb b/test/resolv/test_resource.rb index 3a1c9ae3c3ea41..33a34bfe26b5ed 100644 --- a/test/resolv/test_resource.rb +++ b/test/resolv/test_resource.rb @@ -20,6 +20,54 @@ def test_hash assert_equal(@name1.hash, @name2.hash, bug10857) 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 + + 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' From 5ee95de080a2ec3a991ea1a958c1a4793672df48 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 31 Jul 2026 13:17:33 +0900 Subject: [PATCH 11/19] [ruby/resolv] Compare generic classes by descent and stop building the zipped array Generic#== accepted only direct subclasses while Message compared any descendant, so an instance of a subclass of a generated class compared unequal. Both now go through Generic.type_class_equal?. Passing a block to zip also avoids allocating the paired array and stops at the first mismatch. https://github.com/ruby/resolv/commit/fcff0a19e4 Co-Authored-By: Claude Opus 5 --- lib/resolv.rb | 31 ++++++++++++++----------------- test/resolv/test_resource.rb | 13 +++++++++++++ 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/resolv.rb b/lib/resolv.rb index b2b52eebfe520c..ae6d85204571d6 100644 --- a/lib/resolv.rb +++ b/lib/resolv.rb @@ -1519,16 +1519,11 @@ def ==(other) # identity alone. private def question_equal?(other_question) # :nodoc: return false unless @question.length == other_question.length - @question.zip(other_question).all? {|(name, typeclass), (o_name, o_typeclass)| - name == o_name && typeclass_equal?(typeclass, o_typeclass) + @question.zip(other_question) {|(name, typeclass), (o_name, o_typeclass)| + return false unless name == o_name && + Resource::Generic.type_class_equal?(typeclass, o_typeclass) } - end - - private def typeclass_equal?(typeclass, other) # :nodoc: - return true if typeclass.equal?(other) - Resource::Generic > typeclass && Resource::Generic > other && - typeclass::TypeValue == other::TypeValue && - typeclass::ClassValue == other::ClassValue + return true end def add_question(name, typeclass) @@ -2347,15 +2342,17 @@ def self.decode_rdata(msg) # :nodoc: # 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 false unless other.is_a?(Generic) - unless self.class.equal?(other.class) - return false unless self.class.superclass.equal?(Generic) && - other.class.superclass.equal?(Generic) && - self.class::TypeValue == other.class::TypeValue && - self.class::ClassValue == other.class::ClassValue - end - return @data == other.data + 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: diff --git a/test/resolv/test_resource.rb b/test/resolv/test_resource.rb index 33a34bfe26b5ed..c5d22ec0e6419b 100644 --- a/test/resolv/test_resource.rb +++ b/test/resolv/test_resource.rb @@ -34,6 +34,19 @@ def test_generic_equality 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")) From 4dd33ca657f9cc49e6718f71001368a4ff7803f3 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 26 Aug 2026 22:15:23 -0400 Subject: [PATCH 12/19] ZJIT: Fix lattice bug in SSA minimization (#18482) Block param minimization was introduced here #17311, but a few tests did not properly eliminate redundant block params. This is because I used `Vec::resize`. For most passes this was fine, especially when trivial block params were discovered. However, in the case where the number of params is not reduced, the vec is not properly cleared between analyses. In some cases, this leaves a dangling trivial param that can be changed if _other_ blocks remove trivial params. In addition to resize, we use `Vec::truncate` which fixes the problem. This test also requires the removal of a test that has issues due to pass ordering. Unless we develop a mechanism to run single analysis passes in tests, we may run into more such issues. --- zjit/src/hir.rs | 10 ++++++++-- zjit/src/hir/opt_tests.rs | 25 +++++++++++++------------ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 0e92bdceab02d7..3b0256f9f8285d 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -6144,7 +6144,7 @@ impl Function { // Instantiate the domain for abstract interpretation. // We store possible param values for each block - let mut param_values: Vec> = vec![Vec::new(); self.blocks.len()]; + let mut param_values: Vec> = self.blocks.iter().map(|block| vec![ParamValue::None; block.params.len()]).collect(); let blocks = self.reverse_post_order(); @@ -6169,8 +6169,14 @@ impl Function { while changed { changed = false; + // When trivial params are elided, the number of params per block can shrink. + // When we reset each analysis loop, we do two things: + // 1. Reset analysis state to None (bottom of the lattice) + // 2. Shrink the number of params per row to match the params per block. + // This resizing occurs when former iterations have found and removed trivial params. for (row, block) in param_values.iter_mut().zip(&self.blocks) { - row.resize(block.params.len(), ParamValue::None); + row.truncate(block.params.len()); + row.as_mut_slice().fill(ParamValue::None); } // Scan through each jump, collecting edges with params to analyze from CondBranch and Jump insns. diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index f1ceb59f514752..9c4f4c6c595abb 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -9649,11 +9649,11 @@ mod hir_opt_tests { bb6(): v18:Truthy = RefineType v10, Truthy v20:FalseClass = Const Value(false) - Jump bb5(v9, v18, v20) + Jump bb5(v18, v20) bb4(): v27:NilClass = Const Value(nil) - Jump bb5(v9, v16, v27) - bb5(v29:BasicObject, v30:BasicObject, v31:Falsy): + Jump bb5(v16, v27) + bb5(v30:BasicObject, v31:Falsy): v36:CBool = HasType v31, FalseClass CondBranch v36, bb8(), bb9() bb8(): @@ -18597,9 +18597,9 @@ mod hir_opt_tests { CondBranch v17, bb9(), bb4() bb9(): v35:Fixnum[0] = Const Value(0) - Jump bb8(v8, v35) - bb8(v48:BasicObject, v49:Fixnum): - v52:Array = RefineType v48, Array + Jump bb8(v35) + bb8(v49:Fixnum): + v52:Array = RefineType v8, Array v53:CInt64 = ArrayLength v52 v54:Fixnum = BoxFixnum v53 v55:BoolExact = FixnumGe v49, v54 @@ -18607,9 +18607,9 @@ mod hir_opt_tests { CondBranch v57, bb11(), bb7() bb11(): CheckInterrupts - Return v48 + Return v8 bb7(): - v75:Array = RefineType v48, Array + v75:Array = RefineType v8, Array v76:CInt64 = UnboxFixnum v49 v77:BasicObject = ArrayAref v75, v76 v79:CPtr = GetEP 0 @@ -18625,7 +18625,7 @@ mod hir_opt_tests { v92:Fixnum[1] = Const Value(1) v93:Fixnum = FixnumAdd v49, v92 PatchPoint NoEPEscape(each) - Jump bb8(v48, v93) + Jump bb8(v93) bb4(): v28:BasicObject = InvokeBuiltin , v8 CheckInterrupts @@ -19235,6 +19235,7 @@ mod hir_opt_tests { "); } + #[ignore = "pass ordering issues cause this test to fail with an improvement to block param minimization."] #[test] fn test_dedup_guard_type_across_cfg_join() { eval(" @@ -21940,8 +21941,8 @@ mod hir_opt_tests { Jump bb3(v5, v6) bb3(v8:BasicObject, v9:NilClass): v13:Fixnum[0] = Const Value(0) - Jump bb5(v8, v13) - bb5(v18:BasicObject, v19:Fixnum): + Jump bb5(v13) + bb5(v19:Fixnum): v23:Fixnum[10] = Const Value(10) PatchPoint MethodRedefined(Integer@0x1000, <@0x1008, cme:0x1010) v58:BoolExact = FixnumLt v19, v23 @@ -21952,7 +21953,7 @@ mod hir_opt_tests { v48:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, +@0x1038, cme:0x1040) v62:Fixnum = FixnumAdd v19, v48 - Jump bb5(v18, v62) + Jump bb5(v62) bb6(): CheckInterrupts Return v19 From c56fa9401b2fbd21bf91b5cd0747fd485748dd4b Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 27 Aug 2026 09:34:11 +0900 Subject: [PATCH 13/19] Don't record EP escape in JIT when not enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We shouldn't record JIT information before the JIT is actually enabled. This change doesn't seem to affect ZJIT or YJIT performance beyond the margin of error: ZJIT: -------------- ------------ ------------ -------------- ------------- bench master (ms) branch (ms) branch 1st itr master/branch activerecord 50.1 ± 7.4% 49.0 ± 5.7% 0.948 1.022 chunky-png 164.2 ± 2.1% 166.8 ± 2.8% 0.985 0.985 erubi-rails 247.0 ± 2.8% 264.6 ± 2.2% 0.945 0.934 hexapdf 571.3 ± 1.0% 565.4 ± 1.2% 0.959 1.010 liquid-c 17.6 ± 13.1% 19.2 ± 12.9% 0.976 0.920 liquid-compile 15.7 ± 11.0% 15.6 ± 7.5% 0.978 1.003 liquid-il 79.8 ± 2.0% 78.2 ± 2.2% 0.993 1.021 liquid-render 32.3 ± 6.8% 32.3 ± 6.3% 1.001 0.999 lobsters 243.8 ± 4.9% 242.8 ± 4.7% 0.976 1.004 mail 38.8 ± 3.1% 39.5 ± 5.1% 0.993 0.982 psych-load 612.4 ± 1.5% 610.0 ± 1.9% 1.001 1.004 railsbench 343.8 ± 2.9% 350.8 ± 2.2% 0.991 0.980 rubocop 50.0 ± 10.6% 51.2 ± 12.0% 1.019 0.978 ruby-lsp 42.9 ± 3.8% 43.2 ± 3.7% 0.974 0.993 sequel 18.5 ± 6.6% 18.7 ± 6.6% 1.021 0.991 shipit 344.3 ± 1.3% 339.8 ± 3.4% 0.981 1.013 -------------- ------------ ------------ -------------- ------------- YJIT: -------------- ------------ ------------ -------------- ------------- bench master (ms) branch (ms) branch 1st itr master/branch activerecord 42.6 ± 7.7% 40.6 ± 5.5% 1.099 1.048 chunky-png 164.7 ± 1.3% 164.8 ± 1.1% 1.060 0.999 erubi-rails 256.7 ± 3.3% 241.3 ± 2.3% 1.035 1.064 hexapdf 445.6 ± 1.0% 444.6 ± 1.0% 1.012 1.002 liquid-c 16.4 ± 11.2% 16.7 ± 13.3% 1.037 0.986 liquid-compile 14.5 ± 11.5% 14.3 ± 7.8% 0.990 1.011 liquid-il 74.9 ± 2.6% 73.4 ± 3.6% 0.993 1.021 liquid-render 23.1 ± 8.9% 23.3 ± 8.7% 1.001 0.992 lobsters 239.9 ± 9.9% 238.7 ± 9.3% 0.989 1.005 mail 29.2 ± 4.6% 30.0 ± 5.8% 0.977 0.973 psych-load 515.5 ± 1.2% 511.5 ± 0.7% 0.973 1.008 railsbench 317.4 ± 2.2% 317.9 ± 2.5% 1.008 0.998 rubocop 42.1 ± 18.5% 42.0 ± 14.4% 1.052 1.004 ruby-lsp 41.5 ± 3.8% 41.6 ± 3.8% 1.013 0.998 sequel 18.5 ± 6.6% 18.2 ± 5.4% 1.150 1.018 shipit 282.8 ± 3.8% 285.5 ± 5.2% 1.000 0.991 -------------- ------------ ------------ -------------- ------------- --- iseq.c | 4 ++-- vm.c | 4 ++-- vm_eval.c | 2 +- yjit/src/invariants.rs | 5 ----- zjit/src/invariants.rs | 5 ----- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/iseq.c b/iseq.c index 8e424756d95c11..74a6d9bc7e9b71 100644 --- a/iseq.c +++ b/iseq.c @@ -192,14 +192,14 @@ rb_iseq_free(const rb_iseq_t *iseq) iseq_clear_ic_references(iseq); struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq); #if USE_YJIT - rb_yjit_iseq_free(iseq); + if (rb_yjit_enabled_p) rb_yjit_iseq_free(iseq); if (FL_TEST_RAW((VALUE)iseq, ISEQ_TRANSLATED)) { RUBY_ASSERT(rb_yjit_live_iseq_count > 0); rb_yjit_live_iseq_count--; } #endif #if USE_ZJIT - rb_zjit_iseq_free(iseq); + if (rb_zjit_enabled_p) rb_zjit_iseq_free(iseq); #endif SIZED_FREE_N(body->iseq_encoded, body->iseq_size); SIZED_FREE_N(body->insns_info.body, body->insns_info.size); diff --git a/vm.c b/vm.c index d4fac088004a5c..d2d1f55d5e88d0 100644 --- a/vm.c +++ b/vm.c @@ -1140,8 +1140,8 @@ vm_make_env_each(const rb_execution_context_t * const ec, rb_control_frame_t *co // are no longer useful and can slow down Ractors. if (VM_FRAME_RUBYFRAME_P(cfp) && !rbimpl_atomic_load(&ISEQ_BODY(iseq)->jit_ep_escape_recorded, RBIMPL_ATOMIC_RELAXED)) { - rb_yjit_invalidate_ep_is_bp(iseq); - rb_zjit_invalidate_no_ep_escape(iseq); + if (rb_yjit_enabled_p) rb_yjit_invalidate_ep_is_bp(iseq); + if (rb_zjit_enabled_p) rb_zjit_invalidate_no_ep_escape(iseq); } /* diff --git a/vm_eval.c b/vm_eval.c index eccf3bf8256909..e9fdb3354bb69d 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -1997,7 +1997,7 @@ eval_string_with_cref(VALUE self, VALUE src, rb_cref_t *cref, VALUE file, int li // EP is not escaped to the heap here, but captured and reused by another frame. // ZJIT's locals are incompatible with it unlike YJIT's, so invalidate the ISEQ for ZJIT. - rb_zjit_invalidate_no_ep_escape(CFP_ISEQ(cfp)); + if (rb_zjit_enabled_p) rb_zjit_invalidate_no_ep_escape(CFP_ISEQ(cfp)); iseq = eval_make_iseq(src, file, line, &block); if (!iseq) { diff --git a/yjit/src/invariants.rs b/yjit/src/invariants.rs index e726b83df3d6e5..68eb84604259ff 100644 --- a/yjit/src/invariants.rs +++ b/yjit/src/invariants.rs @@ -573,11 +573,6 @@ pub extern "C" fn rb_yjit_invalidate_no_singleton_class(klass: VALUE) { /// equal to base pointer. #[no_mangle] pub extern "C" fn rb_yjit_invalidate_ep_is_bp(iseq: IseqPtr) { - // Skip tracking EP escapes on boot. We don't need to invalidate anything during boot. - if unsafe { INVARIANTS.is_none() } { - return; - } - with_vm_lock(src_loc!(), || { // If an EP escape for this ISEQ is detected for the first time, invalidate all blocks // associated to the ISEQ. The iseq flag records the escape, so the map keeps only diff --git a/zjit/src/invariants.rs b/zjit/src/invariants.rs index 32be2087d62fd9..743c5979b74912 100644 --- a/zjit/src/invariants.rs +++ b/zjit/src/invariants.rs @@ -194,11 +194,6 @@ pub extern "C" fn rb_zjit_bop_redefined(klass: RedefinitionFlag, bop: ruby_basic /// equal to base pointer. #[unsafe(no_mangle)] pub extern "C" fn rb_zjit_invalidate_no_ep_escape(iseq: IseqPtr) { - // Skip tracking EP escapes on boot. We don't need to invalidate anything during boot. - if !ZJITState::has_instance() { - return; - } - with_vm_lock(src_loc!(), || { // Remember that this ISEQ may escape EP unsafe { rb_jit_iseq_mark_ep_escape_recorded(iseq) }; From 553792743191505de5f0b0c3b4c273c240ab2115 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 02:10:05 +0000 Subject: [PATCH 14/19] Bump taiki-e/install-action Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `taiki-e/install-action` from 2.86.6 to 2.86.7 - [Release notes](https://github.com/taiki-e/install-action/releases) - [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/taiki-e/install-action/compare/6cd13508893c0e7eab5f273c2575d3859bd7229a...b6ff580856c41316412a0b9b60540fbc6f8c82cc) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.86.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 9d657a6cd82b29..85688e3024946c 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2.86.6 + - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 81d10e9870278d..b8f64735891d9c 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@6cd13508893c0e7eab5f273c2575d3859bd7229a # v2.86.6 + - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 5496feb0b989d2ad982197d08e821b0269abd771 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 27 Aug 2026 07:01:50 +0900 Subject: [PATCH 15/19] [ruby/rubygems] Drop the settings write behind the removed --no-prune flag Every documented spelling of the flag raises before reaching it, so the only way in was Thor resolving `--skip-no-prune` to a negated value, which `flag_passed?` does not match. That let an undocumented spelling change a setting the documented one is refused for. https://github.com/ruby/rubygems/commit/197eccbeed Co-Authored-By: Claude Opus 5 --- lib/bundler/cli/install.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/bundler/cli/install.rb b/lib/bundler/cli/install.rb index d242094a5acf5e..2cebdff5c8a43d 100644 --- a/lib/bundler/cli/install.rb +++ b/lib/bundler/cli/install.rb @@ -118,8 +118,6 @@ def normalize_settings Bundler::CLI::Common.validate_cooldown!(options["cooldown"]) Bundler.settings.set_command_option_if_given :cooldown, options["cooldown"] - Bundler.settings.set_command_option_if_given :no_prune, options["no-prune"] - Bundler.settings.set_command_option_if_given :no_install, options["no-install"] Bundler.settings.set_command_option_if_given :clean, options["clean"] From 1b33a60f8735bfc399b6633ae8d8cc407d066a37 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 27 Aug 2026 07:01:50 +0900 Subject: [PATCH 16/19] [ruby/rubygems] Rename the no_prune setting to keep_outdated_cache The `prune` vocabulary is being reused for a new setting, and `no_prune` was already a poor name for a flag that only decides whether `bundle cache` keeps gems that dropped out of the resolution. `no_prune` is still read until Bundler 5 removes it, resolved one priority level at a time so that an old name set locally keeps beating a current name set globally, the way the documented order promises. https://github.com/ruby/rubygems/commit/98eaec7e9e Co-Authored-By: Claude Opus 5 --- lib/bundler/cli.rb | 9 ++-- lib/bundler/man/bundle-config.1 | 4 +- lib/bundler/man/bundle-config.1.ronn | 7 ++- lib/bundler/runtime.rb | 2 +- lib/bundler/settings.rb | 49 ++++++++++++++++---- spec/bundler/bundler/settings_spec.rb | 22 +++++++++ spec/bundler/cache/gems_spec.rb | 41 ++++++++++++++++ spec/bundler/other/major_deprecation_spec.rb | 22 ++++++++- 8 files changed, 137 insertions(+), 19 deletions(-) diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index f82a94fe6fd80e..9655a8ad1b7de1 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -276,10 +276,12 @@ def remove(*gems) method_option "with", type: :array, banner: "Include gems that are part of the specified named group (removed)." method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def install - %w[clean deployment frozen no-prune path shebang without with].each do |option| + %w[clean deployment frozen path shebang without with].each do |option| remembered_flag_deprecation(option) end + remembered_flag_deprecation("no-prune", option_name: "keep_outdated_cache") + print_remembered_flag_deprecation("--system", "path.system", "true") if ARGV.include?("--system") remembered_flag_deprecation("deployment", negative: true) @@ -473,9 +475,8 @@ def cache print_remembered_flag_deprecation("--all", "cache_all", "true") if ARGV.include?("--all") print_remembered_flag_deprecation("--no-all", "cache_all", "false") if ARGV.include?("--no-all") - %w[frozen no-prune].each do |option| - remembered_flag_deprecation(option) - end + remembered_flag_deprecation("frozen") + remembered_flag_deprecation("no-prune", option_name: "keep_outdated_cache") if flag_passed?("--path") removed_message = diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 368a85c1812f93..fcfe49fa165959 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -151,6 +151,8 @@ The store can also be selected per host with \fBcredential_store\.\fR (\fB .IP "\(bu" 4 \fBjobs\fR (\fBBUNDLE_JOBS\fR): The number of gems Bundler can download and install in parallel\. Defaults to the number of available processors\. .IP "\(bu" 4 +\fBkeep_outdated_cache\fR (\fBBUNDLE_KEEP_OUTDATED_CACHE\fR): Whether Bundler should leave outdated gems unpruned when caching\. Defaults to false\. +.IP "\(bu" 4 \fBlockfile\fR (\fBBUNDLE_LOCKFILE\fR): The path to the lockfile that bundler should use\. By default, Bundler adds \fB\.lock\fR to the end of the \fBgemfile\fR entry\. Can be set to \fBfalse\fR in the Gemfile to disable lockfile creation entirely (see gemfile(5))\. .IP "\(bu" 4 \fBlockfile_checksums\fR (\fBBUNDLE_LOCKFILE_CHECKSUMS\fR): Whether Bundler should include a checksums section in new lockfiles, to protect from compromised gem sources\. Defaults to true\. Bundler's own checksum is only included when its \fB\.gem\fR file is cached, which may not be the case when Bundler is installed as a default gem\. @@ -161,7 +163,7 @@ The store can also be selected per host with \fBcredential_store\.\fR (\fB .IP "\(bu" 4 \fBno_install_plugin\fR (\fBBUNDLE_NO_INSTALL_PLUGIN\fR): Whether Bundler should skip installing RubyGems plugins during installation\. When set, plugin files are not written to the plugins directory\. To install plugins later, unset this setting and run \fBbundle pristine \fR\. .IP "\(bu" 4 -\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Whether Bundler should leave outdated gems unpruned when caching\. +\fBno_prune\fR (\fBBUNDLE_NO_PRUNE\fR): Deprecated name for \fBkeep_outdated_cache\fR\. Each priority level is checked for \fBkeep_outdated_cache\fR and then for \fBno_prune\fR, and the first value found wins\. \fBno_prune\fR will be removed in Bundler 5\. .IP "\(bu" 4 \fBonly\fR (\fBBUNDLE_ONLY\fR): A space\-separated list of groups to install only gems of the specified groups\. Please check carefully if you want to install also gems without a group, because they get put inside \fBdefault\fR group\. For example \fBonly test:default\fR will install all gems specified in test group and without one\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 834356a2d0643d..689d6227ca0d81 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -276,6 +276,9 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). * `jobs` (`BUNDLE_JOBS`): The number of gems Bundler can download and install in parallel. Defaults to the number of available processors. +* `keep_outdated_cache` (`BUNDLE_KEEP_OUTDATED_CACHE`): + Whether Bundler should leave outdated gems unpruned when caching. Defaults + to false. * `lockfile` (`BUNDLE_LOCKFILE`): The path to the lockfile that bundler should use. By default, Bundler adds `.lock` to the end of the `gemfile` entry. Can be set to `false` in the @@ -295,7 +298,9 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). When set, plugin files are not written to the plugins directory. To install plugins later, unset this setting and run `bundle pristine `. * `no_prune` (`BUNDLE_NO_PRUNE`): - Whether Bundler should leave outdated gems unpruned when caching. + Deprecated name for `keep_outdated_cache`. Each priority level is checked + for `keep_outdated_cache` and then for `no_prune`, and the first value + found wins. `no_prune` will be removed in Bundler 5. * `only` (`BUNDLE_ONLY`): A space-separated list of groups to install only gems of the specified groups. Please check carefully if you want to install also gems without a group, because diff --git a/lib/bundler/runtime.rb b/lib/bundler/runtime.rb index 248327cb82d329..9d2ba5f1c37787 100644 --- a/lib/bundler/runtime.rb +++ b/lib/bundler/runtime.rb @@ -148,7 +148,7 @@ def cache(custom_path = nil, local = false) FileUtils.touch(File.expand_path("../.bundlecache", git_dir)) end - prune_cache(cache_path) unless Bundler.settings[:no_prune] + prune_cache(cache_path) unless Bundler.settings[:keep_outdated_cache] end def prune_cache(cache_path) diff --git a/lib/bundler/settings.rb b/lib/bundler/settings.rb index c439b5c01db411..e9a26172b7786a 100644 --- a/lib/bundler/settings.rb +++ b/lib/bundler/settings.rb @@ -29,6 +29,7 @@ class Settings ignore_messages init_gems_rb inline + keep_outdated_cache lockfile_checksums no_build_extension no_install @@ -95,6 +96,14 @@ class Settings "BUNDLE_UPDATE_REQUIRES_ALL_FLAG" => false, }.freeze + ## + # Settings renamed in Bundler 4, mapping the current name to the one it + # replaced. The old name is still read, and goes away in Bundler 5. + + RENAMED_KEYS = { + "keep_outdated_cache" => "no_prune", + }.freeze + def initialize(root = nil) @root = root @local_config = load_config(local_config_file) @@ -111,16 +120,7 @@ def initialize(root = nil) end def [](name) - key = key_for(name) - - value = nil - configs.each do |_, config| - value = config[key] - next if value.nil? - break - end - - converted_value(value, name) + converted_value(configured_value(name), name) end def set_command_option(key, value) @@ -398,6 +398,35 @@ def value_for(name, config) converted_value(config[key_for(name)], name) end + ## + # A renamed setting is resolved one level at a time rather than by looking + # for the current name everywhere first, so that the old name keeps the + # documented priority order: an old name set locally still beats a current + # name set globally. + + def configured_value(name) + key = key_for(name) + old_name = RENAMED_KEYS[self.class.key_to_s(name)] + old_key = key_for(old_name) if old_name + + configs.each do |_, config| + value = config[key] + return value unless value.nil? + + next if old_key.nil? + + value = config[old_key] + next if value.nil? + + SharedHelpers.feature_deprecated! "The `#{old_name}` setting has been renamed to `#{name}` and will be " \ + "removed in Bundler 5. Use `#{name}` instead." + + return value + end + + nil + end + def parent_setting_for(name) split_specific_setting_for(name)[0] end diff --git a/spec/bundler/bundler/settings_spec.rb b/spec/bundler/bundler/settings_spec.rb index cb689fe167d3dd..42e3dca1280a7b 100644 --- a/spec/bundler/bundler/settings_spec.rb +++ b/spec/bundler/bundler/settings_spec.rb @@ -136,6 +136,28 @@ end end + context "when the setting has been renamed" do + it "reads the value set under the old name" do + settings.set_local :no_prune, "true" + + expect(settings[:keep_outdated_cache]).to be true + end + + it "prefers the current name set at the same level" do + settings.set_local :no_prune, "true" + settings.set_local :keep_outdated_cache, "false" + + expect(settings[:keep_outdated_cache]).to be false + end + + it "prefers the old name set at a higher priority level" do + settings.set_global :keep_outdated_cache, "false" + settings.set_local :no_prune, "true" + + expect(settings[:keep_outdated_cache]).to be true + end + end + context "when it's not possible to create the settings directory" do it "raises an PermissionError with explanation" do settings_dir = settings.send(:local_config_file).dirname diff --git a/spec/bundler/cache/gems_spec.rb b/spec/bundler/cache/gems_spec.rb index 198279d84cdfb1..6f405c0305deb4 100644 --- a/spec/bundler/cache/gems_spec.rb +++ b/spec/bundler/cache/gems_spec.rb @@ -266,6 +266,47 @@ expect(cached_gem("activesupport-2.3.2")).not_to exist end + it "keeps outdated .gems when keep_outdated_cache is set" do + setup_main_repo + bundle_config "keep_outdated_cache true" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + expect(cached_gem("myrack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + end + + it "keeps outdated .gems when only the deprecated no_prune is set" do + setup_main_repo + bundle_config "no_prune true" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + expect(cached_gem("myrack-1.0.0")).to exist + expect(cached_gem("actionpack-2.3.2")).to exist + expect(cached_gem("activesupport-2.3.2")).to exist + expect(deprecations.count {|d| d.include?("no_prune") }).to eq 1 + end + + it "lets keep_outdated_cache win over the deprecated no_prune" do + setup_main_repo + bundle_config "no_prune true" + bundle_config "keep_outdated_cache false" + + install_gemfile <<-G + source "https://gem.repo2" + gem "myrack" + G + expect(cached_gem("actionpack-2.3.2")).not_to exist + expect(cached_gem("activesupport-2.3.2")).not_to exist + expect(err).not_to include("no_prune") + end + it "removes .gems when gem changes to git source" do setup_main_repo build_git "myrack" diff --git a/spec/bundler/other/major_deprecation_spec.rb b/spec/bundler/other/major_deprecation_spec.rb index 8e30b531d4e036..bb714e6ac87259 100644 --- a/spec/bundler/other/major_deprecation_spec.rb +++ b/spec/bundler/other/major_deprecation_spec.rb @@ -255,6 +255,24 @@ end end + context "bundle config no_prune" do + before do + install_gemfile <<-G + source "https://gem.repo1" + gem "myrack" + G + + bundle_config "no_prune true" + bundle :cache + end + + it "warns that the setting has been renamed" do + expect(deprecations).to include( + "The `no_prune` setting has been renamed to `keep_outdated_cache` and will be removed in Bundler 5. Use `keep_outdated_cache` instead." + ) + end + end + context "bundle cache --no-prune" do before do gemfile <<-G @@ -269,7 +287,7 @@ expect(err).to include( "The `--no-prune` flag has been removed because it relied on being " \ "remembered across bundler invocations, which bundler no longer " \ - "does. Instead please use `bundle config set no_prune true`, " \ + "does. Instead please use `bundle config set keep_outdated_cache true`, " \ "and stop using this flag" ) end @@ -448,7 +466,7 @@ "deployment" => ["deployment", "true"], "frozen" => ["frozen", "true"], "no-deployment" => ["deployment", "false"], - "no-prune" => ["no_prune", "true"], + "no-prune" => ["keep_outdated_cache", "true"], "path" => ["path", "'vendor/bundle'"], "shebang" => ["shebang", "'ruby27'"], "system" => ["path.system", "true"], From 3d139905db6539189b347e3de8257e9dc088715b Mon Sep 17 00:00:00 2001 From: Max Bernstein Date: Wed, 26 Aug 2026 23:39:16 -0400 Subject: [PATCH 17/19] ZJIT: Eliminate constant-valued block params (#18369) In the following code, ```ruby def test(cond) if cond x = 1 else x = 1 end x end ``` the `x` after the conditional would ordinarily get a block param merging two constant `1`. Even if we had dominator-based GVN, this would still happen; each `1` is in a sibling block (they would need to be hoisted). This also happens a lot with nil-filled locals; interpreter and JIT entry blocks nil-fill the FrameState for each local variable and these nils get passed around when they don't need to. Instead of doing that, determine if we know the value of a parameter even if it has multiple input SSA values; joining two `nil` will always produce a `nil`, for eaxmple. This speeds up some local-heavy benchmarks such as protoboeuf. Before: ``` plum% WARMUP_ITRS=0 MIN_BENCH_ITRS=10 MIN_BENCH_TIME=0 ruby --zjit benchmarks/protoboeuf/benchmark.rb ruby 4.1.0dev (2026-08-18T13:12:35Z master 6b719acc57) +ZJIT stats +PRISM [arm64-darwin25] itr: time #1: 96ms #2: 22ms #3: 33ms #4: 21ms #5: 19ms #6: 20ms #7: 19ms #8: 19ms #9: 20ms #10: 20ms ``` After: ``` plum% WARMUP_ITRS=0 MIN_BENCH_ITRS=10 MIN_BENCH_TIME=0 ruby --zjit benchmarks/protoboeuf/benchmark.rb ruby 4.1.0dev (2026-08-18T15:51:16Z mb-remove-constant.. 2fbcac766e) +ZJIT stats +PRISM [arm64-darwin25] itr: time #1: 81ms #2: 16ms #3: 26ms #4: 15ms #5: 14ms #6: 14ms #7: 14ms #8: 14ms #9: 14ms #10: 14ms ``` --- zjit/src/hir.rs | 20 +- zjit/src/hir/opt_tests.rs | 579 +++++++++++++++++++++----------------- zjit/src/hir_type/mod.rs | 37 ++- 3 files changed, 362 insertions(+), 274 deletions(-) diff --git a/zjit/src/hir.rs b/zjit/src/hir.rs index 3b0256f9f8285d..9a6461b9301d93 100644 --- a/zjit/src/hir.rs +++ b/zjit/src/hir.rs @@ -3066,6 +3066,13 @@ impl Function { id } + pub fn prepend_insn(&mut self, block: BlockId, insn: Insn) -> InsnId { + assert!(!matches!(insn, Insn::Param), "Cannot prepend a Param instruction"); + let id = self.new_insn(insn); + self.blocks[block].insns.insert(0, id); + id + } + pub fn push_comment(&mut self, block: BlockId, message: String) -> InsnId { self.push_insn(block, Insn::Comment { message }) } @@ -6206,6 +6213,17 @@ impl Function { for (idx, state) in block_preds.iter().enumerate() { if let ParamValue::One(_) = state { trivial_indices.push(idx); + } else { + // If the param has a constant Ruby object associated with it, even if it + // is passed muliple InsnId, we can still optimize it away. + let param_id = self.blocks[*block_id].params[idx]; + if let Some(obj) = self.type_of(param_id).ruby_object() { + let const_insn = self.prepend_insn(*block_id, Insn::Const { val: Const::Value(obj) }); + self.insn_types[const_insn] = self.infer_type(const_insn); + self.make_equal_to(param_id, const_insn); + trivial_indices.push(idx); + changed = true; + } } } @@ -7241,8 +7259,8 @@ impl Function { } else { false }; - run_pass!(remove_trivial_block_params); run_pass!(convert_no_profile_sends); + run_pass!(remove_trivial_block_params); run_pass!(optimize_load_store); run_pass!(canonicalize); run_pass!(fold_constants); diff --git a/zjit/src/hir/opt_tests.rs b/zjit/src/hir/opt_tests.rs index 9c4f4c6c595abb..53b77e05601e31 100644 --- a/zjit/src/hir/opt_tests.rs +++ b/zjit/src/hir/opt_tests.rs @@ -43,14 +43,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v40:NilClass = Const Value(nil) v13:TrueClass = Const Value(true) v24:Fixnum[3] = Const Value(3) CheckInterrupts @@ -75,14 +74,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v40:NilClass = Const Value(nil) v13:FalseClass = Const Value(false) v34:Fixnum[4] = Const Value(4) CheckInterrupts @@ -1734,6 +1732,49 @@ mod hir_opt_tests { "); } + // `m`'s else branch is profiled with Fixnums, so it gets specialized to FixnumAdd. Once `m` is + // inlined into `test`, where `x` is known to be nil, that branch becomes statically + // unreachable: infer_types skips it and its instructions keep the `Empty` type. Later passes + // still walk the block, so `Empty` must not masquerade as a known Ruby object (`nil`) -- + // folding FixnumAdd would then call `as_fixnum` on `nil` and panic. + #[test] + fn test_fixnum_add_in_unreachable_block_after_inlining() { + eval(" + def m(x) + if x.nil? + 0 + else + x + 1 + end + end + def test = m(nil) + m(1); m(2) + test; test + "); + assert_snapshot!(hir_string("test"), @" + fn test@:9: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + Jump bb3(v1) + bb2(): + EntryPoint JIT(0) + v4:BasicObject = LoadArg :self@0 + Jump bb3(v4) + bb3(v6:BasicObject): + v11:NilClass = Const Value(nil) + PatchPoint MethodRedefined(Object@0x1000, m@0x1008, cme:0x1010) + v20:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v6, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile + PushInlineFrame :m, v20 (0x1038), num_args=1 + PatchPoint MethodRedefined(NilClass@0x1060, nil?@0x1068, cme:0x1070) + v52:Fixnum[0] = Const Value(0) + CheckInterrupts + v84:Fixnum[0] = Const Value(0) + PopInlineFrame + Return v84 + "); + } + #[test] fn test_optimize_send_to_aliased_cfunc() { eval(" @@ -2060,8 +2101,8 @@ mod hir_opt_tests { v58:NilClass = Const Value(nil) CheckInterrupts PopInlineFrame - v120:NilClass = Const Value(nil) - Return v120 + v131:NilClass = Const Value(nil) + Return v131 "); } @@ -2100,8 +2141,8 @@ mod hir_opt_tests { v61:NilClass = Const Value(nil) CheckInterrupts PopInlineFrame - v129:NilClass = Const Value(nil) - Return v129 + v140:NilClass = Const Value(nil) + Return v140 "); } @@ -2581,14 +2622,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v27:NilClass = Const Value(nil) v13:Fixnum[2] = Const Value(2) v17:Fixnum[1] = Const Value(1) v26:RangeExact = NewRangeFixnum v17 NewRangeInclusive v13 @@ -2612,14 +2652,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v27:NilClass = Const Value(nil) v13:Fixnum[2] = Const Value(2) v17:Fixnum[1] = Const Value(1) v26:RangeExact = NewRangeFixnum v17 NewRangeExclusive v13 @@ -2780,14 +2819,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v23:NilClass = Const Value(nil) v13:ArrayExact = NewArray v17:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2876,14 +2914,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v23:NilClass = Const Value(nil) v13:RangeExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v17:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2905,14 +2942,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v31:NilClass = Const Value(nil) PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_UMINUS) v14:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v16:StringExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) @@ -2941,15 +2977,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :a@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :a@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v28:NilClass = Const Value(nil) v18:ArrayExact = NewArray v12 v22:Fixnum[5] = Const Value(5) CheckInterrupts @@ -2970,14 +3005,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v25:NilClass = Const Value(nil) v13:HashExact = NewHash PatchPoint NoEPEscape(test) v19:Fixnum[5] = Const Value(5) @@ -3002,16 +3036,15 @@ mod hir_opt_tests { v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :aval@0x1000 v4:BasicObject = LoadField v2, :bval@0x1001 - v5:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4, v5) + Jump bb3(v1, v3, v4) bb2(): EntryPoint JIT(0) v8:BasicObject = LoadArg :self@0 v9:BasicObject = LoadArg :aval@1 v10:BasicObject = LoadArg :bval@2 - v11:NilClass = Const Value(nil) - Jump bb3(v8, v9, v10, v11) - bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:NilClass): + Jump bb3(v8, v9, v10) + bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject): + v38:NilClass = Const Value(nil) v20:StaticSymbol[:a] = Const Value(VALUE(0x1008)) v23:StaticSymbol[:b] = Const Value(VALUE(0x1010)) v26:HashExact = NewHash v20: v14, v23: v15 @@ -3036,14 +3069,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v24:NilClass = Const Value(nil) v13:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v14:ArrayExact = ArrayDup v13 v18:Fixnum[5] = Const Value(5) @@ -3065,14 +3097,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v24:NilClass = Const Value(nil) v13:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v14:HashExact = HashDup v13 v18:Fixnum[5] = Const Value(5) @@ -3095,14 +3126,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v22:NilClass = Const Value(nil) v16:Fixnum[5] = Const Value(5) CheckInterrupts Return v16 @@ -3123,14 +3153,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v24:NilClass = Const Value(nil) v13:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v14:StringExact = StringCopy v13 v18:Fixnum[5] = Const Value(5) @@ -3637,14 +3666,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v30:NilClass = Const Value(nil) v13:ArrayExact = NewArray PatchPoint NoSingletonClass(Array@0x1000) PatchPoint MethodRedefined(Array@0x1000, itself@0x1008, cme:0x1010) @@ -3670,14 +3698,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v33:NilClass = Const Value(nil) PatchPoint StableConstantNames(0x1000, M) v14:ModuleExact[M@0x1008] = Const Value(VALUE(0x1008)) PatchPoint NoSingletonClass(Module@0x1010) @@ -4004,15 +4031,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :x@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :x@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v32:NilClass = Const Value(nil) v17:ArrayExact[VALUE(0x1008)] = Const Value(VALUE(0x1008)) v18:ArrayExact = ArrayDup v17 PatchPoint NoSingletonClass(Array@0x1010) @@ -4605,14 +4631,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v36:NilClass = Const Value(nil) v13:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v34:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile @@ -4643,16 +4668,16 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 v6:NilClass = Const Value(nil) v7:CPtr = GetEP 0 StoreField v7, :a@0x1000, v6 - Jump bb3(v5, v6) - bb3(v10:BasicObject, v11:NilClass): + Jump bb3(v5) + bb3(v10:BasicObject): + v48:NilClass = Const Value(nil) v15:Fixnum[1] = Const Value(1) SetLocal :a, l0, EP@3, v15 PatchPoint MethodRedefined(Object@0x1008, lambda@0x1010, cme:0x1018) @@ -5965,15 +5990,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :s@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :s@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v37:NilClass = Const Value(nil) v17:ArrayExact = NewArray v22:TrueClass = Const Value(true) v24:BasicObject = Send v12, 0x1008, :each_line, v22 # SendFallbackReason: Complex argument passing @@ -6766,15 +6790,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :block@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :block@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v50:NilClass = Const Value(nil) v18:CPtr = GetEP 0 v19:CUInt64 = LoadField v18, :VM_ENV_DATA_INDEX_FLAGS@0x1001 v20:CBool = IsBlockParamModified v19 @@ -6818,14 +6841,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v45:NilClass = Const Value(nil) v14:CPtr = GetEP 1 v15:CUInt64 = LoadField v14, :VM_ENV_DATA_INDEX_FLAGS@0x1000 v16:CBool = IsBlockParamModified v15 @@ -7365,15 +7387,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :p@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :p@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v31:NilClass = Const Value(nil) v17:ArrayExact = NewArray v23:ArrayExact = ToArray v17 v25:BasicObject = Send v12, :call, v23 # SendFallbackReason: Complex argument passing @@ -8687,14 +8708,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v36:NilClass = Const Value(nil) v20:NilClass = Const Value(nil) CheckInterrupts Return v20 @@ -8715,18 +8735,19 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v37:NilClass = Const Value(nil) v13:Fixnum[1] = Const Value(1) PatchPoint MethodRedefined(Integer@0x1000, itself@0x1008, cme:0x1010) + v39:Fixnum[1] = Const Value(1) + v38:Fixnum[1] = Const Value(1) CheckInterrupts - Return v13 + Return v39 "); } @@ -11085,14 +11106,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v43:NilClass = Const Value(nil) v13:ArrayExact = NewArray PatchPoint SingleRactorMode PatchPoint StableConstantNames(0x1000, A) @@ -11368,8 +11388,9 @@ mod hir_opt_tests { PushInlineFrame :foo, v19 (0x1038), num_args=0 v43:Fixnum[42] = Const Value(42) CheckInterrupts + v53:Fixnum[42] = Const Value(42) PopInlineFrame - Return v43 + Return v53 "); } @@ -11389,14 +11410,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v25:NilClass = Const Value(nil) v13:StaticSymbol[:to_s] = Const Value(VALUE(0x1000)) v19:BasicObject = Send v8, &block, :foo, v13 # SendFallbackReason: Send: block argument is not nil CheckInterrupts @@ -11420,14 +11440,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v29:NilClass = Const Value(nil) v13:NilClass = Const Value(nil) PatchPoint MethodRedefined(Object@0x1000, foo@0x1008, cme:0x1010) v27:ObjectSubclass[class_exact*:Object@VALUE(0x1000)] = GuardType v8, ObjectSubclass[class_exact*:Object@VALUE(0x1000)] recompile @@ -12134,22 +12153,21 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v38:NilClass = Const Value(nil) v13:ArrayExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v14:ArrayExact = ArrayDup v13 v19:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(Array@0x1008) PatchPoint MethodRedefined(Array@0x1008, []@0x1010, cme:0x1018) - v38:CInt64[0] = Const CInt64(0) + v39:CInt64[0] = Const CInt64(0) v32:CInt64 = ArrayLength v14 - v33:CInt64[0] = GuardLess v38, v32 + v33:CInt64[0] = GuardLess v39, v32 v37:BasicObject = ArrayAref v14, v33 CheckInterrupts Return v37 @@ -12250,14 +12268,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v32:NilClass = Const Value(nil) v13:HashExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v14:HashExact = HashDup v13 v19:Fixnum[1] = Const Value(1) @@ -12378,14 +12395,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v39:NilClass = Const Value(nil) v13:HashExact = NewHash PatchPoint NoEPEscape(test) v22:Fixnum[1] = Const Value(1) @@ -14651,14 +14667,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v49:NilClass = Const Value(nil) v13:ArrayExact = NewArray v19:ArrayExact = ToArray v13 v21:BasicObject = Send v8, :foo, v19 # SendFallbackReason: Complex argument passing @@ -15241,21 +15256,20 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v32:NilClass = Const Value(nil) PatchPoint BOPRedefined(STRING_REDEFINED_OP_FLAG, BOP_FREEZE) v14:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) PatchPoint NoSingletonClass(String@0x1008) PatchPoint MethodRedefined(String@0x1008, ==@0x1010, cme:0x1018) - v32:TrueClass = Const Value(true) + v33:TrueClass = Const Value(true) CheckInterrupts - Return v32 + Return v33 "); } @@ -15439,16 +15453,14 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - v3:NilClass = Const Value(nil) - Jump bb3(v1, v2, v3) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v6:BasicObject = LoadArg :self@0 - v7:NilClass = Const Value(nil) - v8:NilClass = Const Value(nil) - Jump bb3(v6, v7, v8) - bb3(v10:BasicObject, v11:NilClass, v12:NilClass): + Jump bb3(v6) + bb3(v10:BasicObject): + v57:NilClass = Const Value(nil) + v56:NilClass = Const Value(nil) v16:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) v17:StringExact = StringCopy v16 v21:StringExact[VALUE(0x1000)] = Const Value(VALUE(0x1000)) @@ -16524,8 +16536,7 @@ mod hir_opt_tests { v3:BasicObject = LoadField v2, :a@0x1000 v4:BasicObject = LoadField v2, :_b@0x1001 v5:BasicObject = LoadField v2, :_c@0x1002 - v6:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4, v5, v6) + Jump bb3(v1, v3, v4, v5) bb2(): EntryPoint JIT(0) v9:BasicObject = LoadArg :self@0 @@ -16538,8 +16549,9 @@ mod hir_opt_tests { StoreField v11, :_c@0x1003, v15 v17:NilClass = Const Value(nil) StoreField v11, :formatted@0x1004, v17 - Jump bb3(v9, v10, v13, v15, v17) - bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject, v24:NilClass): + Jump bb3(v9, v10, v13, v15) + bb3(v20:BasicObject, v21:BasicObject, v22:BasicObject, v23:BasicObject): + v82:NilClass = Const Value(nil) SetLocal :formatted, l0, EP@3, v21 PatchPoint SingleRactorMode v47:HeapBasicObject = GuardType v20, HeapBasicObject @@ -17784,8 +17796,7 @@ mod hir_opt_tests { v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :blk@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:HeapBasicObject = LoadArg :self@0 @@ -17794,8 +17805,9 @@ mod hir_opt_tests { StoreField v9, :blk@0x1001, v8 v11:NilClass = Const Value(nil) StoreField v9, :other_block@0x1002, v11 - Jump bb3(v7, v8, v11) - bb3(v14:HeapBasicObject, v15:BasicObject, v16:NilClass): + Jump bb3(v7, v8) + bb3(v14:HeapBasicObject, v15:BasicObject): + v44:NilClass = Const Value(nil) PatchPoint NoSingletonClass(B@0x1008) PatchPoint MethodRedefined(B@0x1008, proc@0x1010, cme:0x1018) v42:ObjectSubclass[class_exact:B] = GuardType v14, ObjectSubclass[class_exact:B] recompile @@ -18482,17 +18494,16 @@ mod hir_opt_tests { v3:BasicObject = LoadField v2, :list@0x1000 v4:BasicObject = LoadField v2, :sep@0x1001 v5:BasicObject = LoadField v2, :iter_method@0x1002 - v6:NilClass = Const Value(nil) v7:CPtr = LoadPC v8:CPtr[CPtr(0x1003)] = Const CPtr(0x1003) v9:CBool = IsBitEqual v7, v8 - CondBranch v9, bb3(v1, v3, v4, v5, v6), bb9() + CondBranch v9, bb3(v1, v3, v4, v5), bb9() bb9(): v11:CPtr[CPtr(0x1004)] = Const CPtr(0x1004) v12:CBool = IsBitEqual v7, v11 - CondBranch v12, bb5(v1, v3, v4, v5, v6), bb10() + CondBranch v12, bb5(v1, v3, v4, v5), bb10() bb10(): - Jump bb7(v1, v3, v4, v5, v6) + Jump bb7(v1, v3, v4, v5) bb2(): EntryPoint JIT(0) v16:BasicObject = LoadArg :self@0 @@ -18505,11 +18516,12 @@ mod hir_opt_tests { StoreField v18, :iter_method@0x1005, v22 v24:NilClass = Const Value(nil) StoreField v18, :kwsplat@0x1006, v24 - Jump bb3(v16, v17, v20, v22, v24) - bb3(v51:BasicObject, v52:BasicObject, v53:BasicObject, v54:BasicObject, v55:NilClass): + Jump bb3(v16, v17, v20, v22) + bb3(v51:BasicObject, v52:BasicObject, v53:BasicObject, v54:BasicObject): + v132:NilClass = Const Value(nil) v58:NilClass = Const Value(nil) SetLocal :sep, l0, EP@5, v58 - Jump bb5(v51, v52, v58, v54, v55) + Jump bb5(v51, v52, v58, v54) bb4(): EntryPoint JIT(1) v28:BasicObject = LoadArg :self@0 @@ -18522,11 +18534,12 @@ mod hir_opt_tests { StoreField v30, :iter_method@0x1005, v34 v36:NilClass = Const Value(nil) StoreField v30, :kwsplat@0x1006, v36 - Jump bb5(v28, v29, v32, v34, v36) - bb5(v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject, v66:NilClass): + Jump bb5(v28, v29, v32, v34) + bb5(v62:BasicObject, v63:BasicObject, v64:BasicObject, v65:BasicObject): + v133:NilClass = Const Value(nil) v69:StaticSymbol[:each] = Const Value(VALUE(0x1008)) SetLocal :iter_method, l0, EP@4, v69 - Jump bb7(v62, v63, v64, v69, v66) + Jump bb7(v62, v63, v64, v69) bb6(): EntryPoint JIT(2) v40:BasicObject = LoadArg :self@0 @@ -18539,11 +18552,12 @@ mod hir_opt_tests { StoreField v42, :iter_method@0x1005, v46 v48:NilClass = Const Value(nil) StoreField v42, :kwsplat@0x1006, v48 - Jump bb7(v40, v41, v44, v46, v48) - bb7(v73:BasicObject, v74:BasicObject, v75:BasicObject, v76:BasicObject, v77:NilClass): + Jump bb7(v40, v41, v44, v46) + bb7(v73:BasicObject, v74:BasicObject, v75:BasicObject, v76:BasicObject): + v134:NilClass = Const Value(nil) v82:CBool = Test v75 v83:Truthy = RefineType v75, Truthy - CondBranch v82, bb8(v74, v83, v76, v77), bb11() + CondBranch v82, bb8(v74, v83, v76, v134), bb11() bb11(): v85:Falsy = RefineType v75, Falsy PatchPoint MethodRedefined(Object@0x1010, lambda@0x1018, cme:0x1020) @@ -18583,14 +18597,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v99:NilClass = Const Value(nil) v13:NilClass = Const Value(nil) v15:TrueClass|NilClass = Defined yield, v13 v17:CBool = Test v15 @@ -18757,14 +18770,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v45:NilClass = Const Value(nil) v13:Fixnum[1] = Const Value(1) PatchPoint SingleRactorMode v19:HeapBasicObject = GuardType v8, HeapBasicObject @@ -18802,16 +18814,14 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - v3:NilClass = Const Value(nil) - Jump bb3(v1, v2, v3) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v6:BasicObject = LoadArg :self@0 - v7:NilClass = Const Value(nil) - v8:NilClass = Const Value(nil) - Jump bb3(v6, v7, v8) - bb3(v10:BasicObject, v11:NilClass, v12:NilClass): + Jump bb3(v6) + bb3(v10:BasicObject): + v64:NilClass = Const Value(nil) + v63:NilClass = Const Value(nil) v16:Fixnum[1] = Const Value(1) PatchPoint SingleRactorMode v22:HeapBasicObject = GuardType v10, HeapBasicObject @@ -18824,7 +18834,7 @@ mod hir_opt_tests { v32:Fixnum[5] = Const Value(5) PatchPoint NoEPEscape(initialize) PatchPoint MethodRedefined(Integer@0x1008, +@0x1010, cme:0x1018) - v63:Fixnum[6] = Const Value(6) + v65:Fixnum[6] = Const Value(6) PatchPoint SingleRactorMode WriteBarrier v22, v16 CheckInterrupts @@ -18851,14 +18861,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v57:NilClass = Const Value(nil) v13:Fixnum[1] = Const Value(1) PatchPoint SingleRactorMode v19:HeapBasicObject = GuardType v8, HeapBasicObject @@ -19322,14 +19331,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:HeapBasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:HeapBasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:HeapBasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:HeapBasicObject): + v99:NilClass = Const Value(nil) v13:Fixnum[0] = Const Value(0) Jump bb6(v8, v13) bb6(v18:HeapBasicObject, v19:Fixnum): @@ -19960,15 +19968,14 @@ mod hir_opt_tests { v1:BasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :obj@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:BasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :obj@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:BasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v226:NilClass = Const Value(nil) v17:Fixnum[0] = Const Value(0) PatchPoint NoSingletonClass(C@0x1008) PatchPoint MethodRedefined(C@0x1008, var@0x1010, cme:0x1018) @@ -20137,15 +20144,14 @@ mod hir_opt_tests { v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :x@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:HeapBasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :x@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:HeapBasicObject, v12:BasicObject): + v74:NilClass = Const Value(nil) v17:Fixnum[1] = Const Value(1) PatchPoint SingleRactorMode v21:CShape = LoadField v11, :shape_id@0x1001 @@ -20197,15 +20203,14 @@ mod hir_opt_tests { v1:HeapBasicObject = LoadSelf v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :x@0x1000 - v4:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4) + Jump bb3(v1, v3) bb2(): EntryPoint JIT(0) v7:HeapBasicObject = LoadArg :self@0 v8:BasicObject = LoadArg :x@1 - v9:NilClass = Const Value(nil) - Jump bb3(v7, v8, v9) - bb3(v11:HeapBasicObject, v12:BasicObject, v13:NilClass): + Jump bb3(v7, v8) + bb3(v11:HeapBasicObject, v12:BasicObject): + v90:NilClass = Const Value(nil) v17:Fixnum[1] = Const Value(1) PatchPoint SingleRactorMode v21:CShape = LoadField v11, :shape_id@0x1001 @@ -21734,10 +21739,10 @@ mod hir_opt_tests { PatchPoint NoEPEscape(==) PatchPoint MethodRedefined(Point@0x1008, x@0x10f8, cme:0x1100) PatchPoint MethodRedefined(Integer@0x1128, ==@0x10a0, cme:0x1130) - v253:Fixnum = GuardType v199, Fixnum recompile - v255:BoolExact = FixnumEq v253, v49 - v210:CBool = Test v255 - v211:FalseClass = RefineType v255, Falsy + v255:Fixnum = GuardType v199, Fixnum recompile + v257:BoolExact = FixnumEq v255, v49 + v210:CBool = Test v257 + v211:FalseClass = RefineType v257, Falsy CondBranch v210, bb19(), bb18(v211) bb19(): PatchPoint SingleRactorMode @@ -21747,14 +21752,14 @@ mod hir_opt_tests { PatchPoint NoEPEscape(==) PatchPoint NoSingletonClass(Point@0x1008) PatchPoint MethodRedefined(Point@0x1008, y@0x1158, cme:0x1160) - v260:CShape = LoadField v97, :shape_id@0x1090 - v261:CShape[0x1095] = GuardBitEquals v260, CShape(0x1095) recompile - v262:BasicObject = LoadField v97, :@y@0x1094 + v262:CShape = LoadField v97, :shape_id@0x1090 + v263:CShape[0x1095] = GuardBitEquals v262, CShape(0x1095) recompile + v264:BasicObject = LoadField v97, :@y@0x1094 PatchPoint MethodRedefined(Integer@0x1128, ==@0x10a0, cme:0x1130) - v265:Fixnum = GuardType v220, Fixnum recompile - v266:Fixnum = GuardType v262, Fixnum - v267:BoolExact = FixnumEq v265, v266 - Jump bb18(v267) + v267:Fixnum = GuardType v220, Fixnum recompile + v268:Fixnum = GuardType v264, Fixnum + v269:BoolExact = FixnumEq v267, v268 + Jump bb18(v269) bb18(v232:BoolExact): CheckInterrupts PopInlineFrame @@ -21814,16 +21819,15 @@ mod hir_opt_tests { v2:CPtr = LoadSP v3:BasicObject = LoadField v2, :obj@0x1000 v4:BasicObject = LoadField v2, :flag@0x1001 - v5:NilClass = Const Value(nil) - Jump bb3(v1, v3, v4, v5) + Jump bb3(v1, v3, v4) bb2(): EntryPoint JIT(0) v8:BasicObject = LoadArg :self@0 v9:BasicObject = LoadArg :obj@1 v10:BasicObject = LoadArg :flag@2 - v11:NilClass = Const Value(nil) - Jump bb3(v8, v9, v10, v11) - bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject, v16:NilClass): + Jump bb3(v8, v9, v10) + bb3(v13:BasicObject, v14:BasicObject, v15:BasicObject): + v72:NilClass = Const Value(nil) v21:Fixnum[1] = Const Value(1) v23:Fixnum[2] = Const Value(2) v25:Fixnum[3] = Const Value(3) @@ -21932,14 +21936,13 @@ mod hir_opt_tests { bb1(): EntryPoint interpreter v1:BasicObject = LoadSelf - v2:NilClass = Const Value(nil) - Jump bb3(v1, v2) + Jump bb3(v1) bb2(): EntryPoint JIT(0) v5:BasicObject = LoadArg :self@0 - v6:NilClass = Const Value(nil) - Jump bb3(v5, v6) - bb3(v8:BasicObject, v9:NilClass): + Jump bb3(v5) + bb3(v8:BasicObject): + v63:NilClass = Const Value(nil) v13:Fixnum[0] = Const Value(0) Jump bb5(v13) bb5(v19:Fixnum): @@ -21959,4 +21962,48 @@ mod hir_opt_tests { Return v19 "); } + + #[test] + fn test_same_constant_does_not_create_block_param() { + eval(r#" + def test(cond) + if cond + x = 1 + else + x = 1 + end + x + end + + test(true) + "#); + assert_snapshot!(hir_string("test"), @" + fn test@:3: + bb1(): + EntryPoint interpreter + v1:BasicObject = LoadSelf + v2:CPtr = LoadSP + v3:BasicObject = LoadField v2, :cond@0x1000 + Jump bb3(v1, v3) + bb2(): + EntryPoint JIT(0) + v7:BasicObject = LoadArg :self@0 + v8:BasicObject = LoadArg :cond@1 + Jump bb3(v7, v8) + bb3(v11:BasicObject, v12:BasicObject): + v48:NilClass = Const Value(nil) + v18:CBool = Test v12 + v19:Falsy = RefineType v12, Falsy + CondBranch v18, bb6(), bb4() + bb6(): + v21:Truthy = RefineType v12, Truthy + Jump bb5(v21) + bb4(): + Jump bb5(v19) + bb5(v38:BasicObject): + v49:Fixnum[1] = Const Value(1) + CheckInterrupts + Return v49 + "); + } } diff --git a/zjit/src/hir_type/mod.rs b/zjit/src/hir_type/mod.rs index 9a44575042a44d..1a886070e30f4e 100644 --- a/zjit/src/hir_type/mod.rs +++ b/zjit/src/hir_type/mod.rs @@ -436,6 +436,13 @@ impl Type { /// Return the object specialization, if any. pub fn ruby_object(&self) -> Option { + // We ask not for the type, but for a specific value associated with this Type. If the Type + // is Empty, it will be a subtype of every other Type, but it will never have any value. + // Therefore, special-case Empty. + if self.is_subtype(types::Empty) { return None; } + if self.is_subtype(types::NilClass) { return Some(Qnil); } + if self.is_subtype(types::TrueClass) { return Some(Qtrue); } + if self.is_subtype(types::FalseClass) { return Some(Qfalse); } match self.spec() { Specialization::Object(val) => Some(val), _ => None, @@ -595,6 +602,10 @@ impl Type { if let Some(val) = self.exact_ruby_class() { return Some(val); } + // As in `ruby_object`, we ask not for the type but for a property of the values it + // describes. Empty is a subtype of every Type, so the scan below would report the first + // entry's class, but Empty describes no value and therefore no run-time class. + if self.is_subtype(types::Empty) { return None; } types::ExactBitsAndClass .iter() .find(|&(bits, _)| self.is_subtype(Type::from_bits(*bits))) @@ -843,13 +854,25 @@ mod tests { } #[test] - fn singletons_do_not_have_ruby_object() { - assert_eq!(Type::from_value(Qnil).ruby_object(), None); - assert_eq!(types::NilClass.ruby_object(), None); - assert_eq!(Type::from_value(Qtrue).ruby_object(), None); - assert_eq!(types::TrueClass.ruby_object(), None); - assert_eq!(Type::from_value(Qfalse).ruby_object(), None); - assert_eq!(types::FalseClass.ruby_object(), None); + fn singletons_have_ruby_object() { + assert_eq!(Type::from_value(Qnil).ruby_object(), Some(Qnil)); + assert_eq!(types::NilClass.ruby_object(), Some(Qnil)); + assert_eq!(Type::from_value(Qtrue).ruby_object(), Some(Qtrue)); + assert_eq!(types::TrueClass.ruby_object(), Some(Qtrue)); + assert_eq!(Type::from_value(Qfalse).ruby_object(), Some(Qfalse)); + assert_eq!(types::FalseClass.ruby_object(), Some(Qfalse)); + } + + #[test] + fn empty_has_no_ruby_object() { + // Empty is a subtype of every type, but has no value. + assert_eq!(types::Empty.ruby_object(), None); + assert_eq!(types::Empty.fixnum_value(), None); + assert_eq!(types::Empty.runtime_exact_ruby_class(), None); + assert_eq!(types::Empty.cint64_value(), None); + assert_eq!(types::Empty.exact_ruby_class(), None); + assert_eq!(types::Empty.inexact_ruby_class(), None); + assert_eq!(types::Empty.builtin_type_equivalent(), None); } #[test] From 38a746771400147cacb79978fbbed4a194b68664 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 26 Aug 2026 14:30:39 +0900 Subject: [PATCH 18/19] [ruby/rubygems] Error out when `bundle init --gemspec` gets no specification `Bundler.load_gemspec_uncached` returns nil when the gemspec evaluates to nil, which an empty file does. `CLI::Init` was the only caller not guarding against that, so it crashed with a `NoMethodError` backtrace after already writing a partial Gemfile. https://github.com/ruby/rubygems/commit/fc20938c4f Co-Authored-By: Claude Opus 5 --- lib/bundler/cli/init.rb | 4 ++++ spec/bundler/commands/init_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/bundler/cli/init.rb b/lib/bundler/cli/init.rb index 246b9d64604460..10a56866eaf896 100644 --- a/lib/bundler/cli/init.rb +++ b/lib/bundler/cli/init.rb @@ -26,6 +26,10 @@ def run end spec = Bundler.load_gemspec_uncached(gemspec) + unless spec + Bundler.ui.error "Gem specification #{gemspec} did not produce a specification" + exit 1 + end File.open(gemfile, "wb") do |file| file << "# Generated from #{gemspec}\n" diff --git a/spec/bundler/commands/init_spec.rb b/spec/bundler/commands/init_spec.rb index 989d6fa812f81f..7314a2b1e63587 100644 --- a/spec/bundler/commands/init_spec.rb +++ b/spec/bundler/commands/init_spec.rb @@ -116,6 +116,16 @@ expect(err).to include("There was an error while loading `test.gemspec`") end end + + context "when gemspec file does not define a specification" do + it "notifies the user that no specification was produced" do + FileUtils.touch(spec_file) + + bundle :init, gemspec: spec_file, raise_on_error: false + expect(err).to include("Gem specification #{spec_file} did not produce a specification") + expect(bundled_app_gemfile).not_to exist + end + end end context "when init_gems_rb setting is enabled" do From b4322d90201dcedea39670d77bf3bbb697b872bc Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 27 Aug 2026 12:36:47 +0900 Subject: [PATCH 19/19] Fix reference update bug in iseq_scan_bits In iseq_scan_bits, we only update original_iseq if the element at code moved. However, this isn't correct as it's possible that code has already gotten updated but not original_iseq. This will cause original_iseq to not get updated. --- iseq.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/iseq.c b/iseq.c index 74a6d9bc7e9b71..a88a5745f72635 100644 --- a/iseq.c +++ b/iseq.c @@ -300,11 +300,16 @@ iseq_scan_bits(unsigned int page, iseq_bits_t bits, VALUE *code, VALUE *original while (bits) { offset = ntz_intptr(bits); - VALUE op = code[page_offset + offset]; - rb_gc_mark_and_move(&code[page_offset + offset]); - VALUE newop = code[page_offset + offset]; - if (original_iseq && newop != op) { - original_iseq[page_offset + offset] = newop; + if (original_iseq) { + VALUE op = original_iseq[page_offset + offset]; + rb_gc_mark_and_move(&code[page_offset + offset]); + VALUE newop = code[page_offset + offset]; + if (op != newop) { + original_iseq[page_offset + offset] = newop; + } + } + else { + rb_gc_mark_and_move(&code[page_offset + offset]); } bits &= bits - 1; // Reset Lowest Set Bit (BLSR) }