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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//! A Mount: the Grant handing a Filesystem to the guest at an absolute
|
|
2
|
+
//! guest path, with the access it allows there. The Mount decides each
|
|
3
|
+
//! open and each change itself, so a runtime asks it rather than reading
|
|
4
|
+
//! its access, and a Filesystem of a third party's checks nothing.
|
|
5
|
+
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use crate::filesystem::{Access, Filesystem};
|
|
10
|
+
|
|
11
|
+
/// The Host App's decision for one path a guest opens or changes, made in
|
|
12
|
+
/// a Mount's access's place.
|
|
13
|
+
pub trait Policy: Send + Sync {
|
|
14
|
+
/// Whether the guest may reach `path`, the absolute guest path, with
|
|
15
|
+
/// `access`.
|
|
16
|
+
fn allows(&self, path: &str, access: Access) -> bool;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Why a Mount refused a call.
|
|
20
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
21
|
+
pub enum Denial {
|
|
22
|
+
/// A change beneath a Mount allowing only reads.
|
|
23
|
+
ReadOnly,
|
|
24
|
+
/// The Mount's Policy said no.
|
|
25
|
+
Refused,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// A Filesystem at an absolute guest path. Its path bounds what a Policy
|
|
29
|
+
/// can allow either way.
|
|
30
|
+
#[derive(Clone)]
|
|
31
|
+
pub struct Mount {
|
|
32
|
+
at: String,
|
|
33
|
+
access: Access,
|
|
34
|
+
filesystem: Arc<dyn Filesystem>,
|
|
35
|
+
policy: Option<Arc<dyn Policy>>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// A path no Mount can stand at: one that is not an absolute guest path.
|
|
39
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
40
|
+
pub struct MountPathError(String);
|
|
41
|
+
|
|
42
|
+
impl fmt::Display for MountPathError {
|
|
43
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
44
|
+
write!(
|
|
45
|
+
f,
|
|
46
|
+
"mount path must be an absolute guest path, got {:?}",
|
|
47
|
+
self.0
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl std::error::Error for MountPathError {}
|
|
53
|
+
|
|
54
|
+
/// An absolute guest path has no empty, `.`, or `..` component; the root
|
|
55
|
+
/// is `/` alone.
|
|
56
|
+
fn is_guest_path(at: &str) -> bool {
|
|
57
|
+
at == "/"
|
|
58
|
+
|| at.strip_prefix('/').is_some_and(|rest| {
|
|
59
|
+
rest.split('/')
|
|
60
|
+
.all(|part| !part.is_empty() && part != "." && part != "..")
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl Mount {
|
|
65
|
+
/// A Mount of `filesystem` at `at`, refused unless `at` is an absolute
|
|
66
|
+
/// guest path, which every path the Mount answers for is built on.
|
|
67
|
+
pub fn new(
|
|
68
|
+
at: impl Into<String>,
|
|
69
|
+
filesystem: Arc<dyn Filesystem>,
|
|
70
|
+
access: Access,
|
|
71
|
+
) -> Result<Self, MountPathError> {
|
|
72
|
+
let at = at.into();
|
|
73
|
+
if !is_guest_path(&at) {
|
|
74
|
+
return Err(MountPathError(at));
|
|
75
|
+
}
|
|
76
|
+
Ok(Self {
|
|
77
|
+
at,
|
|
78
|
+
access,
|
|
79
|
+
filesystem,
|
|
80
|
+
policy: None,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// This Mount with `policy` deciding each open and each change in its
|
|
85
|
+
/// access's place.
|
|
86
|
+
pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
|
|
87
|
+
Self {
|
|
88
|
+
policy: Some(policy),
|
|
89
|
+
..self
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The absolute guest path the Filesystem appears at.
|
|
94
|
+
pub fn at(&self) -> &str {
|
|
95
|
+
&self.at
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The Filesystem an engine hands each call beneath this Mount.
|
|
99
|
+
pub fn filesystem(&self) -> &Arc<dyn Filesystem> {
|
|
100
|
+
&self.filesystem
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Whether a change beneath this Mount may be allowed at all: it was made
|
|
104
|
+
/// to be written, or a Policy decides each change in its access's place.
|
|
105
|
+
/// The Policy itself is not asked.
|
|
106
|
+
pub fn allows_writes(&self) -> bool {
|
|
107
|
+
self.policy.is_some() || self.access == Access::ReadWrite
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// The absolute guest path `path`, relative to this Mount's root,
|
|
111
|
+
/// stands for.
|
|
112
|
+
pub fn guest_path(&self, path: &str) -> String {
|
|
113
|
+
match (self.at.as_str(), path) {
|
|
114
|
+
(at, "") => at.to_string(),
|
|
115
|
+
("/", path) => format!("/{path}"),
|
|
116
|
+
(at, path) => format!("{at}/{path}"),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Whether the guest may reach `path`, relative to this Mount's root,
|
|
121
|
+
/// with `access`. A Policy judges the path as the Filesystem stores
|
|
122
|
+
/// its names, so a host folding case or Unicode form cannot hand it
|
|
123
|
+
/// one spelling of an entry it refuses by another; a Filesystem that
|
|
124
|
+
/// cannot spell it refuses.
|
|
125
|
+
pub fn check(&self, path: &str, access: Access) -> Result<(), Denial> {
|
|
126
|
+
match &self.policy {
|
|
127
|
+
Some(policy) => {
|
|
128
|
+
let stored = self
|
|
129
|
+
.filesystem
|
|
130
|
+
.stored_path(path)
|
|
131
|
+
.map_err(|_| Denial::Refused)?;
|
|
132
|
+
if policy.allows(&self.guest_path(&stored), access) {
|
|
133
|
+
Ok(())
|
|
134
|
+
} else {
|
|
135
|
+
Err(Denial::Refused)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
None if access == Access::ReadWrite && self.access == Access::Read => {
|
|
139
|
+
Err(Denial::ReadOnly)
|
|
140
|
+
}
|
|
141
|
+
None => Ok(()),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Whether a directory may move beneath this Mount. Under a Policy it
|
|
146
|
+
/// may not: everything beneath it would take paths the Policy never
|
|
147
|
+
/// judged, so a move is refused as one across devices and the guest
|
|
148
|
+
/// copies entry by entry instead, as Landlock refuses such a move.
|
|
149
|
+
pub fn moves_directories(&self) -> bool {
|
|
150
|
+
self.policy.is_none()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Whether a path beneath this Mount may pass through a link the
|
|
154
|
+
/// Filesystem holds. Under a Policy it may not: the link leads to a
|
|
155
|
+
/// path the Policy never judged, so the call is refused as one past
|
|
156
|
+
/// too many links.
|
|
157
|
+
pub fn follows_links(&self) -> bool {
|
|
158
|
+
self.policy.is_none()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// This Mount as a new Invocation starts from it, over the Filesystem
|
|
162
|
+
/// that Invocation receives; beneath a Policy, the one that follows no
|
|
163
|
+
/// link, so each call reaches the path the Policy judged.
|
|
164
|
+
pub fn for_invocation(&self) -> Self {
|
|
165
|
+
let filesystem = self
|
|
166
|
+
.filesystem
|
|
167
|
+
.for_invocation()
|
|
168
|
+
.unwrap_or_else(|| self.filesystem.clone());
|
|
169
|
+
let filesystem = match self.policy {
|
|
170
|
+
Some(_) => filesystem.for_policy().unwrap_or(filesystem),
|
|
171
|
+
None => filesystem,
|
|
172
|
+
};
|
|
173
|
+
Self {
|
|
174
|
+
filesystem,
|
|
175
|
+
..self.clone()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
/// The ordered isolation ladder a runtime builds one rung of.
|
|
10
10
|
///
|
|
11
11
|
/// `Hermetic` is the full ambient-denial posture: ambient time and
|
|
12
|
-
/// entropy denied at the WASI boundary, no filesystem /
|
|
13
|
-
/// network reachability, and no host import beyond the wire ABI's
|
|
14
|
-
/// `__kobako_upcall`. `Permissive` differs in
|
|
15
|
-
/// live
|
|
12
|
+
/// entropy denied at the WASI boundary, no ambient filesystem /
|
|
13
|
+
/// environment / network reachability, and no host import beyond the wire ABI's
|
|
14
|
+
/// `__kobako_upcall`. `Permissive` differs only in the ambient
|
|
15
|
+
/// authority it grants — live time and entropy at the WASI boundary. Ordering
|
|
16
16
|
/// follows strength (`Permissive < Hermetic`), so a floor check is a
|
|
17
17
|
/// plain comparison.
|
|
18
18
|
///
|
|
@@ -13,6 +13,7 @@ use kobako_transport::envelope::Preamble;
|
|
|
13
13
|
|
|
14
14
|
use crate::dispatch::DispatchHandler;
|
|
15
15
|
use crate::error::InvokeError;
|
|
16
|
+
use crate::grant::{Grant, GrantKind};
|
|
16
17
|
use crate::profile::Profile;
|
|
17
18
|
use crate::snapshot::Snapshot;
|
|
18
19
|
|
|
@@ -49,13 +50,24 @@ pub enum Entry<'a> {
|
|
|
49
50
|
/// wrapper and GC-marking it; the runtime itself touches no frontend
|
|
50
51
|
/// value.
|
|
51
52
|
pub trait Runtime {
|
|
53
|
+
/// `grants` are this Invocation's, each of a kind `supports` declared;
|
|
54
|
+
/// the runtime presents them to the guest and keeps none afterwards.
|
|
52
55
|
fn invoke(
|
|
53
56
|
&self,
|
|
54
57
|
entry: Entry<'_>,
|
|
55
58
|
preamble: &Preamble,
|
|
56
59
|
handler: Option<Arc<dyn DispatchHandler>>,
|
|
60
|
+
grants: &[Grant],
|
|
57
61
|
) -> Result<Snapshot, InvokeError>;
|
|
58
62
|
|
|
63
|
+
/// Whether this runtime presents a Grant of `kind` to the guest. A
|
|
64
|
+
/// frontend refuses to register one it does not, before any guest runs.
|
|
65
|
+
/// The default supports none, so an engine that has not learned Grants
|
|
66
|
+
/// still serves every invocation without one.
|
|
67
|
+
fn supports(&self, _kind: GrantKind) -> bool {
|
|
68
|
+
false
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/// The isolation profile this runtime provides. Deliberately
|
|
60
72
|
/// without a default: every engine states its posture explicitly,
|
|
61
73
|
/// and a frontend refuses construction when the declaration falls
|
|
@@ -18,6 +18,7 @@ use kobako_transport::envelope::Outcome;
|
|
|
18
18
|
/// pre-invocation sentinel — no bytes, cap not reached — shared by every
|
|
19
19
|
/// frontend's before-first-invocation readout.
|
|
20
20
|
#[derive(Default)]
|
|
21
|
+
#[non_exhaustive]
|
|
21
22
|
pub struct Capture {
|
|
22
23
|
pub bytes: Vec<u8>,
|
|
23
24
|
pub truncated: bool,
|
|
@@ -39,12 +40,13 @@ pub enum Completion {
|
|
|
39
40
|
Trap(Trap),
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
/// Resource usage of one guest invocation, measured across the
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
/// `memory.grow` delta in bytes past the entry-time
|
|
46
|
-
/// (all zeros) is the pre-invocation sentinel.
|
|
43
|
+
/// Resource usage of one guest invocation, each measured across the span
|
|
44
|
+
/// its cap governs: `wall_time` is the seconds from setting the guest up to
|
|
45
|
+
/// its export call returning, the span the deadline covers; `memory_peak`
|
|
46
|
+
/// is the high-water `memory.grow` delta in bytes past the entry-time
|
|
47
|
+
/// baseline. `Default` (all zeros) is the pre-invocation sentinel.
|
|
47
48
|
#[derive(Clone, Copy, Default)]
|
|
49
|
+
#[non_exhaustive]
|
|
48
50
|
pub struct Usage {
|
|
49
51
|
pub wall_time: f64,
|
|
50
52
|
pub memory_peak: usize,
|
|
@@ -54,9 +56,22 @@ pub struct Usage {
|
|
|
54
56
|
/// kinds: captures and usage are collected on trap and outcome alike.
|
|
55
57
|
/// What a frontend exposes from the trap path is its own contract
|
|
56
58
|
/// decision.
|
|
59
|
+
#[non_exhaustive]
|
|
57
60
|
pub struct Snapshot {
|
|
58
61
|
pub completion: Completion,
|
|
59
62
|
pub stdout: Capture,
|
|
60
63
|
pub stderr: Capture,
|
|
61
64
|
pub usage: Usage,
|
|
62
65
|
}
|
|
66
|
+
|
|
67
|
+
impl Snapshot {
|
|
68
|
+
/// What an engine hands back from one invocation.
|
|
69
|
+
pub fn new(completion: Completion, stdout: Capture, stderr: Capture, usage: Usage) -> Self {
|
|
70
|
+
Self {
|
|
71
|
+
completion,
|
|
72
|
+
stdout,
|
|
73
|
+
stderr,
|
|
74
|
+
usage,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -15,11 +15,16 @@ use crate::error::Trap;
|
|
|
15
15
|
/// Host-initiated re-entry into the in-flight guest instance to run a
|
|
16
16
|
/// yielded block.
|
|
17
17
|
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
20
|
-
///
|
|
21
|
-
///
|
|
22
|
-
/// the
|
|
18
|
+
/// Each open Call has a `frame`, counted from `0` at the outermost.
|
|
19
|
+
/// `yield_to_block` ships `args` as a `yield` downcall to the block of the
|
|
20
|
+
/// open Call `frame` names, and returns the decoded Yield Reply,
|
|
21
|
+
/// `Unsupported` arm included, or a `Trap` — surfaced through the
|
|
22
|
+
/// frontend's trap-error mapping — when the re-entry traps, the guest
|
|
23
|
+
/// returns an empty or unframeable result, or a payload exceeds the
|
|
24
|
+
/// 16 MiB cap.
|
|
23
25
|
pub trait Yielder {
|
|
24
|
-
|
|
26
|
+
/// The frame of the Call this Yielder was lent for.
|
|
27
|
+
fn frame(&self) -> u32;
|
|
28
|
+
|
|
29
|
+
fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap>;
|
|
25
30
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.18.0...kobako-transport-v0.19.0) (2026-10-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* in kobako-transport, YieldReply::Break carries no payload and Reply gains a Break arm, so a Guest Binary built against the earlier version 5 wire must be rebuilt.
|
|
9
|
+
* the Guest ABI is version 5, so a Guest Binary built against version 4 is refused at Sandbox construction until rebuilt. The Yielder trait of kobako-runtime gains frame and takes the frame in yield_to_block; the Guest trait of kobako-core and the MrbGuest trait of kobako-mruby take the frame in yield_to_block; Downcall::Yield carries frame and args.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* let a block's break or return unwind a Service as a plain method ([4e9eca0](https://github.com/elct9620/kobako/commit/4e9eca05f1647c8b6409fc00d08317b431c0c648))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
|
|
19
|
+
|
|
3
20
|
## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.17.0...kobako-transport-v0.18.0) (2026-10-13)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/// version it reports equals this one, so a framing or symbol change is
|
|
11
11
|
/// an increment rather than a negotiation. A new request tag needs
|
|
12
12
|
/// neither, but a new answer arm is an increment.
|
|
13
|
-
pub const ABI_VERSION: u32 =
|
|
13
|
+
pub const ABI_VERSION: u32 = 5;
|
|
14
14
|
|
|
15
15
|
/// Width in bytes of the length prefix that frames a message on WASI
|
|
16
16
|
/// stdin, such as the Preamble.
|
|
@@ -32,7 +32,8 @@ const KIND_BLOCK: u8 = 4;
|
|
|
32
32
|
pub enum FaultKind {
|
|
33
33
|
/// A Ruby exception raised inside the Service method.
|
|
34
34
|
Runtime,
|
|
35
|
-
/// The
|
|
35
|
+
/// The Service method refused its arguments, whether the call would
|
|
36
|
+
/// not bind or the method rejected what it was given.
|
|
36
37
|
Argument,
|
|
37
38
|
/// No such member or method.
|
|
38
39
|
Undefined,
|
|
@@ -243,7 +243,7 @@ mod tests {
|
|
|
243
243
|
writer.into_bytes()
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
-
fn
|
|
246
|
+
fn preamble_with(records: &[Vec<u8>]) -> Vec<u8> {
|
|
247
247
|
let mut bytes = (records.len() as u32).to_be_bytes().to_vec();
|
|
248
248
|
for record in records {
|
|
249
249
|
bytes.extend_from_slice(record);
|
|
@@ -307,7 +307,7 @@ mod tests {
|
|
|
307
307
|
// @behavior WE-053
|
|
308
308
|
#[test]
|
|
309
309
|
fn an_absent_record_reads_as_empty() {
|
|
310
|
-
let bytes =
|
|
310
|
+
let bytes = preamble_with(&[record(0, 0x07, &encode_flag(&flag("f", "", false)))]);
|
|
311
311
|
let preamble = Preamble::decode(&bytes).expect("a flag-only Preamble must decode");
|
|
312
312
|
assert_eq!(
|
|
313
313
|
(preamble.bindings, preamble.snippets),
|
|
@@ -375,7 +375,7 @@ mod tests {
|
|
|
375
375
|
// @behavior WE-056
|
|
376
376
|
#[test]
|
|
377
377
|
fn an_unknown_snippet_kind_is_refused() {
|
|
378
|
-
let bytes =
|
|
378
|
+
let bytes = preamble_with(&[record(1, 0x06, &[0, 0, 0, 1, 9])]);
|
|
379
379
|
assert!(
|
|
380
380
|
Preamble::decode(&bytes).is_err(),
|
|
381
381
|
"a snippet kind that is neither source nor bytecode through Preamble::decode must \
|
|
@@ -387,7 +387,7 @@ mod tests {
|
|
|
387
387
|
#[test]
|
|
388
388
|
fn a_count_the_preamble_cannot_satisfy_is_refused() {
|
|
389
389
|
let records = [0xff, 0xff, 0xff, 0xff];
|
|
390
|
-
let snippets =
|
|
390
|
+
let snippets = preamble_with(&[record(1, 0x06, &[0xff, 0xff, 0xff, 0xff])]);
|
|
391
391
|
assert!(
|
|
392
392
|
Preamble::decode(&records).is_err() && Preamble::decode(&snippets).is_err(),
|
|
393
393
|
"a record or snippet count larger than what follows through Preamble::decode must \
|
|
@@ -400,7 +400,7 @@ mod tests {
|
|
|
400
400
|
fn trailing_bytes_are_refused() {
|
|
401
401
|
let mut past_the_list = Preamble::default().encode();
|
|
402
402
|
past_the_list.push(0);
|
|
403
|
-
let past_a_body =
|
|
403
|
+
let past_a_body = preamble_with(&[record(1, 0x05, &[0, 0, 0, 0, 0])]);
|
|
404
404
|
assert!(
|
|
405
405
|
Preamble::decode(&past_the_list).is_err() && Preamble::decode(&past_a_body).is_err(),
|
|
406
406
|
"bytes past the last record or past a record body's last field through \
|
|
@@ -411,7 +411,7 @@ mod tests {
|
|
|
411
411
|
// @behavior WE-077
|
|
412
412
|
#[test]
|
|
413
413
|
fn an_advisory_record_of_an_unknown_kind_is_skipped() {
|
|
414
|
-
let bytes =
|
|
414
|
+
let bytes = preamble_with(&[
|
|
415
415
|
record(0, 0x7e, b"anything"),
|
|
416
416
|
record(
|
|
417
417
|
1,
|
|
@@ -432,7 +432,7 @@ mod tests {
|
|
|
432
432
|
// @behavior WE-078
|
|
433
433
|
#[test]
|
|
434
434
|
fn a_critical_record_of_an_unknown_kind_is_refused() {
|
|
435
|
-
let bytes =
|
|
435
|
+
let bytes = preamble_with(&[record(1, 0x7e, b"anything")]);
|
|
436
436
|
assert!(
|
|
437
437
|
Preamble::decode(&bytes).is_err(),
|
|
438
438
|
"a critical record of a kind the registry has not assigned through \
|
|
@@ -444,7 +444,7 @@ mod tests {
|
|
|
444
444
|
#[test]
|
|
445
445
|
fn a_record_kind_assigned_to_another_message_is_refused() {
|
|
446
446
|
for kind in [0x00, 0x01, 0x10] {
|
|
447
|
-
let bytes =
|
|
447
|
+
let bytes = preamble_with(&[record(0, kind, b"")]);
|
|
448
448
|
assert!(
|
|
449
449
|
Preamble::decode(&bytes).is_err(),
|
|
450
450
|
"an advisory record of kind {kind:#04x}, which the registry assigns elsewhere, \
|
|
@@ -459,7 +459,7 @@ mod tests {
|
|
|
459
459
|
let bindings = encode_bindings(&Bindings::default());
|
|
460
460
|
let snippets = encode_snippets(&Snippets::default());
|
|
461
461
|
for (kind, body) in [(0x05, bindings), (0x06, snippets)] {
|
|
462
|
-
let bytes =
|
|
462
|
+
let bytes = preamble_with(&[record(1, kind, &body), record(1, kind, &body)]);
|
|
463
463
|
assert!(
|
|
464
464
|
Preamble::decode(&bytes).is_err(),
|
|
465
465
|
"a second record of kind {kind:#04x} through Preamble::decode must be refused"
|
|
@@ -485,7 +485,7 @@ mod tests {
|
|
|
485
485
|
// @behavior WE-082
|
|
486
486
|
#[test]
|
|
487
487
|
fn a_criticality_that_is_neither_value_is_refused() {
|
|
488
|
-
let bytes =
|
|
488
|
+
let bytes = preamble_with(&[record(2, 0x07, &encode_flag(&flag("f", "", false)))]);
|
|
489
489
|
assert!(
|
|
490
490
|
Preamble::decode(&bytes).is_err(),
|
|
491
491
|
"a criticality byte other than 0 or 1 through Preamble::decode must be refused"
|
|
@@ -504,8 +504,8 @@ mod tests {
|
|
|
504
504
|
);
|
|
505
505
|
let flag = record(0, 0x07, &encode_flag(&flag("f", "", false)));
|
|
506
506
|
assert_eq!(
|
|
507
|
-
Preamble::decode(&
|
|
508
|
-
Preamble::decode(&
|
|
507
|
+
Preamble::decode(&preamble_with(&[flag.clone(), bindings.clone()])),
|
|
508
|
+
Preamble::decode(&preamble_with(&[bindings, flag])),
|
|
509
509
|
"the same records in another order through Preamble::decode must read as the same \
|
|
510
510
|
Preamble"
|
|
511
511
|
);
|
|
@@ -17,6 +17,9 @@ pub enum Reply {
|
|
|
17
17
|
/// every field of a Fault is kobako's, so a guest reads a refusal
|
|
18
18
|
/// whatever schema it speaks.
|
|
19
19
|
Fault(Fault),
|
|
20
|
+
/// A block exit unwound the method; the guest continues that exit,
|
|
21
|
+
/// which it kept, so nothing of it crosses.
|
|
22
|
+
Break,
|
|
20
23
|
/// The receiver does not serve the request it was sent.
|
|
21
24
|
Unsupported(Unsupported),
|
|
22
25
|
}
|
|
@@ -29,9 +32,13 @@ impl Reply {
|
|
|
29
32
|
// No `finish` here: a Fault owns everything after the tag,
|
|
30
33
|
// including the fields a later contract version appends.
|
|
31
34
|
tag::FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
|
|
35
|
+
tag::BREAK => {
|
|
36
|
+
reader.finish()?;
|
|
37
|
+
Ok(Reply::Break)
|
|
38
|
+
}
|
|
32
39
|
tag::UNSUPPORTED => Ok(Reply::Unsupported(Unsupported::read(&mut reader)?)),
|
|
33
40
|
_ => Err(DecodeError::new(
|
|
34
|
-
"Reply tag must be 0x10 (ok), 0x11 (fault), or 0x15 (unsupported)",
|
|
41
|
+
"Reply tag must be 0x10 (ok), 0x11 (fault), 0x12 (break), or 0x15 (unsupported)",
|
|
35
42
|
)),
|
|
36
43
|
}
|
|
37
44
|
}
|
|
@@ -46,21 +53,23 @@ impl Reply {
|
|
|
46
53
|
writer.u8(tag::FAULT);
|
|
47
54
|
fault.write(&mut writer);
|
|
48
55
|
}
|
|
56
|
+
Reply::Break => {
|
|
57
|
+
writer.u8(tag::BREAK);
|
|
58
|
+
}
|
|
49
59
|
Reply::Unsupported(refusal) => return refusal.encode(),
|
|
50
60
|
}
|
|
51
61
|
writer.into_bytes()
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
/// The answer to one Yield Call.
|
|
56
|
-
/// Reply has no counterpart for, because a block can end its caller.
|
|
65
|
+
/// The answer to one Yield Call.
|
|
57
66
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
58
67
|
pub enum YieldReply {
|
|
59
68
|
/// The block completed; the bytes are its value, codec-encoded.
|
|
60
69
|
Ok(Vec<u8>),
|
|
61
|
-
/// The block ran `break
|
|
62
|
-
///
|
|
63
|
-
Break
|
|
70
|
+
/// The block ran `break` or `return` to a frame past the Service.
|
|
71
|
+
/// The guest keeps the exit, so the Service is only unwound.
|
|
72
|
+
Break,
|
|
64
73
|
/// The block raised, or failed in a way the host re-raises at the
|
|
65
74
|
/// yield site.
|
|
66
75
|
Error(ErrorRecord),
|
|
@@ -73,7 +82,10 @@ impl YieldReply {
|
|
|
73
82
|
let mut reader = Reader::new(bytes);
|
|
74
83
|
match reader.u8()? {
|
|
75
84
|
tag::OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
|
|
76
|
-
tag::BREAK =>
|
|
85
|
+
tag::BREAK => {
|
|
86
|
+
reader.finish()?;
|
|
87
|
+
Ok(YieldReply::Break)
|
|
88
|
+
}
|
|
77
89
|
tag::ERROR => {
|
|
78
90
|
let record = ErrorRecord::read(&mut reader)?;
|
|
79
91
|
reader.finish()?;
|
|
@@ -90,8 +102,8 @@ impl YieldReply {
|
|
|
90
102
|
YieldReply::Ok(body) => {
|
|
91
103
|
writer.u8(tag::OK).remainder(body);
|
|
92
104
|
}
|
|
93
|
-
YieldReply::Break
|
|
94
|
-
writer.u8(tag::BREAK)
|
|
105
|
+
YieldReply::Break => {
|
|
106
|
+
writer.u8(tag::BREAK);
|
|
95
107
|
}
|
|
96
108
|
YieldReply::Error(record) => {
|
|
97
109
|
writer.u8(tag::ERROR);
|
|
@@ -116,6 +128,7 @@ mod tests {
|
|
|
116
128
|
super::super::FaultKind::Undefined,
|
|
117
129
|
"no such method",
|
|
118
130
|
)),
|
|
131
|
+
Reply::Break,
|
|
119
132
|
Reply::Unsupported(Unsupported::new("no such operation")),
|
|
120
133
|
] {
|
|
121
134
|
let encoded = reply.encode();
|
|
@@ -142,7 +155,7 @@ mod tests {
|
|
|
142
155
|
#[test]
|
|
143
156
|
fn an_unknown_reply_tag_is_refused() {
|
|
144
157
|
assert!(
|
|
145
|
-
Reply::decode(&[
|
|
158
|
+
Reply::decode(&[0x13, 0]).is_err(),
|
|
146
159
|
"a Reply tag outside its arms must be rejected as a wire violation"
|
|
147
160
|
);
|
|
148
161
|
}
|
|
@@ -161,7 +174,7 @@ mod tests {
|
|
|
161
174
|
fn every_live_yield_arm_round_trips() {
|
|
162
175
|
let arms = [
|
|
163
176
|
YieldReply::Ok(vec![0xa4]),
|
|
164
|
-
YieldReply::Break
|
|
177
|
+
YieldReply::Break,
|
|
165
178
|
YieldReply::Error(ErrorRecord {
|
|
166
179
|
name: "LocalJumpError".into(),
|
|
167
180
|
message: "no block".into(),
|
|
@@ -214,6 +227,11 @@ mod tests {
|
|
|
214
227
|
"an unsupported Reply must encode as tag byte 0x15 followed by kind 0 and the \
|
|
215
228
|
message"
|
|
216
229
|
);
|
|
230
|
+
assert_eq!(
|
|
231
|
+
Reply::Break.encode(),
|
|
232
|
+
vec![0x12],
|
|
233
|
+
"a break Reply must encode as tag byte 0x12 alone, since the guest keeps the exit"
|
|
234
|
+
);
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
// @behavior WE-068
|
|
@@ -242,9 +260,9 @@ mod tests {
|
|
|
242
260
|
"a Yield Reply carrying the block's value must encode as tag byte 0x10"
|
|
243
261
|
);
|
|
244
262
|
assert_eq!(
|
|
245
|
-
YieldReply::Break
|
|
246
|
-
vec![0x12
|
|
247
|
-
"a Yield Reply
|
|
263
|
+
YieldReply::Break.encode(),
|
|
264
|
+
vec![0x12],
|
|
265
|
+
"a Yield Reply for a block exit must encode as tag byte 0x12 alone"
|
|
248
266
|
);
|
|
249
267
|
assert_eq!(
|
|
250
268
|
YieldReply::Unsupported(Unsupported::new("x")).encode(),
|
|
@@ -18,9 +18,9 @@ pub enum Downcall {
|
|
|
18
18
|
Eval(Vec<u8>),
|
|
19
19
|
/// Call the entrypoint the Run names.
|
|
20
20
|
Run(Run),
|
|
21
|
-
/// Re-enter the block
|
|
22
|
-
/// its arguments, codec-encoded.
|
|
23
|
-
Yield
|
|
21
|
+
/// Re-enter the block of the open Call `frame` counts to, from `0`
|
|
22
|
+
/// at the outermost; `args` are its arguments, codec-encoded.
|
|
23
|
+
Yield { frame: u32, args: Vec<u8> },
|
|
24
24
|
/// Compile the Preamble's source Snippets without running them. It
|
|
25
25
|
/// carries no body: what it compiles is the Preamble an invocation
|
|
26
26
|
/// would read.
|
|
@@ -37,7 +37,10 @@ impl Downcall {
|
|
|
37
37
|
Ok(match tag {
|
|
38
38
|
tag::EVAL => Downcall::Eval(body.to_vec()),
|
|
39
39
|
tag::RUN => Downcall::Run(Run::decode(body)?),
|
|
40
|
-
tag::YIELD => Downcall::Yield
|
|
40
|
+
tag::YIELD => Downcall::Yield {
|
|
41
|
+
frame: reader.u32()?,
|
|
42
|
+
args: reader.remaining().to_vec(),
|
|
43
|
+
},
|
|
41
44
|
tag::COMPILE if body.is_empty() => Downcall::Compile,
|
|
42
45
|
tag::COMPILE => return Err(DecodeError::new("a compile request carries no body")),
|
|
43
46
|
tag => Downcall::Unknown {
|
|
@@ -52,7 +55,7 @@ impl Downcall {
|
|
|
52
55
|
match self {
|
|
53
56
|
Downcall::Eval(source) => writer.u8(tag::EVAL).remainder(source),
|
|
54
57
|
Downcall::Run(run) => writer.u8(tag::RUN).remainder(&run.encode()),
|
|
55
|
-
Downcall::Yield
|
|
58
|
+
Downcall::Yield { frame, args } => writer.u8(tag::YIELD).u32(*frame).remainder(args),
|
|
56
59
|
Downcall::Compile => writer.u8(tag::COMPILE),
|
|
57
60
|
Downcall::Unknown { tag, body } => writer.u8(tag.get()).remainder(body),
|
|
58
61
|
};
|
|
@@ -146,14 +149,18 @@ mod tests {
|
|
|
146
149
|
(
|
|
147
150
|
Downcall::Eval(b"1".to_vec()).encode(),
|
|
148
151
|
Downcall::Run(run).encode(),
|
|
149
|
-
Downcall::Yield
|
|
152
|
+
Downcall::Yield {
|
|
153
|
+
frame: 2,
|
|
154
|
+
args: vec![0x90],
|
|
155
|
+
}
|
|
156
|
+
.encode(),
|
|
150
157
|
Downcall::Compile.encode(),
|
|
151
158
|
Upcall::Call(call).encode(),
|
|
152
159
|
),
|
|
153
160
|
(
|
|
154
161
|
vec![0x01, b'1'],
|
|
155
162
|
vec![0x02, 0, 0, 0, 1, b'M', 0x90],
|
|
156
|
-
vec![0x03, 0x90],
|
|
163
|
+
vec![0x03, 0, 0, 0, 2, 0x90],
|
|
157
164
|
vec![0x08],
|
|
158
165
|
vec![
|
|
159
166
|
0x04, // tag: call
|
|
@@ -177,7 +184,10 @@ mod tests {
|
|
|
177
184
|
entrypoint: "Main".into(),
|
|
178
185
|
payload: vec![0x90],
|
|
179
186
|
}),
|
|
180
|
-
Downcall::Yield
|
|
187
|
+
Downcall::Yield {
|
|
188
|
+
frame: 0,
|
|
189
|
+
args: vec![0x91, 0x01],
|
|
190
|
+
},
|
|
181
191
|
Downcall::Compile,
|
|
182
192
|
Downcall::Unknown {
|
|
183
193
|
tag: UnknownTag::new(0xf0).unwrap(),
|