From 04c503bc16e2ebc51bd3e8b5bbab844eb905c968 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Mon, 31 Aug 2026 09:54:35 +0900 Subject: [PATCH 01/14] Explicitly cast to `unsigned long` to fix the `sign-compare` warning --- hash.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hash.c b/hash.c index 172ed3bd49a502..2686b913c3381a 100644 --- a/hash.c +++ b/hash.c @@ -1941,7 +1941,8 @@ rb_hash_init(rb_execution_context_t *ec, VALUE hash, VALUE capa_value, VALUE ifn if (capa_value != INT2FIX(0)) { long capa = NUM2LONG(capa_value); - if (capa > 0 && RHASH_AR_TABLE_P(hash) && RHASH_SIZE(hash) == 0 && capa > RHASH_AR_TABLE_MAX_BOUND(hash)) { + if (capa > 0 && RHASH_AR_TABLE_P(hash) && RHASH_SIZE(hash) == 0 && + (unsigned long)capa > RHASH_AR_TABLE_MAX_BOUND(hash)) { hash_st_table_init(hash, capa); } } From 94e94ca63e617e6afbb130c0d650cf2973a645ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:15:53 +0000 Subject: [PATCH 02/14] Bump the github-actions group across 1 directory with 4 updates Bumps the github-actions group with 4 updates in the / directory: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action), [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) and [taiki-e/install-action](https://github.com/taiki-e/install-action). Updates `github/codeql-action/init` from 4.37.8 to 4.37.9 - [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/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/analyze` from 4.37.8 to 4.37.9 - [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/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `github/codeql-action/upload-sarif` from 4.37.8 to 4.37.9 - [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/db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28...cdf488f595d80d6e07e03d4674febd5ab45fa938) Updates `taiki-e/install-action` from 2.86.7 to 2.87.0 - [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/b6ff580856c41316412a0b9b60540fbc6f8c82cc...37f7c5781271959fb65b6b35224e28652ff2b63d) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/upload-sarif dependency-version: 4.37.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: taiki-e/install-action dependency-version: 2.87.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/check_sast.yml | 6 +++--- .github/workflows/scorecards.yml | 2 +- .github/workflows/zjit-macos.yml | 2 +- .github/workflows/zjit-ubuntu.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/check_sast.yml b/.github/workflows/check_sast.yml index 34cf7e4b9b7a44..669261535b13a0 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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: ${{ matrix.language }} build-mode: none config-file: .github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: sarif-results/${{ matrix.language }}.sarif continue-on-error: true diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 6a2f162b3a9c56..8f3ed45b13cb0f 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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: sarif_file: results.sarif diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 85688e3024946c..feb8c462890303 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -98,7 +98,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 + - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 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 b8f64735891d9c..9623059e035e18 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -152,7 +152,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@b6ff580856c41316412a0b9b60540fbc6f8c82cc # v2.86.7 + - uses: taiki-e/install-action@37f7c5781271959fb65b6b35224e28652ff2b63d # v2.87.0 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} From 9a7e81e38b75ef338e14105a4f9b053d8788d8b9 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 10:34:51 +0900 Subject: [PATCH 03/14] [ruby/rubygems] Update simplecov to 1.1 The current filter and group API only exists from simplecov 1.0. simplecov 1.x also ships the HTML and JSON formatters in the gem itself, so the explicit `simplecov_json_formatter` activation no longer resolves under the dev bundle. https://github.com/ruby/rubygems/commit/caf28e32a9 Co-Authored-By: Claude Opus 5 --- spec/bundler/spec_helper.rb | 1 - test/rubygems/helper.rb | 1 - tool/bundler/dev_gems.rb | 2 +- tool/bundler/dev_gems.rb.lock | 15 +++------------ 4 files changed, 4 insertions(+), 15 deletions(-) diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index f4030e70a6fa16..1d130becdf1fe4 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -43,7 +43,6 @@ begin raise LoadError if File.exist?(File.expand_path("../../lib/bundler/bundler.gemspec", __dir__)) - gem "simplecov_json_formatter" require "simplecov" SimpleCov.start do diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index 73e109796a7ba5..c237115d7130c7 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -5,7 +5,6 @@ begin raise LoadError if ENV["GEM_COMMAND"] - gem "simplecov_json_formatter" require "simplecov" unless ENV["SIMPLECOV_SUBPROCESS"] diff --git a/tool/bundler/dev_gems.rb b/tool/bundler/dev_gems.rb index c8e4d5345c4297..7dc32f80e00c20 100644 --- a/tool/bundler/dev_gems.rb +++ b/tool/bundler/dev_gems.rb @@ -14,7 +14,7 @@ gem "rspec-expectations", "~> 3.12" gem "rspec-mocks", "~> 3.12" gem "rubygems-generate_index", "~> 1.1" -gem "simplecov", "~> 0.22" +gem "simplecov", "~> 1.1" group :doc do gem "ronn-ng", "~> 0.10.1", platform: :ruby diff --git a/tool/bundler/dev_gems.rb.lock b/tool/bundler/dev_gems.rb.lock index ee91c8baffd35f..72916fb1e1d95f 100644 --- a/tool/bundler/dev_gems.rb.lock +++ b/tool/bundler/dev_gems.rb.lock @@ -3,7 +3,6 @@ GEM specs: compact_index (0.15.0) diff-lcs (1.6.2) - docile (1.4.1) kramdown (2.5.2) rexml (>= 3.4.4) kramdown-parser-gfm (1.1.0) @@ -58,12 +57,7 @@ GEM rspec-support (3.13.7) rubygems-generate_index (1.1.3) compact_index (~> 0.15.0) - simplecov (0.22.0) - docile (~> 1.1) - simplecov-html (~> 0.11) - simplecov_json_formatter (~> 0.1) - simplecov-html (0.13.2) - simplecov_json_formatter (0.1.4) + simplecov (1.1.1) test-unit (3.7.7) power_assert test-unit-ruby-core (1.0.14) @@ -96,7 +90,7 @@ DEPENDENCIES rspec-expectations (~> 3.12) rspec-mocks (~> 3.12) rubygems-generate_index (~> 1.1) - simplecov (~> 0.22) + simplecov (~> 1.1) test-unit (~> 3.0) test-unit-ruby-core turbo_tests (~> 2.2.3) @@ -104,7 +98,6 @@ DEPENDENCIES CHECKSUMS compact_index (0.15.0) sha256=5c6c404afca8928a7d9f4dde9524f6e1610db17e675330803055db282da84a8b diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 - docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e kramdown (2.5.2) sha256=1ba542204c66b6f9111ff00dcc26075b95b220b07f2905d8261740c82f7f02fa kramdown-parser-gfm (1.1.0) sha256=fb39745516427d2988543bf01fc4cf0ab1149476382393e0e9c48592f6581729 mini_portile2 (2.8.9) sha256=0cd7c7f824e010c072e33f68bc02d85a00aeb6fce05bb4819c03dfd3c140c289 @@ -133,9 +126,7 @@ CHECKSUMS rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c rubygems-generate_index (1.1.3) sha256=3571424322666598e9586a906485e1543b617f87644913eaf137d986a3393f5c - simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 - simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 - simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + simplecov (1.1.1) sha256=25825ef13f0b2e74694d769817dad6ab8e90131dabdaa666e522fea105521e78 test-unit (3.7.7) sha256=3c89d5ff0690a16bef9946156c4624390402b9d54dfcf4ce9cbd5b06bead1e45 test-unit-ruby-core (1.0.14) sha256=d2e997796c9c5c5e8e31ac014f83a473ff5c2523a67cfa491b08893e12d43d22 turbo_tests (2.2.5) sha256=3fa31497d12976d11ccc298add29107b92bda94a90d8a0a5783f06f05102509f From 2f01d54e1bf831ea018166be5a1f06be98ac47e5 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 10:35:04 +0900 Subject: [PATCH 04/14] [ruby/rubygems] Replace deprecated SimpleCov filter and group API simplecov 1.x renamed `add_filter` to `skip` and `add_group` to `group`, and warns once per call for the old names. https://github.com/ruby/rubygems/commit/4e15871c6d Co-Authored-By: Claude Opus 5 --- spec/bundler/spec_helper.rb | 14 +++++++------- test/rubygems/helper.rb | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 1d130becdf1fe4..2391502d9d5b7f 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -50,13 +50,13 @@ root File.expand_path("../bundler", __dir__) coverage_dir File.expand_path("../coverage", __dir__) - add_filter "/spec/" - add_filter "/test/" - add_filter "/lib/rubygems/" - add_filter "/lib/bundler/vendor/" - add_filter "/tool/" - add_filter "/tmp/" - add_filter ".gemspec" + skip "/spec/" + skip "/test/" + skip "/lib/rubygems/" + skip "/lib/bundler/vendor/" + skip "/tool/" + skip "/tmp/" + skip ".gemspec" end SimpleCov.print_error_status = false diff --git a/test/rubygems/helper.rb b/test/rubygems/helper.rb index c237115d7130c7..1fe8577b4fc94b 100644 --- a/test/rubygems/helper.rb +++ b/test/rubygems/helper.rb @@ -13,11 +13,11 @@ root File.expand_path("../..", __dir__) coverage_dir File.expand_path("../../coverage", __dir__) - add_filter "/test/" - add_filter "/bundler/" - add_filter "/tool/" - add_filter "/lib/rubygems/vendor/" - add_filter ".gemspec" + skip "/test/" + skip "/bundler/" + skip "/tool/" + skip "/lib/rubygems/vendor/" + skip ".gemspec" end # Prevent SimpleCov from running in subprocesses spawned by assert_separately From 9d543b560bcfb81161bbd716734082f83ea6a7db Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 10:35:04 +0900 Subject: [PATCH 05/14] [ruby/rubygems] Point bundler coverage root back at the repository root Moving bundler/spec to spec/ rewrote the path to spec/../bundler, which does not exist, so every bundler spec run reported 0 / 0 lines. https://github.com/ruby/rubygems/commit/090a0cddde Co-Authored-By: Claude Opus 5 --- spec/bundler/spec_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index 2391502d9d5b7f..ebbec5e683d28c 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -47,7 +47,7 @@ SimpleCov.start do command_name "bundler:#{Process.pid}" - root File.expand_path("../bundler", __dir__) + root File.expand_path("..", __dir__) coverage_dir File.expand_path("../coverage", __dir__) skip "/spec/" From b943bdf6c4cad5cf05815f1aa69ea583486dfb12 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 10:35:12 +0900 Subject: [PATCH 06/14] [ruby/rubygems] Format the coverage report once, after the workers finish turbo_tests workers share one coverage directory, and simplecov warns when a worker overwrites a coverage.json another worker just wrote. Leave the workers with their resultset and let coverage:report build the report, which `rake spec` now runs too. https://github.com/ruby/rubygems/commit/0df514194f Co-Authored-By: Claude Opus 5 --- spec/bundler/spec_helper.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/bundler/spec_helper.rb b/spec/bundler/spec_helper.rb index ebbec5e683d28c..e39c75aa61f5d5 100644 --- a/spec/bundler/spec_helper.rb +++ b/spec/bundler/spec_helper.rb @@ -60,12 +60,10 @@ end SimpleCov.print_error_status = false - SimpleCov.at_exit do - $stdout = File.open(File::NULL, "w") - SimpleCov.result.format! - ensure - $stdout = STDOUT - end + + # Only merge this process result into the resultset. Parallel workers share a + # coverage directory, so the report is formatted once by `rake coverage:report`. + SimpleCov.at_exit { SimpleCov.result } rescue LoadError # SimpleCov is not installed end From 60adb2e2684af459e85f02d77571407953495a00 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 11:52:51 +0900 Subject: [PATCH 07/14] Drop simplecov's former dependencies from the coverage load path simplecov 1.1 ships the HTML and JSON formatters and has no runtime dependencies, so the globs for simplecov-html, simplecov_json_formatter and docile never match anything. Pin `make update-coverage` to the same `~> 1.1` the bundler dev gems now use, so a `.bundle` left over with 0.x, which does need docile on the load path, gets replaced. Co-Authored-By: Claude Opus 5 --- common.mk | 2 +- tool/test-coverage.rb | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/common.mk b/common.mk index 68c0e1e33587fa..fcf64566f5c1e4 100644 --- a/common.mk +++ b/common.mk @@ -1537,7 +1537,7 @@ update-config_files: PHONY update-coverage: main PHONY $(XRUBY) -C "$(srcdir)" bin/gem install --no-document \ - --install-dir .bundle --conservative "simplecov" + --install-dir .bundle --conservative "simplecov" -v "~> 1.1" refresh-gems: update-bundled_gems prepare-gems # can't recall exactly, but `make` somewhere (not GNU or nmake) diff --git a/tool/test-coverage.rb b/tool/test-coverage.rb index 28ef0bf7f8ce21..4e321213c61edf 100644 --- a/tool/test-coverage.rb +++ b/tool/test-coverage.rb @@ -72,11 +72,8 @@ def save_coverage_data(res1) end def invoke_simplecov_formatter - # XXX docile-x.y.z and simplecov-x.y.z, simplecov-html-x.y.z, simplecov_json_formatter-x.y.z - %w[simplecov simplecov-html simplecov_json_formatter docile].each do |f| - Dir.glob("#{__dir__}/../.bundle/gems/#{f}-*/lib").each do |d| - $LOAD_PATH.unshift d - end + Dir.glob("#{__dir__}/../.bundle/gems/simplecov-*/lib").each do |d| + $LOAD_PATH.unshift d end require "simplecov" From 36d3e55cbc9c297466da5c79988c58b4371a4069 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 11:52:51 +0900 Subject: [PATCH 08/14] Update the ignored simplecov report artifacts simplecov 1.x writes a self-contained index.html plus coverage.json and .report_stamp, and no longer writes an assets directory. The simplecov, simplecov-html and doclie entries date from when update-coverage cloned the gem into coverage/ instead of installing it. Co-Authored-By: Claude Opus 5 --- .gitignore | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index b49185e46879c1..1ea0a323d92558 100644 --- a/.gitignore +++ b/.gitignore @@ -88,12 +88,10 @@ lcov*.info /config.status /config.status.lineno /configure -/coverage/simplecov -/coverage/simplecov-html -/coverage/doclie /coverage/.last_run.json +/coverage/.report_stamp /coverage/.resultset.json* -/coverage/assets +/coverage/coverage.json /coverage/index.html /doc/capi /enc.mk From 71407ded8078d4c7ac445b59162eff6fbc5415bb Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Mon, 31 Aug 2026 11:52:51 +0900 Subject: [PATCH 09/14] Drop the stale bundled simplecov TODO Nothing has been bundled under coverage/ since update-coverage switched to installing the gem. Co-Authored-By: Claude Opus 5 --- coverage/README | 1 - 1 file changed, 1 deletion(-) diff --git a/coverage/README b/coverage/README index a4c3dfcb039400..a69efd9da2a5ef 100644 --- a/coverage/README +++ b/coverage/README @@ -13,5 +13,4 @@ Limitation TODO - * more reduce bundled simplecov(additional configuration, formatter, etc.) * measure rubyspec coverage From 21cb8e2ae3bb944ca46e062ede8de2b03017978e Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sun, 30 Aug 2026 15:54:21 -0500 Subject: [PATCH 10/14] [DOC] Doc for File::exist? --- file.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/file.c b/file.c index 995111e89d88a7..0e38ee11763ad0 100644 --- a/file.c +++ b/file.c @@ -2023,13 +2023,22 @@ rb_file_chardev_p(VALUE obj, VALUE fname) /* * call-seq: - * File.exist?(file_name) -> true or false + * File.exist?(object) -> true or false * - * Return true if the named file exists. + * Return whether the specified +object+, a string path or IO object, exists: * - * _file_name_ can be an IO object. + * # String paths. + * File.exist?('README.md') # => true + * File.exist?('.') # => true + * filepath = 't.tmp' + * File.exist?(filepath) # => false + * File.write(filepath, 'foo') + * File.exist?(filepath) # => true + * # File (IO object). + * file = File.new(filepath) + * File.exist?(file) # => true + * file.close # Clean up. * - * "file exists" means that stat() or fstat() system call is successful. */ static VALUE From 276b37174a1300b7ac80d4054f91db902e699b3c Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sun, 30 Aug 2026 14:16:33 -0500 Subject: [PATCH 11/14] [DOC] Doc for File::executable? --- file.c | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/file.c b/file.c index 0e38ee11763ad0..99b321623a6a92 100644 --- a/file.c +++ b/file.c @@ -2188,16 +2188,35 @@ rb_file_world_writable_p(VALUE obj, VALUE fname) /* * call-seq: - * File.executable?(file_name) -> true or false + * File.executable?(path) -> true or false * - * Returns true if the named file is executable by the effective - * user and group id of this process. See eaccess(3). + * Returns whether the filesystem entry at the given string +path+ + * exists and is executable. * - * Windows does not support execute permissions separately from read - * permissions. On Windows, a file is only considered executable if it ends in - * .bat, .cmd, .com, or .exe. + * On Windows, the entry is executable if its path has file extension + * +.bat+, +.cmd+, +.com+, or +.exe+: * - * Note that some OS-level security features may cause this to return true + * File.executable?('win32/rtname.cmd') # => true + * File.executable?('win32/rtname') # => false + * File.executable?('win32/nosuch.cmd') # => false + * + * On other systems, the entry is executable if it has the execute/search + * permission for the effective user and group id of the current process; + * see {Permissions}[rdoc-ref:file/filesystem_modes.md@Permissions]. + * + * These examples use + * a {helper method}[rdoc-ref:file/filesystem_modes.md@Helper+Method], +mode+, + * that displays a mode both in octal digits and in characters: + * + * File.executable?('.') # => true + * mode('.') # => "040775 drwxrwxr-x" + * File.executable?('bin/gem') # => true + * mode('bin/gem') # => "100775 -rwxrwxr-x" + * File.executable?('/etc/passwd') # => false + * mode('/etc/passwd') # => "100644 -rw-r--r--" + * File.executable?('nosuch') # => false + * + * Note that some filesystem settings may cause this method to return +true+ * even though the file is not executable by the effective user/group. */ From c1e6dbd8c098ed9dda7eb9eb2a4b349e3fbef637 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Sun, 30 Aug 2026 14:19:31 -0500 Subject: [PATCH 12/14] [DOC] Doc for File::executable? --- file.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index 99b321623a6a92..526c6efd338032 100644 --- a/file.c +++ b/file.c @@ -2216,8 +2216,8 @@ rb_file_world_writable_p(VALUE obj, VALUE fname) * mode('/etc/passwd') # => "100644 -rw-r--r--" * File.executable?('nosuch') # => false * - * Note that some filesystem settings may cause this method to return +true+ - * even though the file is not executable by the effective user/group. + * Note that some filesystem settings may cause this method to return +true+ + * even though the entry is not executable by the effective user/group. */ static VALUE From 104c0f06dd32f865dbc2d9116007262e8d1246ea Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 27 Aug 2026 20:07:12 +0900 Subject: [PATCH 13/14] Copy box-local extensions into a process-private directory Flattening the full source path into a single file name made the copy fail with ENAMETOOLONG when the extension lives under a deep path such as a long BUNDLE_PATH, since the name exceeds NAME_MAX. Name each copy with a short per-process sequence instead, in a 0700 directory with an unpredictable name so that other local users cannot occupy the copy destination in a shared TMPDIR. https://bugs.ruby-lang.org/issues/22110 Co-Authored-By: Claude Fable 5 --- box.c | 104 +++++++++++++++++++++++------------------- internal/box.h | 2 +- load.c | 2 +- test/ruby/test_box.rb | 22 +++++++++ 4 files changed, 80 insertions(+), 50 deletions(-) diff --git a/box.c b/box.c index 9121ba003d6ada..629b9d123a8f84 100644 --- a/box.c +++ b/box.c @@ -11,6 +11,7 @@ #include "internal/hash.h" #include "internal/io.h" #include "internal/load.h" +#include "internal/random.h" #include "internal/st.h" #include "internal/variable.h" #include "iseq.h" @@ -47,12 +48,16 @@ static rb_box_gem_flags_t box_gem_flags[1]; static char *tmp_dir; static bool tmp_dir_has_dirsep; -#define BOX_TMP_PREFIX "_ruby_box_" - #ifndef MAXPATHLEN # define MAXPATHLEN 1024 #endif +/* process-private 0700 directory for box-local copies of extensions */ +static char box_ext_tmp_dir[MAXPATHLEN]; +static unsigned long box_ext_seq; + +#define BOX_TMP_PREFIX "_ruby_box_" + #if defined(_WIN32) # define DIRSEP "\\" #else @@ -560,13 +565,40 @@ system_tmpdir(void) /* end of copy */ -static int -sprint_ext_filename(char *str, size_t size, long box_id, const char *prefix, const char *basename) +/* Box-local copies of extensions are placed in a process-private 0700 + * directory with an unpredictable name, so that other local users cannot + * occupy the copy destination in a shared TMPDIR [Bug #22110], and so + * that the file name of each copy stays short regardless of the depth of + * the original path (a full path flattened into a single file name can + * exceed NAME_MAX). */ +static void +ensure_box_ext_tmp_dir(void) { - if (tmp_dir_has_dirsep) { - return snprintf(str, size, "%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, prefix, getpid(), box_id, basename); + if (box_ext_tmp_dir[0]) return; + + int last_errno = 0; + for (int retry = 0; retry < 10; retry++) { + char path[MAXPATHLEN]; + uint64_t suffix; + if (ruby_fill_random_bytes(&suffix, sizeof(suffix), FALSE) != 0) { + /* no random source; mkdir(0700) below still refuses hijacked names */ + suffix = ((uint64_t)getpid() << 32) ^ (uint64_t)(uintptr_t)&suffix ^ (uint64_t)retry; + } + int wrote = snprintf(path, sizeof(path), "%s%s%sp%"PRI_PIDT_PREFIX"u_%.16"PRIx64, + tmp_dir, tmp_dir_has_dirsep ? "" : DIRSEP, + BOX_TMP_PREFIX, getpid(), suffix); + if (wrote >= (int)sizeof(path)) { + rb_raise(rb_eLoadError, "TMPDIR for Ruby Box extensions is too long: %s", tmp_dir); + } + if (mkdir(path, 0700) == 0) { + strlcpy(box_ext_tmp_dir, path, sizeof(box_ext_tmp_dir)); + return; + } + last_errno = errno; + if (last_errno != EEXIST) break; } - return snprintf(str, size, "%s%s%sp%"PRI_PIDT_PREFIX"u_%ld_%s", tmp_dir, DIRSEP, prefix, getpid(), box_id, basename); + rb_raise(rb_eLoadError, "can't create the temporary directory for Ruby Box extensions under %s: %s", + tmp_dir, strerror(last_errno)); } enum copy_error_type { @@ -738,41 +770,14 @@ copy_ext_file(const char *src_path, const char *dst_path) #define isdirsep(x) ((x) == '/') #endif -#define IS_SOEXT(e) (strcmp((e), ".so") == 0 || strcmp((e), ".o") == 0) -#define IS_DLEXT(e) (strcmp((e), DLEXT) == 0) - -static void -fname_without_suffix(const char *fname, char *rvalue, size_t rsize) -{ - size_t len = strlen(fname); - const char *pos; - for (pos = fname + len; pos > fname; pos--) { - if (IS_SOEXT(pos) || IS_DLEXT(pos)) { - len = pos - fname; - break; - } - if (fname + len - pos > DLEXT_MAXLEN) break; - } - if (len > rsize - 1) len = rsize - 1; - memcpy(rvalue, fname, len); - rvalue[len] = '\0'; -} - -static void -escaped_basename(const char *path, const char *fname, char *rvalue, size_t rsize) +static const char * +ext_basename(const char *path) { - char *pos; - const char *leaf = path, *found; - // `leaf + 1` looks uncomfortable (when leaf == path), but fname must not be the top-dir itself - while ((found = strstr(leaf + 1, fname)) != NULL) { - leaf = found; // find the last occurrence for the path like /etc/my-crazy-lib-dir/etc.so - } - strlcpy(rvalue, leaf, rsize); - for (pos = rvalue; *pos; pos++) { - if (isdirsep(*pos)) { - *pos = '+'; - } + const char *base = path; + for (const char *pos = path; *pos; pos++) { + if (isdirsep(*pos)) base = pos + 1; } + return base; } static void @@ -847,22 +852,25 @@ rb_box_unload_local_extensions(void) ext = next; } #endif + if (box_ext_tmp_dir[0]) { + rmdir(box_ext_tmp_dir); + box_ext_tmp_dir[0] = '\0'; + } } VALUE -rb_box_local_extension(VALUE box_value, VALUE fname, VALUE path, VALUE *cleanup) +rb_box_local_extension(VALUE box_value, VALUE path, VALUE *cleanup) { - char ext_path[MAXPATHLEN], fname2[MAXPATHLEN], basename[MAXPATHLEN]; + char ext_path[MAXPATHLEN]; int wrote; - const char *src_path = RSTRING_PTR(path), *fname_ptr = RSTRING_PTR(fname); + const char *src_path = RSTRING_PTR(path); rb_box_t *box = rb_get_box_t(box_value); - fname_without_suffix(fname_ptr, fname2, sizeof(fname2)); - escaped_basename(src_path, fname2, basename, sizeof(basename)); - - wrote = sprint_ext_filename(ext_path, sizeof(ext_path), box->box_id, BOX_TMP_PREFIX, basename); + ensure_box_ext_tmp_dir(); + wrote = snprintf(ext_path, sizeof(ext_path), "%s%s%ld_%lu_%s", + box_ext_tmp_dir, DIRSEP, box->box_id, box_ext_seq++, ext_basename(src_path)); if (wrote >= (int)sizeof(ext_path)) { - rb_bug("Extension file path in the box was too long"); + rb_raise(rb_eLoadError, "extension file path in the box is too long: %"PRIsVALUE, path); } VALUE new_path = rb_str_new_cstr(ext_path); *cleanup = TypedData_Wrap_Struct(0, &box_ext_cleanup_type, NULL); diff --git a/internal/box.h b/internal/box.h index fa01a47307ed66..b5ebc2c3d2c185 100644 --- a/internal/box.h +++ b/internal/box.h @@ -88,7 +88,7 @@ void rb_box_gc_update_references(void *ptr); rb_box_t * rb_get_box_t(VALUE ns); VALUE rb_get_box_object(rb_box_t *ns); -VALUE rb_box_local_extension(VALUE box, VALUE fname, VALUE path, VALUE *cleanup); +VALUE rb_box_local_extension(VALUE box, VALUE path, VALUE *cleanup); void rb_box_cleanup_local_extension(VALUE cleanup); void rb_box_defer_unload_local_extension(void *handle); void rb_box_unload_local_extensions(void); diff --git a/load.c b/load.c index c10de25a45e3b6..ecc3da22597d50 100644 --- a/load.c +++ b/load.c @@ -1218,7 +1218,7 @@ load_ext(VALUE path, VALUE fname) const rb_box_t *box = rb_loading_box(); VALUE cleanup = 0; if (BOX_USER_P(box)) { - loaded = rb_box_local_extension(box->box_object, fname, path, &cleanup); + loaded = rb_box_local_extension(box->box_object, path, &cleanup); } rb_scope_visibility_set(METHOD_VISI_PUBLIC); void *handle = dln_load_feature(RSTRING_PTR(loaded), RSTRING_PTR(fname)); diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 43f13494de3782..5491578e9fb303 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -1279,6 +1279,28 @@ def test_loaded_extension_deleted_in_user_box end end + def test_loading_extension_from_deep_path_in_user_box + require 'date' + dlext = RbConfig::CONFIG['DLEXT'] + src = $LOADED_FEATURES.find {|f| f.end_with?("date_core.#{dlext}")} + omit "date_core.#{dlext} is not loaded dynamically" unless src && File.exist?(src) + + require 'tmpdir' + require 'fileutils' + Dir.mktmpdir do |tmpdir| + # deep enough that this path flattened into a single file name exceeds NAME_MAX + deep = File.join(tmpdir, "d" * 90, "e" * 90, "f" * 90) + FileUtils.mkdir_p(deep) + FileUtils.cp(src, deep) + env = ENV_ENABLE_BOX.merge({'BOX_TEST_EXT_FEATURE'=>File.join(deep, "date_core")}) + assert_ruby_status([env], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + require ENV['BOX_TEST_EXT_FEATURE'] or raise "already loaded" + raise "Date is not defined" unless defined?(Date) + end; + end + end + def test_root_box_iclasses_should_be_boxable assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "#{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) begin; From 6eba15864c427c23b606aca55bccc4c5af8ac4c1 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Aug 2026 07:31:26 +0900 Subject: [PATCH 14/14] Do not mix the PID into the fallback random suffix The PID is already visible in the directory name, so it adds no entropy. Co-Authored-By: Claude Fable 5 --- box.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/box.c b/box.c index 629b9d123a8f84..559170c7a452ce 100644 --- a/box.c +++ b/box.c @@ -582,7 +582,7 @@ ensure_box_ext_tmp_dir(void) uint64_t suffix; if (ruby_fill_random_bytes(&suffix, sizeof(suffix), FALSE) != 0) { /* no random source; mkdir(0700) below still refuses hijacked names */ - suffix = ((uint64_t)getpid() << 32) ^ (uint64_t)(uintptr_t)&suffix ^ (uint64_t)retry; + suffix = (uint64_t)(uintptr_t)&suffix ^ (uint64_t)retry; } int wrote = snprintf(path, sizeof(path), "%s%s%sp%"PRI_PIDT_PREFIX"u_%.16"PRIx64, tmp_dir, tmp_dir_has_dirsep ? "" : DIRSEP,