From f5ef758a756b325208c9d052326bb64239e4b1aa Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:42:25 +0000 Subject: [PATCH 1/9] thread: a new Ractor must not inherit the creating thread's fiber storage thread_create_core inherited fiber storage unconditionally, so a new Ractor's main fiber received the creating thread's fiber-local storage, whose entries may be objects owned by the creating Ractor. Skip the inheritance for Ractor main threads. Co-Authored-By: Claude Opus 4.8 (1M context) --- thread.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/thread.c b/thread.c index 2a89582a274e15..0d58e75760383a 100644 --- a/thread.c +++ b/thread.c @@ -845,7 +845,12 @@ thread_create_core(VALUE thval, struct thread_create_params *params) "can't start a new thread (frozen ThreadGroup)"); } - rb_fiber_inherit_storage(ec, th->ec->fiber_ptr); + /* A new Ractor must not inherit the creating thread's fiber storage: its + * entries may be objects owned by the creating Ractor. Only threads created + * within the same Ractor inherit it. */ + if (params->type != thread_invoke_type_ractor_proc) { + rb_fiber_inherit_storage(ec, th->ec->fiber_ptr); + } switch (params->type) { case thread_invoke_type_proc: From ff96f9dc1cd7074f0ff26d7955ec201bc298297c Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:47:37 +0000 Subject: [PATCH 2/9] thread: build a new Ractor's interrupt queue on its own main thread Defer creating a Ractor main thread's pending-interrupt queue and mask stack from thread_create_core (the creating thread) to thread_start_func_2 (the new Ractor's own main thread). The mask stack starts empty rather than duplicating the creating thread's, so a new Ractor does not inherit its Thread.handle_interrupt state. Co-Authored-By: Claude Opus 4.8 (1M context) --- thread.c | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/thread.c b/thread.c index 0d58e75760383a..22f0fe8e0f73de 100644 --- a/thread.c +++ b/thread.c @@ -680,6 +680,13 @@ thread_start_func_2(rb_thread_t *th, VALUE *stack_start) r->r_stdin = rb_io_prep_stdin(); r->r_stdout = rb_io_prep_stdout(); r->r_stderr = rb_io_prep_stderr(); + + /* Build the interrupt queue and mask stack here, on the new Ractor's + * own main thread, instead of carrying over the ones the creating + * thread made. The mask stack starts empty so a new Ractor does not + * inherit the creating thread's Thread.handle_interrupt state. */ + th->pending_interrupt_queue = rb_ary_hidden_new(0); + th->pending_interrupt_mask_stack = rb_ary_hidden_new(0); } RB_VM_UNLOCK(); } @@ -887,10 +894,19 @@ thread_create_core(VALUE thval, struct thread_create_params *params) th->priority = current_th->priority; th->thgroup = current_th->thgroup; - th->pending_interrupt_queue = rb_ary_hidden_new(0); - th->pending_interrupt_queue_checked = 0; - th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack); - RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack); + if (th->invoke_type == thread_invoke_type_ractor_proc) { + /* A new Ractor's main thread builds these on start + * (thread_start_func_2); leave them unset until then. */ + th->pending_interrupt_queue = 0; + th->pending_interrupt_mask_stack = 0; + th->pending_interrupt_queue_checked = 0; + } + else { + th->pending_interrupt_queue = rb_ary_hidden_new(0); + th->pending_interrupt_queue_checked = 0; + th->pending_interrupt_mask_stack = rb_ary_dup(current_th->pending_interrupt_mask_stack); + RBASIC_CLEAR_CLASS(th->pending_interrupt_mask_stack); + } rb_native_mutex_initialize(&th->interrupt_lock); From 64cf605f6978ddc5701e9ef45db76003c882cec1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 02:09:21 +0000 Subject: [PATCH 3/9] Bump the github-actions group across 1 directory with 5 updates Bumps the github-actions group with 5 updates in the / directory: | Package | From | To | | --- | --- | --- | | [ruby/setup-ruby](https://github.com/ruby/setup-ruby) | `1.315.0` | `1.316.0` | | [github/codeql-action/init](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) | `4.36.2` | `4.36.3` | | [lewagon/wait-on-check-action](https://github.com/lewagon/wait-on-check-action) | `1.8.0` | `1.8.1` | Updates `ruby/setup-ruby` from 1.315.0 to 1.316.0 - [Release notes](https://github.com/ruby/setup-ruby/releases) - [Changelog](https://github.com/ruby/setup-ruby/blob/master/release.rb) - [Commits](https://github.com/ruby/setup-ruby/compare/0dafeac902942906541bc140009cdbf32665b601...d45b1a4e94b71acab930e56e79c6aa188764e7f9) Updates `github/codeql-action/init` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `github/codeql-action/analyze` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `github/codeql-action/upload-sarif` from 4.36.2 to 4.36.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/8aad20d150bbac5944a9f9d289da16a4b0d87c1e...54f647b7e1bb85c95cddabcd46b0c578ec92bc1a) Updates `lewagon/wait-on-check-action` from 1.8.0 to 1.8.1 - [Release notes](https://github.com/lewagon/wait-on-check-action/releases) - [Changelog](https://github.com/lewagon/wait-on-check-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/lewagon/wait-on-check-action/compare/96d9100b431964d10e0136aff8b9ccb92470505e...1d57e2c51a58d812d2765e036a028b6bdb5a6154) --- updated-dependencies: - dependency-name: ruby/setup-ruby dependency-version: 1.316.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/init dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.36.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: lewagon/wait-on-check-action dependency-version: 1.8.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/annocheck.yml | 2 +- .github/workflows/auto_review_pr.yml | 2 +- .github/workflows/baseruby.yml | 2 +- .github/workflows/bundled_gems.yml | 2 +- .github/workflows/check_dependencies.yml | 2 +- .github/workflows/check_misc.yml | 2 +- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/dependabot_automerge.yml | 2 +- .github/workflows/modgc.yml | 2 +- .github/workflows/parse_y.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/spec_guards.yml | 2 +- .github/workflows/sync_default_gems.yml | 2 +- .github/workflows/tarball-ubuntu.yml | 2 +- .github/workflows/tarball-windows.yml | 2 +- .github/workflows/ubuntu.yml | 2 +- .github/workflows/wasm.yml | 2 +- .github/workflows/windows.yml | 2 +- .github/workflows/yjit-ubuntu.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 21 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/annocheck.yml b/.github/workflows/annocheck.yml index a7363e3a9ab39f..2c1d35899fb722 100644 --- a/.github/workflows/annocheck.yml +++ b/.github/workflows/annocheck.yml @@ -73,7 +73,7 @@ jobs: builddir: build makeup: true - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/auto_review_pr.yml b/.github/workflows/auto_review_pr.yml index bcbdec54dae635..72611269a40eca 100644 --- a/.github/workflows/auto_review_pr.yml +++ b/.github/workflows/auto_review_pr.yml @@ -29,7 +29,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/baseruby.yml b/.github/workflows/baseruby.yml index 27f98d2d50479a..695c468153be36 100644 --- a/.github/workflows/baseruby.yml +++ b/.github/workflows/baseruby.yml @@ -48,7 +48,7 @@ jobs: - ruby-3.3 steps: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/bundled_gems.yml b/.github/workflows/bundled_gems.yml index fe485d2bfbfa10..464a44dfceb9dd 100644 --- a/.github/workflows/bundled_gems.yml +++ b/.github/workflows/bundled_gems.yml @@ -38,7 +38,7 @@ jobs: with: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: 4.0 diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 6bd21df6faa1aa..56eea4001fd274 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -42,7 +42,7 @@ jobs: - uses: ./.github/actions/setup/directories - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/check_misc.yml b/.github/workflows/check_misc.yml index 0879d72b24ed39..55238ffba504d9 100644 --- a/.github/workflows/check_misc.yml +++ b/.github/workflows/check_misc.yml @@ -23,7 +23,7 @@ jobs: token: ${{ (github.repository == 'ruby/ruby' && !startsWith(github.event_name, 'pull')) && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: head diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 75b0c7f896a69d..537432bd407920 100644 --- a/.github/workflows/check_sast.yml +++ b/.github/workflows/check_sast.yml @@ -78,14 +78,14 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: category: '/language:${{ matrix.language }}' upload: False @@ -127,7 +127,7 @@ jobs: continue-on-error: true - name: Upload SARIF - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/dependabot_automerge.yml b/.github/workflows/dependabot_automerge.yml index de408c97cbeb47..b331f888279bc0 100644 --- a/.github/workflows/dependabot_automerge.yml +++ b/.github/workflows/dependabot_automerge.yml @@ -17,7 +17,7 @@ jobs: id: metadata - name: Wait for status checks - uses: lewagon/wait-on-check-action@96d9100b431964d10e0136aff8b9ccb92470505e # v1.8.0 + uses: lewagon/wait-on-check-action@1d57e2c51a58d812d2765e036a028b6bdb5a6154 # v1.8.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha || github.sha }} diff --git a/.github/workflows/modgc.yml b/.github/workflows/modgc.yml index 8dce73ab16134f..b0fe1c827f51a8 100644 --- a/.github/workflows/modgc.yml +++ b/.github/workflows/modgc.yml @@ -67,7 +67,7 @@ jobs: uses: ./.github/actions/setup/ubuntu if: ${{ contains(matrix.os, 'ubuntu') }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/parse_y.yml b/.github/workflows/parse_y.yml index 026fb28c9fe30b..0e51c852335355 100644 --- a/.github/workflows/parse_y.yml +++ b/.github/workflows/parse_y.yml @@ -59,7 +59,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8dc18ca9f472d5..bea2aba1012685 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -22,7 +22,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: 3.3.4 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 86a39eb0ab0d89..e63a22e5d262b9 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -73,6 +73,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 + uses: github/codeql-action/upload-sarif@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3 with: sarif_file: results.sarif diff --git a/.github/workflows/spec_guards.yml b/.github/workflows/spec_guards.yml index 8b4137f935404c..bf6b6f1d0155ec 100644 --- a/.github/workflows/spec_guards.yml +++ b/.github/workflows/spec_guards.yml @@ -49,7 +49,7 @@ jobs: with: persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: ${{ matrix.ruby }} bundler: none diff --git a/.github/workflows/sync_default_gems.yml b/.github/workflows/sync_default_gems.yml index f4f176b356c341..98957ab01acb29 100644 --- a/.github/workflows/sync_default_gems.yml +++ b/.github/workflows/sync_default_gems.yml @@ -39,7 +39,7 @@ jobs: with: token: ${{ github.repository == 'ruby/ruby' && secrets.MATZBOT_AUTO_UPDATE_TOKEN || secrets.GITHUB_TOKEN }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.4' bundler: none diff --git a/.github/workflows/tarball-ubuntu.yml b/.github/workflows/tarball-ubuntu.yml index 932ed4a8298b05..d0d4e5ba575d66 100644 --- a/.github/workflows/tarball-ubuntu.yml +++ b/.github/workflows/tarball-ubuntu.yml @@ -43,7 +43,7 @@ jobs: set -x sudo apt-get update -q sudo apt-get install --no-install-recommends -q -y build-essential libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev bison- autoconf- - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.2' # test-bundled-gems requires executable host ruby diff --git a/.github/workflows/tarball-windows.yml b/.github/workflows/tarball-windows.yml index c13ba7b0d0cc45..85fe1c073f199a 100644 --- a/.github/workflows/tarball-windows.yml +++ b/.github/workflows/tarball-windows.yml @@ -47,7 +47,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.2' bundler: none diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 1b0371bb5f314d..6518e77d72b07b 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -79,7 +79,7 @@ jobs: with: arch: ${{ matrix.arch }} - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml index 458676137a79e2..75344395bb4155 100644 --- a/.github/workflows/wasm.yml +++ b/.github/workflows/wasm.yml @@ -65,7 +65,7 @@ jobs: sparse-checkout: /.github persist-credentials: false - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 90e2a93ebe37d2..9a9c2be282a000 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -58,7 +58,7 @@ jobs: - run: md build working-directory: - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: # windows-11-arm has only 3.4.1, 3.4.2, 3.4.3, head ruby-version: ${{ !endsWith(matrix.os, 'arm') && '3.1' || '3.4' }} diff --git a/.github/workflows/yjit-ubuntu.yml b/.github/workflows/yjit-ubuntu.yml index 04740c03037ff3..793f93d6d10f44 100644 --- a/.github/workflows/yjit-ubuntu.yml +++ b/.github/workflows/yjit-ubuntu.yml @@ -138,7 +138,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 7dd8e252b79235..a38ad2f0ce5b7d 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -136,7 +136,7 @@ jobs: - uses: ./.github/actions/setup/ubuntu - - uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0 + - uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0 with: ruby-version: '3.1' bundler: none From 633c2a0defa1057d5a77f1a7a9fb1f66df112c6e Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 21:40:02 -0500 Subject: [PATCH 4/9] [DOC] Doc for Pathname#owned? --- pathname_builtin.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/pathname_builtin.rb b/pathname_builtin.rb index beaff62de2acfa..ade839e2bc94bf 100644 --- a/pathname_builtin.rb +++ b/pathname_builtin.rb @@ -1895,7 +1895,26 @@ def pipe?() FileTest.pipe?(@path) end # See FileTest.socket?. def socket?() FileTest.socket?(@path) end - # See FileTest.owned?. + # :markup: markdown + # + # call-seq: + # owned? -> true or false + # + # Returns whether the entry at the path represented by `self` + # exists and is owned by the user of the current process: + # + # ```ruby + # pn = Pathname('doc/t.tmp') + # pn.write('foo') + # pn.owned? # => true + # pn.delete + # pn = Pathname('doc/tmp') + # pn.mkdir + # pn.owned? # => true + # pn.rmdir + # Pathname('/etc').owned? # => false + # ``` + # def owned?() FileTest.owned?(@path) end # See FileTest.readable?. From bf02cf1aae65ad411ec3e56bd52bc6db51165cf2 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 2 Jul 2026 17:45:29 +0900 Subject: [PATCH 5/9] Make rb_free_vm_opt_tables static --- vm.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm.c b/vm.c index a5ab2a153013e1..c30e750d9fee00 100644 --- a/vm.c +++ b/vm.c @@ -2292,7 +2292,7 @@ short ruby_vm_redefined_flag[BOP_LAST_]; static st_table *vm_opt_method_def_table = 0; static st_table *vm_opt_mid_table = 0; -void +static void rb_free_vm_opt_tables(void) { st_free_table(vm_opt_method_def_table); From 10e53afd4e649db1fbc3367b92ba407d464fc8ca Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:53:02 +0000 Subject: [PATCH 6/9] ractor: lock the owner when Port#closed? is queried from another Ractor ractor_closed_port_p asserts the owning ractor's lock is held for foreign access and reads sync.ports via st_lookup, but Ractor::Port#closed? (ractor_port_closed_p) called it without the lock. From a foreign Ractor this tripped the assertion and raced the owner's st_insert/st_delete on the ports table. Take the ractor lock for foreign queries, like every other foreign reader. Co-Authored-By: Claude Opus 4.8 (1M context) --- ractor_sync.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/ractor_sync.c b/ractor_sync.c index 5eeb6e20db9f28..5dfb86e3bff130 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -144,13 +144,27 @@ static VALUE ractor_port_closed_p(rb_execution_context_t *ec, VALUE self) { const struct ractor_port *rp = RACTOR_PORT_PTR(self); + rb_ractor_t *r = rp->r; + bool closed; - if (ractor_closed_port_p(ec, rp->r, rp)) { - return Qtrue; + if (rb_ec_ractor_ptr(ec) == r) { + /* The owner's threads are serialized by the ractor GVL, so the ports + * table can't change under this lookup. */ + closed = ractor_closed_port_p(ec, r, rp); } else { - return Qfalse; + /* A foreign Ractor races the owner's st_insert/st_delete on the ports + * table; take the lock like every other foreign reader. ractor_closed_port_p + * asserts the lock is held for foreign access, and Port#closed? was the + * only path reaching it without the lock. */ + RACTOR_LOCK(r); + { + closed = ractor_closed_port_p(ec, r, rp); + } + RACTOR_UNLOCK(r); } + + return closed ? Qtrue : Qfalse; } static VALUE From 45633fa7b6a8f6060934d0c7e1bfbc11fb085235 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:03:52 +0000 Subject: [PATCH 7/9] object.c: initialize clone freeze-kwarg caches atomically rb_get_freeze_opt and rb_obj_clone_setup lazily initialized shared static caches (the freeze keyword id and the `{freeze: true/false}` hashes) with a plain `if (!cache)` guard. When Ractors run #clone concurrently this races: a thread can publish a half-built hash that another freezes before it is filled, later triggering "can't modify frozen Hash". Use the preinterned idFreeze directly, and publish each frozen, pinned hash with a single atomic CAS. Co-Authored-By: Claude Opus 4.8 (1M context) --- object.c | 51 +++++++++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/object.c b/object.c index cbafd558f9fc9d..0552e7cf44019a 100644 --- a/object.c +++ b/object.c @@ -453,13 +453,13 @@ rb_immutable_obj_clone(int argc, VALUE *argv, VALUE obj) VALUE rb_get_freeze_opt(int argc, VALUE *argv) { - static ID keyword_ids[1]; + /* idFreeze (== :freeze) is preinterned before any Ruby code runs, so use it + * directly instead of lazily initializing a shared static, which races when + * Ractors run this concurrently. */ + const ID keyword_ids[1] = { idFreeze }; VALUE opt; VALUE kwfreeze = Qnil; - if (!keyword_ids[0]) { - CONST_ID(keyword_ids[0], "freeze"); - } rb_scan_args(argc, argv, "0:", &opt); if (!NIL_P(opt)) { rb_get_kwargs(opt, keyword_ids, 0, 1, &kwfreeze); @@ -478,6 +478,29 @@ immutable_obj_clone(VALUE obj, VALUE kwfreeze) return obj; } +/* Cache of the `{freeze: true/false}` keyword hash passed to #initialize_clone. + * Ractors may reach this concurrently, so build a fully populated, frozen and + * pinned hash locally and publish it with a single atomic CAS: any value another + * thread can observe in the static is already complete, and a builder that loses + * the CAS just discards its hash. (The old lazy init published an empty hash that + * a second thread could read and freeze before the first finished filling it.) */ +static VALUE freeze_true_hash, freeze_false_hash; + +static VALUE +clone_freeze_kwarg_hash(VALUE *cache, VALUE freeze_value) +{ + VALUE h = RUBY_ATOMIC_VALUE_LOAD(*cache); + if (!h) { + h = rb_hash_new(); + rb_hash_aset(h, ID2SYM(idFreeze), freeze_value); + rb_obj_freeze(h); + rb_vm_register_global_object(h); /* pin before publishing */ + VALUE prev = RUBY_ATOMIC_VALUE_CAS(*cache, 0, h); + if (prev) h = prev; /* lost the race; our h becomes garbage */ + } + return h; +} + VALUE rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze) { @@ -506,31 +529,15 @@ rb_obj_clone_setup(VALUE obj, VALUE clone, VALUE kwfreeze) } break; case Qtrue: { - static VALUE freeze_true_hash; - if (!freeze_true_hash) { - freeze_true_hash = rb_hash_new(); - rb_vm_register_global_object(freeze_true_hash); - rb_hash_aset(freeze_true_hash, ID2SYM(idFreeze), Qtrue); - rb_obj_freeze(freeze_true_hash); - } - argv[0] = obj; - argv[1] = freeze_true_hash; + argv[1] = clone_freeze_kwarg_hash(&freeze_true_hash, Qtrue); rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS); OBJ_FREEZE(clone); break; } case Qfalse: { - static VALUE freeze_false_hash; - if (!freeze_false_hash) { - freeze_false_hash = rb_hash_new(); - rb_vm_register_global_object(freeze_false_hash); - rb_hash_aset(freeze_false_hash, ID2SYM(idFreeze), Qfalse); - rb_obj_freeze(freeze_false_hash); - } - argv[0] = obj; - argv[1] = freeze_false_hash; + argv[1] = clone_freeze_kwarg_hash(&freeze_false_hash, Qfalse); rb_funcallv_kw(clone, id_init_clone, 2, argv, RB_PASS_KEYWORDS); break; } From 5b5d1fb5e970fbaae800fb9e450eb1b188009d02 Mon Sep 17 00:00:00 2001 From: TAKANO Mitsuhiro Date: Thu, 2 Jul 2026 10:25:02 +0900 Subject: [PATCH 8/9] Re-enable DTrace on FreeBSD Remove the unconditional target_os=freebsd* override that forced rb_cv_dtrace_available=no, which was added in commit 78677f105d ("Disable DTrace in FreeBSD (#3999)") as a Ruby 3.0.0 release unblocker for the build failure reported in Bug #17212. That failure was a link error at the DTrace glommed/static-library stage ("attempted static link of dynamic object libgcc_s.so" on FreeBSD 12.x, "cannot find -lgcc_s" on 11.x), not an exec-stack or relocation problem. On modern FreeBSD (14.4, dtrace Sun D 1.13, clang 19 / lld) that stage links cleanly, so the override is obsolete. With it removed, FreeBSD goes through the normal RUBY_DTRACE_AVAILABLE detection. The rest of the FreeBSD DTrace machinery is already in place (-xnolibs autodetect, -lelf link, and the dtrace -G rebuild/glommed-object path), so no other change is needed. Verified on FreeBSD 14.4/amd64: a --enable-dtrace build succeeds; the ruby binary gets a .SUNW_dof section with the provider "ruby" and all 22 ruby::: USDT probes; and the probes register and fire under a live dtrace(1) (e.g. gc-mark-begin, gc-sweep-end, method-entry). Co-Authored-By: Claude Opus 4.8 (1M context) --- configure.ac | 3 --- 1 file changed, 3 deletions(-) diff --git a/configure.ac b/configure.ac index 8d020771c6d505..562d9262f837f1 100644 --- a/configure.ac +++ b/configure.ac @@ -3820,9 +3820,6 @@ AS_CASE(["${enable_dtrace}"], ], [ rb_cv_dtrace_available=no ]) -AS_CASE(["$target_os"],[freebsd*],[ - rb_cv_dtrace_available=no - ]) AS_IF([test "${enable_dtrace}" = yes], [dnl AS_IF([test -z "$DTRACE"], [dnl AC_MSG_ERROR([dtrace(1) is missing]) From 508b486c38fe083e61675c6f10231ce971618b44 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 3 Jul 2026 14:32:29 +0900 Subject: [PATCH 9/9] Group test-bundler-parallel workers by recorded runtime (#17647) * Group test-bundler-parallel workers by recorded runtime turbo_tests balances workers by a recorded runtime log only when invoked as a bare `spec`; this build passes an absolute spec/bundler path, so it grouped by file size and let the single heaviest file dominate the wall-clock. A tool/ patch loaded from the recipe records per-file runtimes and reorders the size-based grouper to use the previous run's data, leaving the synced spec/bin/parallel_rspec untouched. Co-Authored-By: Claude Fable 5 * Fix stale reference in runtime recorder comment The launcher script it pointed at was replaced by the recipe calling BundlerRuntimeGrouping.install! directly. Co-Authored-By: Claude Fable 5 * Rescue LoadError when installing runtime grouping install! is meant to degrade to filesize grouping on any failure, but LoadError from the parallel_tests require is a ScriptError and escaped the StandardError rescue, killing the whole recipe instead. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- common.mk | 3 + spec/bundler/spec_helper.rb | 20 ------ tool/lib/bundler_runtime_grouping.rb | 99 ++++++++++++++++++++++++++++ tool/lib/bundler_runtime_recorder.rb | 16 +++++ 4 files changed, 118 insertions(+), 20 deletions(-) create mode 100644 tool/lib/bundler_runtime_grouping.rb create mode 100644 tool/lib/bundler_runtime_recorder.rb diff --git a/common.mk b/common.mk index 29fd0f5bcf6655..07ece19ee98373 100644 --- a/common.mk +++ b/common.mk @@ -1716,10 +1716,13 @@ yes-test-bundler-parallel: $(PREPARE_BUNDLER) $(gnumake_recursive)$(XRUBY) \ -r./$(arch)-fake \ -r$(tooldir)/lib/_tmpdir \ + -r$(tooldir)/lib/bundler_runtime_grouping \ -I$(srcdir)/spec/bundler \ -e "ruby = ENV['RUBY']" \ -e "ARGV[-1] = File.expand_path(ARGV[-1])" \ -e "ENV['RSPEC_EXECUTABLE'] = ruby + ARGV.shift" \ + -e "require 'support/setup'" \ + -e "BundlerRuntimeGrouping.install!" \ -e "load ARGV.shift" \ -s -- -no-report-tmpdir -- \ " -C $(srcdir) -Ispec/bundler -Ispec/lib .bundle/bin/rspec -r spec_helper" \ diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 24b0e71020ccdd..27ddc6a77125a2 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -175,26 +175,6 @@ def self.ruby=(ruby) reset! end - # Opt-in per-example runtime log (set BUNDLER_SPEC_RUNTIME_LOG to a file path). - # Each parallel worker appends one "\t" line per example to its - # own "." file (a single shared file would hit Windows - # cross-process sharing violations), so the heaviest specs can be found after a - # full run. turbo_tests only writes its own --runtime-log when invoked with the - # bare "spec" path, which the build never does, so it produces no log otherwise. - if (runtime_log = ENV["BUNDLER_SPEC_RUNTIME_LOG"]) - worker = ENV["TEST_ENV_NUMBER"].to_s - worker = "1" if worker.empty? - runtime_log = "#{runtime_log}.#{worker}" - config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) } - config.after(:each) do |example| - next unless @__runtime_start - dt = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start - File.write(runtime_log, "#{format("%.4f", dt)}\t#{example.metadata[:file_path]}\n", mode: "a") - rescue StandardError - # never let runtime logging break a test run - end - end - Spec::Shards::EXAMPLE_MAPPINGS.each do |tag, file_paths| file_pattern = Regexp.union(file_paths.map {|path| Regexp.new(Regexp.escape(path) + "$") }) diff --git a/tool/lib/bundler_runtime_grouping.rb b/tool/lib/bundler_runtime_grouping.rb new file mode 100644 index 00000000000000..c1c55810146e76 --- /dev/null +++ b/tool/lib/bundler_runtime_grouping.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Distribute the parallel bundler-spec workers by measured per-file runtime +# instead of file size, with no change to turbo_tests or parallel_tests and +# without touching the synced spec/bin/parallel_rspec. The test-bundler-parallel +# recipe requires this file and calls install! just before loading the stock +# parallel_rspec; install! prepends the grouper (parent) and appends the recorder +# to RSPEC_EXECUTABLE so each worker records (see bundler_runtime_recorder.rb). +# The grouping/recording logic lives under tool/, which sync-default-gems never +# copies, so the rubygems side stays untouched. +# +# turbo_tests only feeds a runtime log to the grouper when invoked with the bare +# "spec" path (TurboTests::CLI passes files: ["spec"] only when given no +# positional arg), which the build never does: the recipe passes an absolute +# spec/bundler path, so grouping falls back to file size and the single heaviest +# file becomes the long pole. We reopen the one grouper hook that size path uses, +# ParallelTests::RSpec::Runner.sort_by_filesize, and return runtime weights when +# the previous run left us data. Its argument is the already-expanded file list, +# so nothing here re-globs or re-reads it. + +module BundlerRuntimeGrouping + WORKER_LOGS = ".[0-9]*" + + module_function + + # Per-worker log base. Under the (gitignored) source tmp so it survives between + # runs; overridable via BUNDLER_SPEC_RUNTIME_LOG. + def log_base + ENV["BUNDLER_SPEC_RUNTIME_LOG"] || + File.expand_path("../../tmp/bundler_runtime_rspec.log", __dir__) + end + + # Append one "\t" line for a finished example to this worker's + # own log ("."); a single shared file would hit Windows + # cross-process sharing violations. Called from the worker (see recorder). + def record(example, seconds) + worker = ENV["TEST_ENV_NUMBER"].to_s + worker = "1" if worker.empty? + File.write("#{log_base}.#{worker}", + "#{format("%.4f", seconds)}\t#{example.metadata[:file_path]}\n", mode: "a") + rescue StandardError + # never let runtime logging break a test run + end + + # {canonical_path => seconds} summed across the previous run's worker logs, + # memoized before this run overwrites them. Empty on the first run. + def runtimes + @runtimes ||= Dir.glob(log_base + WORKER_LOGS).each_with_object(Hash.new(0.0)) do |path, sums| + File.foreach(path) do |line| + seconds, tab, file = line.chomp.partition("\t") + sums[canonical(file)] += seconds.to_f unless tab.empty? + end + rescue StandardError + sums + end + end + + # cwd-independent key: from "spec/bundler/" on, forward slashes. Recorded paths + # are rspec-relative ("./spec/bundler/..."); grouped paths are absolute. + def canonical(path) + normalized = path.tr("\\", "/") + i = normalized.index("spec/bundler/") + i ? normalized[i..] : normalized + end + + # Called once in the parent before parallel_rspec loads (spec/bundler/support + # must already be required so parallel_tests is on the load path). Reads the + # previous run, clears the logs, prepends the grouper, and arranges for each + # worker to record by appending the recorder to RSPEC_EXECUTABLE. + def install! + require "fileutils" + require "parallel_tests/rspec/runner" + FileUtils.mkdir_p(File.dirname(log_base)) + runtimes # capture the previous run before clearing + Dir.glob(log_base + WORKER_LOGS).each { |f| File.delete(f) rescue nil } + ParallelTests::RSpec::Runner.singleton_class.prepend(SortHook) + if ENV["RSPEC_EXECUTABLE"] + recorder = File.expand_path("bundler_runtime_recorder.rb", __dir__) + ENV["RSPEC_EXECUTABLE"] = "#{ENV["RSPEC_EXECUTABLE"]} -r#{recorder}" + end + rescue StandardError, LoadError => e + warn "parallel_rspec: runtime-based grouping disabled (#{e.class}: #{e.message})" + end + + module SortHook + def sort_by_filesize(tests) + rt = BundlerRuntimeGrouping.runtimes + return super if rt.empty? + tests.sort! + known = tests.map { |t| rt[BundlerRuntimeGrouping.canonical(t)] }.select(&:positive?) + return super unless known.size * 1.5 > tests.size # parallel_tests' own threshold + average = known.sum / known.size + tests.map! do |t| + seconds = rt[BundlerRuntimeGrouping.canonical(t)] + [t, seconds.positive? ? seconds : average] + end + end + end +end diff --git a/tool/lib/bundler_runtime_recorder.rb b/tool/lib/bundler_runtime_recorder.rb new file mode 100644 index 00000000000000..1c5b110beeb7e0 --- /dev/null +++ b/tool/lib/bundler_runtime_recorder.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Loaded into each parallel worker (via -r appended to RSPEC_EXECUTABLE by +# BundlerRuntimeGrouping.install!) to record per-example runtimes that the +# next run groups by. Kept in tool/ so recording does not depend on +# spec/bundler/spec_helper.rb (which is synced from rubygems/rubygems). + +require_relative "bundler_runtime_grouping" + +RSpec.configure do |config| + config.before(:each) { @__runtime_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) } + config.after(:each) do |example| + next unless @__runtime_start + BundlerRuntimeGrouping.record(example, Process.clock_gettime(Process::CLOCK_MONOTONIC) - @__runtime_start) + end +end