kobako 0.1.2 → 0.2.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 +4 -4
- data/Cargo.lock +1 -1
- data/README.md +95 -60
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +39 -1
- data/ext/kobako/src/wasm/dispatch.rs +20 -20
- data/ext/kobako/src/wasm/host_state.rs +261 -34
- data/ext/kobako/src/wasm/instance.rs +467 -272
- data/ext/kobako/src/wasm.rs +50 -19
- data/lib/kobako/capture.rb +46 -0
- data/lib/kobako/codec/decoder.rb +66 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +33 -0
- data/lib/kobako/codec/factory.rb +155 -0
- data/lib/kobako/codec/utils.rb +55 -0
- data/lib/kobako/codec.rb +27 -0
- data/lib/kobako/errors.rb +24 -1
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +133 -0
- data/lib/kobako/rpc/dispatcher.rb +169 -0
- data/lib/kobako/rpc/envelope.rb +118 -0
- data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
- data/lib/kobako/{wire → rpc}/handle.rb +4 -2
- data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
- data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
- data/lib/kobako/rpc/server.rb +156 -0
- data/lib/kobako/rpc.rb +11 -0
- data/lib/kobako/sandbox.rb +149 -69
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako/wasm.rb +6 -16
- data/lib/kobako.rb +2 -0
- data/sig/kobako/capture.rbs +13 -0
- data/sig/kobako/codec/decoder.rbs +11 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +9 -0
- data/sig/kobako/errors.rbs +52 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/rpc/dispatcher.rbs +33 -0
- data/sig/kobako/rpc/envelope.rbs +51 -0
- data/sig/kobako/rpc/fault.rbs +20 -0
- data/sig/kobako/rpc/handle.rbs +19 -0
- data/sig/kobako/rpc/handle_table.rbs +25 -0
- data/sig/kobako/rpc/namespace.rbs +24 -0
- data/sig/kobako/rpc/server.rbs +37 -0
- data/sig/kobako/rpc.rbs +4 -0
- data/sig/kobako/sandbox.rbs +53 -0
- data/sig/kobako/wasm.rbs +37 -0
- data/sig/kobako.rbs +0 -1
- metadata +37 -17
- data/lib/kobako/registry/dispatcher.rb +0 -168
- data/lib/kobako/registry.rb +0 -160
- data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
- data/lib/kobako/sandbox/output_buffer.rb +0 -79
- data/lib/kobako/wire/codec/decoder.rb +0 -87
- data/lib/kobako/wire/codec/encoder.rb +0 -41
- data/lib/kobako/wire/codec/error.rb +0 -35
- data/lib/kobako/wire/codec/factory.rb +0 -136
- data/lib/kobako/wire/codec.rb +0 -44
- data/lib/kobako/wire/envelope/payloads.rb +0 -145
- data/lib/kobako/wire/envelope.rb +0 -147
- data/lib/kobako/wire.rb +0 -40
|
@@ -3,56 +3,86 @@
|
|
|
3
3
|
//! Constructed via [`Instance::from_path`]; the wasmtime [`Engine`] and
|
|
4
4
|
//! compiled [`Module`] are owned by the [`super::cache`] singletons and
|
|
5
5
|
//! never surface to Ruby. The instance wraps a [`StoreCell`] (interior-
|
|
6
|
-
//! mutability around `wasmtime::Store<HostState>`) plus
|
|
7
|
-
//! [`TypedFunc`] handles for the SPEC ABI exports
|
|
6
|
+
//! mutability around `wasmtime::Store<HostState>`) plus two cached
|
|
7
|
+
//! [`TypedFunc`] handles for the SPEC ABI exports used by the host-driven
|
|
8
|
+
//! run path.
|
|
9
|
+
//!
|
|
10
|
+
//! The Ruby surface intentionally exposes intent, not the underlying ABI
|
|
11
|
+
//! (SPEC.md "Code Organization"). The two-frame stdin protocol, packed-u64
|
|
12
|
+
//! outcome encoding, and `__kobako_alloc` / `__kobako_take_outcome` /
|
|
13
|
+
//! `__kobako_run` exports are all wrapped inside [`Instance::run`] and
|
|
14
|
+
//! [`Instance::outcome`]; Ruby callers see only `#run(preamble, source)`,
|
|
15
|
+
//! `#stdout`, `#stderr`, `#outcome!`, and `#server=`.
|
|
8
16
|
//!
|
|
9
17
|
//! WASI stdout/stderr capture (SPEC.md B-04): wasmtime-wasi p1 bindings
|
|
10
|
-
//! route guest fd 1 and fd 2 into per-run [`MemoryOutputPipe`] instances
|
|
11
|
-
//!
|
|
12
|
-
//!
|
|
13
|
-
//!
|
|
14
|
-
//!
|
|
15
|
-
//!
|
|
16
|
-
//!
|
|
17
|
-
//!
|
|
18
|
-
//!
|
|
18
|
+
//! route guest fd 1 and fd 2 into per-run [`MemoryOutputPipe`] instances
|
|
19
|
+
//! rebuilt at the start of every [`Instance::run`]. The per-channel cap
|
|
20
|
+
//! is enforced directly on the pipe — the pipe is sized at `cap + 1` so
|
|
21
|
+
//! a guest that writes exactly `cap` bytes is distinguishable from one
|
|
22
|
+
//! that exceeded the cap, and `#stdout` / `#stderr` slice the captured
|
|
23
|
+
//! bytes back to `cap` before returning them paired with a truncation
|
|
24
|
+
//! flag. Uncapped channels (`None`) build the pipe at `usize::MAX`;
|
|
25
|
+
//! `memory_limit` provides the real upper bound in that case.
|
|
26
|
+
//!
|
|
27
|
+
//! Per-run cap enforcement (SPEC.md B-01, E-19, E-20): every Store
|
|
28
|
+
//! installs an epoch-deadline callback for wall-clock timeout and a
|
|
29
|
+
//! [`ResourceLimiter`] for the linear-memory cap. Wasmtime turns
|
|
30
|
+
//! limiter / callback errors into traps; `Instance::run` downcasts the
|
|
31
|
+
//! trap source to surface as `Kobako::Wasm::TimeoutError` or
|
|
32
|
+
//! `Kobako::Wasm::MemoryLimitError` so the `Sandbox` layer can map them
|
|
33
|
+
//! to the named `Kobako::TrapError` subclasses.
|
|
19
34
|
//!
|
|
20
35
|
//! [`Engine`]: wasmtime::Engine
|
|
21
36
|
//! [`Module`]: wasmtime::Module
|
|
22
37
|
//! [`TypedFunc`]: wasmtime::TypedFunc
|
|
23
38
|
//! [`MemoryOutputPipe`]: wasmtime_wasi::p2::pipe::MemoryOutputPipe
|
|
39
|
+
//! [`ResourceLimiter`]: wasmtime::ResourceLimiter
|
|
24
40
|
|
|
25
|
-
use std::cell::RefCell;
|
|
26
41
|
use std::path::Path;
|
|
42
|
+
use std::time::{Duration, Instant};
|
|
27
43
|
|
|
28
|
-
use magnus::RString;
|
|
29
|
-
use magnus::{value::Opaque, Error as MagnusError, Ruby, Value};
|
|
44
|
+
use magnus::{value::Opaque, Error as MagnusError, RArray, RString, Ruby, Value};
|
|
30
45
|
use wasmtime::{
|
|
31
|
-
AsContextMut, Caller,
|
|
32
|
-
|
|
46
|
+
AsContextMut, Caller, Extern, Instance as WtInstance, Linker, Memory, Module as WtModule,
|
|
47
|
+
ResourceLimiter, Store as WtStore, StoreContextMut, TypedFunc, UpdateDeadline,
|
|
33
48
|
};
|
|
34
49
|
use wasmtime_wasi::p1;
|
|
35
50
|
use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
|
|
36
51
|
use wasmtime_wasi::WasiCtxBuilder;
|
|
37
52
|
|
|
38
53
|
use super::cache::{cached_module, shared_engine};
|
|
39
|
-
use super::dispatch
|
|
40
|
-
use super::host_state::{HostState, StoreCell};
|
|
41
|
-
use super::wasm_err;
|
|
54
|
+
use super::dispatch;
|
|
55
|
+
use super::host_state::{HostState, MemoryLimitTrap, StoreCell, TimeoutTrap};
|
|
56
|
+
use super::{memory_limit_err, timeout_err, wasm_err};
|
|
42
57
|
|
|
43
58
|
#[magnus::wrap(class = "Kobako::Wasm::Instance", free_immediately, size)]
|
|
44
59
|
pub(crate) struct Instance {
|
|
45
60
|
inner: WtInstance,
|
|
46
61
|
store: StoreCell,
|
|
47
|
-
// Cached TypedFunc handles for the
|
|
48
|
-
// test fixtures (a minimal "ping" module) need not
|
|
49
|
-
// kobako.wasm always does, and the
|
|
62
|
+
// Cached TypedFunc handles for the two host-driven ABI exports.
|
|
63
|
+
// Optional because test fixtures (a minimal "ping" module) need not
|
|
64
|
+
// provide them; real kobako.wasm always does, and the run-path methods
|
|
65
|
+
// raise a Ruby `Kobako::Wasm::Error` when an export is missing.
|
|
50
66
|
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
67
|
+
// `__kobako_alloc` is NOT cached here — only `dispatch.rs` calls it,
|
|
68
|
+
// and it does so through `Caller::get_export` on the wasmtime side.
|
|
53
69
|
run: Option<TypedFunc<(), ()>>,
|
|
54
70
|
take_outcome: Option<TypedFunc<(), u64>>,
|
|
55
|
-
|
|
71
|
+
// Wall-clock cap for one guest `#run` (SPEC.md B-01); `None` disables
|
|
72
|
+
// the cap. Translated into an `Instant`-based deadline stamped into
|
|
73
|
+
// [`HostState`] at the top of every `Instance::run`.
|
|
74
|
+
timeout: Option<Duration>,
|
|
75
|
+
// Per-channel byte caps for guest stdout / stderr capture (SPEC.md
|
|
76
|
+
// B-01 / B-04). `None` disables the cap on that channel. Read by
|
|
77
|
+
// [`Instance::refresh_wasi`] to size the MemoryOutputPipe and by
|
|
78
|
+
// [`Instance::stdout`] / [`Instance::stderr`] to compute the
|
|
79
|
+
// truncation flag. See the module-level note above for the `cap + 1`
|
|
80
|
+
// sizing rationale. Unlike `memory_limit` (which lives on
|
|
81
|
+
// [`HostState`] because the wasmtime [`ResourceLimiter`] callback
|
|
82
|
+
// consumes it from within the wasm engine), these caps are read only
|
|
83
|
+
// by Instance methods, so they live on Instance itself.
|
|
84
|
+
stdout_limit_bytes: Option<usize>,
|
|
85
|
+
stderr_limit_bytes: Option<usize>,
|
|
56
86
|
}
|
|
57
87
|
|
|
58
88
|
impl Instance {
|
|
@@ -60,302 +90,467 @@ impl Instance {
|
|
|
60
90
|
/// shared Engine and per-path Module cache. The single Ruby-facing
|
|
61
91
|
/// constructor for `Kobako::Wasm::Instance` — Engine and Module are
|
|
62
92
|
/// never visible to Ruby.
|
|
63
|
-
|
|
93
|
+
///
|
|
94
|
+
/// `timeout_seconds` is the SPEC.md B-01 wall-clock cap in seconds
|
|
95
|
+
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
96
|
+
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
97
|
+
/// are the per-channel output caps (SPEC.md B-01 / B-04; `None`
|
|
98
|
+
/// disables). All four are validated by the caller
|
|
99
|
+
/// (`Kobako::Sandbox`); this method only refuses non-finite or
|
|
100
|
+
/// non-positive timeouts as a defence in depth.
|
|
101
|
+
pub(crate) fn from_path(
|
|
102
|
+
path: String,
|
|
103
|
+
timeout_seconds: Option<f64>,
|
|
104
|
+
memory_limit: Option<usize>,
|
|
105
|
+
stdout_limit_bytes: Option<usize>,
|
|
106
|
+
stderr_limit_bytes: Option<usize>,
|
|
107
|
+
) -> Result<Self, MagnusError> {
|
|
108
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
109
|
+
let timeout = match timeout_seconds {
|
|
110
|
+
None => None,
|
|
111
|
+
Some(secs) if secs.is_finite() && secs > 0.0 => Some(Duration::from_secs_f64(secs)),
|
|
112
|
+
Some(secs) => {
|
|
113
|
+
return Err(wasm_err(
|
|
114
|
+
&ruby,
|
|
115
|
+
format!("timeout_seconds must be > 0 and finite, got {secs}"),
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
64
120
|
let engine = shared_engine()?;
|
|
65
121
|
let module = cached_module(Path::new(&path))?;
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
|
|
122
|
+
|
|
123
|
+
let mut store = WtStore::new(engine, HostState::new(memory_limit));
|
|
124
|
+
store.limiter(|state: &mut HostState| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
125
|
+
store.epoch_deadline_callback(epoch_deadline_callback);
|
|
126
|
+
|
|
127
|
+
let store_cell = StoreCell::new(store);
|
|
128
|
+
Self::build(
|
|
129
|
+
engine,
|
|
130
|
+
&module,
|
|
131
|
+
store_cell,
|
|
132
|
+
timeout,
|
|
133
|
+
stdout_limit_bytes,
|
|
134
|
+
stderr_limit_bytes,
|
|
135
|
+
)
|
|
69
136
|
}
|
|
70
137
|
|
|
71
|
-
///
|
|
72
|
-
///
|
|
73
|
-
///
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
138
|
+
/// Build an `Instance` from an engine, module, and store cell. The
|
|
139
|
+
/// store cell is moved in and ends up owned by the returned Instance.
|
|
140
|
+
/// Wires the WASI p1 imports plus the `__kobako_dispatch` host import.
|
|
141
|
+
fn build(
|
|
142
|
+
engine: &wasmtime::Engine,
|
|
143
|
+
module: &WtModule,
|
|
144
|
+
store_cell: StoreCell,
|
|
145
|
+
timeout: Option<Duration>,
|
|
146
|
+
stdout_limit_bytes: Option<usize>,
|
|
147
|
+
stderr_limit_bytes: Option<usize>,
|
|
148
|
+
) -> Result<Self, MagnusError> {
|
|
149
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
150
|
+
let mut linker: Linker<HostState> = Linker::new(engine);
|
|
151
|
+
|
|
152
|
+
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
153
|
+
// to the MemoryOutputPipes set up before each run via
|
|
154
|
+
// `Instance::run`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
155
|
+
// HostState; the panic semantics live inside `HostState::wasi_mut`
|
|
156
|
+
// so the wiring stays honest about its precondition.
|
|
157
|
+
p1::add_to_linker_sync(&mut linker, |state: &mut HostState| state.wasi_mut())
|
|
158
|
+
.map_err(|e| wasm_err(&ruby, format!("add WASI p1 to linker: {}", e)))?;
|
|
159
|
+
|
|
160
|
+
// `__kobako_dispatch` host import. Signature per SPEC Wire ABI:
|
|
161
|
+
// (req_ptr: i32, req_len: i32) -> i64
|
|
162
|
+
// Decodes the Request bytes, dispatches via the Ruby-side
|
|
163
|
+
// `Kobako::RPC::Server` (set per-run via `set_server`), allocates a
|
|
164
|
+
// guest buffer through `__kobako_alloc`, writes the Response bytes
|
|
165
|
+
// there, and returns the packed `(ptr<<32)|len`. The dispatcher
|
|
166
|
+
// returns 0 on any wire-layer fault (including a missing
|
|
167
|
+
// Server); see `dispatch::handle`.
|
|
168
|
+
linker
|
|
169
|
+
.func_wrap(
|
|
170
|
+
"env",
|
|
171
|
+
"__kobako_dispatch",
|
|
172
|
+
|mut caller: Caller<'_, HostState>, req_ptr: i32, req_len: i32| -> i64 {
|
|
173
|
+
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
.map_err(|e| wasm_err(&ruby, format!("define __kobako_dispatch: {}", e)))?;
|
|
177
|
+
|
|
178
|
+
let instance = {
|
|
179
|
+
let mut store_ref = store_cell.borrow_mut();
|
|
180
|
+
linker
|
|
181
|
+
.instantiate(store_ref.as_context_mut(), module)
|
|
182
|
+
.map_err(|e| instantiate_err(&ruby, e))?
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Best-effort export lookup. Missing exports are not an error here
|
|
186
|
+
// (test fixture is a bare module); the host enforces presence at
|
|
187
|
+
// invocation time by raising a Ruby `Kobako::Wasm::Error` when the
|
|
188
|
+
// cached Option is None. Only the SPEC ABI `() -> ()` shape is
|
|
189
|
+
// accepted for `__kobako_run`.
|
|
190
|
+
let (run, take_outcome) = {
|
|
191
|
+
let mut store_ref = store_cell.borrow_mut();
|
|
192
|
+
let mut ctx = store_ref.as_context_mut();
|
|
193
|
+
let run = instance
|
|
194
|
+
.get_typed_func::<(), ()>(&mut ctx, "__kobako_run")
|
|
195
|
+
.ok();
|
|
196
|
+
let take_outcome = instance
|
|
197
|
+
.get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
|
|
198
|
+
.ok();
|
|
199
|
+
(run, take_outcome)
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
Ok(Self {
|
|
203
|
+
inner: instance,
|
|
204
|
+
store: store_cell,
|
|
205
|
+
run,
|
|
206
|
+
take_outcome,
|
|
207
|
+
timeout,
|
|
208
|
+
stdout_limit_bytes,
|
|
209
|
+
stderr_limit_bytes,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Install the Ruby-side `Kobako::RPC::Server` into HostState. Bound to
|
|
214
|
+
/// Ruby as `Instance#server=`. From this point on, every
|
|
215
|
+
/// `__kobako_dispatch` import invocation routes through
|
|
216
|
+
/// `server.dispatch(req_bytes)`.
|
|
217
|
+
pub(crate) fn set_server(&self, server: Value) -> Result<(), MagnusError> {
|
|
218
|
+
let mut store_ref = self.store.borrow_mut();
|
|
219
|
+
store_ref.data_mut().bind_server(Opaque::from(server));
|
|
78
220
|
Ok(())
|
|
79
221
|
}
|
|
80
222
|
|
|
81
223
|
// -----------------------------------------------------------------
|
|
82
|
-
// Run-path methods.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// three-class taxonomy.
|
|
224
|
+
// Run-path methods. Each method is best-effort — it raises a Ruby
|
|
225
|
+
// `Kobako::Wasm::Error` when the corresponding export is missing or
|
|
226
|
+
// fails so the Sandbox layer can map errors to the three-class
|
|
227
|
+
// taxonomy.
|
|
87
228
|
// -----------------------------------------------------------------
|
|
88
229
|
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
///
|
|
92
|
-
///
|
|
93
|
-
|
|
230
|
+
/// Execute one guest run.
|
|
231
|
+
///
|
|
232
|
+
/// Rebuilds the WASI context with fresh stdin / stdout / stderr pipes
|
|
233
|
+
/// (the two-frame stdin protocol carries +preamble+ then +source+ —
|
|
234
|
+
/// SPEC.md ABI Signatures), then invokes `__kobako_run`. Per-run
|
|
235
|
+
/// caps (SPEC.md B-01) are primed here: the wall-clock deadline is
|
|
236
|
+
/// stamped into [`HostState`] and the epoch deadline is set to fire
|
|
237
|
+
/// at the next ticker tick; the memory-cap limiter is already wired.
|
|
238
|
+
pub(crate) fn run(&self, preamble: RString, source: RString) -> Result<(), MagnusError> {
|
|
94
239
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
95
|
-
let
|
|
96
|
-
.
|
|
240
|
+
let run = self
|
|
241
|
+
.run
|
|
97
242
|
.as_ref()
|
|
98
|
-
.ok_or_else(|| wasm_err(&ruby, "guest does not export
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
243
|
+
.ok_or_else(|| wasm_err(&ruby, "guest does not export __kobako_run"))?;
|
|
244
|
+
self.refresh_wasi(preamble, source)?;
|
|
245
|
+
self.prime_caps();
|
|
246
|
+
let result = self.call_guest(run);
|
|
247
|
+
self.disarm_caps();
|
|
248
|
+
result.map_err(|e| run_call_err(&ruby, e))
|
|
103
249
|
}
|
|
104
250
|
|
|
105
|
-
///
|
|
106
|
-
///
|
|
107
|
-
///
|
|
108
|
-
|
|
251
|
+
/// Return the stdout capture from the most recent run as a Ruby
|
|
252
|
+
/// `[bytes, truncated]` Array — `bytes` is a binary String containing
|
|
253
|
+
/// the captured prefix (clipped to `stdout_limit_bytes` when set),
|
|
254
|
+
/// and `truncated` is a boolean that is `true` only when the guest
|
|
255
|
+
/// wrote strictly more than the cap. The pair is recomputed from the
|
|
256
|
+
/// underlying pipe contents on every call; the pipe itself is not
|
|
257
|
+
/// drained until the next `#run` rebuilds it.
|
|
258
|
+
pub(crate) fn stdout(&self) -> Result<RArray, MagnusError> {
|
|
109
259
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
_ => return Err(wasm_err(&ruby, "guest does not export 'memory'")),
|
|
114
|
-
};
|
|
260
|
+
let raw = self.store.borrow().data().stdout_bytes();
|
|
261
|
+
capture_pair(&ruby, &raw, self.stdout_limit_bytes)
|
|
262
|
+
}
|
|
115
263
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
264
|
+
/// Return the stderr capture from the most recent run. Same shape
|
|
265
|
+
/// and semantics as [`Instance::stdout`].
|
|
266
|
+
pub(crate) fn stderr(&self) -> Result<RArray, MagnusError> {
|
|
267
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
268
|
+
let raw = self.store.borrow().data().stderr_bytes();
|
|
269
|
+
capture_pair(&ruby, &raw, self.stderr_limit_bytes)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Read OUTCOME_BUFFER bytes captured during the most recent run.
|
|
273
|
+
/// Bound to Ruby as `Instance#outcome!`. The bang signals that the
|
|
274
|
+
/// underlying `__kobako_take_outcome` export is guest-side destructive
|
|
275
|
+
/// — the buffer pointer is invalidated after this call, so a second
|
|
276
|
+
/// invocation within the same run is undefined — and that any failure
|
|
277
|
+
/// (missing export, length overflow, OOB read) raises
|
|
278
|
+
/// `Kobako::Wasm::Error`.
|
|
279
|
+
pub(crate) fn outcome(&self) -> Result<RString, MagnusError> {
|
|
280
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
281
|
+
let bytes = self.fetch_outcome_bytes(&ruby)?;
|
|
282
|
+
Ok(ruby.str_from_slice(&bytes))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// -----------------------------------------------------------------
|
|
286
|
+
// Private helpers.
|
|
287
|
+
// -----------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
/// Stamp the per-run wall-clock deadline into [`HostState`] and prime
|
|
290
|
+
/// the wasmtime epoch deadline so the next ticker tick wakes the
|
|
291
|
+
/// epoch-deadline callback. When `timeout` is disabled, the deadline
|
|
292
|
+
/// is set far enough in the future that the callback effectively
|
|
293
|
+
/// never fires.
|
|
294
|
+
fn prime_caps(&self) {
|
|
295
|
+
let mut store_ref = self.store.borrow_mut();
|
|
296
|
+
match self.timeout {
|
|
297
|
+
Some(timeout) => {
|
|
298
|
+
let deadline = Instant::now() + timeout;
|
|
299
|
+
store_ref.data_mut().set_deadline(Some(deadline));
|
|
300
|
+
store_ref.set_epoch_deadline(1);
|
|
301
|
+
}
|
|
302
|
+
None => {
|
|
303
|
+
store_ref.data_mut().set_deadline(None);
|
|
304
|
+
store_ref.set_epoch_deadline(u64::MAX);
|
|
305
|
+
}
|
|
133
306
|
}
|
|
134
|
-
|
|
307
|
+
store_ref.data_mut().limiter_mut().activate();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Drop the memory cap as soon as the guest call returns so that
|
|
311
|
+
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
312
|
+
/// which can grow guest memory transiently) is not attributed to
|
|
313
|
+
/// the user script. Paired with [`Instance::prime_caps`].
|
|
314
|
+
fn disarm_caps(&self) {
|
|
315
|
+
self.store
|
|
316
|
+
.borrow_mut()
|
|
317
|
+
.data_mut()
|
|
318
|
+
.limiter_mut()
|
|
319
|
+
.deactivate();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Invoke the cached `__kobako_run` TypedFunc against the live
|
|
323
|
+
/// Store. Lives in its own helper so [`Instance::run`] reads as
|
|
324
|
+
/// the run-path outline (export check → refresh WASI → prime caps
|
|
325
|
+
/// → call guest → disarm caps → map errors) without the
|
|
326
|
+
/// `RefCell::borrow_mut` boilerplate inline.
|
|
327
|
+
fn call_guest(&self, run: &TypedFunc<(), ()>) -> wasmtime::Result<()> {
|
|
328
|
+
let mut store_ref = self.store.borrow_mut();
|
|
329
|
+
run.call(store_ref.as_context_mut(), ())
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Rebuild the WASI context with fresh stdin (two-frame: preamble then
|
|
333
|
+
/// source) plus fresh stdout/stderr pipes. Called at the top of every
|
|
334
|
+
/// `#run`. Each pipe is sized at `cap + 1` so [`Instance::stdout`] /
|
|
335
|
+
/// [`Instance::stderr`] can distinguish "wrote exactly cap bytes"
|
|
336
|
+
/// from "exceeded cap"; uncapped channels fall back to `usize::MAX`
|
|
337
|
+
/// and rely on `memory_limit` (E-20) for the real ceiling.
|
|
338
|
+
fn refresh_wasi(&self, preamble: RString, source: RString) -> Result<(), MagnusError> {
|
|
339
|
+
// SAFETY: `as_slice` borrows are scoped to building the stdin Vec
|
|
340
|
+
// below — no Ruby allocations happen between the borrow and the
|
|
341
|
+
// copy, so the underlying RString cannot move.
|
|
342
|
+
let preamble_bytes: &[u8] = unsafe { preamble.as_slice() };
|
|
343
|
+
let source_bytes: &[u8] = unsafe { source.as_slice() };
|
|
344
|
+
|
|
345
|
+
let mut stdin_content: Vec<u8> =
|
|
346
|
+
Vec::with_capacity(4 + preamble_bytes.len() + 4 + source_bytes.len());
|
|
347
|
+
// Frame 1 — preamble
|
|
348
|
+
stdin_content.extend_from_slice(&(preamble_bytes.len() as u32).to_be_bytes());
|
|
349
|
+
stdin_content.extend_from_slice(preamble_bytes);
|
|
350
|
+
// Frame 2 — user script
|
|
351
|
+
stdin_content.extend_from_slice(&(source_bytes.len() as u32).to_be_bytes());
|
|
352
|
+
stdin_content.extend_from_slice(source_bytes);
|
|
353
|
+
|
|
354
|
+
let stdin_pipe = MemoryInputPipe::new(stdin_content);
|
|
355
|
+
let stdout_pipe = MemoryOutputPipe::new(pipe_capacity(self.stdout_limit_bytes));
|
|
356
|
+
let stderr_pipe = MemoryOutputPipe::new(pipe_capacity(self.stderr_limit_bytes));
|
|
357
|
+
|
|
358
|
+
let mut builder = WasiCtxBuilder::new();
|
|
359
|
+
builder.stdin(stdin_pipe);
|
|
360
|
+
builder.stdout(stdout_pipe.clone());
|
|
361
|
+
builder.stderr(stderr_pipe.clone());
|
|
362
|
+
let wasi = builder.build_p1();
|
|
363
|
+
|
|
364
|
+
self.store
|
|
365
|
+
.borrow_mut()
|
|
366
|
+
.data_mut()
|
|
367
|
+
.install_wasi(wasi, stdout_pipe, stderr_pipe);
|
|
368
|
+
|
|
135
369
|
Ok(())
|
|
136
370
|
}
|
|
137
371
|
|
|
138
|
-
///
|
|
139
|
-
///
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
372
|
+
/// Invoke `__kobako_take_outcome`, decode the packed +(ptr<<32)|len+
|
|
373
|
+
/// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
|
|
374
|
+
/// `Kobako::Wasm::Error` when the export is missing, the +ptr+/+len+
|
|
375
|
+
/// arithmetic overflows, the slice falls outside live memory, or the
|
|
376
|
+
/// `memory` export itself is absent.
|
|
377
|
+
fn fetch_outcome_bytes(&self, ruby: &Ruby) -> Result<Vec<u8>, MagnusError> {
|
|
378
|
+
let take = self
|
|
379
|
+
.take_outcome
|
|
380
|
+
.as_ref()
|
|
381
|
+
.ok_or_else(|| wasm_err(ruby, "guest does not export __kobako_take_outcome"))?;
|
|
382
|
+
|
|
383
|
+
let mut store_ref = self.store.borrow_mut();
|
|
384
|
+
let packed = take
|
|
385
|
+
.call(store_ref.as_context_mut(), ())
|
|
386
|
+
.map_err(|e| wasm_err(ruby, format!("__kobako_take_outcome(): {}", e)))?;
|
|
387
|
+
let ptr = ((packed >> 32) & 0xffff_ffff) as usize;
|
|
388
|
+
let len = (packed & 0xffff_ffff) as usize;
|
|
389
|
+
|
|
143
390
|
let mem: Memory = match self.inner.get_export(store_ref.as_context_mut(), "memory") {
|
|
144
391
|
Some(Extern::Memory(m)) => m,
|
|
145
|
-
_ => return Err(wasm_err(
|
|
392
|
+
_ => return Err(wasm_err(ruby, "guest does not export 'memory'")),
|
|
146
393
|
};
|
|
147
394
|
let data = mem.data(store_ref.as_context_mut());
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
.ok_or_else(|| wasm_err(&ruby, "read_memory: ptr + len overflow"))?;
|
|
395
|
+
let end = ptr
|
|
396
|
+
.checked_add(len)
|
|
397
|
+
.ok_or_else(|| wasm_err(ruby, "outcome: ptr + len overflow"))?;
|
|
152
398
|
if end > data.len() {
|
|
153
399
|
return Err(wasm_err(
|
|
154
|
-
|
|
400
|
+
ruby,
|
|
155
401
|
format!(
|
|
156
|
-
"
|
|
157
|
-
|
|
402
|
+
"outcome: range [{}, {}) exceeds memory size {}",
|
|
403
|
+
ptr,
|
|
158
404
|
end,
|
|
159
405
|
data.len()
|
|
160
406
|
),
|
|
161
407
|
));
|
|
162
408
|
}
|
|
163
|
-
Ok(
|
|
409
|
+
Ok(data[ptr..end].to_vec())
|
|
164
410
|
}
|
|
411
|
+
}
|
|
165
412
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.ok_or_else(|| wasm_err(&ruby, "guest does not export __kobako_run"))?;
|
|
175
|
-
let mut store_ref = self.store.0.borrow_mut();
|
|
176
|
-
run.call(store_ref.as_context_mut(), ())
|
|
177
|
-
.map_err(|e| wasm_err(&ruby, format!("__kobako_run(): {}", e)))
|
|
413
|
+
/// Translate a per-channel byte cap into the MemoryOutputPipe capacity:
|
|
414
|
+
/// `cap + 1` (saturated against `usize::MAX`) when a cap is set so the
|
|
415
|
+
/// "wrote exactly cap" and "exceeded cap" cases stay distinguishable;
|
|
416
|
+
/// `usize::MAX` when the channel is uncapped.
|
|
417
|
+
fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
418
|
+
match cap {
|
|
419
|
+
Some(c) => c.saturating_add(1),
|
|
420
|
+
None => usize::MAX,
|
|
178
421
|
}
|
|
422
|
+
}
|
|
179
423
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.map_err(|e| wasm_err(&ruby, format!("__kobako_take_outcome(): {}", e)))
|
|
424
|
+
/// Pure slicing core shared by [`Instance::stdout`] / [`Instance::stderr`]:
|
|
425
|
+
/// given the unclipped pipe snapshot and the configured cap, return the
|
|
426
|
+
/// bytes Ruby should observe (clipped to `cap`) plus the truncation flag.
|
|
427
|
+
/// `truncated` is `true` only when the snapshot strictly exceeded the cap
|
|
428
|
+
/// — this is the "wrote `cap + 1` bytes into a `cap + 1`-sized pipe" case;
|
|
429
|
+
/// "wrote exactly `cap` bytes" stays `false`.
|
|
430
|
+
fn clip_capture(raw: &[u8], cap: Option<usize>) -> (&[u8], bool) {
|
|
431
|
+
match cap {
|
|
432
|
+
Some(c) if raw.len() > c => (&raw[..c], true),
|
|
433
|
+
_ => (raw, false),
|
|
191
434
|
}
|
|
435
|
+
}
|
|
192
436
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
437
|
+
/// Build the `[bytes, truncated]` Ruby Array surfaced by
|
|
438
|
+
/// [`Instance::stdout`] / [`Instance::stderr`]. Delegates the slicing
|
|
439
|
+
/// to [`clip_capture`] so the channel-agnostic logic stays unit-
|
|
440
|
+
/// testable from `cargo test`.
|
|
441
|
+
fn capture_pair(ruby: &Ruby, raw: &[u8], cap: Option<usize>) -> Result<RArray, MagnusError> {
|
|
442
|
+
let (visible, truncated) = clip_capture(raw, cap);
|
|
443
|
+
let arr = ruby.ary_new_capa(2);
|
|
444
|
+
arr.push(ruby.str_from_slice(visible))?;
|
|
445
|
+
arr.push(truncated)?;
|
|
446
|
+
Ok(arr)
|
|
447
|
+
}
|
|
196
448
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
/// * A MemoryOutputPipe for fd 2 (stderr) — transport-layer pipe.
|
|
206
|
-
///
|
|
207
|
-
/// `stdout_cap` and `stderr_cap` are accepted but the transport pipes are
|
|
208
|
-
/// uncapped: SPEC.md B-04 requires that overflowing the OutputBuffer limit
|
|
209
|
-
/// is a non-error outcome. A capped WASI pipe would produce a real trap.
|
|
210
|
-
pub(crate) fn setup_wasi_pipes(
|
|
211
|
-
&self,
|
|
212
|
-
stdout_cap: i64,
|
|
213
|
-
stderr_cap: i64,
|
|
214
|
-
preamble_bytes: RString,
|
|
215
|
-
source_bytes: RString,
|
|
216
|
-
) -> Result<(), MagnusError> {
|
|
217
|
-
let _ = (stdout_cap, stderr_cap);
|
|
218
|
-
|
|
219
|
-
// Build the two-frame stdin content. Each frame: 4-byte BE u32 length
|
|
220
|
-
// prefix + payload bytes (SPEC.md ABI Signatures — two-frame protocol).
|
|
221
|
-
let preamble: &[u8] = unsafe { preamble_bytes.as_slice() };
|
|
222
|
-
let source: &[u8] = unsafe { source_bytes.as_slice() };
|
|
223
|
-
|
|
224
|
-
let mut stdin_content: Vec<u8> = Vec::with_capacity(4 + preamble.len() + 4 + source.len());
|
|
225
|
-
// Frame 1 — preamble
|
|
226
|
-
stdin_content.extend_from_slice(&(preamble.len() as u32).to_be_bytes());
|
|
227
|
-
stdin_content.extend_from_slice(preamble);
|
|
228
|
-
// Frame 2 — user script
|
|
229
|
-
stdin_content.extend_from_slice(&(source.len() as u32).to_be_bytes());
|
|
230
|
-
stdin_content.extend_from_slice(source);
|
|
449
|
+
#[cfg(test)]
|
|
450
|
+
mod tests {
|
|
451
|
+
//! Host-side unit tests for the pure capture helpers. The Ruby-
|
|
452
|
+
//! facing E2E suite exercises stdout only (the kobako mrbgem
|
|
453
|
+
//! allowlist excludes guest fd 2 writes); these tests pin the
|
|
454
|
+
//! channel-agnostic slicing so a regression that only breaks one
|
|
455
|
+
//! channel cannot sneak through.
|
|
456
|
+
use super::{clip_capture, pipe_capacity};
|
|
231
457
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
458
|
+
#[test]
|
|
459
|
+
fn pipe_capacity_adds_one_when_cap_is_set() {
|
|
460
|
+
assert_eq!(pipe_capacity(Some(5)), 6);
|
|
461
|
+
assert_eq!(pipe_capacity(Some(0)), 1);
|
|
462
|
+
}
|
|
235
463
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
let wasi = builder.build_p1();
|
|
464
|
+
#[test]
|
|
465
|
+
fn pipe_capacity_falls_back_to_usize_max_when_uncapped() {
|
|
466
|
+
assert_eq!(pipe_capacity(None), usize::MAX);
|
|
467
|
+
}
|
|
241
468
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
data.stdout_pipe = Some(stdout_pipe);
|
|
247
|
-
data.stderr_pipe = Some(stderr_pipe);
|
|
248
|
-
}
|
|
469
|
+
#[test]
|
|
470
|
+
fn pipe_capacity_saturates_at_usize_max() {
|
|
471
|
+
assert_eq!(pipe_capacity(Some(usize::MAX)), usize::MAX);
|
|
472
|
+
}
|
|
249
473
|
|
|
250
|
-
|
|
474
|
+
#[test]
|
|
475
|
+
fn clip_capture_returns_full_bytes_when_under_cap() {
|
|
476
|
+
let (bytes, truncated) = clip_capture(b"abc", Some(5));
|
|
477
|
+
assert_eq!(bytes, b"abc");
|
|
478
|
+
assert!(!truncated);
|
|
251
479
|
}
|
|
252
480
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
pub(crate) fn take_stdout(&self) -> Result<RString, MagnusError> {
|
|
259
|
-
let ruby = Ruby::get().expect("Ruby thread");
|
|
260
|
-
let store_ref = self.store.0.borrow();
|
|
261
|
-
let bytes = store_ref
|
|
262
|
-
.data()
|
|
263
|
-
.stdout_pipe
|
|
264
|
-
.as_ref()
|
|
265
|
-
.map(|p| p.contents())
|
|
266
|
-
.unwrap_or_default();
|
|
267
|
-
Ok(ruby.str_from_slice(&bytes))
|
|
481
|
+
#[test]
|
|
482
|
+
fn clip_capture_does_not_flag_truncation_at_exactly_cap_bytes() {
|
|
483
|
+
let (bytes, truncated) = clip_capture(b"abcde", Some(5));
|
|
484
|
+
assert_eq!(bytes, b"abcde");
|
|
485
|
+
assert!(!truncated);
|
|
268
486
|
}
|
|
269
487
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
let
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
488
|
+
#[test]
|
|
489
|
+
fn clip_capture_clips_to_cap_and_flags_truncation_on_overflow() {
|
|
490
|
+
// The pipe is sized `cap + 1`, so the snapshot can be at most
|
|
491
|
+
// 6 bytes when `cap == 5`; that surface is what triggers the
|
|
492
|
+
// truncation flag.
|
|
493
|
+
let (bytes, truncated) = clip_capture(b"abcdef", Some(5));
|
|
494
|
+
assert_eq!(bytes, b"abcde");
|
|
495
|
+
assert!(truncated);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
fn clip_capture_treats_none_as_uncapped() {
|
|
500
|
+
let (bytes, truncated) = clip_capture(b"abcdef", None);
|
|
501
|
+
assert_eq!(bytes, b"abcdef");
|
|
502
|
+
assert!(!truncated);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
#[test]
|
|
506
|
+
fn clip_capture_handles_empty_input() {
|
|
507
|
+
let (bytes, truncated) = clip_capture(b"", Some(5));
|
|
508
|
+
assert_eq!(bytes, b"");
|
|
509
|
+
assert!(!truncated);
|
|
283
510
|
}
|
|
284
511
|
}
|
|
285
512
|
|
|
286
|
-
///
|
|
287
|
-
///
|
|
288
|
-
///
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
p1::add_to_linker_sync(&mut linker, |state: &mut HostState| {
|
|
304
|
-
state
|
|
305
|
-
.wasi
|
|
306
|
-
.as_mut()
|
|
307
|
-
.expect("WASI context not initialised — call setup_wasi_pipes before run")
|
|
308
|
-
})
|
|
309
|
-
.map_err(|e| wasm_err(&ruby, format!("add WASI p1 to linker: {}", e)))?;
|
|
310
|
-
|
|
311
|
-
// `__kobako_rpc_call` host import. Signature per SPEC Wire ABI:
|
|
312
|
-
// (req_ptr: i32, req_len: i32) -> i64
|
|
313
|
-
// Decodes the Request bytes, dispatches via the Ruby-side
|
|
314
|
-
// `Kobako::Registry` (set per-run via `set_registry`), allocates a guest
|
|
315
|
-
// buffer through `__kobako_alloc`, writes the Response bytes there, and
|
|
316
|
-
// returns the packed `(ptr<<32)|len`. The dispatcher returns 0 on any
|
|
317
|
-
// wire-layer fault (including a missing Registry); see `dispatch_rpc`.
|
|
318
|
-
linker
|
|
319
|
-
.func_wrap(
|
|
320
|
-
"env",
|
|
321
|
-
"__kobako_rpc_call",
|
|
322
|
-
|mut caller: Caller<'_, HostState>, req_ptr: i32, req_len: i32| -> i64 {
|
|
323
|
-
dispatch_rpc(&mut caller, req_ptr, req_len)
|
|
324
|
-
},
|
|
325
|
-
)
|
|
326
|
-
.map_err(|e| wasm_err(&ruby, format!("define __kobako_rpc_call: {}", e)))?;
|
|
513
|
+
/// Epoch-deadline callback installed on every Store. Read the per-run
|
|
514
|
+
/// wall-clock deadline from [`HostState`] (SPEC.md B-01) and trap with
|
|
515
|
+
/// [`TimeoutTrap`] once the deadline has passed; otherwise extend the
|
|
516
|
+
/// next check by one tick of the process-wide epoch ticker. When the
|
|
517
|
+
/// deadline is `None` the callback should not fire under normal
|
|
518
|
+
/// `Instance::run` flow because `set_epoch_deadline(u64::MAX)` is used;
|
|
519
|
+
/// returning a long extension keeps the callback inert as a defence in
|
|
520
|
+
/// depth.
|
|
521
|
+
fn epoch_deadline_callback(
|
|
522
|
+
ctx: StoreContextMut<'_, HostState>,
|
|
523
|
+
) -> wasmtime::Result<UpdateDeadline> {
|
|
524
|
+
match ctx.data().deadline() {
|
|
525
|
+
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
526
|
+
Some(_) => Ok(UpdateDeadline::Continue(1)),
|
|
527
|
+
None => Ok(UpdateDeadline::Continue(u64::MAX / 2)),
|
|
528
|
+
}
|
|
529
|
+
}
|
|
327
530
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
Ok(Instance {
|
|
355
|
-
inner: instance,
|
|
356
|
-
store: store_cell,
|
|
357
|
-
run,
|
|
358
|
-
take_outcome,
|
|
359
|
-
alloc,
|
|
360
|
-
})
|
|
531
|
+
/// Map a wasmtime call error to the right `Kobako::Wasm::*` Ruby
|
|
532
|
+
/// exception class. `__kobako_run` traps are downcast to identify the
|
|
533
|
+
/// configured-cap path (SPEC.md E-19 / E-20); everything else surfaces
|
|
534
|
+
/// as the base `Kobako::Wasm::Error`.
|
|
535
|
+
fn run_call_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
536
|
+
if err.downcast_ref::<TimeoutTrap>().is_some() {
|
|
537
|
+
return timeout_err(ruby, format!("__kobako_run(): {}", err));
|
|
538
|
+
}
|
|
539
|
+
if err.downcast_ref::<MemoryLimitTrap>().is_some() {
|
|
540
|
+
return memory_limit_err(ruby, format!("__kobako_run(): {}", err));
|
|
541
|
+
}
|
|
542
|
+
wasm_err(ruby, format!("__kobako_run(): {}", err))
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/// Map an instantiation error to the right `Kobako::Wasm::*` Ruby
|
|
546
|
+
/// exception. The memory cap is dormant during instantiation by design
|
|
547
|
+
/// (see [`HostState::set_memory_cap_active`]), but [`MemoryLimitTrap`]
|
|
548
|
+
/// is still possible if a future Sandbox configuration enables it
|
|
549
|
+
/// during instantiation — keep the mapping symmetric with
|
|
550
|
+
/// [`run_call_err`].
|
|
551
|
+
fn instantiate_err(ruby: &Ruby, err: wasmtime::Error) -> MagnusError {
|
|
552
|
+
if err.downcast_ref::<MemoryLimitTrap>().is_some() {
|
|
553
|
+
return memory_limit_err(ruby, format!("instantiate: {}", err));
|
|
554
|
+
}
|
|
555
|
+
wasm_err(ruby, format!("instantiate: {}", err))
|
|
361
556
|
}
|