From 60de70c55242293864092490f8a7c3f66546f896 Mon Sep 17 00:00:00 2001 From: Kenta Ishizaki Date: Mon, 30 Mar 2026 08:54:46 +0900 Subject: [PATCH 01/21] [ruby/rubygems] Improve error message when current platform is not in lockfile Fixes https://github.com/ruby/rubygems/pull/9413 https://github.com/ruby/rubygems/commit/02d2179047 --- lib/bundler/resolver.rb | 18 ++++++++++ .../install/gemfile/specific_platform_spec.rb | 35 +++++++++++++++++++ spec/bundler/install/gems/resolving_spec.rb | 4 ++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 1dbf565d4676e8..3c361d8ea51a9f 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -353,9 +353,27 @@ def raise_not_found!(package) message << "\n#{other_specs_matching_message(specs, matching_part)}" end + if specs_matching_requirement.any? && (hint = platform_mismatch_hint) + message << "\n\n#{hint}" + end + raise GemNotFound, message end + def platform_mismatch_hint + locked_platforms = Bundler.locked_gems&.platforms + return unless locked_platforms + + local_platform = Bundler.local_platform + return if locked_platforms.include?(local_platform) + return if locked_platforms.any? {|p| p == Gem::Platform::RUBY } + + "Your current platform (#{local_platform}) is not included in the lockfile's platforms (#{locked_platforms.join(", ")}). " \ + "Add the current platform to the lockfile with\n`bundle lock --add-platform #{local_platform}` and try again." + rescue GemfileNotFound + nil + end + def filtered_versions_for(package) @gem_version_promoter.filter_versions(package, @all_versions[package]) end diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb index 2c811a666d18a0..97b1d233bfaf39 100644 --- a/spec/bundler/install/gemfile/specific_platform_spec.rb +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -535,6 +535,41 @@ expect(err).to include(error_message).once end + it "shows a platform mismatch hint when the current platform is not in the lockfile's platforms" do + build_repo4 do + build_gem("sorbet-static", "0.5.6433") {|s| s.platform = "x86_64-linux-musl" } + end + + gemfile <<~G + source "https://gem.repo4" + + gem "sorbet-static", "0.5.6433" + G + + lockfile <<~L + GEM + remote: https://gem.repo4/ + specs: + sorbet-static (0.5.6433-x86_64-linux-musl) + + PLATFORMS + x86_64-linux-musl + + DEPENDENCIES + sorbet-static (= 0.5.6433) + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "x86_64-linux" do + bundle "install", raise_on_error: false + end + + expect(err).to include("Your current platform (x86_64-linux) is not included in the lockfile's platforms (x86_64-linux-musl)") + expect(err).to include("bundle lock --add-platform x86_64-linux") + end + it "does not resolve if the current platform does not match any of available platform specific variants for a transitive dependency" do build_repo4 do build_gem("sorbet", "0.5.6433") {|s| s.add_dependency "sorbet-static", "= 0.5.6433" } diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb index f59bb70c7b3f93..e6a395a6696315 100644 --- a/spec/bundler/install/gems/resolving_spec.rb +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -440,7 +440,9 @@ The source contains the following gems matching 'sorbet-static (= 0.5.10554)': * sorbet-static-0.5.10554-universal-darwin-21 E - expect(err).to end_with(nice_error) + expect(err).to include(nice_error) + expect(err).to include("Your current platform (aarch64-linux) is not included in the lockfile's platforms (arm64-darwin-21)") + expect(err).to include("bundle lock --add-platform aarch64-linux") end end From 20af0e90c64b3e5fa52be38e25428bcb757d4615 Mon Sep 17 00:00:00 2001 From: Takashi Kokubun Date: Tue, 31 Mar 2026 17:56:34 -0700 Subject: [PATCH 02/21] Add retry logic to stdgems.org curl requests in CI The curl requests to stdgems.org occasionally fail with connection errors, causing spurious CI failures. Add curl's built-in retry flags to handle transient network issues with exponential backoff. example: https://github.com/ruby/ruby/actions/runs/23825769292/job/69448266927 --- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/default_gems_list.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index a4cb520ed53002..e9e4e0eac16c9c 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -56,7 +56,7 @@ jobs: mkdir -p .downloaded-cache for data in bundled_gems.json default_gems.json; do ln -s .downloaded-cache/$data . - curl -O -R -z ./$data https://stdgems.org/$data + curl --retry 5 --retry-connrefused --retry-delay 2 --retry-max-time 60 -O -R -z ./$data https://stdgems.org/$data done - name: Update bundled gems list diff --git a/.github/workflows/default_gems_list.yml b/.github/workflows/default_gems_list.yml index 0634933499c0fa..f52b83103ce34c 100644 --- a/.github/workflows/default_gems_list.yml +++ b/.github/workflows/default_gems_list.yml @@ -43,7 +43,7 @@ jobs: data=default_gems.json mkdir -p .downloaded-cache ln -s .downloaded-cache/$data . - curl -O -R -z ./$data https://stdgems.org/$data + curl --retry 5 --retry-connrefused --retry-delay 2 --retry-max-time 60 -O -R -z ./$data https://stdgems.org/$data if: ${{ steps.gems.outcome == 'success' }} - name: Make default gems list From 05086ee83838a91936ac640445426c49ac55bde9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 02:18:25 +0000 Subject: [PATCH 03/21] 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.70.3 to 2.70.4 - [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/6ef672efc2b5aabc787a9e94baf4989aa02a97df...bfadeaba214680fb4ab63e710bcb2a6a17019fdc) --- updated-dependencies: - dependency-name: taiki-e/install-action dependency-version: 2.70.4 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 1cacfbb81f833c..d5ada785b16ecf 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@6ef672efc2b5aabc787a9e94baf4989aa02a97df # v2.70.3 + - uses: taiki-e/install-action@bfadeaba214680fb4ab63e710bcb2a6a17019fdc # v2.70.4 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 d4b30409ee0572..fc34cc2202aab5 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@6ef672efc2b5aabc787a9e94baf4989aa02a97df # v2.70.3 + - uses: taiki-e/install-action@bfadeaba214680fb4ab63e710bcb2a6a17019fdc # v2.70.4 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 1a668deeab8eaa5fbd6542e14a4738fbebbec329 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 1 Apr 2026 10:03:23 +0900 Subject: [PATCH 04/21] pty: Rename as the purpose This function prevents other users from opening this device for writing so not to be sent messages using commands such as `talk` or `wall`. --- ext/pty/pty.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ext/pty/pty.c b/ext/pty/pty.c index 54a8b9d189e598..2bae4a728ff532 100644 --- a/ext/pty/pty.c +++ b/ext/pty/pty.c @@ -286,8 +286,8 @@ ptsname_r(int fd, char *buf, size_t buflen) #endif #if defined(HAVE_POSIX_OPENPT) || defined(HAVE_OPENPTY) || defined(HAVE_PTSNAME_R) -static int -set_device_mode(const char *slavedevice, int fd, int nomesg) +static inline int +prevent_messages(const char *slavedevice, int fd, int nomesg) { if (nomesg) return change_mode(slavedevice, fd, 0600); @@ -352,7 +352,7 @@ get_device_once(int *master, int *slave, char SlaveName[DEVICELEN], int nomesg, if (ptsname_r(masterfd, SlaveName, DEVICELEN) != 0) goto error; slavedevice = SlaveName; if ((slavefd = rb_cloexec_open(slavedevice, O_RDWR|O_NOCTTY, 0)) == -1) goto error; - if (set_device_mode(slavedevice, slavefd, nomesg) == -1) goto error; + if (prevent_messages(slavedevice, slavefd, nomesg) == -1) goto error; rb_update_max_fd(slavefd); #if defined(I_PUSH) && !defined(__linux__) && !defined(_AIX) @@ -386,7 +386,7 @@ get_device_once(int *master, int *slave, char SlaveName[DEVICELEN], int nomesg, } rb_fd_fix_cloexec(*master); rb_fd_fix_cloexec(*slave); - if (set_device_mode(SlaveName, *slave, nomesg) == -1) { + if (prevent_messages(SlaveName, *slave, nomesg) == -1) { close(*master); close(*slave); if (!fail) return -1; @@ -438,7 +438,7 @@ get_device_once(int *master, int *slave, char SlaveName[DEVICELEN], int nomesg, if (ptsname_r(masterfd, SlaveName, DEVICELEN) != 0) goto error; slavedevice = SlaveName; if((slavefd = rb_cloexec_open(slavedevice, O_RDWR, 0)) == -1) goto error; - if (set_device_mode(slavedevice, slavefd, nomesg) == -1) goto error; + if (prevent_messages(slavedevice, slavefd, nomesg) == -1) goto error; rb_update_max_fd(slavefd); #if defined(I_PUSH) && !defined(__linux__) && !defined(_AIX) if(ioctl_I_PUSH(slavefd, "ptem") == -1) goto error; From 2477916e3c3d2c91110f4f39dd2b407a8ff45fbd Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 1 Apr 2026 10:20:14 +0900 Subject: [PATCH 05/21] pty: Fix missing arguments --- ext/pty/pty.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/pty/pty.c b/ext/pty/pty.c index 2bae4a728ff532..3d5977707fcae4 100644 --- a/ext/pty/pty.c +++ b/ext/pty/pty.c @@ -491,8 +491,8 @@ get_device_once(int *master, int *slave, char SlaveName[DEVICELEN], int nomesg, if ((slavefd = rb_cloexec_open(SlaveName,O_RDWR,0)) >= 0) { rb_update_max_fd(slavefd); *slave = slavefd; - if (change_owner(slavefd, getuid(), getgid()) != 0) goto error; - if (change_mode(slavefd, nomesg ? 0600 : 0622) != 0) goto error; + if (change_owner(SlaveName, slavefd, getuid(), getgid()) != 0) goto error; + if (change_mode(SlaveName, slavefd, nomesg ? 0600 : 0622) != 0) goto error; return 0; } close(masterfd); From 53099633e208b43e26a8ee3c6fdd936e2943b0d5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 30 Mar 2026 13:45:14 +0100 Subject: [PATCH 06/21] Make `ruby_xfree_sized` and `ruby_xrealloc_sized` public [Feature #21861] --- NEWS.md | 13 +++ array.c | 4 +- compile.c | 4 +- darray.h | 8 +- gc.c | 36 ++++---- imemo.c | 6 +- include/ruby/internal/xmalloc.h | 151 ++++++++++++++++++++++++++++++++ internal/gc.h | 11 +-- io.c | 2 +- iseq.c | 4 +- marshal.c | 2 +- parse.y | 16 ++-- ruby_parser.c | 2 +- st.c | 6 +- string.c | 2 +- thread.c | 10 +-- thread_sync.c | 2 +- tool/dump_ast.c | 2 + transcode.c | 24 ++--- universal_parser.c | 2 +- variable.c | 2 +- vm_eval.c | 6 +- vm_method.c | 8 +- 23 files changed, 242 insertions(+), 81 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4a6710e836c778..920c6d181ddfea 100644 --- a/NEWS.md +++ b/NEWS.md @@ -130,6 +130,18 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det and in some case memory usage. See the C extension documentation for details. [[Feature #21853]] +* Added new C23 inspired allocator functions, that takes the previous memory size. + This allow the Ruby GC to better keep track of memory usage, improving its heuristics. + It also improves the performance of system allocators that support C23 `free_sized`. + + However, it is important to note that passing an incorrect size to these function is undefined + behavior and may result in crashes or memory leaks. + + - `ruby_xfree_sized(void *ptr, size_t size)` + - `ruby_xrealloc_sized(void *ptr, size_t newsiz, size_t oldsiz)` + - `ruby_xrealloc2_sized(void *ptr, size_t newelems, size_t newsiz, size_t oldelems)` + + [[Feature #21861]] ## Implementation improvements @@ -144,6 +156,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #21390]: https://bugs.ruby-lang.org/issues/21390 [Feature #21785]: https://bugs.ruby-lang.org/issues/21785 [Feature #21853]: https://bugs.ruby-lang.org/issues/21853 +[Feature #21861]: https://bugs.ruby-lang.org/issues/21861 [Feature #21932]: https://bugs.ruby-lang.org/issues/21932 [test-unit-3.7.4]: https://github.com/test-unit/test-unit/releases/tag/3.7.4 [test-unit-3.7.5]: https://github.com/test-unit/test-unit/releases/tag/3.7.5 diff --git a/array.c b/array.c index fbb712c7262624..abf52634214af0 100644 --- a/array.c +++ b/array.c @@ -361,7 +361,7 @@ ary_heap_alloc_buffer(size_t capa) static void ary_heap_free_ptr(VALUE ary, const VALUE *ptr, long size) { - ruby_sized_xfree((void *)ptr, size); + ruby_xfree_sized((void *)ptr, size); } static void @@ -2427,7 +2427,7 @@ rb_ary_resize(VALUE ary, long len) MEMCPY((VALUE *)ARY_EMBED_PTR(ary), ptr, VALUE, len); /* WB: no new reference */ ARY_SET_EMBED_LEN(ary, len); - if (is_malloc_ptr) ruby_sized_xfree((void *)ptr, ptr_capa); + if (is_malloc_ptr) ruby_xfree_sized((void *)ptr, ptr_capa); } else { if (olen > len + ARY_DEFAULT_SIZE) { diff --git a/compile.c b/compile.c index 100ab126ed152e..910e9a8b63ea92 100644 --- a/compile.c +++ b/compile.c @@ -1666,7 +1666,7 @@ iseq_setup(rb_iseq_t *iseq, LINK_ANCHOR *const anchor) debugs("[compile step 6.1 (remove unused catch tables)] \n"); RUBY_ASSERT(ISEQ_COMPILE_DATA(iseq)); if (!ISEQ_COMPILE_DATA(iseq)->catch_except_p && ISEQ_BODY(iseq)->catch_table) { - ruby_sized_xfree(ISEQ_BODY(iseq)->catch_table, iseq_catch_table_bytes(ISEQ_BODY(iseq)->catch_table->size)); + ruby_xfree_sized(ISEQ_BODY(iseq)->catch_table, iseq_catch_table_bytes(ISEQ_BODY(iseq)->catch_table->size)); ISEQ_BODY(iseq)->catch_table = NULL; } @@ -13315,7 +13315,7 @@ ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offse } if (size == 1 && table[0] == idERROR_INFO) { - ruby_sized_xfree(table, sizeof(ID) * size); + ruby_xfree_sized(table, sizeof(ID) * size); return rb_iseq_shared_exc_local_tbl; } else { diff --git a/darray.h b/darray.h index 31ab7d412aa441..d6521be19fa7bc 100644 --- a/darray.h +++ b/darray.h @@ -161,14 +161,12 @@ rb_darray_free(void *ary) xfree(ary); } -void ruby_sized_xfree(void *x, size_t size); - static inline void rb_darray_free_sized0(void *ary, size_t element_size) { const rb_darray_meta_t *meta = ary; if (meta) { - ruby_sized_xfree(ary, sizeof(*meta) + (element_size * meta->capa)); + ruby_xfree_sized(ary, sizeof(*meta) + (element_size * meta->capa)); } } #define rb_darray_free_sized(ary, T) rb_darray_free_sized0((ary), sizeof(T)) @@ -203,7 +201,7 @@ rb_darray_calloc_mul_add_without_gc(size_t x, size_t y, size_t z) return ptr; } -void *ruby_sized_xrealloc(void *ptr, size_t new_size, size_t old_size); +void *ruby_xrealloc_sized(void *ptr, size_t new_size, size_t old_size); /* Internal function. Like rb_xrealloc_mul_add. */ static inline void * @@ -212,7 +210,7 @@ rb_darray_realloc_mul_add(void *orig_ptr, size_t capa, size_t element_size, size size_t size = rbimpl_size_add_or_raise(rbimpl_size_mul_or_raise(capa, element_size), header_size); size_t old_size = (rb_darray_capa(orig_ptr) * element_size) + header_size; // We know it won't overflow - void *ptr = ruby_sized_xrealloc(orig_ptr, size, old_size); + void *ptr = ruby_xrealloc_sized(orig_ptr, size, old_size); RUBY_ASSERT(ptr != NULL); return ptr; diff --git a/gc.c b/gc.c index 741d6c8c7a0a88..50868ea390b001 100644 --- a/gc.c +++ b/gc.c @@ -5417,19 +5417,19 @@ ruby_xcalloc_body(size_t n, size_t size) return rb_gc_impl_calloc(rb_gc_get_objspace(), xmalloc2_size(n, size), malloc_gc_allowed()); } -static void *ruby_sized_xrealloc_body(void *ptr, size_t new_size, size_t old_size); +static void *ruby_xrealloc_sized_body(void *ptr, size_t new_size, size_t old_size); -#ifdef ruby_sized_xrealloc -#undef ruby_sized_xrealloc +#ifdef ruby_xrealloc_sized +#undef ruby_xrealloc_sized #endif void * -ruby_sized_xrealloc(void *ptr, size_t new_size, size_t old_size) +ruby_xrealloc_sized(void *ptr, size_t new_size, size_t old_size) { - return handle_malloc_failure(ruby_sized_xrealloc_body(ptr, new_size, old_size)); + return handle_malloc_failure(ruby_xrealloc_sized_body(ptr, new_size, old_size)); } static void * -ruby_sized_xrealloc_body(void *ptr, size_t new_size, size_t old_size) +ruby_xrealloc_sized_body(void *ptr, size_t new_size, size_t old_size) { if ((ssize_t)new_size < 0) { negative_size_allocation_error("too large allocation size"); @@ -5441,22 +5441,22 @@ ruby_sized_xrealloc_body(void *ptr, size_t new_size, size_t old_size) void * ruby_xrealloc(void *ptr, size_t new_size) { - return ruby_sized_xrealloc(ptr, new_size, 0); + return ruby_xrealloc_sized(ptr, new_size, 0); } -static void *ruby_sized_xrealloc2_body(void *ptr, size_t n, size_t size, size_t old_n); +static void *ruby_xrealloc2_sized_body(void *ptr, size_t n, size_t size, size_t old_n); -#ifdef ruby_sized_xrealloc2 -#undef ruby_sized_xrealloc2 +#ifdef ruby_xrealloc2_sized +#undef ruby_xrealloc2_sized #endif void * -ruby_sized_xrealloc2(void *ptr, size_t n, size_t size, size_t old_n) +ruby_xrealloc2_sized(void *ptr, size_t n, size_t size, size_t old_n) { - return handle_malloc_failure(ruby_sized_xrealloc2_body(ptr, n, size, old_n)); + return handle_malloc_failure(ruby_xrealloc2_sized_body(ptr, n, size, old_n)); } static void * -ruby_sized_xrealloc2_body(void *ptr, size_t n, size_t size, size_t old_n) +ruby_xrealloc2_sized_body(void *ptr, size_t n, size_t size, size_t old_n) { size_t len = xmalloc2_size(n, size); return rb_gc_impl_realloc(rb_gc_get_objspace(), ptr, len, old_n * size, malloc_gc_allowed()); @@ -5465,14 +5465,14 @@ ruby_sized_xrealloc2_body(void *ptr, size_t n, size_t size, size_t old_n) void * ruby_xrealloc2(void *ptr, size_t n, size_t size) { - return ruby_sized_xrealloc2(ptr, n, size, 0); + return ruby_xrealloc2_sized(ptr, n, size, 0); } -#ifdef ruby_sized_xfree -#undef ruby_sized_xfree +#ifdef ruby_xfree_sized +#undef ruby_xfree_sized #endif void -ruby_sized_xfree(void *x, size_t size) +ruby_xfree_sized(void *x, size_t size) { if (LIKELY(x)) { /* It's possible for a C extension's pthread destructor function set by pthread_key_create @@ -5490,7 +5490,7 @@ ruby_sized_xfree(void *x, size_t size) void ruby_xfree(void *x) { - ruby_sized_xfree(x, 0); + ruby_xfree_sized(x, 0); } void * diff --git a/imemo.c b/imemo.c index 3300ec4b7c24f6..ce27ea56019bc9 100644 --- a/imemo.c +++ b/imemo.c @@ -94,7 +94,7 @@ rb_free_tmp_buffer(volatile VALUE *store) void *ptr = ATOMIC_PTR_EXCHANGE(s->ptr, 0); long size = s->size; s->size = 0; - ruby_sized_xfree(ptr, size); + ruby_xfree_sized(ptr, size); } } @@ -587,7 +587,7 @@ rb_imemo_free(VALUE obj) if (ci->kwarg) { ((struct rb_callinfo_kwarg *)ci->kwarg)->references--; if (ci->kwarg->references == 0) { - ruby_sized_xfree((void *)ci->kwarg, rb_callinfo_kwarg_bytes(ci->kwarg->keyword_len)); + ruby_xfree_sized((void *)ci->kwarg, rb_callinfo_kwarg_bytes(ci->kwarg->keyword_len)); } } RB_DEBUG_COUNTER_INC(obj_imemo_callinfo); @@ -637,7 +637,7 @@ rb_imemo_free(VALUE obj) break; case imemo_tmpbuf: - ruby_sized_xfree(((rb_imemo_tmpbuf_t *)obj)->ptr, ((rb_imemo_tmpbuf_t *)obj)->size); + ruby_xfree_sized(((rb_imemo_tmpbuf_t *)obj)->ptr, ((rb_imemo_tmpbuf_t *)obj)->size); RB_DEBUG_COUNTER_INC(obj_imemo_tmpbuf); break; diff --git a/include/ruby/internal/xmalloc.h b/include/ruby/internal/xmalloc.h index 132bc478ce26ec..2551609123e7bb 100644 --- a/include/ruby/internal/xmalloc.h +++ b/include/ruby/internal/xmalloc.h @@ -57,6 +57,10 @@ #define xrealloc2 ruby_xrealloc2 /**< @old{ruby_xrealloc2} */ #define xfree ruby_xfree /**< @old{ruby_xfree} */ +#define xfree_sized ruby_xfree_sized +#define xrealloc_sized ruby_xrealloc_sized +#define xrealloc2_sized ruby_xrealloc2_sized + RBIMPL_SYMBOL_EXPORT_BEGIN() RBIMPL_ATTR_NODISCARD() @@ -194,6 +198,58 @@ void *ruby_xrealloc(void *ptr, size_t newsiz) RBIMPL_ATTR_NOEXCEPT(realloc(ptr, newsiz)) ; +RBIMPL_ATTR_NODISCARD() +RBIMPL_ATTR_RETURNS_NONNULL() +RBIMPL_ATTR_ALLOC_SIZE((2)) +/** + * Identical to ruby_xrealloc(), except that it takes the old storage size + * as third argument. + * + * @param[in] ptr A valid pointer to a storage instance that was + * previously returned from either: + * - ruby_xmalloc(), + * - ruby_xmalloc2(), + * - ruby_xcalloc(), + * - ruby_xrealloc(), or + * - ruby_xrealloc2(). + * @param[in] newsiz Requested new amount of memory. + * @param[in] oldsiz Existing amount of memory. + * @exception rb_eNoMemError No space left for `newsiz` bytes allocation. + * @return A valid pointer to a (possibly newly allocated) storage + * instance; which has at least `newsiz` bytes width, with + * appropriate alignment detected by the underlying realloc() + * routine. + * @pre The passed pointer must point to a valid live storage instance. + * It is a failure to pass an already freed pointer. + * @pre The passed oldsiz must be exactly equal to the size the pointer + * was allocated with. + * Passing an incorrect oldsiz is undefined behavior, which may + * cause memory leaks or crashes. + * @post In case the function returns the passed pointer as-is, the + * storage instance that the pointer holds is either grown or + * shrunken to have at least `newsiz` bytes. Otherwise a valid + * pointer to a newly allocated storage instance is returned. In + * this case `ptr` is invalidated as if it was passed to + * ruby_xfree(). + * @note It doesn't return NULL. + * @warning Unlike some realloc() implementations, passing zero to `newsiz` + * is not the same as calling ruby_xfree(), because this function + * never returns NULL. Something meaningful still returns then. + * @warning It is a failure not to check the return value. Do not assume + * anything on it. It could be either identical to, or distinct + * form the passed argument. + * @warning Do not assume anything on the alignment of the return value. + * There is no guarantee that it inherits the passed argument's + * one. + * @warning The return value shall be invalidated exactly once by either + * ruby_xfree(), ruby_xrealloc(), or ruby_xrealloc2(). It is a + * failure to pass it to system free(), because the system and Ruby + * might or might not share the same malloc() implementation. + */ +void *ruby_xrealloc_sized(void *ptr, size_t newsiz, size_t oldsiz) +RBIMPL_ATTR_NOEXCEPT(realloc(ptr, newsiz)) +; + RBIMPL_ATTR_NODISCARD() RBIMPL_ATTR_RETURNS_NONNULL() RBIMPL_ATTR_ALLOC_SIZE((2,3)) @@ -251,6 +307,64 @@ void *ruby_xrealloc2(void *ptr, size_t newelems, size_t newsiz) RBIMPL_ATTR_NOEXCEPT(realloc(ptr, newelems * newsiz)) ; +RBIMPL_ATTR_NODISCARD() +RBIMPL_ATTR_RETURNS_NONNULL() +RBIMPL_ATTR_ALLOC_SIZE((2,3)) +/** + * Identical to ruby_xrealloc2(), except it takes the old elements count. + * + * This is roughly the same as reallocarray() function that OpenBSD + * etc. provides, but also interacts with our GC. + * + * @param[in] ptr A valid pointer to a storage instance that was + * previously returned from either: + * - ruby_xmalloc(), + * - ruby_xmalloc2(), + * - ruby_xcalloc(), + * - ruby_xrealloc(), or + * - ruby_xrealloc2(). + * @param[in] newelems Requested new number of elements. + * @param[in] newsiz Requested new size of each element. + * @param[in] oldelems The number of elements the pointer was + * previously allocated with. + * @exception rb_eNoMemError No space left for allocation. + * @exception rb_eArgError `newelems` * `newsiz` would overflow. + * @return A valid pointer to a (possibly newly allocated) storage + * instance; which has at least `newelems` * `newsiz` bytes width, + * with appropriate alignment detected by the underlying realloc() + * routine. + * @pre The passed pointer must point to a valid live storage instance. + * It is a failure to pass an already freed pointer. + * @pre The passed oldelems must be exactly equal to the number of + * elements the pointer was allocated with. + * Passing an incorrect oldelems is undefined behavior, which may + * cause memory leaks or crashes. + * @post In case the function returns the passed pointer as-is, the + * storage instance that the pointer holds is either grown or + * shrunken to have at least `newelems` * `newsiz` bytes. + * Otherwise a valid pointer to a newly allocated storage instance + * is returned. In this case `ptr` is invalidated as if it was + * passed to ruby_xfree(). + * @note It doesn't return NULL. + * @warning Unlike some realloc() implementations, passing zero to either + * `newelems` or `elemsiz` are not the same as calling + * ruby_xfree(), because this function never returns NULL. + * Something meaningful still returns then. + * @warning It is a failure not to check the return value. Do not assume + * anything on it. It could be either identical to, or distinct + * form the passed argument. + * @warning Do not assume anything on the alignment of the return value. + * There is no guarantee that it inherits the passed argument's + * one. + * @warning The return value shall be invalidated exactly once by either + * ruby_xfree(), ruby_xrealloc(), or ruby_xrealloc2(). It is a + * failure to pass it to system free(), because the system and Ruby + * might or might not share the same malloc() implementation. + */ +void *ruby_xrealloc2_sized(void *ptr, size_t newelems, size_t newsiz, size_t oldelems) +RBIMPL_ATTR_NOEXCEPT(realloc(ptr, newelems * newsiz)) +; + /** * Deallocates a storage instance. * @@ -283,6 +397,43 @@ void ruby_xfree(void *ptr) RBIMPL_ATTR_NOEXCEPT(free(ptr)) ; +/** + * Deallocates a storage instance of a specific ssize. + * + * @param[out] ptr Either + * - NULL, or + * - a valid pointer previously returned from one of: + * - ruby_xmalloc(), + * - ruby_xmalloc2(), + * - ruby_xcalloc(), + * - ruby_xrealloc(), or + * - ruby_xrealloc2(). + * @pre The passed pointer must point to a valid live storage instance. + * It is a failure to pass an already freed pointer. + * @pre The passed size must be exactly equal to the size the pointer + * was allocated with. + * Passing an incorrect size is undefined behavior, which may + * cause memory leaks or crashes. + * @post The storage instance pointed by the passed pointer gets + * invalidated; it is no longer addressable. + * @warning Every single storage instance that was previously allocated by + * either ruby_xmalloc(), ruby_xmalloc2(), ruby_xcalloc(), + * ruby_xrealloc(), or ruby_xrealloc2() shall be invalidated + * exactly once by either passing it to ruby_xfree(), or passing + * it to either ruby_xrealloc(), ruby_xrealloc2() then check the + * return value for invalidation. + * @warning Do not pass anything other than pointers described above. For + * instance pointers returned from malloc() or mmap() shall not be + * passed to this function, because the underlying memory + * management mechanism could differ. + * @warning Do not pass any invalid pointers to this function e.g. by + * calling it twice with a same argument. + */ +void ruby_xfree_sized(void *ptr, size_t size) +RBIMPL_ATTR_NOEXCEPT(free(ptr)) +; + + RBIMPL_SYMBOL_EXPORT_END() #endif /* RBIMPL_XMALLOC_H */ diff --git a/internal/gc.h b/internal/gc.h index d0bbc033882d17..f1b27dd31d8c3e 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -253,9 +253,6 @@ struct rb_gc_object_metadata_entry *rb_gc_object_metadata(VALUE obj); void rb_gc_mark_values(long n, const VALUE *values); void rb_gc_mark_vm_stack_values(long n, const VALUE *values); void rb_gc_update_values(long n, VALUE *values); -void *ruby_sized_xrealloc(void *ptr, size_t new_size, size_t old_size) RUBY_ATTR_RETURNS_NONNULL RUBY_ATTR_ALLOC_SIZE((2)); -void *ruby_sized_xrealloc2(void *ptr, size_t new_count, size_t element_size, size_t old_count) RUBY_ATTR_RETURNS_NONNULL RUBY_ATTR_ALLOC_SIZE((2, 3)); -void ruby_sized_xfree(void *x, size_t size); const char *rb_gc_active_gc_name(void); int rb_gc_modular_gc_loaded_p(void); @@ -288,15 +285,15 @@ const char *rb_obj_info(VALUE obj); void ruby_annotate_mmap(const void *addr, unsigned long size, const char *name); # define SIZED_REALLOC_N(v, T, m, n) \ - ((v) = (T *)ruby_sized_xrealloc2((void *)(v), (m), sizeof(T), (n))) + ((v) = (T *)ruby_xrealloc2_sized((void *)(v), (m), sizeof(T), (n))) -# define SIZED_FREE(v) ruby_sized_xfree((void *)(v), sizeof(*(v))) -# define SIZED_FREE_N(v, n) ruby_sized_xfree((void *)(v), sizeof(*(v)) * (n)) +# define SIZED_FREE(v) ruby_xfree_sized((void *)(v), sizeof(*(v))) +# define SIZED_FREE_N(v, n) ruby_xfree_sized((void *)(v), sizeof(*(v)) * (n)) static inline void * ruby_sized_realloc_n(void *ptr, size_t new_count, size_t element_size, size_t old_count) { - return ruby_sized_xrealloc2(ptr, new_count, element_size, old_count); + return ruby_xrealloc2_sized(ptr, new_count, element_size, old_count); } void rb_gc_verify_shareable(VALUE); diff --git a/io.c b/io.c index d74aa7d5c36ec4..aceb534dd33c01 100644 --- a/io.c +++ b/io.c @@ -5652,7 +5652,7 @@ static void free_io_buffer(rb_io_buffer_t *buf) { if (buf->ptr) { - ruby_sized_xfree(buf->ptr, (size_t)buf->capa); + ruby_xfree_sized(buf->ptr, (size_t)buf->capa); buf->ptr = NULL; } } diff --git a/iseq.c b/iseq.c index 6f87b2df3e085b..759c8be6900eae 100644 --- a/iseq.c +++ b/iseq.c @@ -88,7 +88,7 @@ free_arena(struct iseq_compile_data_storage *cur) while (cur) { next = cur->next; - ruby_sized_xfree(cur, offsetof(struct iseq_compile_data_storage, buff) + cur->size * sizeof(char)); + ruby_xfree_sized(cur, offsetof(struct iseq_compile_data_storage, buff) + cur->size * sizeof(char)); cur = next; } } @@ -208,7 +208,7 @@ rb_iseq_free(const rb_iseq_t *iseq) SIZED_FREE_N(body->is_entries, ISEQ_IS_SIZE(body)); SIZED_FREE_N(body->call_data, body->ci_size); if (body->catch_table) { - ruby_sized_xfree(body->catch_table, iseq_catch_table_bytes(body->catch_table->size)); + ruby_xfree_sized(body->catch_table, iseq_catch_table_bytes(body->catch_table->size)); } SIZED_FREE_N(body->param.opt_table, body->param.opt_num + 1); if (ISEQ_MBITS_BUFLEN(body->iseq_size) > 1 && body->mark_bits.list) { diff --git a/marshal.c b/marshal.c index 967855529e6d76..2a7f3563cfd0a9 100644 --- a/marshal.c +++ b/marshal.c @@ -2361,7 +2361,7 @@ r_object(struct load_arg *arg) static void clear_load_arg(struct load_arg *arg) { - ruby_sized_xfree(arg->buf, BUFSIZ); + ruby_xfree_sized(arg->buf, BUFSIZ); arg->buf = NULL; arg->buflen = 0; arg->offset = 0; diff --git a/parse.y b/parse.y index 170ed08a5e87a7..4f5c93c6006384 100644 --- a/parse.y +++ b/parse.y @@ -829,7 +829,7 @@ pop_end_expect_token_locations(struct parser_params *p) if(!p->end_expect_token_locations) return; end_expect_token_locations_t *locations = p->end_expect_token_locations->prev; - ruby_sized_xfree(p->end_expect_token_locations, sizeof(end_expect_token_locations_t)); + ruby_xfree_sized(p->end_expect_token_locations, sizeof(end_expect_token_locations_t)); p->end_expect_token_locations = locations; debug_end_expect_token_locations(p, "pop_end_expect_token_locations"); @@ -7040,7 +7040,7 @@ token_info_pop(struct parser_params *p, const char *token, const rb_code_locatio token_info_warn(p, token, ptinfo_beg, 1, loc); p->token_info = ptinfo_beg->next; - ruby_sized_xfree(ptinfo_beg, sizeof(*ptinfo_beg)); + ruby_xfree_sized(ptinfo_beg, sizeof(*ptinfo_beg)); } static void @@ -7060,7 +7060,7 @@ token_info_drop(struct parser_params *p, const char *token, rb_code_position_t b ptinfo_beg->token); } - ruby_sized_xfree(ptinfo_beg, sizeof(*ptinfo_beg)); + ruby_xfree_sized(ptinfo_beg, sizeof(*ptinfo_beg)); } static void @@ -7312,9 +7312,9 @@ vtable_free_gen(struct parser_params *p, int line, const char *name, #endif if (!DVARS_TERMINAL_P(tbl)) { if (tbl->tbl) { - ruby_sized_xfree(tbl->tbl, tbl->capa * sizeof(ID)); + ruby_xfree_sized(tbl->tbl, tbl->capa * sizeof(ID)); } - ruby_sized_xfree(tbl, sizeof(*tbl)); + ruby_xfree_sized(tbl, sizeof(*tbl)); } } #define vtable_free(tbl) vtable_free_gen(p, __LINE__, #tbl, tbl) @@ -14917,7 +14917,7 @@ local_free(struct parser_params *p, struct local_vars *local) vtable_chain_free(p, local->args); vtable_chain_free(p, local->vars); - ruby_sized_xfree(local, sizeof(struct local_vars)); + ruby_xfree_sized(local, sizeof(struct local_vars)); } static void @@ -15176,7 +15176,7 @@ dyna_pop(struct parser_params *p, const struct vtable *lvargs) dyna_pop_1(p); if (!p->lvtbl->args) { struct local_vars *local = p->lvtbl->prev; - ruby_sized_xfree(p->lvtbl, sizeof(*p->lvtbl)); + ruby_xfree_sized(p->lvtbl, sizeof(*p->lvtbl)); p->lvtbl = local; } } @@ -15566,7 +15566,7 @@ rb_ruby_parser_free(void *ptr) #endif if (p->tokenbuf) { - ruby_sized_xfree(p->tokenbuf, p->toksiz); + ruby_xfree_sized(p->tokenbuf, p->toksiz); } for (local = p->lvtbl; local; local = prev) { diff --git a/ruby_parser.c b/ruby_parser.c index 267f619bf9cd18..a96fc4974bda2a 100644 --- a/ruby_parser.c +++ b/ruby_parser.c @@ -407,7 +407,7 @@ static const rb_parser_config_t rb_global_parser_config = { .set_errinfo = rb_set_errinfo, .make_exception = rb_make_exception, - .sized_xfree = ruby_sized_xfree, + .sized_xfree = ruby_xfree_sized, .sized_realloc_n = ruby_sized_realloc_n, .gc_guard = gc_guard, .gc_mark = rb_gc_mark, diff --git a/st.c b/st.c index d884a0218dbdfc..0a2408c6e0a6e6 100644 --- a/st.c +++ b/st.c @@ -174,10 +174,10 @@ static const struct st_hash_type type_strcasehash = { #define malloc ruby_xmalloc #define calloc ruby_xcalloc #define realloc ruby_xrealloc -#define sized_realloc ruby_sized_xrealloc +#define sized_realloc ruby_xrealloc_sized #define free ruby_xfree -#define sized_free ruby_sized_xfree -#define free_fixed_ptr(v) ruby_sized_xfree((v), sizeof(*(v))) +#define sized_free ruby_xfree_sized +#define free_fixed_ptr(v) ruby_xfree_sized((v), sizeof(*(v))) #else #define sized_realloc(ptr, new_size, old_size) realloc(ptr, new_size) #define sized_free(v, s) free(v) diff --git a/string.c b/string.c index 55a229f37c3b5c..19b8a1e225c32a 100644 --- a/string.c +++ b/string.c @@ -7828,7 +7828,7 @@ mapping_buffer_free(void *p) while (current_buffer) { previous_buffer = current_buffer; current_buffer = current_buffer->next; - ruby_sized_xfree(previous_buffer, offsetof(mapping_buffer, space) + previous_buffer->capa); + ruby_xfree_sized(previous_buffer, offsetof(mapping_buffer, space) + previous_buffer->capa); } } diff --git a/thread.c b/thread.c index f876b4bd05c80e..25dde73fd7f28f 100644 --- a/thread.c +++ b/thread.c @@ -4289,7 +4289,7 @@ rb_fd_init_copy(rb_fdset_t *dst, rb_fdset_t *src) void rb_fd_term(rb_fdset_t *fds) { - ruby_sized_xfree(fds->fdset, fdset_memsize(fds->maxfd)); + ruby_xfree_sized(fds->fdset, fdset_memsize(fds->maxfd)); fds->maxfd = 0; fds->fdset = 0; } @@ -4308,7 +4308,7 @@ rb_fd_resize(int n, rb_fdset_t *fds) size_t o = fdset_memsize(fds->maxfd); if (m > o) { - fds->fdset = ruby_sized_xrealloc(fds->fdset, m, o); + fds->fdset = ruby_xrealloc_sized(fds->fdset, m, o); memset((char *)fds->fdset + o, 0, m - o); } if (n >= fds->maxfd) fds->maxfd = n + 1; @@ -4339,7 +4339,7 @@ void rb_fd_copy(rb_fdset_t *dst, const fd_set *src, int max) { size_t size = fdset_memsize(max); - dst->fdset = ruby_sized_xrealloc(dst->fdset, size, fdset_memsize(dst->maxfd)); + dst->fdset = ruby_xrealloc_sized(dst->fdset, size, fdset_memsize(dst->maxfd)); dst->maxfd = max; memcpy(dst->fdset, src, size); } @@ -4348,7 +4348,7 @@ void rb_fd_dup(rb_fdset_t *dst, const rb_fdset_t *src) { size_t size = fdset_memsize(rb_fd_max(src)); - dst->fdset = ruby_sized_xrealloc(dst->fdset, size, fdset_memsize(dst->maxfd)); + dst->fdset = ruby_xrealloc_sized(dst->fdset, size, fdset_memsize(dst->maxfd)); dst->maxfd = src->maxfd; memcpy(dst->fdset, src->fdset, size); } @@ -4413,7 +4413,7 @@ fdset_memsize(int capa) void rb_fd_term(rb_fdset_t *set) { - ruby_sized_xfree(set->fdset, fdset_memsize(set->capa)); + ruby_xfree_sized(set->fdset, fdset_memsize(set->capa)); set->fdset = NULL; set->capa = 0; } diff --git a/thread_sync.c b/thread_sync.c index cf4e3843ff6c2f..c9e056dfe2deb5 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -916,7 +916,7 @@ ring_buffer_expand(struct rb_queue *q) VALUE *old_buffer = q->buffer; q->buffer = new_buffer; q->offset = 0; - ruby_sized_xfree(old_buffer, q->capa * sizeof(VALUE)); + ruby_xfree_sized(old_buffer, q->capa * sizeof(VALUE)); q->capa *= 2; } diff --git a/tool/dump_ast.c b/tool/dump_ast.c index cd7bcb84748739..58250e9b8c4837 100644 --- a/tool/dump_ast.c +++ b/tool/dump_ast.c @@ -13,6 +13,8 @@ void *ruby_xmalloc(size_t size) { return malloc(size); } void *ruby_xcalloc(size_t nelems, size_t elemsiz) { return calloc(nelems, elemsiz); } void *ruby_xrealloc(void *ptr, size_t newsiz) { return realloc(ptr, newsiz); } void ruby_xfree(void *ptr) { free(ptr); } +void ruby_xfree_sized(void *ptr, size_t _oldsize) { free(ptr); } +void *ruby_xrealloc_sized(void *ptr, size_t newsiz, size_t _oldsiz) { return realloc(ptr, newsiz); } #include "prism.h" diff --git a/transcode.c b/transcode.c index f8b0fec42ef275..a70dc9d9ea6a62 100644 --- a/transcode.c +++ b/transcode.c @@ -875,11 +875,11 @@ rb_transcoding_close(rb_transcoding *tc) (tr->state_fini_func)(TRANSCODING_STATE(tc)); /* check return value? */ } if (TRANSCODING_STATE_EMBED_MAX < tr->state_size) - ruby_sized_xfree(tc->state.ptr, tr->state_size); + ruby_xfree_sized(tc->state.ptr, tr->state_size); if ((int)sizeof(tc->readbuf.ary) < tr->max_input) - ruby_sized_xfree(tc->readbuf.ptr, tr->max_input); + ruby_xfree_sized(tc->readbuf.ptr, tr->max_input); if ((int)sizeof(tc->writebuf.ary) < tr->max_output) - ruby_sized_xfree(tc->writebuf.ptr, tr->max_output); + ruby_xfree_sized(tc->writebuf.ptr, tr->max_output); SIZED_FREE(tc); } @@ -1472,12 +1472,12 @@ output_hex_charref(rb_econv_t *ec) } if (utf_allocated) - ruby_sized_xfree((void *)utf, utf_bufsize); + ruby_xfree_sized((void *)utf, utf_bufsize); return 0; fail: if (utf_allocated) - ruby_sized_xfree((void *)utf, utf_bufsize); + ruby_xfree_sized((void *)utf, utf_bufsize); return -1; } @@ -1601,7 +1601,7 @@ allocate_converted_string(const char *sname, const char *dname, dst_str = tmp; } else { - dst_str = ruby_sized_xrealloc(dst_str, dst_bufsize, dst_bufsize / 2); + dst_str = ruby_xrealloc_sized(dst_str, dst_bufsize, dst_bufsize / 2); } dp = dst_str+dst_len; res = rb_econv_convert(ec, &sp, str+len, &dp, dst_str+dst_bufsize, 0); @@ -1617,7 +1617,7 @@ allocate_converted_string(const char *sname, const char *dname, fail: if (dst_str != caller_dst_buf) - ruby_sized_xfree(dst_str, dst_bufsize); + ruby_xfree_sized(dst_str, dst_bufsize); rb_econv_close(ec); return NULL; } @@ -1712,7 +1712,7 @@ rb_econv_insert_output(rb_econv_t *ec, size_t s = (*data_end_p - *buf_start_p) + need; if (s < need) goto fail; - buf = ruby_sized_xrealloc(*buf_start_p, s, buf_end_p - buf_start_p); + buf = ruby_xrealloc_sized(*buf_start_p, s, buf_end_p - buf_start_p); *data_start_p = buf; *data_end_p = buf + (*data_end_p - *buf_start_p); *buf_start_p = buf; @@ -1729,12 +1729,12 @@ rb_econv_insert_output(rb_econv_t *ec, } if (insert_str != str && insert_str != insert_buf) - ruby_sized_xfree((void *)insert_str, insert_bufsize); + ruby_xfree_sized((void *)insert_str, insert_bufsize); return 0; fail: if (insert_str != str && insert_str != insert_buf) - ruby_sized_xfree((void *)insert_str, insert_bufsize); + ruby_xfree_sized((void *)insert_str, insert_bufsize); return -1; } @@ -1748,7 +1748,7 @@ rb_econv_close(rb_econv_t *ec) } for (i = 0; i < ec->num_trans; i++) { rb_transcoding_close(ec->elems[i].tc); - ruby_sized_xfree(ec->elems[i].out_buf_start, ec->elems[i].out_buf_end - ec->elems[i].out_buf_start); + ruby_xfree_sized(ec->elems[i].out_buf_start, ec->elems[i].out_buf_end - ec->elems[i].out_buf_start); } SIZED_FREE_N(ec->in_buf_start, ec->in_buf_end - ec->in_buf_start); SIZED_FREE_N(ec->elems, ec->num_allocated); @@ -2047,7 +2047,7 @@ rb_econv_binmode(rb_econv_t *ec) for (i=0; i < num_trans; i++) { if (transcoder == ec->elems[i].tc->transcoder) { rb_transcoding_close(ec->elems[i].tc); - ruby_sized_xfree(ec->elems[i].out_buf_start, ec->elems[i].out_buf_end - ec->elems[i].out_buf_start); + ruby_xfree_sized(ec->elems[i].out_buf_start, ec->elems[i].out_buf_end - ec->elems[i].out_buf_start); ec->num_trans--; } else diff --git a/universal_parser.c b/universal_parser.c index 84c71748af7de2..b9cddd2879d717 100644 --- a/universal_parser.c +++ b/universal_parser.c @@ -166,7 +166,7 @@ #define rb_set_errinfo p->config->set_errinfo #define rb_make_exception p->config->make_exception -#define ruby_sized_xfree p->config->sized_xfree +#define ruby_xfree_sized p->config->sized_xfree #define SIZED_REALLOC_N(v, T, m, n) ((v) = (T *)p->config->sized_realloc_n((void *)(v), (m), sizeof(T), (n))) #undef RB_GC_GUARD #define RB_GC_GUARD p->config->gc_guard diff --git a/variable.c b/variable.c index 1856095f8c33a5..3ae06b069b732c 100644 --- a/variable.c +++ b/variable.c @@ -1719,7 +1719,7 @@ rb_ivar_delete(VALUE obj, ID id, VALUE undef) SIZED_FREE_N(fields, RSHAPE_CAPACITY(old_shape_id)); } else if (RSHAPE_CAPACITY(old_shape_id) != RSHAPE_CAPACITY(next_shape_id)) { - IMEMO_OBJ_FIELDS(fields_obj)->as.external.ptr = ruby_sized_xrealloc(fields, RSHAPE_CAPACITY(next_shape_id) * sizeof(VALUE), RSHAPE_CAPACITY(old_shape_id) * sizeof(VALUE)); + IMEMO_OBJ_FIELDS(fields_obj)->as.external.ptr = ruby_xrealloc_sized(fields, RSHAPE_CAPACITY(next_shape_id) * sizeof(VALUE), RSHAPE_CAPACITY(old_shape_id) * sizeof(VALUE)); } } } diff --git a/vm_eval.c b/vm_eval.c index c6d5a1dd44d1ea..a3a30703b815bf 100644 --- a/vm_eval.c +++ b/vm_eval.c @@ -1761,7 +1761,7 @@ pm_eval_make_iseq(VALUE src, VALUE fname, int line, if (rb_is_local_id(local)) { VALUE name_obj = rb_id2str(local); const char *name = RSTRING_PTR(name_obj); - size_t length = strlen(name); + size_t length = RSTRING_LEN(name_obj); // Explicitly skip numbered parameters. These should not be sent // into the eval. @@ -1780,8 +1780,8 @@ pm_eval_make_iseq(VALUE src, VALUE fname, int line, /* We need to duplicate the string because the Ruby string may * be embedded so compaction could move the string and the pointer * will change. */ - char *name_dup = xmalloc(length + 1); - strlcpy(name_dup, name, length + 1); + char *name_dup = xmalloc(length); + MEMCPY(name_dup, name, char, length); RB_GC_GUARD(name_obj); diff --git a/vm_method.c b/vm_method.c index f6f666a9188076..fc26c4e8b4eda3 100644 --- a/vm_method.c +++ b/vm_method.c @@ -40,7 +40,7 @@ mark_cc_entry_i(VALUE ccs_ptr, void *data) VM_ASSERT(!vm_cc_super_p(cc) && !vm_cc_refinement_p(cc)); vm_cc_invalidate(cc); } - ruby_sized_xfree(ccs, vm_ccs_alloc_size(ccs->capa)); + ruby_xfree_sized(ccs, vm_ccs_alloc_size(ccs->capa)); return ID_TABLE_DELETE; } else { @@ -71,7 +71,7 @@ cc_table_free_i(VALUE ccs_ptr, void *data) struct rb_class_cc_entries *ccs = (struct rb_class_cc_entries *)ccs_ptr; VM_ASSERT(vm_ccs_p(ccs)); - ruby_sized_xfree(ccs, vm_ccs_alloc_size(ccs->capa)); + ruby_xfree_sized(ccs, vm_ccs_alloc_size(ccs->capa)); return ID_TABLE_CONTINUE; } @@ -201,7 +201,7 @@ rb_vm_ccs_invalidate_and_free(struct rb_class_cc_entries *ccs) { RB_DEBUG_COUNTER_INC(ccs_free); vm_ccs_invalidate(ccs); - ruby_sized_xfree(ccs, vm_ccs_alloc_size(ccs->capa)); + ruby_xfree_sized(ccs, vm_ccs_alloc_size(ccs->capa)); } void @@ -577,7 +577,7 @@ invalidate_ccs_in_iclass_cc_tbl(VALUE value, void *data) { struct rb_class_cc_entries *ccs = (struct rb_class_cc_entries *)value; vm_cme_invalidate((rb_callable_method_entry_t *)ccs->cme); - ruby_sized_xfree(ccs, vm_ccs_alloc_size(ccs->capa)); + ruby_xfree_sized(ccs, vm_ccs_alloc_size(ccs->capa)); return ID_TABLE_DELETE; } From a9b8d47de2856a0d42ab8457ad79db6d6cbf0751 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Feb 2026 18:11:16 +0900 Subject: [PATCH 07/21] [ruby/rubygems] Merge rubygems-attestation-patch.rb from rubygems/release-gem https://github.com/ruby/rubygems/commit/64f085f5ee --- lib/rubygems/commands/push_command.rb | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index d2ce86703ba382..2168c5dc2f7d6d 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -92,6 +92,19 @@ def send_gem(name) private def send_push_request(name, args) + if RUBY_ENGINE == "jruby" || options[:attestations].any? || !attestation_supported_host? + return send_push_request_without_attestation(name, args) + end + + begin + send_push_request_with_attestation(name, args) + rescue StandardError => e + alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}" + send_push_request_without_attestation(name, args) + end + end + + def send_push_request_without_attestation(name, args) scope = get_push_scope rubygems_api_request(*args, scope: scope) do |request| body = Gem.read_binary name @@ -109,6 +122,34 @@ def send_push_request(name, args) end end + def send_push_request_with_attestation(name, args) + attestation = attest!(name) + + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form([ + ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], + ["attestations", "[#{Gem.read_binary(attestation)}]", { content_type: "application/json" }], + ], "multipart/form-data") + request.add_field "Authorization", api_key + end + end + + def attest!(name) + require "open3" + + bundle = "#{name}.sigstore.json" + env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h + out, st = Open3.capture2e( + env, + Gem.ruby, "-S", "gem", "exec", + "sigstore-cli:0.2.2", "sign", name, "--bundle", bundle, + unsetenv_others: true + ) + raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? + + bundle + end + def get_hosts_for(name) gem_metadata = Gem::Package.new(name).spec.metadata @@ -122,6 +163,10 @@ def get_push_scope :push_rubygem end + def attestation_supported_host? + (@host || Gem.host) == "https://rubygems.org" + end + def get_attestations_part bundles = "[" + options[:attestations].map do |attestation| Gem.read_binary(attestation) From 2041c87736eababcdf399cdc6639da69db957850 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Feb 2026 18:26:18 +0900 Subject: [PATCH 08/21] [ruby/rubygems] Refactor push request to support attestations https://github.com/ruby/rubygems/commit/74924da660 --- lib/rubygems/commands/push_command.rb | 37 ++++++++++----------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 2168c5dc2f7d6d..b8af8cfd9188bc 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -92,7 +92,7 @@ def send_gem(name) private def send_push_request(name, args) - if RUBY_ENGINE == "jruby" || options[:attestations].any? || !attestation_supported_host? + if RUBY_ENGINE == "jruby" || !attestation_supported_host? return send_push_request_without_attestation(name, args) end @@ -108,27 +108,27 @@ def send_push_request_without_attestation(name, args) scope = get_push_scope rubygems_api_request(*args, scope: scope) do |request| body = Gem.read_binary name - if options[:attestations].any? - request.set_form([ - ["gem", body, { filename: name, content_type: "application/octet-stream" }], - get_attestations_part, - ], "multipart/form-data") - else - request.body = body - request.add_field "Content-Type", "application/octet-stream" - request.add_field "Content-Length", request.body.size - end + request.body = body + request.add_field "Content-Type", "application/octet-stream" + request.add_field "Content-Length", request.body.size request.add_field "Authorization", api_key end end def send_push_request_with_attestation(name, args) - attestation = attest!(name) + attestations = if options[:attestations].any? + options[:attestations].map do |attestation| + Gem.read_binary(attestation) + end + else + [Gem.read_binary(attest!(name))] + end + bundles = "[" + attestations.join(",") + "]" rubygems_api_request(*args, scope: get_push_scope) do |request| request.set_form([ ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], - ["attestations", "[#{Gem.read_binary(attestation)}]", { content_type: "application/json" }], + ["attestations", bundles, { content_type: "application/json" }], ], "multipart/form-data") request.add_field "Authorization", api_key end @@ -166,15 +166,4 @@ def get_push_scope def attestation_supported_host? (@host || Gem.host) == "https://rubygems.org" end - - def get_attestations_part - bundles = "[" + options[:attestations].map do |attestation| - Gem.read_binary(attestation) - end.join(",") + "]" - [ - "attestations", - bundles, - { content_type: "application/json" }, - ] - end end From e35eaf5695db3049c2e4c4747a950fcd799b560c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Feb 2026 18:47:15 +0900 Subject: [PATCH 09/21] [ruby/rubygems] Added test for auto-attestation https://github.com/ruby/rubygems/commit/df2bdde8e5 --- .../test_gem_commands_push_command.rb | 87 ++++++++++++++----- 1 file changed, 64 insertions(+), 23 deletions(-) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 978ed3ada88e8c..66e92b616f4a40 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -115,32 +115,44 @@ def test_execute_attestation assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class content_length = @fetcher.last_request["Content-Length"].to_i assert_equal content_length, @fetcher.last_request.body.length - assert_equal "multipart", @fetcher.last_request.main_type, @fetcher.last_request.content_type - assert_equal "form-data", @fetcher.last_request.sub_type - assert_include @fetcher.last_request.type_params, "boundary" - boundary = @fetcher.last_request.type_params["boundary"] + assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") + end - parts = @fetcher.last_request.body.split(/(?:\r\n|\A)--#{Regexp.quote(boundary)}(?:\r\n|--)/m) - refute_empty parts - assert_empty parts[0] - parts.shift # remove the first empty part + def test_execute_attestation_auto + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - p1 = parts.shift - p2 = parts.shift - assert_equal "\r\n", parts.shift - assert_empty parts + attestation_path = "#{@path}.sigstore.json" + File.write(attestation_path, "auto-attestation") + @cmd.options[:args] = [@path] - assert_equal [ - "Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"", - "Content-Type: application/octet-stream", - nil, - Gem.read_binary(@path), - ].join("\r\n").b, p1 - assert_equal [ - "Content-Disposition: form-data; name=\"attestations\"", - nil, - "[#{Gem.read_binary("#{@path}.sigstore.json")}]", - ].join("\r\n").b, p2 + @cmd.stub(:attest!, attestation_path) do + @cmd.execute + end + + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + content_length = @fetcher.last_request["Content-Length"].to_i + assert_equal content_length, @fetcher.last_request.body.length + assert_attestation_multipart Gem.read_binary(attestation_path) + end + + def test_execute_attestation_fallback + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + @cmd.stub(:attest!, proc { raise Gem::Exception, "boom" }) do + use_ui @ui do + @cmd.execute + end + end + + assert_match "Failed to push with attestation, retrying without attestation.", @ui.error + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] end def test_execute_allowed_push_host @@ -643,6 +655,35 @@ def test_sending_gem_with_no_local_creds private + def assert_attestation_multipart(attestation_payload) + assert_equal "multipart", @fetcher.last_request.main_type, @fetcher.last_request.content_type + assert_equal "form-data", @fetcher.last_request.sub_type + assert_include @fetcher.last_request.type_params, "boundary" + boundary = @fetcher.last_request.type_params["boundary"] + + parts = @fetcher.last_request.body.split(/(?:\r\n|\A)--#{Regexp.quote(boundary)}(?:\r\n|--)/m) + refute_empty parts + assert_empty parts[0] + parts.shift # remove the first empty part + + p1 = parts.shift + p2 = parts.shift + assert_equal "\r\n", parts.shift + assert_empty parts + + assert_equal [ + "Content-Disposition: form-data; name=\"gem\"; filename=\"#{@path}\"", + "Content-Type: application/octet-stream", + nil, + Gem.read_binary(@path), + ].join("\r\n").b, p1 + assert_equal [ + "Content-Disposition: form-data; name=\"attestations\"", + nil, + "[#{attestation_payload}]", + ].join("\r\n").b, p2 + end + def singleton_gem_class class << Gem; self; end end From bf6173110d933b5eed129d2e2cab1deaf222ad16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:18:20 +0000 Subject: [PATCH 10/21] [ruby/rubygems] Add test coverage for skipping auto-attestation https://github.com/ruby/rubygems/commit/6cd04a57da Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com> --- .../test_gem_commands_push_command.rb | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 66e92b616f4a40..2503d3a53a85be 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -155,6 +155,55 @@ def test_execute_attestation_fallback @fetcher.last_request["Content-Type"] end + def test_execute_attestation_skipped_on_non_rubygems_host + @spec, @path = util_gem "freebird", "1.0.1" do |spec| + spec.metadata["allowed_push_host"] = "https://privategemserver.example" + end + + @response = "Successfully registered gem: freebird (1.0.1)" + @fetcher.data["#{@spec.metadata["allowed_push_host"]}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + attest_called = false + @cmd.stub(:attest!, proc { attest_called = true; raise "attest! should not be called" }) do + @cmd.execute + end + + refute attest_called, "attest! should not be called for non-rubygems.org hosts" + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + end + + def test_execute_attestation_skipped_on_jruby + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + + @cmd.options[:args] = [@path] + + attest_called = false + engine = RUBY_ENGINE + Object.send :remove_const, :RUBY_ENGINE + Object.const_set :RUBY_ENGINE, "jruby" + + begin + @cmd.stub(:attest!, proc { attest_called = true; raise "attest! should not be called" }) do + @cmd.execute + end + + refute attest_called, "attest! should not be called on JRuby" + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + ensure + Object.send :remove_const, :RUBY_ENGINE + Object.const_set :RUBY_ENGINE, engine + end + end + def test_execute_allowed_push_host @spec, @path = util_gem "freebird", "1.0.1" do |spec| spec.metadata["allowed_push_host"] = "https://privategemserver.example" From c4eeb675124cd489dde0d4803d81bf6835d0d8d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:19:16 +0000 Subject: [PATCH 11/21] [ruby/rubygems] Remove raise from stub to rely on flag for test assertions https://github.com/ruby/rubygems/commit/7ebc1abafd Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com> --- test/rubygems/test_gem_commands_push_command.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 2503d3a53a85be..d160ecb0b98dbf 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -166,7 +166,7 @@ def test_execute_attestation_skipped_on_non_rubygems_host @cmd.options[:args] = [@path] attest_called = false - @cmd.stub(:attest!, proc { attest_called = true; raise "attest! should not be called" }) do + @cmd.stub(:attest!, proc { attest_called = true }) do @cmd.execute end @@ -189,7 +189,7 @@ def test_execute_attestation_skipped_on_jruby Object.const_set :RUBY_ENGINE, "jruby" begin - @cmd.stub(:attest!, proc { attest_called = true; raise "attest! should not be called" }) do + @cmd.stub(:attest!, proc { attest_called = true }) do @cmd.execute end From 6ef9fe59c8ea97f4ff67ffd150f72d22ceeab494 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 10:26:20 +0000 Subject: [PATCH 12/21] [ruby/rubygems] Use Tempfile for auto-attestation bundles and clean up after use https://github.com/ruby/rubygems/commit/498401c010 Co-authored-by: hsbt <12301+hsbt@users.noreply.github.com> --- lib/rubygems/commands/push_command.rb | 13 +++++++++++-- test/rubygems/test_gem_commands_push_command.rb | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index b8af8cfd9188bc..95f23e06330ac3 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -121,7 +121,12 @@ def send_push_request_with_attestation(name, args) Gem.read_binary(attestation) end else - [Gem.read_binary(attest!(name))] + bundle_path = attest!(name) + begin + [Gem.read_binary(bundle_path)] + ensure + File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path) + end end bundles = "[" + attestations.join(",") + "]" @@ -136,8 +141,12 @@ def send_push_request_with_attestation(name, args) def attest!(name) require "open3" + require "tempfile" + + tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"]) + bundle = tempfile.path + tempfile.close(false) - bundle = "#{name}.sigstore.json" env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h out, st = Open3.capture2e( env, diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index d160ecb0b98dbf..9bb90a248c93af 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -123,7 +123,8 @@ def test_execute_attestation_auto @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") attestation_path = "#{@path}.sigstore.json" - File.write(attestation_path, "auto-attestation") + attestation_content = "auto-attestation" + File.write(attestation_path, attestation_content) @cmd.options[:args] = [@path] @cmd.stub(:attest!, attestation_path) do @@ -133,7 +134,7 @@ def test_execute_attestation_auto assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class content_length = @fetcher.last_request["Content-Length"].to_i assert_equal content_length, @fetcher.last_request.body.length - assert_attestation_multipart Gem.read_binary(attestation_path) + assert_attestation_multipart attestation_content end def test_execute_attestation_fallback From d2f9872eab745ce4863f53d24ab6db93876fd82d Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 10 Feb 2026 19:35:25 +0900 Subject: [PATCH 13/21] [ruby/rubygems] Omit tests of auto-attestation with JRuby https://github.com/ruby/rubygems/commit/543ac52412 --- test/rubygems/test_gem_commands_push_command.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 9bb90a248c93af..3f2513563ed6c8 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -103,6 +103,8 @@ def test_execute_host end def test_execute_attestation + omit if RUBY_ENGINE == "jruby" + @response = "Successfully registered gem: freewill (1.0.0)" @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") @@ -119,6 +121,8 @@ def test_execute_attestation end def test_execute_attestation_auto + omit if RUBY_ENGINE == "jruby" + @response = "Successfully registered gem: freewill (1.0.0)" @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") @@ -138,6 +142,8 @@ def test_execute_attestation_auto end def test_execute_attestation_fallback + omit if RUBY_ENGINE == "jruby" + @response = "Successfully registered gem: freewill (1.0.0)" @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") From 54ce91133a733ffbde33f3579b5c083134ba3a96 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 24 Feb 2026 18:57:11 +0900 Subject: [PATCH 14/21] [ruby/rubygems] Reverse to use attestation condition https://github.com/ruby/rubygems/commit/ea1f43c4ae --- lib/rubygems/commands/push_command.rb | 13 ++- .../test_gem_commands_push_command.rb | 85 +++++++++++-------- 2 files changed, 56 insertions(+), 42 deletions(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 95f23e06330ac3..62dbd14e0bb2f3 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -92,14 +92,10 @@ def send_gem(name) private def send_push_request(name, args) - if RUBY_ENGINE == "jruby" || !attestation_supported_host? - return send_push_request_without_attestation(name, args) - end - - begin + # Attestation is only supported on rubygems.org with GitHub Actions (not JRuby) + if RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"] send_push_request_with_attestation(name, args) - rescue StandardError => e - alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}" + else send_push_request_without_attestation(name, args) end end @@ -137,6 +133,9 @@ def send_push_request_with_attestation(name, args) ], "multipart/form-data") request.add_field "Authorization", api_key end + rescue StandardError => e + alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}" + send_push_request_without_attestation(name, args) end def attest!(name) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 3f2513563ed6c8..11efca8a64d652 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -105,61 +105,76 @@ def test_execute_host def test_execute_attestation omit if RUBY_ENGINE == "jruby" - @response = "Successfully registered gem: freewill (1.0.0)" - @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + ENV["GITHUB_ACTIONS"] = "true" + begin + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - File.write("#{@path}.sigstore.json", "attestation") - @cmd.options[:args] = [@path] - @cmd.options[:attestations] = ["#{@path}.sigstore.json"] + File.write("#{@path}.sigstore.json", "attestation") + @cmd.options[:args] = [@path] + @cmd.options[:attestations] = ["#{@path}.sigstore.json"] - @cmd.execute + @cmd.execute - assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class - content_length = @fetcher.last_request["Content-Length"].to_i - assert_equal content_length, @fetcher.last_request.body.length - assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + content_length = @fetcher.last_request["Content-Length"].to_i + assert_equal content_length, @fetcher.last_request.body.length + assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") + ensure + ENV.delete("GITHUB_ACTIONS") + end end def test_execute_attestation_auto omit if RUBY_ENGINE == "jruby" - @response = "Successfully registered gem: freewill (1.0.0)" - @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + ENV["GITHUB_ACTIONS"] = "true" + begin + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - attestation_path = "#{@path}.sigstore.json" - attestation_content = "auto-attestation" - File.write(attestation_path, attestation_content) - @cmd.options[:args] = [@path] + attestation_path = "#{@path}.sigstore.json" + attestation_content = "auto-attestation" + File.write(attestation_path, attestation_content) + @cmd.options[:args] = [@path] - @cmd.stub(:attest!, attestation_path) do - @cmd.execute - end + @cmd.stub(:attest!, attestation_path) do + @cmd.execute + end - assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class - content_length = @fetcher.last_request["Content-Length"].to_i - assert_equal content_length, @fetcher.last_request.body.length - assert_attestation_multipart attestation_content + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + content_length = @fetcher.last_request["Content-Length"].to_i + assert_equal content_length, @fetcher.last_request.body.length + assert_attestation_multipart attestation_content + ensure + ENV.delete("GITHUB_ACTIONS") + end end def test_execute_attestation_fallback omit if RUBY_ENGINE == "jruby" - @response = "Successfully registered gem: freewill (1.0.0)" - @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + ENV["GITHUB_ACTIONS"] = "true" + begin + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - @cmd.options[:args] = [@path] + @cmd.options[:args] = [@path] - @cmd.stub(:attest!, proc { raise Gem::Exception, "boom" }) do - use_ui @ui do - @cmd.execute + @cmd.stub(:attest!, proc { raise Gem::Exception, "boom" }) do + use_ui @ui do + @cmd.execute + end end - end - assert_match "Failed to push with attestation, retrying without attestation.", @ui.error - assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class - assert_equal Gem.read_binary(@path), @fetcher.last_request.body - assert_equal "application/octet-stream", - @fetcher.last_request["Content-Type"] + assert_match "Failed to push with attestation, retrying without attestation.", @ui.error + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + assert_equal Gem.read_binary(@path), @fetcher.last_request.body + assert_equal "application/octet-stream", + @fetcher.last_request["Content-Type"] + ensure + ENV.delete("GITHUB_ACTIONS") + end end def test_execute_attestation_skipped_on_non_rubygems_host From 8047213452983afddc821f02eec5ebfd4fae19b4 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 1 Apr 2026 10:15:12 +0900 Subject: [PATCH 15/21] [ruby/rubygems] Honor explicit --attestation option regardless of platform and host The send_push_request method previously skipped all attestation handling on JRuby, non-rubygems.org hosts, or outside GitHub Actions. This meant that even when a user explicitly passed --attestation with a local sigstore bundle, the attestation was silently ignored. Now we check options[:attestations] first and always use them when provided, only gating the auto-attestation path behind the platform/host/CI checks. https://github.com/ruby/rubygems/commit/0178a0dc56 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/commands/push_command.rb | 5 ++-- .../test_gem_commands_push_command.rb | 27 +++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 62dbd14e0bb2f3..3c067366bd97ca 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -92,8 +92,9 @@ def send_gem(name) private def send_push_request(name, args) - # Attestation is only supported on rubygems.org with GitHub Actions (not JRuby) - if RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"] + # Always honor explicit --attestation option + # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby) + if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"]) send_push_request_with_attestation(name, args) else send_push_request_without_attestation(name, args) diff --git a/test/rubygems/test_gem_commands_push_command.rb b/test/rubygems/test_gem_commands_push_command.rb index 11efca8a64d652..186ef68701e25b 100644 --- a/test/rubygems/test_gem_commands_push_command.rb +++ b/test/rubygems/test_gem_commands_push_command.rb @@ -103,26 +103,19 @@ def test_execute_host end def test_execute_attestation - omit if RUBY_ENGINE == "jruby" - - ENV["GITHUB_ACTIONS"] = "true" - begin - @response = "Successfully registered gem: freewill (1.0.0)" - @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") + @response = "Successfully registered gem: freewill (1.0.0)" + @fetcher.data["#{Gem.host}/api/v1/gems"] = HTTPResponseFactory.create(body: @response, code: 200, msg: "OK") - File.write("#{@path}.sigstore.json", "attestation") - @cmd.options[:args] = [@path] - @cmd.options[:attestations] = ["#{@path}.sigstore.json"] + File.write("#{@path}.sigstore.json", "attestation") + @cmd.options[:args] = [@path] + @cmd.options[:attestations] = ["#{@path}.sigstore.json"] - @cmd.execute + @cmd.execute - assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class - content_length = @fetcher.last_request["Content-Length"].to_i - assert_equal content_length, @fetcher.last_request.body.length - assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") - ensure - ENV.delete("GITHUB_ACTIONS") - end + assert_equal Gem::Net::HTTP::Post, @fetcher.last_request.class + content_length = @fetcher.last_request["Content-Length"].to_i + assert_equal content_length, @fetcher.last_request.body.length + assert_attestation_multipart Gem.read_binary("#{@path}.sigstore.json") end def test_execute_attestation_auto From b9358e9f359d71928771b286afdd9da8a6884d78 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 1 Apr 2026 10:15:34 +0900 Subject: [PATCH 16/21] [ruby/rubygems] Show only error message on attestation failure unless verbose When auto-attestation fails, the warning previously included e.full_message with a full backtrace that could be noisy and expose local paths. Now only e.message is shown by default, and the full backtrace is included only when Gem.configuration.really_verbose is set. https://github.com/ruby/rubygems/commit/4a4d9b8911 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/commands/push_command.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 3c067366bd97ca..7d3748a7637289 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -135,7 +135,13 @@ def send_push_request_with_attestation(name, args) request.add_field "Authorization", api_key end rescue StandardError => e - alert_warning "Failed to push with attestation, retrying without attestation.\n#{e.full_message}" + message = "Failed to push with attestation, retrying without attestation.\n" + message += if Gem.configuration.really_verbose + e.full_message + else + e.message + end + alert_warning message send_push_request_without_attestation(name, args) end From a27f303feb852a1fec1be5c4950ee10a73fb8c18 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 1 Apr 2026 10:15:58 +0900 Subject: [PATCH 17/21] [ruby/rubygems] Normalize host comparison in attestation_supported_host? The method compared against a hardcoded string, so a trailing slash (e.g. "https://rubygems.org/") would cause auto-attestation to be silently skipped. Now we chomp the trailing slash and compare against Gem::DEFAULT_HOST to be consistent with the rest of the codebase. https://github.com/ruby/rubygems/commit/a4fa24c8cf Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/commands/push_command.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 7d3748a7637289..7a01e5bd457906 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -179,6 +179,7 @@ def get_push_scope end def attestation_supported_host? - (@host || Gem.host) == "https://rubygems.org" + host = (@host || Gem.host).to_s.chomp("/") + host == Gem::DEFAULT_HOST end end From 3f1acedb62a0dbb2509e05ea314f6cd3383247b6 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 1 Apr 2026 10:19:22 +0900 Subject: [PATCH 18/21] [ruby/rubygems] Use gem exec --conservative for sigstore-cli instead of pinning version Hardcoding sigstore-cli:0.2.2 meant rubygems would need a new release whenever sigstore-cli ships a new version. Using --conservative lets gem exec prefer an already-installed version and falls back to the latest when sigstore-cli is not yet installed. https://github.com/ruby/rubygems/commit/6ac00f34de Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rubygems/commands/push_command.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 7a01e5bd457906..02931b30252a7b 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -156,8 +156,8 @@ def attest!(name) env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h out, st = Open3.capture2e( env, - Gem.ruby, "-S", "gem", "exec", - "sigstore-cli:0.2.2", "sign", name, "--bundle", bundle, + Gem.ruby, "-S", "gem", "exec", "--conservative", + "sigstore-cli", "sign", name, "--bundle", bundle, unsetenv_others: true ) raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? From 9a2ffd8e5448e0c2ac4ae58d0ef02afc195f2cb8 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Tue, 24 Mar 2026 12:25:24 +0100 Subject: [PATCH 19/21] Fix sending backtraces across ractors [Bug #21818] Currently exceptions can be sent across ractors, but because of a limitation in the TypedData API, the exception backtrace is duped as an empty backtrace. The problem is that backtraces are embedded objects, hence the classic `rb_class_alloc(klass)` API is insufficient because we need to know the size of the Backtrace object we're duping to instantiate the copy. This is worked around by changing Ractors to call `#clone` on objects rather than use `rb_obj_clone`, and to implement `Thread::Backtrace#clone` to properly clone the variable size object. --- bootstraptest/test_ractor.rb | 2 +- defs/id.def | 1 + depend | 1 + ractor.c | 30 +++++++++++++--- template/id.c.tmpl | 2 +- test/ruby/test_ractor.rb | 53 +++++++++++++++++++++++++++- vm_backtrace.c | 68 ++++++++++++++++++++++++++++++------ 7 files changed, 140 insertions(+), 17 deletions(-) diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 040943a0b94fc8..987f146dc68ebb 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -507,7 +507,7 @@ def test n } # To copy the object, now Marshal#dump is used -assert_match /can not copy unshareable object/, %q{ +assert_match /can't clone unshareable instance of Thread/, %q{ obj = Thread.new{} begin r = Ractor.new obj do |msg| diff --git a/defs/id.def b/defs/id.def index cd3a8480a241df..344b072e7615a3 100644 --- a/defs/id.def +++ b/defs/id.def @@ -28,6 +28,7 @@ firstline, predefined = __LINE__+1, %[\ send __send__ __recursive_key__ + clone initialize initialize_copy initialize_clone diff --git a/depend b/depend index eef68cd3fcb09d..f0cb82823fe1d8 100644 --- a/depend +++ b/depend @@ -19980,6 +19980,7 @@ vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/compilers.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/error.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/gc.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/imemo.h +vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/object.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/sanitizers.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/serial.h vm_backtrace.$(OBJEXT): $(top_srcdir)/internal/set_table.h diff --git a/ractor.c b/ractor.c index 4726cf107bfb03..cad9f56e4a312c 100644 --- a/ractor.c +++ b/ractor.c @@ -2066,6 +2066,31 @@ ractor_move(VALUE obj) } } +static VALUE +ractor_call_clone_try(VALUE obj) +{ + return rb_funcall(obj, idClone, 0); +} + +static VALUE +ractor_call_clone_rescue(VALUE obj, VALUE exc) +{ + rb_raise(rb_eRactorError, "can't clone unshareable instance of %"PRIsVALUE, rb_class_of(obj)); + UNREACHABLE_RETURN(Qnil); +} + +static VALUE +ractor_obj_clone(VALUE obj) +{ + VALUE clone = rb_rescue(ractor_call_clone_try, obj, ractor_call_clone_rescue, obj); + + if (obj == clone) { + rb_raise(rb_eRactorError, "#clone returned self"); + } + + return clone; +} + static enum obj_traverse_iterator_result copy_enter(VALUE obj, struct obj_traverse_replace_data *data) { @@ -2074,10 +2099,7 @@ copy_enter(VALUE obj, struct obj_traverse_replace_data *data) return traverse_skip; } else { - if (!rb_get_alloc_func(rb_obj_class(obj))) { - rb_raise(rb_eRactorError, "can not copy unshareable object %+"PRIsVALUE, obj); - } - data->replacement = rb_obj_clone(obj); + data->replacement = ractor_obj_clone(obj); return traverse_cont; } } diff --git a/template/id.c.tmpl b/template/id.c.tmpl index 5aa8e47ce7372b..d40f1430654bee 100644 --- a/template/id.c.tmpl +++ b/template/id.c.tmpl @@ -1,5 +1,5 @@ %# -*- c -*- -/* DO NOT EDIT THIS FILE DIRECTLY */ +/* DO NOT EDIT THIS FILE DIRECTLY: source is at template/id.c.tmpl */ /********************************************************************** id.c - diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 4aeda424598f13..f4957d6fdf193c 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -50,6 +50,57 @@ def x.to_s assert_unshareable(x, "can not make shareable object for # because it refers unshareable objects", exception: Ractor::Error) end + def test_sending_exception_with_backtrace + assert_ractor(<<~'RUBY') + def build_error + raise "Test" + rescue => error + error + end + + error = build_error + refute_empty error.backtrace + refute_empty error.backtrace_locations + + backtrace, backtrace_locations = Ractor.new(error) do |error2| + [error2.backtrace, error2.backtrace_locations] + end.value + + assert_equal error.backtrace, backtrace + refute_empty backtrace_locations + RUBY + end + + def test_sending_exception_with_array_backtrace + assert_ractor(<<~'RUBY') + error = StandardError.new + error.set_backtrace(["foo", "bar"]) + refute_empty error.backtrace + assert_nil error.backtrace_locations + + backtrace, backtrace_locations = Ractor.new(error) do |error2| + [error2.backtrace, error2.backtrace_locations] + end.value + + assert_equal error.backtrace, backtrace + assert_nil backtrace_locations + RUBY + end + + def test_sending_object_with_broken_clone + assert_ractor(<<~'RUBY') + o = Object.new + def o.clone + self + end + ractor = Ractor.new { Ractor.receive } + error = assert_raise Ractor::Error do + ractor.send(o) + end + assert_match "#clone returned self", error.message + RUBY + end + def test_default_thread_group assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -219,7 +270,7 @@ def test_copy_unshareable_object_error_message err = assert_raise(Ractor::Error) do Ractor.new(pr) {}.join end - assert_match(/can not copy unshareable object/, err.message) + assert_match(/can not copy Proc object/, err.message) RUBY end diff --git a/vm_backtrace.c b/vm_backtrace.c index c0bc46b8caf5c7..af5cf219fbeaaa 100644 --- a/vm_backtrace.c +++ b/vm_backtrace.c @@ -13,6 +13,7 @@ #include "internal.h" #include "internal/class.h" #include "internal/error.h" +#include "internal/object.h" #include "internal/vm.h" #include "iseq.h" #include "ruby/debug.h" @@ -576,14 +577,6 @@ rb_backtrace_p(VALUE obj) return rb_typeddata_is_kind_of(obj, &backtrace_data_type); } -static VALUE -backtrace_alloc(VALUE klass) -{ - rb_backtrace_t *bt; - VALUE obj = TypedData_Make_Struct(klass, rb_backtrace_t, &backtrace_data_type, bt); - return obj; -} - static VALUE backtrace_alloc_capa(long num_frames, rb_backtrace_t **backtrace) { @@ -962,6 +955,46 @@ backtrace_limit(VALUE self) return LONG2NUM(rb_backtrace_length_limit); } +/* :nodoc: */ +static VALUE +backtrace_clone(VALUE self) +{ + rb_backtrace_t *bt; + TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt); + + rb_backtrace_t *other_bt; + VALUE clone = backtrace_alloc_capa(bt->backtrace_size, &other_bt); + + rb_obj_clone_setup(self, clone, Qfalse); + + return clone; +} + +/* :nodoc: */ +static VALUE +backtrace_dup(VALUE self) +{ + rb_notimplement(); + + UNREACHABLE_RETURN(Qnil); +} + +/* :nodoc: */ +static VALUE +backtrace_initialize_copy(VALUE self, VALUE original) +{ + rb_backtrace_t *bt; + TypedData_Get_Struct(self, rb_backtrace_t, &backtrace_data_type, bt); + + rb_backtrace_t *original_bt; + TypedData_Get_Struct(original, rb_backtrace_t, &backtrace_data_type, original_bt); + + bt->backtrace_size = original_bt->backtrace_size; + MEMCPY(bt->backtrace, original_bt->backtrace, rb_backtrace_location_t, original_bt->backtrace_size); + + return Qnil; +} + VALUE rb_ec_backtrace_str_ary(const rb_execution_context_t *ec, long lev, long n) { @@ -1406,6 +1439,14 @@ each_caller_location(int argc, VALUE *argv, VALUE _) return Qnil; } +static VALUE +backtrace_no_allocator(VALUE klass) +{ + rb_notimplement(); + + UNREACHABLE_RETURN(Qnil); +} + /* called from Init_vm() in vm.c */ void Init_vm_backtrace(void) @@ -1416,11 +1457,18 @@ Init_vm_backtrace(void) * settings of the current session. */ rb_cBacktrace = rb_define_class_under(rb_cThread, "Backtrace", rb_cObject); - rb_define_alloc_func(rb_cBacktrace, backtrace_alloc); - rb_undef_method(CLASS_OF(rb_cBacktrace), "new"); + + // Can't undefine the allocator, as it's needed as a key by Marshal + rb_define_alloc_func(rb_cBacktrace, backtrace_no_allocator); rb_marshal_define_compat(rb_cBacktrace, rb_cArray, backtrace_dump_data, backtrace_load_data); + + rb_undef_method(CLASS_OF(rb_cBacktrace), "new"); rb_define_singleton_method(rb_cBacktrace, "limit", backtrace_limit, 0); + rb_define_method(rb_cBacktrace, "clone", backtrace_clone, 0); + rb_define_method(rb_cBacktrace, "dup", backtrace_dup, 0); + rb_define_method(rb_cBacktrace, "initialize_copy", backtrace_initialize_copy, 1); + /* * An object representation of a stack frame, initialized by * Kernel#caller_locations. From ce398d7545f04bd8b81a908f5ae905ba290c13b9 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:00:37 +0200 Subject: [PATCH 20/21] [ruby/prism] Remove a warning from `discarded-qualifiers` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I've been seeing this for a while now: ``` ../../../../ext/prism/../../src/memchr.c: In function ‘pm_memchr’: ../../../../ext/prism/../../src/memchr.c:35:16: warning: return discards ‘const’ qualifier from pointer target type [-Wdiscarded-qualifiers] 35 | return memchr(memory, character, number); | ^~~~~~ ``` https://github.com/ruby/prism/commit/a3b1f10dbc --- prism/internal/memchr.h | 2 +- prism/memchr.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/prism/internal/memchr.h b/prism/internal/memchr.h index 63c738387d65d7..6f6b0bca302008 100644 --- a/prism/internal/memchr.h +++ b/prism/internal/memchr.h @@ -10,6 +10,6 @@ * we need to search for a character in a buffer that could be the trailing byte * of a multibyte character. */ -void * pm_memchr(const void *source, int character, size_t number, bool encoding_changed, const pm_encoding_t *encoding); +const void * pm_memchr(const void *source, int character, size_t number, bool encoding_changed, const pm_encoding_t *encoding); #endif diff --git a/prism/memchr.c b/prism/memchr.c index 6266d4ca7a5192..900e6245b783a3 100644 --- a/prism/memchr.c +++ b/prism/memchr.c @@ -11,7 +11,7 @@ * we need to search for a character in a buffer that could be the trailing byte * of a multibyte character. */ -void * +const void * pm_memchr(const void *memory, int character, size_t number, bool encoding_changed, const pm_encoding_t *encoding) { if (encoding_changed && encoding->multibyte && character >= TRAILING_BYTE_MINIMUM) { const uint8_t *source = (const uint8_t *) memory; From 9224d90f125ab8b860558aeda008e3c696d1ee11 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 1 Apr 2026 20:34:42 +0900 Subject: [PATCH 21/21] Refactor socket timestamp tests - Avoid unnecessarily binding sender sockets, as ephemeral ports can very occasionally conflict on Linux. This investigation is assisted by Claude Code. - Compare timestamps using a range instead of relying on `inspect`. - Use the `timestamp_retry_rw` helper in `test_bintime` as well. - Use `String#unpack1` with offset to extract the timestamp data. Co-authored-by: Koichi Sasada --- test/socket/test_socket.rb | 65 ++++++++++++-------------------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb index 29812c423ff2e7..0d5bad96920890 100644 --- a/test/socket/test_socket.rb +++ b/test/socket/test_socket.rb @@ -485,9 +485,11 @@ def test_linger } end - def timestamp_retry_rw(s1, s2, t1, type) + def timestamp_retry_rw(s1, type) IO.pipe do |r,w| + t1 = Time.now # UDP may not be reliable, keep sending until recvmsg returns: + s2 = Socket.new(:INET, :DGRAM, 0) th = Thread.new do n = 0 begin @@ -500,80 +502,53 @@ def timestamp_retry_rw(s1, s2, t1, type) assert_equal([[s1],[],[]], IO.select([s1], nil, nil, timeout)) msg, _, _, stamp = s1.recvmsg assert_equal("a", msg) - assert(stamp.cmsg_is?(:SOCKET, type)) + assert_send([stamp, :cmsg_is?, :SOCKET, type]) w.close # stop th n = th.value th = nil n > 1 and warn "UDP packet loss for #{type} over loopback, #{n} tries needed" - t2 = Time.now.strftime("%Y-%m-%d") - pat = Regexp.union([t1, t2].uniq) - assert_match(pat, stamp.inspect) - t = stamp.timestamp - assert_match(pat, t.strftime("%Y-%m-%d")) + t2 = Time.now + assert_include(t1..t2, stamp.timestamp) stamp ensure if th and !th.join(10) th.kill.join(10) end + s2.close end end def test_timestamp return if /linux|freebsd|netbsd|openbsd|darwin/ !~ RUBY_PLATFORM return if !defined?(Socket::AncillaryData) || !defined?(Socket::SO_TIMESTAMP) - t1 = Time.now.strftime("%Y-%m-%d") - stamp = nil Addrinfo.udp("127.0.0.1", 0).bind {|s1| - Addrinfo.udp("127.0.0.1", 0).bind {|s2| - s1.setsockopt(:SOCKET, :TIMESTAMP, true) - stamp = timestamp_retry_rw(s1, s2, t1, :TIMESTAMP) - } + s1.setsockopt(:SOCKET, :TIMESTAMP, true) + timestamp_retry_rw(s1, :TIMESTAMP) } - t = stamp.timestamp - pat = /\.#{"%06d" % t.usec}/ - assert_match(pat, stamp.inspect) end def test_timestampns return if /linux/ !~ RUBY_PLATFORM || !defined?(Socket::SO_TIMESTAMPNS) - t1 = Time.now.strftime("%Y-%m-%d") - stamp = nil Addrinfo.udp("127.0.0.1", 0).bind {|s1| - Addrinfo.udp("127.0.0.1", 0).bind {|s2| - begin - s1.setsockopt(:SOCKET, :TIMESTAMPNS, true) - rescue Errno::ENOPROTOOPT - # SO_TIMESTAMPNS is available since Linux 2.6.22 - return - end - stamp = timestamp_retry_rw(s1, s2, t1, :TIMESTAMPNS) - } + begin + s1.setsockopt(:SOCKET, :TIMESTAMPNS, true) + rescue Errno::ENOPROTOOPT + # SO_TIMESTAMPNS is available since Linux 2.6.22 + return + end + timestamp_retry_rw(s1, :TIMESTAMPNS) } - t = stamp.timestamp - pat = /\.#{"%09d" % t.nsec}/ - assert_match(pat, stamp.inspect) end def test_bintime return if /freebsd/ !~ RUBY_PLATFORM - t1 = Time.now.strftime("%Y-%m-%d") - stamp = nil - Addrinfo.udp("127.0.0.1", 0).bind {|s1| - Addrinfo.udp("127.0.0.1", 0).bind {|s2| - s1.setsockopt(:SOCKET, :BINTIME, true) - s2.send "a", 0, s1.local_address - msg, _, _, stamp = s1.recvmsg - assert_equal("a", msg) - assert(stamp.cmsg_is?(:SOCKET, :BINTIME)) - } + stamp = Addrinfo.udp("127.0.0.1", 0).bind {|s1| + s1.setsockopt(:SOCKET, :BINTIME, true) + timestamp_retry_rw(s1, :BINTIME) } - t2 = Time.now.strftime("%Y-%m-%d") - pat = Regexp.union([t1, t2].uniq) - assert_match(pat, stamp.inspect) t = stamp.timestamp - assert_match(pat, t.strftime("%Y-%m-%d")) - assert_equal(stamp.data[-8,8].unpack("Q")[0], t.subsec * 2**64) + assert_equal(stamp.data.unpack1("Q", offset: -8), t.subsec * 2**64) end def test_closed_read