From f0f07b032e234aa635ba5fe52d86ae4d45e4185f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 17 Aug 2026 15:17:25 +0900 Subject: [PATCH 1/7] [ruby/rubygems] Filter git command output against the configured URI Credentials coming from `bundle config` are injected into the remote URI only in `configured_uri`, so filtering command strings and git output against the original URI never matched and left them visible in `GitCommandError` messages. Memoize `configured_uri` since it is now consulted on every git invocation. https://github.com/ruby/rubygems/commit/b8a6244189 Co-Authored-By: Claude Opus 5 --- lib/bundler/source/git/git_proxy.rb | 11 ++-- .../bundler/source/git/git_proxy_spec.rb | 62 +++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 78ab747215ca7d..3df37e487bdb91 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -364,9 +364,10 @@ def verify(reference) git("rev-parse", "--verify", reference, dir: path).strip end - # Adds credentials to the URI + # Adds credentials to the URI. This is the URI given to git, so it's + # also the one command output must be filtered against. def configured_uri - if /https?:/.match?(uri) + @configured_uri ||= if /https?:/.match?(uri) remote = Gem::URI(uri) config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] remote.userinfo ||= config_auth @@ -409,7 +410,7 @@ def redact_and_check_presence(command) raise GitNotInstalledError.new unless Bundler.git_present? require "shellwords" - URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", uri) + URICredentialsFilter.credential_filtered_string("git #{command.shelljoin}", configured_uri) end def run_command(*command, dir: nil) @@ -429,10 +430,10 @@ def capture(cmd, dir, ignore_err: false) require "open3" out, err, status = Open3.capture3(*capture3_args_for(cmd, dir)) - filtered_out = URICredentialsFilter.credential_filtered_string(out, uri) + filtered_out = URICredentialsFilter.credential_filtered_string(out, configured_uri) return [filtered_out, status] if ignore_err - filtered_err = URICredentialsFilter.credential_filtered_string(err, uri) + filtered_err = URICredentialsFilter.credential_filtered_string(err, configured_uri) [filtered_out, filtered_err, status] end end diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb index e2d3bbb6e7efaf..9633c910171043 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -98,6 +98,68 @@ end end + describe "filtering credentials out of command output" do + let(:secret) { "s3cr3tp4ss" } + let(:credentialed_uri) { "https://user:#{secret}@github.com/ruby/rubygems.git" } + let(:redacted_uri) { "https://user@github.com/ruby/rubygems.git" } + + before do + allow(Open3).to receive(:capture3).and_return(["", "fatal: repository '#{credentialed_uri}' not found", fail_result]) + allow(Open3).to receive(:capture3).with("git", "--version").and_return(["git version 2.14.0", "", clone_result]) + end + + it "redacts credentials configured for the host from failed commands" do + Bundler.settings.temporary("github.com" => "user:#{secret}") do + expect { git_proxy.checkout }.to raise_error(Bundler::Source::Git::GitCommandError) do |error| + expect(error.message).not_to include(secret) + expect(error.message).to include(redacted_uri) + end + end + end + + it "redacts credentials configured for the full URI from failed commands" do + Bundler.settings.temporary(uri => "user:#{secret}") do + expect { git_proxy.checkout }.to raise_error(Bundler::Source::Git::GitCommandError) do |error| + expect(error.message).not_to include(secret) + expect(error.message).to include(redacted_uri) + end + end + end + + it "redacts configured credentials from stdout and stderr" do + Bundler.settings.temporary("github.com" => "user:#{secret}") do + allow(Open3).to receive(:capture3).and_return(["cloning #{credentialed_uri}", "error: #{credentialed_uri}", fail_result]) + + out, err, = git_proxy.send(:capture, ["fetch"], nil) + + expect(out).to eq("cloning #{redacted_uri}") + expect(err).to eq("error: #{redacted_uri}") + end + end + + context "when the URI itself embeds credentials" do + let(:uri) { credentialed_uri } + + it "redacts them from failed commands" do + expect { git_proxy.checkout }.to raise_error(Bundler::Source::Git::GitCommandError) do |error| + expect(error.message).not_to include(secret) + expect(error.message).to include(redacted_uri) + end + end + end + + context "when no credentials are involved" do + it "leaves output untouched" do + allow(Open3).to receive(:capture3).and_return(["cloning #{uri}", "error: #{uri}", fail_result]) + + out, err, = git_proxy.send(:capture, ["fetch"], nil) + + expect(out).to eq("cloning #{uri}") + expect(err).to eq("error: #{uri}") + end + end + end + describe "#copy_to" do let(:revision) { "abc123" } let(:destination) { tmp("git-proxy-copy") } From 54de4df94bc15360a8b17a374cee6bf78365d6dd Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Mon, 17 Aug 2026 12:54:55 -0400 Subject: [PATCH 2/7] ZJIT: fastpath allocation for gen_new_array (embedded) Previously, only empty arrays were taking the gen_new_array fastpath. Now, if an array can be embedded it will take the fastpath. --- array.c | 30 ++++++++++--------- zjit.h | 1 + zjit/bindgen/src/main.rs | 2 +- zjit/src/codegen.rs | 55 +++++++++++++++++++++++----------- zjit/src/codegen_tests.rs | 39 ++++++++++++++++++++++++ zjit/src/cruby_bindings.inc.rs | 6 +++- 6 files changed, 100 insertions(+), 33 deletions(-) diff --git a/array.c b/array.c index 50f4b3ead89b58..a96c5a0b9727b0 100644 --- a/array.c +++ b/array.c @@ -2937,27 +2937,29 @@ rb_ary_resurrect(VALUE ary) #if USE_ZJIT bool -rb_zjit_array_dup_can_fastpath(VALUE ary, size_t *alloc_size_out, VALUE *flags_out, long *len_out) +rb_zjit_array_new_can_fastpath(long len, size_t *alloc_size_out, VALUE *flags_out) { - long len = RARRAY_LEN(ary); - long embed_capa = (sizeof(struct RArray) - offsetof(struct RArray, as.ary)) / sizeof(VALUE); - - if (len > embed_capa) return false; + if (!ary_embeddable_p(len)) { + return false; + } + long embed_size = ary_embed_size(len); - *alloc_size_out = sizeof(struct RArray); + *alloc_size_out = embed_size; *flags_out = T_ARRAY | RARRAY_EMBED_FLAG | ((VALUE)len << RARRAY_EMBED_LEN_SHIFT); - *len_out = len; return true; } -void -rb_zjit_array_new_fastpath(size_t *alloc_size_out, VALUE *flags_out) +bool +rb_zjit_array_dup_can_fastpath(VALUE ary, size_t *alloc_size_out, VALUE *flags_out, long *len_out) { - size_t size = sizeof(struct RArray); - shape_id_t shape_id = rb_shape_transition_slot_size(ROOT_SHAPE_ID | SHAPE_ID_LAYOUT_OTHER, - rb_gc_size_slot_size(size)); - *alloc_size_out = size; - *flags_out = T_ARRAY | RARRAY_EMBED_FLAG | ((VALUE)shape_id << SHAPE_FLAG_SHIFT); + long len = RARRAY_LEN(ary); + if (!rb_zjit_array_new_can_fastpath(len, alloc_size_out, flags_out)) { + return false; + } + else { + *len_out = len; + return true; + } } #endif diff --git a/zjit.h b/zjit.h index f7928989c5ec18..8757c4d386f01a 100644 --- a/zjit.h +++ b/zjit.h @@ -137,6 +137,7 @@ VALUE rb_zjit_new_obj_shape(VALUE flags, size_t alloc_size); bool rb_zjit_class_allocate_instance_fastpath(VALUE klass, size_t *size_out, VALUE *flags_out); bool rb_zjit_str_resurrect_fastpath(VALUE str, bool chilled, size_t *size_out, VALUE *flags_out, long *len_out, size_t *byte_size_out); bool rb_zjit_array_dup_can_fastpath(VALUE ary, size_t *alloc_size_out, VALUE *flags_out, long *len_out); +bool rb_zjit_array_new_can_fastpath(long len, size_t *alloc_size_out, VALUE *flags_out); bool rb_zjit_hash_dup_can_fastpath(VALUE hash, size_t *alloc_size_out, VALUE *flags_out, VALUE *ifnone_out, long *bound_out); void rb_zjit_range_new_fastpath(bool exclude_end, size_t *alloc_size_out, VALUE *flags_out); void rb_zjit_array_new_fastpath(size_t *alloc_size_out, VALUE *flags_out); diff --git a/zjit/bindgen/src/main.rs b/zjit/bindgen/src/main.rs index 782df9f2dd7812..30e0aa5578aa10 100644 --- a/zjit/bindgen/src/main.rs +++ b/zjit/bindgen/src/main.rs @@ -117,9 +117,9 @@ fn main() { .allowlist_function("rb_zjit_class_allocate_instance_fastpath") .allowlist_function("rb_zjit_str_resurrect_fastpath") .allowlist_function("rb_zjit_array_dup_can_fastpath") + .allowlist_function("rb_zjit_array_new_can_fastpath") .allowlist_function("rb_zjit_hash_dup_can_fastpath") .allowlist_function("rb_zjit_range_new_fastpath") - .allowlist_function("rb_zjit_array_new_fastpath") // For crashing .allowlist_function("rb_bug") diff --git a/zjit/src/codegen.rs b/zjit/src/codegen.rs index 77086268f2775e..9f4ea3050ab65b 100644 --- a/zjit/src/codegen.rs +++ b/zjit/src/codegen.rs @@ -2225,23 +2225,40 @@ fn gen_new_array( elements: Vec, state: &FrameState, ) -> lir::Opnd { - gen_prepare_leaf_call_with_gc(asm, state); - let num: c_long = elements.len().try_into().expect("Unable to fit length of elements into c_long"); - if !elements.is_empty() { - let argv = gen_push_opnds(jit, asm, &elements); - return asm_ccall!(asm, rb_ec_ary_new_from_values, EC, num.into(), argv); - } - let mut alloc_size: usize = 0; - let mut flags: VALUE = VALUE(0); - unsafe { rb_zjit_array_new_fastpath(&mut alloc_size, &mut flags) }; - let klass = unsafe { rb_cArray }; + let mut flags = VALUE(0); + let mut argv = Opnd::UImm(0); + // NOTE: we can't use gen_push_opnds in slow path because jit var can't be borrowed properly + if elements.len() > 0 { + argv = asm.alloc_stack(jit, elements.len()); + } + // When the new array would be embedded, bump-allocate it inline and initialize the elements + // directly. The fresh object is young and white, so those writes need no write barriers. + if unsafe { rb_zjit_array_new_can_fastpath(num, &mut alloc_size, &mut flags) } { + let klass = unsafe { rb_cArray }; + return gc_fastpath::gc_fastpath_new_obj(jit, asm, function, state, alloc_size, flags.into(), klass, + |asm, ary| { + for (i, &elem) in elements.iter().enumerate() { + let offset = RUBY_OFFSET_RARRAY_AS_ARY + (i as i32) * SIZEOF_VALUE_I32; + asm.store(Opnd::mem(VALUE_BITS, ary, offset), elem); + } + }, + |asm| { + gen_prepare_leaf_call_with_gc(asm, state); + if elements.len() > 0 { + gen_write_operands(asm, &elements, argv); + } + asm_ccall!(asm, rb_ec_ary_new_from_values, EC, num.into(), argv) + }); + } - gc_fastpath::gc_fastpath_new_obj(jit, asm, function, state, alloc_size, flags.into(), klass, |_asm, _obj| {}, |asm| { - asm_ccall!(asm, rb_ec_ary_new_from_values, EC, 0i64.into(), Opnd::UImm(0)) - }) + gen_prepare_leaf_call_with_gc(asm, state); + if elements.len() > 0 { + gen_write_operands(asm, &elements, argv); + } + asm_ccall!(asm, rb_ec_ary_new_from_values, EC, num.into(), argv) } /// Adjust potentially-negative index by the given length, returning the adjusted index. If still negative, @@ -4084,14 +4101,18 @@ fn gen_push_opnds(jit: &JITState, asm: &mut Assembler, opnds: &[Opnd]) -> lir::O Opnd::UImm(0) }; - // Write operands into stack slots allocated by asm.alloc_stack() - for (idx, &opnd) in opnds.iter().enumerate() { - asm.mov(Opnd::mem(VALUE_BITS, argv, idx as i32 * SIZEOF_VALUE_I32), opnd); - } + gen_write_operands(asm, opnds, argv); argv } +/// Write operands into stack slots previously reserved by asm.alloc_stack(). +fn gen_write_operands(asm: &mut Assembler, opnds: &[Opnd], stack: lir::Opnd) { + for (idx, &opnd) in opnds.iter().enumerate() { + asm.mov(Opnd::mem(VALUE_BITS, stack, idx as i32 * SIZEOF_VALUE_I32), opnd); + } +} + fn gen_toregexp(jit: &mut JITState, asm: &mut Assembler, function: &Function, opt: usize, values: Vec, state: &FrameState) -> Opnd { gen_prepare_non_leaf_call(jit, asm, function, state); diff --git a/zjit/src/codegen_tests.rs b/zjit/src/codegen_tests.rs index 669bf900b78e57..0ae3b270c23d02 100644 --- a/zjit/src/codegen_tests.rs +++ b/zjit/src/codegen_tests.rs @@ -4250,6 +4250,45 @@ fn test_new_array_order() { "), @"[3, 2, 1]"); } +#[test] +fn test_new_array_embedded_gc_stress() { + eval(r#" + def make(a) = [a, a, a] + "#); + assert_contains_opcode("make", YARVINSN_newarray); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + s = "x" + make(s) + a = make(s) + a << :extra + [a.frozen?, a.class, a] + ensure + GC.stress = false + end + "#), @r#"[false, Array, ["x", "x", "x", :extra]]"#); +} + +#[test] +fn test_new_array_embedded_memcpy_gc_stress() { + eval(r#" + def make(a) = [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a] # size: 17 + "#); + assert_contains_opcode("make", YARVINSN_newarray); + assert_snapshot!(assert_compiles(r#" + begin + GC.stress = true + s = "y" + make(s) + m = make(s) + [m.frozen?, m.length, m.class] + ensure + GC.stress = false + end + "#), @r#"[false, 17, Array]"#); +} + #[test] fn test_array_dup() { assert_snapshot!(inspect(" diff --git a/zjit/src/cruby_bindings.inc.rs b/zjit/src/cruby_bindings.inc.rs index 3513f5dabe051d..a7087f8cf5a6a6 100644 --- a/zjit/src/cruby_bindings.inc.rs +++ b/zjit/src/cruby_bindings.inc.rs @@ -2304,6 +2304,11 @@ unsafe extern "C" { flags_out: *mut VALUE, len_out: *mut ::std::os::raw::c_long, ) -> bool; + pub fn rb_zjit_array_new_can_fastpath( + len: ::std::os::raw::c_long, + alloc_size_out: *mut usize, + flags_out: *mut VALUE, + ) -> bool; pub fn rb_zjit_hash_dup_can_fastpath( hash: VALUE, alloc_size_out: *mut usize, @@ -2316,7 +2321,6 @@ unsafe extern "C" { alloc_size_out: *mut usize, flags_out: *mut VALUE, ); - pub fn rb_zjit_array_new_fastpath(alloc_size_out: *mut usize, flags_out: *mut VALUE); pub fn rb_profile_frames( start: ::std::os::raw::c_int, limit: ::std::os::raw::c_int, From 09addc1d504b0020ee4d1ec30ffff1cc41e08852 Mon Sep 17 00:00:00 2001 From: Jarek Prokop Date: Fri, 7 Aug 2026 17:44:08 +0200 Subject: [PATCH 3/7] [ruby/rubygems] Restrict UserDefined handling to explicit class set for Gem::SafeMarshal. A crafted payload using the UserDefined (u:) format for a permitted class such as Date, which normally serializes as UserMarshal (U:), can result in Date._load to invoke rb_marshal_load on attacker-controlled bytes, bypassing SafeMarshal's allowlists. HackerOne report 3915697, triaged as hardening. https://github.com/ruby/rubygems/commit/2fe0a5e5fa Co-Authored-By: Sonnet 4.6 (1M context) --- lib/rubygems/safe_marshal/visitors/to_ruby.rb | 9 ++++++++- test/rubygems/test_gem_safe_marshal.rb | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/safe_marshal/visitors/to_ruby.rb b/lib/rubygems/safe_marshal/visitors/to_ruby.rb index a1f94817760e43..3743b2972a9142 100644 --- a/lib/rubygems/safe_marshal/visitors/to_ruby.rb +++ b/lib/rubygems/safe_marshal/visitors/to_ruby.rb @@ -183,8 +183,15 @@ def visit_Gem_SafeMarshal_Elements_SymbolLink(o) @symbols.fetch(o.offset) end + SAFE_USER_DEFINED_CLASSES = [::Time, ::Gem::Specification].freeze + private_constant :SAFE_USER_DEFINED_CLASSES + def visit_Gem_SafeMarshal_Elements_UserDefined(o) - register_object(call_method(resolve_class(o.name), :_load, o.binary_string)) + klass = resolve_class(o.name) + unless SAFE_USER_DEFINED_CLASSES.include?(klass) + raise UnsupportedError.new("Unsupported user-defined class #{klass} in marshal stream", stack: formatted_stack) + end + register_object(call_method(klass, :_load, o.binary_string)) end def visit_Gem_SafeMarshal_Elements_UserMarshal(o) diff --git a/test/rubygems/test_gem_safe_marshal.rb b/test/rubygems/test_gem_safe_marshal.rb index 7e3a046c4ea97b..3d11c1cb9f6eeb 100644 --- a/test/rubygems/test_gem_safe_marshal.rb +++ b/test/rubygems/test_gem_safe_marshal.rb @@ -461,6 +461,16 @@ def test_negative_length end end + def test_date_user_defined_rejected + # Provide string as the inner payload, Date._load passes it raw to rb_marshal_load. + inner = Marshal.dump("exploit") + payload = "\x04\bu:\tDate" + (inner.bytesize + 5).chr + inner + e = assert_raise(Gem::SafeMarshal::Visitors::ToRuby::UnsupportedError) do + Gem::SafeMarshal.safe_load(payload) + end + assert_equal "Unsupported user-defined class Date in marshal stream @ root", e.message + end + def assert_safe_load_marshal(dumped, additional_methods: [], permitted_ivars: nil, equality: true, marshal_dump_equality: true, inspect: true, to_s: true) loaded = Marshal.load(dumped) From 167645b9c6674b824b6ea07f3da266c601ebeeac Mon Sep 17 00:00:00 2001 From: Florian Meyer Date: Sun, 9 Aug 2026 06:16:54 +0200 Subject: [PATCH 4/7] [ruby/rubygems] Correct Bundler default path version in config docs https://github.com/ruby/rubygems/commit/0808e3189d --- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-config.1.ronn | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 32291e048c1f90..c924dcc8998089 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -151,7 +151,7 @@ Cooldown filtering depends on the gem server providing a per\-version \fBcreated .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 -\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 4, and to the default system path (\fBGem\.dir\fR) before Bundler 4\. That means that before Bundler 4, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 4\. +\fBpath\fR (\fBBUNDLE_PATH\fR): The location on disk where all gems in your bundle will be located regardless of \fB$GEM_HOME\fR or \fB$GEM_PATH\fR values\. Bundle gems not found in this location will be installed by \fBbundle install\fR\. When not set, Bundler install by default to a \fB\.bundle\fR directory relative to repository root in Bundler 5, and to the default system path (\fBGem\.dir\fR) before Bundler 5\. That means that before Bundler 5, Bundler shares this location with Rubygems, and \fBgem install \|\.\|\.\|\.\fR will have gems installed in the same location and therefore, gems installed without \fBpath\fR set will show up by calling \fBgem list\fR\. This will not be the case in Bundler 5\. .IP "\(bu" 4 \fBpath\.system\fR (\fBBUNDLE_PATH__SYSTEM\fR): Whether Bundler will install gems into the default system path (\fBGem\.dir\fR)\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 6b52288783b5c7..7e25a0aa1972ea 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -253,12 +253,12 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). The location on disk where all gems in your bundle will be located regardless of `$GEM_HOME` or `$GEM_PATH` values. Bundle gems not found in this location will be installed by `bundle install`. When not set, Bundler install by - default to a `.bundle` directory relative to repository root in Bundler 4, - and to the default system path (`Gem.dir`) before Bundler 4. That means that - before Bundler 4, Bundler shares this location with Rubygems, and `gem + default to a `.bundle` directory relative to repository root in Bundler 5, + and to the default system path (`Gem.dir`) before Bundler 5. That means that + before Bundler 5, Bundler shares this location with Rubygems, and `gem install ...` will have gems installed in the same location and therefore, gems installed without `path` set will show up by calling `gem list`. This - will not be the case in Bundler 4. + will not be the case in Bundler 5. * `path.system` (`BUNDLE_PATH__SYSTEM`): Whether Bundler will install gems into the default system path (`Gem.dir`). * `plugins` (`BUNDLE_PLUGINS`): From a672a0b84ceb048751e60cbe2ae89414cd0c398f Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 17 Aug 2026 14:51:36 +0900 Subject: [PATCH 5/7] [ruby/rubygems] Validate the platform field in Gem::Installer#verify_spec spec.platform.to_s is interpolated into Gem::Specification#full_name, which Gem::Installer uses to build the gem and extension directories it removes and then extracts into, so a platform carrying path separators lands outside the gems directory. Neither entry point normalizes it: Gem::Platform is restored from gem metadata without going through #initialize, and its String parser keeps the cpu part verbatim. Replace the newline-only check with the same kind of allowed-character pattern already used for the gem name. https://github.com/ruby/rubygems/commit/7cde597e47 --- lib/rubygems/installer.rb | 2 +- test/rubygems/test_gem_installer.rb | 56 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index a6e1dc4730a617..2599c0ff6ec81b 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -697,7 +697,7 @@ def verify_spec raise Gem::InstallError, "#{spec} has an invalid extensions" end - if /\R/.match?(spec.platform.to_s) + unless /\A[\w.-]+\z/.match?(spec.platform.to_s) raise Gem::InstallError, "#{spec.platform} is an invalid platform" end diff --git a/test/rubygems/test_gem_installer.rb b/test/rubygems/test_gem_installer.rb index d94193e2205144..5bd5bf89f05dcb 100644 --- a/test/rubygems/test_gem_installer.rb +++ b/test/rubygems/test_gem_installer.rb @@ -2057,6 +2057,62 @@ def test_pre_install_checks_malicious_platform_before_eval end end + # Psych assigns @cpu/@os/@version directly, so a platform coming from gem + # metadata never goes through Gem::Platform#initialize. + def test_pre_install_checks_traversal_platform_from_array + spec = util_spec "a", "1" + def spec.validate(*args); end + spec.platform = Gem::Platform.new([nil, "../../../../tmp/X", nil]) + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "../../../../tmp/X is an invalid platform", e.message + end + end + + # Gem::Platform#initialize normalizes the os but keeps the cpu verbatim. + def test_pre_install_checks_traversal_platform_from_string + spec = util_spec "a", "1" + def spec.validate(*args); end + spec.platform = Gem::Platform.new("../../../../tmp/X-linux") + + installer = Gem::Installer.for_spec spec + installer.gem_home = @gemhome + + use_ui @ui do + e = assert_raise Gem::InstallError do + installer.pre_install_checks + end + assert_equal "../../../../tmp/X-linux is an invalid platform", e.message + end + end + + def test_pre_install_checks_accepts_real_platforms + %w[x86_64-linux arm64-darwin-23 x64-mingw-ucrt java ruby].each do |platform| + spec = util_spec "a", "1" do |s| + s.platform = platform + end + + util_build_gem spec + + installer = Gem::Installer.at spec.cache_file, + install_dir: @gemhome, + user_install: false, + force: true + + use_ui @ui do + assert_equal spec, installer.install, platform + end + + assert_path_exist File.join(@gemhome, "gems", spec.full_name), platform + end + end + def test_shebang load_relative "no" do installer = setup_base_installer From 98f0921f2b06bcc6435fe96a99da3a857ac10477 Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Wed, 12 Aug 2026 13:36:52 -0400 Subject: [PATCH 6/7] Small GC optimizations --- gc.c | 2 +- gc/default/default.c | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/gc.c b/gc.c index 241951082293cd..4db541967aeb18 100644 --- a/gc.c +++ b/gc.c @@ -4210,7 +4210,7 @@ rb_gc_obj_foreign_p(VALUE obj) bool rb_gc_single_objspace_p(void) { - if (!rb_gc_impl_multi_objspace_p()) return true; + if (!rb_gc_impl_multi_objspace_p() || ruby_single_main_ractor) return true; rb_vm_t *vm = GET_VM(); return vm->ractor.cnt == 1 && vm->gc.zombie_objspaces_count == 0 && gc_absorbing_zombie == 0 && !gc_absorbed_since_global_gc && diff --git a/gc/default/default.c b/gc/default/default.c index 349afb0030528c..cefcf9d508838c 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1677,7 +1677,7 @@ RVALUE_UNCOLLECTIBLE(rb_objspace_t *objspace, VALUE obj) #define RVALUE_PAGE_UNCOLLECTIBLE(page, obj) MARKED_IN_BITMAP((page)->uncollectible_bits, (obj)) #define RVALUE_PAGE_MARKING(page, obj) MARKED_IN_BITMAP((page)->marking_bits, (obj)) -static int rgengc_remember(rb_objspace_t *objspace, VALUE obj); +static void rgengc_remember(rb_objspace_t *objspace, VALUE obj); static void gc_bitmaps_clear(rb_objspace_t *objspace, rb_heap_t *heap, bool clear_shref); static void rgengc_rememberset_mark(rb_objspace_t *objspace, rb_heap_t *heap); static bool verify_pointer_in_any_heap_p(const void *ptr); /* cross-objspace ownership test */ @@ -5786,10 +5786,10 @@ gc_pin(rb_objspace_t *objspace, VALUE obj) { GC_ASSERT(!SPECIAL_CONST_P(obj)); - /* Never write a foreign page's pinned bit (a global GC may: everyone is stopped). */ - if (gc_skip_foreign_object_p(objspace, obj)) return; - if (RB_UNLIKELY(objspace->flags.during_compacting)) { + /* Never write a foreign page's pinned bit (a global GC may: everyone is stopped). */ + if (gc_skip_foreign_object_p(objspace, obj)) return; + if (RB_LIKELY(during_gc)) { if (!RVALUE_PINNED(objspace, obj)) { GC_ASSERT(GET_HEAP_PAGE(obj)->pinned_slots <= GET_HEAP_PAGE(obj)->total_slots); @@ -7553,7 +7553,7 @@ gc_report_body(int level, rb_objspace_t *objspace, const char *fmt, ...) /* bit operations */ -static int +static void rgengc_remembersetbits_set(rb_objspace_t *objspace, VALUE obj) { struct heap_page *page = GET_HEAP_PAGE(obj); @@ -7563,16 +7563,14 @@ rgengc_remembersetbits_set(rb_objspace_t *objspace, VALUE obj) * local a (under its Ractor's GVL) and a global GC writes from the driver alone. * Set the bit before the page flag so a page pending re-scan stays in * rememberset_mark. */ - const bool newly = !_MARKED_IN_BITMAP(bits, page, obj); _MARK_IN_BITMAP(bits, page, obj); page->flags.has_remembered_objects = TRUE; - return newly ? TRUE : FALSE; } /* wb, etc */ /* return FALSE if already remembered */ -static int +static void rgengc_remember(rb_objspace_t *objspace, VALUE obj) { gc_report(6, objspace, "rgengc_remember: %s %s\n", rb_obj_info(obj), @@ -7595,7 +7593,7 @@ rgengc_remember(rb_objspace_t *objspace, VALUE obj) } #endif /* RGENGC_PROFILE > 0 */ - return rgengc_remembersetbits_set(objspace, obj); + rgengc_remembersetbits_set(objspace, obj); } #ifndef PROFILE_REMEMBERSET_MARK @@ -7785,9 +7783,6 @@ rb_gc_impl_writebarrier(void *objspace_ptr, VALUE a, VALUE b) GC_ASSERT(RB_BUILTIN_TYPE(a) != T_NONE); GC_ASSERT(RB_BUILTIN_TYPE(a) != T_MOVED); GC_ASSERT(RB_BUILTIN_TYPE(a) != T_ZOMBIE); - GC_ASSERT(RB_BUILTIN_TYPE(b) != T_NONE); - GC_ASSERT(RB_BUILTIN_TYPE(b) != T_MOVED); - GC_ASSERT(RB_BUILTIN_TYPE(b) != T_ZOMBIE); /* A shareable object now references an unshareable one: record b as a shref so its * owner's local GC roots it (the parent may live in another objspace, untraversed @@ -7844,6 +7839,7 @@ rb_gc_impl_obj_became_shareable(void *objspace_ptr, VALUE obj) * the only writer, so a plain clear is enough. */ if (_MARKED_IN_BITMAP(page->shref_bits, page, obj)) { _CLEAR_IN_BITMAP(page->shref_bits, page, obj); + // NOTE: page->has_shref_objects could become stale here (value is true even though logically false) } } From 221534546daa315c1aaa2680116a7ebaff28f296 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 18 Aug 2026 08:19:11 +0900 Subject: [PATCH 7/7] [ruby/rubygems] Fix auto-clean default version in bundle config docs Auto-clean after install was deferred to Bundler 5 in https://github.com/ruby/rubygems/commit/c314d7b25156, and clean_after_install? checks bundler_5_mode?, but the man page still said Bundler 4. https://github.com/ruby/rubygems/commit/e7be51e53b Co-Authored-By: Claude Fable 5 --- lib/bundler/man/bundle-config.1 | 2 +- lib/bundler/man/bundle-config.1.ronn | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index c924dcc8998089..8cf56c9a130629 100644 --- a/lib/bundler/man/bundle-config.1 +++ b/lib/bundler/man/bundle-config.1 @@ -82,7 +82,7 @@ The following is a list of all configuration keys and their purpose\. You can le .IP "\(bu" 4 \fBcache_path\fR (\fBBUNDLE_CACHE_PATH\fR): The directory that bundler will place cached gems in when running \fBbundle package\fR, and that bundler will look in when installing gems\. Defaults to \fBvendor/cache\fR\. .IP "\(bu" 4 -\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 4, as long as \fBpath\fR is not explicitly configured\. +\fBclean\fR (\fBBUNDLE_CLEAN\fR): Whether Bundler should run \fBbundle clean\fR automatically after \fBbundle install\fR\. Defaults to \fBtrue\fR in Bundler 5, as long as \fBpath\fR is not explicitly configured\. .IP "\(bu" 4 \fBconsole\fR (\fBBUNDLE_CONSOLE\fR): The console that \fBbundle console\fR starts\. Defaults to \fBirb\fR\. .IP "\(bu" 4 diff --git a/lib/bundler/man/bundle-config.1.ronn b/lib/bundler/man/bundle-config.1.ronn index 7e25a0aa1972ea..27fd5e040b5abb 100644 --- a/lib/bundler/man/bundle-config.1.ronn +++ b/lib/bundler/man/bundle-config.1.ronn @@ -133,7 +133,7 @@ learn more about their operation in [bundle install(1)](bundle-install.1.html). Defaults to `vendor/cache`. * `clean` (`BUNDLE_CLEAN`): Whether Bundler should run `bundle clean` automatically after - `bundle install`. Defaults to `true` in Bundler 4, as long as `path` is not + `bundle install`. Defaults to `true` in Bundler 5, as long as `path` is not explicitly configured. * `console` (`BUNDLE_CONSOLE`): The console that `bundle console` starts. Defaults to `irb`.