[ruby/rubygems] Unquote Gem.ruby when spawning it as a separate argv element

Gem.ruby wraps the interpreter path in double quotes when it contains
whitespace. That suits single-string shell commands but breaks the
multi-argument spawns that pass it as its own argv element. Under an
install prefix containing a space, `gem update --system`, mkrf extension
builds, `gem push` attestation, and the bundler auto-switch all fail to
spawn with exit 127. Split it with Shellwords, matching the existing
`Gem::Ext::Builder.ruby` idiom.

https://github.com/ruby/rubygems/commit/00e189e851

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hiroshi SHIBATA 2026-07-11 05:59:11 +09:00 committed by git
parent 290136b6df
commit e5bf3bbc81
8 changed files with 124 additions and 4 deletions

View File

@ -75,7 +75,12 @@ module Bundler
argv0 = File.exist?($PROGRAM_NAME) ? $PROGRAM_NAME : Process.argv0
cmd = [argv0, *ARGV]
cmd.unshift(Gem.ruby) unless File.executable?(argv0)
unless File.executable?(argv0)
# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the exec'd command.
require "shellwords"
cmd.unshift(*Shellwords.split(Gem.ruby))
end
Bundler.with_original_env do
Kernel.exec(

View File

@ -147,6 +147,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
def attest!(name)
require "open3"
require "shellwords"
require "tempfile"
tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"])
@ -154,9 +155,11 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo
tempfile.close(false)
env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h
# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the spawned command.
out, st = Open3.capture2e(
env,
Gem.ruby, "-S", "gem", "exec", "--conservative",
*Shellwords.split(Gem.ruby), "-S", "gem", "exec", "--conservative",
"sigstore-cli", "sign", name, "--bundle", bundle,
unsetenv_others: true
)

View File

@ -183,7 +183,10 @@ command to remove old versions.
say "Installing RubyGems #{version}" unless options[:silent]
installed = preparing_gem_layout_for(version) do
system Gem.ruby, "--disable-gems", "setup.rb", *args
# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the spawned command.
require "shellwords"
system(*Shellwords.split(Gem.ruby), "--disable-gems", "setup.rb", *args)
end
unless options[:silent]

View File

@ -14,7 +14,9 @@ class Gem::Ext::RakeBuilder < Gem::Ext::Builder
end
if /mkrf_conf/i.match?(File.basename(extension))
run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the spawned command.
run([*shellsplit(Gem.ruby), File.basename(extension), *args], results, class_name, extension_dir)
end
rake = ENV["rake"]

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
RSpec.describe Bundler::SelfManager do
describe "#restart_with" do
# When the running Ruby lives under a path containing whitespace, Gem.ruby
# returns a quoted string. That quoting must not leak into the argv passed
# to Kernel.exec, or the interpreter can't be found and the auto-switch to
# the locked bundler version fails to spawn.
it "does not embed quotes in the ruby executable when Gem.ruby is quoted" do
manager = described_class.new
allow(Gem).to receive(:ruby).and_return('"/path with space/bin/ruby"')
# Force the branch that prepends Gem.ruby to the command.
allow(File).to receive(:executable?).and_return(false)
allow(Bundler).to receive(:with_original_env).and_yield
captured = nil
allow(Kernel).to receive(:exec) do |_env, *cmd|
captured = cmd
end
manager.send(:restart_with, Gem::Version.new("1.0.0"))
expect(captured.first).to eq("/path with space/bin/ruby")
expect(captured.first).not_to include('"')
end
end
end

View File

@ -219,6 +219,36 @@ class TestGemCommandsPushCommand < Gem::TestCase
end
end
# When the running Ruby lives under a path containing whitespace, Gem.ruby
# returns a quoted string. That quoting must not leak into the argv passed to
# Open3.capture2e, or signing spawns fail and push silently falls back to an
# unsigned upload.
def test_attest_unquotes_ruby
require "open3"
fake_status = Object.new
def fake_status.success?
true
end
captured = nil
capture_stub = lambda do |*args, **_kwargs|
captured = args
["", fake_status]
end
Gem.stub(:ruby, '"/path with space/bin/ruby"') do
Open3.stub(:capture2e, capture_stub) do
@cmd.send(:attest!, @path)
end
end
refute_nil captured, "signing command should have been spawned"
# captured[0] is the env hash; the interpreter follows it.
assert_equal "/path with space/bin/ruby", captured[1]
refute_includes captured[1], '"'
assert_equal "-S", captured[2]
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"

View File

@ -816,6 +816,36 @@ class TestGemCommandsUpdateCommand < Gem::TestCase
assert_empty @cmd.updated
end
# When the running Ruby lives under a path containing whitespace, Gem.ruby
# returns a quoted string. That quoting must not leak into the argv passed to
# the non-shell `system` call that runs setup.rb, or the interpreter can't be
# found and `gem update --system` fails to spawn.
def test_install_rubygems_unquotes_ruby
version = Gem::Version.new("99.0.0")
update_dir = File.join @tempdir, "gems", "rubygems-update-#{version}"
FileUtils.mkdir_p update_dir
spec = Struct.new(:version, :base_dir).new(version, @tempdir)
captured = nil
@cmd.define_singleton_method(:system) do |*args|
captured = args
true
end
@cmd.options[:silent] = true
Gem.stub(:ruby, '"/path with space/bin/ruby"') do
@cmd.install_rubygems(spec)
end
refute_nil captured, "setup.rb should have been spawned"
assert_equal "/path with space/bin/ruby", captured.first
refute_includes captured.first, '"'
assert_includes captured, "--disable-gems"
assert_includes captured, "setup.rb"
end
def test_update_rubygems_arguments
@cmd.options[:system] = true

View File

@ -101,6 +101,24 @@ class TestGemExtRakeBuilder < Gem::TestCase
end
end
# When the running Ruby lives under a path containing whitespace, Gem.ruby
# returns a quoted string. That quoting must not leak into the argv passed to
# the non-shell spawn that runs mkrf_conf.rb, or the interpreter can't be found.
def test_class_build_mkrf_conf_unquotes_ruby
commands = []
Gem.stub(:ruby, '"/path with space/bin/ruby"') do
Gem::Ext::RakeBuilder.stub(:run, ->(command, *) { commands << command }) do
Gem::Ext::RakeBuilder.build "mkrf_conf.rb", @dest_path, [], [], nil, @ext
end
end
mkrf_command = commands.find {|command| command.include?("mkrf_conf.rb") }
refute_nil mkrf_command, "mkrf_conf.rb should have been spawned"
assert_equal "/path with space/bin/ruby", mkrf_command.first
refute_includes mkrf_command.first, '"'
end
def create_temp_mkrf_file(rakefile_content)
File.open File.join(@ext, "mkrf_conf.rb"), "w" do |mkrf_conf|
mkrf_conf.puts <<-EO_MKRF