Skip to content
Merged
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: 7 additions & 0 deletions .github/workflows/zjit-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,18 @@ jobs:
strategy:
matrix:
include:
# Run with the inliner enabled (on by default)
# Test --call-threshold=2 with 2 iterations in total
- ruby_opts: '--zjit-call-threshold=2'
bench_opts: '--warmup=1 --bench=1 --excludes=shipit'
configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow

# Run with inliner disabled #
# Test --call-threshold=2 with 2 iterations in total
- ruby_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2'
bench_opts: '--warmup=1 --bench=1 --excludes=shipit'
configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow

runs-on: macos-26

if: >-
Expand Down
40 changes: 27 additions & 13 deletions .github/workflows/zjit-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,43 @@ jobs:
fail-fast: false
matrix:
include:
- test_task: 'check'
run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1'
specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1'
configure: '--enable-zjit=dev'

## Run with default options (inliner enabled) ##
- test_task: 'check'
run_opts: '--zjit-call-threshold=1'
specopts: '-T --zjit-call-threshold=1'
configure: '--enable-zjit=dev'

- test_task: 'check'
run_opts: '--zjit-call-threshold=1 --zjit-inline-threshold=30'
specopts: '-T --zjit-call-threshold=1 -T --zjit-inline-threshold=30'
# The optimizer benefits from at least 1 iteration of profiling. Also, many
# regression tests in bootstraptest/test_yjit.rb assume call-threshold=2.
- test_task: 'btest'
run_opts: '--zjit-call-threshold=2'
configure: '--enable-zjit=dev'
continue-on-test_task: true

- test_task: 'test-bundled-gems'
configure: '--enable-zjit=dev'
run_opts: '--zjit-call-threshold=1'

## Run with inliner disabled ##
- test_task: 'check'
run_opts: '--zjit-disable-hir-opt --zjit-call-threshold=1'
specopts: '-T --zjit-disable-hir-opt -T --zjit-call-threshold=1'
run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=1'
specopts: '-T --zjit-inline-threshold=0 -T --zjit-call-threshold=1'
configure: '--enable-zjit=dev'

# The optimizer benefits from at least 1 iteration of profiling. Also, many
# regression tests in bootstraptest/test_yjit.rb assume call-threshold=2.
- test_task: 'btest'
run_opts: '--zjit-call-threshold=2'
run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2'
configure: '--enable-zjit=dev'

- test_task: 'test-bundled-gems'
configure: '--enable-zjit=dev'
run_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=1'

- test_task: 'zjit-check' # zjit-test + quick feedback of test_zjit.rb
configure: '--enable-yjit --enable-zjit=dev'
rust_version: '1.85.0'
Expand All @@ -91,10 +106,6 @@ jobs:
clang_path: '/usr/bin/clang-16'
runs-on: 'ubuntu-24.04' # for clang-16

- test_task: 'test-bundled-gems'
configure: '--enable-zjit=dev'
run_opts: '--zjit-call-threshold=1'

env:
GITPULLOPTIONS: --no-tags origin ${{ github.ref }}
RUN_OPTS: ${{ matrix.run_opts }}
Expand Down Expand Up @@ -246,11 +257,14 @@ jobs:
strategy:
matrix:
include:
# Test --call-threshold=2 with 2 iterations in total
# Test --call-threshold=2 with 2 iterations in total (inlining enabled)
- ruby_opts: '--zjit-call-threshold=2'
bench_opts: '--warmup=1 --bench=1 --excludes=shipit'
configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow

# Test --call-threshold=2 with 2 iterations in total (inlining disabled)
- ruby_opts: '--zjit-inline-threshold=0 --zjit-call-threshold=2'
bench_opts: '--warmup=1 --bench=1 --excludes=shipit'
configure: '--enable-zjit=dev_nodebug' # --enable-zjit=dev is too slow
runs-on: ubuntu-24.04

if: >-
Expand Down
36 changes: 18 additions & 18 deletions lib/rubygems/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -510,24 +510,6 @@ def gzip_to(io) # :yields: gz_io
gz_io.close
end

##
# Returns the full path for installing +filename+.
#
# If +filename+ is not inside +destination_dir+ an exception is raised.

def install_location(filename, destination_dir) # :nodoc:
raise Gem::Package::PathError.new(filename, destination_dir) if
filename.start_with? "/"

destination_dir = File.realpath(destination_dir)
destination = File.expand_path(filename, destination_dir)

raise Gem::Package::PathError.new(destination, destination_dir) unless
normalize_path(destination).start_with? normalize_path(destination_dir + "/")

destination
end

if Gem.win_platform?
def normalize_path(pathname) # :nodoc:
pathname.downcase
Expand Down Expand Up @@ -662,6 +644,24 @@ def verify

private

##
# Returns the full path for installing +filename+ into +destination_dir+,
# which must already be resolved with File.realpath by the caller.
#
# If +filename+ is not inside +destination_dir+ an exception is raised.

def install_location(filename, destination_dir) # :nodoc:
raise Gem::Package::PathError.new(filename, destination_dir) if
filename.start_with? "/"

destination = File.expand_path(filename, destination_dir)

raise Gem::Package::PathError.new(destination, destination_dir) unless
normalize_path(destination).start_with? normalize_path(destination_dir + "/")

destination
end

##
# Verifies the +checksums+ against the +digests+. This check is not
# cryptographically secure. Missing checksums are ignored.
Expand Down
3 changes: 3 additions & 0 deletions lib/rubygems/package/old.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def extract_files(destination_dir)
header = file_list io
raise Gem::Exception, errstr unless header

