kobako 0.28.0 → 0.30.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 +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -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/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- 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 +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.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 +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- 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 +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//! Guest linear-memory I/O for the ABI's buffer handoffs, whether the
|
|
2
|
+
//! invocation body or an upcall drives one.
|
|
3
|
+
//!
|
|
4
|
+
//! The two shared steps answer an error kind, and each caller words it,
|
|
5
|
+
//! because depth decides what the same problem means: before the export
|
|
6
|
+
//! call it is a `Trap` or an intact runtime, inside a callback it is a
|
|
7
|
+
//! wire failure the guest receives.
|
|
8
|
+
|
|
9
|
+
use kobako_transport::abi::{unpack_ptr_len, MAX_DISPATCH_PAYLOAD};
|
|
10
|
+
|
|
11
|
+
use super::{GuestInstance, RUNTIME_INCOMPATIBLE, SANDBOX_RUNTIME_NOT_KOBAKO};
|
|
12
|
+
|
|
13
|
+
// The size-limit messages here and in the endpoint name the cap as 16 MiB, so
|
|
14
|
+
// moving the cap fails the build until they are reworded with it.
|
|
15
|
+
const _: () = assert!(MAX_DISPATCH_PAYLOAD == 16 << 20);
|
|
16
|
+
|
|
17
|
+
/// Why a host write into guest memory failed.
|
|
18
|
+
pub(super) enum WriteError<F> {
|
|
19
|
+
TooLarge,
|
|
20
|
+
/// The allocator or the memory is missing or mistyped; carries the
|
|
21
|
+
/// message for it.
|
|
22
|
+
Incompatible(&'static str),
|
|
23
|
+
Trapped(F),
|
|
24
|
+
/// The allocator ran and answered 0.
|
|
25
|
+
OutOfMemory,
|
|
26
|
+
OutOfBounds,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Why a host read of a guest-named buffer failed.
|
|
30
|
+
pub(super) enum ReadError {
|
|
31
|
+
TooLarge,
|
|
32
|
+
OutOfBounds,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Allocate a guest buffer and copy `bytes` into it, answering the buffer's
|
|
36
|
+
/// address.
|
|
37
|
+
pub(super) fn alloc_and_write<G: GuestInstance>(
|
|
38
|
+
guest: &mut G,
|
|
39
|
+
bytes: &[u8],
|
|
40
|
+
) -> Result<u32, WriteError<G::Fault>> {
|
|
41
|
+
let len = checked_payload_len(bytes.len()).map_err(|_| WriteError::TooLarge)?;
|
|
42
|
+
if guest.memory().is_none() {
|
|
43
|
+
return Err(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO));
|
|
44
|
+
}
|
|
45
|
+
let ptr = guest
|
|
46
|
+
.alloc(len as u32)
|
|
47
|
+
.ok_or(WriteError::Incompatible(RUNTIME_INCOMPATIBLE))?
|
|
48
|
+
.map_err(WriteError::Trapped)?;
|
|
49
|
+
if ptr == 0 {
|
|
50
|
+
return Err(WriteError::OutOfMemory);
|
|
51
|
+
}
|
|
52
|
+
let memory = guest
|
|
53
|
+
.memory()
|
|
54
|
+
.ok_or(WriteError::Incompatible(SANDBOX_RUNTIME_NOT_KOBAKO))?;
|
|
55
|
+
let range = guest_buffer_range(ptr as usize, bytes.len(), memory.len())
|
|
56
|
+
.map_err(|_| WriteError::OutOfBounds)?;
|
|
57
|
+
memory[range].copy_from_slice(bytes);
|
|
58
|
+
Ok(ptr)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Copy out the buffer the guest names; a length past the 16 MiB cap is
|
|
62
|
+
/// refused before memory is touched.
|
|
63
|
+
pub(super) fn read_buffer(data: &[u8], ptr: usize, len: usize) -> Result<Vec<u8>, ReadError> {
|
|
64
|
+
if len > MAX_DISPATCH_PAYLOAD {
|
|
65
|
+
return Err(ReadError::TooLarge);
|
|
66
|
+
}
|
|
67
|
+
let range = guest_buffer_range(ptr, len, data.len()).map_err(|_| ReadError::OutOfBounds)?;
|
|
68
|
+
Ok(data[range].to_vec())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Every host write boundary routes its length through here, so the
|
|
72
|
+
/// wire-violation reason is uniform.
|
|
73
|
+
pub(super) fn checked_payload_len(len: usize) -> Result<i32, &'static str> {
|
|
74
|
+
if len > MAX_DISPATCH_PAYLOAD {
|
|
75
|
+
return Err("payload exceeds the 16 MiB limit");
|
|
76
|
+
}
|
|
77
|
+
// The cap above sits below `i32::MAX`, so this conversion cannot wrap.
|
|
78
|
+
i32::try_from(len).map_err(|_| "payload exceeds the 16 MiB limit")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// The bytes of guest memory a `(ptr, len)` names.
|
|
82
|
+
fn guest_buffer_range(
|
|
83
|
+
ptr: usize,
|
|
84
|
+
len: usize,
|
|
85
|
+
mem_size: usize,
|
|
86
|
+
) -> Result<core::ops::Range<usize>, &'static str> {
|
|
87
|
+
let end = ptr.checked_add(len).ok_or("ptr + len overflow")?;
|
|
88
|
+
if end > mem_size {
|
|
89
|
+
return Err("range exceeds Sandbox memory size");
|
|
90
|
+
}
|
|
91
|
+
Ok(ptr..end)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// The `(ptr, len)` the guest packs for any answer it hands back, the
|
|
95
|
+
/// outcome and a block's result alike.
|
|
96
|
+
pub(super) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
|
|
97
|
+
let (ptr, len) = unpack_ptr_len(packed);
|
|
98
|
+
(ptr as usize, len as usize)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
use super::{
|
|
104
|
+
checked_payload_len, guest_buffer_range, unpack_guest_buffer, MAX_DISPATCH_PAYLOAD,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// @behavior WE-059
|
|
108
|
+
#[test]
|
|
109
|
+
fn a_transfer_of_nothing_or_at_the_cap_is_admitted() {
|
|
110
|
+
assert_eq!(
|
|
111
|
+
(
|
|
112
|
+
checked_payload_len(0),
|
|
113
|
+
checked_payload_len(MAX_DISPATCH_PAYLOAD)
|
|
114
|
+
),
|
|
115
|
+
(Ok(0), Ok(MAX_DISPATCH_PAYLOAD as i32)),
|
|
116
|
+
"0 bytes and MAX_DISPATCH_PAYLOAD bytes through checked_payload_len must be admitted"
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// @behavior WE-060
|
|
121
|
+
#[test]
|
|
122
|
+
fn a_transfer_past_the_cap_is_refused() {
|
|
123
|
+
assert!(
|
|
124
|
+
checked_payload_len(MAX_DISPATCH_PAYLOAD + 1).is_err()
|
|
125
|
+
&& checked_payload_len(usize::MAX).is_err(),
|
|
126
|
+
"one byte past MAX_DISPATCH_PAYLOAD and usize::MAX through checked_payload_len must be \
|
|
127
|
+
refused"
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// @behavior WE-061
|
|
132
|
+
#[test]
|
|
133
|
+
fn a_guest_buffer_is_read_half_open() {
|
|
134
|
+
assert_eq!(
|
|
135
|
+
guest_buffer_range(10, 5, 100),
|
|
136
|
+
Ok(10..15),
|
|
137
|
+
"5 bytes at 10 in 100 bytes of memory through guest_buffer_range must answer 10..15"
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// @behavior WE-062
|
|
142
|
+
#[test]
|
|
143
|
+
fn a_buffer_of_no_length_is_read_at_any_pointer_inside_memory() {
|
|
144
|
+
assert_eq!(
|
|
145
|
+
(guest_buffer_range(0, 0, 0), guest_buffer_range(42, 0, 100)),
|
|
146
|
+
(Ok(0..0), Ok(42..42)),
|
|
147
|
+
"no bytes at 0 in no memory and at 42 in 100 bytes through guest_buffer_range must \
|
|
148
|
+
answer an empty range there"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// @behavior WE-063
|
|
153
|
+
#[test]
|
|
154
|
+
fn a_pointer_and_length_that_overflow_are_refused() {
|
|
155
|
+
assert!(
|
|
156
|
+
guest_buffer_range(usize::MAX, 1, usize::MAX).is_err(),
|
|
157
|
+
"1 byte at usize::MAX through guest_buffer_range must be refused"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// @behavior WE-064
|
|
162
|
+
#[test]
|
|
163
|
+
fn a_buffer_ending_past_memory_is_refused() {
|
|
164
|
+
assert_eq!(
|
|
165
|
+
(guest_buffer_range(10, 100, 50).is_err(), guest_buffer_range(0, 50, 50)),
|
|
166
|
+
(true, Ok(0..50)),
|
|
167
|
+
"100 bytes at 10 in 50 bytes of memory through guest_buffer_range must be refused, and \
|
|
168
|
+
50 bytes at 0 admitted"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// @behavior WE-001
|
|
173
|
+
#[test]
|
|
174
|
+
fn a_packed_answer_carries_the_pointer_high_and_the_length_low() {
|
|
175
|
+
assert_eq!(
|
|
176
|
+
unpack_guest_buffer(0xAABB_CCDD_1122_3344),
|
|
177
|
+
(0xAABB_CCDD, 0x1122_3344),
|
|
178
|
+
"0xAABB_CCDD_1122_3344 through unpack_guest_buffer must answer pointer 0xAABB_CCDD and \
|
|
179
|
+
length 0x1122_3344"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// @behavior WE-065
|
|
184
|
+
#[test]
|
|
185
|
+
fn a_packed_answer_of_nothing_is_a_zero_pair() {
|
|
186
|
+
assert_eq!(
|
|
187
|
+
unpack_guest_buffer(0),
|
|
188
|
+
(0, 0),
|
|
189
|
+
"0 through unpack_guest_buffer must answer pointer 0 and length 0"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//! Per-channel stdout / stderr capture sizing and clipping.
|
|
2
|
+
//!
|
|
3
|
+
//! Two pure helpers shared by the run path: one sizes the per-run pipe, the
|
|
4
|
+
//! other clips a captured snapshot back to the configured limit and reports
|
|
5
|
+
//! whether the limit was passed. Kept channel-agnostic (a function of `limit`,
|
|
6
|
+
//! not of which channel) so a regression that only breaks one channel cannot
|
|
7
|
+
//! sneak through the test that pins them.
|
|
8
|
+
|
|
9
|
+
/// One byte past the limit, so "wrote exactly the limit" and "passed the
|
|
10
|
+
/// limit" stay distinguishable.
|
|
11
|
+
pub fn pipe_capacity(limit: Option<usize>) -> usize {
|
|
12
|
+
match limit {
|
|
13
|
+
Some(limit) => limit.saturating_add(1),
|
|
14
|
+
None => usize::MAX,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Takes the snapshot by value and truncates in place, so the readback
|
|
19
|
+
/// costs one copy out of the pipe rather than two.
|
|
20
|
+
pub fn clip_capture(mut raw: Vec<u8>, limit: Option<usize>) -> (Vec<u8>, bool) {
|
|
21
|
+
match limit {
|
|
22
|
+
Some(limit) if raw.len() > limit => {
|
|
23
|
+
raw.truncate(limit);
|
|
24
|
+
(raw, true)
|
|
25
|
+
}
|
|
26
|
+
_ => (raw, false),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[cfg(test)]
|
|
31
|
+
mod tests {
|
|
32
|
+
use super::{clip_capture, pipe_capacity};
|
|
33
|
+
|
|
34
|
+
// @behavior S-108
|
|
35
|
+
#[test]
|
|
36
|
+
fn a_pipe_holds_one_byte_past_its_limit() {
|
|
37
|
+
assert_eq!(
|
|
38
|
+
(pipe_capacity(Some(5)), pipe_capacity(Some(0))),
|
|
39
|
+
(6, 1),
|
|
40
|
+
"a limit of 5 and of 0 through pipe_capacity must size the pipe 6 and 1"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// @behavior S-109
|
|
45
|
+
#[test]
|
|
46
|
+
fn a_pipe_without_a_limit_holds_usize_max() {
|
|
47
|
+
assert_eq!(
|
|
48
|
+
pipe_capacity(None),
|
|
49
|
+
usize::MAX,
|
|
50
|
+
"no limit through pipe_capacity must size the pipe usize::MAX"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// @behavior S-110
|
|
55
|
+
#[test]
|
|
56
|
+
fn a_pipe_past_usize_max_saturates() {
|
|
57
|
+
assert_eq!(
|
|
58
|
+
pipe_capacity(Some(usize::MAX)),
|
|
59
|
+
usize::MAX,
|
|
60
|
+
"a limit of usize::MAX through pipe_capacity must saturate at usize::MAX"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// @behavior S-111
|
|
65
|
+
#[test]
|
|
66
|
+
fn a_capture_under_its_limit_stays_whole() {
|
|
67
|
+
assert_eq!(
|
|
68
|
+
clip_capture(b"abc".to_vec(), Some(5)),
|
|
69
|
+
(b"abc".to_vec(), false),
|
|
70
|
+
"3 bytes under a limit of 5 through clip_capture must stay whole and untruncated"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// @behavior S-112
|
|
75
|
+
#[test]
|
|
76
|
+
fn a_capture_at_its_limit_is_not_truncated() {
|
|
77
|
+
assert_eq!(
|
|
78
|
+
clip_capture(b"abcde".to_vec(), Some(5)),
|
|
79
|
+
(b"abcde".to_vec(), false),
|
|
80
|
+
"5 bytes at a limit of 5 through clip_capture must stay whole and untruncated"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// @behavior S-024
|
|
85
|
+
#[test]
|
|
86
|
+
fn a_capture_past_its_limit_is_clipped_and_truncated() {
|
|
87
|
+
// The pipe holds one byte past the limit, so 6 bytes is the most a
|
|
88
|
+
// limit of 5 can capture.
|
|
89
|
+
assert_eq!(
|
|
90
|
+
clip_capture(b"abcdef".to_vec(), Some(5)),
|
|
91
|
+
(b"abcde".to_vec(), true),
|
|
92
|
+
"6 bytes past a limit of 5 through clip_capture must clip to 5 and flag truncation"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// @behavior S-033
|
|
97
|
+
#[test]
|
|
98
|
+
fn a_capture_without_a_limit_stays_whole() {
|
|
99
|
+
assert_eq!(
|
|
100
|
+
clip_capture(b"abcdef".to_vec(), None),
|
|
101
|
+
(b"abcdef".to_vec(), false),
|
|
102
|
+
"6 bytes with no limit through clip_capture must stay whole and untruncated"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// @behavior S-100 S-101
|
|
107
|
+
#[test]
|
|
108
|
+
fn an_empty_capture_stays_empty() {
|
|
109
|
+
assert_eq!(
|
|
110
|
+
clip_capture(Vec::new(), Some(5)),
|
|
111
|
+
(Vec::new(), false),
|
|
112
|
+
"no bytes under a limit of 5 through clip_capture must stay empty and untruncated"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
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.
|
|
5
|
+
//!
|
|
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.
|
|
12
|
+
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
|
|
15
|
+
use kobako_transport::abi::pack_ptr_len;
|
|
16
|
+
use kobako_transport::envelope::{
|
|
17
|
+
Downcall, Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
use super::buffer::{
|
|
21
|
+
alloc_and_write, checked_payload_len, read_buffer, unpack_guest_buffer, ReadError, WriteError,
|
|
22
|
+
};
|
|
23
|
+
use super::{GuestInstance, RUNTIME_INCOMPATIBLE, SANDBOX_RUNTIME_NOT_KOBAKO};
|
|
24
|
+
use crate::dispatch::DispatchHandler;
|
|
25
|
+
use crate::error::Trap;
|
|
26
|
+
use crate::yielder::Yielder;
|
|
27
|
+
|
|
28
|
+
/// The message of the trap ending an invocation its host gave up on.
|
|
29
|
+
pub const ABANDONED: &str = "the host abandoned the invocation";
|
|
30
|
+
|
|
31
|
+
/// The state of the Calls open on one instance: who answers them, how many
|
|
32
|
+
/// stand open, and the first trap a callback into the guest raised, as the
|
|
33
|
+
/// guest counts the Calls it has open.
|
|
34
|
+
pub struct Calls<F> {
|
|
35
|
+
on_dispatch: Option<Arc<dyn DispatchHandler>>,
|
|
36
|
+
open: u32,
|
|
37
|
+
reentry_trap: Option<F>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl<F> Default for Calls<F> {
|
|
41
|
+
fn default() -> Self {
|
|
42
|
+
Self {
|
|
43
|
+
on_dispatch: None,
|
|
44
|
+
open: 0,
|
|
45
|
+
reentry_trap: None,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl<F> Calls<F> {
|
|
51
|
+
/// Have `handler` answer the Calls on this instance.
|
|
52
|
+
pub fn bind(&mut self, handler: Arc<dyn DispatchHandler>) {
|
|
53
|
+
self.on_dispatch = Some(handler);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Whether its host gave up on the invocation, which ends at the next
|
|
57
|
+
/// host call it answers.
|
|
58
|
+
pub fn abandoned(&self) -> bool {
|
|
59
|
+
self.on_dispatch
|
|
60
|
+
.as_ref()
|
|
61
|
+
.is_some_and(|handler| handler.abandoned())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// How an upcall ends an invocation instead of answering.
|
|
66
|
+
pub enum UpcallEnd<F> {
|
|
67
|
+
/// The guest trapped during a callback into it.
|
|
68
|
+
Fault(F),
|
|
69
|
+
/// The host gave the invocation up.
|
|
70
|
+
Abandoned,
|
|
71
|
+
/// The answer could not be written into guest memory, naming why.
|
|
72
|
+
Unwritable(&'static str),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// The Yielder of one upcall's frame, built over that frame's guest, so nested
|
|
76
|
+
/// dispatch frames each carry their own and stack on the call stack with no
|
|
77
|
+
/// shared slot.
|
|
78
|
+
struct FrameYielder<'a, G> {
|
|
79
|
+
guest: &'a mut G,
|
|
80
|
+
frame: u32,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl<G: GuestInstance> Yielder for FrameYielder<'_, G> {
|
|
84
|
+
fn frame(&self) -> u32 {
|
|
85
|
+
self.frame
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap> {
|
|
89
|
+
let bytes =
|
|
90
|
+
drive_yield(self.guest, frame, args).map_err(|msg| Trap::Other(msg.to_string()))?;
|
|
91
|
+
YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Answer one `__kobako_upcall` call with the packed `(ptr<<32)|len` of
|
|
96
|
+
/// the Reply, or what ends the invocation instead.
|
|
97
|
+
pub fn handle<G: GuestInstance>(
|
|
98
|
+
guest: &mut G,
|
|
99
|
+
req_ptr: i32,
|
|
100
|
+
req_len: i32,
|
|
101
|
+
) -> Result<i64, UpcallEnd<G::Fault>> {
|
|
102
|
+
let reply = reply_to(guest, req_ptr, req_len)
|
|
103
|
+
.unwrap_or_else(|reason| Reply::Fault(Fault::new(FaultKind::Internal, reason)));
|
|
104
|
+
let written = write_reply(guest, &fit_in_message(reply).encode());
|
|
105
|
+
if let Some(trap) = guest.calls().reentry_trap.take() {
|
|
106
|
+
return Err(UpcallEnd::Fault(trap));
|
|
107
|
+
}
|
|
108
|
+
if guest.calls().abandoned() {
|
|
109
|
+
return Err(UpcallEnd::Abandoned);
|
|
110
|
+
}
|
|
111
|
+
written.map_err(UpcallEnd::Unwritable)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// An answer past the message cap is the Service's to change, as any other
|
|
115
|
+
/// answer the host cannot write is.
|
|
116
|
+
fn fit_in_message(reply: Reply) -> Reply {
|
|
117
|
+
let bytes = reply.encode().len();
|
|
118
|
+
match checked_payload_len(bytes) {
|
|
119
|
+
Ok(_) => reply,
|
|
120
|
+
Err(limit) => Reply::Fault(Fault::new(
|
|
121
|
+
FaultKind::Runtime,
|
|
122
|
+
format!("Sandbox could not write the Service's answer: {limit}"),
|
|
123
|
+
)),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fn reply_to<G: GuestInstance>(
|
|
128
|
+
guest: &mut G,
|
|
129
|
+
req_ptr: i32,
|
|
130
|
+
req_len: i32,
|
|
131
|
+
) -> Result<Reply, &'static str> {
|
|
132
|
+
let req_bytes = read_request(guest, req_ptr, req_len)?;
|
|
133
|
+
// The endpoint decodes the Envelope, here and in `FrameYielder`, so
|
|
134
|
+
// the frontend never sees a frame; the payload inside it stays bytes
|
|
135
|
+
// the whole way through.
|
|
136
|
+
let call = match Upcall::decode(&req_bytes) {
|
|
137
|
+
Ok(Upcall::Call(call)) => call,
|
|
138
|
+
Ok(_) => {
|
|
139
|
+
return Ok(Reply::Unsupported(Unsupported::new(
|
|
140
|
+
"the host does not serve this request",
|
|
141
|
+
)))
|
|
142
|
+
}
|
|
143
|
+
Err(_) => {
|
|
144
|
+
return Err(
|
|
145
|
+
"the guest sent a malformed Call envelope — please report this as a kobako bug",
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// A frontend always installs the dispatch handler before invoking the
|
|
151
|
+
// runtime, so reaching this branch indicates a misuse rather than a
|
|
152
|
+
// normal control path.
|
|
153
|
+
let handler = guest
|
|
154
|
+
.calls()
|
|
155
|
+
.on_dispatch
|
|
156
|
+
.clone()
|
|
157
|
+
.ok_or("a Sandbox callback fired outside an active invocation — please report this as a kobako bug")?;
|
|
158
|
+
|
|
159
|
+
// A frame-scoped yielder lends the guest to the handler. The borrow ends
|
|
160
|
+
// with the block, freeing it for `write_reply`; nested dispatch frames
|
|
161
|
+
// each build their own, so the LIFO re-entry lives on the stack.
|
|
162
|
+
let frame = {
|
|
163
|
+
let calls = guest.calls();
|
|
164
|
+
calls.open += 1;
|
|
165
|
+
calls.open - 1
|
|
166
|
+
};
|
|
167
|
+
let reply = {
|
|
168
|
+
let mut yielder = FrameYielder { guest, frame };
|
|
169
|
+
handler.dispatch(call, &mut yielder)
|
|
170
|
+
};
|
|
171
|
+
guest.calls().open -= 1;
|
|
172
|
+
reply.ok_or(
|
|
173
|
+
"a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn write_reply<G: GuestInstance>(guest: &mut G, bytes: &[u8]) -> Result<i64, &'static str> {
|
|
178
|
+
let ptr = write_from_callback(guest, bytes)?;
|
|
179
|
+
// The write has already held the length to the payload cap.
|
|
180
|
+
Ok(pack_ptr_len(ptr, bytes.len() as u32) as i64)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// The request the guest hands the upcall import. Its `(ptr, len)` are
|
|
184
|
+
/// guest addresses, so they are read unsigned.
|
|
185
|
+
fn read_request<G: GuestInstance>(
|
|
186
|
+
guest: &mut G,
|
|
187
|
+
req_ptr: i32,
|
|
188
|
+
req_len: i32,
|
|
189
|
+
) -> Result<Vec<u8>, &'static str> {
|
|
190
|
+
let memory = guest.memory().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
|
|
191
|
+
read_buffer(memory, req_ptr as u32 as usize, req_len as u32 as usize).map_err(|err| match err {
|
|
192
|
+
ReadError::TooLarge => "request payload exceeds the 16 MiB limit",
|
|
193
|
+
ReadError::OutOfBounds => "the Sandbox produced an out-of-bounds request",
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Keep a trap the guest raised during a callback into it for the upcall to
|
|
198
|
+
/// end the invocation with, and answer the callback's own reason.
|
|
199
|
+
fn keep_trap<G: GuestInstance>(
|
|
200
|
+
guest: &mut G,
|
|
201
|
+
trap: G::Fault,
|
|
202
|
+
reason: &'static str,
|
|
203
|
+
) -> &'static str {
|
|
204
|
+
guest.calls().reentry_trap.get_or_insert(trap);
|
|
205
|
+
reason
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Write `bytes` into the guest from inside a callback. A guest that has
|
|
209
|
+
/// already trapped is not called again.
|
|
210
|
+
fn write_from_callback<G: GuestInstance>(guest: &mut G, bytes: &[u8]) -> Result<u32, &'static str> {
|
|
211
|
+
if guest.calls().reentry_trap.is_some() {
|
|
212
|
+
return Err("the Sandbox already trapped during this invocation");
|
|
213
|
+
}
|
|
214
|
+
alloc_and_write(guest, bytes).map_err(|err| match err {
|
|
215
|
+
WriteError::TooLarge => "payload exceeds the 16 MiB limit",
|
|
216
|
+
WriteError::Incompatible(message) => message,
|
|
217
|
+
WriteError::Trapped(trap) => keep_trap(
|
|
218
|
+
guest,
|
|
219
|
+
trap,
|
|
220
|
+
"the Sandbox trapped while allocating memory for the request",
|
|
221
|
+
),
|
|
222
|
+
WriteError::OutOfMemory => "the Sandbox ran out of memory while preparing the request",
|
|
223
|
+
WriteError::OutOfBounds => "could not write the request into the Sandbox's memory",
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn drive_yield<G: GuestInstance>(
|
|
228
|
+
guest: &mut G,
|
|
229
|
+
frame: u32,
|
|
230
|
+
args: &[u8],
|
|
231
|
+
) -> Result<Vec<u8>, &'static str> {
|
|
232
|
+
let request = Downcall::Yield {
|
|
233
|
+
frame,
|
|
234
|
+
args: args.to_vec(),
|
|
235
|
+
}
|
|
236
|
+
.encode();
|
|
237
|
+
let len_i32 = checked_payload_len(request.len())?;
|
|
238
|
+
let req_ptr = write_from_callback(guest, &request)? as i32;
|
|
239
|
+
|
|
240
|
+
let packed = guest
|
|
241
|
+
.downcall(req_ptr, len_i32)
|
|
242
|
+
.ok_or(RUNTIME_INCOMPATIBLE)?
|
|
243
|
+
.map_err(|trap| keep_trap(guest, trap, "the Sandbox trapped while invoking a block"))?;
|
|
244
|
+
let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
|
|
245
|
+
if reply_len == 0 {
|
|
246
|
+
return Err("the Sandbox returned an empty block result");
|
|
247
|
+
}
|
|
248
|
+
let memory = guest.memory().ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
|
|
249
|
+
read_buffer(memory, reply_ptr, reply_len).map_err(|err| match err {
|
|
250
|
+
ReadError::TooLarge => "block result payload exceeds the 16 MiB limit",
|
|
251
|
+
ReadError::OutOfBounds => "the Sandbox returned an out-of-bounds block result",
|
|
252
|
+
})
|
|
253
|
+
}
|