# frozen_string_literal: true require_relative "visitor" module Gem::SafeMarshal module Visitors class ToRuby < Visitor def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:) @permitted_classes = permitted_classes @permitted_symbols = ["E"].concat(permitted_symbols).concat(permitted_classes) @permitted_ivars = permitted_ivars @objects = [] @symbols = [] @class_cache = {} @stack = ["root"] @stack_idx = 1 end def inspect # :nodoc: format("#<%s permitted_classes: %p permitted_symbols: %p permitted_ivars: %p>", self.class, @permitted_classes, @permitted_symbols, @permitted_ivars) end def visit(target) stack_idx = @stack_idx super ensure @stack_idx = stack_idx - 1 end private def push_stack(element) @stack[@stack_idx] = element @stack_idx += 1 end def visit_Gem_SafeMarshal_Elements_Array(a) array = register_object([]) elements = a.elements 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 def visit_Gem_SafeMarshal_Elements_Symbol(s) name = s.name raise UnpermittedSymbolError.new(symbol: name, stack: formatted_stack) unless @permitted_symbols.include?(name) visit_symbol_type(s) end def map_ivars(klass, ivars) stack_idx = @stack_idx ivars.map.with_index do |(k, v), i| @stack_idx = stack_idx push_stack "ivar_" push_stack i k = resolve_ivar(klass, k) @stack_idx = stack_idx push_stack k next k, visit(v) end end def visit_Gem_SafeMarshal_Elements_WithIvars(e) object_offset = @objects.size push_stack "object" object = visit(e.object) ivars = map_ivars(object.class, e.ivars) case e.object when Elements::UserDefined if object.class == ::Time internal = [] ivars.reject! do |k, v| case k when :offset, :zone, :nano_num, :nano_den, :submicro internal << [k, v] true else false end end s = e.object.binary_string marshal_string = "\x04\bIu:\tTime".b marshal_string.concat(s.size + 5) marshal_string << s marshal_string.concat(internal.size + 5) internal.each do |k, v| 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 object = @objects[object_offset] = Marshal.load(marshal_string) end when Elements::String enc = nil ivars.reject! do |k, v| case k when :E case v when TrueClass enc = "UTF-8" when FalseClass enc = "US-ASCII" else raise FormatError, "Unexpected value for String :E #{v.inspect}" end when :encoding enc = v else next false end true end object.force_encoding(enc) if enc end ivars.each do |k, v| object.instance_variable_set k, v end object end def visit_Gem_SafeMarshal_Elements_Hash(o) hash = register_object({}) o.pairs.each_with_index do |(k, v), i| push_stack i k = visit(k) push_stack k hash[k] = visit(v) end hash end def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o) hash = visit_Gem_SafeMarshal_Elements_Hash(o) push_stack :default hash.default = visit(o.default) hash end def visit_Gem_SafeMarshal_Elements_Object(o) register_object(resolve_class(o.name).allocate) end def visit_Gem_SafeMarshal_Elements_ObjectLink(o) @objects[o.offset] end def visit_Gem_SafeMarshal_Elements_SymbolLink(o) @symbols[o.offset] end def visit_Gem_SafeMarshal_Elements_UserDefined(o) register_object(call_method(resolve_class(o.name), :_load, o.binary_string)) end def visit_Gem_SafeMarshal_Elements_UserMarshal(o) klass = resolve_class(o.name) compat = COMPAT_CLASSES.fetch(klass, nil) idx = @objects.size object = register_object(call_method(compat || klass, :allocate)) push_stack :data ret = call_method(object, :marshal_load, visit(o.data)) if compat object = @objects[idx] = ret end object end def visit_Gem_SafeMarshal_Elements_Integer(i) i.int end def visit_Gem_SafeMarshal_Elements_Nil(_) nil end def visit_Gem_SafeMarshal_Elements_True(_) true end def visit_Gem_SafeMarshal_Elements_False(_) false end def visit_Gem_SafeMarshal_Elements_String(s) register_object(+s.str) end def visit_Gem_SafeMarshal_Elements_Float(f) case f.string when "inf" ::Float::INFINITY when "-inf" -::Float::INFINITY when "nan" ::Float::NAN else f.string.to_f end end def visit_Gem_SafeMarshal_Elements_Bignum(b) result = 0 b.data.each_byte.with_index do |byte, exp| result += (byte * 2**(exp * 8)) end case b.sign when 43 # ?+ result when 45 # ?- -result else raise FormatError, "Unexpected sign for Bignum #{b.sign.chr.inspect} (#{b.sign})" end end def visit_Gem_SafeMarshal_Elements_UserClass(r) if resolve_class(r.name) == ::Hash && r.wrapped_object.is_a?(Elements::Hash) hash = register_object({}.compare_by_identity) o = r.wrapped_object o.pairs.each_with_index do |(k, v), i| push_stack i k = visit(k) push_stack k hash[k] = visit(v) end if o.is_a?(Elements::HashWithDefaultValue) push_stack :default hash.default = visit(o.default) end hash else raise UnsupportedError.new("Unsupported user class #{resolve_class(r.name)} in marshal stream", stack: formatted_stack) end end def resolve_class(n) @class_cache[n] ||= begin to_s = resolve_symbol_name(n) raise UnpermittedClassError.new(name: to_s, stack: formatted_stack) unless @permitted_classes.include?(to_s) visit_symbol_type(n) begin ::Object.const_get(to_s) rescue NameError raise ArgumentError, "Undefined class #{to_s.inspect}" end end end class RationalCompat def marshal_load(s) 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 private_constant :RationalCompat COMPAT_CLASSES = {}.tap do |h| h[Rational] = RationalCompat end.compare_by_identity.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: formatted_stack) 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 sym when Elements::SymbolLink visit_Gem_SafeMarshal_Elements_SymbolLink(element) end end # This is a hot method, so avoid respond_to? checks on every invocation if :read.respond_to?(:name) def resolve_symbol_name(element) case element when Elements::Symbol element.name when Elements::SymbolLink 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 def register_object(o) @objects << o o 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 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 end class UnpermittedSymbolError < Error def initialize(symbol:, stack:) @symbol = symbol @stack = stack super "Attempting to load unpermitted symbol #{symbol.inspect} @ #{stack.join "."}" end end class UnpermittedIvarError < Error 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 < Error def initialize(name:, stack:) @name = name @stack = stack super "Attempting to load unpermitted class #{name.inspect} @ #{stack.join "."}" end end class UnsupportedError < Error def initialize(message, stack:) super "#{message} @ #{stack.join "."}" end end class FormatError < Error end class MethodCallError < Error end end end end