New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace Marshal.load
with a fully-checked safe gemspec loader
#6896
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for doing this! Ensuring that we can load marshaled specs safely has been on our list of good ideas that seem hard for several years. 🎉
cee54aa
to
b206cba
Compare
b206cba
to
4f51741
Compare
Tested with: `ruby -e 'trap("INT") { exit 1 }; TZ=%w[UTC +0000 -0000]; RUBY=%w[ruby-2.7 ruby-3.2.2 jruby-9.4 truffleruby-22 truffleruby-23]; TZ.product(RUBY).each { |t, r| puts ?**120, "TZ=#{t} RUBY=#{r}", "*"*120; system({"TZ"=>t,"RUBY"=>r}, *ARGV) }' zsh -lic 'chruby $RUBY; ruby -vw -Ilib test/rubygems/test_gem_safe_marshal.rb --verbose=progress'`
Specs here finally passed! @deivid-rodriguez if you want to take a look |
Updated the PR title and description with an explanation of the problem and how this PR solves the problem. 👍🏻 |
|
||
private | ||
|
||
MARSHAL_VERSION = [Marshal::MAJOR_VERSION, Marshal::MINOR_VERSION].map(&:chr).join.freeze |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand it well, this reader implements Marshal 4.8. Would it make sense to hardcode this value in here instead of following original Marshal version? Latest Marshal minor update happened 21 years ago at ruby/ruby@73a4724, but in any case marshal is being updated, there is no guarantee this reader will work as intended.
assert_safe_load_marshal(dumped, equality: equality, **kwargs) | ||
end | ||
|
||
def with_const(mod, name, new_value, &block) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is used to temporarily modify PERMITTED_IVARS
of Gem::SafeMarshal
, since that is used in safe_load
method. Wouldn't be easier using Gem::SafeMarshal.load
with permitted_ivars
attribute for such a purpose directly?
2**16, 2**16 - 1, 2**20 - 1, | ||
2**28, 2**28 - 1, | ||
2**32, 2**32 - 1, | ||
2**63, 2**63 - 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any specific reason for those numbers? Maybe comment would be handy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is HUUUUUGE! Big applause for reporting found issues in JRuby and Truffleruby, Truffleruby one seems already fixed 💪 (unreleased).
I can imagine packing this as separate gem at some point and vendoring back using Automatiek, since it can be super useful outside of RubyGems/Bundler.
Is there any plan on testing this on real gems in bulk?
I tested it on every gemspec in rubygems.org on ruby 2.7, 3.2, jruby 9.4, and truffleruby 23. only a few gemspecs could not load because they used some random symbols, all were very old (plus another few that can't properly be downloaded) |
I was thinking to test this against top 100/1000 gems by download (all versions, to include also old releases) in bulk somehow. |
@simi It seems like a lot of work to dynamically calculate the most popular gems and download their gemspecs in a test... since we have tested against existing gemspecs, maybe we add a few existing marshaled gemspecs to the system tests, and add some code that verifies gemspecs can go through |
I meant this for one off test before release to ensure everything works properly. Clearly this is not good idea to add to test suite or CI. I can craft something locally and report back. |
@simi ohh ok! I think we are good then, since @segiddins has already tested every gemspec for every gem? |
Uhh, I have missed that. By |
I have fixed one of the issues reported. Thanks for letting us know! Obviously there's still gaps in Marshal specs and tests. |
Replace Marshal.load with a fully-checked safe gemspec loader (cherry picked from commit 622bbdd)
Replace Marshal.load with a fully-checked safe gemspec loader (cherry picked from commit 622bbdd)
Replace Marshal.load with a fully-checked safe gemspec loader (cherry picked from commit 622bbdd)
Marshal.load
with a fully-checked safe gemspec loader
The validate_vendored_ruby test started failing on AIX 7.2 when loading the gemspecs from artifactory: # /opt/puppetlabs/puppet/bin/gem update --system -V GET https://rubygems.org/specs.4.8.gz 304 Not Modified GET https://artifactory.delivery.puppetlabs.net/artifactory/api/gems/rubygems/specs.4.8.gz 304 Not Modified [FATAL] failed to allocate memory It appears the new behavior is triggered when using the new safe marshall code introduced in rubygems-update[1]: # truss -f -i -t kopen /opt/puppetlabs/puppet/bin/gem update --system ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/rubygems.org%443/specs.4.8", 0440001401, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH |S_IWOTH) = 9 10813880: 11338039: kopen("/opt/puppetlabs/puppet/lib/ruby/site_ruby/3.2.0/rubygems/safe_marshal.rb", 0440000004) = 9 ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/artifactory.delivery.puppetlabs.net%443/artifactory/api/gems/rubygems/specs.4.8" , 0440001401, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) = 10 [FATAL] failed to allocate memory Pin back to an older rubygems-update on AIX 7.2. [1] rubygems/rubygems#6896
The validate_vendored_ruby test started failing on AIX 7.2 when loading the gemspecs from artifactory: # /opt/puppetlabs/puppet/bin/gem update --system -V GET https://rubygems.org/specs.4.8.gz 304 Not Modified GET https://artifactory.delivery.puppetlabs.net/artifactory/api/gems/rubygems/specs.4.8.gz 304 Not Modified [FATAL] failed to allocate memory It appears the new behavior is triggered when using the new safe marshall code introduced in rubygems-update[1]: # truss -f -i -t kopen /opt/puppetlabs/puppet/bin/gem update --system ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/rubygems.org%443/specs.4.8", 0440001401, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH |S_IWOTH) = 9 10813880: 11338039: kopen("/opt/puppetlabs/puppet/lib/ruby/site_ruby/3.2.0/rubygems/safe_marshal.rb", 0440000004) = 9 ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/artifactory.delivery.puppetlabs.net%443/artifactory/api/gems/rubygems/specs.4.8" [FATAL] failed to allocate memory Pin back to an older rubygems-update on AIX 7.2. [1] rubygems/rubygems#6896
The validate_vendored_ruby test started failing on AIX 7.2 when loading the gemspecs from artifactory: # /opt/puppetlabs/puppet/bin/gem update --system -V GET https://rubygems.org/specs.4.8.gz 304 Not Modified GET https://artifactory.delivery.puppetlabs.net/artifactory/api/gems/rubygems/specs.4.8.gz 304 Not Modified [FATAL] failed to allocate memory It appears the new behavior is triggered when using the new safe marshall code introduced in rubygems-update[1]: # truss -f -i -t kopen /opt/puppetlabs/puppet/bin/gem update --system ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/rubygems.org%443/specs.4.8", 0440000002) = 9 10813880: 11338039: kopen("/opt/puppetlabs/puppet/lib/ruby/site_ruby/3.2.0/rubygems/safe_marshal.rb", 0440000004) = 9 ... 10813880: 11338039: kopen("/root/.local/share/gem/specs/artifactory.delivery.puppetlabs.net%443/artifactory/api/gems/rubygems/specs.4.8", 0440000004) = 10 [FATAL] failed to allocate memory Pin back to an older rubygems-update on AIX 7.2. [1] rubygems/rubygems#6896
What was the end-user or developer problem that led to this PR?
Ruby has historically had many security issues while loading serialized YAML or Marshal data, with the worst outcome being remote code execution. RubyGems and Bundler both load gemspecs from gem source URLs provided by users. To prevent exploits by malicious gem servers against developer machines installing gems, we need to load only a safe subset of Ruby classes and objects.
Previously, we implemented
YAML.safe_load
to prevent exploits by malicious gem servers via crafted YAML files. Until now, we have not been able to provide the same defenses for marshaled gemspecs due to the lack of aMarshal.safe_load
.This PR implements
Marshal.safe_load
, a loader for marshaled data that excludes unknown objects and classes. The loader prevents exploits by malicious gem servers, or crafted marshaled data provided to RubyGems or Bundler.What is your fix for the problem, implemented in this PR?
Implement a new reader / AST visitor for Marshal documents, following https://rubyreferences.github.io/rubyref/builtin/marshal.html and using the
Psych
ToRuby
visitor for inspiration.As a followup, will conditionally use the new module in Bundler
Make sure the following tasks are checked