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
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//! A receiver saying it does not serve the operation it was sent.
|
|
2
|
+
//!
|
|
3
|
+
//! It is an answer rather than a breakage, so it rides every operation's
|
|
4
|
+
//! answer as its own arm and the caller decides whether to fall back or
|
|
5
|
+
//! give up. It names no operation: the caller knows what it sent.
|
|
6
|
+
|
|
7
|
+
use super::bytes::{Reader, Writer};
|
|
8
|
+
use super::{tag, DecodeError};
|
|
9
|
+
|
|
10
|
+
/// The only kind defined. A kind this reader predates reads as this one,
|
|
11
|
+
/// so a later kind still delivers its message.
|
|
12
|
+
const KIND_UNSUPPORTED: u8 = 0;
|
|
13
|
+
|
|
14
|
+
/// The refusal a receiver answers with when it does not serve an
|
|
15
|
+
/// operation.
|
|
16
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
17
|
+
pub struct Unsupported {
|
|
18
|
+
pub message: String,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl Unsupported {
|
|
22
|
+
pub fn new(message: impl Into<String>) -> Self {
|
|
23
|
+
Unsupported {
|
|
24
|
+
message: message.into(),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// This refusal as a whole answer. Every operation's answer spells
|
|
29
|
+
/// the arm the same way, so a receiver refusing a tag it cannot
|
|
30
|
+
/// read needs no answer type to refuse it in.
|
|
31
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
32
|
+
let mut writer = Writer::new();
|
|
33
|
+
writer
|
|
34
|
+
.u8(tag::UNSUPPORTED)
|
|
35
|
+
.u8(KIND_UNSUPPORTED)
|
|
36
|
+
.bytes(self.message.as_bytes());
|
|
37
|
+
writer.into_bytes()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub(crate) fn read(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
|
|
41
|
+
reader.u8()?;
|
|
42
|
+
let message = reader.text()?.to_owned();
|
|
43
|
+
// The last thing its answer carries, so whatever follows belongs
|
|
44
|
+
// to a later version of these fields, as for a Fault.
|
|
45
|
+
reader.skip_unknown_fields();
|
|
46
|
+
Ok(Unsupported { message })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[cfg(test)]
|
|
51
|
+
mod tests {
|
|
52
|
+
use super::super::{CompileReply, Outcome, Reply, YieldReply};
|
|
53
|
+
use super::*;
|
|
54
|
+
|
|
55
|
+
// @behavior WE-069
|
|
56
|
+
#[test]
|
|
57
|
+
fn an_unsupported_answer_round_trips_on_every_operation() {
|
|
58
|
+
let refusal = Unsupported::new("not served");
|
|
59
|
+
assert_eq!(
|
|
60
|
+
refusal.encode(),
|
|
61
|
+
Reply::Unsupported(refusal.clone()).encode(),
|
|
62
|
+
"an Unsupported encoded on its own must be the same answer every operation carries"
|
|
63
|
+
);
|
|
64
|
+
assert_eq!(
|
|
65
|
+
(
|
|
66
|
+
Reply::decode(&Reply::Unsupported(refusal.clone()).encode()),
|
|
67
|
+
YieldReply::decode(&YieldReply::Unsupported(refusal.clone()).encode()),
|
|
68
|
+
CompileReply::decode(&CompileReply::Unsupported(refusal.clone()).encode()),
|
|
69
|
+
Outcome::decode(&Outcome::Unsupported(refusal.clone()).encode()),
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
Ok(Reply::Unsupported(refusal.clone())),
|
|
73
|
+
Ok(YieldReply::Unsupported(refusal.clone())),
|
|
74
|
+
Ok(CompileReply::Unsupported(refusal.clone())),
|
|
75
|
+
Ok(Outcome::Unsupported(refusal)),
|
|
76
|
+
),
|
|
77
|
+
"an Unsupported through each answer's encode and decode must arrive as it was \
|
|
78
|
+
given"
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// @behavior WE-070
|
|
83
|
+
#[test]
|
|
84
|
+
fn a_kind_this_reader_predates_still_delivers_its_message() {
|
|
85
|
+
let bytes = [0x15, 9, 0, 0, 0, 1, b'm'];
|
|
86
|
+
assert_eq!(
|
|
87
|
+
Reply::decode(&bytes),
|
|
88
|
+
Ok(Reply::Unsupported(Unsupported::new("m"))),
|
|
89
|
+
"an Unsupported of an unknown kind through Reply::decode must read as the one \
|
|
90
|
+
kind defined, its message intact"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// @behavior WE-090
|
|
95
|
+
#[test]
|
|
96
|
+
fn a_field_this_reader_predates_is_skipped() {
|
|
97
|
+
let bytes = [0x15, 0, 0, 0, 0, 1, b'm', 0xff];
|
|
98
|
+
assert_eq!(
|
|
99
|
+
Reply::decode(&bytes),
|
|
100
|
+
Ok(Reply::Unsupported(Unsupported::new("m"))),
|
|
101
|
+
"an Unsupported carrying a trailing field through Reply::decode must skip it, \
|
|
102
|
+
its message intact"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -6,40 +6,47 @@
|
|
|
6
6
|
//! tier that only routes messages therefore needs no payload codec at all,
|
|
7
7
|
//! and the one an assembly does use is its own choice.
|
|
8
8
|
//!
|
|
9
|
-
//! A decoded
|
|
10
|
-
//! its reader as a view rather than a copy.
|
|
11
|
-
//!
|
|
12
|
-
//! from a frame at boot and consumed later — are copied at decode.
|
|
9
|
+
//! A decoded Call or Upcall borrows the buffer it came from, so a dispatched
|
|
10
|
+
//! payload reaches its reader as a view rather than a copy. Every other
|
|
11
|
+
//! message copies what it holds at decode.
|
|
13
12
|
//!
|
|
14
|
-
//!
|
|
15
|
-
//!
|
|
13
|
+
//! Every answer is exhaustive. Each arm is a tag byte the layout document
|
|
14
|
+
//! fixes, so an answer's set grows only with an ABI version — and when it
|
|
16
15
|
//! does, a peer that has not been taught the new arm should fail to
|
|
17
|
-
//! compile rather than fall into a wildcard and answer a message it
|
|
18
|
-
//! read.
|
|
16
|
+
//! compile rather than fall into a wildcard and answer a message it
|
|
17
|
+
//! cannot read. Three kinds of type are the exception, because a reader
|
|
18
|
+
//! answers what it predates instead of refusing it: a request reads an
|
|
19
|
+
//! unassigned tag as unknown, a Fault's category degrades, and a
|
|
20
|
+
//! Preamble skips an advisory record of a kind it does not know.
|
|
19
21
|
//!
|
|
20
22
|
//! This layer has one implementation, so its `golden_layout_*` tests are
|
|
21
23
|
//! what hold it to the layout document. They spell each tag as the literal
|
|
22
24
|
//! byte that document fixes, never as the constant beside it: a golden
|
|
23
25
|
//! written from the constant compares the implementation to itself.
|
|
24
|
-
//!
|
|
25
|
-
//! [core envelope]: ../../../docs/wire/envelope.md
|
|
26
26
|
|
|
27
27
|
pub(crate) mod bytes;
|
|
28
28
|
pub mod call;
|
|
29
|
+
pub mod compile_reply;
|
|
29
30
|
pub mod error_record;
|
|
30
31
|
pub mod fault;
|
|
31
|
-
pub mod invocation_frames;
|
|
32
32
|
pub mod outcome;
|
|
33
|
+
pub mod preamble;
|
|
33
34
|
pub mod reply;
|
|
35
|
+
pub mod request;
|
|
34
36
|
pub mod run;
|
|
37
|
+
pub(crate) mod tag;
|
|
38
|
+
pub mod unsupported;
|
|
35
39
|
|
|
36
40
|
pub use call::{Call, Target};
|
|
41
|
+
pub use compile_reply::CompileReply;
|
|
37
42
|
pub use error_record::ErrorRecord;
|
|
38
43
|
pub use fault::{Fault, FaultKind};
|
|
39
|
-
pub use invocation_frames::{Bindings, Snippet, Snippets};
|
|
40
44
|
pub use outcome::{Origin, Outcome, Panic};
|
|
45
|
+
pub use preamble::{Bindings, Flag, Preamble, Snippet, Snippets};
|
|
41
46
|
pub use reply::{Reply, YieldReply};
|
|
47
|
+
pub use request::{Downcall, UnknownTag, Upcall};
|
|
42
48
|
pub use run::Run;
|
|
49
|
+
pub use unsupported::Unsupported;
|
|
43
50
|
|
|
44
51
|
use std::fmt;
|
|
45
52
|
|
|
@@ -14,8 +14,6 @@
|
|
|
14
14
|
//! interpreter, or a schema — and nothing may come to: a tier every other
|
|
15
15
|
//! tier composes against cannot carry a choice any of them might want to
|
|
16
16
|
//! make differently.
|
|
17
|
-
//!
|
|
18
|
-
//! [core envelope]: ../../../docs/wire/envelope.md
|
|
19
17
|
|
|
20
18
|
pub mod abi;
|
|
21
19
|
pub mod envelope;
|
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.17.0...kobako-wasmtime-v0.18.0) (2026-10-13)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* the kobako and kobako-wasmtime crates declare a minimum supported Rust version of 1.96.
|
|
9
|
+
* **transport:** the stdin layout a Guest Binary reads changed within ABI 4. kobako_transport::envelope::Frames is replaced by Preamble, which is #[non_exhaustive] and built from Default; Bindings and Snippets no longer have their own decode and encode. Guest::eval and Guest::run, MrbGuest::eval and MrbGuest::run, and Runtime::invoke take a Preamble in place of Frames. kobako_core re-exports Preamble and Flag in place of Frames.
|
|
10
|
+
* **transport:** the ABI version is 4, so a Guest Binary built for ABI 3 is refused at Sandbox construction. kobako_core::export_guest! emits __kobako_downcall in place of __kobako_eval, __kobako_run, __kobako_yield_to_block and __kobako_take_outcome, and the host import is __kobako_upcall. kobako_core::abi::{take_outcome, write_outcome, write_reply, decode_run} are replaced by serve and answer, and the frames module is no longer public. Reply, YieldReply and Outcome gain an Unsupported arm and take their tags from the shared registry, and kobako_core::DispatchError and kobako::YieldError gain an Unsupported arm. A yield to a guest without block support raises Kobako::UnsupportedError at the Service's yield instead of failing the invocation as a trap.
|
|
11
|
+
* **runtime:** Runtime::invoke takes &kobako_transport::envelope::Frames in place of kobako_runtime::Frames, whose byte fields are gone. Guest::eval and Guest::run (and their MrbGuest flows) receive the decoded Frames, eval also its source; kobako_core::frames::read_frame is replaced by read_eval_frames and read_frames.
|
|
12
|
+
* **runtime:** kobako_runtime::snapshot::Completion::Outcome holds a decoded kobako_transport::envelope::Outcome instead of Vec<u8>; an engine decodes the result itself and reports an unframed one as
|
|
13
|
+
* **runtime:** kobako_runtime::runtime::Entry::Run holds { entrypoint: &str, payload: Vec<u8> } instead of { envelope: &[u8] }; a frontend passes the fields, and an engine encodes the Run envelope itself.
|
|
14
|
+
* **runtime:** kobako_runtime::yielder::Yielder::yield_to_block returns Result<YieldReply, Trap> instead of the encoded bytes; an engine implementing it decodes the reply itself.
|
|
15
|
+
* a memory budget or deadline reached during a Service answer or a yielded block now raises Kobako::MemoryLimitError or
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **guest:** raise Kobako::UnsupportedError when a host does not serve a call ([db003af](https://github.com/elct9620/kobako/commit/db003af75cf2d11b5f644986aa77c0408f5f59ba)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
20
|
+
* **runtime:** compile a Preamble's snippets on an instance of their own ([303432d](https://github.com/elct9620/kobako/commit/303432dd90ed62dc378391515e68e784e1b473a4)), closes [#75](https://github.com/elct9620/kobako/issues/75)
|
|
21
|
+
* **transport:** carry each operation as a tag on one channel per direction ([c7d574b](https://github.com/elct9620/kobako/commit/c7d574b59a42e58632f2bdc46cc9a8e4c7ac6121)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
22
|
+
* **transport:** keep an unknown request's body, and refuse tag 0x00 ([edb64df](https://github.com/elct9620/kobako/commit/edb64df8f87694d17c5b6fd71ac63329023306ec)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
23
|
+
* **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Bug Fixes
|
|
27
|
+
|
|
28
|
+
* end the invocation as the trap a host callback into the guest hits ([b8f47d3](https://github.com/elct9620/kobako/commit/b8f47d3699a19adcfd5596af93136401a286c761))
|
|
29
|
+
* keep a compile's bytecode only when the Preamble still fits it ([765db08](https://github.com/elct9620/kobako/commit/765db08ad31c8f5163794c7e144355f3237af93b))
|
|
30
|
+
* **wasmtime:** keep a compile answer only when it covers every source snippet ([93e9ddd](https://github.com/elct9620/kobako/commit/93e9ddd79f714446ae713a3145c59d3d2f4145dc))
|
|
31
|
+
* **wasmtime:** tell every frontend how to recover, not only the gem ([cab805a](https://github.com/elct9620/kobako/commit/cab805ab62441a0ca2c78775b7a0d7ce9cc05c43))
|
|
32
|
+
* **wasmtime:** word a missing or mistyped export one way ([8ab0aee](https://github.com/elct9620/kobako/commit/8ab0aeec8bda2960b6cab0502ef51860ea9093b8))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Code Refactoring
|
|
36
|
+
|
|
37
|
+
* **runtime:** hand a frontend the decoded Outcome ([0da6aac](https://github.com/elct9620/kobako/commit/0da6aac0f7943d694a5a4ab9439b2d6c527ae1b0)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
38
|
+
* **runtime:** hand a frontend the decoded Yield Reply ([e80ab54](https://github.com/elct9620/kobako/commit/e80ab54e36c2a80b0f746c675f85bd1238fd4331)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
39
|
+
* **runtime:** let the ABI endpoints frame and read the Frames ([dea069a](https://github.com/elct9620/kobako/commit/dea069ac49a59c12a6f7102ed3cf2925f316cbff)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
40
|
+
* **runtime:** let the engine frame a run's entrypoint and payload ([62f9c94](https://github.com/elct9620/kobako/commit/62f9c9407ff3ce690723625bea075fa49d2dda47)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Build System
|
|
44
|
+
|
|
45
|
+
* declare the Rust 1.96 floor wasmtime 49 sets ([f49a2df](https://github.com/elct9620/kobako/commit/f49a2df88fa51d3b0eecb619cab1e98d79bb4245))
|
|
46
|
+
|
|
3
47
|
## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-wasmtime-v0.16.0...kobako-wasmtime-v0.17.0) (2026-09-20)
|
|
4
48
|
|
|
5
49
|
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
[package]
|
|
17
17
|
name = "kobako-wasmtime"
|
|
18
|
-
version = "0.
|
|
18
|
+
version = "0.18.0"
|
|
19
19
|
edition = "2021"
|
|
20
|
+
# wasmtime's own floor.
|
|
21
|
+
rust-version = "1.96"
|
|
20
22
|
description = "wasmtime implementation of the kobako host runtime contract."
|
|
21
23
|
license = "Apache-2.0"
|
|
22
24
|
repository = "https://github.com/elct9620/kobako"
|
|
@@ -28,17 +30,17 @@ categories = ["wasm", "virtualization"]
|
|
|
28
30
|
# The engine-neutral contract this crate implements. The version pin
|
|
29
31
|
# rides the linked release group; the path keeps in-tree builds (and
|
|
30
32
|
# the Ruby gem, which ships both crates) resolving locally.
|
|
31
|
-
kobako-runtime = { version = "0.
|
|
33
|
+
kobako-runtime = { version = "0.18.0", path = "../kobako-runtime" }
|
|
32
34
|
# The fixed tier: the core envelope this driver shuttles and the ABI
|
|
33
35
|
# values it drives the exports against.
|
|
34
|
-
kobako-transport = { version = "0.
|
|
36
|
+
kobako-transport = { version = "0.18.0", path = "../kobako-transport" }
|
|
35
37
|
# wasmtime — host-side embedder for kobako.wasm. We disable default-features
|
|
36
38
|
# and opt back in only what kobako needs: a Cranelift-backed runtime that can
|
|
37
39
|
# compile a pre-built wasm32-wasip1 module on the host triple, plus the `wat`
|
|
38
40
|
# feature so test fixtures can be expressed as text.
|
|
39
41
|
# `cache` / `parallel-compilation` / `pooling` / `component-model` / `async`
|
|
40
42
|
# are intentionally off — kobako runs short-lived synchronous sandboxes.
|
|
41
|
-
wasmtime = { version = "
|
|
43
|
+
wasmtime = { version = "49.0.2", default-features = false, features = [
|
|
42
44
|
"cranelift",
|
|
43
45
|
"runtime",
|
|
44
46
|
"gc",
|
|
@@ -52,7 +54,7 @@ wasmtime = { version = "48.0.1", default-features = false, features = [
|
|
|
52
54
|
# WasiCtxBuilder + preview1 adapter which wires fd 1/2 to pipes. We omit
|
|
53
55
|
# `p2` (component-model) and `p0`/`p3` (async) because kobako runs
|
|
54
56
|
# synchronous sandboxes only.
|
|
55
|
-
wasmtime-wasi = { version = "
|
|
57
|
+
wasmtime-wasi = { version = "49.0.2", default-features = false, features = ["p1"] }
|
|
56
58
|
# sha2 keys the on-disk compiled-module cache by Guest Binary content
|
|
57
59
|
# (see cache.rs); a collision would load the wrong artifact, so the
|
|
58
60
|
# hash must be cryptographic.
|
|
@@ -13,7 +13,7 @@ contract surface.
|
|
|
13
13
|
| Compilation | process-wide Engine and compiled-Module caches, with an on-disk AOT (`.cwasm`) artifact cache keyed by Guest Binary content |
|
|
14
14
|
| Instantiation | a pre-linked `InstancePre` per guest path; every invocation runs on a fresh instance and discards its Store afterwards |
|
|
15
15
|
| Bounds | the epoch-based wall-clock timeout and the per-invocation linear-memory cap |
|
|
16
|
-
| Ambient denial |
|
|
16
|
+
| Ambient denial | under the default hermetic profile, frozen clocks and a constant RNG |
|
|
17
17
|
|
|
18
18
|
The kobako Ruby gem's native ext is the first frontend; a Rust host
|
|
19
19
|
SDK consumes the same surface.
|
|
@@ -22,7 +22,7 @@ SDK consumes the same surface.
|
|
|
22
22
|
|
|
23
23
|
```toml
|
|
24
24
|
[dependencies]
|
|
25
|
-
kobako-wasmtime = "0.
|
|
25
|
+
kobako-wasmtime = "0.18.0" # x-release-please-version
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## License
|
|
@@ -10,7 +10,7 @@ use wasmtime::{AsContextMut, InstancePre, Store as WtStore};
|
|
|
10
10
|
use crate::cache::shared_engine;
|
|
11
11
|
use crate::config::Config;
|
|
12
12
|
use crate::invocation::Invocation;
|
|
13
|
-
use crate::{
|
|
13
|
+
use crate::{io, trap};
|
|
14
14
|
use kobako_runtime::error::SetupError;
|
|
15
15
|
use kobako_runtime::profile::Profile;
|
|
16
16
|
use kobako_transport::abi::ABI_VERSION;
|
|
@@ -29,19 +29,16 @@ const PROBE_CONFIG: Config = Config {
|
|
|
29
29
|
profile: Profile::Hermetic,
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
-
///
|
|
33
|
-
///
|
|
34
|
-
///
|
|
35
|
-
/// fault, so every failure is a `SetupError` for the frontend to
|
|
36
|
-
/// attribute. The frameless WASI context keeps a third-party guest
|
|
37
|
-
/// whose start section touches WASI on the `SetupError` path instead of
|
|
32
|
+
/// An absent or unequal version is a deterministic artifact fault, so
|
|
33
|
+
/// every failure is a `SetupError`. The probe's own WASI context keeps a
|
|
34
|
+
/// guest whose start section touches WASI on that path instead of
|
|
38
35
|
/// panicking in `Invocation::wasi_mut`.
|
|
39
36
|
pub(crate) fn verify(pre: &InstancePre<Invocation>) -> Result<(), SetupError> {
|
|
40
37
|
let mut store = WtStore::new(shared_engine()?, Invocation::new(None));
|
|
41
38
|
// Epoch interruption is on engine-wide and a fresh Store's deadline
|
|
42
39
|
// has already elapsed, so the probe must name one it cannot reach.
|
|
43
40
|
store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
44
|
-
|
|
41
|
+
io::install_wasi(&mut store, &PROBE_CONFIG, &[])
|
|
45
42
|
.map_err(|t| SetupError::Dead(t.to_string()))?;
|
|
46
43
|
let instance = pre
|
|
47
44
|
.instantiate(store.as_context_mut())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//! Denial of guest ambient authority at the WASI layer — the one grant
|
|
2
2
|
//! that separates the hermetic rung from permissive;
|
|
3
|
-
//! `
|
|
3
|
+
//! `io::install_wasi` wires these sources on `Hermetic` only.
|
|
4
4
|
//!
|
|
5
5
|
//! `wasmtime-wasi`'s `WasiCtxBuilder` defaults the guest's `wasi:clocks` to
|
|
6
6
|
//! the host wall / monotonic clock and `wasi:random` to a fresh per-context
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//! (`build_config/wasi.rb`), but a future libc-backed gem would silently
|
|
9
9
|
//! obtain real time and host entropy — a covert timing channel and a
|
|
10
10
|
//! nondeterminism source the hermetic posture deliberately excludes
|
|
11
|
-
//! (docs/security
|
|
11
|
+
//! (docs/guides/security.md).
|
|
12
12
|
//! Pinning the clocks to the Unix epoch and the RNG to a constant stream
|
|
13
13
|
//! makes that denial a property of the host, not merely of the gem allowlist.
|
|
14
14
|
//!
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//! Process-wide caches for the wasmtime `Engine` and compiled
|
|
2
2
|
//! `Module`, plus the on-disk compiled-artifact cache.
|
|
3
3
|
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
4
|
+
//! Wasm engine types stay hidden from the Host App and downstream
|
|
5
|
+
//! gems. To amortise Engine creation and
|
|
6
6
|
//! Module JIT compilation across multiple sandbox constructions, the
|
|
7
7
|
//! driver keeps a process-scope shared Engine and a per-path Module
|
|
8
8
|
//! cache. Both are transparent to frontends, which construct a
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
//! Binary bytes; every cache failure falls
|
|
14
14
|
//! back to in-process compilation.
|
|
15
15
|
//!
|
|
16
|
-
//! Concurrency:
|
|
17
|
-
//!
|
|
18
|
-
//!
|
|
19
|
-
//!
|
|
16
|
+
//! Concurrency: the Mutex is held only for the HashMap lookup and insert,
|
|
17
|
+
//! never across a compile, so threads building Drivers at once do not
|
|
18
|
+
//! wait on each other. Two concurrent misses on one path both compile,
|
|
19
|
+
//! and the later insert wins with an equivalent Module.
|
|
20
20
|
|
|
21
21
|
use std::collections::HashMap;
|
|
22
22
|
use std::fmt::Write as _;
|
|
@@ -43,21 +43,10 @@ static MODULE_CACHE: OnceLock<Mutex<HashMap<PathBuf, WtModule>>> = OnceLock::new
|
|
|
43
43
|
/// 10 ms across the whole process).
|
|
44
44
|
const EPOCH_TICK: Duration = Duration::from_millis(10);
|
|
45
45
|
|
|
46
|
-
///
|
|
47
|
-
///
|
|
48
|
-
///
|
|
49
|
-
///
|
|
50
|
-
/// can be loaded. The mruby wasi build config uses
|
|
51
|
-
/// `-mllvm -wasm-use-legacy-eh=false`, which generates new-style
|
|
52
|
-
/// exception handling instructions in the wasm32 object files;
|
|
53
|
-
/// wasmtime must have the proposal enabled to parse and JIT those
|
|
54
|
-
/// instructions.
|
|
55
|
-
///
|
|
56
|
-
/// Also enables `epoch_interruption(true)` so every Store can install an
|
|
57
|
-
/// `epoch_deadline_callback` for the per-run wall-clock cap
|
|
58
|
-
/// cap. The first call spawns the process-singleton ticker
|
|
59
|
-
/// thread that drives `engine.increment_epoch()` at `EPOCH_TICK`
|
|
60
|
-
/// cadence; subsequent calls reuse the same engine and ticker.
|
|
46
|
+
/// The exceptions proposal is on because the mruby wasi build emits
|
|
47
|
+
/// new-style exception handling (`-mllvm -wasm-use-legacy-eh=false`) for
|
|
48
|
+
/// its setjmp path. Epoch interruption backs the wall-clock cap, driven by
|
|
49
|
+
/// one process-wide ticker thread.
|
|
61
50
|
pub(crate) fn shared_engine() -> Result<&'static WtEngine, SetupError> {
|
|
62
51
|
if let Some(engine) = SHARED_ENGINE.get() {
|
|
63
52
|
return Ok(engine);
|
|
@@ -72,11 +61,6 @@ pub(crate) fn shared_engine() -> Result<&'static WtEngine, SetupError> {
|
|
|
72
61
|
Ok(engine)
|
|
73
62
|
}
|
|
74
63
|
|
|
75
|
-
/// Spawn the process-singleton epoch ticker. The thread holds a clone of
|
|
76
|
-
/// the shared Engine (`wasmtime::Engine` is reference-counted internally)
|
|
77
|
-
/// and ticks the epoch counter at `EPOCH_TICK` cadence. Idempotent
|
|
78
|
-
/// across reentrant calls to `shared_engine` because `OnceLock`
|
|
79
|
-
/// gates the spawn.
|
|
80
64
|
fn spawn_epoch_ticker(engine: WtEngine) {
|
|
81
65
|
static TICKER_SPAWNED: OnceLock<()> = OnceLock::new();
|
|
82
66
|
TICKER_SPAWNED.get_or_init(|| {
|
|
@@ -90,11 +74,8 @@ fn spawn_epoch_ticker(engine: WtEngine) {
|
|
|
90
74
|
});
|
|
91
75
|
}
|
|
92
76
|
|
|
93
|
-
///
|
|
94
|
-
///
|
|
95
|
-
/// (boundary → `Kobako::ModuleNotBuiltError`) when the file is missing —
|
|
96
|
-
/// the headline error for the common pre-build state on a fresh clone
|
|
97
|
-
/// before `rake compile`.
|
|
77
|
+
/// A missing file answers `SetupError::ModuleNotBuilt`, the common state of
|
|
78
|
+
/// a fresh clone before `rake compile`.
|
|
98
79
|
pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
99
80
|
let cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
100
81
|
|
|
@@ -109,7 +90,7 @@ pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
|
109
90
|
|
|
110
91
|
if !path.exists() {
|
|
111
92
|
return Err(SetupError::ModuleNotBuilt(format!(
|
|
112
|
-
"Sandbox runtime not found at {}
|
|
93
|
+
"Sandbox runtime not found at {}",
|
|
113
94
|
path.display()
|
|
114
95
|
)));
|
|
115
96
|
}
|
|
@@ -145,17 +126,11 @@ pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
|
145
126
|
/// whole window are removed by `prune_stale`.
|
|
146
127
|
const ARTIFACT_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60);
|
|
147
128
|
|
|
148
|
-
///
|
|
149
|
-
///
|
|
150
|
-
///
|
|
151
|
-
///
|
|
152
|
-
///
|
|
153
|
-
/// two installed kobako-wasmtime versions (each pinning its own
|
|
154
|
-
/// wasmtime) from sharing a key and recompile-thrashing each other's
|
|
155
|
-
/// entry. wasmtime
|
|
156
|
-
/// itself rejects an artifact produced by an incompatible wasmtime
|
|
157
|
-
/// version or Config at deserialize time. Returns `None` when no home
|
|
158
|
-
/// directory is available — the caller then just compiles in-process.
|
|
129
|
+
/// Content addressing makes a rebuilt Guest Binary a new entry rather than
|
|
130
|
+
/// an invalidation problem; the crate-version segment keeps two installed
|
|
131
|
+
/// kobako-wasmtime versions, each pinning its own wasmtime, from thrashing
|
|
132
|
+
/// one key. wasmtime itself rejects an artifact from an incompatible
|
|
133
|
+
/// version or Config at deserialize time.
|
|
159
134
|
fn artifact_path(wasm_bytes: &[u8]) -> Option<PathBuf> {
|
|
160
135
|
let base = std::env::var_os("XDG_CACHE_HOME")
|
|
161
136
|
.map(PathBuf::from)
|
|
@@ -169,11 +144,8 @@ fn artifact_path(wasm_bytes: &[u8]) -> Option<PathBuf> {
|
|
|
169
144
|
Some(base.join("kobako").join(name))
|
|
170
145
|
}
|
|
171
146
|
|
|
172
|
-
///
|
|
173
|
-
///
|
|
174
|
-
/// Config mismatch — returns `None` and the caller recompiles. A hit
|
|
175
|
-
/// refreshes the file's mtime so `prune_stale`'s retention window
|
|
176
|
-
/// measures time since last use, not since creation.
|
|
147
|
+
/// A hit refreshes the file's mtime, so the retention window measures time
|
|
148
|
+
/// since last use rather than since creation.
|
|
177
149
|
fn load_artifact(engine: &WtEngine, artifact: &Path) -> Option<WtModule> {
|
|
178
150
|
if !artifact.exists() || !artifact.parent().is_some_and(dir_is_private) {
|
|
179
151
|
return None;
|
|
@@ -192,11 +164,9 @@ fn load_artifact(engine: &WtEngine, artifact: &Path) -> Option<WtModule> {
|
|
|
192
164
|
Some(module)
|
|
193
165
|
}
|
|
194
166
|
|
|
195
|
-
///
|
|
196
|
-
///
|
|
197
|
-
///
|
|
198
|
-
/// optimisation. A successful write also triggers `prune_stale` so the
|
|
199
|
-
/// cache directory cannot grow without bound across Guest Binary
|
|
167
|
+
/// The temp-file and rename pair keeps concurrent processes from observing
|
|
168
|
+
/// a partial write. Failures are swallowed because the cache is only an
|
|
169
|
+
/// optimisation, and pruning on write keeps the directory bounded across
|
|
200
170
|
/// rebuilds.
|
|
201
171
|
fn store_artifact(module: &WtModule, artifact: &Path) {
|
|
202
172
|
let Ok(bytes) = module.serialize() else {
|
|
@@ -252,10 +222,8 @@ fn dir_is_private(_dir: &Path) -> bool {
|
|
|
252
222
|
true
|
|
253
223
|
}
|
|
254
224
|
|
|
255
|
-
///
|
|
256
|
-
///
|
|
257
|
-
/// just-written `keep`. Live temp files are seconds old and never
|
|
258
|
-
/// qualify; foreign file names are left untouched.
|
|
225
|
+
/// Live temp files are seconds old and never qualify; foreign file names
|
|
226
|
+
/// are left untouched.
|
|
259
227
|
fn prune_stale(dir: &Path, keep: &Path) {
|
|
260
228
|
let Ok(entries) = fs::read_dir(dir) else {
|
|
261
229
|
return;
|
|
@@ -277,8 +245,6 @@ fn prune_stale(dir: &Path, keep: &Path) {
|
|
|
277
245
|
}
|
|
278
246
|
}
|
|
279
247
|
|
|
280
|
-
/// Returns whether `path` carries a file name this cache wrote — a
|
|
281
|
-
/// `.cwasm` artifact or a `.tmp*` leftover.
|
|
282
248
|
fn cache_entry_name(path: &Path) -> bool {
|
|
283
249
|
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
|
|
284
250
|
return false;
|
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
//! channel) so a regression that only breaks one channel cannot sneak
|
|
8
8
|
//! through the test that pins them.
|
|
9
9
|
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
/// "wrote exactly cap" and "exceeded cap" cases stay distinguishable;
|
|
13
|
-
/// `usize::MAX` when the channel is uncapped.
|
|
10
|
+
/// One byte past the cap, so "wrote exactly cap" and "exceeded cap" stay
|
|
11
|
+
/// distinguishable.
|
|
14
12
|
pub(crate) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
15
13
|
match cap {
|
|
16
14
|
Some(c) => c.saturating_add(1),
|
|
@@ -18,13 +16,8 @@ pub(crate) fn pipe_capacity(cap: Option<usize>) -> usize {
|
|
|
18
16
|
}
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
///
|
|
22
|
-
///
|
|
23
|
-
/// costs one copy out of the pipe, not two), return the bytes Ruby
|
|
24
|
-
/// should observe plus the truncation flag. `truncated` is `true` only
|
|
25
|
-
/// when the snapshot strictly exceeded the cap — this is the "wrote
|
|
26
|
-
/// `cap + 1` bytes into a `cap + 1`-sized pipe" case; "wrote exactly
|
|
27
|
-
/// `cap` bytes" stays `false`.
|
|
19
|
+
/// Takes the snapshot by value and truncates in place, so the readback
|
|
20
|
+
/// costs one copy out of the pipe rather than two.
|
|
28
21
|
pub(crate) fn clip_capture(mut raw: Vec<u8>, cap: Option<usize>) -> (Vec<u8>, bool) {
|
|
29
22
|
match cap {
|
|
30
23
|
Some(c) if raw.len() > c => {
|
|
@@ -18,23 +18,20 @@ use kobako_runtime::profile::Profile;
|
|
|
18
18
|
/// The caps and requested isolation profile for one `Driver`. `None` on
|
|
19
19
|
/// any cap field disables that cap.
|
|
20
20
|
pub struct Config {
|
|
21
|
-
/// Wall-clock cap for one guest `#eval` / `#run
|
|
22
|
-
///
|
|
21
|
+
/// Wall-clock cap for one guest `#eval` / `#run`; the invocation traps
|
|
22
|
+
/// once it passes.
|
|
23
23
|
pub timeout: Option<Duration>,
|
|
24
|
-
/// Guest linear-memory cap, in bytes
|
|
25
|
-
///
|
|
26
|
-
/// consumes it from inside the engine.
|
|
24
|
+
/// Guest linear-memory cap, in bytes, charged to what one invocation
|
|
25
|
+
/// grows beyond the image it starts from.
|
|
27
26
|
pub memory_limit: Option<usize>,
|
|
28
|
-
/// Byte cap for guest stdout capture
|
|
29
|
-
///
|
|
30
|
-
/// flag in `Driver::build_snapshot`.
|
|
27
|
+
/// Byte cap for guest stdout capture; output past it is dropped and
|
|
28
|
+
/// the capture marked truncated.
|
|
31
29
|
pub stdout_limit: Option<usize>,
|
|
32
|
-
/// Byte cap for guest stderr capture
|
|
30
|
+
/// Byte cap for guest stderr capture, the same as `stdout_limit`.
|
|
33
31
|
pub stderr_limit: Option<usize>,
|
|
34
|
-
/// Isolation posture the frontend requested.
|
|
35
|
-
///
|
|
36
|
-
///
|
|
37
|
-
///
|
|
38
|
-
/// the built posture.
|
|
32
|
+
/// Isolation posture the frontend requested. `Hermetic` freezes
|
|
33
|
+
/// ambient time and entropy, `Permissive` leaves the live WASI
|
|
34
|
+
/// sources, and `Driver::profile` declares it back as the built
|
|
35
|
+
/// posture.
|
|
39
36
|
pub profile: Profile,
|
|
40
37
|
}
|