[rubygems/rubygems] Aggressively optimize allocations in SafeMarshal
Reduces allocations in a bundle install --full-index by an order of magnitude Main wins are (a) getting rid of exessive string allocations for exception message stack (b) Avoiding hash allocations caused by kwargs for #initialize (c) avoid using unpack to do bit math, its easy enough to do by hand (d) special case the most common elements so they can be read without an allocation (e) avoid string allocations every time a symbol->string lookup is done by using symbol#name https://github.com/rubygems/rubygems/commit/7d2ee51402
This commit is contained in:
parent
a49d17a080
commit
0a423d4c4e
@ -7,14 +7,14 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Symbol < Element
|
class Symbol < Element
|
||||||
def initialize(name:)
|
def initialize(name)
|
||||||
@name = name
|
@name = name
|
||||||
end
|
end
|
||||||
attr_reader :name
|
attr_reader :name
|
||||||
end
|
end
|
||||||
|
|
||||||
class UserDefined < Element
|
class UserDefined < Element
|
||||||
def initialize(name:, binary_string:)
|
def initialize(name, binary_string)
|
||||||
@name = name
|
@name = name
|
||||||
@binary_string = binary_string
|
@binary_string = binary_string
|
||||||
end
|
end
|
||||||
@ -23,7 +23,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class UserMarshal < Element
|
class UserMarshal < Element
|
||||||
def initialize(name:, data:)
|
def initialize(name, data)
|
||||||
@name = name
|
@name = name
|
||||||
@data = data
|
@data = data
|
||||||
end
|
end
|
||||||
@ -32,7 +32,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class String < Element
|
class String < Element
|
||||||
def initialize(str:)
|
def initialize(str)
|
||||||
@str = str
|
@str = str
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -40,7 +40,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Hash < Element
|
class Hash < Element
|
||||||
def initialize(pairs:)
|
def initialize(pairs)
|
||||||
@pairs = pairs
|
@pairs = pairs
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class HashWithDefaultValue < Hash
|
class HashWithDefaultValue < Hash
|
||||||
def initialize(default:, **kwargs)
|
def initialize(pairs, default)
|
||||||
super(**kwargs)
|
super(pairs)
|
||||||
@default = default
|
@default = default
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Array < Element
|
class Array < Element
|
||||||
def initialize(elements:)
|
def initialize(elements)
|
||||||
@elements = elements
|
@elements = elements
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -65,7 +65,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Integer < Element
|
class Integer < Element
|
||||||
def initialize(int:)
|
def initialize(int)
|
||||||
@int = int
|
@int = int
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class WithIvars < Element
|
class WithIvars < Element
|
||||||
def initialize(object:,ivars:)
|
def initialize(object, ivars)
|
||||||
@object = object
|
@object = object
|
||||||
@ivars = ivars
|
@ivars = ivars
|
||||||
end
|
end
|
||||||
@ -95,7 +95,7 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Object < Element
|
class Object < Element
|
||||||
def initialize(name:)
|
def initialize(name)
|
||||||
@name = name
|
@name = name
|
||||||
end
|
end
|
||||||
attr_reader :name
|
attr_reader :name
|
||||||
@ -106,28 +106,28 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
class ObjectLink < Element
|
class ObjectLink < Element
|
||||||
def initialize(offset:)
|
def initialize(offset)
|
||||||
@offset = offset
|
@offset = offset
|
||||||
end
|
end
|
||||||
attr_reader :offset
|
attr_reader :offset
|
||||||
end
|
end
|
||||||
|
|
||||||
class SymbolLink < Element
|
class SymbolLink < Element
|
||||||
def initialize(offset:)
|
def initialize(offset)
|
||||||
@offset = offset
|
@offset = offset
|
||||||
end
|
end
|
||||||
attr_reader :offset
|
attr_reader :offset
|
||||||
end
|
end
|
||||||
|
|
||||||
class Float < Element
|
class Float < Element
|
||||||
def initialize(string:)
|
def initialize(string)
|
||||||
@string = string
|
@string = string
|
||||||
end
|
end
|
||||||
attr_reader :string
|
attr_reader :string
|
||||||
end
|
end
|
||||||
|
|
||||||
class Bignum < Element # rubocop:disable Lint/UnifiedInteger
|
class Bignum < Element # rubocop:disable Lint/UnifiedInteger
|
||||||
def initialize(sign:, data:)
|
def initialize(sign, data)
|
||||||
@sign = sign
|
@sign = sign
|
||||||
@data = data
|
@data = data
|
||||||
end
|
end
|
||||||
|
@ -49,19 +49,19 @@ module Gem
|
|||||||
when 0x00
|
when 0x00
|
||||||
0
|
0
|
||||||
when 0x01
|
when 0x01
|
||||||
@io.read(1).unpack1("C")
|
read_byte
|
||||||
when 0x02
|
when 0x02
|
||||||
@io.read(2).unpack1("S<")
|
read_byte | (read_byte << 8)
|
||||||
when 0x03
|
when 0x03
|
||||||
(@io.read(3) + "\0").unpack1("L<")
|
read_byte | (read_byte << 8) | (read_byte << 16)
|
||||||
when 0x04
|
when 0x04
|
||||||
@io.read(4).unpack1("L<")
|
read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24)
|
||||||
when 0xFC
|
when 0xFC
|
||||||
@io.read(4).unpack1("L<") | -0x100000000
|
read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24) | -0x100000000
|
||||||
when 0xFD
|
when 0xFD
|
||||||
(@io.read(3) + "\0").unpack1("L<") | -0x1000000
|
read_byte | (read_byte << 8) | (read_byte << 16) | -0x1000000
|
||||||
when 0xFE
|
when 0xFE
|
||||||
@io.read(2).unpack1("s<") | -0x10000
|
read_byte | (read_byte << 8) | -0x10000
|
||||||
when 0xFF
|
when 0xFF
|
||||||
read_byte | -0x100
|
read_byte | -0x100
|
||||||
else
|
else
|
||||||
@ -88,31 +88,51 @@ module Gem
|
|||||||
when 85 then read_user_marshal # ?U
|
when 85 then read_user_marshal # ?U
|
||||||
when 91 then read_array # ?[
|
when 91 then read_array # ?[
|
||||||
when 102 then read_float # ?f
|
when 102 then read_float # ?f
|
||||||
when 105 then Elements::Integer.new int: read_integer # ?i
|
when 105 then Elements::Integer.new(read_integer) # ?i
|
||||||
when 108 then read_bignum # ?l
|
when 108 then read_bignum # ?l
|
||||||
when 111 then read_object # ?o
|
when 111 then read_object # ?o
|
||||||
when 117 then read_user_defined # ?u
|
when 117 then read_user_defined # ?u
|
||||||
when 123 then read_hash # ?{
|
when 123 then read_hash # ?{
|
||||||
when 125 then read_hash_with_default_value # ?}
|
when 125 then read_hash_with_default_value # ?}
|
||||||
when "e".ord then read_extended_object
|
when 101 then read_extended_object # ?e
|
||||||
when "c".ord then read_class
|
when 99 then read_class # ?c
|
||||||
when "m".ord then read_module
|
when 109 then read_module # ?m
|
||||||
when "M".ord then read_class_or_module
|
when 77 then read_class_or_module # ?M
|
||||||
when "d".ord then read_data
|
when 100 then read_data # ?d
|
||||||
when "/".ord then read_regexp
|
when 47 then read_regexp # ?/
|
||||||
when "S".ord then read_struct
|
when 83 then read_struct # ?S
|
||||||
when "C".ord then read_user_class
|
when 67 then read_user_class # ?C
|
||||||
else
|
else
|
||||||
raise Error, "Unknown marshal type discriminator #{type.chr.inspect} (#{type})"
|
raise Error, "Unknown marshal type discriminator #{type.chr.inspect} (#{type})"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
STRING_E_SYMBOL = Elements::Symbol.new("E").freeze
|
||||||
|
private_constant :STRING_E_SYMBOL
|
||||||
|
|
||||||
def read_symbol
|
def read_symbol
|
||||||
Elements::Symbol.new name: @io.read(read_integer)
|
len = read_integer
|
||||||
|
if len == 1
|
||||||
|
byte = read_byte
|
||||||
|
if byte == 69 # ?E
|
||||||
|
STRING_E_SYMBOL
|
||||||
|
else
|
||||||
|
Elements::Symbol.new(byte.chr)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
name = -@io.read(len)
|
||||||
|
Elements::Symbol.new(name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EMPTY_STRING = Elements::String.new("".b.freeze).freeze
|
||||||
|
private_constant :EMPTY_STRING
|
||||||
|
|
||||||
def read_string
|
def read_string
|
||||||
Elements::String.new(str: @io.read(read_integer))
|
length = read_integer
|
||||||
|
return EMPTY_STRING if length == 0
|
||||||
|
str = @io.read(length)
|
||||||
|
Elements::String.new(str)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_true
|
def read_true
|
||||||
@ -124,55 +144,108 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def read_user_defined
|
def read_user_defined
|
||||||
Elements::UserDefined.new(name: read_element, binary_string: @io.read(read_integer))
|
name = read_element
|
||||||
|
binary_string = @io.read(read_integer)
|
||||||
|
Elements::UserDefined.new(name, binary_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EMPTY_ARRAY = Elements::Array.new([].freeze).freeze
|
||||||
|
private_constant :EMPTY_ARRAY
|
||||||
|
|
||||||
def read_array
|
def read_array
|
||||||
Elements::Array.new(elements: Array.new(read_integer) do |_i|
|
length = read_integer
|
||||||
read_element
|
return EMPTY_ARRAY if length == 0
|
||||||
end)
|
elements = Array.new(length) do
|
||||||
|
read_element
|
||||||
|
end
|
||||||
|
Elements::Array.new(elements)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_object_with_ivars
|
def read_object_with_ivars
|
||||||
Elements::WithIvars.new(object: read_element, ivars:
|
object = read_element
|
||||||
Array.new(read_integer) do
|
ivars = Array.new(read_integer) do
|
||||||
[read_element, read_element]
|
[read_element, read_element]
|
||||||
end)
|
end
|
||||||
|
Elements::WithIvars.new(object, ivars)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_symbol_link
|
def read_symbol_link
|
||||||
Elements::SymbolLink.new offset: read_integer
|
offset = read_integer
|
||||||
|
Elements::SymbolLink.new(offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_user_marshal
|
def read_user_marshal
|
||||||
Elements::UserMarshal.new(name: read_element, data: read_element)
|
name = read_element
|
||||||
|
data = read_element
|
||||||
|
Elements::UserMarshal.new(name, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# profiling bundle install --full-index shows that
|
||||||
|
# offset 6 is by far the most common object link,
|
||||||
|
# so we special case it to avoid allocating a new
|
||||||
|
# object a third of the time.
|
||||||
|
# the following are all the object links that
|
||||||
|
# appear more than 10000 times in my profiling
|
||||||
|
|
||||||
|
OBJECT_LINKS = {
|
||||||
|
6 => Elements::ObjectLink.new(6).freeze,
|
||||||
|
30 => Elements::ObjectLink.new(30).freeze,
|
||||||
|
81 => Elements::ObjectLink.new(81).freeze,
|
||||||
|
34 => Elements::ObjectLink.new(34).freeze,
|
||||||
|
38 => Elements::ObjectLink.new(38).freeze,
|
||||||
|
50 => Elements::ObjectLink.new(50).freeze,
|
||||||
|
91 => Elements::ObjectLink.new(91).freeze,
|
||||||
|
42 => Elements::ObjectLink.new(42).freeze,
|
||||||
|
46 => Elements::ObjectLink.new(46).freeze,
|
||||||
|
150 => Elements::ObjectLink.new(150).freeze,
|
||||||
|
100 => Elements::ObjectLink.new(100).freeze,
|
||||||
|
104 => Elements::ObjectLink.new(104).freeze,
|
||||||
|
108 => Elements::ObjectLink.new(108).freeze,
|
||||||
|
242 => Elements::ObjectLink.new(242).freeze,
|
||||||
|
246 => Elements::ObjectLink.new(246).freeze,
|
||||||
|
139 => Elements::ObjectLink.new(139).freeze,
|
||||||
|
143 => Elements::ObjectLink.new(143).freeze,
|
||||||
|
114 => Elements::ObjectLink.new(114).freeze,
|
||||||
|
308 => Elements::ObjectLink.new(308).freeze,
|
||||||
|
200 => Elements::ObjectLink.new(200).freeze,
|
||||||
|
54 => Elements::ObjectLink.new(54).freeze,
|
||||||
|
62 => Elements::ObjectLink.new(62).freeze,
|
||||||
|
1_286_245 => Elements::ObjectLink.new(1_286_245).freeze,
|
||||||
|
}.freeze
|
||||||
|
private_constant :OBJECT_LINKS
|
||||||
|
|
||||||
def read_object_link
|
def read_object_link
|
||||||
Elements::ObjectLink.new(offset: read_integer)
|
offset = read_integer
|
||||||
|
OBJECT_LINKS[offset] || Elements::ObjectLink.new(offset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EMPTY_HASH = Elements::Hash.new([].freeze).freeze
|
||||||
|
private_constant :EMPTY_HASH
|
||||||
|
|
||||||
def read_hash
|
def read_hash
|
||||||
pairs = Array.new(read_integer) do
|
length = read_integer
|
||||||
|
return EMPTY_HASH if length == 0
|
||||||
|
pairs = Array.new(length) do
|
||||||
[read_element, read_element]
|
[read_element, read_element]
|
||||||
end
|
end
|
||||||
Elements::Hash.new(pairs: pairs)
|
Elements::Hash.new(pairs)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_hash_with_default_value
|
def read_hash_with_default_value
|
||||||
pairs = Array.new(read_integer) do
|
pairs = Array.new(read_integer) do
|
||||||
[read_element, read_element]
|
[read_element, read_element]
|
||||||
end
|
end
|
||||||
Elements::HashWithDefaultValue.new(pairs: pairs, default: read_element)
|
default = read_element
|
||||||
|
Elements::HashWithDefaultValue.new(pairs, default)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_object
|
def read_object
|
||||||
Elements::WithIvars.new(
|
name = read_element
|
||||||
object: Elements::Object.new(name: read_element),
|
object = Elements::Object.new(name)
|
||||||
ivars: Array.new(read_integer) do
|
ivars = Array.new(read_integer) do
|
||||||
[read_element, read_element]
|
[read_element, read_element]
|
||||||
end
|
end
|
||||||
)
|
Elements::WithIvars.new(object, ivars)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_nil
|
def read_nil
|
||||||
@ -180,11 +253,14 @@ module Gem
|
|||||||
end
|
end
|
||||||
|
|
||||||
def read_float
|
def read_float
|
||||||
Elements::Float.new string: @io.read(read_integer)
|
string = @io.read(read_integer)
|
||||||
|
Elements::Float.new(string)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_bignum
|
def read_bignum
|
||||||
Elements::Bignum.new(sign: read_byte, data: @io.read(read_integer * 2))
|
sign = read_byte
|
||||||
|
data = @io.read(read_integer * 2)
|
||||||
|
Elements::Bignum.new(sign, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_extended_object
|
def read_extended_object
|
||||||
|
@ -7,7 +7,7 @@ module Gem::SafeMarshal
|
|||||||
class ToRuby < Visitor
|
class ToRuby < Visitor
|
||||||
def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:)
|
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 = ["E"].concat(permitted_symbols).concat(permitted_classes)
|
||||||
@permitted_ivars = permitted_ivars
|
@permitted_ivars = permitted_ivars
|
||||||
|
|
||||||
@objects = []
|
@objects = []
|
||||||
@ -15,6 +15,7 @@ module Gem::SafeMarshal
|
|||||||
@class_cache = {}
|
@class_cache = {}
|
||||||
|
|
||||||
@stack = ["root"]
|
@stack = ["root"]
|
||||||
|
@stack_idx = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def inspect # :nodoc:
|
def inspect # :nodoc:
|
||||||
@ -23,39 +24,61 @@ module Gem::SafeMarshal
|
|||||||
end
|
end
|
||||||
|
|
||||||
def visit(target)
|
def visit(target)
|
||||||
depth = @stack.size
|
stack_idx = @stack_idx
|
||||||
super
|
super
|
||||||
ensure
|
ensure
|
||||||
@stack.slice!(depth.pred..)
|
@stack_idx = stack_idx - 1
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def push_stack(element)
|
||||||
|
@stack[@stack_idx] = element
|
||||||
|
@stack_idx += 1
|
||||||
|
end
|
||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_Array(a)
|
def visit_Gem_SafeMarshal_Elements_Array(a)
|
||||||
register_object([]).replace(a.elements.each_with_index.map do |e, i|
|
array = register_object([])
|
||||||
@stack << "[#{i}]"
|
|
||||||
visit(e)
|
elements = a.elements
|
||||||
end)
|
size = elements.size
|
||||||
|
idx = 0
|
||||||
|
# not idiomatic, but there's a huge number of IMEMOs allocated here, so we avoid the block
|
||||||
|
# because this is such a hot path when doing a bundle install with the full index
|
||||||
|
until idx == size
|
||||||
|
push_stack idx
|
||||||
|
array << visit(elements[idx])
|
||||||
|
idx += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
array
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_Symbol(s)
|
def visit_Gem_SafeMarshal_Elements_Symbol(s)
|
||||||
name = s.name
|
name = s.name
|
||||||
raise UnpermittedSymbolError.new(symbol: name, stack: @stack.dup) unless @permitted_symbols.include?(name)
|
raise UnpermittedSymbolError.new(symbol: name, stack: formatted_stack) unless @permitted_symbols.include?(name)
|
||||||
visit_symbol_type(s)
|
visit_symbol_type(s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def map_ivars(klass, ivars)
|
def map_ivars(klass, ivars)
|
||||||
|
stack_idx = @stack_idx
|
||||||
ivars.map.with_index do |(k, v), i|
|
ivars.map.with_index do |(k, v), i|
|
||||||
@stack << "ivar_#{i}"
|
@stack_idx = stack_idx
|
||||||
|
|
||||||
|
push_stack "ivar_"
|
||||||
|
push_stack i
|
||||||
k = resolve_ivar(klass, k)
|
k = resolve_ivar(klass, k)
|
||||||
@stack[-1] = k
|
|
||||||
|
@stack_idx = stack_idx
|
||||||
|
push_stack k
|
||||||
|
|
||||||
next k, visit(v)
|
next k, visit(v)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_WithIvars(e)
|
def visit_Gem_SafeMarshal_Elements_WithIvars(e)
|
||||||
object_offset = @objects.size
|
object_offset = @objects.size
|
||||||
@stack << "object"
|
push_stack "object"
|
||||||
object = visit(e.object)
|
object = visit(e.object)
|
||||||
ivars = map_ivars(object.class, e.ivars)
|
ivars = map_ivars(object.class, e.ivars)
|
||||||
|
|
||||||
@ -76,12 +99,18 @@ module Gem::SafeMarshal
|
|||||||
|
|
||||||
s = e.object.binary_string
|
s = e.object.binary_string
|
||||||
|
|
||||||
marshal_string = "\x04\bIu:\tTime#{(s.size + 5).chr}#{s.b}".b
|
marshal_string = "\x04\bIu:\tTime".b
|
||||||
|
marshal_string.concat(s.size + 5)
|
||||||
marshal_string << (internal.size + 5).chr
|
marshal_string << s
|
||||||
|
marshal_string.concat(internal.size + 5)
|
||||||
|
|
||||||
internal.each do |k, v|
|
internal.each do |k, v|
|
||||||
marshal_string << ":#{(k.size + 5).chr}#{k}#{Marshal.dump(v)[2..-1]}"
|
marshal_string.concat(":")
|
||||||
|
marshal_string.concat(k.size + 5)
|
||||||
|
marshal_string.concat(k.to_s)
|
||||||
|
dumped = Marshal.dump(v)
|
||||||
|
dumped[0, 2] = ""
|
||||||
|
marshal_string.concat(dumped)
|
||||||
end
|
end
|
||||||
|
|
||||||
object = @objects[object_offset] = Marshal.load(marshal_string)
|
object = @objects[object_offset] = Marshal.load(marshal_string)
|
||||||
@ -108,7 +137,7 @@ module Gem::SafeMarshal
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
object.replace ::String.new(object, encoding: enc)
|
object.force_encoding(enc) if enc
|
||||||
end
|
end
|
||||||
|
|
||||||
ivars.each do |k, v|
|
ivars.each do |k, v|
|
||||||
@ -121,9 +150,9 @@ module Gem::SafeMarshal
|
|||||||
hash = register_object({})
|
hash = register_object({})
|
||||||
|
|
||||||
o.pairs.each_with_index do |(k, v), i|
|
o.pairs.each_with_index do |(k, v), i|
|
||||||
@stack << i
|
push_stack i
|
||||||
k = visit(k)
|
k = visit(k)
|
||||||
@stack << k
|
push_stack k
|
||||||
hash[k] = visit(v)
|
hash[k] = visit(v)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -132,7 +161,7 @@ module Gem::SafeMarshal
|
|||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o)
|
def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o)
|
||||||
hash = visit_Gem_SafeMarshal_Elements_Hash(o)
|
hash = visit_Gem_SafeMarshal_Elements_Hash(o)
|
||||||
@stack << :default
|
push_stack :default
|
||||||
hash.default = visit(o.default)
|
hash.default = visit(o.default)
|
||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
@ -159,7 +188,7 @@ module Gem::SafeMarshal
|
|||||||
idx = @objects.size
|
idx = @objects.size
|
||||||
object = register_object(call_method(compat || klass, :allocate))
|
object = register_object(call_method(compat || klass, :allocate))
|
||||||
|
|
||||||
@stack << :data
|
push_stack :data
|
||||||
ret = call_method(object, :marshal_load, visit(o.data))
|
ret = call_method(object, :marshal_load, visit(o.data))
|
||||||
|
|
||||||
if compat
|
if compat
|
||||||
@ -186,7 +215,7 @@ module Gem::SafeMarshal
|
|||||||
end
|
end
|
||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_String(s)
|
def visit_Gem_SafeMarshal_Elements_String(s)
|
||||||
register_object(s.str)
|
register_object(+s.str)
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit_Gem_SafeMarshal_Elements_Float(f)
|
def visit_Gem_SafeMarshal_Elements_Float(f)
|
||||||
@ -221,7 +250,7 @@ module Gem::SafeMarshal
|
|||||||
def resolve_class(n)
|
def resolve_class(n)
|
||||||
@class_cache[n] ||= begin
|
@class_cache[n] ||= begin
|
||||||
to_s = resolve_symbol_name(n)
|
to_s = resolve_symbol_name(n)
|
||||||
raise UnpermittedClassError.new(name: to_s, stack: @stack.dup) unless @permitted_classes.include?(to_s)
|
raise UnpermittedClassError.new(name: to_s, stack: formatted_stack) unless @permitted_classes.include?(to_s)
|
||||||
visit_symbol_type(n)
|
visit_symbol_type(n)
|
||||||
begin
|
begin
|
||||||
::Object.const_get(to_s)
|
::Object.const_get(to_s)
|
||||||
@ -238,16 +267,17 @@ module Gem::SafeMarshal
|
|||||||
Rational(num, den)
|
Rational(num, den)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
private_constant :RationalCompat
|
||||||
|
|
||||||
COMPAT_CLASSES = {}.tap do |h|
|
COMPAT_CLASSES = {}.tap do |h|
|
||||||
h[Rational] = RationalCompat
|
h[Rational] = RationalCompat
|
||||||
end.freeze
|
end.compare_by_identity.freeze
|
||||||
private_constant :COMPAT_CLASSES
|
private_constant :COMPAT_CLASSES
|
||||||
|
|
||||||
def resolve_ivar(klass, name)
|
def resolve_ivar(klass, name)
|
||||||
to_s = resolve_symbol_name(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)
|
raise UnpermittedIvarError.new(symbol: to_s, klass: klass, stack: formatted_stack) unless @permitted_ivars.fetch(klass.name, [].freeze).include?(to_s)
|
||||||
|
|
||||||
visit_symbol_type(name)
|
visit_symbol_type(name)
|
||||||
end
|
end
|
||||||
@ -263,14 +293,28 @@ module Gem::SafeMarshal
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_symbol_name(element)
|
# This is a hot method, so avoid respond_to? checks on every invocation
|
||||||
case element
|
if :read.respond_to?(:name)
|
||||||
when Elements::Symbol
|
def resolve_symbol_name(element)
|
||||||
element.name
|
case element
|
||||||
when Elements::SymbolLink
|
when Elements::Symbol
|
||||||
visit_Gem_SafeMarshal_Elements_SymbolLink(element).to_s
|
element.name
|
||||||
else
|
when Elements::SymbolLink
|
||||||
raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{@stack.join(".")}"
|
visit_Gem_SafeMarshal_Elements_SymbolLink(element).name
|
||||||
|
else
|
||||||
|
raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
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} @ #{formatted_stack.join(".")}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -287,6 +331,22 @@ module Gem::SafeMarshal
|
|||||||
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}"
|
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
|
end
|
||||||
|
|
||||||
|
def formatted_stack
|
||||||
|
formatted = []
|
||||||
|
@stack[0, @stack_idx].each do |e|
|
||||||
|
if e.is_a?(Integer)
|
||||||
|
if formatted.last == "ivar_"
|
||||||
|
formatted[-1] = "ivar_#{e}"
|
||||||
|
else
|
||||||
|
formatted << "[#{e}]"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
formatted << e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
formatted
|
||||||
|
end
|
||||||
|
|
||||||
class Error < StandardError
|
class Error < StandardError
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -17,7 +17,9 @@ class TestGemSafeMarshal < Gem::TestCase
|
|||||||
define_method("test_safe_load_marshal Float 30000000.0") { assert_safe_load_marshal "\x04\bf\b3e7" }
|
define_method("test_safe_load_marshal Float 30000000.0") { assert_safe_load_marshal "\x04\bf\b3e7" }
|
||||||
define_method("test_safe_load_marshal Float -30000000.0") { assert_safe_load_marshal "\x04\bf\t-3e7" }
|
define_method("test_safe_load_marshal Float -30000000.0") { assert_safe_load_marshal "\x04\bf\t-3e7" }
|
||||||
define_method("test_safe_load_marshal Gem::Version #<Gem::Version \"1.abc\">") { assert_safe_load_marshal "\x04\bU:\x11Gem::Version[\x06I\"\n1.abc\x06:\x06ET" }
|
define_method("test_safe_load_marshal Gem::Version #<Gem::Version \"1.abc\">") { assert_safe_load_marshal "\x04\bU:\x11Gem::Version[\x06I\"\n1.abc\x06:\x06ET" }
|
||||||
define_method("test_safe_load_marshal Hash {}") { assert_safe_load_marshal "\x04\b}\x00[\x00" }
|
define_method("test_safe_load_marshal Hash {} default value") { assert_safe_load_marshal "\x04\b}\x00[\x00", additional_methods: [:default] }
|
||||||
|
define_method("test_safe_load_marshal Hash {}") { assert_safe_load_marshal "\x04\b{\x00" }
|
||||||
|
define_method("test_safe_load_marshal Array {}") { assert_safe_load_marshal "\x04\b[\x00" }
|
||||||
define_method("test_safe_load_marshal Hash {:runtime=>:development}") { assert_safe_load_marshal "\x04\bI{\x06:\fruntime:\x10development\x06:\n@type[\x00", permitted_ivars: { "Hash" => %w[@type] } }
|
define_method("test_safe_load_marshal Hash {:runtime=>:development}") { assert_safe_load_marshal "\x04\bI{\x06:\fruntime:\x10development\x06:\n@type[\x00", permitted_ivars: { "Hash" => %w[@type] } }
|
||||||
define_method("test_safe_load_marshal Integer -1") { assert_safe_load_marshal "\x04\bi\xFA" }
|
define_method("test_safe_load_marshal Integer -1") { assert_safe_load_marshal "\x04\bi\xFA" }
|
||||||
define_method("test_safe_load_marshal Integer -1048575") { assert_safe_load_marshal "\x04\bi\xFD\x01\x00\xF0" }
|
define_method("test_safe_load_marshal Integer -1048575") { assert_safe_load_marshal "\x04\bi\xFD\x01\x00\xF0" }
|
||||||
@ -124,6 +126,12 @@ class TestGemSafeMarshal < Gem::TestCase
|
|||||||
assert_safe_load_as [:development, :development]
|
assert_safe_load_as [:development, :development]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_length_one_symbols
|
||||||
|
with_const(Gem::SafeMarshal, :PERMITTED_SYMBOLS, %w[E A b 0] << "") do
|
||||||
|
assert_safe_load_as [:A, :E, :E, :A, "".to_sym, "".to_sym], additional_methods: [:instance_variables]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_repeated_string
|
def test_repeated_string
|
||||||
s = "hello"
|
s = "hello"
|
||||||
a = [s]
|
a = [s]
|
||||||
@ -156,6 +164,12 @@ class TestGemSafeMarshal < Gem::TestCase
|
|||||||
String.new("abc", encoding: "Windows-1256"),
|
String.new("abc", encoding: "Windows-1256"),
|
||||||
String.new("abc", encoding: Encoding::BINARY),
|
String.new("abc", encoding: Encoding::BINARY),
|
||||||
String.new("abc", encoding: "UTF-32"),
|
String.new("abc", encoding: "UTF-32"),
|
||||||
|
|
||||||
|
String.new("", encoding: "US-ASCII"),
|
||||||
|
String.new("", encoding: "UTF-8"),
|
||||||
|
String.new("", encoding: "Windows-1256"),
|
||||||
|
String.new("", encoding: Encoding::BINARY),
|
||||||
|
String.new("", encoding: "UTF-32"),
|
||||||
].each do |s|
|
].each do |s|
|
||||||
assert_safe_load_as s, additional_methods: [:encoding]
|
assert_safe_load_as s, additional_methods: [:encoding]
|
||||||
assert_safe_load_as [s, s], additional_methods: [->(a) { a.map(&:encoding) }]
|
assert_safe_load_as [s, s], additional_methods: [->(a) { a.map(&:encoding) }]
|
||||||
@ -282,6 +296,20 @@ class TestGemSafeMarshal < Gem::TestCase
|
|||||||
assert_equal e.message, "Attempting to load unpermitted symbol \"rspec\" @ root.[9].[0].@name"
|
assert_equal e.message, "Attempting to load unpermitted symbol \"rspec\" @ root.[9].[0].@name"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_gem_spec_disallowed_ivar
|
||||||
|
e = assert_raise(Gem::SafeMarshal::Visitors::ToRuby::UnpermittedIvarError) do
|
||||||
|
spec = Gem::Specification.new do |s|
|
||||||
|
s.name = "hi"
|
||||||
|
s.version = "1.2.3"
|
||||||
|
|
||||||
|
s.dependencies << Gem::Dependency.new("rspec", Gem::Requirement.new([">= 1.2.3"]), :runtime).tap {|d| d.instance_variable_set(:@foobar, "rspec") }
|
||||||
|
end
|
||||||
|
Gem::SafeMarshal.safe_load(Marshal.dump(spec))
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_equal e.message, "Attempting to set unpermitted ivar \"@foobar\" on object of class Gem::Dependency @ root.[9].[0].ivar_5"
|
||||||
|
end
|
||||||
|
|
||||||
def assert_safe_load_marshal(dumped, additional_methods: [], permitted_ivars: nil, equality: true, marshal_dump_equality: true)
|
def assert_safe_load_marshal(dumped, additional_methods: [], permitted_ivars: nil, equality: true, marshal_dump_equality: true)
|
||||||
loaded = Marshal.load(dumped)
|
loaded = Marshal.load(dumped)
|
||||||
safe_loaded =
|
safe_loaded =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user