# install_location expects an already-resolved destination dir
destination_dir = File.realpath(destination_dir)

header.each do |entry|
full_name = entry["path"]

Expand Down
20 changes: 19 additions & 1 deletion ractor.c
Original file line number Diff line number Diff line change
Expand Up @@ -1461,11 +1461,29 @@ allow_frozen_shareable_p(VALUE obj)
return false;
}

static void
make_shareable_freeze(VALUE obj)
{
VALUE klass = RBASIC_CLASS(obj);
if (klass == rb_cString && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, STRING_REDEFINED_OP_FLAG)) {
rb_str_freeze(obj);
}
else if (klass == rb_cArray && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, ARRAY_REDEFINED_OP_FLAG)) {
rb_ary_freeze(obj);
}
else if (klass == rb_cHash && BASIC_OP_UNREDEFINED_P(BOP_FREEZE, HASH_REDEFINED_OP_FLAG)) {
rb_hash_freeze(obj);
}
else {
rb_funcall(obj, idFreeze, 0);
}
}

static enum obj_traverse_iterator_result
make_shareable_check_shareable_freeze(VALUE obj, enum obj_traverse_iterator_result result)
{
if (!RB_OBJ_FROZEN_RAW(obj)) {
rb_funcall(obj, idFreeze, 0);
make_shareable_freeze(obj);

if (UNLIKELY(!RB_OBJ_FROZEN_RAW(obj))) {
rb_raise(rb_eRactorError, "#freeze does not freeze object correctly");
Expand Down
85 changes: 53 additions & 32 deletions test/rubygems/test_gem_package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -835,79 +835,100 @@ def test_extract_tar_gz_case_insensitive
end
end

def test_install_location
package = Gem::Package.new @gem

file = "file.rb".dup

destination = package.install_location file, @destination

assert_equal File.join(@destination, "file.rb"), destination
end
# The following tests exercise install_location's path resolution and
# traversal protection through the real extraction path (extract_tar_gz)
# rather than calling the private helper directly. The absolute-path case is
# already covered by test_extract_tar_gz_absolute.

def test_install_location_absolute
def test_extract_tar_gz_basic_file
package = Gem::Package.new @gem

e = assert_raise Gem::Package::PathError do
package.install_location "/absolute.rb", @destination
tgz_io = util_tar_gz do |tar|
tar.add_file "file.rb", 0o644 do |io|
io.write "hi"
end
end

assert_equal("installing into parent path /absolute.rb of " \
"#{@destination} is not allowed", e.message)
package.extract_tar_gz tgz_io, @destination

extracted = File.join @destination, "file.rb"
assert_path_exist extracted
assert_equal "hi", File.read(extracted)
end

def test_install_location_dots
def test_extract_tar_gz_collapses_parent_dots
package = Gem::Package.new @gem

file = "file.rb"

destination = File.join @destination, "foo", "..", "bar"

FileUtils.mkdir_p File.join @destination, "foo"
FileUtils.mkdir_p File.expand_path destination
tgz_io = util_tar_gz do |tar|
tar.add_file "foo/../bar/file.rb", 0o644 do |io|
io.write "hi"
end
end

destination = package.install_location file, destination
package.extract_tar_gz tgz_io, @destination

# this test only fails on ruby missing File.realpath
assert_equal File.join(@destination, "bar", "file.rb"), destination
extracted = File.join @destination, "bar", "file.rb"
assert_path_exist extracted
assert_equal "hi", File.read(extracted)
assert_path_not_exist File.join(@destination, "foo")
end

def test_install_location_extra_slash
def test_extract_tar_gz_collapses_extra_slash
package = Gem::Package.new @gem

file = "foo//file.rb".dup
tgz_io = util_tar_gz do |tar|
tar.add_file "foo//file.rb", 0o644 do |io|
io.write "hi"
end
end

destination = package.install_location file, @destination
package.extract_tar_gz tgz_io, @destination

assert_equal File.join(@destination, "foo", "file.rb"), destination
extracted = File.join @destination, "foo", "file.rb"
assert_path_exist extracted
assert_equal "hi", File.read(extracted)
end

def test_install_location_relative
def test_extract_tar_gz_rejects_relative_escape
package = Gem::Package.new @gem

tgz_io = util_tar_gz do |tar|
tar.add_file "../relative.rb", 0o644 do |io|
io.write "hi"
end
end

e = assert_raise Gem::Package::PathError do
package.install_location "../relative.rb", @destination
package.extract_tar_gz tgz_io, @destination
end

parent = File.expand_path File.join @destination, "../relative.rb"

assert_equal("installing into parent path #{parent} of " \
"#{@destination} is not allowed", e.message)
assert_path_not_exist parent
end

def test_install_location_suffix
def test_extract_tar_gz_rejects_suffix_escape
package = Gem::Package.new @gem

filename = "../#{File.basename(@destination)}suffix.rb"

tgz_io = util_tar_gz do |tar|
tar.add_file filename, 0o644 do |io|
io.write "hi"
end
end

e = assert_raise Gem::Package::PathError do
package.install_location filename, @destination
package.extract_tar_gz tgz_io, @destination
end

parent = File.expand_path File.join @destination, filename

assert_equal("installing into parent path #{parent} of " \
"#{@destination} is not allowed", e.message)
assert_path_not_exist parent
end

def test_load_spec_from_metadata
Expand Down
Loading