[rubygems/rubygems] Verified working on mri/jruby/truffleruby with specs on rubygems.org

https://github.com/rubygems/rubygems/commit/4f51741cc6
This commit is contained in:
Samuel Giddins 2023-08-20 10:42:20 -07:00 коммит произвёл git
Родитель cdcc760dc0
Коммит 1fff3e44ff
4 изменённых файлов: 201 добавлений и 70 удалений

Просмотреть файл

@ -11,8 +11,9 @@ module Gem
module SafeMarshal module SafeMarshal
PERMITTED_CLASSES = %w[ PERMITTED_CLASSES = %w[
Time
Date Date
Time
Rational
Gem::Dependency Gem::Dependency
Gem::NameTuple Gem::NameTuple
@ -28,45 +29,39 @@ module Gem
private_constant :PERMITTED_CLASSES private_constant :PERMITTED_CLASSES
PERMITTED_SYMBOLS = %w[ PERMITTED_SYMBOLS = %w[
E
offset
zone
nano_num
nano_den
submicro
@_zone
@cpu
@debug_created_info
@force_ruby_platform
@marshal_with_utc_coercion
@name
@os
@platform
@prerelease
@requirement
@taguri
@type
@type_id
@value
@version
@version_requirement
@version_requirements
development development
runtime runtime
].freeze ].freeze
private_constant :PERMITTED_SYMBOLS private_constant :PERMITTED_SYMBOLS
PERMITTED_IVARS = {
"String" => %w[E @taguri @debug_created_info],
"Time" => %w[
offset zone nano_num nano_den submicro
@_zone @marshal_with_utc_coercion
],
"Gem::Dependency" => %w[
@name @requirement @prerelease @version_requirement @version_requirements @type
@force_ruby_platform
],
"Gem::NameTuple" => %w[@name @version @platform],
"Gem::Platform" => %w[@os @cpu @version],
"Psych::PrivateType" => %w[@value @type_id],
}.freeze
private_constant :PERMITTED_IVARS
def self.safe_load(input) def self.safe_load(input)
load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS) load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, permitted_ivars: PERMITTED_IVARS)
end end
def self.load(input, permitted_classes: [::Symbol], permitted_symbols: []) def self.load(input, permitted_classes: [::Symbol], permitted_symbols: [], permitted_ivars: {})
root = Reader.new(StringIO.new(input, "r")).read! root = Reader.new(StringIO.new(input, "r")).read!
Visitors::ToRuby.new(permitted_classes: permitted_classes, permitted_symbols: permitted_symbols).visit(root) Visitors::ToRuby.new(
permitted_classes: permitted_classes,
permitted_symbols: permitted_symbols,
permitted_ivars: permitted_ivars,
).visit(root)
end end
end end
end end

Просмотреть файл

@ -0,0 +1,31 @@
# frozen_string_literal: true
require_relative "visitor"
module Gem::SafeMarshal
module Visitors
class StreamPrinter < Visitor
def initialize(io, indent: "")
@io = io
@indent = indent
@level = 0
end
def visit(target)
@io.write("#{@indent * @level}#{target.class}")
target.instance_variables.each do |ivar|
value = target.instance_variable_get(ivar)
next if Elements::Element === value || Array === value
@io.write(" #{ivar}=#{value.inspect}")
end
@io.write("\n")
begin
@level += 1
super
ensure
@level -= 1
end
end
end
end
end

Просмотреть файл

