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
|
@@ -1,56 +1,283 @@
|
|
|
1
1
|
//! Per-Store host state shared with every wasmtime callback.
|
|
2
2
|
//!
|
|
3
3
|
//! Owned by [`StoreCell`] (a `RefCell` shim wrapping `wasmtime::Store`)
|
|
4
|
-
//! and threaded through every host import — the `
|
|
5
|
-
//! dispatcher reads
|
|
6
|
-
//! [`crate::wasm::Instance`]
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
4
|
+
//! and threaded through every host import — the `__kobako_dispatch`
|
|
5
|
+
//! dispatcher reads the server handle, while the run-path methods on
|
|
6
|
+
//! [`crate::wasm::Instance`] install fresh WASI context + pipes before
|
|
7
|
+
//! every `#run` (SPEC.md B-03 / B-04).
|
|
8
|
+
//!
|
|
9
|
+
//! The state also carries the per-run wall-clock deadline (SPEC.md B-01,
|
|
10
|
+
//! E-19) and the linear-memory cap [`KobakoLimiter`] (SPEC.md B-01,
|
|
11
|
+
//! E-20). Both are read from the wasmtime `epoch_deadline_callback` /
|
|
12
|
+
//! `ResourceLimiter` callbacks installed in
|
|
13
|
+
//! [`crate::wasm::Instance::from_path`].
|
|
9
14
|
|
|
10
|
-
use std::cell::RefCell;
|
|
15
|
+
use std::cell::{Ref, RefCell, RefMut};
|
|
16
|
+
use std::time::Instant;
|
|
11
17
|
|
|
12
18
|
use magnus::{value::Opaque, Value};
|
|
13
|
-
use wasmtime::Store as WtStore;
|
|
19
|
+
use wasmtime::{ResourceLimiter, Store as WtStore};
|
|
14
20
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
15
21
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
16
22
|
|
|
17
23
|
/// Per-Store host state threaded through every host import callback.
|
|
18
24
|
///
|
|
19
|
-
///
|
|
20
|
-
/// fresh before each `#run`
|
|
21
|
-
///
|
|
22
|
-
/// captured bytes
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
pub wasi: Option<WasiP1Ctx>,
|
|
33
|
-
/// Clone of the MemoryOutputPipe wired to guest fd 1 (stdout). Retained
|
|
34
|
-
/// here so `take_stdout` can call `contents()` after execution without
|
|
35
|
-
/// having to dig into the WASI ctx internals.
|
|
36
|
-
pub stdout_pipe: Option<MemoryOutputPipe>,
|
|
37
|
-
/// Clone of the MemoryOutputPipe wired to guest fd 2 (stderr).
|
|
38
|
-
pub stderr_pipe: Option<MemoryOutputPipe>,
|
|
39
|
-
/// Ruby-side `Kobako::Registry`. When set, the `__kobako_rpc_call`
|
|
40
|
-
/// import calls `registry.dispatch(req_bytes)` and hands the returned
|
|
41
|
-
/// Response bytes back to the guest. `Opaque<Value>` is `Send + Sync`;
|
|
42
|
-
/// calling `get_inner` requires a `Ruby` handle, which we obtain on
|
|
43
|
-
/// every Ruby thread entry via `Ruby::get()`.
|
|
44
|
-
pub registry: Option<Opaque<Value>>,
|
|
25
|
+
/// All field access is mediated by methods on this type — the WASI ctx is
|
|
26
|
+
/// rebuilt fresh before each `#run` via [`HostState::install_wasi`], the
|
|
27
|
+
/// Ruby Server handle is set once via [`HostState::bind_server`], and
|
|
28
|
+
/// captured stdout/stderr bytes are read after the run via
|
|
29
|
+
/// [`HostState::stdout_bytes`] / [`HostState::stderr_bytes`]. The fields
|
|
30
|
+
/// are private so the mutation surface stays narrow.
|
|
31
|
+
pub(super) struct HostState {
|
|
32
|
+
wasi: Option<WasiP1Ctx>,
|
|
33
|
+
stdout_pipe: Option<MemoryOutputPipe>,
|
|
34
|
+
stderr_pipe: Option<MemoryOutputPipe>,
|
|
35
|
+
server: Option<Opaque<Value>>,
|
|
36
|
+
deadline: Option<Instant>,
|
|
37
|
+
limiter: KobakoLimiter,
|
|
45
38
|
}
|
|
46
39
|
|
|
40
|
+
impl HostState {
|
|
41
|
+
/// Build a fresh per-Store host state. `memory_limit` carries the
|
|
42
|
+
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
43
|
+
/// it is read from the wasmtime [`ResourceLimiter`] callback every
|
|
44
|
+
/// time the guest grows linear memory (SPEC.md B-01, E-20).
|
|
45
|
+
pub(super) fn new(memory_limit: Option<usize>) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
wasi: None,
|
|
48
|
+
stdout_pipe: None,
|
|
49
|
+
stderr_pipe: None,
|
|
50
|
+
server: None,
|
|
51
|
+
deadline: None,
|
|
52
|
+
limiter: KobakoLimiter::new(memory_limit),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Install a freshly-built WASI context plus the matching stdout/stderr
|
|
57
|
+
/// pipe clones. Called from [`crate::wasm::Instance::run`] at the top
|
|
58
|
+
/// of every guest invocation (SPEC.md B-03 / B-04).
|
|
59
|
+
pub(super) fn install_wasi(
|
|
60
|
+
&mut self,
|
|
61
|
+
wasi: WasiP1Ctx,
|
|
62
|
+
stdout: MemoryOutputPipe,
|
|
63
|
+
stderr: MemoryOutputPipe,
|
|
64
|
+
) {
|
|
65
|
+
self.wasi = Some(wasi);
|
|
66
|
+
self.stdout_pipe = Some(stdout);
|
|
67
|
+
self.stderr_pipe = Some(stderr);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Bind the Ruby-side `Kobako::RPC::Server` handle. From this point on,
|
|
71
|
+
/// every `__kobako_dispatch` host import invocation routes through it.
|
|
72
|
+
pub(super) fn bind_server(&mut self, server: Opaque<Value>) {
|
|
73
|
+
self.server = Some(server);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Snapshot the bytes captured on guest fd 1 during the most recent
|
|
77
|
+
/// run. Empty vec before any run.
|
|
78
|
+
pub(super) fn stdout_bytes(&self) -> Vec<u8> {
|
|
79
|
+
self.stdout_pipe
|
|
80
|
+
.as_ref()
|
|
81
|
+
.map(|p| p.contents().to_vec())
|
|
82
|
+
.unwrap_or_default()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Snapshot the bytes captured on guest fd 2 during the most recent
|
|
86
|
+
/// run. Empty vec before any run.
|
|
87
|
+
pub(super) fn stderr_bytes(&self) -> Vec<u8> {
|
|
88
|
+
self.stderr_pipe
|
|
89
|
+
.as_ref()
|
|
90
|
+
.map(|p| p.contents().to_vec())
|
|
91
|
+
.unwrap_or_default()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Return the bound Server handle. `Opaque<Value>` is `Copy`, so the
|
|
95
|
+
/// handle is returned by value rather than by reference. None means no
|
|
96
|
+
/// Server has been bound yet via [`HostState::bind_server`].
|
|
97
|
+
pub(super) fn server(&self) -> Option<Opaque<Value>> {
|
|
98
|
+
self.server
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Mutable handle to the live WASI context. Panics if no context has
|
|
102
|
+
/// been installed yet — every call site is downstream of
|
|
103
|
+
/// [`HostState::install_wasi`] running at the top of `Instance::run`,
|
|
104
|
+
/// so reaching this branch with `None` signals a host-side wiring bug.
|
|
105
|
+
pub(super) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
|
|
106
|
+
self.wasi
|
|
107
|
+
.as_mut()
|
|
108
|
+
.expect("WASI context not initialised — call Instance#run before any WASI use")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Replace the per-run wall-clock deadline. `Some(at)` makes the
|
|
112
|
+
/// epoch-deadline callback trap once `Instant::now() >= at`; `None`
|
|
113
|
+
/// disables the cap. Called at the top of every `#run` (SPEC.md B-01).
|
|
114
|
+
pub(super) fn set_deadline(&mut self, deadline: Option<Instant>) {
|
|
115
|
+
self.deadline = deadline;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
119
|
+
/// callback installed by [`crate::wasm::Instance::from_path`].
|
|
120
|
+
pub(super) fn deadline(&self) -> Option<Instant> {
|
|
121
|
+
self.deadline
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Mutable handle to the embedded [`KobakoLimiter`]. Shared by the
|
|
125
|
+
/// wasmtime [`ResourceLimiter`] callback (set once at Store build
|
|
126
|
+
/// time) and by [`crate::wasm::Instance`] for arming / disarming the
|
|
127
|
+
/// memory cap around each guest run. Same shape as
|
|
128
|
+
/// [`HostState::wasi_mut`] — callers operate on the inner type
|
|
129
|
+
/// directly instead of going through a per-action passthrough.
|
|
130
|
+
pub(super) fn limiter_mut(&mut self) -> &mut KobakoLimiter {
|
|
131
|
+
&mut self.limiter
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Resource limiter that enforces the `memory_limit` cap from SPEC.md
|
|
136
|
+
/// B-01 / E-20 on every guest `memory.grow`.
|
|
137
|
+
///
|
|
138
|
+
/// `max_memory` is the byte cap (`None` disables the cap). `cap_active`
|
|
139
|
+
/// gates whether the cap is enforced — wasmtime's `ResourceLimiter`
|
|
140
|
+
/// fires for both the module's declared initial allocation and every
|
|
141
|
+
/// subsequent `memory.grow`, but SPEC.md E-20 scopes the trap to
|
|
142
|
+
/// `memory.grow` specifically. [`KobakoLimiter::activate`] /
|
|
143
|
+
/// [`KobakoLimiter::deactivate`] flip the flag for the lifetime of an
|
|
144
|
+
/// `Instance::run` call. When `cap_active` is `false`, the limiter
|
|
145
|
+
/// always allows growth.
|
|
146
|
+
///
|
|
147
|
+
/// When `memory.grow` would push linear memory past the cap, the
|
|
148
|
+
/// limiter returns [`MemoryLimitTrap`] from `memory_growing`; wasmtime
|
|
149
|
+
/// turns that into the trap surfaced to the host as `__kobako_run`
|
|
150
|
+
/// failure.
|
|
151
|
+
#[derive(Debug, Clone, Copy)]
|
|
152
|
+
pub(super) struct KobakoLimiter {
|
|
153
|
+
max_memory: Option<usize>,
|
|
154
|
+
cap_active: bool,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
impl KobakoLimiter {
|
|
158
|
+
fn new(max_memory: Option<usize>) -> Self {
|
|
159
|
+
Self {
|
|
160
|
+
max_memory,
|
|
161
|
+
cap_active: false,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Arm the cap so subsequent `memory.grow` calls are checked
|
|
166
|
+
/// against `memory_limit`. The cap is dormant by default — the
|
|
167
|
+
/// module's declared initial memory is allocated during
|
|
168
|
+
/// `Linker::instantiate` and SPEC.md E-20 scopes the trap to
|
|
169
|
+
/// `memory.grow` (not the instantiation-time initial allocation).
|
|
170
|
+
/// [`crate::wasm::Instance::run`] calls this right before
|
|
171
|
+
/// `__kobako_run`.
|
|
172
|
+
pub(super) fn activate(&mut self) {
|
|
173
|
+
self.cap_active = true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Disarm the cap so post-run host bookkeeping (e.g. fetching the
|
|
177
|
+
/// OUTCOME_BUFFER, which can grow guest memory transiently) is
|
|
178
|
+
/// not attributed to the user script. Paired with
|
|
179
|
+
/// [`KobakoLimiter::activate`].
|
|
180
|
+
pub(super) fn deactivate(&mut self) {
|
|
181
|
+
self.cap_active = false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
impl ResourceLimiter for KobakoLimiter {
|
|
186
|
+
fn memory_growing(
|
|
187
|
+
&mut self,
|
|
188
|
+
_current: usize,
|
|
189
|
+
desired: usize,
|
|
190
|
+
_maximum: Option<usize>,
|
|
191
|
+
) -> wasmtime::Result<bool> {
|
|
192
|
+
if !self.cap_active {
|
|
193
|
+
return Ok(true);
|
|
194
|
+
}
|
|
195
|
+
if let Some(limit) = self.max_memory {
|
|
196
|
+
if desired > limit {
|
|
197
|
+
return Err(wasmtime::Error::new(MemoryLimitTrap { desired, limit }));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
Ok(true)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn table_growing(
|
|
204
|
+
&mut self,
|
|
205
|
+
_current: usize,
|
|
206
|
+
_desired: usize,
|
|
207
|
+
_maximum: Option<usize>,
|
|
208
|
+
) -> wasmtime::Result<bool> {
|
|
209
|
+
Ok(true)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Marker error returned from [`KobakoLimiter::memory_growing`] on
|
|
214
|
+
/// SPEC.md E-20. Downcast from the wasmtime trap error to surface as
|
|
215
|
+
/// `Kobako::Wasm::MemoryLimitError` on the Ruby side. Callers use the
|
|
216
|
+
/// `Display` impl below — no field is read directly — so the inner
|
|
217
|
+
/// state stays private.
|
|
218
|
+
#[derive(Debug)]
|
|
219
|
+
pub(crate) struct MemoryLimitTrap {
|
|
220
|
+
desired: usize,
|
|
221
|
+
limit: usize,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
impl std::fmt::Display for MemoryLimitTrap {
|
|
225
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
226
|
+
write!(
|
|
227
|
+
f,
|
|
228
|
+
"guest memory.grow would exceed memory_limit: desired={} bytes, limit={} bytes",
|
|
229
|
+
self.desired, self.limit
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
impl std::error::Error for MemoryLimitTrap {}
|
|
235
|
+
|
|
236
|
+
/// Marker error returned from the epoch-deadline callback on SPEC.md
|
|
237
|
+
/// E-19. Downcast from the wasmtime trap error to surface as
|
|
238
|
+
/// `Kobako::Wasm::TimeoutError` on the Ruby side.
|
|
239
|
+
#[derive(Debug)]
|
|
240
|
+
pub(crate) struct TimeoutTrap;
|
|
241
|
+
|
|
242
|
+
impl std::fmt::Display for TimeoutTrap {
|
|
243
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
244
|
+
write!(f, "guest exceeded the configured wall-clock timeout")
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
impl std::error::Error for TimeoutTrap {}
|
|
249
|
+
|
|
47
250
|
/// Interior-mutability wrapper around `wasmtime::Store<HostState>`.
|
|
48
251
|
///
|
|
49
252
|
/// Magnus requires `Send + Sync` for wrapped types. `wasmtime::Store` is not
|
|
50
253
|
/// `Sync`, so we wrap it in a `RefCell`. `RefCell` alone is sufficient
|
|
51
254
|
/// because magnus enforces single-threaded GVL access from Ruby; `Send` and
|
|
52
255
|
/// `Sync` are asserted via the unsafe impls below.
|
|
53
|
-
pub(
|
|
256
|
+
pub(super) struct StoreCell {
|
|
257
|
+
inner: RefCell<WtStore<HostState>>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
impl StoreCell {
|
|
261
|
+
/// Wrap a freshly-built `wasmtime::Store<HostState>` so it can be owned
|
|
262
|
+
/// by the magnus-wrapped `Instance`.
|
|
263
|
+
pub(super) fn new(store: WtStore<HostState>) -> Self {
|
|
264
|
+
Self {
|
|
265
|
+
inner: RefCell::new(store),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Immutable borrow of the wrapped Store. Panics if a `borrow_mut()`
|
|
270
|
+
/// is currently live — matches `RefCell::borrow` semantics.
|
|
271
|
+
pub(super) fn borrow(&self) -> Ref<'_, WtStore<HostState>> {
|
|
272
|
+
self.inner.borrow()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Mutable borrow of the wrapped Store. Panics if any other borrow is
|
|
276
|
+
/// currently live — matches `RefCell::borrow_mut` semantics.
|
|
277
|
+
pub(super) fn borrow_mut(&self) -> RefMut<'_, WtStore<HostState>> {
|
|
278
|
+
self.inner.borrow_mut()
|
|
279
|
+
}
|
|
280
|
+
}
|
|
54
281
|
|
|
55
282
|
// SAFETY: Ruby's GVL serialises access to magnus-wrapped objects on a single
|
|
56
283
|
// OS thread at a time. `wasmtime::Store` is `Send` (verified upstream); the
|