module Eventbox::Sanitizer

Module for argument and result value sanitation.

All call arguments and result values between external and event scope an vice versa are passed through the Sanitizer. This filter is required to prevent data races through shared objects or non-synchonized proc execution. It also wraps blocks and Proc objects to arbitrate between external blocking behaviour and internal event based behaviour.

Depending on the type of the object and the direction of the call it is passed

The filter is recursively applied to all object data (instance variables or elements), if the object is non copyable.

In detail this works as following. Objects which are passed through unchanged are:

The following rules apply for wrapping/unwrapping:

Both cases even work if the object is encapsulated by another object.

In all other cases the following rules apply:

Public Instance Methods

dissect_array_values(arg, source_event_loop, target_event_loop, name) { |arg| ... } click to toggle source
# File lib/eventbox/sanitizer.rb, line 124
def dissect_array_values(arg, source_event_loop, target_event_loop, name)
  vs = arg.dup

  vs.each_index do |i|
    arg[i] = nil
  end

  arg2 = yield(arg)
rescue
  # Restore the original object in case of Marshal.dump failure
  vs.each_with_index do |v, i|
    arg[i] = vs[i]
  end
  raise
else
  vs.each_with_index do |v, i|
    arg[i] = vs[i]
    v2 = sanitize_value(v, source_event_loop, target_event_loop, name)
    arg2[i] = v2
  end

  arg2
end
dissect_hash_values(arg, source_event_loop, target_event_loop) { |arg| ... } click to toggle source
# File lib/eventbox/sanitizer.rb, line 101
def dissect_hash_values(arg, source_event_loop, target_event_loop)
  h = arg.dup

  h.each_key do |k|
    arg[k] = nil
  end

  arg2 = yield(arg)
rescue
  # Restore the original object in case of Marshal.dump failure
  h.each do |k, v|
    arg[k] = v
  end
  raise
else
  h.each do |k, v|
    arg[k] = v
    arg2[k] = sanitize_value(v, source_event_loop, target_event_loop, k)
  end

  arg2
end
dissect_instance_variables(arg, source_event_loop, target_event_loop) { |arg| ... } click to toggle source
# File lib/eventbox/sanitizer.rb, line 45
def dissect_instance_variables(arg, source_event_loop, target_event_loop)
  # Separate the instance variables from the object
  ivns = arg.instance_variables
  ivvs = ivns.map do |ivn|
    ivv = arg.instance_variable_get(ivn)
    # Temporary set all instance variables to nil
    arg.instance_variable_set(ivn, nil)
    ivv
  end

  # Copy the object
  arg2 = yield(arg)
rescue
  # Restore the original object in case of Marshal.dump failure
  ivns.each_with_index do |ivn, ivni|
    arg.instance_variable_set(ivn, ivvs[ivni])
  end
  raise
else
  ivns.each_with_index do |ivn, ivni|
    # Restore the original object
    arg.instance_variable_set(ivn, ivvs[ivni])
    # sanitize instance variables independently and write them to the copied object
    ivv = sanitize_value(ivvs[ivni], source_event_loop, target_event_loop, ivn)
    arg2.instance_variable_set(ivn, ivv)
  end

  arg2
end
dissect_struct_members(arg, source_event_loop, target_event_loop) { |arg| ... } click to toggle source
# File lib/eventbox/sanitizer.rb, line 75
def dissect_struct_members(arg, source_event_loop, target_event_loop)
  ms = arg.members
  # call Array#map on Struct#values to work around bug JRuby bug https://github.com/jruby/jruby/issues/5372
  vs = arg.values.map{|a| a }

  ms.each do |m|
    arg[m] = nil
  end

  arg2 = yield(arg)
rescue
  # Restore the original object in case of Marshal.dump failure
  ms.each_with_index do |m, i|
    arg[m] = vs[i]
  end
  raise
else
  ms.each_with_index do |m, i|
    arg[m] = vs[i]
    v2 = sanitize_value(vs[i], source_event_loop, target_event_loop, m)
    arg2[m] = v2
  end

  arg2
end
return_args(args) click to toggle source
# File lib/eventbox/sanitizer.rb, line 41
def return_args(args)
  args.length <= 1 ? args.first : args
end
sanitize_kwargs(args, source_event_loop, target_event_loop, name=nil) click to toggle source
# File lib/eventbox/sanitizer.rb, line 232
def sanitize_kwargs(args, source_event_loop, target_event_loop, name=nil)
  args.transform_values { |arg| sanitize_value(arg, source_event_loop, target_event_loop, name) }