@ -5,9 +5,10 @@ require_relative "visitor"
module Gem::SafeMarshal module Gem::SafeMarshal
module Visitors module Visitors
class ToRuby < Visitor class ToRuby < Visitor
def initialize(permitted_classes:, permitted_symbols:) def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:)
@permitted_classes = permitted_classes @permitted_classes = permitted_classes
@permitted_symbols = permitted_symbols | permitted_classes | ["E"] @permitted_symbols = permitted_symbols | permitted_classes | ["E"]
@permitted_ivars = permitted_ivars
@objects = [] @objects = []
@symbols = [] @symbols = []
@ -17,7 +18,8 @@ module Gem::SafeMarshal
end end
def inspect # :nodoc: def inspect # :nodoc:
format("#<%s permitted_classes: %p permitted_symbols: %p>", self.class, @permitted_classes, @permitted_symbols) format("#<%s permitted_classes: %p permitted_symbols: %p permitted_ivars: %p>",
self.class, @permitted_classes, @permitted_symbols, @permitted_ivars)
end end
def visit(target) def visit(target)
@ -37,14 +39,16 @@ module Gem::SafeMarshal
end end
def visit_Gem_SafeMarshal_Elements_Symbol(s) def visit_Gem_SafeMarshal_Elements_Symbol(s)
resolve_symbol(s.name) name = s.name
raise UnpermittedSymbolError.new(symbol: name, stack: @stack.dup) unless @permitted_symbols.include?(name)
visit_symbol_type(s)
end end
def map_ivars(ivars) def map_ivars(klass, ivars)
ivars.map.with_index do |(k, v), i| ivars.map.with_index do |(k, v), i|
@stack << "ivar #{i}" @stack << "ivar_#{i}"
k = visit(k) k = resolve_ivar(klass, k)
@stack << k @stack[-1] = k
next k, visit(v) next k, visit(v)
end end
end end
@ -54,12 +58,12 @@ module Gem::SafeMarshal
object_offset = @objects.size object_offset = @objects.size
@stack << "object" @stack << "object"
object = visit(e.object) object = visit(e.object)
ivars = map_ivars(e.ivars) ivars = map_ivars(object.class, e.ivars)
case e.object case e.object
when Elements::UserDefined when Elements::UserDefined
if object.class == ::Time if object.class == ::Time
offset = zone = nano_num = nano_den = nil offset = zone = nano_num = nano_den = submicro = nil
ivars.reject! do |k, v| ivars.reject! do |k, v|
case k case k
when :offset when :offset
@ -71,6 +75,7 @@ module Gem::SafeMarshal
when :nano_den when :nano_den
nano_den = v nano_den = v
when :submicro when :submicro
submicro = v
else else
next false next false
end end
@ -80,17 +85,23 @@ module Gem::SafeMarshal
if (nano_den || nano_num) && !(nano_den && nano_num) if (nano_den || nano_num) && !(nano_den && nano_num)
raise FormatError, "Must have all of nano_den, nano_num for Time #{e.pretty_inspect}" raise FormatError, "Must have all of nano_den, nano_num for Time #{e.pretty_inspect}"
elsif nano_den && nano_num elsif nano_den && nano_num
if RUBY_ENGINE == "jruby"
nano = Rational(nano_num, nano_den * 1_000_000_000)
object = Time.at(object.to_i + nano + object.subsec)
elsif RUBY_ENGINE == "truffleruby"
object = Time.at(object.to_i, Rational(nano_num, nano_den).to_i, :nanosecond)
else # assume "ruby"
nano = Rational(nano_num, nano_den) nano = Rational(nano_num, nano_den)
nsec, subnano = nano.divmod(1) nsec, subnano = nano.divmod(1)
nano = nsec + subnano nano = nsec + subnano
object = Time.at(object.to_r, nano, :nanosecond) object = Time.at(object.to_r, nano, :nanosecond)
end end
end
if zone if zone
require "time" require "time"
zone = "+0000" if zone == "UTC" && offset == 0 zone = "+0000" if zone == "UTC" && offset == 0
Time.send(:force_zone!, object, zone, offset) call_method(Time, :force_zone!, object, zone, offset)
elsif offset elsif offset
object = object.localtime offset object = object.localtime offset
end end
@ -157,14 +168,23 @@ module Gem::SafeMarshal
end end
def visit_Gem_SafeMarshal_Elements_UserDefined(o) def visit_Gem_SafeMarshal_Elements_UserDefined(o)
register_object(resolve_class(o.name).send(:_load, o.binary_string)) register_object(call_method(resolve_class(o.name), :_load, o.binary_string))
end end
def visit_Gem_SafeMarshal_Elements_UserMarshal(o) def visit_Gem_SafeMarshal_Elements_UserMarshal(o)
register_object(resolve_class(o.name).allocate).tap do |object| klass = resolve_class(o.name)
compat = COMPAT_CLASSES.fetch(klass, nil)
idx = @objects.size
object = register_object(call_method(compat || klass, :allocate))
@stack << :data @stack << :data
object.marshal_load visit(o.data) ret = call_method(object, :marshal_load, visit(o.data))
if compat
object = @objects[idx] = ret
end end
object
end end
def visit_Gem_SafeMarshal_Elements_Integer(i) def visit_Gem_SafeMarshal_Elements_Integer(i)
@ -218,16 +238,9 @@ module Gem::SafeMarshal
def resolve_class(n) def resolve_class(n)
@class_cache[n] ||= begin @class_cache[n] ||= begin
name = nil to_s = resolve_symbol_name(n)
case n raise UnpermittedClassError.new(name: to_s, stack: @stack.dup) unless @permitted_classes.include?(to_s)
when Elements::Symbol, Elements::SymbolLink visit_symbol_type(n)
@stack << "class name"
name = visit(n)
else
raise FormatError, "Class names must be Symbol or SymbolLink"
end
to_s = name.to_s
raise UnpermittedClassError.new(name: name, stack: @stack.dup) unless @permitted_classes.include?(to_s)
begin begin
::Object.const_get(to_s) ::Object.const_get(to_s)
rescue NameError rescue NameError
@ -236,11 +249,47 @@ module Gem::SafeMarshal
end end
end end
def resolve_symbol(name) class RationalCompat
raise UnpermittedSymbolError.new(symbol: name, stack: @stack.dup) unless @permitted_symbols.include?(name) def marshal_load(s)
sym = name.to_sym num, den = s
raise ArgumentError, "Expected 2 ints" unless s.size == 2 && num.is_a?(Integer) && den.is_a?(Integer)
Rational(num, den)
end
end
COMPAT_CLASSES = {}.tap do |h|
h[Rational] = RationalCompat if RUBY_VERSION >= "3"
end.freeze
private_constant :COMPAT_CLASSES
def resolve_ivar(klass, name)
to_s = resolve_symbol_name(name)
raise UnpermittedIvarError.new(symbol: to_s, klass: klass, stack: @stack.dup) unless @permitted_ivars.fetch(klass.name, [].freeze).include?(to_s)
visit_symbol_type(name)
end
def visit_symbol_type(element)
case element
when Elements::Symbol
sym = element.name.to_sym
@symbols << sym @symbols << sym
sym sym
when Elements::SymbolLink
visit_Gem_SafeMarshal_Elements_SymbolLink(element)
end
end
def resolve_symbol_name(element)
case element
when Elements::Symbol
element.name
when Elements::SymbolLink
visit_Gem_SafeMarshal_Elements_SymbolLink(element).to_s
else
raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{@stack.join(".")}"
end
end end
def register_object(o) def register_object(o)
@ -248,6 +297,14 @@ module Gem::SafeMarshal
o o
end end
def call_method(receiver, method, *args)
receiver.__send__(method, *args)
rescue NoMethodError => e
raise unless e.receiver == receiver
raise MethodCallError, "Unable to call #{method.inspect} on #{receiver.inspect}, perhaps it is a class using marshal compat, which is not visible in ruby? #{e}"
end
class UnpermittedSymbolError < StandardError class UnpermittedSymbolError < StandardError
def initialize(symbol:, stack:) def initialize(symbol:, stack:)
@symbol = symbol @symbol = symbol
@ -256,6 +313,15 @@ module Gem::SafeMarshal
end end
end end
class UnpermittedIvarError < StandardError
def initialize(symbol:, klass:, stack:)
@symbol = symbol
@klass = klass
@stack = stack
super "Attempting to set unpermitted ivar #{symbol.inspect} on object of class #{klass} @ #{stack.join "."}"
end
end
class UnpermittedClassError < StandardError class UnpermittedClassError < StandardError
def initialize(name:, stack:) def initialize(name:, stack:)
@name = name @name = name
@ -266,6 +332,9 @@ module Gem::SafeMarshal
class FormatError < StandardError class FormatError < StandardError
end end
class MethodCallError < StandardError
end
end end
end end
end end

