uri:classloader: resources: IO#pos, IO#seek and IO#pread silently return wrong results
- applies to any entry read through
uri:classloader: / classpath: (any gem or app file in a bundled .jar or .war)
- came out with latest Psych (5.5.0) update, see bellow, where Rails is no longer able to read .yml files inside the archive.
Reproduction
For simplicity, let's just attempt to read a resource out of jruby.jar itself:
path = "uri:classloader:/jruby/kernel.rb" # 1124 bytes
File.size(path) # => 1124 (correct)
File.open(path, "r") do |f|
f.read(3) # => "# T"
f.pos # => 0 EXPECTED 3
f.seek(0) # => 0
f.read(3) # => "his" EXPECTED "# T"
end
File.open(path, "r") do |f|
f.pread(10, 0) # => "# This fil"
f.pread(10, 50) # => "e boots th" EXPECTED "module JRu"
f.pread(10, 0) # => "e Ruby-bas" EXPECTED "# This fil"
end
File.open(path, "r:bom|utf-8") do |f|
f.pos # => 0
f.read # => "" EXPECTED the whole file
end
IO#pos always reports 0, whatever has been read.
IO#seek and IO#pos= return 0 and raise nothing, but position does not move.
IO#pread ignores offset argument and reads sequentially from wherever the handle happens to be.
Nothing raises in any of these cases. Callers get wrong data and cannot tell.
psych >= 5.5.0: Rails will not boot from a jar
Recent psych 5.5.0 added a BOM pre-pass to Psych::Parser#parse (lib/psych/parser.rb, for Bug #13615):
def parse yaml, path = ...
_native_parse @handler, strip_bom(yaml), path
end
def skip_io_bom io, bom
begin
pos = io.pos
rescue SystemCallError, IOError
return # guard never fires: pos does not raise
end
head = io.read(bom.bytesize)
io.seek(pos, IO::SEEK_SET) if head && head.b != bom
end
Psych.unsafe_load_file opens with File.open(filename, 'r:bom|utf-8'), stream goes empty, Psych.parse returns false for "no documents" and every YAML inside the jar reads as empty.
For a Rails app this is fatal at boot, because i18n rejects the non-Hash:
ArgumentError: (InvalidLocaleData) can not load translations from
uri:classloader:/gems/activesupport-8.1.3.1/lib/active_support/locale/en.yml:
expects it to return a hash, but does not
at RUBY.load_file(uri:classloader:/gems/i18n-1.14.8/lib/i18n/backend/base.rb:245)
...
at RUBY.eager_load!(uri:classloader:/gems/i18n-1.14.8/lib/i18n.rb:93)
at RUBY.initialize!(uri:classloader:/gems/railties-8.1.3.1/lib/rails/application.rb:442)
mini_mime: reported in 2019, closed, still broken
discourse/mini_mime#37 is the same bug, hit through seek + read in MiniMime::Db::RandomAccessDb.
The reporter diagnosed it correctly at the time and pointed at jruby/jruby#3399.
It was closed by PR #50, which replaced seek + read with pread for fork safety and noted that this "also happens to fix an outstanding JRuby issue". On JRuby it does not, because of symptom 3: pread ignores its offset. The crash is gone, but mini_mime now reads from the wrong place in the database and returns a wrong MIME type instead. That is harder to notice than the original crash.
History
jruby/jruby#3399 (2015) reported seek raising Errno::EPIPE on these paths - got closed by making seek stop raising. The position never started moving, so the failure became silent.
uri:classloader:resources:IO#pos,IO#seekandIO#preadsilently return wrong resultsuri:classloader:/classpath:(any gem or app file in a bundled.jaror.war)Reproduction
For simplicity, let's just attempt to read a resource out of
jruby.jaritself:IO#posalways reports0, whatever has been read.IO#seekandIO#pos=return0and raise nothing, but position does not move.IO#preadignoresoffsetargument and reads sequentially from wherever the handle happens to be.Nothing raises in any of these cases. Callers get wrong data and cannot tell.
psych >= 5.5.0: Rails will not boot from a jar
Recent psych 5.5.0 added a BOM pre-pass to
Psych::Parser#parse(lib/psych/parser.rb, for Bug #13615):Psych.unsafe_load_fileopens withFile.open(filename, 'r:bom|utf-8'), stream goes empty,Psych.parsereturnsfalsefor "no documents" and every YAML inside the jar reads as empty.For a Rails app this is fatal at boot, because i18n rejects the non-Hash:
mini_mime: reported in 2019, closed, still broken
discourse/mini_mime#37 is the same bug, hit through
seek + readinMiniMime::Db::RandomAccessDb.The reporter diagnosed it correctly at the time and pointed at jruby/jruby#3399.
It was closed by PR #50, which replaced
seek + readwithpreadfor fork safety and noted that this "also happens to fix an outstanding JRuby issue". On JRuby it does not, because of symptom 3:preadignores its offset. The crash is gone, but mini_mime now reads from the wrong place in the database and returns a wrong MIME type instead. That is harder to notice than the original crash.History
jruby/jruby#3399 (2015) reported
seekraisingErrno::EPIPEon these paths - got closed by makingseekstop raising. The position never started moving, so the failure became silent.