kobako 0.12.1 → 0.12.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +11 -0
- data/Cargo.lock +15 -2
- data/Cargo.toml +6 -2
- data/README.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +8 -0
- data/crates/kobako-runtime/Cargo.toml +23 -0
- data/crates/kobako-runtime/README.md +34 -0
- data/crates/kobako-runtime/src/dispatch.rs +22 -0
- data/crates/kobako-runtime/src/error.rs +64 -0
- data/crates/kobako-runtime/src/lib.rs +16 -0
- data/crates/kobako-runtime/src/runtime.rs +50 -0
- data/crates/kobako-runtime/src/snapshot.rs +46 -0
- data/crates/kobako-runtime/src/yielder.rs +22 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +8 -0
- data/crates/kobako-wasmtime/Cargo.toml +62 -0
- data/crates/kobako-wasmtime/README.md +32 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/ambient.rs +3 -3
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/cache.rs +30 -41
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/capture.rs +2 -2
- data/crates/kobako-wasmtime/src/config.rs +25 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +110 -0
- data/crates/kobako-wasmtime/src/driver.rs +285 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/exports.rs +5 -6
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/frames.rs +70 -82
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/guest_mem.rs +38 -15
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/instance_pre.rs +13 -21
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/invocation.rs +54 -49
- data/crates/kobako-wasmtime/src/lib.rs +47 -0
- data/{ext/kobako/src/runtime → crates/kobako-wasmtime/src}/trap.rs +29 -35
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +9 -32
- data/ext/kobako/src/runtime/bridge.rs +150 -0
- data/ext/kobako/src/runtime/errors.rs +45 -13
- data/ext/kobako/src/runtime.rs +156 -406
- data/ext/kobako/src/snapshot.rs +27 -62
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/namespaces.rb +4 -0
- data/lib/kobako/catalog/snippets.rb +4 -0
- data/lib/kobako/codec/encoder.rb +5 -1
- data/lib/kobako/codec/factory.rb +41 -13
- data/lib/kobako/codec/handle_walk.rb +4 -0
- data/lib/kobako/errors.rb +18 -16
- data/lib/kobako/sandbox.rb +20 -18
- data/lib/kobako/sandbox_options.rb +25 -9
- data/lib/kobako/snapshot.rb +7 -13
- data/lib/kobako/transport/dispatcher.rb +2 -2
- data/lib/kobako/transport/response.rb +14 -14
- data/lib/kobako/transport/run.rb +2 -6
- data/lib/kobako/transport/yield.rb +1 -1
- data/lib/kobako/transport/yielder.rb +2 -2
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +48 -3
- data/sig/kobako/codec/factory.rbs +3 -0
- data/sig/kobako/errors.rbs +7 -14
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +2 -2
- data/sig/kobako/sandbox_options.rbs +4 -2
- data/sig/kobako/snapshot.rbs +0 -3
- data/sig/kobako/transport/dispatcher.rbs +1 -1
- data/sig/kobako/transport/run.rbs +2 -2
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +27 -12
- data/ext/kobako/src/runtime/config.rs +0 -25
- data/ext/kobako/src/runtime/dispatch.rs +0 -211
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/// `cap + 1` (saturated against `usize::MAX`) when a cap is set so the
|
|
12
12
|
/// "wrote exactly cap" and "exceeded cap" cases stay distinguishable;
|
|
13
13
|
/// `usize::MAX` when the channel is uncapped.
|
|
14
|
-
pub(
|
|
14
|
+
pub(crate) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
15
15
|
match cap {
|
|
16
16
|
Some(c) => c.saturating_add(1),
|
|
17
17
|
None => usize::MAX,
|
|
@@ -24,7 +24,7 @@ pub(super) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
|
24
24
|
/// `true` only when the snapshot strictly exceeded the cap — this is the
|
|
25
25
|
/// "wrote `cap + 1` bytes into a `cap + 1`-sized pipe" case; "wrote
|
|
26
26
|
/// exactly `cap` bytes" stays `false`.
|
|
27
|
-
pub(
|
|
27
|
+
pub(crate) fn clip_capture(raw: &[u8], cap: Option<usize>) -> (&[u8], bool) {
|
|
28
28
|
match cap {
|
|
29
29
|
Some(c) if raw.len() > c => (&raw[..c], true),
|
|
30
30
|
_ => (raw, false),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//! Per-`Driver` execution configuration.
|
|
2
|
+
//!
|
|
3
|
+
//! The wall-clock and per-channel capture caps a frontend forwards into
|
|
4
|
+
//! `Driver::new`. A plain value carrier owned by the `Driver` — distinct
|
|
5
|
+
//! from the process-wide engine/module `crate::cache` (which is shared
|
|
6
|
+
//! across every sandbox) and from the per-invocation
|
|
7
|
+
//! `crate::invocation::Invocation` (which the wasm engine mutates from
|
|
8
|
+
//! inside a run). These caps are read only by `Driver` methods between
|
|
9
|
+
//! runs, so they live here.
|
|
10
|
+
|
|
11
|
+
use std::time::Duration;
|
|
12
|
+
|
|
13
|
+
/// Wall-clock and output caps for one `Driver`. `None` on any field
|
|
14
|
+
/// disables that cap.
|
|
15
|
+
pub struct Config {
|
|
16
|
+
/// Wall-clock cap for one guest `#eval` / `#run`. Stamped into a
|
|
17
|
+
/// per-run `Instant` deadline by `Driver::prime_caps`.
|
|
18
|
+
pub timeout: Option<Duration>,
|
|
19
|
+
/// Byte cap for guest stdout capture.
|
|
20
|
+
/// Sizes the per-run `MemoryOutputPipe` and computes the truncation
|
|
21
|
+
/// flag in `Driver::build_snapshot`.
|
|
22
|
+
pub stdout_limit_bytes: Option<usize>,
|
|
23
|
+
/// Byte cap for guest stderr capture. Mirror of `stdout_limit_bytes`.
|
|
24
|
+
pub stderr_limit_bytes: Option<usize>,
|
|
25
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//! Host-side dispatch for the `__kobako_dispatch` import.
|
|
2
|
+
//!
|
|
3
|
+
//! When the guest invokes the wasm import declared in
|
|
4
|
+
//! `wasm/kobako-core/src/abi.rs`, wasmtime calls back into the host
|
|
5
|
+
//! through the closure registered by `instance_pre::build_linker`.
|
|
6
|
+
//! That closure delegates here. The dispatcher:
|
|
7
|
+
//!
|
|
8
|
+
//! 1. Reads the Request bytes from guest linear memory.
|
|
9
|
+
//! 2. Invokes the bound `DispatchHandler` (the frontend's dispatch
|
|
10
|
+
//! bridge, e.g. a Ruby Proc) and recovers Response bytes.
|
|
11
|
+
//! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
|
|
12
|
+
//! through `Caller::get_export`.
|
|
13
|
+
//! 4. Writes the Response bytes into the guest buffer.
|
|
14
|
+
//! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
|
|
15
|
+
//!
|
|
16
|
+
//! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
|
|
17
|
+
//! installs the dispatch handler before any invocation, so reaching the
|
|
18
|
+
//! dispatcher with no handler bound is itself a wire-layer fault; the
|
|
19
|
+
//! guest maps a 0 return to a trap. Failures during normal dispatch
|
|
20
|
+
//! surface as Response.err envelopes from
|
|
21
|
+
//! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
|
|
22
|
+
//! this 0-return path.
|
|
23
|
+
//!
|
|
24
|
+
//! ## Why this module writes to `stderr`
|
|
25
|
+
//!
|
|
26
|
+
//! This file is the one place in the driver that deliberately prints
|
|
27
|
+
//! through `eprintln!`. The host normally surfaces faults through the
|
|
28
|
+
//! contract's error channels; the dispatcher contract is the exception
|
|
29
|
+
//! — it must return a packed `i64` to the guest and cannot fail, so a
|
|
30
|
+
//! 0 return is the only signal the wasm side receives. The guest collapses every 0 into the same trap, so the
|
|
31
|
+
//! Ruby host has no way to attribute the failure to a specific step
|
|
32
|
+
//! (missing `memory` export vs. no dispatch handler bound vs. the
|
|
33
|
+
//! handler raised vs. `__kobako_alloc` returned 0 vs. `memory.write`
|
|
34
|
+
//! rejected).
|
|
35
|
+
//!
|
|
36
|
+
//! `handle` writes a single `[kobako-dispatch] <reason>` line to
|
|
37
|
+
//! `stderr` on each failure path so operators have a breadcrumb to
|
|
38
|
+
//! correlate the trap with the actual cause. The line is emitted in
|
|
39
|
+
//! both debug and release builds on purpose: dispatcher failures are
|
|
40
|
+
//! wire-layer faults rather than expected error paths (`Kobako::Sandbox`
|
|
41
|
+
//! always installs the handler, the handler is contracted never to
|
|
42
|
+
//! raise, etc.), so the "release-build noise" cost is bounded — under
|
|
43
|
+
//! normal operation the line is never written. Operators that need to
|
|
44
|
+
//! silence the stream can redirect the host process's stderr, but the
|
|
45
|
+
//! kobako convention is "ext never logs" plus this single, named
|
|
46
|
+
//! exception.
|
|
47
|
+
|
|
48
|
+
use wasmtime::Caller;
|
|
49
|
+
|
|
50
|
+
use crate::invocation::Invocation;
|
|
51
|
+
|
|
52
|
+
/// Drive a single `__kobako_dispatch` invocation end-to-end. Entry point
|
|
53
|
+
/// from the wasmtime closure registered by `instance_pre::build_linker`.
|
|
54
|
+
///
|
|
55
|
+
/// Returns the packed `(ptr<<32)|len` u64 on success, 0 on any
|
|
56
|
+
/// wire-layer fault. Failure paths log a `[kobako-dispatch]` line to
|
|
57
|
+
/// `stderr` so operators have a breadcrumb when the guest sees a 0
|
|
58
|
+
/// return and traps. The bound dispatch handler is contracted never to
|
|
59
|
+
/// raise (it folds Service exceptions into Response.err envelopes),
|
|
60
|
+
/// so reaching the failure path is always a wiring bug or wire-layer
|
|
61
|
+
/// fault rather than an expected path.
|
|
62
|
+
pub(crate) fn handle(caller: &mut Caller<'_, Invocation>, req_ptr: i32, req_len: i32) -> i64 {
|
|
63
|
+
match try_handle(caller, req_ptr, req_len) {
|
|
64
|
+
Ok(packed) => packed,
|
|
65
|
+
Err(reason) => {
|
|
66
|
+
eprintln!("[kobako-dispatch] {reason}");
|
|
67
|
+
0
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Result-returning core of `handle`. Pulled out so each early
|
|
73
|
+
/// failure path carries a diagnostic string instead of an opaque 0.
|
|
74
|
+
fn try_handle(
|
|
75
|
+
caller: &mut Caller<'_, Invocation>,
|
|
76
|
+
req_ptr: i32,
|
|
77
|
+
req_len: i32,
|
|
78
|
+
) -> Result<i64, &'static str> {
|
|
79
|
+
let req_bytes = crate::guest_mem::read(caller, req_ptr, req_len)?;
|
|
80
|
+
|
|
81
|
+
// `Kobako::Sandbox` always installs the dispatch handler before
|
|
82
|
+
// invoking the runtime, so reaching this branch indicates a misuse
|
|
83
|
+
// rather than a normal control path.
|
|
84
|
+
let handler = caller
|
|
85
|
+
.data()
|
|
86
|
+
.on_dispatch()
|
|
87
|
+
.ok_or("a Sandbox callback fired outside an active Sandbox#run — please report this as a kobako bug")?;
|
|
88
|
+
|
|
89
|
+
// Build a frame-scoped yielder over this Caller and hand it to the
|
|
90
|
+
// handler. The borrow ends with the block, freeing the Caller for
|
|
91
|
+
// `write_response`; nested dispatch frames each build their own, so
|
|
92
|
+
// the LIFO re-entry lives on the Rust stack — no shared slot.
|
|
93
|
+
let resp_bytes = {
|
|
94
|
+
let mut yielder = crate::guest_mem::CallerYielder::new(caller);
|
|
95
|
+
handler.dispatch(&req_bytes, &mut yielder)
|
|
96
|
+
}
|
|
97
|
+
.ok_or(
|
|
98
|
+
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
99
|
+
)?;
|
|
100
|
+
|
|
101
|
+
write_response(caller, &resp_bytes)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Allocate a guest-side buffer and copy the response bytes into it via
|
|
105
|
+
/// `crate::guest_mem::alloc_and_write`, returning the packed
|
|
106
|
+
/// `(ptr<<32)|len` u64 the guest's `__kobako_dispatch` import expects.
|
|
107
|
+
fn write_response(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
|
|
108
|
+
let ptr = crate::guest_mem::alloc_and_write(caller, bytes)?;
|
|
109
|
+
Ok(((ptr as i64) << 32) | (bytes.len() as i64))
|
|
110
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
//! The wasmtime driver: everything needed to run one guest invocation,
|
|
2
|
+
//! expressed purely in contract and wasmtime types.
|
|
3
|
+
//!
|
|
4
|
+
//! A `Driver` is the engine half of a kobako host — the pre-linked
|
|
5
|
+
//! `InstancePre` plus the per-Driver caps — and implements the contract
|
|
6
|
+
//! `Runtime` trait over it. It is free of any frontend type; a frontend
|
|
7
|
+
//! shell (the Ruby ext's `Kobako::Runtime`) only shuttles its
|
|
8
|
+
//! host-language values across the contract boundary.
|
|
9
|
+
|
|
10
|
+
use std::path::Path;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use std::time::Instant;
|
|
13
|
+
|
|
14
|
+
use wasmtime::{
|
|
15
|
+
AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
use crate::cache::shared_engine;
|
|
19
|
+
use crate::config::Config;
|
|
20
|
+
use crate::exports::Exports;
|
|
21
|
+
use crate::invocation::Invocation;
|
|
22
|
+
use crate::{capture, frames, instance_pre, trap};
|
|
23
|
+
use kobako_runtime::dispatch::DispatchHandler;
|
|
24
|
+
use kobako_runtime::error::{Error, SetupError, Trap};
|
|
25
|
+
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
26
|
+
use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
|
|
27
|
+
|
|
28
|
+
/// The wire ABI version this host implements (docs/wire-codec.md § ABI
|
|
29
|
+
/// Version). A Guest Binary is accepted only when its
|
|
30
|
+
/// `__kobako_abi_version` export reports the same value; a mismatch
|
|
31
|
+
/// is a deterministic artifact fault. The guest-side mirror is
|
|
32
|
+
/// `kobako_core::abi::ABI_VERSION`. Version 2
|
|
33
|
+
/// carries the per-invocation instance discipline: the host
|
|
34
|
+
/// drives every invocation on a fresh instance, so the guest may leave
|
|
35
|
+
/// its VM state dirty at exit.
|
|
36
|
+
const ABI_VERSION: u32 = 2;
|
|
37
|
+
|
|
38
|
+
/// The wasmtime execution unit behind one sandbox runtime.
|
|
39
|
+
pub struct Driver {
|
|
40
|
+
// Pre-linked instantiation template (import wiring + type checks
|
|
41
|
+
// done once in `instance_pre::cached_instance_pre`). Every
|
|
42
|
+
// invocation instantiates a fresh instance from it and discards the
|
|
43
|
+
// whole Store afterwards — the per-invocation instance discipline.
|
|
44
|
+
instance_pre: WtInstancePre<Invocation>,
|
|
45
|
+
// Per-invocation linear-memory cap,
|
|
46
|
+
// threaded into each fresh `Invocation`; lives apart from `Config`
|
|
47
|
+
// because the wasmtime `ResourceLimiter` callback consumes it from
|
|
48
|
+
// inside the wasm engine.
|
|
49
|
+
memory_limit: Option<usize>,
|
|
50
|
+
// Wall-clock + per-channel capture caps forwarded from the Sandbox;
|
|
51
|
+
// see `Config`.
|
|
52
|
+
config: Config,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl Driver {
|
|
56
|
+
/// Construct a Driver from a wasm file path, using the process-wide
|
|
57
|
+
/// shared Engine and per-path Module / InstancePre caches, and verify
|
|
58
|
+
/// the artifact's ABI version. Every failure is a `SetupError` for
|
|
59
|
+
/// the frontend to attribute — Engine and Module never leave the
|
|
60
|
+
/// driver.
|
|
61
|
+
pub fn new(
|
|
62
|
+
path: &Path,
|
|
63
|
+
memory_limit: Option<usize>,
|
|
64
|
+
config: Config,
|
|
65
|
+
) -> Result<Self, SetupError> {
|
|
66
|
+
let instance_pre = instance_pre::cached_instance_pre(path)?;
|
|
67
|
+
let driver = Self {
|
|
68
|
+
instance_pre,
|
|
69
|
+
memory_limit,
|
|
70
|
+
config,
|
|
71
|
+
};
|
|
72
|
+
driver.probe_abi_version()?;
|
|
73
|
+
Ok(driver)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Instantiate a throwaway probe instance at construction and require
|
|
77
|
+
/// the guest's `__kobako_abi_version` export to equal `ABI_VERSION`.
|
|
78
|
+
/// An absent export or a non-equal value is a deterministic artifact
|
|
79
|
+
/// fault. The probe Store drops here; invocation instances are
|
|
80
|
+
/// created per invoke. The frameless WASI context keeps a third-party
|
|
81
|
+
/// guest whose start section touches WASI on the `SetupError` path
|
|
82
|
+
/// instead of panicking in `Invocation::wasi_mut`.
|
|
83
|
+
fn probe_abi_version(&self) -> Result<(), SetupError> {
|
|
84
|
+
let mut store = self.new_store()?;
|
|
85
|
+
frames::install_wasi_frames(&mut store, &self.config, &[])
|
|
86
|
+
.map_err(|t| SetupError::Dead(t.to_string()))?;
|
|
87
|
+
let instance = self
|
|
88
|
+
.instance_pre
|
|
89
|
+
.instantiate(store.as_context_mut())
|
|
90
|
+
.map_err(trap::instantiate_err)?;
|
|
91
|
+
let probe = instance
|
|
92
|
+
.get_typed_func::<(), u32>(store.as_context_mut(), "__kobako_abi_version")
|
|
93
|
+
.map_err(|_| {
|
|
94
|
+
SetupError::Dead(format!(
|
|
95
|
+
"the Guest Binary does not export __kobako_abi_version; \
|
|
96
|
+
rebuild it against ABI version {ABI_VERSION}"
|
|
97
|
+
))
|
|
98
|
+
})?;
|
|
99
|
+
let reported = probe.call(store.as_context_mut(), ()).map_err(|e| {
|
|
100
|
+
SetupError::Dead(format!(
|
|
101
|
+
"failed to read the Guest Binary's ABI version: {e}"
|
|
102
|
+
))
|
|
103
|
+
})?;
|
|
104
|
+
if reported != ABI_VERSION {
|
|
105
|
+
return Err(SetupError::Dead(format!(
|
|
106
|
+
"the Guest Binary reports ABI version {reported}, but this host \
|
|
107
|
+
implements ABI version {ABI_VERSION}; rebuild the Guest Binary \
|
|
108
|
+
against the host's version"
|
|
109
|
+
)));
|
|
110
|
+
}
|
|
111
|
+
Ok(())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Build the per-invocation Store: a fresh `Invocation` wired with
|
|
115
|
+
/// the memory limiter and the epoch-deadline callback.
|
|
116
|
+
fn new_store(&self) -> Result<WtStore<Invocation>, SetupError> {
|
|
117
|
+
let mut store = WtStore::new(shared_engine()?, Invocation::new(self.memory_limit));
|
|
118
|
+
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
119
|
+
store.epoch_deadline_callback(trap::epoch_deadline_callback);
|
|
120
|
+
Ok(store)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Instantiate the per-invocation instance from the pre-linked
|
|
124
|
+
/// template and resolve its host-driven export handles. An
|
|
125
|
+
/// instantiation failure at invocation time is an engine fault —
|
|
126
|
+
/// a `Trap` — unlike the construction-time probe, whose failure is
|
|
127
|
+
/// `SetupError`.
|
|
128
|
+
fn instantiate(&self, store: &mut WtStore<Invocation>) -> Result<Exports, Trap> {
|
|
129
|
+
let instance = self
|
|
130
|
+
.instance_pre
|
|
131
|
+
.instantiate(store.as_context_mut())
|
|
132
|
+
.map_err(|e| Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}")))?;
|
|
133
|
+
Ok(Exports::resolve(&instance, store.as_context_mut()))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Run one guest export call inside the per-invocation cap window:
|
|
137
|
+
/// `Driver::prime_caps` before, `disarm_caps` after — the shared
|
|
138
|
+
/// bracket for both run-path exports (`__kobako_eval` /
|
|
139
|
+
/// `__kobako_run`). Disarm runs whether the call returns or traps, so
|
|
140
|
+
/// the `wall_time` bracket and the memory
|
|
141
|
+
/// cap always close — that close-on-trap guarantee is the reason this
|
|
142
|
+
/// bracket lives in one place rather than inline at each call site.
|
|
143
|
+
/// The wasmtime trap is returned unmapped; the caller classifies it
|
|
144
|
+
/// through `trap::trap_from`.
|
|
145
|
+
fn call_with_caps<Params, Results>(
|
|
146
|
+
&self,
|
|
147
|
+
store: &mut WtStore<Invocation>,
|
|
148
|
+
exports: &Exports,
|
|
149
|
+
export: &TypedFunc<Params, Results>,
|
|
150
|
+
params: Params,
|
|
151
|
+
) -> Result<Results, wasmtime::Error>
|
|
152
|
+
where
|
|
153
|
+
Params: wasmtime::WasmParams,
|
|
154
|
+
Results: wasmtime::WasmResults,
|
|
155
|
+
{
|
|
156
|
+
self.prime_caps(store, exports);
|
|
157
|
+
let result = export.call(store.as_context_mut(), params);
|
|
158
|
+
disarm_caps(store);
|
|
159
|
+
result
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Stamp the per-invocation wall-clock deadline into `Invocation`
|
|
163
|
+
/// and prime the wasmtime epoch deadline so the next ticker tick
|
|
164
|
+
/// wakes the epoch-deadline callback. When `timeout` is disabled,
|
|
165
|
+
/// the deadline is set far enough in the future that the callback
|
|
166
|
+
/// effectively never fires.
|
|
167
|
+
///
|
|
168
|
+
/// Also captures the current linear-memory size as the baseline
|
|
169
|
+
/// for the per-invocation memory delta cap —
|
|
170
|
+
/// the pre-initialized image's allocation is folded into the
|
|
171
|
+
/// baseline rather than the budget — and stamps the wall-clock
|
|
172
|
+
/// entry instant for the `wall_time`
|
|
173
|
+
/// measurement. The bracket closes in `disarm_caps` so it matches
|
|
174
|
+
/// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
175
|
+
/// decoding and stdout / stderr capture readout.
|
|
176
|
+
fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
|
|
177
|
+
match self.config.timeout {
|
|
178
|
+
Some(timeout) => {
|
|
179
|
+
let deadline = Instant::now() + timeout;
|
|
180
|
+
store.data_mut().set_deadline(Some(deadline));
|
|
181
|
+
store.set_epoch_deadline(1);
|
|
182
|
+
}
|
|
183
|
+
None => {
|
|
184
|
+
store.data_mut().set_deadline(None);
|
|
185
|
+
store.set_epoch_deadline(u64::MAX);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
let baseline = match exports.memory {
|
|
189
|
+
Some(m) => m.data_size(store.as_context_mut()),
|
|
190
|
+
None => 0,
|
|
191
|
+
};
|
|
192
|
+
store.data_mut().arm_memory_cap(baseline);
|
|
193
|
+
store.data_mut().start_wall_clock();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Bundle one invocation's observables into a fresh `Snapshot`,
|
|
197
|
+
/// uniformly for every `completion` — the clipped captures and the
|
|
198
|
+
/// cap-bracket usage must survive a trap just as they do an outcome.
|
|
199
|
+
fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
|
|
200
|
+
let data = store.data();
|
|
201
|
+
let usage = Usage {
|
|
202
|
+
wall_time: data.wall_time().as_secs_f64(),
|
|
203
|
+
memory_peak: data.memory_peak(),
|
|
204
|
+
};
|
|
205
|
+
let (stdout_raw, stderr_raw) = (data.stdout_bytes(), data.stderr_bytes());
|
|
206
|
+
let (stdout_visible, stdout_truncated) =
|
|
207
|
+
capture::clip_capture(&stdout_raw, self.config.stdout_limit_bytes);
|
|
208
|
+
let (stderr_visible, stderr_truncated) =
|
|
209
|
+
capture::clip_capture(&stderr_raw, self.config.stderr_limit_bytes);
|
|
210
|
+
Snapshot {
|
|
211
|
+
completion,
|
|
212
|
+
stdout: Capture {
|
|
213
|
+
bytes: stdout_visible.to_vec(),
|
|
214
|
+
truncated: stdout_truncated,
|
|
215
|
+
},
|
|
216
|
+
stderr: Capture {
|
|
217
|
+
bytes: stderr_visible.to_vec(),
|
|
218
|
+
truncated: stderr_truncated,
|
|
219
|
+
},
|
|
220
|
+
usage,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
impl ContractRuntime for Driver {
|
|
226
|
+
/// Drive one guest invocation on a fresh instance and return its
|
|
227
|
+
/// `Snapshot`, `Ok` iff the guest export ran. Builds a fresh Store,
|
|
228
|
+
/// binds the borrowed dispatch handler, installs the stdin frames
|
|
229
|
+
/// (three for `Eval` — preamble / source / snippets; two for `Run` —
|
|
230
|
+
/// preamble / snippets, with the envelope copied into guest memory),
|
|
231
|
+
/// and primes the per-invocation caps around the export call. A fault
|
|
232
|
+
/// before the export call is the `Err` channel; once the call starts,
|
|
233
|
+
/// every fault folds into the Snapshot's `Completion` — the
|
|
234
|
+
/// configured-cap paths as `Trap::Timeout` / `Trap::MemoryLimit`,
|
|
235
|
+
/// everything else as `Trap::Other` — so captures and usage survive
|
|
236
|
+
/// it. The body touches no frontend value — the handler is only
|
|
237
|
+
/// borrowed (see the trait's safety contract).
|
|
238
|
+
fn invoke(
|
|
239
|
+
&self,
|
|
240
|
+
entry: Entry<'_>,
|
|
241
|
+
frames: Frames<'_>,
|
|
242
|
+
handler: Option<Arc<dyn DispatchHandler>>,
|
|
243
|
+
) -> Result<Snapshot, Error> {
|
|
244
|
+
let mut store = self.new_store()?;
|
|
245
|
+
if let Some(handler) = handler {
|
|
246
|
+
store.data_mut().bind_on_dispatch(handler);
|
|
247
|
+
}
|
|
248
|
+
let frame_list: Vec<&[u8]> = match &entry {
|
|
249
|
+
Entry::Eval { source } => vec![frames.preamble, source, frames.snippets],
|
|
250
|
+
Entry::Run { .. } => vec![frames.preamble, frames.snippets],
|
|
251
|
+
};
|
|
252
|
+
frames::install_wasi_frames(&mut store, &self.config, &frame_list)?;
|
|
253
|
+
let exports = self.instantiate(&mut store)?;
|
|
254
|
+
let called = match entry {
|
|
255
|
+
Entry::Eval { .. } => {
|
|
256
|
+
let eval = frames::require_export(exports.eval.as_ref())?;
|
|
257
|
+
self.call_with_caps(&mut store, &exports, eval, ())
|
|
258
|
+
}
|
|
259
|
+
Entry::Run { envelope } => {
|
|
260
|
+
let run = frames::require_export(exports.run.as_ref())?;
|
|
261
|
+
let (env_ptr, env_len) = frames::write_envelope(&mut store, &exports, envelope)?;
|
|
262
|
+
self.call_with_caps(&mut store, &exports, run, (env_ptr, env_len))
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
let completion = match called {
|
|
266
|
+
Ok(()) => match frames::fetch_outcome_bytes(&mut store, &exports) {
|
|
267
|
+
Ok(bytes) => Completion::Outcome(bytes),
|
|
268
|
+
Err(t) => Completion::Trap(t),
|
|
269
|
+
},
|
|
270
|
+
Err(e) => Completion::Trap(trap::trap_from(e)),
|
|
271
|
+
};
|
|
272
|
+
Ok(self.build_snapshot(&store, completion))
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Drop the memory cap as soon as the guest call returns so that
|
|
277
|
+
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
278
|
+
/// which can grow guest memory transiently) is not attributed to
|
|
279
|
+
/// the user script. Also closes the
|
|
280
|
+
/// `wall_time` bracket opened by `Driver::prime_caps`. Paired
|
|
281
|
+
/// with `Driver::prime_caps`.
|
|
282
|
+
fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
283
|
+
store.data_mut().stop_wall_clock();
|
|
284
|
+
store.data_mut().disarm_memory_cap();
|
|
285
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
//! Per-invocation wasmtime export handles for the host-driven ABI
|
|
2
2
|
//! surface.
|
|
3
3
|
//!
|
|
4
|
-
//! `
|
|
4
|
+
//! `Driver::instantiate` resolves the ABI exports the run path drives
|
|
5
5
|
//! (`__kobako_eval` / `__kobako_run` / `__kobako_take_outcome` /
|
|
6
6
|
//! `__kobako_alloc`) plus the `memory` export against each fresh
|
|
7
7
|
//! per-invocation instance and bundles their
|
|
8
8
|
//! typed handles here, so the invocation body passes one struct around
|
|
9
9
|
//! rather than re-resolving exports by name at every step. Distinct
|
|
10
|
-
//! from `
|
|
10
|
+
//! from `crate::cache` (the process-wide Engine / Module cache): this
|
|
11
11
|
//! carries *which guest function to call*, per invocation.
|
|
12
12
|
//!
|
|
13
|
-
//! `
|
|
13
|
+
//! `crate::dispatch` does not reach this struct — a host import runs
|
|
14
14
|
//! against a `Caller`, so the dispatch path resolves `__kobako_alloc`
|
|
15
15
|
//! and `memory` through `Caller::get_export` instead.
|
|
16
16
|
|
|
@@ -18,9 +18,8 @@ use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
|
|
|
18
18
|
|
|
19
19
|
/// The resolved host-driven export handles. Each is `Option` because test
|
|
20
20
|
/// fixtures (a minimal "ping" module) need not provide them; real
|
|
21
|
-
/// `kobako.wasm` always does, and the run-path methods
|
|
22
|
-
///
|
|
23
|
-
/// handle is `None`.
|
|
21
|
+
/// `kobako.wasm` always does, and the run-path methods surface a `Trap`
|
|
22
|
+
/// (via `require_export` / `require_memory`) when a handle is `None`.
|
|
24
23
|
///
|
|
25
24
|
/// The handles are indices into the owning Store, not borrows of the
|
|
26
25
|
/// `Instance` — they stay valid for the Store's lifetime, which is why
|