kobako 0.27.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 +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- 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 +12 -2
- 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 +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -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/fault.rs +2 -1
- 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 +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -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 +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -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 +66 -59
- 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 +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- 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 +11 -13
- 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 +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -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 +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- 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 +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- 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 +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//!
|
|
1
|
+
//! The caps a `Driver` applies to every invocation.
|
|
2
2
|
//!
|
|
3
3
|
//! Every cap a frontend forwards into `Driver::new`. A plain value carrier
|
|
4
4
|
//! owned by the `Driver` — distinct from the process-wide engine/module
|
|
@@ -17,24 +17,78 @@ use kobako_runtime::profile::Profile;
|
|
|
17
17
|
|
|
18
18
|
/// The caps and requested isolation profile for one `Driver`. `None` on
|
|
19
19
|
/// any cap field disables that cap.
|
|
20
|
+
#[non_exhaustive]
|
|
20
21
|
pub struct Config {
|
|
21
|
-
/// Wall-clock cap for one guest `#eval` / `#run
|
|
22
|
-
///
|
|
22
|
+
/// Wall-clock cap for one guest `#eval` / `#run`; the invocation traps
|
|
23
|
+
/// once it passes.
|
|
23
24
|
pub timeout: Option<Duration>,
|
|
24
|
-
///
|
|
25
|
-
///
|
|
26
|
-
|
|
25
|
+
/// Wall-clock cap for setting the Guest Binary up the first time its
|
|
26
|
+
/// path is loaded; past it, construction fails.
|
|
27
|
+
pub setup_timeout: Option<Duration>,
|
|
28
|
+
/// Guest linear-memory cap, in bytes, charged to what one invocation
|
|
29
|
+
/// grows beyond the image it starts from.
|
|
27
30
|
pub memory_limit: Option<usize>,
|
|
28
|
-
/// Byte cap for guest stdout capture
|
|
29
|
-
///
|
|
30
|
-
/// flag in `Driver::build_snapshot`.
|
|
31
|
+
/// Byte cap for guest stdout capture; output past it is dropped and
|
|
32
|
+
/// the capture marked truncated.
|
|
31
33
|
pub stdout_limit: Option<usize>,
|
|
32
|
-
/// Byte cap for guest stderr capture
|
|
34
|
+
/// Byte cap for guest stderr capture, the same as `stdout_limit`.
|
|
33
35
|
pub stderr_limit: Option<usize>,
|
|
34
|
-
///
|
|
35
|
-
///
|
|
36
|
-
|
|
37
|
-
///
|
|
38
|
-
/// the
|
|
36
|
+
/// Descriptors one invocation may hold open, the standard ones and
|
|
37
|
+
/// each Mount's preopen included; an open past it answers `MFILE`.
|
|
38
|
+
pub open_file_limit: Option<usize>,
|
|
39
|
+
/// Isolation posture the frontend requested. `Hermetic` freezes
|
|
40
|
+
/// ambient time and entropy, `Permissive` leaves the live WASI
|
|
41
|
+
/// sources, and `Driver::profile` declares it back as the built
|
|
42
|
+
/// posture.
|
|
39
43
|
pub profile: Profile,
|
|
40
44
|
}
|
|
45
|
+
|
|
46
|
+
impl Default for Config {
|
|
47
|
+
/// The caps every frontend defaults to: a 60-second deadline, 10
|
|
48
|
+
/// seconds to set the Guest Binary up, 4 MiB for memory, 1 MiB for
|
|
49
|
+
/// each output channel, 1024 open files, and the hermetic floor.
|
|
50
|
+
fn default() -> Self {
|
|
51
|
+
Config {
|
|
52
|
+
timeout: Some(Duration::from_secs(60)),
|
|
53
|
+
setup_timeout: Some(Duration::from_secs(10)),
|
|
54
|
+
memory_limit: Some(4 << 20),
|
|
55
|
+
stdout_limit: Some(1 << 20),
|
|
56
|
+
stderr_limit: Some(1 << 20),
|
|
57
|
+
open_file_limit: Some(1024),
|
|
58
|
+
profile: Profile::Hermetic,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[cfg(test)]
|
|
64
|
+
mod tests {
|
|
65
|
+
use super::*;
|
|
66
|
+
|
|
67
|
+
// Literal values, so a drift from the defaults the SDK's Options and
|
|
68
|
+
// the Ruby frontend apply fails here.
|
|
69
|
+
#[test]
|
|
70
|
+
fn a_default_config_carries_every_frontends_default_caps() {
|
|
71
|
+
let config = Config::default();
|
|
72
|
+
assert_eq!(
|
|
73
|
+
(
|
|
74
|
+
config.timeout,
|
|
75
|
+
config.setup_timeout,
|
|
76
|
+
config.memory_limit,
|
|
77
|
+
config.stdout_limit,
|
|
78
|
+
config.stderr_limit,
|
|
79
|
+
config.open_file_limit,
|
|
80
|
+
config.profile
|
|
81
|
+
),
|
|
82
|
+
(
|
|
83
|
+
Some(Duration::from_secs(60)),
|
|
84
|
+
Some(Duration::from_secs(10)),
|
|
85
|
+
Some(4 * 1024 * 1024),
|
|
86
|
+
Some(1024 * 1024),
|
|
87
|
+
Some(1024 * 1024),
|
|
88
|
+
Some(1024),
|
|
89
|
+
Profile::Hermetic
|
|
90
|
+
),
|
|
91
|
+
"Config::default must carry the caps every frontend defaults to"
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//! The guest's descriptor table: what each preview1 file descriptor
|
|
2
|
+
//! stands for within one invocation.
|
|
3
|
+
//!
|
|
4
|
+
//! kobako answers every preview1 call that reads this table, so the guest
|
|
5
|
+
//! sees one table and no call falls through to a second one that
|
|
6
|
+
//! disagrees. Numbering follows wasmtime-wasi's: a closed number is reused
|
|
7
|
+
//! before a fresh one is taken.
|
|
8
|
+
|
|
9
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
|
|
12
|
+
use kobako_runtime::filesystem::{DirEntry, File, Hold, Metadata};
|
|
13
|
+
use kobako_runtime::mount::Mount;
|
|
14
|
+
use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
|
|
15
|
+
|
|
16
|
+
/// What one descriptor stands for.
|
|
17
|
+
pub(crate) enum Descriptor {
|
|
18
|
+
/// Descriptor 0, carrying the Preamble.
|
|
19
|
+
Stdin(MemoryInputPipe),
|
|
20
|
+
/// Descriptor 1 or 2, a capture pipe the guest writes a Channel into.
|
|
21
|
+
Channel(MemoryOutputPipe),
|
|
22
|
+
/// A directory beneath a Mount; a preopen is the Mount's own root.
|
|
23
|
+
Directory(OpenDirectory),
|
|
24
|
+
File(OpenFile),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// What a directory is, apart from where it stands: the device and inode
|
|
28
|
+
/// numbers its Filesystem names it by.
|
|
29
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
30
|
+
pub(crate) struct Identity {
|
|
31
|
+
dev: u64,
|
|
32
|
+
ino: u64,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Identity {
|
|
36
|
+
/// The identity `metadata` names, or `None` where its inode of zero
|
|
37
|
+
/// reports none.
|
|
38
|
+
pub(crate) fn of(metadata: &Metadata) -> Option<Self> {
|
|
39
|
+
(metadata.ino != 0).then_some(Self {
|
|
40
|
+
dev: metadata.dev,
|
|
41
|
+
ino: metadata.ino,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pub(crate) struct OpenDirectory {
|
|
47
|
+
pub(crate) mount: Arc<Mount>,
|
|
48
|
+
/// The directory's path relative to the mount's root.
|
|
49
|
+
pub(crate) path: String,
|
|
50
|
+
/// The identity the directory had when it was opened, so one made in
|
|
51
|
+
/// its place is not taken for it; without one it follows its path.
|
|
52
|
+
pub(crate) identity: Option<Identity>,
|
|
53
|
+
/// What keeps that identity from passing to a directory made in this
|
|
54
|
+
/// one's place while the descriptor is open.
|
|
55
|
+
pub(crate) _hold: Option<Hold>,
|
|
56
|
+
pub(crate) preopen: bool,
|
|
57
|
+
/// The entries a listing began with, held until it reaches its end, so
|
|
58
|
+
/// each later call reads on without listing the directory again.
|
|
59
|
+
pub(crate) listing: Option<Vec<DirEntry>>,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub(crate) struct OpenFile {
|
|
63
|
+
pub(crate) file: Box<dyn File>,
|
|
64
|
+
pub(crate) position: u64,
|
|
65
|
+
pub(crate) append: bool,
|
|
66
|
+
pub(crate) readable: bool,
|
|
67
|
+
pub(crate) writable: bool,
|
|
68
|
+
/// The Mount it was opened beneath, whose permission to write answers a
|
|
69
|
+
/// wait to write and a resize, as wasmtime-wasi answers both by its
|
|
70
|
+
/// preopen's permissions.
|
|
71
|
+
pub(crate) mount: Arc<Mount>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pub(crate) struct Descriptors {
|
|
75
|
+
used: BTreeMap<u32, Descriptor>,
|
|
76
|
+
free: BTreeSet<u32>,
|
|
77
|
+
limit: Option<usize>,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
impl Descriptors {
|
|
81
|
+
/// The table every invocation starts from: stdin, then the two
|
|
82
|
+
/// Channels, then one preopen per Mount from descriptor 3. It holds at
|
|
83
|
+
/// most `limit` descriptors, those included, as a process's
|
|
84
|
+
/// `RLIMIT_NOFILE` counts every one it holds.
|
|
85
|
+
pub(crate) fn new(
|
|
86
|
+
stdin: MemoryInputPipe,
|
|
87
|
+
stdout: MemoryOutputPipe,
|
|
88
|
+
stderr: MemoryOutputPipe,
|
|
89
|
+
mounts: Vec<Mount>,
|
|
90
|
+
limit: Option<usize>,
|
|
91
|
+
) -> Self {
|
|
92
|
+
let mut used = BTreeMap::from([
|
|
93
|
+
(0, Descriptor::Stdin(stdin)),
|
|
94
|
+
(1, Descriptor::Channel(stdout)),
|
|
95
|
+
(2, Descriptor::Channel(stderr)),
|
|
96
|
+
]);
|
|
97
|
+
for (fd, mount) in (3..).zip(mounts) {
|
|
98
|
+
// A root the Filesystem cannot stat or hold reports no
|
|
99
|
+
// identity, and the descriptor follows its path alone.
|
|
100
|
+
let filesystem = mount.filesystem();
|
|
101
|
+
let identity = filesystem
|
|
102
|
+
.metadata("")
|
|
103
|
+
.ok()
|
|
104
|
+
.and_then(|root| Identity::of(&root));
|
|
105
|
+
let hold = filesystem.hold_directory("").ok().flatten();
|
|
106
|
+
used.insert(
|
|
107
|
+
fd,
|
|
108
|
+
Descriptor::Directory(OpenDirectory {
|
|
109
|
+
mount: Arc::new(mount),
|
|
110
|
+
path: String::new(),
|
|
111
|
+
identity,
|
|
112
|
+
_hold: hold,
|
|
113
|
+
preopen: true,
|
|
114
|
+
listing: None,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
Self {
|
|
119
|
+
used,
|
|
120
|
+
free: BTreeSet::new(),
|
|
121
|
+
limit,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub(crate) fn get(&self, fd: u32) -> Option<&Descriptor> {
|
|
126
|
+
self.used.get(&fd)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub(crate) fn get_mut(&mut self, fd: u32) -> Option<&mut Descriptor> {
|
|
130
|
+
self.used.get_mut(&fd)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Take `fd` out of the table, freeing its number for reuse.
|
|
134
|
+
pub(crate) fn remove(&mut self, fd: u32) -> Option<Descriptor> {
|
|
135
|
+
let descriptor = self.used.remove(&fd)?;
|
|
136
|
+
self.free.insert(fd);
|
|
137
|
+
Some(descriptor)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Place `descriptor` at `fd`, a number the caller has just freed.
|
|
141
|
+
pub(crate) fn place(&mut self, fd: u32, descriptor: Descriptor) {
|
|
142
|
+
self.free.remove(&fd);
|
|
143
|
+
self.used.insert(fd, descriptor);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Whether the table holds as many descriptors as it may.
|
|
147
|
+
pub(crate) fn is_full(&self) -> bool {
|
|
148
|
+
self.limit.is_some_and(|limit| self.used.len() >= limit)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Add `descriptor` under the highest freed number, or the one past
|
|
152
|
+
/// the highest in use; `None` when the table is full.
|
|
153
|
+
pub(crate) fn push(&mut self, descriptor: Descriptor) -> Option<u32> {
|
|
154
|
+
if self.is_full() {
|
|
155
|
+
return None;
|
|
156
|
+
}
|
|
157
|
+
let fd = match self.free.pop_last() {
|
|
158
|
+
Some(fd) => fd,
|
|
159
|
+
None => match self.used.last_key_value() {
|
|
160
|
+
Some((last, _)) => last.checked_add(1)?,
|
|
161
|
+
None => 0,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
self.used.insert(fd, descriptor);
|
|
165
|
+
Some(fd)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -1,89 +1,99 @@
|
|
|
1
|
-
//! Host
|
|
1
|
+
//! Host side of the `__kobako_upcall` import: hand the guest's Call to
|
|
2
|
+
//! the bound `DispatchHandler` and write its Reply back into guest memory.
|
|
3
|
+
//! A request under a tag the host does not serve is answered as
|
|
4
|
+
//! unsupported, so a guest asking for a later operation can fall back.
|
|
2
5
|
//!
|
|
3
|
-
//!
|
|
4
|
-
//!
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
7
|
-
//!
|
|
8
|
-
//!
|
|
9
|
-
//! 2. Invokes the bound `DispatchHandler` (the frontend's dispatch
|
|
10
|
-
//! bridge, e.g. a Ruby Proc) and recovers Reply bytes.
|
|
11
|
-
//! 3. Allocates a guest buffer via `__kobako_alloc(len)` invoked
|
|
12
|
-
//! through `Caller::get_export`.
|
|
13
|
-
//! 4. Writes the Reply bytes into the guest buffer.
|
|
14
|
-
//! 5. Returns packed `(ptr<<32)|len` for the guest to decode.
|
|
15
|
-
//!
|
|
16
|
-
//! Returns 0 on any step failure. `Kobako::Sandbox#initialize` always
|
|
17
|
-
//! installs the dispatch handler before any invocation, so reaching the
|
|
18
|
-
//! dispatcher with no handler bound is itself a wire-layer fault; the
|
|
19
|
-
//! guest maps a 0 return to a trap. Failures during normal dispatch
|
|
20
|
-
//! take the Reply's fault arm from
|
|
21
|
-
//! `Kobako::Transport::Dispatcher.dispatch` itself — they never reach
|
|
22
|
-
//! this 0-return path.
|
|
23
|
-
//!
|
|
24
|
-
//! ## Why this module writes to `stderr`
|
|
25
|
-
//!
|
|
26
|
-
//! This file is the one place in the driver that deliberately prints
|
|
27
|
-
//! through `eprintln!`. The host normally surfaces faults through the
|
|
28
|
-
//! contract's error channels; the dispatcher contract is the exception
|
|
29
|
-
//! — it must return a packed `i64` to the guest and cannot fail, so a
|
|
30
|
-
//! 0 return is the only signal the wasm side receives. The guest collapses every 0 into the same trap, so the
|
|
31
|
-
//! Ruby host has no way to attribute the failure to a specific step
|
|
32
|
-
//! (missing `memory` export vs. no dispatch handler bound vs. the
|
|
33
|
-
//! handler raised vs. `__kobako_alloc` returned 0 vs. `memory.write`
|
|
34
|
-
//! rejected).
|
|
35
|
-
//!
|
|
36
|
-
//! `handle` writes a single `[kobako-dispatch] <reason>` line to
|
|
37
|
-
//! `stderr` on each failure path so operators have a breadcrumb to
|
|
38
|
-
//! correlate the trap with the actual cause. The line is emitted in
|
|
39
|
-
//! both debug and release builds on purpose: dispatcher failures are
|
|
40
|
-
//! wire-layer faults rather than expected error paths (`Kobako::Sandbox`
|
|
41
|
-
//! always installs the handler, the handler is contracted never to
|
|
42
|
-
//! raise, etc.), so the "release-build noise" cost is bounded — under
|
|
43
|
-
//! normal operation the line is never written. Operators that need to
|
|
44
|
-
//! silence the stream can redirect the host process's stderr, but the
|
|
45
|
-
//! kobako convention is "ext never logs" plus this single, named
|
|
46
|
-
//! exception.
|
|
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.
|
|
47
12
|
|
|
48
13
|
use wasmtime::Caller;
|
|
49
14
|
|
|
50
|
-
use
|
|
15
|
+
use kobako_runtime::error::Trap;
|
|
16
|
+
use kobako_runtime::yielder::Yielder;
|
|
17
|
+
use kobako_transport::abi::pack_ptr_len;
|
|
18
|
+
use kobako_transport::envelope::{Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply};
|
|
51
19
|
|
|
20
|
+
use crate::guest_mem;
|
|
52
21
|
use crate::invocation::Invocation;
|
|
53
22
|
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
23
|
+
/// The wasmtime-backed `Yielder`: built per `__kobako_upcall` frame over
|
|
24
|
+
/// that frame's `Caller`, so nested dispatch frames each carry their own and
|
|
25
|
+
/// stack on the Rust call stack with no shared slot.
|
|
26
|
+
struct CallerYielder<'a, 'c> {
|
|
27
|
+
caller: &'a mut Caller<'c, Invocation>,
|
|
28
|
+
frame: u32,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Yielder for CallerYielder<'_, '_> {
|
|
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)
|
|
38
|
+
.map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
39
|
+
YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
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.
|
|
45
|
+
pub(crate) fn handle(
|
|
46
|
+
caller: &mut Caller<'_, Invocation>,
|
|
47
|
+
req_ptr: i32,
|
|
48
|
+
req_len: i32,
|
|
49
|
+
) -> wasmtime::Result<i64> {
|
|
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());
|
|
53
|
+
if let Some(trap) = caller.data_mut().take_reentry_trap() {
|
|
54
|
+
return Err(trap);
|
|
55
|
+
}
|
|
56
|
+
if let Some(trap) = caller.data().abandonment() {
|
|
57
|
+
return Err(trap);
|
|
58
|
+
}
|
|
59
|
+
written.map_err(wasmtime::Error::msg)
|
|
60
|
+
}
|
|
61
|
+
|
|
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
|
+
)),
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
/// failure path carries a diagnostic string instead of an opaque 0.
|
|
76
|
-
fn try_handle(
|
|
75
|
+
fn reply_to(
|
|
77
76
|
caller: &mut Caller<'_, Invocation>,
|
|
78
77
|
req_ptr: i32,
|
|
79
78
|
req_len: i32,
|
|
80
|
-
) -> Result<
|
|
81
|
-
let req_bytes =
|
|
82
|
-
// The driver decodes the core envelope
|
|
83
|
-
// frame; the payload inside it stays bytes
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
) -> Result<Reply, &'static str> {
|
|
80
|
+
let req_bytes = guest_mem::read_request(caller, req_ptr, req_len)?;
|
|
81
|
+
// The driver decodes the core envelope, here and in `CallerYielder`, so
|
|
82
|
+
// the frontend never sees a frame; the payload inside it stays bytes
|
|
83
|
+
// the whole way through.
|
|
84
|
+
let call = match Upcall::decode(&req_bytes) {
|
|
85
|
+
Ok(Upcall::Call(call)) => call,
|
|
86
|
+
Ok(_) => {
|
|
87
|
+
return Ok(Reply::Unsupported(Unsupported::new(
|
|
88
|
+
"the host does not serve this request",
|
|
89
|
+
)))
|
|
90
|
+
}
|
|
91
|
+
Err(_) => {
|
|
92
|
+
return Err(
|
|
93
|
+
"the guest sent a malformed Call envelope — please report this as a kobako bug",
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
};
|
|
87
97
|
|
|
88
98
|
// `Kobako::Sandbox` always installs the dispatch handler before
|
|
89
99
|
// invoking the runtime, so reaching this branch indicates a misuse
|
|
@@ -91,27 +101,25 @@ fn try_handle(
|
|
|
91
101
|
let handler = caller
|
|
92
102
|
.data()
|
|
93
103
|
.on_dispatch()
|
|
94
|
-
.ok_or("a Sandbox callback fired outside an active
|
|
104
|
+
.ok_or("a Sandbox callback fired outside an active invocation — please report this as a kobako bug")?;
|
|
95
105
|
|
|
96
106
|
// Build a frame-scoped yielder over this Caller and hand it to the
|
|
97
107
|
// handler. The borrow ends with the block, freeing the Caller for
|
|
98
|
-
// `
|
|
108
|
+
// `write_reply`; nested dispatch frames each build their own, so
|
|
99
109
|
// the LIFO re-entry lives on the Rust stack — no shared slot.
|
|
110
|
+
let frame = caller.data_mut().open_call();
|
|
100
111
|
let reply = {
|
|
101
|
-
let mut yielder =
|
|
112
|
+
let mut yielder = CallerYielder { caller, frame };
|
|
102
113
|
handler.dispatch(call, &mut yielder)
|
|
103
|
-
}
|
|
104
|
-
.
|
|
114
|
+
};
|
|
115
|
+
caller.data_mut().close_call();
|
|
116
|
+
reply.ok_or(
|
|
105
117
|
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
write_response(caller, &reply.encode())
|
|
118
|
+
)
|
|
109
119
|
}
|
|
110
120
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let ptr = crate::guest_mem::alloc_and_write(caller, bytes)?;
|
|
116
|
-
Ok(((ptr as i64) << 32) | (bytes.len() as i64))
|
|
121
|
+
fn write_reply(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
|
|
122
|
+
let ptr = guest_mem::write_from_callback(caller, bytes)?;
|
|
123
|
+
// The write has already held the length to the payload cap.
|
|
124
|
+
Ok(pack_ptr_len(ptr, bytes.len() as u32) as i64)
|
|
117
125
|
}
|