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,89 +1,81 @@
|
|
|
1
|
-
//! Host
|
|
1
|
+
//! Host side of the `__kobako_upcall` import: hand the guest's Call to
|
|
2
|
+
//! the bound `DispatchHandler` and write its Reply back into guest memory.
|
|
3
|
+
//! A request under a tag the host does not serve is answered as
|
|
4
|
+
//! unsupported, so a guest asking for a later operation can fall back.
|
|
2
5
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
6
|
+
//! A failure lands on whoever is answerable for it. A trap the guest
|
|
7
|
+
//! raises during a callback into it ends the invocation as that trap; any
|
|
8
|
+
//! other failure is the guest's or the wire's, so the import answers 0,
|
|
9
|
+
//! which the guest receives as a wire failure.
|
|
7
10
|
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
11
|
-
//! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
|
|
12
|
-
//! through `Caller::get_export`.
|
|
13
|
-
//! 4. Writes the Reply 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
|
-
//! take the Reply's fault arm 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.
|
|
11
|
+
//! Each 0 also writes one `[kobako-dispatch] <reason>` line to stderr,
|
|
12
|
+
//! since the 0 itself carries no reason. It is the one place the driver
|
|
13
|
+
//! logs, and normal operation never reaches it.
|
|
47
14
|
|
|
48
15
|
use wasmtime::Caller;
|
|
49
16
|
|
|
50
|
-
use
|
|
17
|
+
use kobako_runtime::error::Trap;
|
|
18
|
+
use kobako_runtime::yielder::Yielder;
|
|
19
|
+
use kobako_transport::abi::pack_ptr_len;
|
|
20
|
+
use kobako_transport::envelope::{Unsupported, Upcall, YieldReply};
|
|
51
21
|
|
|
22
|
+
use crate::guest_mem;
|
|
52
23
|
use crate::invocation::Invocation;
|
|
53
24
|
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Ok(packed) => packed,
|
|
67
|
-
Err(reason) => {
|
|
68
|
-
eprintln!("[kobako-dispatch] {reason}");
|
|
69
|
-
0
|
|
70
|
-
}
|
|
25
|
+
/// The wasmtime-backed `Yielder`: built per `__kobako_upcall` frame over
|
|
26
|
+
/// that frame's `Caller`, so nested dispatch frames each carry their own and
|
|
27
|
+
/// stack on the Rust call stack with no shared slot.
|
|
28
|
+
struct CallerYielder<'a, 'c> {
|
|
29
|
+
caller: &'a mut Caller<'c, Invocation>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Yielder for CallerYielder<'_, '_> {
|
|
33
|
+
fn yield_to_block(&mut self, args: &[u8]) -> Result<YieldReply, Trap> {
|
|
34
|
+
let bytes = guest_mem::drive_yield(self.caller, args)
|
|
35
|
+
.map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
36
|
+
YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
|
|
71
37
|
}
|
|
72
38
|
}
|
|
73
39
|
|
|
74
|
-
///
|
|
75
|
-
///
|
|
40
|
+
/// Answer one `__kobako_upcall` call: the packed `(ptr<<32)|len` of the
|
|
41
|
+
/// Reply, 0 for a failure the guest or the wire is answerable for, or the
|
|
42
|
+
/// trap a callback into the guest raised.
|
|
43
|
+
pub(crate) fn handle(
|
|
44
|
+
caller: &mut Caller<'_, Invocation>,
|
|
45
|
+
req_ptr: i32,
|
|
46
|
+
req_len: i32,
|
|
47
|
+
) -> wasmtime::Result<i64> {
|
|
48
|
+
let answer = try_handle(caller, req_ptr, req_len);
|
|
49
|
+
if let Some(trap) = caller.data_mut().take_reentry_trap() {
|
|
50
|
+
return Err(trap);
|
|
51
|
+
}
|
|
52
|
+
Ok(answer.unwrap_or_else(|reason| {
|
|
53
|
+
eprintln!("[kobako-dispatch] {reason}");
|
|
54
|
+
0
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
|
|
76
58
|
fn try_handle(
|
|
77
59
|
caller: &mut Caller<'_, Invocation>,
|
|
78
60
|
req_ptr: i32,
|
|
79
61
|
req_len: i32,
|
|
80
62
|
) -> Result<i64, &'static str> {
|
|
81
|
-
let req_bytes =
|
|
82
|
-
// The driver decodes the core envelope
|
|
83
|
-
// frame; the payload inside it stays bytes
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
63
|
+
let req_bytes = guest_mem::read_request(caller, req_ptr, req_len)?;
|
|
64
|
+
// The driver decodes the core envelope, here and in `CallerYielder`, so
|
|
65
|
+
// the frontend never sees a frame; the payload inside it stays bytes
|
|
66
|
+
// the whole way through.
|
|
67
|
+
let call = match Upcall::decode(&req_bytes) {
|
|
68
|
+
Ok(Upcall::Call(call)) => call,
|
|
69
|
+
Ok(_) => {
|
|
70
|
+
let refusal = Unsupported::new("the host does not serve this request");
|
|
71
|
+
return write_reply(caller, &refusal.encode());
|
|
72
|
+
}
|
|
73
|
+
Err(_) => {
|
|
74
|
+
return Err(
|
|
75
|
+
"the guest sent a malformed Call envelope — please report this as a kobako bug",
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
};
|
|
87
79
|
|
|
88
80
|
// `Kobako::Sandbox` always installs the dispatch handler before
|
|
89
81
|
// invoking the runtime, so reaching this branch indicates a misuse
|
|
@@ -91,27 +83,25 @@ fn try_handle(
|
|
|
91
83
|
let handler = caller
|
|
92
84
|
.data()
|
|
93
85
|
.on_dispatch()
|
|
94
|
-
.ok_or("a Sandbox callback fired outside an active
|
|
86
|
+
.ok_or("a Sandbox callback fired outside an active invocation — please report this as a kobako bug")?;
|
|
95
87
|
|
|
96
88
|
// Build a frame-scoped yielder over this Caller and hand it to the
|
|
97
89
|
// handler. The borrow ends with the block, freeing the Caller for
|
|
98
|
-
// `
|
|
90
|
+
// `write_reply`; nested dispatch frames each build their own, so
|
|
99
91
|
// the LIFO re-entry lives on the Rust stack — no shared slot.
|
|
100
92
|
let reply = {
|
|
101
|
-
let mut yielder =
|
|
93
|
+
let mut yielder = CallerYielder { caller };
|
|
102
94
|
handler.dispatch(call, &mut yielder)
|
|
103
95
|
}
|
|
104
96
|
.ok_or(
|
|
105
97
|
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
106
98
|
)?;
|
|
107
99
|
|
|
108
|
-
|
|
100
|
+
write_reply(caller, &reply.encode())
|
|
109
101
|
}
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let ptr = crate::guest_mem::alloc_and_write(caller, bytes)?;
|
|
116
|
-
Ok(((ptr as i64) << 32) | (bytes.len() as i64))
|
|
103
|
+
fn write_reply(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
|
|
104
|
+
let ptr = guest_mem::write_from_callback(caller, bytes)?;
|
|
105
|
+
// The write has already held the length to the payload cap.
|
|
106
|
+
Ok(pack_ptr_len(ptr, bytes.len() as u32) as i64)
|
|
117
107
|
}
|
|
@@ -17,14 +17,15 @@ use wasmtime::{
|
|
|
17
17
|
|
|
18
18
|
use crate::cache::shared_engine;
|
|
19
19
|
use crate::config::Config;
|
|
20
|
-
use crate::exports::Exports;
|
|
20
|
+
use crate::exports::{self, Exports};
|
|
21
21
|
use crate::invocation::Invocation;
|
|
22
|
-
use crate::{capture,
|
|
22
|
+
use crate::{capture, guest_mem, instance_pre, io, trap};
|
|
23
23
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
24
24
|
use kobako_runtime::error::{InvokeError, SetupError, Trap};
|
|
25
25
|
use kobako_runtime::profile::Profile;
|
|
26
|
-
use kobako_runtime::runtime::{Entry,
|
|
26
|
+
use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
|
|
27
27
|
use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
|
|
28
|
+
use kobako_transport::envelope::{CompileReply, Downcall, Preamble, Run, Snippet};
|
|
28
29
|
|
|
29
30
|
/// The wasmtime execution unit behind one sandbox runtime.
|
|
30
31
|
pub struct Driver {
|
|
@@ -39,11 +40,9 @@ pub struct Driver {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
impl Driver {
|
|
42
|
-
///
|
|
43
|
-
/// shared
|
|
44
|
-
///
|
|
45
|
-
/// (`crate::abi`). Every failure is a `SetupError` for the frontend
|
|
46
|
-
/// to attribute — Engine and Module never leave the driver.
|
|
43
|
+
/// Load the Guest Binary at `path` and verify its ABI version. The
|
|
44
|
+
/// Engine is shared by the process and the Module cached per path;
|
|
45
|
+
/// neither leaves the driver.
|
|
47
46
|
pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
|
|
48
47
|
Ok(Self {
|
|
49
48
|
instance_pre: instance_pre::cached_instance_pre(path)?,
|
|
@@ -51,8 +50,6 @@ impl Driver {
|
|
|
51
50
|
})
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
/// Build the per-invocation Store: a fresh `Invocation` wired with
|
|
55
|
-
/// the memory limiter and the epoch-deadline callback.
|
|
56
53
|
fn new_store(&self) -> Result<WtStore<Invocation>, SetupError> {
|
|
57
54
|
let mut store = WtStore::new(shared_engine()?, Invocation::new(self.config.memory_limit));
|
|
58
55
|
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
@@ -60,28 +57,21 @@ impl Driver {
|
|
|
60
57
|
Ok(store)
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
///
|
|
64
|
-
///
|
|
65
|
-
/// instantiation failure at invocation time is an engine fault —
|
|
66
|
-
/// a `Trap` — unlike the construction-time probe, whose failure is
|
|
67
|
-
/// `SetupError`.
|
|
60
|
+
/// Failing here is an engine fault — a `Trap` — unlike the
|
|
61
|
+
/// construction-time probe, whose failure is a `SetupError`.
|
|
68
62
|
fn instantiate(&self, store: &mut WtStore<Invocation>) -> Result<Exports, Trap> {
|
|
69
63
|
let instance = self
|
|
70
64
|
.instance_pre
|
|
71
65
|
.instantiate(store.as_context_mut())
|
|
72
66
|
.map_err(|e| Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}")))?;
|
|
73
|
-
|
|
67
|
+
let exports = Exports::resolve(&instance, store.as_context_mut());
|
|
68
|
+
store.data_mut().set_exports(exports.clone());
|
|
69
|
+
Ok(exports)
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
///
|
|
77
|
-
///
|
|
78
|
-
/// bracket
|
|
79
|
-
/// `__kobako_run`). Disarm runs whether the call returns or traps, so
|
|
80
|
-
/// the `wall_time` bracket and the memory
|
|
81
|
-
/// cap always close — that close-on-trap guarantee is the reason this
|
|
82
|
-
/// bracket lives in one place rather than inline at each call site.
|
|
83
|
-
/// The wasmtime trap is returned unmapped; the caller classifies it
|
|
84
|
-
/// through `trap::trap_from`.
|
|
72
|
+
/// Disarm runs whether the call returns or traps, so the `wall_time`
|
|
73
|
+
/// bracket and the memory cap always close; that guarantee is why the
|
|
74
|
+
/// bracket lives in one place rather than at each call site.
|
|
85
75
|
fn call_with_caps<Params, Results>(
|
|
86
76
|
&self,
|
|
87
77
|
store: &mut WtStore<Invocation>,
|
|
@@ -99,20 +89,8 @@ impl Driver {
|
|
|
99
89
|
result
|
|
100
90
|
}
|
|
101
91
|
|
|
102
|
-
///
|
|
103
|
-
///
|
|
104
|
-
/// wakes the epoch-deadline callback. When `timeout` is disabled,
|
|
105
|
-
/// the deadline is set far enough in the future that the callback
|
|
106
|
-
/// effectively never fires.
|
|
107
|
-
///
|
|
108
|
-
/// Also captures the current linear-memory size as the baseline
|
|
109
|
-
/// for the per-invocation memory delta cap —
|
|
110
|
-
/// the pre-initialized image's allocation is folded into the
|
|
111
|
-
/// baseline rather than the budget — and stamps the wall-clock
|
|
112
|
-
/// entry instant for the `wall_time`
|
|
113
|
-
/// measurement. The bracket closes in `disarm_caps` so it matches
|
|
114
|
-
/// the `timeout` deadline window and excludes `OUTCOME_BUFFER`
|
|
115
|
-
/// decoding and stdout / stderr capture readout.
|
|
92
|
+
/// The pre-initialized image's allocation is folded into the memory
|
|
93
|
+
/// baseline rather than the budget.
|
|
116
94
|
fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
|
|
117
95
|
match self.config.timeout {
|
|
118
96
|
Some(timeout) => {
|
|
@@ -133,9 +111,8 @@ impl Driver {
|
|
|
133
111
|
store.data_mut().start_wall_clock();
|
|
134
112
|
}
|
|
135
113
|
|
|
136
|
-
///
|
|
137
|
-
///
|
|
138
|
-
/// cap-bracket usage must survive a trap just as they do an outcome.
|
|
114
|
+
/// Built the same for every `completion`: captures and usage must
|
|
115
|
+
/// survive a trap just as they do an outcome.
|
|
139
116
|
fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
|
|
140
117
|
let data = store.data();
|
|
141
118
|
let usage = Usage {
|
|
@@ -162,48 +139,39 @@ impl Driver {
|
|
|
162
139
|
}
|
|
163
140
|
|
|
164
141
|
impl ContractRuntime for Driver {
|
|
165
|
-
///
|
|
166
|
-
///
|
|
167
|
-
///
|
|
168
|
-
///
|
|
169
|
-
/// preamble / snippets, with the envelope copied into guest memory),
|
|
170
|
-
/// and primes the per-invocation caps around the export call. A fault
|
|
171
|
-
/// before the export call is the `Err` channel; once the call starts,
|
|
172
|
-
/// every fault folds into the Snapshot's `Completion` — the
|
|
173
|
-
/// configured-cap paths as `Trap::Timeout` / `Trap::MemoryLimit`,
|
|
174
|
-
/// everything else as `Trap::Other` — so captures and usage survive
|
|
175
|
-
/// it. The body touches no frontend value — the handler is only
|
|
176
|
-
/// borrowed (see the trait's safety contract).
|
|
142
|
+
/// A fault before the export call is the `Err`; once the call
|
|
143
|
+
/// starts, every fault folds into the Snapshot's `Completion` so
|
|
144
|
+
/// captures and usage survive it. The handler is only borrowed (see the
|
|
145
|
+
/// trait's safety contract).
|
|
177
146
|
fn invoke(
|
|
178
147
|
&self,
|
|
179
148
|
entry: Entry<'_>,
|
|
180
|
-
|
|
149
|
+
preamble: &Preamble,
|
|
181
150
|
handler: Option<Arc<dyn DispatchHandler>>,
|
|
182
151
|
) -> Result<Snapshot, InvokeError> {
|
|
183
152
|
let mut store = self.new_store()?;
|
|
184
153
|
if let Some(handler) = handler {
|
|
185
154
|
store.data_mut().bind_on_dispatch(handler);
|
|
186
155
|
}
|
|
187
|
-
|
|
188
|
-
Entry::Eval { source } => vec![frames.preamble, source, frames.snippets],
|
|
189
|
-
Entry::Run { .. } => vec![frames.preamble, frames.snippets],
|
|
190
|
-
};
|
|
191
|
-
frames::install_wasi_frames(&mut store, &self.config, &frame_list)?;
|
|
156
|
+
io::install_wasi(&mut store, &self.config, &preamble.encode())?;
|
|
192
157
|
let exports = self.instantiate(&mut store)?;
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
}
|
|
158
|
+
let downcall = exports::require(exports.downcall.as_ref())?;
|
|
159
|
+
let request = match entry {
|
|
160
|
+
Entry::Eval { source } => Downcall::Eval(source.to_vec()),
|
|
161
|
+
Entry::Run {
|
|
162
|
+
entrypoint,
|
|
163
|
+
payload,
|
|
164
|
+
} => Downcall::Run(Run {
|
|
165
|
+
entrypoint: entrypoint.to_owned(),
|
|
166
|
+
payload,
|
|
167
|
+
}),
|
|
168
|
+
}
|
|
169
|
+
.encode();
|
|
170
|
+
let (ptr, len) = io::write_request(&mut store, &exports, &request)?;
|
|
171
|
+
let called = self.call_with_caps(&mut store, &exports, downcall, (ptr, len));
|
|
204
172
|
let completion = match called {
|
|
205
|
-
Ok(
|
|
206
|
-
Ok(
|
|
173
|
+
Ok(packed) => match io::read_outcome(&store, &exports, packed) {
|
|
174
|
+
Ok(outcome) => Completion::Outcome(outcome),
|
|
207
175
|
Err(t) => Completion::Trap(t),
|
|
208
176
|
},
|
|
209
177
|
Err(e) => Completion::Trap(trap::trap_from(e)),
|
|
@@ -211,25 +179,104 @@ impl ContractRuntime for Driver {
|
|
|
211
179
|
Ok(self.build_snapshot(&store, completion))
|
|
212
180
|
}
|
|
213
181
|
|
|
214
|
-
///
|
|
215
|
-
///
|
|
216
|
-
///
|
|
217
|
-
/// and entropy (`crate::ambient`), `Permissive` leaves the live WASI
|
|
218
|
-
/// sources. Both rungs wire no filesystem, environment, or network,
|
|
219
|
-
/// and the linker adds only the wire ABI's `__kobako_dispatch`
|
|
220
|
-
/// beyond that confined WASI surface (`crate::instance_pre`).
|
|
182
|
+
/// Exactly the rung `Config` requested. `Hermetic` freezes ambient time
|
|
183
|
+
/// and entropy; both rungs wire no filesystem, environment, or network,
|
|
184
|
+
/// and the linker adds only `__kobako_upcall` beyond that WASI surface.
|
|
221
185
|
fn profile(&self) -> Profile {
|
|
222
186
|
self.config.profile
|
|
223
187
|
}
|
|
188
|
+
|
|
189
|
+
/// Bounded by the same caps as an invocation, on an instance of its
|
|
190
|
+
/// own that no script ever runs on, so what it answers carries
|
|
191
|
+
/// nothing a script could have written.
|
|
192
|
+
fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
|
|
193
|
+
let mut store = self.new_store().ok()?;
|
|
194
|
+
io::install_wasi(&mut store, &self.config, &preamble.encode()).ok()?;
|
|
195
|
+
let exports = self.instantiate(&mut store).ok()?;
|
|
196
|
+
let downcall = exports::require(exports.downcall.as_ref()).ok()?;
|
|
197
|
+
let (ptr, len) =
|
|
198
|
+
io::write_request(&mut store, &exports, &Downcall::Compile.encode()).ok()?;
|
|
199
|
+
let packed = self
|
|
200
|
+
.call_with_caps(&mut store, &exports, downcall, (ptr, len))
|
|
201
|
+
.ok()?;
|
|
202
|
+
match io::read_compile_reply(&store, &exports, packed).ok()? {
|
|
203
|
+
CompileReply::Ok(bytecodes) => one_per_source(preamble, bytecodes)
|
|
204
|
+
.filter(|bytecodes| replay_fits(preamble, bytecodes)),
|
|
205
|
+
CompileReply::Error(_) | CompileReply::Unsupported(_) => None,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Each invocation sends the Preamble with every bytecode in place of its
|
|
211
|
+
/// source, so an answer that would carry it past the payload cap would trap
|
|
212
|
+
/// every invocation the source alone fits.
|
|
213
|
+
fn replay_fits(preamble: &Preamble, bytecodes: &[Vec<u8>]) -> bool {
|
|
214
|
+
let mut replay = preamble.clone();
|
|
215
|
+
let sources = replay
|
|
216
|
+
.snippets
|
|
217
|
+
.entries
|
|
218
|
+
.iter_mut()
|
|
219
|
+
.filter(|entry| matches!(entry, Snippet::Source { .. }));
|
|
220
|
+
for (entry, body) in sources.zip(bytecodes) {
|
|
221
|
+
*entry = Snippet::Bytecode { body: body.clone() };
|
|
222
|
+
}
|
|
223
|
+
guest_mem::checked_payload_len(replay.encode().len()).is_ok()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// A frontend replays each bytecode in place of one source snippet, in
|
|
227
|
+
/// order, so an answer of any other length leaves nothing to replay.
|
|
228
|
+
fn one_per_source(preamble: &Preamble, bytecodes: Vec<Vec<u8>>) -> Option<Vec<Vec<u8>>> {
|
|
229
|
+
let sources = preamble
|
|
230
|
+
.snippets
|
|
231
|
+
.entries
|
|
232
|
+
.iter()
|
|
233
|
+
.filter(|entry| matches!(entry, Snippet::Source { .. }))
|
|
234
|
+
.count();
|
|
235
|
+
(bytecodes.len() == sources).then_some(bytecodes)
|
|
224
236
|
}
|
|
225
237
|
|
|
226
|
-
/// Drop the memory cap as soon as the guest call returns so that
|
|
227
|
-
/// any post-run host bookkeeping (e.g. fetching the OUTCOME_BUFFER,
|
|
228
|
-
/// which can grow guest memory transiently) is not attributed to
|
|
229
|
-
/// the user script. Also closes the
|
|
230
|
-
/// `wall_time` bracket opened by `Driver::prime_caps`. Paired
|
|
231
|
-
/// with `Driver::prime_caps`.
|
|
232
238
|
fn disarm_caps(store: &mut WtStore<Invocation>) {
|
|
233
239
|
store.data_mut().stop_wall_clock();
|
|
234
240
|
store.data_mut().disarm_memory_cap();
|
|
235
241
|
}
|
|
242
|
+
|
|
243
|
+
#[cfg(test)]
|
|
244
|
+
mod tests {
|
|
245
|
+
use kobako_transport::envelope::Snippets;
|
|
246
|
+
|
|
247
|
+
use super::*;
|
|
248
|
+
|
|
249
|
+
/// One source between two bytecode snippets: a compile answers one
|
|
250
|
+
/// bytecode for it.
|
|
251
|
+
fn one_source() -> Preamble {
|
|
252
|
+
let mut preamble = Preamble::default();
|
|
253
|
+
preamble.snippets = Snippets {
|
|
254
|
+
entries: vec![
|
|
255
|
+
Snippet::Bytecode { body: vec![0xaa] },
|
|
256
|
+
Snippet::Source {
|
|
257
|
+
name: "Only".into(),
|
|
258
|
+
body: "1".into(),
|
|
259
|
+
},
|
|
260
|
+
Snippet::Bytecode { body: vec![0xbb] },
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
preamble
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// @behavior WE-089
|
|
267
|
+
#[test]
|
|
268
|
+
fn a_compiled_answer_of_another_length_leaves_nothing_compiled() {
|
|
269
|
+
let preamble = one_source();
|
|
270
|
+
|
|
271
|
+
assert_eq!(
|
|
272
|
+
[
|
|
273
|
+
one_per_source(&preamble, Vec::new()),
|
|
274
|
+
one_per_source(&preamble, vec![vec![0x01], vec![0x02]]),
|
|
275
|
+
one_per_source(&preamble, vec![vec![0x01]]),
|
|
276
|
+
],
|
|
277
|
+
[None, None, Some(vec![vec![0x01]])],
|
|
278
|
+
"a compiled answer through Driver#compile must be kept only when it carries one \
|
|
279
|
+
bytecode per source snippet"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -1,56 +1,47 @@
|
|
|
1
1
|
//! Per-invocation wasmtime export handles for the host-driven ABI
|
|
2
2
|
//! surface.
|
|
3
3
|
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
10
|
-
//! from `crate::cache` (the process-wide Engine / Module cache): this
|
|
11
|
-
//! carries *which guest function to call*, per invocation.
|
|
12
|
-
//!
|
|
13
|
-
//! `crate::dispatch` does not reach this struct — a host import runs
|
|
14
|
-
//! against a `Caller`, so the dispatch path resolves `__kobako_alloc`
|
|
15
|
-
//! and `memory` through `Caller::get_export` instead.
|
|
4
|
+
//! Each ABI export is named and typed once, here, against each fresh
|
|
5
|
+
//! per-invocation instance. The invocation body and the host imports
|
|
6
|
+
//! running inside it both drive the guest through these handles, never
|
|
7
|
+
//! by looking an export up by name. Distinct from `crate::cache` (the
|
|
8
|
+
//! process-wide Engine / Module cache): this carries *which guest
|
|
9
|
+
//! function to call*, per invocation.
|
|
16
10
|
|
|
17
11
|
use wasmtime::{AsContextMut, Instance as WtInstance, Memory, TypedFunc};
|
|
18
12
|
|
|
13
|
+
use crate::guest_mem::SANDBOX_RUNTIME_NOT_KOBAKO;
|
|
14
|
+
use kobako_runtime::error::Trap;
|
|
15
|
+
|
|
16
|
+
/// For a required guest export that is absent or mistyped, wherever it is
|
|
17
|
+
/// looked up. Names neither the export nor a frontend's artifact path;
|
|
18
|
+
/// what a caller can do is rebuild the runtime.
|
|
19
|
+
pub(crate) const RUNTIME_INCOMPATIBLE: &str =
|
|
20
|
+
"the Sandbox runtime is incompatible; rebuild it against the kobako version in use";
|
|
21
|
+
|
|
19
22
|
/// The resolved host-driven export handles. Each is `Option` because test
|
|
20
23
|
/// fixtures (a minimal "ping" module) need not provide them; real
|
|
21
|
-
/// `kobako.wasm` always does, and the run
|
|
22
|
-
///
|
|
24
|
+
/// `kobako.wasm` always does, and the run path surfaces a `Trap` (via
|
|
25
|
+
/// `require` / `Exports::require_memory`) when a handle is `None`.
|
|
23
26
|
///
|
|
24
27
|
/// The handles are indices into the owning Store, not borrows of the
|
|
25
28
|
/// `Instance` — they stay valid for the Store's lifetime, which is why
|
|
26
|
-
/// no `Instance` field is kept
|
|
29
|
+
/// no `Instance` field is kept, and why a clone can ride in the Store's
|
|
30
|
+
/// own data for the host imports to reach.
|
|
31
|
+
#[derive(Clone)]
|
|
27
32
|
pub(crate) struct Exports {
|
|
28
|
-
pub(crate)
|
|
29
|
-
pub(crate) run: Option<TypedFunc<(i32, i32), ()>>,
|
|
30
|
-
pub(crate) take_outcome: Option<TypedFunc<(), u64>>,
|
|
33
|
+
pub(crate) downcall: Option<TypedFunc<(i32, i32), u64>>,
|
|
31
34
|
pub(crate) alloc: Option<TypedFunc<u32, u32>>,
|
|
32
35
|
pub(crate) memory: Option<Memory>,
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
impl Exports {
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
/// (the test fixture is a bare module); the host enforces presence at
|
|
39
|
-
/// invocation time. Only the SPEC ABI shapes are accepted —
|
|
40
|
-
/// `__kobako_eval` is `() -> ()`, `__kobako_run` is
|
|
41
|
-
/// `(env_ptr, env_len) -> ()`, `__kobako_take_outcome` is `() -> u64`,
|
|
42
|
-
/// `__kobako_alloc` is `(len) -> ptr`
|
|
43
|
-
/// (docs/wire-codec.md § ABI Signatures).
|
|
39
|
+
/// A missing export is not an error here, because the test fixture is a
|
|
40
|
+
/// bare module; presence is enforced at invocation time.
|
|
44
41
|
pub(crate) fn resolve(instance: &WtInstance, mut ctx: impl AsContextMut) -> Self {
|
|
45
42
|
Self {
|
|
46
|
-
|
|
47
|
-
.get_typed_func::<(),
|
|
48
|
-
.ok(),
|
|
49
|
-
run: instance
|
|
50
|
-
.get_typed_func::<(i32, i32), ()>(&mut ctx, "__kobako_run")
|
|
51
|
-
.ok(),
|
|
52
|
-
take_outcome: instance
|
|
53
|
-
.get_typed_func::<(), u64>(&mut ctx, "__kobako_take_outcome")
|
|
43
|
+
downcall: instance
|
|
44
|
+
.get_typed_func::<(i32, i32), u64>(&mut ctx, "__kobako_downcall")
|
|
54
45
|
.ok(),
|
|
55
46
|
alloc: instance
|
|
56
47
|
.get_typed_func::<u32, u32>(&mut ctx, "__kobako_alloc")
|
|
@@ -58,4 +49,67 @@ impl Exports {
|
|
|
58
49
|
memory: instance.get_memory(&mut ctx, "memory"),
|
|
59
50
|
}
|
|
60
51
|
}
|
|
52
|
+
|
|
53
|
+
pub(crate) fn require_memory(&self) -> Result<Memory, Trap> {
|
|
54
|
+
self.memory
|
|
55
|
+
.ok_or_else(|| Trap::Other(SANDBOX_RUNTIME_NOT_KOBAKO.to_string()))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub(crate) fn require<Params, Results>(
|
|
60
|
+
export: Option<&TypedFunc<Params, Results>>,
|
|
61
|
+
) -> Result<&TypedFunc<Params, Results>, Trap>
|
|
62
|
+
where
|
|
63
|
+
Params: wasmtime::WasmParams,
|
|
64
|
+
Results: wasmtime::WasmResults,
|
|
65
|
+
{
|
|
66
|
+
export.ok_or_else(|| Trap::Other(RUNTIME_INCOMPATIBLE.to_string()))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
//! The ABI is a closed set of guest exports, so the bundled Guest
|
|
72
|
+
//! Binary is read for what it exports rather than only for whether
|
|
73
|
+
//! `resolve` finds what it looks up — an extra entry point would pass
|
|
74
|
+
//! every lookup. A missing binary is a hard failure under
|
|
75
|
+
//! CI (which always builds it) and a silent skip locally.
|
|
76
|
+
use std::path::Path;
|
|
77
|
+
|
|
78
|
+
use wasmtime::ExternType;
|
|
79
|
+
|
|
80
|
+
use crate::cache::cached_module;
|
|
81
|
+
|
|
82
|
+
const WASM: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../data/kobako.wasm");
|
|
83
|
+
|
|
84
|
+
const ABI_EXPORTS: [&str; 3] = [
|
|
85
|
+
"__kobako_abi_version",
|
|
86
|
+
"__kobako_alloc",
|
|
87
|
+
"__kobako_downcall",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
// @behavior RT-068
|
|
91
|
+
#[test]
|
|
92
|
+
fn the_guest_binary_exports_the_abi_functions_and_nothing_else() {
|
|
93
|
+
let path = Path::new(WASM);
|
|
94
|
+
if !path.exists() {
|
|
95
|
+
assert!(
|
|
96
|
+
std::env::var_os("CI").is_none(),
|
|
97
|
+
"data/kobako.wasm missing under CI — run `bundle exec rake wasm:build`"
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let module = cached_module(path).expect("the Guest Binary must load");
|
|
102
|
+
|
|
103
|
+
let mut functions: Vec<&str> = module
|
|
104
|
+
.exports()
|
|
105
|
+
.filter(|export| matches!(export.ty(), ExternType::Func(_)))
|
|
106
|
+
.map(|export| export.name())
|
|
107
|
+
.collect();
|
|
108
|
+
functions.sort_unstable();
|
|
109
|
+
|
|
110
|
+
assert_eq!(
|
|
111
|
+
functions, ABI_EXPORTS,
|
|
112
|
+
"data/kobako.wasm read through its export section must export exactly the ABI's functions"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
61
115
|
}
|