Skip to content

Commit 89e47ef

Browse files
committed
Ensure symlinked realpaths are not loaded twice
closes #3138 Use a hash to cache realpath lookups and another to cache realpath -> feature similar to what was done in CRuby: https://github.com/ruby/ruby/blob/499eb3990faeaac2603787f2a41b2d9625e180dc/load.c#L358-L402 https://github.com/ruby/ruby/blob/499eb3990faeaac2603787f2a41b2d9625e180dc/load.c#L1195-L1289
1 parent 4dab7ea commit 89e47ef

File tree

5 files changed

+59
-3
lines changed

5 files changed

+59
-3
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Compatibility:
3131
* Add `Refinement#refined_class` (#3039, @itarato).
3232
* Add `rb_hash_new_capa` function (#3039, @itarato).
3333
* Fix `Encoding::Converter#primitive_convert` and raise `FrozenError` when a destination buffer argument is frozen (@andrykonchin).
34+
* Ensure symlinked paths aren't loaded more than once (@rwstauner).
3435

3536
Performance:
3637

Diff for: spec/ruby/core/kernel/require_relative_spec.rb

+18
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,24 @@
235235
features.should_not include(canonical_path)
236236
end
237237

238+
ruby_version_is "3.1" do
239+
it "does not require symlinked target file twice" do
240+
symlink_path = "#{@symlink_basename}/load_fixture.rb"
241+
absolute_path = "#{tmp("")}#{symlink_path}"
242+
canonical_path = "#{CODE_LOADING_DIR}/load_fixture.rb"
243+
touch(@requiring_file) { |f|
244+
f.puts "require_relative #{symlink_path.inspect}"
245+
f.puts "require_relative #{canonical_path.inspect}"
246+
}
247+
load(@requiring_file)
248+
ScratchPad.recorded.should == [:loaded]
249+
250+
features = $LOADED_FEATURES.select { |path| path.end_with?('load_fixture.rb') }
251+
features.should include(absolute_path)
252+
features.should_not include(canonical_path)
253+
end
254+
end
255+
238256
it "stores the same path that __FILE__ returns in the required file" do
239257
symlink_path = "#{@symlink_basename}/load_fixture_and__FILE__.rb"
240258
touch(@requiring_file) { |f|

Diff for: spec/ruby/core/kernel/shared/require.rb

+13
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,19 @@
403403
$".last.should == loaded_feature
404404
$LOAD_PATH[0].should == @symlink_to_dir
405405
end
406+
407+
ruby_version_is "3.1" do
408+
it "requires the realpath only once" do
409+
$LOAD_PATH.unshift(@symlink_to_dir)
410+
@object.require("symfile").should be_true
411+
loaded_feature = "#{@dir}/symfile.rb"
412+
ScratchPad.recorded.should == [loaded_feature]
413+
$".last.should == loaded_feature
414+
415+
@object.require("realfile").should be_false
416+
ScratchPad.recorded.should == [loaded_feature]
417+
end
418+
end
406419
end
407420
end
408421

Diff for: src/main/ruby/truffleruby/core/kernel.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def autoload?(name)
231231
when :feature_loaded
232232
false
233233
when :feature_found
234-
Primitive.load_feature(feature, path)
234+
Truffle::FeatureLoader.load_unless_realpath_loaded(feature, path)
235235
when :not_found
236236
raise Truffle::KernelOperations.load_error(feature)
237237
end
@@ -257,7 +257,7 @@ def require(feature)
257257
when :feature_loaded
258258
false
259259
when :feature_found
260-
Primitive.load_feature(feature, path)
260+
Truffle::FeatureLoader.load_unless_realpath_loaded(feature, path)
261261
when :not_found
262262
if lazy_rubygems
263263
Truffle::KernelOperations.loading_rubygems = true
@@ -290,7 +290,7 @@ def require_relative(feature)
290290
false
291291
when :feature_found
292292
# The first argument needs to be the expanded path here for patching to work correctly
293-
Primitive.load_feature(path, path)
293+
Truffle::FeatureLoader.load_unless_realpath_loaded(path, path)
294294
when :not_found
295295
raise Truffle::KernelOperations.load_error(feature)
296296
end

Diff for: src/main/ruby/truffleruby/core/truffle/feature_loader.rb

+24
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ module FeatureLoader
3535
# A snapshot of $LOADED_FEATURES, to check if the @loaded_features_index cache is up to date.
3636
@loaded_features_version = -1
3737

38+
@features_realpath_cache = {}
39+
@loaded_features_realpaths = {}
40+
3841
@expanded_load_path = []
3942
# A snapshot of $LOAD_PATH, to check if the @expanded_load_path cache is up to date.
4043
@load_path_version = -1
@@ -140,6 +143,16 @@ def self.find_feature_or_file(feature, use_feature_provided = true)
140143
end
141144
end
142145

146+
def self.load_unless_realpath_loaded(feature, path)
147+
# TODO: does this need to be synchronized?
148+
realpath = (@features_realpath_cache[path] ||= File.realpath(path) || path)
149+
return false if @loaded_features_realpaths.key?(realpath)
150+
151+
result = Primitive.load_feature(feature, path)
152+
@loaded_features_realpaths[realpath] = path
153+
result
154+
end
155+
143156
def self.expanded_path_provided(path, ext, use_feature_provided)
144157
if use_feature_provided && feature_provided?(path, true)
145158
[:feature_loaded, path, ext]
@@ -292,13 +305,24 @@ def self.get_loaded_features_index
292305
unless @loaded_features_version == $LOADED_FEATURES.version
293306
raise '$LOADED_FEATURES is frozen; cannot append feature' if $LOADED_FEATURES.frozen?
294307
@loaded_features_index.clear
308+
previous_realpaths = @features_realpath_cache.dup
309+
@features_realpath_cache.clear
310+
@loaded_features_realpaths.clear
295311
$LOADED_FEATURES.map! do |val|
296312
val = StringValue(val)
297313
#val.freeze # TODO freeze these but post-boot.rb issue using replace
298314
val
299315
end
300316
$LOADED_FEATURES.each_with_index do |val, idx|
301317
features_index_add(val, idx)
318+
# TODO: do we need to do this in a separate loop on a copy to avoid
319+
# concurrency issues? it's already called from with_synchronized_features.
320+
# https://github.com/ruby/ruby/pull/4887/commits/972f2744d8145db965f1c4218313bd200ea0a740#:~:text=To%20avoid%20concurrency%20issues%20when%20rebuilding%20the%20loaded%20features%0Aindex%2C%20the%20building%20of%20the%20index%20itself%20is%20left%20alone%2C%20and%0Aafterwards%2C%20a%20separate%20loop%20is%20done%20on%20a%20copy%20of%20the%20loaded%20feature%0Asnapshot%20in%20order%20to%20rebuild%20the%20realpaths%20hash.
321+
# end
322+
# $LOADED_FEATURES.dup.each_with_index do |val, idx|
323+
realpath = previous_realpaths[val] || (File.exist?(val) ? File.realpath(val) : val)
324+
@features_realpath_cache[val] = realpath
325+
@loaded_features_realpaths[realpath] = val
302326
end
303327
@loaded_features_version = $LOADED_FEATURES.version
304328
end

0 commit comments

Comments
 (0)