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 separately and recursively.

  • If the object can't be marshaled or dissected, it is wrapped as ExternalObject when passed from external scope to event scope and wrapped as WrappedObject when passed from the event scope. 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


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# 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) ⇒ Object


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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) ⇒ 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
73
# 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) ⇒ Object


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 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) ⇒ 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


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
217
218
219
220
221
222
223
224
225
226
# 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) ⇒ Object


228
229
230
# 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) ⇒ Object


253
254
255
256
257
258
259
260
# File 'lib/eventbox/sanitizer.rb', line 253

def wrap_object(object, source_event_loop, target_event_loop, name)
  if target_event_loop&.event_scope?
    creation_answer_queue = target_event_loop.latest_answer_queue
    ExternalObject.new(object, source_event_loop, target_event_loop, creation_answer_queue, name)
  else
    WrappedObject.new(object, source_event_loop, name)
  end
end

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


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/eventbox/sanitizer.rb', line 232

def wrap_proc(arg, name, source_event_loop, target_event_loop)
  if target_event_loop&.event_scope?
    creation_answer_queue = target_event_loop.latest_answer_queue
    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.pop if Proc === args.last
        target_event_loop._external_object_call(arg, :call, name, args, block, cbblock, source_event_loop, creation_answer_queue)
      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)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# 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) ⇒ Object (private)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# 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) ⇒ 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
73
# 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) ⇒ Object (private)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# 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) ⇒ 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)


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
217
218
219
220
221
222
223
224
225
226
# 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) ⇒ Object (private)


228
229
230
# 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) ⇒ Object (private)


253
254
255
256
257
258
259
260
# File 'lib/eventbox/sanitizer.rb', line 253

def wrap_object(object, source_event_loop, target_event_loop, name)
  if target_event_loop&.event_scope?
    creation_answer_queue = target_event_loop.latest_answer_queue
    ExternalObject.new(object, source_event_loop, target_event_loop, creation_answer_queue, name)
  else
    WrappedObject.new(object, source_event_loop, name)
  end
end

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


232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/eventbox/sanitizer.rb', line 232

def wrap_proc(arg, name, source_event_loop, target_event_loop)
  if target_event_loop&.event_scope?
    creation_answer_queue = target_event_loop.latest_answer_queue
    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.pop if Proc === args.last
        target_event_loop._external_object_call(arg, :call, name, args, block, cbblock, source_event_loop, creation_answer_queue)
      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