kobako 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +12 -2
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +66 -59
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +11 -13
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -8,43 +8,26 @@
|
|
|
8
8
|
//! lives behind the contract surface, so a frontend shell (the Ruby
|
|
9
9
|
//! ext's `Kobako::Runtime`) sees no wasmtime type.
|
|
10
10
|
//!
|
|
11
|
-
//!
|
|
12
|
-
//!
|
|
13
|
-
//! * `driver` — `Driver` + `impl kobako_runtime::runtime::Runtime`
|
|
14
|
-
//! (the run mechanics).
|
|
15
|
-
//! * `cache` — process-wide Engine + per-path Module cache and the
|
|
16
|
-
//! process-singleton epoch ticker thread.
|
|
17
|
-
//! * `config` — per-Driver caps (timeout / stdout / stderr limits).
|
|
18
|
-
//! * `exports` — per-invocation `__kobako_eval` / `_run` /
|
|
19
|
-
//! `_take_outcome` / `_alloc` / `memory` handles.
|
|
20
|
-
//! * `instance_pre` — host-import Linker wiring + per-path
|
|
21
|
-
//! `InstancePre` cache.
|
|
22
|
-
//! * `abi` — the Guest Binary's declared ABI version check, verified
|
|
23
|
-
//! with the cached `InstancePre` it guards.
|
|
24
|
-
//! * `invocation` — Invocation (per-Store context), the
|
|
25
|
-
//! `MemoryLimiter` memory cap, and the trap marker types
|
|
26
|
-
//! (`TimeoutTrap` / `MemoryLimitTrap`).
|
|
27
|
-
//! * `dispatch` — `__kobako_dispatch` host-import dispatch helpers.
|
|
28
|
-
//! * `frames` — stdin frame stream + WASI context assembly, `#run`
|
|
29
|
-
//! envelope write, OUTCOME_BUFFER readout.
|
|
30
|
-
//! * `guest_mem` — Caller-based guest linear-memory alloc / write / read.
|
|
31
|
-
//! * `capture` — stdout / stderr pipe sizing + clip helpers.
|
|
32
|
-
//! * `ambient` — frozen WASI clocks + constant RNG (ambient denial).
|
|
33
|
-
//! * `trap` — wasmtime-error → neutral `Trap` classification.
|
|
11
|
+
//! Each module holds one responsibility and opens with its own doc.
|
|
34
12
|
|
|
35
13
|
mod abi;
|
|
36
14
|
mod ambient;
|
|
37
15
|
mod cache;
|
|
38
16
|
mod capture;
|
|
39
17
|
mod config;
|
|
18
|
+
mod descriptors;
|
|
40
19
|
mod dispatch;
|
|
41
20
|
mod driver;
|
|
42
21
|
mod exports;
|
|
43
|
-
mod frames;
|
|
44
22
|
mod guest_mem;
|
|
23
|
+
mod imports;
|
|
45
24
|
mod instance_pre;
|
|
46
25
|
mod invocation;
|
|
26
|
+
mod io;
|
|
27
|
+
mod limiter;
|
|
28
|
+
mod preview1;
|
|
47
29
|
mod trap;
|
|
30
|
+
mod wasi;
|
|
48
31
|
|
|
49
32
|
pub use config::Config;
|
|
50
33
|
pub use driver::Driver;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
//! The per-invocation memory cap: only the `memory.grow` delta past the
|
|
2
|
+
//! linear-memory size captured at invocation entry is charged, so the
|
|
3
|
+
//! image's initial allocation is outside the budget.
|
|
4
|
+
|
|
5
|
+
use wasmtime::ResourceLimiter;
|
|
6
|
+
|
|
7
|
+
/// Resource limiter that enforces the per-invocation `memory_limit`
|
|
8
|
+
/// cap.
|
|
9
|
+
///
|
|
10
|
+
/// `max_memory` is the byte cap on per-invocation growth (`None` disables
|
|
11
|
+
/// the cap). `baseline` is the linear-memory size captured at invocation
|
|
12
|
+
/// entry by `MemoryLimiter::activate`; the limiter charges only the
|
|
13
|
+
/// `memory.grow` delta past `baseline` against `max_memory`, so the
|
|
14
|
+
/// mruby image's initial allocation and any high-water mark left by
|
|
15
|
+
/// prior invocations on the same Sandbox do not consume the budget.
|
|
16
|
+
/// `cap_active` gates whether the cap is enforced — wasmtime's
|
|
17
|
+
/// `ResourceLimiter` also fires for the module's declared initial
|
|
18
|
+
/// allocation at instantiation time, but the cap stays dormant until
|
|
19
|
+
/// `MemoryLimiter::activate` flips the flag for one `Driver` invoke
|
|
20
|
+
/// call. When `cap_active` is `false`, the limiter always allows
|
|
21
|
+
/// growth.
|
|
22
|
+
///
|
|
23
|
+
/// When `memory.grow` would push the per-invocation delta past
|
|
24
|
+
/// `max_memory`, the limiter returns `MemoryLimitTrap` from
|
|
25
|
+
/// `memory_growing`; wasmtime turns that into the trap surfaced to the
|
|
26
|
+
/// host as a guest invocation failure.
|
|
27
|
+
#[derive(Debug, Clone, Copy)]
|
|
28
|
+
pub(crate) struct MemoryLimiter {
|
|
29
|
+
max_memory: Option<usize>,
|
|
30
|
+
baseline: usize,
|
|
31
|
+
cap_active: bool,
|
|
32
|
+
peak: usize,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl MemoryLimiter {
|
|
36
|
+
pub(crate) fn new(max_memory: Option<usize>) -> Self {
|
|
37
|
+
Self {
|
|
38
|
+
max_memory,
|
|
39
|
+
baseline: 0,
|
|
40
|
+
cap_active: false,
|
|
41
|
+
peak: 0,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// The cap stays dormant until armed, because the module's declared
|
|
46
|
+
/// initial memory is allocated during instantiation and must pass.
|
|
47
|
+
pub(crate) fn activate(&mut self, baseline: usize) {
|
|
48
|
+
self.baseline = baseline;
|
|
49
|
+
self.cap_active = true;
|
|
50
|
+
self.peak = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(crate) fn deactivate(&mut self) {
|
|
54
|
+
self.cap_active = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// A grow the cap rejects never updates the peak, so the reported value
|
|
58
|
+
/// never exceeds `memory_limit`.
|
|
59
|
+
pub(crate) fn peak(&self) -> usize {
|
|
60
|
+
self.peak
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl ResourceLimiter for MemoryLimiter {
|
|
65
|
+
fn memory_growing(
|
|
66
|
+
&mut self,
|
|
67
|
+
_current: usize,
|
|
68
|
+
desired: usize,
|
|
69
|
+
_maximum: Option<usize>,
|
|
70
|
+
) -> wasmtime::Result<bool> {
|
|
71
|
+
if !self.cap_active {
|
|
72
|
+
return Ok(true);
|
|
73
|
+
}
|
|
74
|
+
let delta = desired.saturating_sub(self.baseline);
|
|
75
|
+
if let Some(limit) = self.max_memory {
|
|
76
|
+
if delta > limit {
|
|
77
|
+
return Err(wasmtime::Error::new(MemoryLimitTrap { desired, limit }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if delta > self.peak {
|
|
81
|
+
self.peak = delta;
|
|
82
|
+
}
|
|
83
|
+
Ok(true)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn table_growing(
|
|
87
|
+
&mut self,
|
|
88
|
+
_current: usize,
|
|
89
|
+
_desired: usize,
|
|
90
|
+
_maximum: Option<usize>,
|
|
91
|
+
) -> wasmtime::Result<bool> {
|
|
92
|
+
Ok(true)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Marker error returned from `MemoryLimiter::memory_growing` when the
|
|
97
|
+
/// per-invocation memory cap is exceeded. Downcast from the wasmtime
|
|
98
|
+
/// trap error to classify the failure as a memory-limit `Trap`. Callers
|
|
99
|
+
/// use the `Display` impl below — no field is read directly — so the
|
|
100
|
+
/// inner state stays private.
|
|
101
|
+
#[derive(Debug)]
|
|
102
|
+
pub(crate) struct MemoryLimitTrap {
|
|
103
|
+
desired: usize,
|
|
104
|
+
limit: usize,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl MemoryLimitTrap {
|
|
108
|
+
#[cfg(test)]
|
|
109
|
+
pub(crate) fn new(desired: usize, limit: usize) -> Self {
|
|
110
|
+
Self { desired, limit }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl std::fmt::Display for MemoryLimitTrap {
|
|
115
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
116
|
+
write!(
|
|
117
|
+
f,
|
|
118
|
+
"memory usage exceeded memory_limit: \
|
|
119
|
+
requested={} bytes, limit={} bytes",
|
|
120
|
+
self.desired, self.limit
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
impl std::error::Error for MemoryLimitTrap {}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
//! Unit tests for `MemoryLimiter` — the per-invocation memory
|
|
130
|
+
//! delta cap. The Ruby-facing E2E suite exercises the full path
|
|
131
|
+
//! through wasmtime; these tests pin the pure delta arithmetic so
|
|
132
|
+
//! a regression that breaks the baseline accounting (e.g. dropping
|
|
133
|
+
//! the `baseline` subtraction, or letting `activate` carry stale
|
|
134
|
+
//! state across invocations) is caught without spinning up a
|
|
135
|
+
//! Store.
|
|
136
|
+
use super::{MemoryLimitTrap, MemoryLimiter};
|
|
137
|
+
use wasmtime::ResourceLimiter;
|
|
138
|
+
|
|
139
|
+
fn assert_growing(limiter: &mut MemoryLimiter, desired: usize) {
|
|
140
|
+
assert!(
|
|
141
|
+
limiter.memory_growing(0, desired, None).unwrap(),
|
|
142
|
+
"expected memory_growing({desired}) to allow growth"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn assert_trapping(limiter: &mut MemoryLimiter, desired: usize) {
|
|
147
|
+
let err = limiter
|
|
148
|
+
.memory_growing(0, desired, None)
|
|
149
|
+
.expect_err("expected memory_growing to trap");
|
|
150
|
+
assert!(
|
|
151
|
+
err.downcast_ref::<MemoryLimitTrap>().is_some(),
|
|
152
|
+
"expected MemoryLimitTrap, got {err:?}"
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// @behavior S-113
|
|
157
|
+
#[test]
|
|
158
|
+
fn dormant_limiter_allows_any_growth() {
|
|
159
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
160
|
+
// Without `activate`, the cap is dormant — the module's
|
|
161
|
+
// declared initial allocation must pass through unconditionally.
|
|
162
|
+
assert_growing(&mut limiter, 100 << 20);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// @behavior S-114
|
|
166
|
+
#[test]
|
|
167
|
+
fn delta_below_cap_passes_after_activate() {
|
|
168
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
169
|
+
limiter.activate(2 << 20);
|
|
170
|
+
// baseline=2 MiB, desired=2.5 MiB → delta=0.5 MiB ≤ 1 MiB cap.
|
|
171
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// @behavior S-121
|
|
175
|
+
#[test]
|
|
176
|
+
fn delta_past_cap_traps_with_memory_limit_trap() {
|
|
177
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
178
|
+
limiter.activate(2 << 20);
|
|
179
|
+
// baseline=2 MiB, desired=4 MiB → delta=2 MiB > 1 MiB cap.
|
|
180
|
+
assert_trapping(&mut limiter, 4 << 20);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// @behavior S-008
|
|
184
|
+
#[test]
|
|
185
|
+
fn activate_resets_baseline_on_each_invocation() {
|
|
186
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
187
|
+
limiter.activate(2 << 20);
|
|
188
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 20));
|
|
189
|
+
// Second invocation: linear memory has grown to 3 MiB. Re-arming
|
|
190
|
+
// must re-anchor the baseline so the next 1 MiB of growth fits
|
|
191
|
+
// the per-invocation budget rather than being charged against
|
|
192
|
+
// the prior invocation's residue.
|
|
193
|
+
limiter.activate(3 << 20);
|
|
194
|
+
assert_growing(&mut limiter, (3 << 20) + (1 << 20));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// @behavior S-009
|
|
198
|
+
#[test]
|
|
199
|
+
fn disabled_cap_ignores_delta_size() {
|
|
200
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
201
|
+
limiter.activate(0);
|
|
202
|
+
assert_growing(&mut limiter, 100 << 20);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// @behavior S-116
|
|
206
|
+
#[test]
|
|
207
|
+
fn peak_starts_at_zero_before_any_grow() {
|
|
208
|
+
let limiter = MemoryLimiter::new(Some(1 << 20));
|
|
209
|
+
assert_eq!(limiter.peak(), 0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// @behavior S-115
|
|
213
|
+
#[test]
|
|
214
|
+
fn peak_tracks_high_water_of_delta_past_baseline() {
|
|
215
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
216
|
+
limiter.activate(2 << 20);
|
|
217
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 18)); // delta=256 KiB
|
|
218
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB (new peak)
|
|
219
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 17)); // delta=128 KiB (below peak)
|
|
220
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// @behavior S-062
|
|
224
|
+
#[test]
|
|
225
|
+
fn trap_does_not_update_peak() {
|
|
226
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
227
|
+
limiter.activate(2 << 20);
|
|
228
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19)); // delta=512 KiB
|
|
229
|
+
assert_trapping(&mut limiter, (2 << 20) + (2 << 20)); // would be 2 MiB > 1 MiB cap
|
|
230
|
+
// Peak reflects the last accepted grow, not the rejected desired.
|
|
231
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// @behavior S-117
|
|
235
|
+
#[test]
|
|
236
|
+
fn activate_resets_peak_for_new_invocation() {
|
|
237
|
+
let mut limiter = MemoryLimiter::new(Some(1 << 20));
|
|
238
|
+
limiter.activate(2 << 20);
|
|
239
|
+
assert_growing(&mut limiter, (2 << 20) + (1 << 19));
|
|
240
|
+
assert_eq!(limiter.peak(), 1 << 19);
|
|
241
|
+
limiter.activate(3 << 20);
|
|
242
|
+
assert_eq!(limiter.peak(), 0);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// @behavior S-118
|
|
246
|
+
#[test]
|
|
247
|
+
fn disabled_cap_still_tracks_peak() {
|
|
248
|
+
let mut limiter = MemoryLimiter::new(None);
|
|
249
|
+
limiter.activate(1 << 20);
|
|
250
|
+
assert_growing(&mut limiter, (1 << 20) + (4 << 20));
|
|
251
|
+
assert_eq!(limiter.peak(), 4 << 20);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
//! The checks preview1 makes on an enum or flags argument before a call
|
|
2
|
+
//! looks at its descriptor: a value outside what the witx defines traps,
|
|
3
|
+
//! as wasmtime-wasi's generated bindings refuse to convert it.
|
|
4
|
+
|
|
5
|
+
use super::errno::{Answer, Errno, Failure};
|
|
6
|
+
|
|
7
|
+
/// The bits of `fdflags`.
|
|
8
|
+
pub(crate) const FDFLAGS: u64 = 0x1f;
|
|
9
|
+
/// The bits of `rights`.
|
|
10
|
+
pub(crate) const RIGHTS: u64 = (1 << 30) - 1;
|
|
11
|
+
/// The bits of `lookupflags`.
|
|
12
|
+
pub(crate) const LOOKUPFLAGS: u64 = 0x1;
|
|
13
|
+
/// The bits of `oflags`.
|
|
14
|
+
pub(crate) const OFLAGS: u64 = 0xf;
|
|
15
|
+
/// The bits of `riflags`.
|
|
16
|
+
pub(crate) const RIFLAGS: u64 = 0x3;
|
|
17
|
+
/// The bits of `sdflags`.
|
|
18
|
+
pub(crate) const SDFLAGS: u64 = 0x3;
|
|
19
|
+
|
|
20
|
+
pub(crate) const FDFLAGS_APPEND: u32 = 1 << 0;
|
|
21
|
+
pub(crate) const FDFLAGS_NONBLOCK: u32 = 1 << 2;
|
|
22
|
+
/// The `fdflags` asking each write to be synchronized: `dsync`, `rsync`,
|
|
23
|
+
/// and `sync`.
|
|
24
|
+
pub(crate) const FDFLAGS_SYNCHRONIZED: u32 = (1 << 1) | (1 << 3) | (1 << 4);
|
|
25
|
+
pub(crate) const RIGHTS_FD_READ: u64 = 1 << 1;
|
|
26
|
+
pub(crate) const RIGHTS_FD_WRITE: u64 = 1 << 6;
|
|
27
|
+
|
|
28
|
+
/// The highest `advice` variant.
|
|
29
|
+
pub(crate) const ADVICE_MAX: u32 = 5;
|
|
30
|
+
/// The highest `whence` variant.
|
|
31
|
+
pub(crate) const WHENCE_MAX: u32 = 2;
|
|
32
|
+
|
|
33
|
+
pub(crate) fn flags(value: impl Into<u64>, defined: u64) -> Answer {
|
|
34
|
+
if value.into() & !defined != 0 {
|
|
35
|
+
return Err(Failure::trap("flags value outside what the witx defines"));
|
|
36
|
+
}
|
|
37
|
+
Ok(())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub(crate) fn variant(value: u32, max: u32) -> Answer {
|
|
41
|
+
if value > max {
|
|
42
|
+
return Err(Failure::trap("enum value outside what the witx defines"));
|
|
43
|
+
}
|
|
44
|
+
Ok(())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// `fstflags`, where setting a timestamp and setting it to now are
|
|
48
|
+
/// exclusive: a defined value asking both is `INVAL`.
|
|
49
|
+
pub(crate) fn fstflags(value: u32) -> Answer {
|
|
50
|
+
const ATIM: u32 = 1 << 0;
|
|
51
|
+
const ATIM_NOW: u32 = 1 << 1;
|
|
52
|
+
const MTIM: u32 = 1 << 2;
|
|
53
|
+
const MTIM_NOW: u32 = 1 << 3;
|
|
54
|
+
flags(value, 0xf)?;
|
|
55
|
+
let both = |set, now| value & set != 0 && value & now != 0;
|
|
56
|
+
if both(ATIM, ATIM_NOW) || both(MTIM, MTIM_NOW) {
|
|
57
|
+
return Err(Errno::Inval.into());
|
|
58
|
+
}
|
|
59
|
+
Ok(())
|
|
60
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
//! Resolving a guest path beneath a directory descriptor.
|
|
2
|
+
//!
|
|
3
|
+
//! The runtime is the boundary: a path never climbs above the directory
|
|
4
|
+
//! descriptor it is resolved beneath, as a capability never widens, and
|
|
5
|
+
//! the Mount is asked what the guest may do there before the Filesystem
|
|
6
|
+
//! is, so a Filesystem of a third party's need check neither.
|
|
7
|
+
//! A `..` steps out of what the Filesystem holds rather than out of the
|
|
8
|
+
//! path's text, as a host resolves it, so `missing/..` names nothing and
|
|
9
|
+
//! a `..` out of a link is refused rather than taken from its text. A link
|
|
10
|
+
//! is followed only within the directory the descriptor opened, as
|
|
11
|
+
//! wasmtime-wasi's cap-std `Dir` follows one.
|
|
12
|
+
|
|
13
|
+
use kobako_runtime::filesystem::{FileType, Filesystem, FilesystemError, Metadata};
|
|
14
|
+
|
|
15
|
+
use super::errno::{Answer, Errno};
|
|
16
|
+
use crate::descriptors::{Identity, OpenDirectory};
|
|
17
|
+
|
|
18
|
+
/// Linux's limits on a path, its terminating NUL counted, and on one name
|
|
19
|
+
/// within it.
|
|
20
|
+
pub(crate) const PATH_MAX: usize = 4096;
|
|
21
|
+
pub(crate) const NAME_MAX: usize = 255;
|
|
22
|
+
|
|
23
|
+
/// A guest path resolved beneath a directory: relative to the mount's
|
|
24
|
+
/// root, and whether it ended in `/` or `/.`, which only a directory
|
|
25
|
+
/// satisfies.
|
|
26
|
+
pub(crate) struct ResolvedPath {
|
|
27
|
+
pub(crate) path: String,
|
|
28
|
+
pub(crate) names_directory: bool,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl ResolvedPath {
|
|
32
|
+
/// The path, refused as `NOTDIR` when it must name a directory and
|
|
33
|
+
/// `lookup` finds anything else there: `metadata` for a call that
|
|
34
|
+
/// follows a link at the end, `symlink_metadata` for one that acts on
|
|
35
|
+
/// the link itself.
|
|
36
|
+
pub(crate) fn check_directory(
|
|
37
|
+
self,
|
|
38
|
+
filesystem: &(dyn Filesystem + 'static),
|
|
39
|
+
lookup: fn(&(dyn Filesystem + 'static), &str) -> Result<Metadata, FilesystemError>,
|
|
40
|
+
) -> Answer<String> {
|
|
41
|
+
if self.names_directory {
|
|
42
|
+
match lookup(filesystem, &self.path) {
|
|
43
|
+
Ok(metadata) if metadata.file_type != FileType::Directory => {
|
|
44
|
+
return Err(Errno::Notdir.into())
|
|
45
|
+
}
|
|
46
|
+
Ok(_) | Err(FilesystemError::NotFound) => {}
|
|
47
|
+
Err(err) => return Err(err.into()),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
Ok(self.path)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// The path with a link standing at its end followed within the
|
|
54
|
+
/// directory `dir` opened, for a call following it. A link pointing
|
|
55
|
+
/// at nothing there is `LOOP` to a call that `creates`, since what it
|
|
56
|
+
/// would create could lie outside `dir`, and `NOENT` otherwise.
|
|
57
|
+
pub(crate) fn follow_last(self, dir: &OpenDirectory, creates: bool) -> Answer<Self> {
|
|
58
|
+
let Some(base) = bounded_base(dir) else {
|
|
59
|
+
return Ok(self);
|
|
60
|
+
};
|
|
61
|
+
let filesystem = dir.mount.filesystem();
|
|
62
|
+
let Some(rest) = beneath_base(base, &self.path) else {
|
|
63
|
+
return Ok(self);
|
|
64
|
+
};
|
|
65
|
+
match filesystem.symlink_metadata(&self.path) {
|
|
66
|
+
Ok(metadata) if metadata.file_type == FileType::Symlink => {}
|
|
67
|
+
_ => return Ok(self),
|
|
68
|
+
}
|
|
69
|
+
let path = match filesystem.canonicalize_beneath(base, rest) {
|
|
70
|
+
Ok(found) => join(base, &found),
|
|
71
|
+
Err(FilesystemError::NotFound) if creates => return Err(Errno::Loop.into()),
|
|
72
|
+
Err(err) => return Err(err.into()),
|
|
73
|
+
};
|
|
74
|
+
Ok(Self { path, ..self })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// The directory a descriptor's links are kept within, or `None` where
|
|
79
|
+
/// the Filesystem keeps them within the Mount's root already, or a Mount
|
|
80
|
+
/// follows no link at all.
|
|
81
|
+
fn bounded_base(dir: &OpenDirectory) -> Option<&str> {
|
|
82
|
+
(!dir.path.is_empty() && dir.mount.follows_links()).then_some(dir.path.as_str())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// What `path` names beneath `base`, or `None` for `base` itself.
|
|
86
|
+
fn beneath_base<'a>(base: &str, path: &'a str) -> Option<&'a str> {
|
|
87
|
+
path.strip_prefix(base)?.strip_prefix('/')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn join(base: &str, path: &str) -> String {
|
|
91
|
+
if path.is_empty() {
|
|
92
|
+
base.to_string()
|
|
93
|
+
} else {
|
|
94
|
+
format!("{base}/{path}")
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// `path` with the links on its way to its last name followed within the
|
|
99
|
+
/// directory `dir` opened, so none leads the call out of it. A way the
|
|
100
|
+
/// Filesystem finds nothing on stays as given, for the call to answer.
|
|
101
|
+
fn within(dir: &OpenDirectory, path: String) -> Answer<String> {
|
|
102
|
+
let Some(base) = bounded_base(dir) else {
|
|
103
|
+
return Ok(path);
|
|
104
|
+
};
|
|
105
|
+
let Some((way, name)) = beneath_base(base, &path).and_then(|rest| rest.rsplit_once('/')) else {
|
|
106
|
+
return Ok(path);
|
|
107
|
+
};
|
|
108
|
+
match dir.mount.filesystem().canonicalize_beneath(base, way) {
|
|
109
|
+
Ok(found) => Ok(join(&join(base, &found), name)),
|
|
110
|
+
Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => Ok(path),
|
|
111
|
+
Err(err) => Err(err.into()),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// `path`, beneath `dir`. An empty path names nothing; an absolute path or
|
|
116
|
+
/// a `..` above `dir` itself is not the guest's to reach; a `..` or a final `.`
|
|
117
|
+
/// after a component that is missing, no directory, or a link is `NOENT`,
|
|
118
|
+
/// `NOTDIR`, or `LOOP`. A
|
|
119
|
+
/// name past `NAME_MAX`, or a resolved path the Filesystem could not be
|
|
120
|
+
/// handed within `PATH_MAX`, is `NAMETOOLONG`.
|
|
121
|
+
pub(crate) fn resolve(dir: &OpenDirectory, path: &str) -> Answer<ResolvedPath> {
|
|
122
|
+
confirm(dir)?;
|
|
123
|
+
if path.is_empty() {
|
|
124
|
+
return Err(Errno::Noent.into());
|
|
125
|
+
}
|
|
126
|
+
if path.starts_with('/') {
|
|
127
|
+
return Err(Errno::Notcapable.into());
|
|
128
|
+
}
|
|
129
|
+
let mut components: Vec<&str> = dir.path.split('/').filter(|c| !c.is_empty()).collect();
|
|
130
|
+
let opened = components.len();
|
|
131
|
+
let mut last = "";
|
|
132
|
+
for component in path.split('/') {
|
|
133
|
+
last = component;
|
|
134
|
+
match component {
|
|
135
|
+
"" | "." => {}
|
|
136
|
+
".." => {
|
|
137
|
+
if components.len() <= opened {
|
|
138
|
+
return Err(Errno::Notcapable.into());
|
|
139
|
+
}
|
|
140
|
+
step_out_of(dir.mount.filesystem().as_ref(), &components.join("/"))?;
|
|
141
|
+
components.pop();
|
|
142
|
+
}
|
|
143
|
+
component if component.len() > NAME_MAX => return Err(Errno::Nametoolong.into()),
|
|
144
|
+
component => components.push(component),
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
let path = components.join("/");
|
|
148
|
+
if path.len() >= PATH_MAX {
|
|
149
|
+
return Err(Errno::Nametoolong.into());
|
|
150
|
+
}
|
|
151
|
+
// A final `.` resolves through the name before it, as a `..` does.
|
|
152
|
+
if last == "." && components.len() > opened {
|
|
153
|
+
step_out_of(dir.mount.filesystem().as_ref(), &path)?;
|
|
154
|
+
}
|
|
155
|
+
Ok(ResolvedPath {
|
|
156
|
+
path: within(dir, path)?,
|
|
157
|
+
names_directory: matches!(last, "" | "."),
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// `dir` still stands at its path, `NOENT` once another directory or
|
|
162
|
+
/// nothing does, as a POSIX descriptor never reaches a directory made in
|
|
163
|
+
/// its place. A directory opened with no identity follows its path alone.
|
|
164
|
+
pub(crate) fn confirm(dir: &OpenDirectory) -> Answer {
|
|
165
|
+
let Some(identity) = dir.identity else {
|
|
166
|
+
return Ok(());
|
|
167
|
+
};
|
|
168
|
+
match dir.mount.filesystem().metadata(&dir.path) {
|
|
169
|
+
Ok(found) if Identity::of(&found) == Some(identity) => Ok(()),
|
|
170
|
+
Ok(_) | Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => {
|
|
171
|
+
Err(Errno::Noent.into())
|
|
172
|
+
}
|
|
173
|
+
Err(err) => Err(err.into()),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// A `..` leaves `path` only when it is a directory the Filesystem holds.
|
|
178
|
+
/// A host leaves a link by the parent of where it points, which no guest
|
|
179
|
+
/// is told, so a link there is refused as `LOOP`, as `openat2(2)`
|
|
180
|
+
/// refuses a link it may not follow.
|
|
181
|
+
fn step_out_of(filesystem: &dyn Filesystem, path: &str) -> Answer {
|
|
182
|
+
match filesystem.symlink_metadata(path) {
|
|
183
|
+
Ok(metadata) if metadata.file_type == FileType::Directory => Ok(()),
|
|
184
|
+
Ok(metadata) if metadata.file_type == FileType::Symlink => Err(Errno::Loop.into()),
|
|
185
|
+
Ok(_) => Err(Errno::Notdir.into()),
|
|
186
|
+
Err(err) => Err(err.into()),
|
|
187
|
+
}
|
|
188
|
+
}
|