Ruby on Rails
MarshalDataTooShort

Symptoms

On Windows, Rails bombs out with a marshal data too short (ArgumentError) message after a non-eventful piece of code. After that, Rails will exit every request with an error until the session data in /tmp gets deleted.

Diagnosis

Rails stores its session state using Ruby’s PStore class in ruby/lib/pstore.rb. Because the session data is stored as binary data, Windows needs the IO streams for accessing the session files to be set to binary mode (using the stream.binmode method).

Unfortunately Ruby’s PStore doesn’t use .binmode on the streams it uses, which causes the session files to become corrupted.

Fix

To fix this I had to patch ruby/lib/pstore.rb. Matz is after this, so hopefully this will be incorporated in a future release of Ruby.

Patch

--- pstore.rb	2004-10-05 11:52:51.965332800 +0200
+++ pstore.rb.unpatched	2004-07-03 04:38:31.000000000 +0200
@@ -99,13 +99,11 @@
       content = nil
       unless read_only
         file = File.open(@filename, File::RDWR | File::CREAT)
-        file.binmode
         file.flock(File::LOCK_EX)
         commit_new(file) if <a href="http://wiki.rubyonrails.org/rails/pages/FileTest" class="existingWikiWord">FileTest</a>.exist?(new_file)
         content = file.read()
       else
         file = File.open(@filename, File::RDONLY)
-        file.binmode
         file.flock(File::LOCK_SH)
         content = (File.read(new_file) rescue file.read())
       end
@@ -134,7 +132,6 @@
 	  content = dump(@table)
 	  if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
             File.open(tmp_file, "w") {|t|
-              t.binmode
               t.write(content)
             }
             File.rename(tmp_file, new_file)
@@ -169,7 +166,6 @@
     f.rewind
     new_file = @filename + ".new"
     File.open(new_file) do |nf|
-      nf.binmode
       <span class="newWikiWord">FileUtils<a href="http://wiki.rubyonrails.org/rails/pages/FileUtils">?</a></span>.copy_stream(nf, f)
     end
     File.unlink(new_file)

pstore.rb with patch above applied

Relevant lines are those containing a call to File.open.

#
# How to use:
#
# db = PStore.new("/tmp/foo")
# db.transaction do
#   p db.roots
#   ary = db["root"] = [1,2,3,4]
#   ary[0] = [1,1.5]
# end

# db.transaction do
#   p db["root"]
# end

require "fileutils"
require "digest/md5"

class PStore
  class Error < <span class="newWikiWord">StandardError<a href="http://wiki.rubyonrails.org/rails/pages/StandardError">?</a></span>
  end

  def initialize(file)
    dir = File::dirname(file)
    unless File::directory? dir
      raise PStore::Error, format("directory %s does not exist", dir)
    end
    if File::exist? file and not File::readable? file
      raise PStore::Error, format("file %s not readable", file)
    end
    @transaction = false
    @filename = file
    @abort = false
  end

  def in_transaction
    raise PStore::Error, "not in transaction" unless @transaction
  end
  def in_transaction_wr()
    in_transaction()
    raise PStore::Error, "in read-only transaction" if @rdonly
  end
  private :in_transaction, :in_transaction_wr

  def [](name)
    in_transaction
    @table[name]
  end
  def fetch(name, default=PStore::Error)
    unless @table.key? name
      if default==PStore::Error
	raise PStore::Error, format("undefined root name `%s'", name)
      else
	default
      end
    end
    self[name]
  end
  def []=(name, value)
    in_transaction_wr()
    @table[name] = value
  end
  def delete(name)
    in_transaction_wr()
    @table.delete name
  end

  def roots
    in_transaction
    @table.keys
  end
  def root?(name)
    in_transaction
    @table.key? name
  end
  def path
    @filename
  end

  def commit
    in_transaction
    @abort = false
    throw :pstore_abort_transaction
  end
  def abort
    in_transaction
    @abort = true
    throw :pstore_abort_transaction
  end

  def transaction(read_only=false)
    raise PStore::Error, "nested transaction" if @transaction
    begin
      @rdonly = read_only
      @abort = false
      @transaction = true
      value = nil
      new_file = @filename + ".new"

      content = nil
      unless read_only
        file = File.open(@filename, File::RDWR | File::CREAT)
        file.binmode
        file.flock(File::LOCK_EX)
        commit_new(file) if <a href="http://wiki.rubyonrails.org/rails/pages/FileTest" class="existingWikiWord">FileTest</a>.exist?(new_file)
        content = file.read()
      else
        file = File.open(@filename, File::RDONLY)
        file.binmode
        file.flock(File::LOCK_SH)
        content = (File.read(new_file) rescue file.read())
      end

      if content != ""
	@table = load(content)
        if !read_only
          size = content.size
          md5 = Digest::MD5.digest(content)
        end
      else
	@table = {}
      end
      content = nil		# unreference huge data

      begin
	catch(:pstore_abort_transaction) do
	  value = yield(self)
	end
      rescue Exception
	@abort = true
	raise
      ensure
	if !read_only and !@abort
          tmp_file = @filename + ".tmp"
	  content = dump(@table)
	  if !md5 || size != content.size || md5 != Digest::MD5.digest(content)
            File.open(tmp_file, "w") {|t|
              t.binmode
              t.write(content)
            }
            File.rename(tmp_file, new_file)
            commit_new(file)
          end
          content = nil		# unreference huge data
	end
      end
    ensure
      @table = nil
      @transaction = false
      file.close if file
    end
    value
  end

  def dump(table)
    Marshal::dump(table)
  end

  def load(content)
    Marshal::load(content)
  end

  def load_file(file)
    Marshal::load(file)
  end

  private
  def commit_new(f)
    f.truncate(0)
    f.rewind
    new_file = @filename + ".new"
    File.open(new_file) do |nf|
      nf.binmode
      <span class="newWikiWord">FileUtils<a href="http://wiki.rubyonrails.org/rails/pages/FileUtils">?</a></span>.copy_stream(nf, f)
    end
    File.unlink(new_file)
  end
end

if __FILE__ == $0
  db = PStore.new("/tmp/foo")
  db.transaction do
    p db.roots
    ary = db["root"] = [1,2,3,4]
    ary[1] = [1,1.5]
  end

  1000.times do
    db.transaction do
      db["root"][0] += 1
      p db["root"][0]
    end
  end

  db.transaction(true) do
    p db["root"]
  end
end


I haven’t applied this patch yet, however I did find that I could repeat the results with regularity for my application. I created an authentication module based on the “Ruby on Rails” book and modified it to use SHA256, a salt, and an active/disabled state flag. During testing of the module I noticed that after happily running along logging in and out, deleting users, etc it suddenly failed. The only value that is being stored in the session was the user id (session[:user_id] = user.id). Anytime a user with an id of 21 logged in all attempts to access the application would be meet with a 500 series error. Switching to ActiveRecord sessions appears to have solved my problem for now, haven’t looked into it further.


I’m in the same situation as the above anonymous post. My session data is a bit more complex but I’m getting the same results when accessing user id 21. Will try switching to ActiveRecord sessions.


Same problem here. Application fails to respond after setting session[:user_id] = user.id when user.id = 21.

—-
I report the same problem.
ynw

—-
2007-07-20 Same problem here. Currently looking into it.
—-