end
sanitize_value(arg, source_event_loop, target_event_loop, name=nil) click to toggle source
# File lib/eventbox/sanitizer.rb, line 148
def sanitize_value(arg, source_event_loop, target_event_loop, name=nil)
  case arg
  when NilClass, Numeric, Symbol, TrueClass, FalseClass # Immutable objects
    arg
  when WrappedObject
    arg.object_for(target_event_loop)
  when ExternalProc
    arg.object_for(target_event_loop)
  when InternalProc, Action # If object is already wrapped -> pass it through
    arg
  when Module # Class or Module definitions are passed through
    arg
  when Eventbox # Eventbox objects already sanitize all inputs and outputs and are thread safe
    arg
  when Proc
    wrap_proc(arg, name, source_event_loop, target_event_loop)
  else
    # Check if the object has been tagged
    case mel=ObjectRegistry.get_tag(arg)
    when EventLoop # Event scope object marked as shared_object
      unless mel == source_event_loop
        raise InvalidAccess, "object #{arg.inspect} #{"wrapped by #{name} " if name} was marked as shared_object in a different eventbox object than the calling eventbox"
      end
      wrap_object(arg, mel, target_event_loop, name)
    when ExternalSharedObject # External object marked as shared_object
      wrap_object(arg, source_event_loop, target_event_loop, name)
    else
      # Not tagged -> try to deep copy the object
      begin
        dumped = Marshal.dump(arg)
      rescue TypeError

        # Try to separate internal data from the object to sanitize it independently
        begin
          case arg
          when Array
            dissect_array_values(arg, source_event_loop, target_event_loop, name) do |arg2|
              dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
                Marshal.load(Marshal.dump(arg3))
              end
            end

          when Hash
            dissect_hash_values(arg, source_event_loop, target_event_loop) do |arg2|
              dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
                Marshal.load(Marshal.dump(arg3))
              end
            end

          when Struct
            dissect_struct_members(arg, source_event_loop, target_event_loop) do |arg2|
              dissect_instance_variables(arg2, source_event_loop, target_event_loop) do |arg3|
                Marshal.load(Marshal.dump(arg3))
              end
            end

          else
            dissect_instance_variables(arg, source_event_loop, target_event_loop) do |empty_arg|
              # Retry to dump the now empty object
              Marshal.load(Marshal.dump(empty_arg))
            end
          end
        rescue TypeError
          if source_event_loop
            ObjectRegistry.set_tag(arg, source_event_loop)
          else
            ObjectRegistry.set_tag(arg, ExternalSharedObject)
          end

          # Object not copyable -> wrap object as event scope or external object
          sanitize_value(arg, source_event_loop, target_event_loop, name)
        end

      else
        Marshal.load(dumped)
      end
    end
  end
end
sanitize_values(args, source_event_loop, target_event_loop, name=nil) click to toggle source
# File lib/eventbox/sanitizer.rb, line 228
def sanitize_values(args, source_event_loop, target_event_loop, name=nil)
  args.map { |arg| sanitize_value(arg, source_event_loop, target_event_loop, name) }
end
wrap_object(object, source_event_loop, target_event_loop, name) click to toggle source
# File lib/eventbox/sanitizer.rb, line 259
def wrap_object(object, source_event_loop, target_event_loop, name)
  if target_event_loop&.event_scope?
    ExternalObject.new(object, source_event_loop, target_event_loop, name)
  else
    WrappedObject.new(object, source_event_loop, name)
  end
end
wrap_proc(arg, name, source_event_loop, target_event_loop) click to toggle source
# File lib/eventbox/sanitizer.rb, line 236
def wrap_proc(arg, name, source_event_loop, target_event_loop)
  if target_event_loop&.event_scope?
    ExternalProc.new(arg, source_event_loop, name) do |*args, **kwargs, &block|
      if target_event_loop&.event_scope?
        # called in the event scope

        if block && !(WrappedProc === block)
          raise InvalidAccess, "calling #{arg.inspect} with block argument #{block.inspect} is not allowed - use async_proc, sync_proc, yield_proc or an external proc instead"
        end

        call_context = args.shift if CallContext === args.first
        cbblock = args.pop if Proc === args.last
        target_event_loop._external_object_call(arg, :call, name, args, kwargs, block, cbblock, source_event_loop, call_context)
      else
        # called externally
        raise InvalidAccess, "external proc #{arg.inspect} #{"wrapped by #{name} " if name} can not be called in a different eventbox instance"
      end
    end
  else
    WrappedObject.new(arg, source_event_loop, name)
  end
end