Module: SecurityBox::EvalRun
- Defined in:
- lib/security_box/eval_run.rb
Overview
One :oneshot evaluation against a pre-built runtime (Engine + Module + Linker). This is the shared core used by Sandbox (main Ractor, threads) and by RactorPool workers (one Ractor per worker) — both follow the same protocol: sandbox-exclusive tmpdir mounted as /work, per-eval token via ENV, envelope read back from /work/out.json (stdout sentinel fallback), fuel + epoch deadlines per evaluation, and explicit read-only-by-default host-folder mounts (stage 5) validated per eval.
Everything here is self-contained per call: no shared mutable state, safe to run concurrently from multiple Ractors/threads as long as each caller owns its Engine/Module/Linker (Ractors) or shares them thread-safely (wasmtime Engine/Module/Linker are thread-safe).
Constant Summary collapse
- GUEST_ENTRYPOINT =
"/src/main.rb"- CODE_FILE =
"code.rb"- RESULT_FILE =
"out.json"- TOKEN_ENV_VAR =
"SB_TOKEN"
Class Method Summary collapse
-
.run(engine:, module_:, linker:, config:, code:, token:) ⇒ Object
Runs
codeonce and returns a Result.
Class Method Details
.run(engine:, module_:, linker:, config:, code:, token:) ⇒ Object
Runs code once and returns a Result. config is a Configuration
(read-only); token is the per-eval random token generated by the
caller (SecureRandom is not guaranteed Ractor-safe, so workers
receive it pre-generated).
31 32 33 34 35 36 37 38 39 40 41 42 43 44 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 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/security_box/eval_run.rb', line 31 def run(engine:, module_:, linker:, config:, code:, token:) stdout = +"" stderr = +"" t0 = monotonic_ms # Mounts reference host paths that can disappear between building the # configuration and this eval; fail here with a clear note instead of # letting Wasmtime::Store.new raise an opaque error mid-setup. mount_errors = mount_errors(config) unless mount_errors.empty? return Result.new(status: :sandbox_error, stderr: mount_errors.join("\n") << "\n", duration_ms: monotonic_ms - t0) end Dir.mktmpdir("security_box") do |workdir| File.write(File.join(workdir, CODE_FILE), code) # Per-eval RPC state (stage 6): the /work tmpdir plus the # configuration's handlers (nil when none are configured — the # import is still defined, guest calls get a rescuable error). # The calls array doubles as the Result transcript. rpc_data = GuestRpc.store_data( workdir, config.rpcs.empty? ? nil : config.rpcs ) envelope = nil status = nil fuel_used = nil store = nil begin store = Wasmtime::Store.new( engine, rpc_data, wasi_p1_config: build_wasi(workdir, stdout, stderr, config, token), limits: { memory_size: config.memory_size } ) store.set_fuel(config.effective_fuel) instance = linker.instantiate(store, module_) store.set_epoch_deadline(epoch_ticks(config)) status = invoke_guest(instance, store) fuel_used = config.effective_fuel - store.get_fuel unless status == :timeout envelope = read_envelope(workdir, stdout, token) rescue Wasmtime::Error => e # Instantiation can fail before the guest ever runs (e.g. a # memory_size below the module's declared minimum pages). status = :sandbox_error stderr << "security_box: #{e.class}: #{e.message}\n" ensure store&.close end build_result(status, envelope, stdout, stderr, fuel_used, monotonic_ms - t0, rpc_data[:rpc]&.fetch(:calls)) end end |