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
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
//! pure function over the error's downcast chain so it can be exercised
|
|
8
8
|
//! from `cargo test` without any frontend.
|
|
9
9
|
|
|
10
|
-
use std::time::Instant;
|
|
10
|
+
use std::time::{Duration, Instant};
|
|
11
11
|
|
|
12
|
-
use wasmtime::{
|
|
12
|
+
use wasmtime::{Store, StoreHookState, UpdateDeadline};
|
|
13
13
|
|
|
14
14
|
use crate::invocation::Invocation;
|
|
15
|
-
use
|
|
15
|
+
use kobako_runtime::endpoint::MemoryLimitExceeded;
|
|
16
16
|
use kobako_runtime::error::{SetupError, Trap};
|
|
17
17
|
|
|
18
18
|
/// Marker error returned from the epoch-deadline callback when the
|
|
@@ -41,7 +41,7 @@ pub(crate) const NO_TIMEOUT_EPOCH_DELTA: u64 = u64::MAX / 2;
|
|
|
41
41
|
/// firing; returning the same long extension keeps it inert as a defence
|
|
42
42
|
/// in depth.
|
|
43
43
|
pub(crate) fn epoch_deadline_callback(
|
|
44
|
-
ctx:
|
|
44
|
+
ctx: StoreHookState<'_, Invocation>,
|
|
45
45
|
) -> wasmtime::Result<UpdateDeadline> {
|
|
46
46
|
match ctx.data().deadline() {
|
|
47
47
|
Some(deadline) if Instant::now() >= deadline => Err(wasmtime::Error::new(TimeoutTrap)),
|
|
@@ -53,13 +53,33 @@ pub(crate) fn epoch_deadline_callback(
|
|
|
53
53
|
/// The message leaves out the ABI export symbol, so the Sandbox layer can
|
|
54
54
|
/// attach the caller's verb instead. A cap trap carries its own message,
|
|
55
55
|
/// since wasmtime's backtrace framing is noise there.
|
|
56
|
-
pub(crate) fn
|
|
56
|
+
pub(crate) fn to_trap(err: wasmtime::Error) -> Trap {
|
|
57
|
+
cap_trap(&err).unwrap_or_else(|| Trap::Other(other_trap_message(&err)))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Arm a Store's wall-clock deadline `limit` from now, or none. Epoch
|
|
61
|
+
/// interruption is on engine-wide and a fresh Store's deadline has already
|
|
62
|
+
/// elapsed, so an unbounded Store names one it cannot reach.
|
|
63
|
+
pub(crate) fn arm_deadline(store: &mut Store<Invocation>, limit: Option<Duration>) {
|
|
64
|
+
store.epoch_deadline_callback(epoch_deadline_callback);
|
|
65
|
+
store
|
|
66
|
+
.data_mut()
|
|
67
|
+
.set_deadline(limit.map(|limit| Instant::now() + limit));
|
|
68
|
+
store.set_epoch_deadline(if limit.is_some() {
|
|
69
|
+
1
|
|
70
|
+
} else {
|
|
71
|
+
NO_TIMEOUT_EPOCH_DELTA
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// The cap trap `err` carries, if a cap fired: a setup step that fails
|
|
76
|
+
/// for any other reason words its own message instead.
|
|
77
|
+
pub(crate) fn cap_trap(err: &wasmtime::Error) -> Option<Trap> {
|
|
57
78
|
if let Some(t) = err.downcast_ref::<TimeoutTrap>() {
|
|
58
|
-
Trap::Timeout(t.to_string())
|
|
59
|
-
} else if let Some(t) = err.downcast_ref::<MemoryLimitTrap>() {
|
|
60
|
-
Trap::MemoryLimit(t.to_string())
|
|
79
|
+
Some(Trap::Timeout(t.to_string()))
|
|
61
80
|
} else {
|
|
62
|
-
|
|
81
|
+
err.downcast_ref::<MemoryLimitExceeded>()
|
|
82
|
+
.map(|t| Trap::MemoryLimit(t.to_string()))
|
|
63
83
|
}
|
|
64
84
|
}
|
|
65
85
|
|
|
@@ -76,23 +96,23 @@ fn other_trap_message(err: &wasmtime::Error) -> String {
|
|
|
76
96
|
}
|
|
77
97
|
}
|
|
78
98
|
|
|
79
|
-
/// The template is built before any invocation, so
|
|
80
|
-
/// the memory cap is not yet armed and the probe
|
|
81
|
-
///
|
|
99
|
+
/// The template is built before any invocation, so no invocation cap can
|
|
100
|
+
/// fire: the memory cap is not yet armed, and the probe's own setup
|
|
101
|
+
/// deadline is told apart where it fires. Every failure here is a setup
|
|
102
|
+
/// fault, not a trap.
|
|
82
103
|
pub(crate) fn instantiate_err(err: wasmtime::Error) -> SetupError {
|
|
83
104
|
SetupError::Dead(format!("instantiate: {err}"))
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
#[cfg(test)]
|
|
87
108
|
mod tests {
|
|
88
|
-
use super::{other_trap_message,
|
|
89
|
-
use
|
|
90
|
-
use crate::limiter::MemoryLimitTrap;
|
|
109
|
+
use super::{other_trap_message, to_trap, TimeoutTrap, NO_TIMEOUT_EPOCH_DELTA};
|
|
110
|
+
use kobako_runtime::endpoint::MemoryLimitExceeded;
|
|
91
111
|
use kobako_runtime::error::Trap;
|
|
92
112
|
|
|
93
113
|
// The no-timeout priming delta is added to the engine's current
|
|
94
114
|
// epoch inside wasmtime, and the process-wide ticker advances that
|
|
95
|
-
// epoch from the first `
|
|
115
|
+
// epoch from the first `load_engine` call on — so the sum must
|
|
96
116
|
// stay in range for a long-lived engine, not just a fresh one.
|
|
97
117
|
// `increment_epoch` stands in for the ticker to make the ticked
|
|
98
118
|
// state deterministic; under debug overflow checks an overflowing
|
|
@@ -100,34 +120,34 @@ mod tests {
|
|
|
100
120
|
// @behavior S-119
|
|
101
121
|
#[test]
|
|
102
122
|
fn no_timeout_delta_survives_a_ticked_engine_epoch() {
|
|
103
|
-
let engine = crate::cache::
|
|
123
|
+
let engine = crate::cache::load_engine().expect("shared engine must be constructible");
|
|
104
124
|
engine.increment_epoch();
|
|
105
|
-
let mut store = wasmtime::Store::new(engine,
|
|
125
|
+
let mut store = wasmtime::Store::new(engine, ());
|
|
106
126
|
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
// @behavior OC-032
|
|
110
130
|
#[test]
|
|
111
|
-
fn
|
|
131
|
+
fn to_trap_routes_timeout_trap_to_timeout() {
|
|
112
132
|
let err = wasmtime::Error::new(TimeoutTrap);
|
|
113
133
|
let expected = TimeoutTrap.to_string();
|
|
114
|
-
assert!(matches!(
|
|
134
|
+
assert!(matches!(to_trap(err), Trap::Timeout(msg) if msg == expected));
|
|
115
135
|
}
|
|
116
136
|
|
|
117
137
|
// @behavior OC-033
|
|
118
138
|
#[test]
|
|
119
|
-
fn
|
|
120
|
-
let trap =
|
|
139
|
+
fn to_trap_routes_memory_limit_exceeded_to_memory_limit() {
|
|
140
|
+
let trap = MemoryLimitExceeded::new(1 << 20, 1 << 19);
|
|
121
141
|
let expected = trap.to_string();
|
|
122
142
|
let err = wasmtime::Error::new(trap);
|
|
123
|
-
assert!(matches!(
|
|
143
|
+
assert!(matches!(to_trap(err), Trap::MemoryLimit(msg) if msg == expected));
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
// @behavior OC-028
|
|
127
147
|
#[test]
|
|
128
|
-
fn
|
|
148
|
+
fn to_trap_falls_back_to_other_for_unknown_errors() {
|
|
129
149
|
let err = wasmtime::Error::msg("some other wasmtime fault");
|
|
130
|
-
assert!(matches!(
|
|
150
|
+
assert!(matches!(to_trap(err), Trap::Other(_)));
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
// A guest hard trap reaches the host as a wasmtime error whose Display is
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! The guest's WASI context for one instance: the Preamble on stdin,
|
|
2
|
+
//! a capture pipe for each of stdout and stderr, the ambient authority
|
|
3
|
+
//! the instance's purpose grants, and a preopen for each Mount an
|
|
4
|
+
//! invocation was handed.
|
|
5
|
+
|
|
6
|
+
use wasmtime_wasi::p1::WasiP1Ctx;
|
|
7
|
+
use wasmtime_wasi::WasiCtxBuilder;
|
|
8
|
+
|
|
9
|
+
use crate::ambient::{self, Clocks};
|
|
10
|
+
use crate::config::Config;
|
|
11
|
+
use crate::descriptors::Descriptors;
|
|
12
|
+
use kobako_runtime::endpoint;
|
|
13
|
+
use kobako_runtime::error::Trap;
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
|
|
16
|
+
use kobako_runtime::filesystem::{create_pipe_files, PipeInodeInfo};
|
|
17
|
+
use kobako_runtime::grant::Grant;
|
|
18
|
+
use kobako_runtime::mount::Mount;
|
|
19
|
+
use kobako_runtime::profile::Profile;
|
|
20
|
+
use kobako_runtime::security::prepare_creds_within;
|
|
21
|
+
use kobako_runtime::vfs::Cred;
|
|
22
|
+
|
|
23
|
+
/// The Cred an invocation's Mounts open as: kobako's own rules, then each
|
|
24
|
+
/// module the Driver was built with, its Filesystems holding at most the
|
|
25
|
+
/// memory the Driver allots, as Linux's `prepare_creds` gives a task the
|
|
26
|
+
/// LSMs it booted with.
|
|
27
|
+
fn prepare_creds(config: &Config) -> Arc<Cred> {
|
|
28
|
+
prepare_creds_within(&config.security, config.memory_limit)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// One instance's WASI context: its descriptor table, the clocks its
|
|
32
|
+
/// waits pass on, and the capture pipes stdout and stderr write into, so
|
|
33
|
+
/// the captures are read back from the same owner that wired them.
|
|
34
|
+
pub(crate) struct Wasi {
|
|
35
|
+
ctx: WasiP1Ctx,
|
|
36
|
+
descriptors: Descriptors,
|
|
37
|
+
clocks: Clocks,
|
|
38
|
+
stdout: Arc<PipeInodeInfo>,
|
|
39
|
+
stderr: Arc<PipeInodeInfo>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl Wasi {
|
|
43
|
+
/// An invocation's context, granting the posture the Sandbox requested
|
|
44
|
+
/// and presenting each of the invocation's Mounts as a preopen, from
|
|
45
|
+
/// descriptor 3 in order.
|
|
46
|
+
pub(crate) fn for_invocation(
|
|
47
|
+
config: &Config,
|
|
48
|
+
stdin: Vec<u8>,
|
|
49
|
+
grants: &[Grant],
|
|
50
|
+
) -> Result<Self, Trap> {
|
|
51
|
+
Self::new(
|
|
52
|
+
stdin,
|
|
53
|
+
config.stdout_limit,
|
|
54
|
+
config.stderr_limit,
|
|
55
|
+
config.open_file_limit,
|
|
56
|
+
prepare_creds(config),
|
|
57
|
+
config.profile,
|
|
58
|
+
grants.iter().filter_map(to_mount).collect(),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A compile's context, at the strongest posture whatever the Sandbox
|
|
63
|
+
/// requested: compiling the Snippets needs no ambient authority, so
|
|
64
|
+
/// it is granted none.
|
|
65
|
+
pub(crate) fn for_compile(config: &Config, stdin: Vec<u8>) -> Result<Self, Trap> {
|
|
66
|
+
Self::new(
|
|
67
|
+
stdin,
|
|
68
|
+
config.stdout_limit,
|
|
69
|
+
config.stderr_limit,
|
|
70
|
+
config.open_file_limit,
|
|
71
|
+
prepare_creds(config),
|
|
72
|
+
Profile::Hermetic,
|
|
73
|
+
Vec::new(),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// The ABI version probe's context. Instantiating it runs the
|
|
78
|
+
/// artifact's start section, yet reading a version needs no input, no
|
|
79
|
+
/// ambient authority, and no output: the probe takes an empty stdin, the
|
|
80
|
+
/// strongest posture, and pipes sized to keep nothing.
|
|
81
|
+
pub(crate) fn for_probe() -> Result<Self, Trap> {
|
|
82
|
+
Self::new(
|
|
83
|
+
Vec::new(),
|
|
84
|
+
Some(0),
|
|
85
|
+
Some(0),
|
|
86
|
+
None,
|
|
87
|
+
prepare_creds_within(&[], None),
|
|
88
|
+
Profile::Hermetic,
|
|
89
|
+
Vec::new(),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// An uncapped output channel has no ceiling on the host: a guest
|
|
94
|
+
/// rewriting one buffer grows the pipe, not its own memory.
|
|
95
|
+
fn new(
|
|
96
|
+
stdin: Vec<u8>,
|
|
97
|
+
stdout_limit: Option<usize>,
|
|
98
|
+
stderr_limit: Option<usize>,
|
|
99
|
+
open_file_limit: Option<usize>,
|
|
100
|
+
opener: Arc<Cred>,
|
|
101
|
+
profile: Profile,
|
|
102
|
+
mounts: Vec<Mount>,
|
|
103
|
+
) -> Result<Self, Trap> {
|
|
104
|
+
let stdout = PipeInodeInfo::new(endpoint::pipe_capacity(stdout_limit));
|
|
105
|
+
let stderr = PipeInodeInfo::new(endpoint::pipe_capacity(stderr_limit));
|
|
106
|
+
|
|
107
|
+
// stdio lives in kobako's descriptor table, which answers every
|
|
108
|
+
// call that reads a descriptor (`crate::preview1`); the builder
|
|
109
|
+
// keeps only what the remaining imports read.
|
|
110
|
+
let descriptors = Descriptors::new(
|
|
111
|
+
create_pipe_files(&PipeInodeInfo::from_bytes(stdin), &opener).0,
|
|
112
|
+
create_pipe_files(&stdout, &opener).1,
|
|
113
|
+
create_pipe_files(&stderr, &opener).1,
|
|
114
|
+
mounts,
|
|
115
|
+
open_file_limit,
|
|
116
|
+
opener,
|
|
117
|
+
profile,
|
|
118
|
+
)
|
|
119
|
+
.map_err(|err| Trap::Other(format!("a Mount's root did not open: {err:?}")))?;
|
|
120
|
+
|
|
121
|
+
let mut builder = WasiCtxBuilder::new();
|
|
122
|
+
// The profile decides the ambient authority granted: the
|
|
123
|
+
// hermetic rung denies the preview1 time and entropy imports (see
|
|
124
|
+
// `ambient`), the permissive rung leaves the live WASI sources.
|
|
125
|
+
// Ambient filesystem, environment, and network stay absent on both rungs —
|
|
126
|
+
// the builder grants none unless asked. The exhaustive match makes
|
|
127
|
+
// a future ladder rung a compile error here, not authority granted
|
|
128
|
+
// silently.
|
|
129
|
+
let clocks = match profile {
|
|
130
|
+
Profile::Hermetic => {
|
|
131
|
+
builder.wall_clock(ambient::FrozenWallClock);
|
|
132
|
+
builder.monotonic_clock(ambient::FrozenMonotonicClock);
|
|
133
|
+
builder.secure_random(ambient::deterministic_rng());
|
|
134
|
+
Clocks::Frozen
|
|
135
|
+
}
|
|
136
|
+
Profile::Permissive => {
|
|
137
|
+
let monotonic = ambient::LiveMonotonicClock::new();
|
|
138
|
+
builder.monotonic_clock(monotonic);
|
|
139
|
+
Clocks::Live(monotonic)
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
Ok(Self {
|
|
144
|
+
ctx: builder.build_p1(),
|
|
145
|
+
descriptors,
|
|
146
|
+
clocks,
|
|
147
|
+
stdout,
|
|
148
|
+
stderr,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pub(crate) fn ctx_mut(&mut self) -> &mut WasiP1Ctx {
|
|
153
|
+
&mut self.ctx
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub(crate) fn descriptors(&self) -> &Descriptors {
|
|
157
|
+
&self.descriptors
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub(crate) fn descriptors_mut(&mut self) -> &mut Descriptors {
|
|
161
|
+
&mut self.descriptors
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub(crate) fn clocks(&self) -> Clocks {
|
|
165
|
+
self.clocks
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pub(crate) fn stdout_bytes(&self) -> Vec<u8> {
|
|
169
|
+
self.stdout.contents()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
|
|
173
|
+
self.stderr.contents()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// A Mount as this invocation presents it. A kind this driver does not
|
|
178
|
+
/// support has no descriptor to stand behind; `Driver::supports` keeps a
|
|
179
|
+
/// frontend from handing one over.
|
|
180
|
+
fn to_mount(grant: &Grant) -> Option<Mount> {
|
|
181
|
+
match grant {
|
|
182
|
+
Grant::Mount(mount) => Some(mount.clone()),
|
|
183
|
+
_ => None,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[cfg(test)]
|
|
188
|
+
mod tests {
|
|
189
|
+
//! Witness what an invocation's context grants at each profile rung:
|
|
190
|
+
//! `wasi:clocks` / `wasi:random` frozen under `Hermetic` and live
|
|
191
|
+
//! under `Permissive`, and no ambient filesystem, environment, or
|
|
192
|
+
//! socket under either.
|
|
193
|
+
use wasmtime::{AsContextMut, Module, Store};
|
|
194
|
+
|
|
195
|
+
use super::*;
|
|
196
|
+
use crate::cache::load_engine;
|
|
197
|
+
use crate::imports;
|
|
198
|
+
use crate::invocation::Invocation;
|
|
199
|
+
|
|
200
|
+
/// Preview1 `errno` for a descriptor the context does not hold.
|
|
201
|
+
const ERRNO_BADF: i64 = 8;
|
|
202
|
+
|
|
203
|
+
/// Preview1 probe with one export per ambient source: `clock_ns`
|
|
204
|
+
/// reads the realtime clock, `random_word` reads eight entropy bytes,
|
|
205
|
+
/// `environ_count` counts environment variables, and `prestat_errno`
|
|
206
|
+
/// / `accept_errno` ask descriptor 3 for a directory and a
|
|
207
|
+
/// connection. Preview1 numbers preopens from 3, so nothing there
|
|
208
|
+
/// means no directory and no socket was granted.
|
|
209
|
+
const PROBE_WAT: &str = r#"
|
|
210
|
+
(module
|
|
211
|
+
(import "wasi_snapshot_preview1" "clock_time_get"
|
|
212
|
+
(func $clock (param i32 i64 i32) (result i32)))
|
|
213
|
+
(import "wasi_snapshot_preview1" "random_get"
|
|
214
|
+
(func $random (param i32 i32) (result i32)))
|
|
215
|
+
(import "wasi_snapshot_preview1" "environ_sizes_get"
|
|
216
|
+
(func $environ (param i32 i32) (result i32)))
|
|
217
|
+
(import "wasi_snapshot_preview1" "fd_prestat_get"
|
|
218
|
+
(func $prestat (param i32 i32) (result i32)))
|
|
219
|
+
(import "wasi_snapshot_preview1" "sock_accept"
|
|
220
|
+
(func $accept (param i32 i32 i32) (result i32)))
|
|
221
|
+
(memory (export "memory") 1)
|
|
222
|
+
(func (export "clock_ns") (result i64)
|
|
223
|
+
(drop (call $clock (i32.const 0) (i64.const 1) (i32.const 0)))
|
|
224
|
+
(i64.load (i32.const 0)))
|
|
225
|
+
(func (export "random_word") (result i64)
|
|
226
|
+
(drop (call $random (i32.const 8) (i32.const 8)))
|
|
227
|
+
(i64.load (i32.const 8)))
|
|
228
|
+
(func (export "environ_count") (result i64)
|
|
229
|
+
(drop (call $environ (i32.const 16) (i32.const 20)))
|
|
230
|
+
(i64.extend_i32_u (i32.load (i32.const 16))))
|
|
231
|
+
(func (export "prestat_errno") (result i64)
|
|
232
|
+
(i64.extend_i32_u (call $prestat (i32.const 3) (i32.const 24))))
|
|
233
|
+
(func (export "accept_errno") (result i64)
|
|
234
|
+
(i64.extend_i32_u (call $accept (i32.const 3) (i32.const 0) (i32.const 24)))))
|
|
235
|
+
"#;
|
|
236
|
+
|
|
237
|
+
/// Instantiate the probe over an invocation's WASI context built at
|
|
238
|
+
/// `profile` and return a reader for its exports.
|
|
239
|
+
fn probe(profile: Profile) -> impl FnMut(&str) -> i64 {
|
|
240
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
241
|
+
let config = Config {
|
|
242
|
+
timeout: None,
|
|
243
|
+
setup_timeout: None,
|
|
244
|
+
memory_limit: None,
|
|
245
|
+
stdout_limit: None,
|
|
246
|
+
stderr_limit: None,
|
|
247
|
+
open_file_limit: None,
|
|
248
|
+
profile,
|
|
249
|
+
security: Vec::new(),
|
|
250
|
+
};
|
|
251
|
+
let wasi = Wasi::for_invocation(&config, Vec::new(), &[]).expect("WASI context must build");
|
|
252
|
+
let mut store = Store::new(engine, Invocation::new(None, wasi));
|
|
253
|
+
store.set_epoch_deadline(crate::trap::NO_TIMEOUT_EPOCH_DELTA);
|
|
254
|
+
|
|
255
|
+
let linker = imports::linker().expect("host imports must link");
|
|
256
|
+
let module = Module::new(engine, PROBE_WAT).expect("probe module must compile");
|
|
257
|
+
let instance = linker
|
|
258
|
+
.instantiate(&mut store, &module)
|
|
259
|
+
.expect("probe module must instantiate");
|
|
260
|
+
|
|
261
|
+
move |name| {
|
|
262
|
+
instance
|
|
263
|
+
.get_typed_func::<(), i64>(store.as_context_mut(), name)
|
|
264
|
+
.expect("probe export must resolve")
|
|
265
|
+
.call(store.as_context_mut(), ())
|
|
266
|
+
.expect("probe export must run")
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Assert that the context built at `profile` grants no ambient
|
|
271
|
+
/// directory, environment variable, or socket.
|
|
272
|
+
fn assert_grants_no_ambient_resource(profile: Profile) {
|
|
273
|
+
let mut read = probe(profile);
|
|
274
|
+
assert_eq!(
|
|
275
|
+
read("prestat_errno"),
|
|
276
|
+
ERRNO_BADF,
|
|
277
|
+
"a {profile:?} guest's WASI context must preopen no directory"
|
|
278
|
+
);
|
|
279
|
+
assert_eq!(
|
|
280
|
+
read("environ_count"),
|
|
281
|
+
0,
|
|
282
|
+
"a {profile:?} guest's WASI context must carry no environment variable"
|
|
283
|
+
);
|
|
284
|
+
assert_eq!(
|
|
285
|
+
read("accept_errno"),
|
|
286
|
+
ERRNO_BADF,
|
|
287
|
+
"a {profile:?} guest's WASI context must hold no socket"
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// @behavior RT-047 RT-048
|
|
292
|
+
#[test]
|
|
293
|
+
fn hermetic_denies_ambient_time_and_entropy() {
|
|
294
|
+
let mut read = probe(Profile::Hermetic);
|
|
295
|
+
assert_eq!(
|
|
296
|
+
read("clock_ns"),
|
|
297
|
+
0,
|
|
298
|
+
"a hermetic guest's wasi:clocks must read the Unix epoch, not host time"
|
|
299
|
+
);
|
|
300
|
+
assert_eq!(
|
|
301
|
+
read("random_word"),
|
|
302
|
+
0,
|
|
303
|
+
"a hermetic guest's wasi:random must yield the constant stream, not host entropy"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// @behavior RT-049 RT-050
|
|
308
|
+
#[test]
|
|
309
|
+
fn permissive_grants_live_ambient_time_and_entropy() {
|
|
310
|
+
let mut read = probe(Profile::Permissive);
|
|
311
|
+
assert!(
|
|
312
|
+
read("clock_ns") > 0,
|
|
313
|
+
"a permissive guest's wasi:clocks must read live host time"
|
|
314
|
+
);
|
|
315
|
+
assert_ne!(
|
|
316
|
+
read("random_word"),
|
|
317
|
+
0,
|
|
318
|
+
"a permissive guest's wasi:random must yield host entropy"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// @behavior RT-067
|
|
323
|
+
#[test]
|
|
324
|
+
fn hermetic_grants_no_ambient_resource() {
|
|
325
|
+
assert_grants_no_ambient_resource(Profile::Hermetic);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// @behavior RT-067
|
|
329
|
+
#[test]
|
|
330
|
+
fn permissive_grants_no_ambient_resource() {
|
|
331
|
+
assert_grants_no_ambient_resource(Profile::Permissive);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//! A compile reads no host time or entropy, whatever posture the Sandbox
|
|
2
|
+
//! requested. Compiling runs no script, so the bundled guest has no way to
|
|
3
|
+
//! show what its clock read; a fixture guest whose compile answers the
|
|
4
|
+
//! ambient bytes it read shows it instead.
|
|
5
|
+
|
|
6
|
+
use std::path::Path;
|
|
7
|
+
|
|
8
|
+
use kobako_runtime::{Profile, Runtime};
|
|
9
|
+
use kobako_transport::envelope::{Preamble, Snippet, Snippets};
|
|
10
|
+
use kobako_wasmtime::{Config, Driver};
|
|
11
|
+
|
|
12
|
+
const FIXTURE: &str = concat!(
|
|
13
|
+
env!("CARGO_MANIFEST_DIR"),
|
|
14
|
+
"/../../test/fixtures/minimal_compile_ambient.wat"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
// @behavior RT-072
|
|
18
|
+
#[test]
|
|
19
|
+
fn a_compile_reads_no_host_time_or_entropy_under_a_permissive_request() {
|
|
20
|
+
let mut config = Config::default();
|
|
21
|
+
config.timeout = None;
|
|
22
|
+
config.setup_timeout = None;
|
|
23
|
+
config.memory_limit = None;
|
|
24
|
+
config.stdout_limit = None;
|
|
25
|
+
config.stderr_limit = None;
|
|
26
|
+
config.open_file_limit = None;
|
|
27
|
+
config.profile = Profile::Permissive;
|
|
28
|
+
let driver = Driver::new(Path::new(FIXTURE), config).expect("the fixture guest must load");
|
|
29
|
+
let mut preamble = Preamble::default();
|
|
30
|
+
preamble.snippets = Snippets {
|
|
31
|
+
entries: vec![Snippet::Source {
|
|
32
|
+
name: "Only".into(),
|
|
33
|
+
body: "1".into(),
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
assert_eq!(
|
|
38
|
+
driver.compile(&preamble),
|
|
39
|
+
Some(vec![vec![0; 16]]),
|
|
40
|
+
"a permissive request through Driver#compile must read the Unix epoch and the \
|
|
41
|
+
constant stream, not host time and entropy"
|
|
42
|
+
);
|
|
43
|
+
}
|