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
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//! that separates the hermetic rung from permissive
|
|
3
|
-
//!
|
|
1
|
+
//! The guest's time and entropy at the WASI layer — the one ambient
|
|
2
|
+
//! authority that separates the hermetic rung from permissive. `Wasi::new`
|
|
3
|
+
//! wires the frozen sources on `Hermetic` and the live clock on
|
|
4
|
+
//! `Permissive`; `Clocks` is what the guest's waits pass on under either.
|
|
4
5
|
//!
|
|
5
6
|
//! `wasmtime-wasi`'s `WasiCtxBuilder` defaults the guest's `wasi:clocks` to
|
|
6
7
|
//! the host wall / monotonic clock and `wasi:random` to a fresh per-context
|
|
7
8
|
//! seed. No allowlisted mrbgem reaches these preview1 imports today
|
|
8
9
|
//! (`build_config/wasi.rb`), but a future libc-backed gem would silently
|
|
9
10
|
//! obtain real time and host entropy — a covert timing channel and a
|
|
10
|
-
//! nondeterminism source the hermetic posture deliberately excludes
|
|
11
|
-
//! (docs/guides/security.md).
|
|
11
|
+
//! nondeterminism source the hermetic posture deliberately excludes.
|
|
12
12
|
//! Pinning the clocks to the Unix epoch and the RNG to a constant stream
|
|
13
13
|
//! makes that denial a property of the host, not merely of the gem allowlist.
|
|
14
14
|
//!
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
//! wasmtime epoch interruption against a host `Instant`, never the guest's
|
|
17
17
|
//! frozen `wasi:clocks/monotonic-clock`.
|
|
18
18
|
|
|
19
|
-
use std::time::Duration;
|
|
19
|
+
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
|
20
20
|
|
|
21
21
|
use wasmtime_wasi::random::Deterministic;
|
|
22
22
|
use wasmtime_wasi::{HostMonotonicClock, HostWallClock};
|
|
@@ -53,6 +53,64 @@ pub(crate) fn deterministic_rng() -> Deterministic {
|
|
|
53
53
|
Deterministic::new(vec![0])
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/// The host's monotonic clock read from a fixed origin. The guest's
|
|
57
|
+
/// `clock_time_get` and a wait it subscribes to share it, so both agree on
|
|
58
|
+
/// what an instant means.
|
|
59
|
+
#[derive(Clone, Copy)]
|
|
60
|
+
pub(crate) struct LiveMonotonicClock {
|
|
61
|
+
origin: Instant,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl LiveMonotonicClock {
|
|
65
|
+
pub(crate) fn new() -> Self {
|
|
66
|
+
Self {
|
|
67
|
+
origin: Instant::now(),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl HostMonotonicClock for LiveMonotonicClock {
|
|
73
|
+
fn resolution(&self) -> u64 {
|
|
74
|
+
1
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn now(&self) -> u64 {
|
|
78
|
+
u64::try_from(self.origin.elapsed().as_nanos()).unwrap_or(u64::MAX)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The clocks the guest observes. A wait it subscribes to passes on the
|
|
83
|
+
/// same clocks: frozen ones never advance, so the wait ends at once.
|
|
84
|
+
#[derive(Clone, Copy)]
|
|
85
|
+
pub(crate) enum Clocks {
|
|
86
|
+
Frozen,
|
|
87
|
+
Live(LiveMonotonicClock),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Clocks {
|
|
91
|
+
pub(crate) fn monotonic_now(&self) -> u64 {
|
|
92
|
+
match self {
|
|
93
|
+
Self::Frozen => FrozenMonotonicClock.now(),
|
|
94
|
+
Self::Live(clock) => clock.now(),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
pub(crate) fn wall_now(&self) -> u64 {
|
|
99
|
+
let since_epoch = match self {
|
|
100
|
+
Self::Frozen => FrozenWallClock.now(),
|
|
101
|
+
Self::Live(_) => SystemTime::now()
|
|
102
|
+
.duration_since(UNIX_EPOCH)
|
|
103
|
+
.unwrap_or(Duration::ZERO),
|
|
104
|
+
};
|
|
105
|
+
u64::try_from(since_epoch.as_nanos()).unwrap_or(u64::MAX)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Whether a wait on these clocks takes host time.
|
|
109
|
+
pub(crate) fn advances(&self) -> bool {
|
|
110
|
+
matches!(self, Self::Live(_))
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
56
114
|
#[cfg(test)]
|
|
57
115
|
mod tests {
|
|
58
116
|
//! Pin the frozen-clock contract: the guest must observe no real wall
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
//!
|
|
11
11
|
//! Across processes, the Cranelift compile cost is amortised by a
|
|
12
12
|
//! best-effort `.cwasm` disk cache keyed by the SHA-256 of the Guest
|
|
13
|
-
//! Binary bytes; every cache failure
|
|
14
|
-
//! back to in-process compilation.
|
|
13
|
+
//! Binary bytes and the engine that compiles them; every cache failure
|
|
14
|
+
//! falls back to in-process compilation.
|
|
15
15
|
//!
|
|
16
16
|
//! Concurrency: the Mutex is held only for the HashMap lookup and insert,
|
|
17
17
|
//! never across a compile, so threads building Drivers at once do not
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
use std::collections::HashMap;
|
|
22
22
|
use std::fmt::Write as _;
|
|
23
23
|
use std::fs;
|
|
24
|
+
use std::hash::{DefaultHasher, Hash, Hasher};
|
|
24
25
|
use std::path::{Path, PathBuf};
|
|
25
26
|
use std::sync::{Mutex, OnceLock};
|
|
26
27
|
use std::thread;
|
|
27
|
-
use std::time::
|
|
28
|
+
use std::time::Duration;
|
|
28
29
|
|
|
29
30
|
use sha2::{Digest, Sha256};
|
|
30
31
|
use wasmtime::{Config as WtConfig, Engine as WtEngine, Module as WtModule};
|
|
@@ -47,7 +48,7 @@ const EPOCH_TICK: Duration = Duration::from_millis(10);
|
|
|
47
48
|
/// new-style exception handling (`-mllvm -wasm-use-legacy-eh=false`) for
|
|
48
49
|
/// its setjmp path. Epoch interruption backs the wall-clock cap, driven by
|
|
49
50
|
/// one process-wide ticker thread.
|
|
50
|
-
pub(crate) fn
|
|
51
|
+
pub(crate) fn load_engine() -> Result<&'static WtEngine, SetupError> {
|
|
51
52
|
if let Some(engine) = SHARED_ENGINE.get() {
|
|
52
53
|
return Ok(engine);
|
|
53
54
|
}
|
|
@@ -76,7 +77,7 @@ fn spawn_epoch_ticker(engine: WtEngine) {
|
|
|
76
77
|
|
|
77
78
|
/// A missing file answers `SetupError::ModuleNotBuilt`, the common state of
|
|
78
79
|
/// a fresh clone before `rake compile`.
|
|
79
|
-
pub(crate) fn
|
|
80
|
+
pub(crate) fn load_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
80
81
|
let cache = MODULE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
|
|
81
82
|
|
|
82
83
|
if let Some(module) = cache
|
|
@@ -101,8 +102,8 @@ pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
|
101
102
|
path.display()
|
|
102
103
|
))
|
|
103
104
|
})?;
|
|
104
|
-
let engine =
|
|
105
|
-
let artifact = artifact_path(&bytes);
|
|
105
|
+
let engine = load_engine()?;
|
|
106
|
+
let artifact = artifact_path(engine, &bytes);
|
|
106
107
|
let module = match artifact.as_deref().and_then(|p| load_artifact(engine, p)) {
|
|
107
108
|
Some(module) => module,
|
|
108
109
|
None => {
|
|
@@ -124,70 +125,120 @@ pub(crate) fn cached_module(path: &Path) -> Result<WtModule, SetupError> {
|
|
|
124
125
|
/// Retention window for unused cache entries. A hit refreshes the
|
|
125
126
|
/// artifact's mtime, so only entries no process has loaded for the
|
|
126
127
|
/// whole window are removed by `prune_stale`.
|
|
128
|
+
#[cfg(unix)]
|
|
127
129
|
const ARTIFACT_TTL: Duration = Duration::from_secs(30 * 24 * 60 * 60);
|
|
128
130
|
|
|
129
131
|
/// Content addressing makes a rebuilt Guest Binary a new entry rather than
|
|
130
|
-
/// an invalidation problem
|
|
131
|
-
///
|
|
132
|
-
///
|
|
133
|
-
///
|
|
134
|
-
|
|
132
|
+
/// an invalidation problem. The engine segment names the wasmtime release
|
|
133
|
+
/// and Config that compiled the artifact, because wasmtime's own
|
|
134
|
+
/// deserialize check compares only the major version: without it, an
|
|
135
|
+
/// artifact from a release with a since-fixed miscompile would load
|
|
136
|
+
/// under the patched one.
|
|
137
|
+
fn artifact_path(engine: &WtEngine, wasm_bytes: &[u8]) -> Option<PathBuf> {
|
|
135
138
|
let base = std::env::var_os("XDG_CACHE_HOME")
|
|
136
139
|
.map(PathBuf::from)
|
|
137
140
|
.or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))?;
|
|
138
141
|
let digest = Sha256::digest(wasm_bytes);
|
|
139
|
-
let mut name = String::with_capacity(
|
|
142
|
+
let mut name = String::with_capacity(96);
|
|
140
143
|
for byte in digest {
|
|
141
144
|
let _ = write!(name, "{:02x}", byte);
|
|
142
145
|
}
|
|
143
|
-
let _ = write!(name, "-{}.cwasm",
|
|
146
|
+
let _ = write!(name, "-{:016x}.cwasm", engine_key(engine));
|
|
144
147
|
Some(base.join("kobako").join(name))
|
|
145
148
|
}
|
|
146
149
|
|
|
150
|
+
/// wasmtime's hash of everything an artifact's compatibility depends on,
|
|
151
|
+
/// its full release version included. `DefaultHasher::new` is unseeded,
|
|
152
|
+
/// so every process of one build derives the same key.
|
|
153
|
+
fn engine_key(engine: &WtEngine) -> u64 {
|
|
154
|
+
let mut hasher = DefaultHasher::new();
|
|
155
|
+
engine.precompile_compatibility_hash().hash(&mut hasher);
|
|
156
|
+
hasher.finish()
|
|
157
|
+
}
|
|
158
|
+
|
|
147
159
|
/// A hit refreshes the file's mtime, so the retention window measures time
|
|
148
160
|
/// since last use rather than since creation.
|
|
161
|
+
#[cfg(unix)]
|
|
149
162
|
fn load_artifact(engine: &WtEngine, artifact: &Path) -> Option<WtModule> {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// the
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.open(artifact)
|
|
163
|
-
.and_then(|f| f.set_modified(SystemTime::now()));
|
|
164
|
-
Some(module)
|
|
163
|
+
use rustix::fs::{openat, Mode, OFlags};
|
|
164
|
+
let dir = open_private_dir(artifact.parent()?)?;
|
|
165
|
+
let flags = OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
|
|
166
|
+
let file = fs::File::from(openat(&dir, artifact.file_name()?, flags, Mode::empty()).ok()?);
|
|
167
|
+
let _ = file.set_modified(std::time::SystemTime::now());
|
|
168
|
+
// SAFETY: `Module::deserialize_open_file` trusts the artifact bytes
|
|
169
|
+
// and needs them unchanged while the Module lives. The file was
|
|
170
|
+
// opened through the directory `open_private_dir` checked, which only
|
|
171
|
+
// the current user can write, so it is an artifact this module
|
|
172
|
+
// stored, addressed by the content hash of the Guest Binary being
|
|
173
|
+
// constructed — it carries exactly that binary's trust.
|
|
174
|
+
unsafe { WtModule::deserialize_open_file(engine, file) }.ok()
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
/// The temp-file and rename pair keeps concurrent processes from observing
|
|
168
|
-
/// a partial write
|
|
169
|
-
///
|
|
170
|
-
///
|
|
178
|
+
/// a partial write, and syncing the bytes before the rename keeps a crash
|
|
179
|
+
/// from leaving the artifact's name over blocks never written, which the
|
|
180
|
+
/// unsafe deserialize would trust. Failures are swallowed because the
|
|
181
|
+
/// cache is only an optimisation, and pruning on write keeps the
|
|
182
|
+
/// directory bounded across rebuilds.
|
|
183
|
+
#[cfg(unix)]
|
|
171
184
|
fn store_artifact(module: &WtModule, artifact: &Path) {
|
|
185
|
+
use rustix::fs::{fsync, renameat};
|
|
186
|
+
use std::io::Write as _;
|
|
187
|
+
let (Some(dir), Some(name)) = (artifact.parent(), artifact.file_name()) else {
|
|
188
|
+
return;
|
|
189
|
+
};
|
|
172
190
|
let Ok(bytes) = module.serialize() else {
|
|
173
191
|
return;
|
|
174
192
|
};
|
|
175
|
-
|
|
176
|
-
if create_cache_dir(dir).is_err() || !dir_is_private(dir) {
|
|
193
|
+
if create_cache_dir(dir).is_err() {
|
|
177
194
|
return;
|
|
178
195
|
}
|
|
179
|
-
let
|
|
180
|
-
|
|
196
|
+
let Some(dir) = open_private_dir(dir) else {
|
|
197
|
+
return;
|
|
198
|
+
};
|
|
199
|
+
let Some((tmp, mut file)) = create_temp(&dir, name) else {
|
|
200
|
+
return;
|
|
201
|
+
};
|
|
202
|
+
if file
|
|
203
|
+
.write_all(&bytes)
|
|
204
|
+
.and_then(|()| file.sync_all())
|
|
205
|
+
.is_err()
|
|
206
|
+
{
|
|
181
207
|
return;
|
|
182
208
|
}
|
|
183
|
-
if
|
|
184
|
-
|
|
209
|
+
if renameat(&dir, &tmp, &dir, name).is_ok() {
|
|
210
|
+
let _ = fsync(&dir);
|
|
211
|
+
prune_stale(&dir, name);
|
|
185
212
|
}
|
|
186
213
|
}
|
|
187
214
|
|
|
188
|
-
///
|
|
189
|
-
///
|
|
190
|
-
///
|
|
215
|
+
/// A temp file no other writer shares: two threads of one process storing
|
|
216
|
+
/// the same artifact at once would otherwise write into one file, and a
|
|
217
|
+
/// reader could load the interleaved bytes the unsafe deserialize trusts.
|
|
218
|
+
#[cfg(unix)]
|
|
219
|
+
fn create_temp(dir: &rustix::fd::OwnedFd, name: &std::ffi::OsStr) -> Option<(PathBuf, fs::File)> {
|
|
220
|
+
use rustix::fs::{openat, Mode, OFlags};
|
|
221
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
222
|
+
static NEXT_TEMP: AtomicU64 = AtomicU64::new(0);
|
|
223
|
+
let serial = NEXT_TEMP.fetch_add(1, Ordering::Relaxed);
|
|
224
|
+
let tmp = Path::new(name).with_extension(format!("tmp{}.{serial}", std::process::id()));
|
|
225
|
+
let flags = OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::NOFOLLOW | OFlags::CLOEXEC;
|
|
226
|
+
let fd = openat(dir, &tmp, flags, Mode::RUSR | Mode::WUSR).ok()?;
|
|
227
|
+
Some((tmp, fs::File::from(fd)))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// The disk cache trusts its directory's owner, which only Unix reports,
|
|
231
|
+
/// so elsewhere every process compiles the Guest Binary itself.
|
|
232
|
+
#[cfg(not(unix))]
|
|
233
|
+
fn load_artifact(_engine: &WtEngine, _artifact: &Path) -> Option<WtModule> {
|
|
234
|
+
None
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[cfg(not(unix))]
|
|
238
|
+
fn store_artifact(_module: &WtModule, _artifact: &Path) {}
|
|
239
|
+
|
|
240
|
+
/// Create the cache directory owner-only (`0700`) so no other local user
|
|
241
|
+
/// can plant an artifact the unsafe deserialize would trust.
|
|
191
242
|
#[cfg(unix)]
|
|
192
243
|
fn create_cache_dir(dir: &Path) -> std::io::Result<()> {
|
|
193
244
|
use std::os::unix::fs::DirBuilderExt;
|
|
@@ -197,57 +248,117 @@ fn create_cache_dir(dir: &Path) -> std::io::Result<()> {
|
|
|
197
248
|
.create(dir)
|
|
198
249
|
}
|
|
199
250
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
///
|
|
206
|
-
/// deserialize relies on: owned by the current effective user and
|
|
207
|
-
/// writable by no one else. A pre-existing directory another user owns
|
|
208
|
-
/// or can write to — e.g. under a shared `XDG_CACHE_HOME` — fails here
|
|
209
|
-
/// and both disk-cache tiers are skipped.
|
|
251
|
+
/// The cache directory as one descriptor, once it upholds the trust the
|
|
252
|
+
/// unsafe deserialize relies on: a directory rather than a link in its
|
|
253
|
+
/// place, owned by the current effective user and writable by no one
|
|
254
|
+
/// else. Every later step goes through the descriptor, so a path swapped
|
|
255
|
+
/// after the check reaches nothing. One that fails — another user's,
|
|
256
|
+
/// under a shared `XDG_CACHE_HOME`, say — skips both disk-cache tiers.
|
|
210
257
|
#[cfg(unix)]
|
|
211
|
-
fn
|
|
212
|
-
use
|
|
213
|
-
let
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
#[cfg(not(unix))]
|
|
221
|
-
fn dir_is_private(_dir: &Path) -> bool {
|
|
222
|
-
true
|
|
258
|
+
fn open_private_dir(dir: &Path) -> Option<rustix::fd::OwnedFd> {
|
|
259
|
+
use rustix::fs::{fstat, open, Mode, OFlags};
|
|
260
|
+
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC;
|
|
261
|
+
let fd = open(dir, flags, Mode::empty()).ok()?;
|
|
262
|
+
let stat = fstat(&fd).ok()?;
|
|
263
|
+
let owned = stat.st_uid == rustix::process::geteuid().as_raw();
|
|
264
|
+
(owned && stat.st_mode & 0o022 == 0).then_some(fd)
|
|
223
265
|
}
|
|
224
266
|
|
|
225
267
|
/// Live temp files are seconds old and never qualify; foreign file names
|
|
226
268
|
/// are left untouched.
|
|
227
|
-
|
|
228
|
-
|
|
269
|
+
#[cfg(unix)]
|
|
270
|
+
fn prune_stale(dir: &rustix::fd::OwnedFd, keep: &std::ffi::OsStr) {
|
|
271
|
+
use rustix::fs::{statat, unlinkat, AtFlags, Dir};
|
|
272
|
+
use std::os::unix::ffi::OsStrExt;
|
|
273
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
274
|
+
let Ok(mut entries) = Dir::read_from(dir) else {
|
|
229
275
|
return;
|
|
230
276
|
};
|
|
231
|
-
|
|
232
|
-
let
|
|
233
|
-
if
|
|
277
|
+
while let Some(Ok(entry)) = entries.read() {
|
|
278
|
+
let name = entry.file_name();
|
|
279
|
+
if name.to_bytes() == keep.as_bytes() || !name.to_str().is_ok_and(is_cache_entry_name) {
|
|
234
280
|
continue;
|
|
235
281
|
}
|
|
236
|
-
let stale =
|
|
237
|
-
.metadata()
|
|
238
|
-
.and_then(|meta| meta.modified())
|
|
282
|
+
let stale = statat(dir, name, AtFlags::SYMLINK_NOFOLLOW)
|
|
239
283
|
.ok()
|
|
240
|
-
.and_then(|
|
|
284
|
+
.and_then(|stat| u64::try_from(stat.st_mtime).ok())
|
|
285
|
+
.and_then(|secs| {
|
|
286
|
+
SystemTime::now()
|
|
287
|
+
.duration_since(UNIX_EPOCH + Duration::from_secs(secs))
|
|
288
|
+
.ok()
|
|
289
|
+
})
|
|
241
290
|
.is_some_and(|age| age > ARTIFACT_TTL);
|
|
242
291
|
if stale {
|
|
243
|
-
let _ =
|
|
292
|
+
let _ = unlinkat(dir, name, AtFlags::empty());
|
|
244
293
|
}
|
|
245
294
|
}
|
|
246
295
|
}
|
|
247
296
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
return false;
|
|
251
|
-
};
|
|
297
|
+
#[cfg(unix)]
|
|
298
|
+
fn is_cache_entry_name(name: &str) -> bool {
|
|
252
299
|
name.ends_with(".cwasm") || name.contains(".tmp")
|
|
253
300
|
}
|
|
301
|
+
|
|
302
|
+
#[cfg(test)]
|
|
303
|
+
mod tests {
|
|
304
|
+
use super::*;
|
|
305
|
+
use wasmtime::ModuleVersionStrategy;
|
|
306
|
+
|
|
307
|
+
fn engine_with(version: &str) -> WtEngine {
|
|
308
|
+
let mut config = WtConfig::new();
|
|
309
|
+
config
|
|
310
|
+
.module_version(ModuleVersionStrategy::Custom(version.into()))
|
|
311
|
+
.expect("a short version string is accepted");
|
|
312
|
+
WtEngine::new(&config).expect("engine")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn entry_name(engine: &WtEngine) -> PathBuf {
|
|
316
|
+
let path = artifact_path(engine, b"\0asm").expect("a cache root");
|
|
317
|
+
PathBuf::from(path.file_name().expect("an entry name"))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A custom module version stands in for another wasmtime release:
|
|
321
|
+
// wasmtime's compatibility hash covers its full version the same way.
|
|
322
|
+
// @behavior RT-108
|
|
323
|
+
#[test]
|
|
324
|
+
fn an_artifact_from_another_engine_release_is_never_the_entry_read() {
|
|
325
|
+
assert_ne!(
|
|
326
|
+
entry_name(&engine_with("49.0.3")),
|
|
327
|
+
entry_name(&engine_with("49.0.4")),
|
|
328
|
+
"a Guest Binary cached by one engine release must not be the entry another release reads"
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Two writers in one process take temp files in turn; each must get a
|
|
333
|
+
// file of its own rather than reopen the other's.
|
|
334
|
+
// @behavior RT-113
|
|
335
|
+
#[cfg(unix)]
|
|
336
|
+
#[test]
|
|
337
|
+
fn two_writers_of_one_artifact_never_share_a_temp_file() {
|
|
338
|
+
let root = std::env::temp_dir().join(format!("kobako-temp-{}", std::process::id()));
|
|
339
|
+
create_cache_dir(&root).expect("a scratch directory");
|
|
340
|
+
let dir = open_private_dir(&root).expect("a private directory");
|
|
341
|
+
let name = std::ffi::OsStr::new("entry.cwasm");
|
|
342
|
+
let first = create_temp(&dir, name).map(|(path, _)| path);
|
|
343
|
+
let second = create_temp(&dir, name).map(|(path, _)| path);
|
|
344
|
+
fs::remove_dir_all(&root).expect("the scratch directory removed");
|
|
345
|
+
let (first, second) = (
|
|
346
|
+
first.expect("a temp file"),
|
|
347
|
+
second.expect("a second temp file"),
|
|
348
|
+
);
|
|
349
|
+
assert_ne!(
|
|
350
|
+
first, second,
|
|
351
|
+
"two stores of one artifact in one process must each write a temp file of their own"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// @behavior RT-108
|
|
356
|
+
#[test]
|
|
357
|
+
fn engines_of_one_release_and_config_share_the_entry() {
|
|
358
|
+
assert_eq!(
|
|
359
|
+
entry_name(&engine_with("49.0.4")),
|
|
360
|
+
entry_name(&engine_with("49.0.4")),
|
|
361
|
+
"two engines of one release and Config must read the entry either of them cached"
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -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
|
|
@@ -6,21 +6,28 @@
|
|
|
6
6
|
//! per-invocation `crate::invocation::Invocation` (which the wasm engine
|
|
7
7
|
//! mutates from inside a run).
|
|
8
8
|
//!
|
|
9
|
-
//!
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
12
|
-
//!
|
|
9
|
+
//! Its caps are field-for-field the SDK's `kobako::Options`, and deliberately
|
|
10
|
+
//! not shared with it: this struct is free to name a wasmtime type, and
|
|
11
|
+
//! `Options` is not — an engine-free host must still be able to build the SDK.
|
|
12
|
+
//! The security modules are the Driver's own, as Linux's LSMs are the
|
|
13
|
+
//! kernel's: whoever builds the Driver hands them here.
|
|
13
14
|
|
|
15
|
+
use std::sync::Arc;
|
|
14
16
|
use std::time::Duration;
|
|
15
17
|
|
|
16
18
|
use kobako_runtime::profile::Profile;
|
|
19
|
+
use kobako_runtime::vfs::SecurityOperations;
|
|
17
20
|
|
|
18
21
|
/// The caps and requested isolation profile for one `Driver`. `None` on
|
|
19
22
|
/// any cap field disables that cap.
|
|
23
|
+
#[non_exhaustive]
|
|
20
24
|
pub struct Config {
|
|
21
25
|
/// Wall-clock cap for one guest `#eval` / `#run`; the invocation traps
|
|
22
26
|
/// once it passes.
|
|
23
27
|
pub timeout: Option<Duration>,
|
|
28
|
+
/// Wall-clock cap for setting the Guest Binary up the first time its
|
|
29
|
+
/// path is loaded; past it, construction fails.
|
|
30
|
+
pub setup_timeout: Option<Duration>,
|
|
24
31
|
/// Guest linear-memory cap, in bytes, charged to what one invocation
|
|
25
32
|
/// grows beyond the image it starts from.
|
|
26
33
|
pub memory_limit: Option<usize>,
|
|
@@ -29,9 +36,74 @@ pub struct Config {
|
|
|
29
36
|
pub stdout_limit: Option<usize>,
|
|
30
37
|
/// Byte cap for guest stderr capture, the same as `stdout_limit`.
|
|
31
38
|
pub stderr_limit: Option<usize>,
|
|
39
|
+
/// Descriptors one invocation may hold open, the standard ones and
|
|
40
|
+
/// each Mount's preopen included; an open past it answers `MFILE`.
|
|
41
|
+
/// Only the guest's descriptors count, as `RLIMIT_NOFILE` counts a
|
|
42
|
+
/// process's: the host descriptors a Filesystem holds behind one are
|
|
43
|
+
/// its own, up to two for a file and one for each Layer holding a
|
|
44
|
+
/// directory, Directory Layers alone holding any.
|
|
45
|
+
pub open_file_limit: Option<usize>,
|
|
32
46
|
/// Isolation posture the frontend requested. `Hermetic` freezes
|
|
33
47
|
/// ambient time and entropy, `Permissive` leaves the live WASI
|
|
34
48
|
/// sources, and `Driver::profile` declares it back as the built
|
|
35
49
|
/// posture.
|
|
36
50
|
pub profile: Profile,
|
|
51
|
+
/// Security modules every invocation is judged by, each asked in order
|
|
52
|
+
/// after kobako's own rules and before any Mount's Policy, as Linux
|
|
53
|
+
/// stacks the LSMs it was booted with.
|
|
54
|
+
pub security: Vec<Arc<dyn SecurityOperations>>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Default for Config {
|
|
58
|
+
/// The caps every frontend defaults to: a 60-second deadline, 10
|
|
59
|
+
/// seconds to set the Guest Binary up, 4 MiB for memory, 1 MiB for
|
|
60
|
+
/// each output channel, 1024 open files, and the hermetic floor.
|
|
61
|
+
fn default() -> Self {
|
|
62
|
+
Config {
|
|
63
|
+
timeout: Some(Duration::from_secs(60)),
|
|
64
|
+
setup_timeout: Some(Duration::from_secs(10)),
|
|
65
|
+
memory_limit: Some(4 << 20),
|
|
66
|
+
stdout_limit: Some(1 << 20),
|
|
67
|
+
stderr_limit: Some(1 << 20),
|
|
68
|
+
open_file_limit: Some(1024),
|
|
69
|
+
profile: Profile::Hermetic,
|
|
70
|
+
security: Vec::new(),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[cfg(test)]
|
|
76
|
+
mod tests {
|
|
77
|
+
use super::*;
|
|
78
|
+
|
|
79
|
+
// Literal values, so a drift from the defaults the SDK's Options and
|
|
80
|
+
// the Ruby frontend apply fails here.
|
|
81
|
+
#[test]
|
|
82
|
+
fn a_default_config_carries_every_frontends_default_caps() {
|
|
83
|
+
let config = Config::default();
|
|
84
|
+
assert_eq!(
|
|
85
|
+
(
|
|
86
|
+
config.timeout,
|
|
87
|
+
config.setup_timeout,
|
|
88
|
+
config.memory_limit,
|
|
89
|
+
config.stdout_limit,
|
|
90
|
+
config.stderr_limit,
|
|
91
|
+
config.open_file_limit,
|
|
92
|
+
config.profile,
|
|
93
|
+
config.security.len()
|
|
94
|
+
),
|
|
95
|
+
(
|
|
96
|
+
Some(Duration::from_secs(60)),
|
|
97
|
+
Some(Duration::from_secs(10)),
|
|
98
|
+
Some(4 * 1024 * 1024),
|
|
99
|
+
Some(1024 * 1024),
|
|
100
|
+
Some(1024 * 1024),
|
|
101
|
+
Some(1024),
|
|
102
|
+
Profile::Hermetic,
|
|
103
|
+
0
|
|
104
|
+
),
|
|
105
|
+
"Config::default must carry the caps every frontend defaults to, and no security \
|
|
106
|
+
module beyond kobako's own"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
37
109
|
}
|