Просмотреть файл

@ -20,8 +20,10 @@ class TestGemSafeMarshal < Gem::TestCase
def test_recursive_string def test_recursive_string
s = String.new("hello") s = String.new("hello")
s.instance_variable_set(:@type, s) s.instance_variable_set(:@type, s)
with_const(Gem::SafeMarshal, :PERMITTED_IVARS, { "String" => %w[@type E] }) do
assert_safe_load_as s, additional_methods: [:instance_variables] assert_safe_load_as s, additional_methods: [:instance_variables]
end end
end
def test_recursive_array def test_recursive_array
a = [] a = []
@ -39,11 +41,17 @@ class TestGemSafeMarshal < Gem::TestCase
end end
def test_string_with_ivar def test_string_with_ivar
assert_safe_load_as String.new("abc").tap {|s| s.instance_variable_set :@type, "type" } str = String.new("abc")
str.instance_variable_set :@type, "type"
with_const(Gem::SafeMarshal, :PERMITTED_IVARS, { "String" => %w[@type E] }) do
assert_safe_load_as str
end
end end
def test_time_with_ivar def test_time_with_ivar
assert_safe_load_as Time.new.tap {|t| t.instance_variable_set :@type, "type" } with_const(Gem::SafeMarshal, :PERMITTED_IVARS, { "Time" => %w[@type offset zone nano_num nano_den submicro], "String" => "E" }) do
assert_safe_load_as Time.new.tap {|t| t.instance_variable_set :@type, :runtime }
end
end end
secs = Time.new(2000, 12, 31, 23, 59, 59).to_i secs = Time.new(2000, 12, 31, 23, 59, 59).to_i
@ -64,7 +72,7 @@ class TestGemSafeMarshal < Gem::TestCase
Time.at(secs, 1.01, :nanosecond), Time.at(secs, 1.01, :nanosecond),
Time.at(secs, 1.001, :nanosecond), Time.at(secs, 1.001, :nanosecond),
Time.at(secs, 1.00001, :nanosecond), Time.at(secs, 1.00001, :nanosecond),
Time.at(secs, 1.00001, :nanosecond).tap {|t| t.instance_variable_set :@type, "type" }, Time.at(secs, 1.00001, :nanosecond),
].each_with_index do |t, i| ].each_with_index do |t, i|
define_method("test_time_#{i} #{t.inspect}") do define_method("test_time_#{i} #{t.inspect}") do
assert_safe_load_as t, additional_methods: [:ctime, :to_f, :to_r, :to_i, :zone, :subsec, :instance_variables, :dst?, :to_a] assert_safe_load_as t, additional_methods: [:ctime, :to_f, :to_r, :to_i, :zone, :subsec, :instance_variables, :dst?, :to_a]
@ -79,19 +87,33 @@ class TestGemSafeMarshal < Gem::TestCase
end end
def test_hash_with_ivar def test_hash_with_ivar
assert_safe_load_as({ runtime: :development }.tap {|h| h.instance_variable_set :@type, "null" }) h = { runtime: :development }
h.instance_variable_set :@type, []
with_const(Gem::SafeMarshal, :PERMITTED_IVARS, { "Hash" => %w[@type] }) do
assert_safe_load_as(h)
end
end end
def test_hash_with_default_value def test_hash_with_default_value
assert_safe_load_as Hash.new([]) assert_safe_load_as Hash.new([])
end end
def test_hash_with_compare_by_identity
pend "`read_user_class` not yet implemented"
assert_safe_load_as Hash.new.compare_by_identity
end
def test_frozen_object def test_frozen_object
assert_safe_load_as Gem::Version.new("1.abc").freeze assert_safe_load_as Gem::Version.new("1.abc").freeze
end end
def test_date def test_date
assert_safe_load_as Date.new assert_safe_load_as Date.new(1994, 12, 9)
end
def test_rational
assert_safe_load_as Rational(1, 3)
end end
[ [
@ -142,4 +164,18 @@ class TestGemSafeMarshal < Gem::TestCase
end end
assert_equal Marshal.dump(loaded), Marshal.dump(safe_loaded), "should Marshal.dump the same" assert_equal Marshal.dump(loaded), Marshal.dump(safe_loaded), "should Marshal.dump the same"
end end
def with_const(mod, name, new_value, &block)
orig = mod.const_get(name)
mod.send :remove_const, name
mod.const_set name, new_value
begin
yield
ensure
mod.send :remove_const, name
mod.const_set name, orig
mod.send :private_constant, name
end
end
end end