Skip to content

Jar resources issues with IO#seek #pos or #pread #9727

Description

@kares

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
  1. IO#pos always reports 0, whatever has been read.
  2. IO#seek and IO#pos= return 0 and raise nothing, but position does not move.
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions