kobako 0.27.0 → 0.28.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +71 -0
- data/Cargo.lock +117 -140
- data/README.md +100 -125
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +31 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +4 -3
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +28 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +2 -2
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -15
|
@@ -1,36 +1,31 @@
|
|
|
1
|
-
//! Per-invocation host state — the
|
|
2
|
-
//!
|
|
3
|
-
//! for the lifetime of one `Driver` invoke call).
|
|
1
|
+
//! Per-invocation host state — one `Invocation` per OS thread for the
|
|
2
|
+
//! lifetime of one `Driver` invoke call.
|
|
4
3
|
//!
|
|
5
4
|
//! Owned as the data of each per-invocation `wasmtime::Store`
|
|
6
5
|
//! and threaded through every host import —
|
|
7
|
-
//! the `
|
|
6
|
+
//! the `__kobako_upcall` dispatcher reads the bound dispatch handler,
|
|
8
7
|
//! while `Driver::invoke` installs the invocation's WASI
|
|
9
|
-
//! context + pipes (via `
|
|
8
|
+
//! context + pipes (via `io::install_wasi`) before the guest
|
|
10
9
|
//! export call.
|
|
11
10
|
//!
|
|
12
|
-
//! The slot also carries the per-invocation wall-clock deadline
|
|
13
|
-
//!
|
|
14
|
-
//!
|
|
15
|
-
//!
|
|
16
|
-
//! callbacks installed in `Driver::new_store`. The
|
|
17
|
-
//! memory cap measures only the `memory.grow` delta past the linear-
|
|
18
|
-
//! memory size captured at invocation entry — the image's initial
|
|
19
|
-
//! allocation is outside the budget.
|
|
20
|
-
//!
|
|
21
|
-
//! [SPEC.md Single-Invocation Slot]: ../../../SPEC.md
|
|
11
|
+
//! The slot also carries the per-invocation wall-clock deadline and the
|
|
12
|
+
//! memory cap `MemoryLimiter`, both read from the wasmtime
|
|
13
|
+
//! `epoch_deadline_callback` / `ResourceLimiter` callbacks installed in
|
|
14
|
+
//! `Driver::new_store`.
|
|
22
15
|
|
|
23
16
|
use std::sync::Arc;
|
|
24
17
|
use std::time::{Duration, Instant};
|
|
25
18
|
|
|
26
|
-
use wasmtime::ResourceLimiter;
|
|
27
19
|
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
28
20
|
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
29
21
|
|
|
30
22
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
31
23
|
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
use crate::exports::Exports;
|
|
25
|
+
use crate::limiter::MemoryLimiter;
|
|
26
|
+
|
|
27
|
+
/// Per-invocation host state, owned by the invocation's own Store.
|
|
28
|
+
/// Threaded through every host import callback.
|
|
34
29
|
///
|
|
35
30
|
/// All field access is mediated by methods on this type — the WASI ctx
|
|
36
31
|
/// is rebuilt fresh before each invocation via
|
|
@@ -48,13 +43,11 @@ pub(crate) struct Invocation {
|
|
|
48
43
|
limiter: MemoryLimiter,
|
|
49
44
|
wall_entry: Option<Instant>,
|
|
50
45
|
wall_time: Duration,
|
|
46
|
+
reentry_trap: Option<wasmtime::Error>,
|
|
47
|
+
exports: Option<Exports>,
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
impl Invocation {
|
|
54
|
-
/// Build a fresh per-Store host state. `memory_limit` carries the
|
|
55
|
-
/// `Sandbox#memory_limit` cap in bytes (or `None` to disable the cap);
|
|
56
|
-
/// it is read from the wasmtime `ResourceLimiter` callback every
|
|
57
|
-
/// time the guest grows linear memory.
|
|
58
51
|
pub(crate) fn new(memory_limit: Option<usize>) -> Self {
|
|
59
52
|
Self {
|
|
60
53
|
wasi: None,
|
|
@@ -65,13 +58,37 @@ impl Invocation {
|
|
|
65
58
|
limiter: MemoryLimiter::new(memory_limit),
|
|
66
59
|
wall_entry: None,
|
|
67
60
|
wall_time: Duration::ZERO,
|
|
61
|
+
reentry_trap: None,
|
|
62
|
+
exports: None,
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
pub(crate) fn set_exports(&mut self, exports: Exports) {
|
|
67
|
+
self.exports = Some(exports);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// `None` until the instance exists, so a host import reached before
|
|
71
|
+
/// then finds no guest to call back into.
|
|
72
|
+
pub(crate) fn exports(&self) -> Option<Exports> {
|
|
73
|
+
self.exports.clone()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The first trap wins: the upcall import ends the invocation with it,
|
|
77
|
+
/// so the trap keeps its own kind.
|
|
78
|
+
pub(crate) fn record_reentry_trap(&mut self, trap: wasmtime::Error) {
|
|
79
|
+
self.reentry_trap.get_or_insert(trap);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Once a callback into the guest has trapped, the guest is past
|
|
83
|
+
/// resuming.
|
|
84
|
+
pub(crate) fn reentry_trapped(&self) -> bool {
|
|
85
|
+
self.reentry_trap.is_some()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pub(crate) fn take_reentry_trap(&mut self) -> Option<wasmtime::Error> {
|
|
89
|
+
self.reentry_trap.take()
|
|
90
|
+
}
|
|
91
|
+
|
|
75
92
|
pub(crate) fn install_wasi(
|
|
76
93
|
&mut self,
|
|
77
94
|
wasi: WasiP1Ctx,
|
|
@@ -83,15 +100,10 @@ impl Invocation {
|
|
|
83
100
|
self.stderr_pipe = Some(stderr);
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
/// Bind the dispatch handler for this invocation. From this point on,
|
|
87
|
-
/// every `__kobako_dispatch` host import invocation hands the handler
|
|
88
|
-
/// the Call bytes and expects encoded Reply bytes back.
|
|
89
103
|
pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
|
|
90
104
|
self.on_dispatch = Some(handler);
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
/// Snapshot the bytes captured on guest fd 1 during the most recent
|
|
94
|
-
/// run. Empty vec before any run.
|
|
95
107
|
pub(crate) fn stdout_bytes(&self) -> Vec<u8> {
|
|
96
108
|
self.stdout_pipe
|
|
97
109
|
.as_ref()
|
|
@@ -99,8 +111,6 @@ impl Invocation {
|
|
|
99
111
|
.unwrap_or_default()
|
|
100
112
|
}
|
|
101
113
|
|
|
102
|
-
/// Snapshot the bytes captured on guest fd 2 during the most recent
|
|
103
|
-
/// run. Empty vec before any run.
|
|
104
114
|
pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
|
|
105
115
|
self.stderr_pipe
|
|
106
116
|
.as_ref()
|
|
@@ -108,387 +118,60 @@ impl Invocation {
|
|
|
108
118
|
.unwrap_or_default()
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
///
|
|
112
|
-
///
|
|
113
|
-
/// `Caller` so the dispatcher can re-borrow it to write the response.
|
|
114
|
-
/// None means no handler has been bound yet via
|
|
115
|
-
/// `Invocation::bind_on_dispatch`.
|
|
121
|
+
/// A clone, so the borrow on the `Caller` is released and the
|
|
122
|
+
/// dispatcher can re-borrow it to write the Reply.
|
|
116
123
|
pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
117
124
|
self.on_dispatch.clone()
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
/// Mutable handle to the live WASI context. Panics if no context has
|
|
121
|
-
/// been installed yet — every call site is downstream of
|
|
122
|
-
/// `Invocation::install_wasi` running at the top of every `Driver`
|
|
123
|
-
/// invoke, so reaching this branch with `None` signals a host-side
|
|
124
|
-
/// wiring bug.
|
|
125
127
|
pub(crate) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
|
|
126
|
-
self.wasi
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
self.wasi
|
|
129
|
+
.as_mut()
|
|
130
|
+
.expect("WASI context not initialised — the driver must install it before any WASI use")
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
/// Replace the per-run wall-clock deadline. `Some(at)` makes the
|
|
132
|
-
/// epoch-deadline callback trap once `Instant::now() >= at`; `None`
|
|
133
|
-
/// disables the cap. Called from `Driver::prime_caps` at the top of
|
|
134
|
-
/// every invocation (`#eval` and `#run`).
|
|
135
133
|
pub(crate) fn set_deadline(&mut self, deadline: Option<Instant>) {
|
|
136
134
|
self.deadline = deadline;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
/// Return the current per-run deadline. Read from the epoch-deadline
|
|
140
|
-
/// callback installed by `Driver::new_store`.
|
|
141
137
|
pub(crate) fn deadline(&self) -> Option<Instant> {
|
|
142
138
|
self.deadline
|
|
143
139
|
}
|
|
144
140
|
|
|
145
|
-
/// Mutable handle to the embedded `MemoryLimiter`. Required by
|
|
146
|
-
/// the wasmtime `ResourceLimiter` callback wiring in
|
|
147
|
-
/// `Driver::new_store`
|
|
148
|
-
/// (`store.limiter(|state| state.limiter_mut())`); kept private to
|
|
149
|
-
/// the wasm submodule so the only public surface for arming the
|
|
150
|
-
/// cap goes through `Invocation::arm_memory_cap` /
|
|
151
|
-
/// `Invocation::disarm_memory_cap`.
|
|
152
141
|
pub(crate) fn limiter_mut(&mut self) -> &mut MemoryLimiter {
|
|
153
142
|
&mut self.limiter
|
|
154
143
|
}
|
|
155
144
|
|
|
156
|
-
///
|
|
157
|
-
///
|
|
158
|
-
///
|
|
159
|
-
/// the cap, so the mruby image's initial allocation and the
|
|
160
|
-
/// high-water mark left by prior invocations do not consume the
|
|
161
|
-
/// budget. Paired with `Invocation::disarm_memory_cap` around the
|
|
162
|
-
/// call to the corresponding `__kobako_*` export so post-run host
|
|
163
|
-
/// bookkeeping (e.g. fetching the OUTCOME_BUFFER) is not
|
|
164
|
-
/// attributed to the user script.
|
|
145
|
+
/// Only the `memory.grow` delta past `baseline` is charged, so the
|
|
146
|
+
/// image's initial allocation and the high-water mark of earlier
|
|
147
|
+
/// invocations do not consume the budget.
|
|
165
148
|
pub(crate) fn arm_memory_cap(&mut self, baseline: usize) {
|
|
166
149
|
self.limiter.activate(baseline);
|
|
167
150
|
}
|
|
168
151
|
|
|
169
|
-
///
|
|
170
|
-
///
|
|
152
|
+
/// Disarmed once the export returns, so post-run host bookkeeping such
|
|
153
|
+
/// as reading the answer back is not charged to the script.
|
|
171
154
|
pub(crate) fn disarm_memory_cap(&mut self) {
|
|
172
155
|
self.limiter.deactivate();
|
|
173
156
|
}
|
|
174
157
|
|
|
175
|
-
///
|
|
176
|
-
///
|
|
177
|
-
/// invocation immediately before the guest export call so the
|
|
178
|
-
/// bracket matches the `timeout` deadline accounting and
|
|
179
|
-
/// excludes post-run host bookkeeping such as `OUTCOME_BUFFER`
|
|
180
|
-
/// decoding.
|
|
158
|
+
/// Stamped right before the guest export call, so the bracket matches
|
|
159
|
+
/// the `timeout` accounting and excludes post-run host bookkeeping.
|
|
181
160
|
pub(crate) fn start_wall_clock(&mut self) {
|
|
182
161
|
self.wall_entry = Some(Instant::now());
|
|
183
162
|
}
|
|
184
163
|
|
|
185
|
-
/// Close the `wall_time` measurement
|
|
186
|
-
/// started by `Invocation::start_wall_clock`. Idempotent — a
|
|
187
|
-
/// stop with no matching start (e.g. if the guest export call
|
|
188
|
-
/// never executed because of a host-side allocation failure)
|
|
189
|
-
/// leaves the previously-recorded value untouched.
|
|
190
164
|
pub(crate) fn stop_wall_clock(&mut self) {
|
|
191
165
|
if let Some(entry) = self.wall_entry.take() {
|
|
192
166
|
self.wall_time = entry.elapsed();
|
|
193
167
|
}
|
|
194
168
|
}
|
|
195
169
|
|
|
196
|
-
/// Return the wall-clock duration the most recent invocation
|
|
197
|
-
/// spent inside the guest export call.
|
|
198
|
-
/// Zero before the first invocation.
|
|
199
170
|
pub(crate) fn wall_time(&self) -> Duration {
|
|
200
171
|
self.wall_time
|
|
201
172
|
}
|
|
202
173
|
|
|
203
|
-
/// Return the `memory_peak` — the high-
|
|
204
|
-
/// water mark of the per-invocation `memory.grow` delta past the
|
|
205
|
-
/// linear-memory size captured at invocation entry. Zero before
|
|
206
|
-
/// the first invocation.
|
|
207
174
|
pub(crate) fn memory_peak(&self) -> usize {
|
|
208
175
|
self.limiter.peak()
|
|
209
176
|
}
|
|
210
177
|
}
|
|
211
|
-
|
|
212
|
-
/// Resource limiter that enforces the per-invocation `memory_limit`
|
|
213
|
-
/// cap.
|
|
214
|
-
///
|
|
215
|
-
/// `max_memory` is the byte cap on per-invocation growth (`None` disables
|
|
216
|
-
/// the cap). `baseline` is the linear-memory size captured at invocation
|
|
217
|
-
/// entry by `MemoryLimiter::activate`; the limiter charges only the
|
|
218
|
-
/// `memory.grow` delta past `baseline` against `max_memory`, so the
|
|
219
|
-
/// mruby image's initial allocation and any high-water mark left by
|
|
220
|
-
/// prior invocations on the same Sandbox do not consume the budget.
|
|
221
|
-
/// `cap_active` gates whether the cap is enforced — wasmtime's
|
|
222
|
-
/// `ResourceLimiter` also fires for the module's declared initial
|
|
223
|
-
/// allocation at instantiation time, but the cap stays dormant until
|
|
224
|
-
/// `MemoryLimiter::activate` flips the flag for one `Driver` invoke
|
|
225
|
-
/// call. When `cap_active` is `false`, the limiter always allows
|
|
226
|
-
/// growth.
|
|
227
|
-
///
|
|
228
|
-
/// When `memory.grow` would push the per-invocation delta past
|
|
229
|
-
/// `max_memory`, the limiter returns `MemoryLimitTrap` from
|
|
230
|
-
/// `memory_growing`; wasmtime turns that into the trap surfaced to the
|
|
231
|
-
/// host as a guest invocation failure.
|
|
232
|
-
#[derive(Debug, Clone, Copy)]
|
|
233
|
-
pub(crate) struct MemoryLimiter {
|
|
234
|
-
max_memory: Option<usize>,
|
|
235
|
-
baseline: usize,
|
|
236
|
-
cap_active: bool,
|
|
237
|
-
peak: usize,
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
impl MemoryLimiter {
|
|
241
|
-
fn new(max_memory: Option<usize>) -> Self {
|
|
242
|
-
Self {
|
|
243
|
-
max_memory,
|
|
244
|
-
baseline: 0,
|
|
245
|
-
cap_active: false,
|
|
246
|
-
peak: 0,
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/// Arm the cap so subsequent `memory.grow` calls are charged
|
|
251
|
-
/// against `max_memory` starting from `baseline` bytes. Called via
|
|
252
|
-
/// `Invocation::arm_memory_cap` at the top of every invocation;
|
|
253
|
-
/// the cap is dormant by default — the module's declared initial
|
|
254
|
-
/// memory is allocated during `Linker::instantiate` and the
|
|
255
|
-
/// per-invocation budget excludes anything that existed before
|
|
256
|
-
/// arming. Also clears the
|
|
257
|
-
/// per-invocation `MemoryLimiter::peak` high-water so the
|
|
258
|
-
/// `memory_peak` accounting restarts from
|
|
259
|
-
/// zero for the new invocation.
|
|
260
|
-
fn activate(&mut self, baseline: usize) {
|
|
261
|
-
self.baseline = baseline;
|
|
262
|
-
self.cap_active = true;
|
|
263
|
-
self.peak = 0;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/// Disarm the cap so post-run host bookkeeping (e.g. fetching the
|
|
267
|
-
/// OUTCOME_BUFFER, which can grow guest memory transiently) is
|
|
268
|
-
/// not attributed to the user script. Paired with
|
|
269
|
-
/// `MemoryLimiter::activate`.
|
|
270
|
-
fn deactivate(&mut self) {
|
|
271
|
-
self.cap_active = false;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/// Return the high-water mark of the per-invocation
|
|
275
|
-
/// `memory.grow` delta past `baseline` observed since the last
|
|
276
|
-
/// `MemoryLimiter::activate`. Read after the guest export
|
|
277
|
-
/// returns to populate `Kobako::Usage#memory_peak`.
|
|
278
|
-
/// Pinned to the last accepted grow —
|
|
279
|
-
/// rejected `desired` values that trip the memory
|
|
280
|
-
/// cap never update the peak, so the reported value never exceeds
|
|
281
|
-
/// `memory_limit`.
|
|
282
|
-
pub(crate) fn peak(&self) -> usize {
|
|
283
|
-
self.peak
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
impl ResourceLimiter for MemoryLimiter {
|
|
288
|
-
fn memory_growing(
|
|
289
|
-
&mut self,
|
|
290
|
-
_current: usize,
|
|
291
|
-
desired: usize,
|
|
292
|
-
_maximum: Option<usize>,
|
|
293
|
-
) -> wasmtime::Result<bool> {
|
|
294
|
-
if !self.cap_active {
|
|
295
|
-
return Ok(true);
|
|
296
|
-
}
|
|
297
|
-
let delta = desired.saturating_sub(self.baseline);
|
|
298
|
-
if let Some(limit) = self.max_memory {
|
|
299
|
-
if delta > limit {
|
|
300
|
-
return Err(wasmtime::Error::new(MemoryLimitTrap { desired, limit }));
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
if delta > self.peak {
|
|
304
|
-
self.peak = delta;
|
|
305
|
-
}
|
|
306
|
-
Ok(true)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
fn table_growing(
|
|
310
|
-
&mut self,
|
|
311
|
-
_current: usize,
|
|
312
|
-
_desired: usize,
|
|
313
|
-
_maximum: Option<usize>,
|
|
314
|
-
) -> wasmtime::Result<bool> {
|
|
315
|
-
Ok(true)
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/// Marker error returned from `MemoryLimiter::memory_growing` when the
|
|
320
|
-
/// per-invocation memory cap is exceeded. Downcast from the wasmtime
|
|
321
|
-
/// trap error to classify the failure as a memory-limit `Trap`. Callers
|
|
322
|
-
/// use the `Display` impl below — no field is read directly — so the
|
|
323
|
-
/// inner state stays private.
|
|
324
|
-
#[derive(Debug)]
|
|
325
|
-
pub(crate) struct MemoryLimitTrap {
|
|
326
|
-
desired: usize,
|
|
327
|
-
limit: usize,
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
impl MemoryLimitTrap {
|
|
331
|
-
/// Construct a trap with the given `desired` / `limit` pair. Used
|
|
332
|
-
/// internally by `MemoryLimiter::memory_growing` in production and
|
|
333
|
-
/// by the sibling-module `classify_trap` unit tests to materialise
|
|
334
|
-
/// a representative error for downcast routing.
|
|
335
|
-
#[cfg(test)]
|
|
336
|
-
pub(crate) fn new(desired: usize, limit: usize) -> Self {
|
|
337
|
-
Self { desired, limit }
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
impl std::fmt::Display for MemoryLimitTrap {
|
|
342
|
-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
343
|
-
write!(
|
|
344
|
-
f,
|
|
345
|
-
"memory usage exceeded memory_limit: \
|
|
346
|
-
requested={} bytes, limit={} bytes",
|
|
347
|
-
self.desired, self.limit
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
impl std::error::Error for MemoryLimitTrap {}
|
|
353
|
-
|
|
354
|
-
/// Marker error returned from the epoch-deadline callback when the
|
|
355
|
-
/// wall-clock deadline is exceeded. Downcast from the wasmtime trap
|
|
356
|
-
/// error to classify the failure as a timeout `Trap`.
|
|
357
|
-
#[derive(Debug)]
|
|
358
|
-
pub(crate) struct TimeoutTrap;
|
|
359
|
-
|
|
360
|
-
impl std::fmt::Display for TimeoutTrap {
|
|
361
|
-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
362
|
-
write!(f, "wall-clock deadline exceeded")
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
impl std::error::Error for TimeoutTrap {}
|
|
367
|
-
|
|
368
|
-
#[cfg(test)]
|
|
369
|
-
mod tests {
|
|
370
|
-
//! Unit tests for `MemoryLimiter` — the per-invocation memory
|
|
371
|
-
//! delta cap. The Ruby-facing E2E suite exercises the full path
|
|
372
|
-
//! through wasmtime; these tests pin the pure delta arithmetic so
|
|
373
|
-
//! a regression that breaks the baseline accounting (e.g. dropping
|
|
374
|
-
//! the `baseline` subtraction, or letting `activate` carry stale
|
|
375
|
-
//! state across invocations) is caught without spinning up a
|
|
376
|
-
//! Store.
|
|
377
|
-
use super::{MemoryLimitTrap, MemoryLimiter};
|
|
378
|
-
use wasmtime::ResourceLimiter;
|
|
379
|
-
|
|
380
|
-
fn assert_growing(limiter: &mut MemoryLimiter, desired: usize) {
|
|
381
|
-
assert!(
|
|
382
|
-
limiter.memory_growing(0, desired, None).unwrap(),
|
|
383
|
-
"expected memory_growing({desired}) to allow growth"
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
fn assert_trapping(limiter: &mut MemoryLimiter, desired: usize) {
|
|
388
|
-
let err = limiter
|
|
389
|
-
.memory_growing(0, desired, None)
|
|
390
|
-
.expect_err("expected memory_growing to trap");
|
|
391
|
-
assert!(
|
|
392
|
-
err.downcast_ref::<MemoryLimitTrap>().is_some(),
|
|
393
|
-
"expected MemoryLimitTrap, got {err:?}"
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// @behavior S-113
|
|
398
|
-
#[test]
|
|
399
|
-
fn dormant_limiter_allows_any_growth() {
|
|
400
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
401
|
-
// Without `activate`, the cap is dormant — the module's
|
|
402
|
-
// declared initial allocation must pass through unconditionally.
|
|
403
|
-
assert_growing(&mut limiter, 100 << 20);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// @behavior S-114
|
|
407
|
-
#[test]
|
|
408
|
-
fn delta_below_cap_passes_after_activate() {
|
|
409
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
410
|
-
limiter.activate(2 << 20);
|
|
411
|
-
// baseline=2 MiB, desired=2.5 MiB → delta=0.5 MiB ≤ 1 MiB cap.
|
|
412
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// @behavior S-121
|
|
416
|
-
#[test]
|
|
417
|
-
fn delta_past_cap_traps_with_memory_limit_trap() {
|
|
418
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
419
|
-
limiter.activate(2 << 20);
|
|
420
|
-
// baseline=2 MiB, desired=4 MiB → delta=2 MiB > 1 MiB cap.
|
|
421
|
-
assert_trapping(&mut limiter, 4 << 20);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// @behavior S-008
|
|
425
|
-
#[test]
|
|
426
|
-
fn activate_resets_baseline_on_each_invocation() {
|
|
427
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
428
|
-
limiter.activate(2 << 20);
|
|
429
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 20));
|
|
430
|
-
// Second invocation: linear memory has grown to 3 MiB. Re-arming
|
|
431
|
-
// must re-anchor the baseline so the next 1 MiB of growth fits
|
|
432
|
-
// the per-invocation budget rather than being charged against
|
|
433
|
-
// the prior invocation's residue.
|
|
434
|
-
limiter.activate(3 << 20);
|
|
435
|
-
assert_growing(&mut limiter, (3 << 20) + (1 << 20));
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// @behavior S-009
|
|
439
|
-
#[test]
|
|
440
|
-
fn disabled_cap_ignores_delta_size() {
|
|
441
|
-
let mut limiter = MemoryLimiter::new(None);
|
|
442
|
-
limiter.activate(0);
|
|
443
|
-
assert_growing(&mut limiter, 100 << 20);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// @behavior S-116
|
|
447
|
-
#[test]
|
|
448
|
-
fn peak_starts_at_zero_before_any_grow() {
|
|
449
|
-
let limiter = MemoryLimiter::new(Some(1 << 20));
|
|
450
|
-
assert_eq!(limiter.peak(), 0);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// @behavior S-115
|
|
454
|
-
#[test]
|
|
455
|
-
fn peak_tracks_high_water_of_delta_past_baseline() {
|
|
456
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
457
|
-
limiter.activate(2 << 20);
|
|
458
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 18)); // delta=256 KiB
|
|
459
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB (new peak)
|
|
460
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 17)); // delta=128 KiB (below peak)
|
|
461
|
-
assert_eq!(limiter.peak(), 1 << 19);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// @behavior S-062
|
|
465
|
-
#[test]
|
|
466
|
-
fn trap_does_not_update_peak() {
|
|
467
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
468
|
-
limiter.activate(2 << 20);
|
|
469
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB
|
|
470
|
-
assert_trapping(&mut limiter, (2 << 20) + (2 << 20)); // would be 2 MiB > 1 MiB cap
|
|
471
|
-
// Peak reflects the last accepted grow, not the rejected desired.
|
|
472
|
-
assert_eq!(limiter.peak(), 1 << 19);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// @behavior S-117
|
|
476
|
-
#[test]
|
|
477
|
-
fn activate_resets_peak_for_new_invocation() {
|
|
478
|
-
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
479
|
-
limiter.activate(2 << 20);
|
|
480
|
-
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
481
|
-
assert_eq!(limiter.peak(), 1 << 19);
|
|
482
|
-
limiter.activate(3 << 20);
|
|
483
|
-
assert_eq!(limiter.peak(), 0);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// @behavior S-118
|
|
487
|
-
#[test]
|
|
488
|
-
fn disabled_cap_still_tracks_peak() {
|
|
489
|
-
let mut limiter = MemoryLimiter::new(None);
|
|
490
|
-
limiter.activate(1 << 20);
|
|
491
|
-
assert_growing(&mut limiter, (1 << 20) + (4 << 20));
|
|
492
|
-
assert_eq!(limiter.peak(), 4 << 20);
|
|
493
|
-
}
|
|
494
|
-
}
|