kobako 0.28.0 → 0.29.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 +170 -0
- data/Cargo.lock +111 -82
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +11 -1
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +32 -14
- data/crates/kobako-transport/src/envelope/request.rs +18 -8
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/Cargo.toml +21 -16
- data/crates/kobako-wasmtime/README.md +5 -3
- data/crates/kobako-wasmtime/src/abi.rs +66 -32
- data/crates/kobako-wasmtime/src/ambient.rs +63 -4
- data/crates/kobako-wasmtime/src/cache.rs +185 -74
- data/crates/kobako-wasmtime/src/config.rs +58 -1
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
- data/crates/kobako-wasmtime/src/driver.rs +46 -44
- data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
- data/crates/kobako-wasmtime/src/invocation.rs +35 -46
- data/crates/kobako-wasmtime/src/io.rs +5 -202
- data/crates/kobako-wasmtime/src/lib.rs +4 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +39 -19
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
- data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +7 -1
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +120 -199
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +60 -7
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -3,21 +3,19 @@
|
|
|
3
3
|
//! A request under a tag the host does not serve is answered as
|
|
4
4
|
//! unsupported, so a guest asking for a later operation can fall back.
|
|
5
5
|
//!
|
|
6
|
-
//! A failure lands on whoever is answerable for it
|
|
7
|
-
//! raises during a callback into it ends the invocation
|
|
8
|
-
//!
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
12
|
-
//! since the 0 itself carries no reason. It is the one place the driver
|
|
13
|
-
//! logs, and normal operation never reaches it.
|
|
6
|
+
//! A failure lands on whoever is answerable for it, and always says why.
|
|
7
|
+
//! A trap the guest raises during a callback into it ends the invocation
|
|
8
|
+
//! as that trap. A Call the host could not answer is answered as an
|
|
9
|
+
//! internal failure naming why, and an answer too large for a message as
|
|
10
|
+
//! the Service's own failure. Only an answer that cannot be written into
|
|
11
|
+
//! guest memory at all ends the invocation, as a trap naming why.
|
|
14
12
|
|
|
15
13
|
use wasmtime::Caller;
|
|
16
14
|
|
|
17
15
|
use kobako_runtime::error::Trap;
|
|
18
16
|
use kobako_runtime::yielder::Yielder;
|
|
19
17
|
use kobako_transport::abi::pack_ptr_len;
|
|
20
|
-
use kobako_transport::envelope::{Unsupported, Upcall, YieldReply};
|
|
18
|
+
use kobako_transport::envelope::{Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply};
|
|
21
19
|
|
|
22
20
|
use crate::guest_mem;
|
|
23
21
|
use crate::invocation::Invocation;
|
|
@@ -27,39 +25,58 @@ use crate::invocation::Invocation;
|
|
|
27
25
|
/// stack on the Rust call stack with no shared slot.
|
|
28
26
|
struct CallerYielder<'a, 'c> {
|
|
29
27
|
caller: &'a mut Caller<'c, Invocation>,
|
|
28
|
+
frame: u32,
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
impl Yielder for CallerYielder<'_, '_> {
|
|
33
|
-
fn
|
|
34
|
-
|
|
32
|
+
fn frame(&self) -> u32 {
|
|
33
|
+
self.frame
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap> {
|
|
37
|
+
let bytes = guest_mem::drive_yield(self.caller, frame, args)
|
|
35
38
|
.map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
36
39
|
YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
/// Answer one `__kobako_upcall` call
|
|
41
|
-
/// Reply,
|
|
42
|
-
/// trap a callback into the guest raised.
|
|
43
|
+
/// Answer one `__kobako_upcall` call with the packed `(ptr<<32)|len` of
|
|
44
|
+
/// the Reply, or the trap a callback into the guest raised.
|
|
43
45
|
pub(crate) fn handle(
|
|
44
46
|
caller: &mut Caller<'_, Invocation>,
|
|
45
47
|
req_ptr: i32,
|
|
46
48
|
req_len: i32,
|
|
47
49
|
) -> wasmtime::Result<i64> {
|
|
48
|
-
let
|
|
50
|
+
let reply = reply_to(caller, req_ptr, req_len)
|
|
51
|
+
.unwrap_or_else(|reason| Reply::Fault(Fault::new(FaultKind::Internal, reason)));
|
|
52
|
+
let written = write_reply(caller, &fit_in_message(reply).encode());
|
|
49
53
|
if let Some(trap) = caller.data_mut().take_reentry_trap() {
|
|
50
54
|
return Err(trap);
|
|
51
55
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if let Some(trap) = caller.data().abandonment() {
|
|
57
|
+
return Err(trap);
|
|
58
|
+
}
|
|
59
|
+
written.map_err(wasmtime::Error::msg)
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
/// An answer past the message cap is the Service's to change, as any other
|
|
63
|
+
/// answer the host cannot write is.
|
|
64
|
+
fn fit_in_message(reply: Reply) -> Reply {
|
|
65
|
+
let bytes = reply.encode().len();
|
|
66
|
+
match guest_mem::checked_payload_len(bytes) {
|
|
67
|
+
Ok(_) => reply,
|
|
68
|
+
Err(limit) => Reply::Fault(Fault::new(
|
|
69
|
+
FaultKind::Runtime,
|
|
70
|
+
format!("Sandbox could not write the Service's answer: {limit}"),
|
|
71
|
+
)),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn reply_to(
|
|
59
76
|
caller: &mut Caller<'_, Invocation>,
|
|
60
77
|
req_ptr: i32,
|
|
61
78
|
req_len: i32,
|
|
62
|
-
) -> Result<
|
|
79
|
+
) -> Result<Reply, &'static str> {
|
|
63
80
|
let req_bytes = guest_mem::read_request(caller, req_ptr, req_len)?;
|
|
64
81
|
// The driver decodes the core envelope, here and in `CallerYielder`, so
|
|
65
82
|
// the frontend never sees a frame; the payload inside it stays bytes
|
|
@@ -67,8 +84,9 @@ fn try_handle(
|
|
|
67
84
|
let call = match Upcall::decode(&req_bytes) {
|
|
68
85
|
Ok(Upcall::Call(call)) => call,
|
|
69
86
|
Ok(_) => {
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
return Ok(Reply::Unsupported(Unsupported::new(
|
|
88
|
+
"the host does not serve this request",
|
|
89
|
+
)))
|
|
72
90
|
}
|
|
73
91
|
Err(_) => {
|
|
74
92
|
return Err(
|
|
@@ -89,15 +107,15 @@ fn try_handle(
|
|
|
89
107
|
// handler. The borrow ends with the block, freeing the Caller for
|
|
90
108
|
// `write_reply`; nested dispatch frames each build their own, so
|
|
91
109
|
// the LIFO re-entry lives on the Rust stack — no shared slot.
|
|
110
|
+
let frame = caller.data_mut().open_call();
|
|
92
111
|
let reply = {
|
|
93
|
-
let mut yielder = CallerYielder { caller };
|
|
112
|
+
let mut yielder = CallerYielder { caller, frame };
|
|
94
113
|
handler.dispatch(call, &mut yielder)
|
|
95
|
-
}
|
|
96
|
-
.
|
|
114
|
+
};
|
|
115
|
+
caller.data_mut().close_call();
|
|
116
|
+
reply.ok_or(
|
|
97
117
|
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
write_reply(caller, &reply.encode())
|
|
118
|
+
)
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
fn write_reply(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
|
|
10
10
|
use std::path::Path;
|
|
11
11
|
use std::sync::Arc;
|
|
12
|
-
use std::time::Instant;
|
|
13
12
|
|
|
14
13
|
use wasmtime::{
|
|
15
14
|
AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
|
|
@@ -19,9 +18,11 @@ use crate::cache::shared_engine;
|
|
|
19
18
|
use crate::config::Config;
|
|
20
19
|
use crate::exports::{self, Exports};
|
|
21
20
|
use crate::invocation::Invocation;
|
|
21
|
+
use crate::wasi::Wasi;
|
|
22
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
|
+
use kobako_runtime::grant::{Grant, GrantKind};
|
|
25
26
|
use kobako_runtime::profile::Profile;
|
|
26
27
|
use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
|
|
27
28
|
use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
|
|
@@ -45,15 +46,22 @@ impl Driver {
|
|
|
45
46
|
/// neither leaves the driver.
|
|
46
47
|
pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
|
|
47
48
|
Ok(Self {
|
|
48
|
-
instance_pre: instance_pre::cached_instance_pre(path)?,
|
|
49
|
+
instance_pre: instance_pre::cached_instance_pre(path, config.setup_timeout)?,
|
|
49
50
|
config,
|
|
50
51
|
})
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
/// The deadline and the `wall_time` bracket open with the Store, so
|
|
55
|
+
/// instantiating the guest and writing its request count against the
|
|
56
|
+
/// `timeout` as the export call does.
|
|
57
|
+
fn new_store(&self, wasi: Wasi) -> Result<WtStore<Invocation>, SetupError> {
|
|
58
|
+
let mut store = WtStore::new(
|
|
59
|
+
shared_engine()?,
|
|
60
|
+
Invocation::new(self.config.memory_limit, wasi),
|
|
61
|
+
);
|
|
55
62
|
store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
|
|
56
|
-
store.
|
|
63
|
+
trap::arm_deadline(&mut store, self.config.timeout);
|
|
64
|
+
store.data_mut().start_wall_clock();
|
|
57
65
|
Ok(store)
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -63,7 +71,11 @@ impl Driver {
|
|
|
63
71
|
let instance = self
|
|
64
72
|
.instance_pre
|
|
65
73
|
.instantiate(store.as_context_mut())
|
|
66
|
-
.map_err(|e|
|
|
74
|
+
.map_err(|e| {
|
|
75
|
+
trap::cap_trap(&e).unwrap_or_else(|| {
|
|
76
|
+
Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}"))
|
|
77
|
+
})
|
|
78
|
+
})?;
|
|
67
79
|
let exports = Exports::resolve(&instance, store.as_context_mut());
|
|
68
80
|
store.data_mut().set_exports(exports.clone());
|
|
69
81
|
Ok(exports)
|
|
@@ -83,58 +95,41 @@ impl Driver {
|
|
|
83
95
|
Params: wasmtime::WasmParams,
|
|
84
96
|
Results: wasmtime::WasmResults,
|
|
85
97
|
{
|
|
86
|
-
self.
|
|
98
|
+
self.arm_memory_cap(store, exports);
|
|
87
99
|
let result = export.call(store.as_context_mut(), params);
|
|
88
100
|
disarm_caps(store);
|
|
89
101
|
result
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
/// The pre-initialized image's allocation is folded into the memory
|
|
93
|
-
/// baseline rather than the budget
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Some(timeout) => {
|
|
97
|
-
let deadline = Instant::now() + timeout;
|
|
98
|
-
store.data_mut().set_deadline(Some(deadline));
|
|
99
|
-
store.set_epoch_deadline(1);
|
|
100
|
-
}
|
|
101
|
-
None => {
|
|
102
|
-
store.data_mut().set_deadline(None);
|
|
103
|
-
store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
105
|
+
/// baseline rather than the budget, so the cap arms only once the
|
|
106
|
+
/// image is in place.
|
|
107
|
+
fn arm_memory_cap(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
|
|
106
108
|
let baseline = match exports.memory {
|
|
107
109
|
Some(m) => m.data_size(store.as_context_mut()),
|
|
108
110
|
None => 0,
|
|
109
111
|
};
|
|
110
112
|
store.data_mut().arm_memory_cap(baseline);
|
|
111
|
-
store.data_mut().start_wall_clock();
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/// Built the same for every `completion`: captures and usage must
|
|
115
116
|
/// survive a trap just as they do an outcome.
|
|
116
117
|
fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
|
|
117
118
|
let data = store.data();
|
|
118
|
-
let usage = Usage
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
let mut usage = Usage::default();
|
|
120
|
+
usage.wall_time = data.wall_time().as_secs_f64();
|
|
121
|
+
usage.memory_peak = data.memory_peak();
|
|
122
|
+
let clip = |bytes, limit| {
|
|
123
|
+
let mut capture = Capture::default();
|
|
124
|
+
(capture.bytes, capture.truncated) = capture::clip_capture(bytes, limit);
|
|
125
|
+
capture
|
|
121
126
|
};
|
|
122
|
-
|
|
123
|
-
capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit);
|
|
124
|
-
let (stderr_bytes, stderr_truncated) =
|
|
125
|
-
capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit);
|
|
126
|
-
Snapshot {
|
|
127
|
+
Snapshot::new(
|
|
127
128
|
completion,
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
truncated: stdout_truncated,
|
|
131
|
-
},
|
|
132
|
-
stderr: Capture {
|
|
133
|
-
bytes: stderr_bytes,
|
|
134
|
-
truncated: stderr_truncated,
|
|
135
|
-
},
|
|
129
|
+
clip(data.wasi().stdout_bytes(), self.config.stdout_limit),
|
|
130
|
+
clip(data.wasi().stderr_bytes(), self.config.stderr_limit),
|
|
136
131
|
usage,
|
|
137
|
-
|
|
132
|
+
)
|
|
138
133
|
}
|
|
139
134
|
}
|
|
140
135
|
|
|
@@ -148,12 +143,13 @@ impl ContractRuntime for Driver {
|
|
|
148
143
|
entry: Entry<'_>,
|
|
149
144
|
preamble: &Preamble,
|
|
150
145
|
handler: Option<Arc<dyn DispatchHandler>>,
|
|
146
|
+
grants: &[Grant],
|
|
151
147
|
) -> Result<Snapshot, InvokeError> {
|
|
152
|
-
let
|
|
148
|
+
let wasi = Wasi::for_invocation(&self.config, &preamble.encode(), grants)?;
|
|
149
|
+
let mut store = self.new_store(wasi)?;
|
|
153
150
|
if let Some(handler) = handler {
|
|
154
151
|
store.data_mut().bind_on_dispatch(handler);
|
|
155
152
|
}
|
|
156
|
-
io::install_wasi(&mut store, &self.config, &preamble.encode())?;
|
|
157
153
|
let exports = self.instantiate(&mut store)?;
|
|
158
154
|
let downcall = exports::require(exports.downcall.as_ref())?;
|
|
159
155
|
let request = match entry {
|
|
@@ -174,24 +170,30 @@ impl ContractRuntime for Driver {
|
|
|
174
170
|
Ok(outcome) => Completion::Outcome(outcome),
|
|
175
171
|
Err(t) => Completion::Trap(t),
|
|
176
172
|
},
|
|
177
|
-
Err(e) => Completion::Trap(trap::
|
|
173
|
+
Err(e) => Completion::Trap(trap::to_trap(e)),
|
|
178
174
|
};
|
|
179
175
|
Ok(self.build_snapshot(&store, completion))
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
/// Exactly the rung `Config` requested. `Hermetic` freezes ambient time
|
|
183
|
-
/// and entropy; both rungs wire no filesystem, environment, or network,
|
|
179
|
+
/// and entropy; both rungs wire no ambient filesystem, environment, or network,
|
|
184
180
|
/// and the linker adds only `__kobako_upcall` beyond that WASI surface.
|
|
185
181
|
fn profile(&self) -> Profile {
|
|
186
182
|
self.config.profile
|
|
187
183
|
}
|
|
188
184
|
|
|
185
|
+
/// A Mount becomes a preopen of kobako's own descriptor table, which
|
|
186
|
+
/// answers every preview1 call beneath it.
|
|
187
|
+
fn supports(&self, kind: GrantKind) -> bool {
|
|
188
|
+
matches!(kind, GrantKind::Mount)
|
|
189
|
+
}
|
|
190
|
+
|
|
189
191
|
/// Bounded by the same caps as an invocation, on an instance of its
|
|
190
192
|
/// own that no script ever runs on, so what it answers carries
|
|
191
193
|
/// nothing a script could have written.
|
|
192
194
|
fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
|
|
193
|
-
let
|
|
194
|
-
|
|
195
|
+
let wasi = Wasi::for_compile(&self.config, &preamble.encode()).ok()?;
|
|
196
|
+
let mut store = self.new_store(wasi).ok()?;
|
|
195
197
|
let exports = self.instantiate(&mut store).ok()?;
|
|
196
198
|
let downcall = exports::require(exports.downcall.as_ref()).ok()?;
|
|
197
199
|
let (ptr, len) =
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
//! Guest linear-memory I/O for
|
|
2
|
-
//!
|
|
3
|
-
//! drives
|
|
1
|
+
//! Guest linear-memory I/O for the ABI's buffer handoffs, whether the
|
|
2
|
+
//! invocation body drives one through its `Store` or a dispatch or yield
|
|
3
|
+
//! import drives one through its `Caller`. preview1 reads and writes its
|
|
4
|
+
//! arguments through its own `GuestMemory`.
|
|
4
5
|
//!
|
|
5
6
|
//! The two shared steps answer an error kind, and each caller words it,
|
|
6
7
|
//! because depth decides what the same problem means: before the export
|
|
@@ -92,7 +93,7 @@ fn callback_exports(caller: &Caller<'_, Invocation>) -> Result<Exports, &'static
|
|
|
92
93
|
|
|
93
94
|
/// Keep a trap the guest raised during a callback into it for the dispatch
|
|
94
95
|
/// import to end the invocation with, and answer the callback's own reason.
|
|
95
|
-
fn
|
|
96
|
+
fn keep_trap(
|
|
96
97
|
caller: &mut Caller<'_, Invocation>,
|
|
97
98
|
trap: wasmtime::Error,
|
|
98
99
|
reason: &'static str,
|
|
@@ -136,7 +137,7 @@ pub(crate) fn write_from_callback(
|
|
|
136
137
|
alloc_and_write(&mut *caller, &exports, bytes).map_err(|err| match err {
|
|
137
138
|
WriteError::TooLarge => "payload exceeds the 16 MiB limit",
|
|
138
139
|
WriteError::Incompatible(message) => message,
|
|
139
|
-
WriteError::Trapped(trap) =>
|
|
140
|
+
WriteError::Trapped(trap) => keep_trap(
|
|
140
141
|
caller,
|
|
141
142
|
trap,
|
|
142
143
|
"the Sandbox trapped while allocating memory for the request",
|
|
@@ -177,9 +178,14 @@ pub(crate) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
|
|
|
177
178
|
|
|
178
179
|
pub(crate) fn drive_yield(
|
|
179
180
|
caller: &mut Caller<'_, Invocation>,
|
|
181
|
+
frame: u32,
|
|
180
182
|
args: &[u8],
|
|
181
183
|
) -> Result<Vec<u8>, &'static str> {
|
|
182
|
-
let request = Downcall::Yield
|
|
184
|
+
let request = Downcall::Yield {
|
|
185
|
+
frame,
|
|
186
|
+
args: args.to_vec(),
|
|
187
|
+
}
|
|
188
|
+
.encode();
|
|
183
189
|
let len_i32 = checked_payload_len(request.len())?;
|
|
184
190
|
let req_ptr = write_from_callback(caller, &request)? as i32;
|
|
185
191
|
|
|
@@ -188,7 +194,7 @@ pub(crate) fn drive_yield(
|
|
|
188
194
|
let memory = exports.memory.ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
|
|
189
195
|
let packed = yield_fn
|
|
190
196
|
.call(&mut *caller, (req_ptr, len_i32))
|
|
191
|
-
.map_err(|trap|
|
|
197
|
+
.map_err(|trap| keep_trap(caller, trap, "the Sandbox trapped while invoking a block"))?;
|
|
192
198
|
let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
|
|
193
199
|
if reply_len == 0 {
|
|
194
200
|
return Err("the Sandbox returned an empty block result");
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//! The host imports a Guest Binary resolves: the WASI preview1 set and
|
|
2
|
+
//! the wire ABI's `__kobako_upcall`.
|
|
3
|
+
//!
|
|
4
|
+
//! Both closures read all their state from the `Invocation` inside the
|
|
5
|
+
//! calling Store, never from a Runtime, so the wiring is the same for
|
|
6
|
+
//! every Sandbox and `crate::instance_pre` can cache what it resolves.
|
|
7
|
+
|
|
8
|
+
use wasmtime::{Caller, Linker};
|
|
9
|
+
use wasmtime_wasi::p1;
|
|
10
|
+
|
|
11
|
+
use crate::cache::shared_engine;
|
|
12
|
+
use crate::invocation::Invocation;
|
|
13
|
+
use crate::{dispatch, preview1};
|
|
14
|
+
use kobako_runtime::error::SetupError;
|
|
15
|
+
|
|
16
|
+
pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
|
|
17
|
+
let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
|
|
18
|
+
|
|
19
|
+
// Wire the wasmtime-wasi preview1 WASI imports against each
|
|
20
|
+
// invocation's own `Wasi` context, then answer every call that reads
|
|
21
|
+
// a descriptor from kobako's own table, which routes guest fd 1/2 to
|
|
22
|
+
// its capture pipes.
|
|
23
|
+
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| {
|
|
24
|
+
state.wasi_mut().ctx_mut()
|
|
25
|
+
})
|
|
26
|
+
.and_then(|_| preview1::shadow(&mut linker))
|
|
27
|
+
.map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
|
|
28
|
+
|
|
29
|
+
// `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
|
|
30
|
+
// docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
|
|
31
|
+
// answers.
|
|
32
|
+
linker
|
|
33
|
+
.func_wrap(
|
|
34
|
+
"env",
|
|
35
|
+
"__kobako_upcall",
|
|
36
|
+
|mut caller: Caller<'_, Invocation>,
|
|
37
|
+
req_ptr: i32,
|
|
38
|
+
req_len: i32|
|
|
39
|
+
-> wasmtime::Result<i64> {
|
|
40
|
+
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
.map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
|
|
44
|
+
|
|
45
|
+
Ok(linker)
|
|
46
|
+
}
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
//! Per-path cache of pre-instantiated wasmtime artifacts.
|
|
2
2
|
//!
|
|
3
|
-
//! The
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//! the `
|
|
8
|
-
//! Caching the resolved `InstancePre` per path — together with the ABI
|
|
9
|
-
//! version check it guards (`crate::abi`) — takes both off the
|
|
10
|
-
//! `Driver::new` hot path.
|
|
3
|
+
//! The host imports (`crate::imports`) and their type-check against the
|
|
4
|
+
//! compiled Module are identical for every `Kobako::Runtime` on the same
|
|
5
|
+
//! Guest Binary. Caching the resolved `InstancePre` per path — together
|
|
6
|
+
//! with the ABI version check it guards (`crate::abi`) — takes both off
|
|
7
|
+
//! the `Driver::new` hot path.
|
|
11
8
|
//!
|
|
12
9
|
//! Concurrency: the same as `crate::cache` — the Mutex covers lookup and
|
|
13
10
|
//! insert only, and a concurrent miss builds an equivalent `InstancePre`.
|
|
@@ -15,19 +12,24 @@
|
|
|
15
12
|
use std::collections::HashMap;
|
|
16
13
|
use std::path::{Path, PathBuf};
|
|
17
14
|
use std::sync::{Mutex, OnceLock};
|
|
15
|
+
use std::time::Duration;
|
|
18
16
|
|
|
19
|
-
use wasmtime::
|
|
20
|
-
use wasmtime_wasi::p1;
|
|
17
|
+
use wasmtime::InstancePre;
|
|
21
18
|
|
|
22
|
-
use crate::cache::
|
|
19
|
+
use crate::cache::cached_module;
|
|
23
20
|
use crate::invocation::Invocation;
|
|
24
|
-
use crate::{abi,
|
|
21
|
+
use crate::{abi, imports, trap};
|
|
25
22
|
use kobako_runtime::error::SetupError;
|
|
26
23
|
|
|
27
24
|
static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
|
|
28
25
|
OnceLock::new();
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
/// The probe runs only on the first load of a path, so only that load's
|
|
28
|
+
/// `setup_timeout` bounds it; a path already verified is not set up again.
|
|
29
|
+
pub(crate) fn cached_instance_pre(
|
|
30
|
+
path: &Path,
|
|
31
|
+
setup_timeout: Option<Duration>,
|
|
32
|
+
) -> Result<InstancePre<Invocation>, SetupError> {
|
|
31
33
|
let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
32
34
|
|
|
33
35
|
if let Some(pre) = cache
|
|
@@ -40,46 +42,16 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
let module = cached_module(path)?;
|
|
43
|
-
let linker =
|
|
45
|
+
let linker = imports::linker()?;
|
|
44
46
|
let pre = linker
|
|
45
47
|
.instantiate_pre(&module)
|
|
46
48
|
.map_err(trap::instantiate_err)?;
|
|
47
49
|
// Verify before the insert, so a rejected artifact is never
|
|
48
50
|
// remembered as a usable template.
|
|
49
|
-
abi::verify(&pre)?;
|
|
51
|
+
abi::verify(&pre, setup_timeout)?;
|
|
50
52
|
cache
|
|
51
53
|
.lock()
|
|
52
54
|
.expect("instance_pre cache mutex poisoned")
|
|
53
55
|
.insert(path.to_path_buf(), pre.clone());
|
|
54
56
|
Ok(pre)
|
|
55
57
|
}
|
|
56
|
-
|
|
57
|
-
fn build_linker() -> Result<Linker<Invocation>, SetupError> {
|
|
58
|
-
let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
|
|
59
|
-
|
|
60
|
-
// Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
|
|
61
|
-
// to the MemoryOutputPipes set up before each run via
|
|
62
|
-
// `Driver::invoke`. The closure pulls a `&mut WasiP1Ctx` out of
|
|
63
|
-
// Invocation; the panic semantics live inside `Invocation::wasi_mut`
|
|
64
|
-
// so the wiring stays honest about its precondition.
|
|
65
|
-
p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
|
|
66
|
-
.map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
|
|
67
|
-
|
|
68
|
-
// `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
|
|
69
|
-
// docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
|
|
70
|
-
// answers.
|
|
71
|
-
linker
|
|
72
|
-
.func_wrap(
|
|
73
|
-
"env",
|
|
74
|
-
"__kobako_upcall",
|
|
75
|
-
|mut caller: Caller<'_, Invocation>,
|
|
76
|
-
req_ptr: i32,
|
|
77
|
-
req_len: i32|
|
|
78
|
-
-> wasmtime::Result<i64> {
|
|
79
|
-
dispatch::handle(&mut caller, req_ptr, req_len)
|
|
80
|
-
},
|
|
81
|
-
)
|
|
82
|
-
.map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
|
|
83
|
-
|
|
84
|
-
Ok(linker)
|
|
85
|
-
}
|
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
//! Owned as the data of each per-invocation `wasmtime::Store`
|
|
5
5
|
//! and threaded through every host import —
|
|
6
6
|
//! the `__kobako_upcall` dispatcher reads the bound dispatch handler,
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//! export call.
|
|
7
|
+
//! and the WASI imports read the invocation's `Wasi` context, which the
|
|
8
|
+
//! Store is created with.
|
|
10
9
|
//!
|
|
11
10
|
//! The slot also carries the per-invocation wall-clock deadline and the
|
|
12
11
|
//! memory cap `MemoryLimiter`, both read from the wasmtime
|
|
@@ -16,28 +15,22 @@
|
|
|
16
15
|
use std::sync::Arc;
|
|
17
16
|
use std::time::{Duration, Instant};
|
|
18
17
|
|
|
19
|
-
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
20
|
-
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
|
|
21
|
-
|
|
22
18
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
23
19
|
|
|
24
20
|
use crate::exports::Exports;
|
|
25
21
|
use crate::limiter::MemoryLimiter;
|
|
22
|
+
use crate::wasi::Wasi;
|
|
26
23
|
|
|
27
24
|
/// Per-invocation host state, owned by the invocation's own Store.
|
|
28
25
|
/// Threaded through every host import callback.
|
|
29
26
|
///
|
|
30
|
-
/// All field access is mediated by methods on this type — the
|
|
31
|
-
///
|
|
32
|
-
/// `Invocation::
|
|
33
|
-
/// `Invocation::
|
|
34
|
-
///
|
|
35
|
-
/// `Invocation::stderr_bytes`. The fields are private so the mutation
|
|
36
|
-
/// surface stays narrow.
|
|
27
|
+
/// All field access is mediated by methods on this type — the `Wasi`
|
|
28
|
+
/// context arrives with the invocation, the dispatch handler is set once
|
|
29
|
+
/// via `Invocation::bind_on_dispatch`, and the captures are read back
|
|
30
|
+
/// through `Invocation::wasi` after the run. The fields are private so
|
|
31
|
+
/// the mutation surface stays narrow.
|
|
37
32
|
pub(crate) struct Invocation {
|
|
38
|
-
wasi:
|
|
39
|
-
stdout_pipe: Option<MemoryOutputPipe>,
|
|
40
|
-
stderr_pipe: Option<MemoryOutputPipe>,
|
|
33
|
+
wasi: Wasi,
|
|
41
34
|
on_dispatch: Option<Arc<dyn DispatchHandler>>,
|
|
42
35
|
deadline: Option<Instant>,
|
|
43
36
|
limiter: MemoryLimiter,
|
|
@@ -45,14 +38,13 @@ pub(crate) struct Invocation {
|
|
|
45
38
|
wall_time: Duration,
|
|
46
39
|
reentry_trap: Option<wasmtime::Error>,
|
|
47
40
|
exports: Option<Exports>,
|
|
41
|
+
open_calls: u32,
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
impl Invocation {
|
|
51
|
-
pub(crate) fn new(memory_limit: Option<usize
|
|
45
|
+
pub(crate) fn new(memory_limit: Option<usize>, wasi: Wasi) -> Self {
|
|
52
46
|
Self {
|
|
53
|
-
wasi
|
|
54
|
-
stdout_pipe: None,
|
|
55
|
-
stderr_pipe: None,
|
|
47
|
+
wasi,
|
|
56
48
|
on_dispatch: None,
|
|
57
49
|
deadline: None,
|
|
58
50
|
limiter: MemoryLimiter::new(memory_limit),
|
|
@@ -60,6 +52,7 @@ impl Invocation {
|
|
|
60
52
|
wall_time: Duration::ZERO,
|
|
61
53
|
reentry_trap: None,
|
|
62
54
|
exports: None,
|
|
55
|
+
open_calls: 0,
|
|
63
56
|
}
|
|
64
57
|
}
|
|
65
58
|
|
|
@@ -89,45 +82,41 @@ impl Invocation {
|
|
|
89
82
|
self.reentry_trap.take()
|
|
90
83
|
}
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
self.
|
|
85
|
+
/// Count a Call the host is about to answer and name its frame, as the
|
|
86
|
+
/// guest counts the Calls it has open.
|
|
87
|
+
pub(crate) fn open_call(&mut self) -> u32 {
|
|
88
|
+
self.open_calls += 1;
|
|
89
|
+
self.open_calls - 1
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pub(crate) fn close_call(&mut self) {
|
|
93
|
+
self.open_calls -= 1;
|
|
101
94
|
}
|
|
102
95
|
|
|
103
96
|
pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
|
|
104
97
|
self.on_dispatch = Some(handler);
|
|
105
98
|
}
|
|
106
99
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.unwrap_or_default()
|
|
100
|
+
/// A clone, so the borrow on the `Caller` is released and the
|
|
101
|
+
/// dispatcher can re-borrow it to write the Reply.
|
|
102
|
+
pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
103
|
+
self.on_dispatch.clone()
|
|
112
104
|
}
|
|
113
105
|
|
|
114
|
-
|
|
115
|
-
|
|
106
|
+
/// The trap ending an invocation its host gave up on.
|
|
107
|
+
pub(crate) fn abandonment(&self) -> Option<wasmtime::Error> {
|
|
108
|
+
self.on_dispatch
|
|
116
109
|
.as_ref()
|
|
117
|
-
.
|
|
118
|
-
.
|
|
110
|
+
.is_some_and(|handler| handler.abandoned())
|
|
111
|
+
.then(|| wasmtime::Error::msg("the host abandoned the invocation"))
|
|
119
112
|
}
|
|
120
113
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
124
|
-
self.on_dispatch.clone()
|
|
114
|
+
pub(crate) fn wasi(&self) -> &Wasi {
|
|
115
|
+
&self.wasi
|
|
125
116
|
}
|
|
126
117
|
|
|
127
|
-
pub(crate) fn wasi_mut(&mut self) -> &mut
|
|
128
|
-
self.wasi
|
|
129
|
-
.as_mut()
|
|
130
|
-
.expect("WASI context not initialised — the driver must install it before any WASI use")
|
|
118
|
+
pub(crate) fn wasi_mut(&mut self) -> &mut Wasi {
|
|
119
|
+
&mut self.wasi
|
|
131
120
|
}
|
|
132
121
|
|
|
133
122
|
pub(crate) fn set_deadline(&mut self, deadline: Option<Instant>) {
|