Module: Eventbox::Sanitizer

Defined in:
lib/eventbox/sanitizer.rb

Overview

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

  • directly (immutable object types or already wrapped objects)

  • as a deep copy (if copyable)

  • as a safely callable wrapped object (Proc objects)

  • as a non-callable wrapped object (non copyable objects)

  • as an unwrapped object (when passing a wrapped object back to origin scope)

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:

  • If the object is marshalable, it is passed as a deep copy through ‘Marshal.dump` and `Marshal.load` .

  • An object which failed to marshal as a whole is tried to be dissected and values are sanitized recursively.

  • If the object can’t be marshaled or dissected, it is wrapped as WrappedObject. They are unwrapped when passed back to origin scope.

  • Proc objects passed from event scope to external are wrapped as WrappedObject. They are unwrapped when passed back to event scope.

  • Proc objects passed from external to event scope are wrapped as ExternalProc. They are unwrapped when passed back to external scope.

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.dissect_array_values(arg, source_event_loop, target_event_loop, name) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/eventbox/sanitizer.rb', line 117

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)

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

  vs.each_with_index do |v, 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) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/eventbox/sanitizer.rb', line 97

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)

  h.each do |k, v|
    arg[k] = v
  end

  h.each do |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) ⇒ Object



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 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|
    arg.instance_variable_get(ivn)
  end

  # Temporary set all instance variables to nil
  ivns.each do |ivn|
    arg.instance_variable_set(ivn, nil)
  end

  # Copy the object
  arg2 = yield(arg)

  # Restore the original object
  ivns.each_with_index do |ivn, ivni|
    arg.instance_variable_set(ivn, ivvs[ivni])
  end

  # sanitize instance variables independently and write them to the copied object
  ivns.each_with_index do |ivn, ivni|
    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) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/eventbox/sanitizer.rb', line 74

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)

  ms.each_with_index do |m, i|
    arg[m] = vs[i]
  end

  ms.each_with_index do |m, i|
    v2 = sanitize_value(vs[i], source_event_loop, target_event_loop, m)
    arg2[m] = v2
  end

  arg2
end

.return_args(args) ⇒ Object



41
42
43
# File 'lib/eventbox/sanitizer.rb', line 41

def return_args(args)
  args.length <= 1 ? args.first : args
end

.sanitize_value(arg, source_event_loop, target_event_loop, name = nil) ⇒ Object



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/eventbox/sanitizer.rb', line 138

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
      WrappedObject.new(arg, mel, name)
    when ExternalSharedObject # External object marked as shared_object
      WrappedObject.new(arg, source_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) ⇒ Object



218
219
220
# File 'lib/eventbox/sanitizer.rb', line 218

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_proc(arg, name, source_event_loop, target_event_loop) ⇒ Object



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/eventbox/sanitizer.rb', line 222

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, &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
        cbblock = args.last if Proc === args.last
        target_event_loop._external_proc_call(arg, name, args, block, cbblock, source_event_loop)
      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

Instance Method Details

#dissect_array_values(arg, source_event_loop, target_event_loop, name) ⇒ Object (private)



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/eventbox/sanitizer.rb', line 117

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)

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

  vs.each_with_index do |v, 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) ⇒ Object (private)



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/eventbox/sanitizer.rb', line 97

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)

  h.each do |k, v|
    arg[k] = v
  end

  h.each do |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) ⇒ Object (private)



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 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|
    arg.instance_variable_get(ivn)
  end

  # Temporary set all instance variables to nil
  ivns.each do |ivn|
    arg.instance_variable_set(ivn, nil)
  end

  # Copy the object
  arg2 = yield(arg)

  # Restore the original object
  ivns.each_with_index do |ivn, ivni|
    arg.instance_variable_set(ivn, ivvs[ivni])
  end

  # sanitize instance variables independently and write them to the copied object
  ivns.each_with_index do |ivn, ivni|
    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) ⇒ Object (private)



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/eventbox/sanitizer.rb', line 74

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)

  ms.each_with_index do |m, i|
    arg[m] = vs[i]
  end

  ms.each_with_index do |m, i|
    v2 = sanitize_value(vs[i], source_event_loop, target_event_loop, m)
    arg2[m] = v2
  end

  arg2
end

#return_args(args) ⇒ Object (private)



41
42
43
# File 'lib/eventbox/sanitizer.rb', line 41

def return_args(args)
  args.length <= 1 ? args.first : args
end

#sanitize_value(arg, source_event_loop, target_event_loop, name = nil) ⇒ Object (private)



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/eventbox/sanitizer.rb', line 138

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
      WrappedObject.new(arg, mel, name)
    when ExternalSharedObject # External object marked as shared_object
      WrappedObject.new(arg, source_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) ⇒ Object (private)



218
219
220
# File 'lib/eventbox/sanitizer.rb', line 218

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_proc(arg, name, source_event_loop, target_event_loop) ⇒ Object (private)



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/eventbox/sanitizer.rb', line 222

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, &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
        cbblock = args.last if Proc === args.last
        target_event_loop._external_proc_call(arg, name, args, block, cbblock, source_event_loop)
      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