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/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) } } diff --git a/lib/bundler/man/bundle-config.1 b/lib/bundler/man/bundle-config.1 index 32291e048c1f90..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 @@ -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..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`. @@ -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`): 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/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/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/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") } 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 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) 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,