kobako 0.11.0 → 0.11.2

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.
@@ -0,0 +1,90 @@
1
+ //! Ruby error classes (lazy-resolved once the top-level Kobako error
2
+ //! hierarchy is loaded by `lib/kobako/errors.rb`) and the `*_err`
3
+ //! constructors every run-mechanics submodule shares. The ext raises
4
+ //! directly into the invocation-outcome taxonomy (`TrapError` and its
5
+ //! subclasses) for run-path failures and into the construction-layer
6
+ //! `SetupError` (and its `ModuleNotBuiltError` subclass) for `from_path`
7
+ //! setup failures — no engine-specific intermediate layer; the Sandbox
8
+ //! layer adds the verb prefix and lets the subclass identity flow through
9
+ //! unchanged.
10
+
11
+ use magnus::value::Lazy;
12
+ use magnus::{prelude::*, Error as MagnusError, ExceptionClass, RModule, Ruby};
13
+
14
+ /// Resolve `Kobako::<name>` as an `ExceptionClass` — the shared body of
15
+ /// every error-class `Lazy` below, which differ only in the constant
16
+ /// name. The constants are guaranteed present by the time any of these
17
+ /// lazies first resolve (`lib/kobako/errors.rb` loads the hierarchy before
18
+ /// the ext raises into it), so a missing constant is a build / wiring bug
19
+ /// and the `unwrap` is the correct fail-fast.
20
+ fn kobako_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
21
+ let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
22
+ kobako.const_get(name).unwrap()
23
+ }
24
+
25
+ pub(crate) static SETUP_ERROR: Lazy<ExceptionClass> =
26
+ Lazy::new(|ruby| kobako_error_class(ruby, "SetupError"));
27
+
28
+ pub(crate) static MODULE_NOT_BUILT_ERROR: Lazy<ExceptionClass> =
29
+ Lazy::new(|ruby| kobako_error_class(ruby, "ModuleNotBuiltError"));
30
+
31
+ pub(crate) static TRAP_ERROR: Lazy<ExceptionClass> =
32
+ Lazy::new(|ruby| kobako_error_class(ruby, "TrapError"));
33
+
34
+ pub(crate) static TIMEOUT_ERROR: Lazy<ExceptionClass> =
35
+ Lazy::new(|ruby| kobako_error_class(ruby, "TimeoutError"));
36
+
37
+ pub(crate) static MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> =
38
+ Lazy::new(|ruby| kobako_error_class(ruby, "MemoryLimitError"));
39
+
40
+ pub(crate) static SANDBOX_ERROR: Lazy<ExceptionClass> =
41
+ Lazy::new(|ruby| kobako_error_class(ruby, "SandboxError"));
42
+
43
+ /// Build a `MagnusError` in `class` carrying `msg` — the shared body of
44
+ /// the named `*_err` constructors below, which differ only in which
45
+ /// error-class `Lazy` they target.
46
+ fn error_in(ruby: &Ruby, class: &Lazy<ExceptionClass>, msg: impl Into<String>) -> MagnusError {
47
+ MagnusError::new(ruby.get_inner(class), msg.into())
48
+ }
49
+
50
+ /// Construct a `Kobako::TrapError` magnus error. Used for every
51
+ /// invocation-time wasmtime engine failure that is not a configured-cap
52
+ /// trap — missing exports, allocation faults, memory write/read failures.
53
+ /// Construction-time setup failures use `setup_err`, not this.
54
+ pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
55
+ error_in(ruby, &TRAP_ERROR, msg)
56
+ }
57
+
58
+ /// Construct a `Kobako::SetupError` magnus error. Used for every
59
+ /// construction-time failure on the `Runtime.from_path` path before any
60
+ /// invocation runs — unreadable artifact, bytes that are not a valid Wasm
61
+ /// module, or engine / linker / instantiation setup failure. The
62
+ /// `ModuleNotBuiltError` subclass (artifact absent) is
63
+ /// raised through `MODULE_NOT_BUILT_ERROR` directly.
64
+ pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
65
+ error_in(ruby, &SETUP_ERROR, msg)
66
+ }
67
+
68
+ /// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
69
+ /// wall-clock cap path with the verb prefix added
70
+ /// by `Kobako::Sandbox#invoke!`.
71
+ pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
72
+ error_in(ruby, &TIMEOUT_ERROR, msg)
73
+ }
74
+
75
+ /// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
76
+ /// linear-memory cap path with the verb prefix
77
+ /// added by `Kobako::Sandbox#invoke!`.
78
+ pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
79
+ error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
80
+ }
81
+
82
+ /// Construct a `Kobako::SandboxError` magnus error. Used for the
83
+ /// host-side pre-call faults the SPEC attributes to the sandbox / wire
84
+ /// layer rather than the Wasm engine — currently the `#run` invocation
85
+ /// envelope reservation failure (`__kobako_alloc` returns 0).
86
+ /// The runtime is intact, so this must not be a
87
+ /// `TrapError`: no discard-and-recreate recovery is owed to the caller.
88
+ pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
89
+ error_in(ruby, &SANDBOX_ERROR, msg)
90
+ }
@@ -0,0 +1,188 @@
1
+ //! Per-invocation byte-shuttle between Ruby and guest linear memory: it
2
+ //! resolves the required `memory` / ABI-export handles, writes the `#run`
3
+ //! envelope into a freshly allocated guest buffer, builds the stdin frame
4
+ //! stream plus stdout / stderr capture pipes for the WASI context, and
5
+ //! reads the OUTCOME_BUFFER back out. The ext owns no wire codec — these
6
+ //! helpers move raw bytes; Ruby decodes them.
7
+
8
+ use magnus::{Error as MagnusError, RString, Ruby};
9
+ use wasmtime::{AsContextMut, Memory, Store as WtStore, TypedFunc};
10
+ use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
11
+ use wasmtime_wasi::WasiCtxBuilder;
12
+
13
+ use super::config::Config;
14
+ use super::exports::Exports;
15
+ use super::invocation::Invocation;
16
+ use super::rstring_to_vec;
17
+ use super::{ambient, capture, errors, guest_mem};
18
+
19
+ /// Return the resolved `memory` export handle, or raise
20
+ /// `Kobako::TrapError` when the loaded module exports no linear
21
+ /// memory — the "not a Kobako-shaped runtime" failure mode
22
+ /// (`SANDBOX_RUNTIME_NOT_KOBAKO`).
23
+ fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError> {
24
+ exports
25
+ .memory
26
+ .ok_or_else(|| errors::trap_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO))
27
+ }
28
+
29
+ /// Allocate a `len`-byte buffer in guest linear memory via
30
+ /// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
31
+ /// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
32
+ /// Raises `Kobako::TrapError` when the allocation hook is missing or
33
+ /// itself traps, and `Kobako::SandboxError` when the hook runs but
34
+ /// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
35
+ /// intact runtime, not an engine fault.
36
+ pub(crate) fn write_envelope(
37
+ ruby: &Ruby,
38
+ store: &mut WtStore<Invocation>,
39
+ exports: &Exports,
40
+ envelope: RString,
41
+ ) -> Result<(i32, i32), MagnusError> {
42
+ let bytes = rstring_to_vec(envelope);
43
+ let len_i32 =
44
+ guest_mem::checked_payload_len(bytes.len()).map_err(|msg| errors::trap_err(ruby, msg))?;
45
+
46
+ let alloc = require_export(ruby, exports.alloc.as_ref())?;
47
+ let memory = require_memory(ruby, exports)?;
48
+
49
+ let ptr = alloc
50
+ .call(store.as_context_mut(), bytes.len() as u32)
51
+ .map_err(|e| errors::trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
52
+ if ptr == 0 {
53
+ return Err(errors::sandbox_err(
54
+ ruby,
55
+ "could not allocate input buffer (out of memory)",
56
+ ));
57
+ }
58
+ let data = memory.data_mut(store.as_context_mut());
59
+ let range = guest_mem::guest_buffer_range(ptr as usize, bytes.len(), data.len())
60
+ .map_err(|msg| errors::trap_err(ruby, msg))?;
61
+ data[range].copy_from_slice(&bytes);
62
+
63
+ Ok((ptr as i32, len_i32))
64
+ }
65
+
66
+ /// Build the per-invocation WASI context with stdin carrying every frame
67
+ /// in `frames` (each prefixed by its 4-byte big-endian u32 length —
68
+ /// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
69
+ /// pipes, and install it on the invocation's Store. `#eval` passes three
70
+ /// frames (preamble, source, snippets), `#run` passes two (preamble,
71
+ /// snippets — the invocation envelope arrives via linear memory
72
+ /// instead). Each output pipe is sized at `cap + 1` so
73
+ /// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
74
+ /// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
75
+ /// rely on `memory_limit` for the real ceiling.
76
+ /// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
77
+ /// keeps its `u32` length prefix from wrapping.
78
+ pub(crate) fn install_wasi_frames(
79
+ store: &mut WtStore<Invocation>,
80
+ config: &Config,
81
+ frames: &[Vec<u8>],
82
+ ) -> Result<(), MagnusError> {
83
+ let ruby = Ruby::get().expect("Ruby thread");
84
+ // Every frame carries the same 16 MiB cap as the `#run` envelope
85
+ // (`write_envelope`): the length prefix is a `u32`, so a frame past
86
+ // the cap would silently wrap and corrupt the stdin frame stream.
87
+ for frame in frames {
88
+ guest_mem::checked_payload_len(frame.len()).map_err(|msg| errors::trap_err(&ruby, msg))?;
89
+ }
90
+
91
+ let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
92
+ let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
93
+ for frame in frames {
94
+ stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
95
+ stdin_content.extend_from_slice(frame);
96
+ }
97
+
98
+ let stdin_pipe = MemoryInputPipe::new(stdin_content);
99
+ let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
100
+ let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
101
+
102
+ let mut builder = WasiCtxBuilder::new();
103
+ builder.stdin(stdin_pipe);
104
+ builder.stdout(stdout_pipe.clone());
105
+ builder.stderr(stderr_pipe.clone());
106
+ // Deny the preview1 ambient-authority imports the guest never legitimately
107
+ // reaches but the WASI layer would otherwise grant (see `ambient`).
108
+ builder.wall_clock(ambient::FrozenWallClock);
109
+ builder.monotonic_clock(ambient::FrozenMonotonicClock);
110
+ builder.secure_random(ambient::deterministic_rng());
111
+ let wasi = builder.build_p1();
112
+
113
+ store
114
+ .data_mut()
115
+ .install_wasi(wasi, stdout_pipe, stderr_pipe);
116
+ Ok(())
117
+ }
118
+
119
+ /// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
120
+ /// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
121
+ /// `Kobako::TrapError` when the export is missing, `len` exceeds the
122
+ /// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
123
+ /// the slice falls outside live memory, or the `memory` export itself
124
+ /// is absent.
125
+ pub(crate) fn fetch_outcome_bytes(
126
+ ruby: &Ruby,
127
+ store: &mut WtStore<Invocation>,
128
+ exports: &Exports,
129
+ ) -> Result<Vec<u8>, MagnusError> {
130
+ let take = require_export(ruby, exports.take_outcome.as_ref())?;
131
+ let mem = require_memory(ruby, exports)?;
132
+
133
+ let packed = take
134
+ .call(store.as_context_mut(), ())
135
+ .map_err(|e| errors::trap_err(ruby, format!("failed to read the Sandbox result: {}", e)))?;
136
+ let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
137
+ if len > guest_mem::MAX_DISPATCH_PAYLOAD {
138
+ return Err(errors::trap_err(
139
+ ruby,
140
+ "result payload exceeds the 16 MiB limit",
141
+ ));
142
+ }
143
+
144
+ let data = mem.data(store.as_context_mut());
145
+ let range = guest_mem::guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
146
+ errors::trap_err(
147
+ ruby,
148
+ format!("the Sandbox result is out of bounds: {}", msg),
149
+ )
150
+ })?;
151
+ Ok(data[range].to_vec())
152
+ }
153
+
154
+ /// User-facing message for the "Sandbox runtime is missing one of the
155
+ /// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
156
+ /// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
157
+ /// `__kobako_take_outcome`) are not actionable to callers, and the
158
+ /// gem itself raises this error so a self-reference like "matches the
159
+ /// kobako gem version" reads as third-person. The actionable
160
+ /// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
161
+ const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
162
+ rebuild data/kobako.wasm against the installed version";
163
+
164
+ /// User-facing message for the "the loaded Wasm module is not a
165
+ /// Kobako-shaped runtime at all" failure mode (no linear memory
166
+ /// export). Same phrasing philosophy as
167
+ /// `SANDBOX_RUNTIME_MISSING_HOOKS`.
168
+ const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
169
+ "the loaded Wasm module is not a Kobako-compatible runtime";
170
+
171
+ /// Return the resolved `TypedFunc` for an ABI export, or raise
172
+ /// `Kobako::TrapError` when the option is `None`. Both run-path
173
+ /// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
174
+ /// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
175
+ /// error" boilerplate; this helper collapses those sites onto one
176
+ /// safe entry. The user-facing message is intentionally export-
177
+ /// agnostic (see `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol
178
+ /// name is not actionable to callers, so it is not threaded in.
179
+ pub(crate) fn require_export<'a, Params, Results>(
180
+ ruby: &Ruby,
181
+ export: Option<&'a TypedFunc<Params, Results>>,
182
+ ) -> Result<&'a TypedFunc<Params, Results>, MagnusError>
183
+ where
184
+ Params: wasmtime::WasmParams,
185
+ Results: wasmtime::WasmResults,
186
+ {
187
+ export.ok_or_else(|| errors::trap_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))
188
+ }
@@ -20,8 +20,9 @@ use wasmtime::{Caller, InstancePre, Linker};
20
20
  use wasmtime_wasi::p1;
21
21
 
22
22
  use super::cache::{cached_module, shared_engine};
23
+ use super::errors::setup_err;
23
24
  use super::invocation::Invocation;
24
- use super::{dispatch, setup_err, trap};
25
+ use super::{dispatch, trap};
25
26
 
26
27
  static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
27
28
  OnceLock::new();
@@ -13,8 +13,8 @@ use std::time::Instant;
13
13
  use magnus::{Error as MagnusError, Ruby};
14
14
  use wasmtime::{StoreContextMut, UpdateDeadline};
15
15
 
16
+ use super::errors::{memory_limit_err, setup_err, timeout_err, trap_err};
16
17
  use super::invocation::{Invocation, MemoryLimitTrap, TimeoutTrap};
17
- use super::{memory_limit_err, setup_err, timeout_err, trap_err};
18
18
 
19
19
  /// Epoch-deadline callback installed on every Store. Read the per-run
20
20
  /// wall-clock deadline from `Invocation` and trap with