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

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