Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/bundler/self_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ def restart_with(version)

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(
Expand Down
5 changes: 4 additions & 1 deletion lib/rubygems/commands/push_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,19 @@ def send_push_request_with_attestation(name, args)

def attest!(name)
require "open3"
require "shellwords"
require "tempfile"

tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"])
bundle = tempfile.path
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
)
Expand Down
5 changes: 4 additions & 1 deletion lib/rubygems/commands/update_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ def install_rubygems(spec) # :nodoc:
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]
Expand Down
4 changes: 3 additions & 1 deletion lib/rubygems/ext/rake_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extensio
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"]
Expand Down
29 changes: 29 additions & 0 deletions spec/bundler/self_manager_spec.rb
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions test/rubygems/test_gem_commands_push_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,36 @@ def test_execute_attestation_skipped_on_jruby
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"
Expand Down
30 changes: 30 additions & 0 deletions test/rubygems/test_gem_commands_update_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,36 @@ def test_update_gem_unresolved_dependency
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

Expand Down
18 changes: 18 additions & 0 deletions test/rubygems/test_gem_ext_rake_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,24 @@ def test_class_build_fail
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
Expand Down