ruby_gaurden 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/ruby_gaurden/bed.rb +21 -0
- data/lib/ruby_gaurden/bed_error.rb +17 -0
- data/lib/ruby_gaurden/bindings.rb +40 -0
- data/lib/ruby_gaurden/bridging.rb +163 -0
- data/lib/ruby_gaurden/compilation_error.rb +8 -0
- data/lib/ruby_gaurden/error.rb +8 -0
- data/lib/ruby_gaurden/execution.rb +99 -0
- data/lib/ruby_gaurden/execution_error.rb +8 -0
- data/lib/ruby_gaurden/runtime_environment.rb +104 -0
- data/lib/ruby_gaurden/thread_safety.rb +75 -0
- data/lib/ruby_gaurden/timeout_error.rb +8 -0
- data/lib/ruby_gaurden/version.rb +5 -0
- data/lib/ruby_gaurden.rb +38 -0
- metadata +100 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dc546e0183aa5317cd7f463490262c6929e20a8a5261dd6731d6578abce11907
|
|
4
|
+
data.tar.gz: c8fdd3f6d9777ff56780fd933f21cd34432e8dd80656019853fdb0c3e9ec9ee1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a2a9efa813c4d0cf4eca8471b0d93bdce036ba946e0ef3373148f8993d00d606535b6200671ae03832d0af83bef0e1e25397e502f49223342f88c30a1997526f
|
|
7
|
+
data.tar.gz: 5f9892a85b6b83826556f797f6ba6ea709a0307ee4a52665492121d96a261026a405a514dcb3fd152930934bcac910c8c0ad256987472e5bffaa99155287d457
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
|
|
5
|
+
module RubyGaurden
|
|
6
|
+
class Bed
|
|
7
|
+
include Execution
|
|
8
|
+
include RuntimeEnvironment
|
|
9
|
+
include Bindings
|
|
10
|
+
include Bridging
|
|
11
|
+
include ThreadSafety
|
|
12
|
+
|
|
13
|
+
# Creates a new instance of the Bed and executes the provided source.
|
|
14
|
+
# @param args [Array] Arguments passed to #execute.
|
|
15
|
+
# @return [Object] The result of the execution.
|
|
16
|
+
# @see #execute
|
|
17
|
+
def self.execute(...)
|
|
18
|
+
new.execute(...)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
|
|
5
|
+
module RubyGaurden
|
|
6
|
+
class BedError < Error
|
|
7
|
+
def self.[](class_name)
|
|
8
|
+
bed_class_name = :"Bed#{class_name}"
|
|
9
|
+
|
|
10
|
+
if const_defined?(bed_class_name) && (klass = const_get(bed_class_name)) < self
|
|
11
|
+
klass
|
|
12
|
+
else
|
|
13
|
+
const_set(bed_class_name, Class.new(self))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
|
|
5
|
+
module RubyGaurden
|
|
6
|
+
module Bindings
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def bindings
|
|
11
|
+
@bindings ||= if superclass.respond_to?(:bindings)
|
|
12
|
+
superclass.bindings.dup
|
|
13
|
+
else
|
|
14
|
+
[]
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def binds(target, proc = nil, &block)
|
|
21
|
+
actual_proc = proc || block || -> {}
|
|
22
|
+
bindings << [target, actual_proc]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(*)
|
|
27
|
+
super
|
|
28
|
+
|
|
29
|
+
self.class.bindings.each do |target, proc|
|
|
30
|
+
bind target, lambda { |*args|
|
|
31
|
+
instance_exec(*args, &proc)
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def bind(target, proc = -> {})
|
|
37
|
+
context.attach(target, proc)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'active_support/concern'
|
|
6
|
+
|
|
7
|
+
module RubyGaurden
|
|
8
|
+
module Bridging
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
included do
|
|
12
|
+
binds('__rb_exit') { |status| raise Error, "Exit with status #{status.inspect}" }
|
|
13
|
+
binds('__rb_stdout_write') { |data| stdout << data }
|
|
14
|
+
binds('__rb_stderr_write') { |data| stderr << data }
|
|
15
|
+
|
|
16
|
+
executes <<-RUBY
|
|
17
|
+
# backtick_javascript: true
|
|
18
|
+
require 'native'
|
|
19
|
+
require 'singleton'
|
|
20
|
+
require 'json'
|
|
21
|
+
|
|
22
|
+
module RubyGaurden
|
|
23
|
+
VERSION = #{RubyGaurden::VERSION.inspect}
|
|
24
|
+
|
|
25
|
+
class CurrentBedProxy
|
|
26
|
+
include Singleton
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.planted?
|
|
30
|
+
true
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.current
|
|
34
|
+
CurrentBedProxy.instance
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Handles method invocation from the host, ensuring data is correctly
|
|
38
|
+
# translated and errors are caught.
|
|
39
|
+
def self.__invoke_from_host(method_name, args_json)
|
|
40
|
+
args = JSON.parse(args_json)
|
|
41
|
+
value = ::Object.send(method_name, *args)
|
|
42
|
+
|
|
43
|
+
{ isCaught: false, val: value.to_json }
|
|
44
|
+
rescue Exception => e
|
|
45
|
+
{ isCaught: true, val: [e.class.name, e.message].to_json }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Handles code evaluation from the host.
|
|
49
|
+
def self.__eval_from_host(js_source)
|
|
50
|
+
value = `eval(js_source)`
|
|
51
|
+
{ isCaught: false, val: value.to_json }
|
|
52
|
+
rescue Exception => e
|
|
53
|
+
{ isCaught: true, val: [e.class.name, e.message].to_json }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module Kernel
|
|
58
|
+
def exit(status = 0)
|
|
59
|
+
`__rb_exit(status)`
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
$stdout.write_proc = ->(data) { `__rb_stdout_write(data)` }
|
|
64
|
+
$stderr.write_proc = ->(data) { `__rb_stderr_write(data)` }
|
|
65
|
+
|
|
66
|
+
`
|
|
67
|
+
globalThis.__rb_bridge_invoke = function(methodName, argsJson) {
|
|
68
|
+
return Opal.RubyGaurden.$__invoke_from_host(methodName, argsJson).$to_n();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
globalThis.__rb_eval_wrapper = function(source) {
|
|
72
|
+
return Opal.RubyGaurden.$__eval_from_host(source).$to_n();
|
|
73
|
+
};
|
|
74
|
+
`
|
|
75
|
+
RUBY
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class_methods do
|
|
79
|
+
# Exposes host methods to the sandbox, allowing sandboxed code to call
|
|
80
|
+
# back into the main Ruby process safely via JSON serialization.
|
|
81
|
+
# @param method_names [Array<Symbol, String>] Methods on the host to expose.
|
|
82
|
+
# @note Blocks are not supported for bridged methods.
|
|
83
|
+
def exposes(*method_names)
|
|
84
|
+
method_names.each do |method_name|
|
|
85
|
+
handle = "__rb_expose_#{method_name}"
|
|
86
|
+
binds(handle, ->(json) { send(method_name, *JSON.parse(json)).to_json })
|
|
87
|
+
|
|
88
|
+
executes(<<-RUBY)
|
|
89
|
+
# backtick_javascript: true
|
|
90
|
+
def (RubyGaurden::CurrentBedProxy.instance).#{method_name}(*args, &block)
|
|
91
|
+
raise ArgumentError, "Blocks not supported" if block
|
|
92
|
+
JSON.parse(`#{handle}(\#{args.to_json})`)
|
|
93
|
+
end
|
|
94
|
+
RUBY
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Returns the accumulated stdout produced by the sandbox.
|
|
100
|
+
# @return [Array<String>]
|
|
101
|
+
def stdout
|
|
102
|
+
@stdout ||= []
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the accumulated stderr produced by the sandbox.
|
|
106
|
+
# @return [Array<String>]
|
|
107
|
+
def stderr
|
|
108
|
+
@stderr ||= []
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Clears the IO buffers (stdout and stderr).
|
|
112
|
+
def reset_io!
|
|
113
|
+
@stdout = []
|
|
114
|
+
@stderr = []
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Directly invokes a Ruby method defined inside the sandbox.
|
|
118
|
+
# This bypasses the Ruby-to-JS compilation step for the call itself.
|
|
119
|
+
# @param method_name [Symbol, String] The name of the method to call.
|
|
120
|
+
# @param args [Array] Arguments to pass to the method.
|
|
121
|
+
# @return [Object] The result of the method call.
|
|
122
|
+
# @raise [BedError] if the method raises an exception inside the sandbox.
|
|
123
|
+
def call(method_name, *args)
|
|
124
|
+
# We use JSON to move data across the bridge to avoid V8/Ruby object mapping overhead
|
|
125
|
+
# and to ensure Opal objects are correctly initialized.
|
|
126
|
+
serialized_args = args.to_json
|
|
127
|
+
result = context.call('__rb_bridge_invoke', method_name.to_s, serialized_args)
|
|
128
|
+
handle_bridge_result(result)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def eval_compiled_source(source)
|
|
134
|
+
result = context.call('__rb_eval_wrapper', source)
|
|
135
|
+
handle_bridge_result(result) # result is the Ruby Hash returned by MiniRacer
|
|
136
|
+
rescue MiniRacer::ScriptTerminatedError => e
|
|
137
|
+
raise TimeoutError, e.message
|
|
138
|
+
rescue MiniRacer::RuntimeError, StandardError => e
|
|
139
|
+
raise e if e.is_a?(RubyGaurden::Error)
|
|
140
|
+
|
|
141
|
+
raise ExecutionError, e.message
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def handle_bridge_result(result)
|
|
145
|
+
return unless result.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
if result.fetch('isCaught', false)
|
|
148
|
+
class_name, message = result_value(result)
|
|
149
|
+
raise BedError[class_name], message
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
result_value(result)
|
|
153
|
+
rescue MiniRacer::RuntimeError, StandardError => e
|
|
154
|
+
raise e if e.is_a?(RubyGaurden::Error)
|
|
155
|
+
|
|
156
|
+
raise ExecutionError, e.message
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def result_value(result)
|
|
160
|
+
result['val'] ? JSON.parse(result['val'], quirks_mode: true) : nil
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
require 'mini_racer'
|
|
6
|
+
|
|
7
|
+
module RubyGaurden
|
|
8
|
+
module Execution
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAXIMUM_EXECUTION_TIME = 1000
|
|
12
|
+
DEFAULT_MAX_MEMORY = 512 * 1024 * 1024 # 512MB default limit
|
|
13
|
+
|
|
14
|
+
class_methods do
|
|
15
|
+
def maximum_execution_time
|
|
16
|
+
@maximum_execution_time ||= superclass.maximum_execution_time if superclass.respond_to?(:maximum_execution_time)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def maximum_memory
|
|
20
|
+
@maximum_memory ||= superclass.maximum_memory if superclass.respond_to?(:maximum_memory)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sets the maximum time a script can execute before being terminated.
|
|
24
|
+
# @param seconds [Integer, Float] Time in seconds.
|
|
25
|
+
def times_out_in(seconds)
|
|
26
|
+
@maximum_execution_time = seconds
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Sets the maximum memory the V8 context can consume.
|
|
30
|
+
# @param bytes [Integer] Memory limit in bytes.
|
|
31
|
+
def limits_memory_to(bytes)
|
|
32
|
+
@maximum_memory = bytes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Accesses the thread-safe pool of pre-warmed V8 contexts.
|
|
36
|
+
# @return [Thread::Queue]
|
|
37
|
+
def context_pool
|
|
38
|
+
@context_pool ||= ::Queue.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Pre-warms a specified number of V8 contexts and adds them to the pool.
|
|
42
|
+
# @param size [Integer] The number of contexts to create.
|
|
43
|
+
def warm_up(size = 1)
|
|
44
|
+
size.times { context_pool.push(create_context) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Creates a fresh V8 context and evaluates the initialization source.
|
|
48
|
+
# Use this to bypass the pool if needed or to manually populate it.
|
|
49
|
+
# @return [MiniRacer::Context]
|
|
50
|
+
def create_context
|
|
51
|
+
# Use a large timeout (30s) for initialization to ensure the large Opal
|
|
52
|
+
# runtime loads even if the user set a small timeout for their code.
|
|
53
|
+
MiniRacer::Context.new(
|
|
54
|
+
timeout: (maximum_execution_time || DEFAULT_MAXIMUM_EXECUTION_TIME) * 1000,
|
|
55
|
+
max_memory: maximum_memory || DEFAULT_MAX_MEMORY
|
|
56
|
+
).tap do |ctx|
|
|
57
|
+
# We use a secondary timeout for the init phase. If this fails,
|
|
58
|
+
# the context is not returned/tapped, preventing a "broken"
|
|
59
|
+
# context from entering the pool or being used by an instance.
|
|
60
|
+
ctx.eval(send(:initialization_source), timeout: 30_000)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def maximum_execution_time
|
|
66
|
+
@maximum_execution_time ||= self.class.maximum_execution_time
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def maximum_memory
|
|
70
|
+
@maximum_memory ||= self.class.maximum_memory
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def maximum_execution_time_ms
|
|
74
|
+
return unless maximum_execution_time
|
|
75
|
+
|
|
76
|
+
maximum_execution_time * 1000
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def context
|
|
80
|
+
@context ||= checkout_context
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def checkout_context
|
|
86
|
+
self.class.context_pool.pop(true)
|
|
87
|
+
rescue ThreadError
|
|
88
|
+
self.class.create_context
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def eval_compiled_source(source)
|
|
92
|
+
context.eval source
|
|
93
|
+
rescue MiniRacer::RuntimeError => e
|
|
94
|
+
raise ExecutionError, e.message
|
|
95
|
+
rescue MiniRacer::ScriptTerminatedError => e
|
|
96
|
+
raise TimeoutError, e.message
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
require 'opal'
|
|
6
|
+
|
|
7
|
+
module RubyGaurden
|
|
8
|
+
module RuntimeEnvironment
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
MAX_CACHE_SIZE = 1000
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
uses 'opal'
|
|
15
|
+
requires 'opal'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class_methods do
|
|
19
|
+
def compiled_cache
|
|
20
|
+
@compiled_cache ||= {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Declares a gem dependency to be loaded into the sandbox.
|
|
26
|
+
# @param gem_names [Array<String>] List of gem names.
|
|
27
|
+
def uses(*gem_names)
|
|
28
|
+
@uses ||= []
|
|
29
|
+
@uses += gem_names
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Declares a file or feature to be required within the sandbox.
|
|
33
|
+
# @param paths [Array<String>] List of paths to require.
|
|
34
|
+
def requires(*paths)
|
|
35
|
+
@requires ||= []
|
|
36
|
+
@requires += paths
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Defines Ruby code to be executed during the initialization of every sandbox instance.
|
|
40
|
+
# Useful for setting up global state or helper classes.
|
|
41
|
+
# @param source [String] The Ruby code to execute during boot.
|
|
42
|
+
def executes(source)
|
|
43
|
+
@executes ||= []
|
|
44
|
+
@executes << source
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialization_source
|
|
48
|
+
@initialization_source ||= build_initialization_source
|
|
49
|
+
rescue SyntaxError, StandardError => e
|
|
50
|
+
raise CompilationError, e.message
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_initialization_source
|
|
54
|
+
builder = Opal::Builder.new(compiler_options: { arity_check: true, dynamic_require_severity: :error })
|
|
55
|
+
builder.build('opal')
|
|
56
|
+
|
|
57
|
+
# Ensure the builder can resolve requirements by including the gems
|
|
58
|
+
inherited_values(:@uses).uniq.each { |g| builder.use_gem(g) }
|
|
59
|
+
inherited_values(:@requires).uniq.each { |p| builder.build(p) }
|
|
60
|
+
inherited_values(:@executes).each { |s| builder.build_str(s, '(executes)') }
|
|
61
|
+
|
|
62
|
+
builder.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def inherited_values(ivar)
|
|
66
|
+
ancestors.reverse.flat_map do |ancestor|
|
|
67
|
+
ancestor.instance_variable_defined?(ivar) ? ancestor.instance_variable_get(ivar) : []
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Executes a string of Ruby code within the sandbox instance.
|
|
73
|
+
# Results are cached by the source string to optimize repeated calls.
|
|
74
|
+
# @param source [String] The Ruby code to execute.
|
|
75
|
+
# @return [Object] The result of the execution, translated to host Ruby objects.
|
|
76
|
+
# @raise [CompilationError] if the code has syntax errors.
|
|
77
|
+
# @raise [ExecutionError] if the code raises an exception during runtime.
|
|
78
|
+
def execute(source)
|
|
79
|
+
js = self.class.compiled_cache[source] || compile_and_cache(source)
|
|
80
|
+
eval_compiled_source(js)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Compiles Ruby source to JavaScript and clears the cache if the limit is reached.
|
|
84
|
+
# @param source [String] Ruby source code.
|
|
85
|
+
# @return [String] Compiled JavaScript.
|
|
86
|
+
def compile_and_cache(source)
|
|
87
|
+
prune_cache! if self.class.compiled_cache.size >= MAX_CACHE_SIZE
|
|
88
|
+
self.class.compiled_cache[source] = Opal::Compiler.new(source, file: '(execute)', arity_check: true).compile
|
|
89
|
+
rescue SyntaxError => e
|
|
90
|
+
raise CompilationError, e.message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Prune the oldest 10% of entries (at least 1) to avoid performance cliffs.
|
|
94
|
+
# Since Ruby Hashes maintain insertion order, this behaves like FIFO.
|
|
95
|
+
def prune_cache!
|
|
96
|
+
self
|
|
97
|
+
.class
|
|
98
|
+
.compiled_cache
|
|
99
|
+
.keys
|
|
100
|
+
.first([1, MAX_CACHE_SIZE / 10].max)
|
|
101
|
+
.each { |k| self.class.compiled_cache.delete(k) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden'
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
require 'monitor'
|
|
6
|
+
|
|
7
|
+
module RubyGaurden
|
|
8
|
+
module ThreadSafety
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
class_methods do
|
|
12
|
+
def maximum_execution_time
|
|
13
|
+
synchronize { super }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialization_source
|
|
17
|
+
synchronize { super }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bindings
|
|
21
|
+
synchronize { super }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def times_out_in(...)
|
|
27
|
+
synchronize { super }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def uses(...)
|
|
31
|
+
synchronize { super }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def requires(...)
|
|
35
|
+
synchronize { super }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def executes(...)
|
|
39
|
+
synchronize { super }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def binds(...)
|
|
43
|
+
synchronize { super }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exposes(...)
|
|
47
|
+
synchronize { super }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def synchronize(&)
|
|
51
|
+
monitor.synchronize(&)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def monitor
|
|
55
|
+
@monitor ||= Monitor.new
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def maximum_execution_time
|
|
60
|
+
synchronize { super }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute(...)
|
|
64
|
+
synchronize { super }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def synchronize(&)
|
|
68
|
+
monitor.synchronize(&)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def monitor
|
|
72
|
+
@monitor ||= Monitor.new
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/ruby_gaurden.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_gaurden/version'
|
|
4
|
+
|
|
5
|
+
module RubyGaurden
|
|
6
|
+
autoload :Bindings, 'ruby_gaurden/bindings'
|
|
7
|
+
autoload :BedError, 'ruby_gaurden/bed_error'
|
|
8
|
+
autoload :Bridging, 'ruby_gaurden/bridging'
|
|
9
|
+
autoload :CompilationError, 'ruby_gaurden/compilation_error'
|
|
10
|
+
autoload :Error, 'ruby_gaurden/error'
|
|
11
|
+
autoload :Execution, 'ruby_gaurden/execution'
|
|
12
|
+
autoload :ExecutionError, 'ruby_gaurden/execution_error'
|
|
13
|
+
autoload :Bed, 'ruby_gaurden/bed'
|
|
14
|
+
autoload :RuntimeEnvironment, 'ruby_gaurden/runtime_environment'
|
|
15
|
+
autoload :ThreadSafety, 'ruby_gaurden/thread_safety'
|
|
16
|
+
autoload :TimeoutError, 'ruby_gaurden/timeout_error'
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# Checks if the current execution context is inside a sandbox.
|
|
21
|
+
# @return [Boolean] true if inside a sandbox, false otherwise.
|
|
22
|
+
def planted?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the current sandbox proxy instance if running inside a sandbox.
|
|
27
|
+
# @return [Object, nil] The proxy instance or nil if outside a sandbox.
|
|
28
|
+
def current
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Convenience method to execute Ruby code in a fresh, one-off sandbox.
|
|
33
|
+
# @param args [Array] Arguments passed to Bed.execute.
|
|
34
|
+
# @return [Object] The result of the execution.
|
|
35
|
+
def execute(...)
|
|
36
|
+
Bed.execute(...)
|
|
37
|
+
end
|
|
38
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_gaurden
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tayler Phillips
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-05-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: mini_racer
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.21.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.21.0
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: opal
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - '='
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 1.8.0
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - '='
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: 1.8.0
|
|
54
|
+
email:
|
|
55
|
+
- taylerphillips20@gmail.com
|
|
56
|
+
executables: []
|
|
57
|
+
extensions: []
|
|
58
|
+
extra_rdoc_files: []
|
|
59
|
+
files:
|
|
60
|
+
- lib/ruby_gaurden.rb
|
|
61
|
+
- lib/ruby_gaurden/bed.rb
|
|
62
|
+
- lib/ruby_gaurden/bed_error.rb
|
|
63
|
+
- lib/ruby_gaurden/bindings.rb
|
|
64
|
+
- lib/ruby_gaurden/bridging.rb
|
|
65
|
+
- lib/ruby_gaurden/compilation_error.rb
|
|
66
|
+
- lib/ruby_gaurden/error.rb
|
|
67
|
+
- lib/ruby_gaurden/execution.rb
|
|
68
|
+
- lib/ruby_gaurden/execution_error.rb
|
|
69
|
+
- lib/ruby_gaurden/runtime_environment.rb
|
|
70
|
+
- lib/ruby_gaurden/thread_safety.rb
|
|
71
|
+
- lib/ruby_gaurden/timeout_error.rb
|
|
72
|
+
- lib/ruby_gaurden/version.rb
|
|
73
|
+
homepage: https://github.com/Phillita/ruby_gaurden
|
|
74
|
+
licenses:
|
|
75
|
+
- MIT
|
|
76
|
+
metadata:
|
|
77
|
+
allowed_push_host: https://rubygems.org
|
|
78
|
+
source_code_uri: https://github.com/Phillita/ruby_gaurden
|
|
79
|
+
changelog_uri: https://github.com/Phillita/ruby_gaurden/blob/main/CHANGELOG.md
|
|
80
|
+
bug_tracker_uri: https://github.com/Phillita/ruby_gaurden/issues
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
82
|
+
rdoc_options: []
|
|
83
|
+
require_paths:
|
|
84
|
+
- lib
|
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: 3.1.0
|
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
requirements: []
|
|
96
|
+
rubygems_version: 3.6.2
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: RubyGaurden allows the execution of untrusted Ruby code safely in a walled
|
|
99
|
+
garden.
|
|
100
|
+
test_files: []
|