kobako 0.29.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 +217 -10
- data/Cargo.lock +139 -141
- data/README.md +96 -574
- data/ROADMAP.md +2 -1
- data/crates/kobako-runtime/CHANGELOG.md +269 -1
- data/crates/kobako-runtime/Cargo.toml +25 -15
- data/crates/kobako-runtime/README.md +7 -3
- data/crates/kobako-runtime/src/dispatch.rs +1 -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/{directory/spelling.rs → bind/lookup.rs} +91 -49
- 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 +227 -734
- data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
- data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
- 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 +21 -387
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +11 -6
- 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 +31 -104
- data/crates/kobako-runtime/src/profile.rs +137 -1
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- 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/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 +8 -2
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- 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 +2 -2
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/request.rs +2 -2
- 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 +154 -8
- data/crates/kobako-wasmtime/Cargo.toml +15 -14
- data/crates/kobako-wasmtime/README.md +9 -5
- data/crates/kobako-wasmtime/src/abi.rs +12 -20
- data/crates/kobako-wasmtime/src/ambient.rs +1 -2
- data/crates/kobako-wasmtime/src/cache.rs +5 -5
- data/crates/kobako-wasmtime/src/config.rs +22 -7
- data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
- data/crates/kobako-wasmtime/src/driver.rs +29 -96
- 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 +14 -8
- data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
- data/crates/kobako-wasmtime/src/invocation.rs +26 -46
- data/crates/kobako-wasmtime/src/lib.rs +1 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
- data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
- data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
- data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
- data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
- data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
- data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
- 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 +464 -413
- data/crates/kobako-wasmtime/src/trap.rs +9 -9
- data/crates/kobako-wasmtime/src/wasi.rs +53 -37
- data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -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 +1 -1
- data/ext/kobako/src/runtime/bridge.rs +1 -1
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +28 -14
- data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
- data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
- data/ext/kobako/src/runtime/filesystem.rs +109 -137
- data/ext/kobako/src/runtime/gvl.rs +43 -8
- data/ext/kobako/src/runtime/snapshot.rs +1 -1
- data/ext/kobako/src/runtime.rs +6 -1
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -2
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +2 -10
- data/lib/kobako/catalog/services.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +7 -7
- data/lib/kobako/catalog.rb +1 -1
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/ext_types.rb +3 -5
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +3 -3
- data/lib/kobako/context.rb +5 -5
- data/lib/kobako/filesystem.rb +44 -13
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +4 -4
- data/lib/kobako/sandbox.rb +28 -28
- data/lib/kobako/sandbox_options.rb +2 -3
- data/lib/kobako/snippet/bytecode.rb +1 -1
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +5 -5
- data/lib/kobako/transport/exposure.rb +2 -2
- data/lib/kobako/transport/reflection.rb +7 -7
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +34 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +0 -4
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/catalog/snippets.rbs +3 -3
- data/sig/kobako/catalog.rbs +1 -1
- 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/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +1 -1
- data/sig/kobako/codec.rbs +1 -2
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/filesystem.rbs +19 -14
- data/sig/kobako/guest_yielder.rbs +1 -1
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +2 -2
- data/sig/kobako/runtime.rbs +1 -1
- data/sig/kobako/sandbox.rbs +3 -5
- data/sig/kobako/sandbox_options.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -2
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +2 -2
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport.rbs +1 -1
- metadata +67 -19
- data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
- data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
- data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
- data/crates/kobako-wasmtime/src/io.rs +0 -89
- data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
- /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
- /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
|
@@ -16,6 +16,7 @@ use std::sync::Arc;
|
|
|
16
16
|
use std::time::{Duration, Instant};
|
|
17
17
|
|
|
18
18
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
19
|
+
use kobako_runtime::endpoint::{Calls, MemoryCap, ABANDONED};
|
|
19
20
|
|
|
20
21
|
use crate::exports::Exports;
|
|
21
22
|
use crate::limiter::MemoryLimiter;
|
|
@@ -31,31 +32,39 @@ use crate::wasi::Wasi;
|
|
|
31
32
|
/// the mutation surface stays narrow.
|
|
32
33
|
pub(crate) struct Invocation {
|
|
33
34
|
wasi: Wasi,
|
|
34
|
-
|
|
35
|
+
calls: Calls<wasmtime::Error>,
|
|
35
36
|
deadline: Option<Instant>,
|
|
36
37
|
limiter: MemoryLimiter,
|
|
37
38
|
wall_entry: Option<Instant>,
|
|
38
39
|
wall_time: Duration,
|
|
39
|
-
reentry_trap: Option<wasmtime::Error>,
|
|
40
40
|
exports: Option<Exports>,
|
|
41
|
-
|
|
41
|
+
hostcall_fuel: usize,
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
impl Invocation {
|
|
45
45
|
pub(crate) fn new(memory_limit: Option<usize>, wasi: Wasi) -> Self {
|
|
46
46
|
Self {
|
|
47
47
|
wasi,
|
|
48
|
-
|
|
48
|
+
calls: Calls::default(),
|
|
49
49
|
deadline: None,
|
|
50
|
-
limiter: MemoryLimiter::new(memory_limit),
|
|
50
|
+
limiter: MemoryLimiter(MemoryCap::new(memory_limit)),
|
|
51
51
|
wall_entry: None,
|
|
52
52
|
wall_time: Duration::ZERO,
|
|
53
|
-
reentry_trap: None,
|
|
54
53
|
exports: None,
|
|
55
|
-
|
|
54
|
+
hostcall_fuel: 0,
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
/// How much of the guest's memory the call under way may have the host
|
|
59
|
+
/// copy or walk, as wasmtime hands each host call.
|
|
60
|
+
pub(crate) fn hostcall_fuel(&self) -> usize {
|
|
61
|
+
self.hostcall_fuel
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(crate) fn set_hostcall_fuel(&mut self, fuel: usize) {
|
|
65
|
+
self.hostcall_fuel = fuel;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
pub(crate) fn set_exports(&mut self, exports: Exports) {
|
|
60
69
|
self.exports = Some(exports);
|
|
61
70
|
}
|
|
@@ -66,49 +75,20 @@ impl Invocation {
|
|
|
66
75
|
self.exports.clone()
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
/// The
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
self.reentry_trap.get_or_insert(trap);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/// Once a callback into the guest has trapped, the guest is past
|
|
76
|
-
/// resuming.
|
|
77
|
-
pub(crate) fn reentry_trapped(&self) -> bool {
|
|
78
|
-
self.reentry_trap.is_some()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
pub(crate) fn take_reentry_trap(&mut self) -> Option<wasmtime::Error> {
|
|
82
|
-
self.reentry_trap.take()
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/// Count a Call the host is about to answer and name its frame, as the
|
|
86
|
-
/// guest counts the Calls it has open.
|
|
87
|
-
pub(crate) fn open_call(&mut self) -> u32 {
|
|
88
|
-
self.open_calls += 1;
|
|
89
|
-
self.open_calls - 1
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
pub(crate) fn close_call(&mut self) {
|
|
93
|
-
self.open_calls -= 1;
|
|
78
|
+
/// The state of the Calls open on this instance.
|
|
79
|
+
pub(crate) fn calls_mut(&mut self) -> &mut Calls<wasmtime::Error> {
|
|
80
|
+
&mut self.calls
|
|
94
81
|
}
|
|
95
82
|
|
|
96
83
|
pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
|
|
97
|
-
self.
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/// A clone, so the borrow on the `Caller` is released and the
|
|
101
|
-
/// dispatcher can re-borrow it to write the Reply.
|
|
102
|
-
pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
|
|
103
|
-
self.on_dispatch.clone()
|
|
84
|
+
self.calls.bind(handler);
|
|
104
85
|
}
|
|
105
86
|
|
|
106
87
|
/// The trap ending an invocation its host gave up on.
|
|
107
88
|
pub(crate) fn abandonment(&self) -> Option<wasmtime::Error> {
|
|
108
|
-
self.
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.then(|| wasmtime::Error::msg("the host abandoned the invocation"))
|
|
89
|
+
self.calls
|
|
90
|
+
.abandoned()
|
|
91
|
+
.then(|| wasmtime::Error::msg(ABANDONED))
|
|
112
92
|
}
|
|
113
93
|
|
|
114
94
|
pub(crate) fn wasi(&self) -> &Wasi {
|
|
@@ -135,13 +115,13 @@ impl Invocation {
|
|
|
135
115
|
/// image's initial allocation and the high-water mark of earlier
|
|
136
116
|
/// invocations do not consume the budget.
|
|
137
117
|
pub(crate) fn arm_memory_cap(&mut self, baseline: usize) {
|
|
138
|
-
self.limiter.activate(baseline);
|
|
118
|
+
self.limiter.0.activate(baseline);
|
|
139
119
|
}
|
|
140
120
|
|
|
141
121
|
/// Disarmed once the export returns, so post-run host bookkeeping such
|
|
142
122
|
/// as reading the answer back is not charged to the script.
|
|
143
123
|
pub(crate) fn disarm_memory_cap(&mut self) {
|
|
144
|
-
self.limiter.deactivate();
|
|
124
|
+
self.limiter.0.deactivate();
|
|
145
125
|
}
|
|
146
126
|
|
|
147
127
|
/// Stamped right before the guest export call, so the bracket matches
|
|
@@ -161,6 +141,6 @@ impl Invocation {
|
|
|
161
141
|
}
|
|
162
142
|
|
|
163
143
|
pub(crate) fn memory_peak(&self) -> usize {
|
|
164
|
-
self.limiter.peak()
|
|
144
|
+
self.limiter.0.peak()
|
|
165
145
|
}
|
|
166
146
|
}
|
|
@@ -13,17 +13,14 @@
|
|
|
13
13
|
mod abi;
|
|
14
14
|
mod ambient;
|
|
15
15
|
mod cache;
|
|
16
|
-
mod capture;
|
|
17
16
|
mod config;
|
|
18
17
|
mod descriptors;
|
|
19
|
-
mod dispatch;
|
|
20
18
|
mod driver;
|
|
21
19
|
mod exports;
|
|
22
|
-
mod
|
|
20
|
+
mod guest;
|
|
23
21
|
mod imports;
|
|
24
22
|
mod instance_pre;
|
|
25
23
|
mod invocation;
|
|
26
|
-
mod io;
|
|
27
24
|
mod limiter;
|
|
28
25
|
mod preview1;
|
|
29
26
|
mod trap;
|
|
@@ -1,65 +1,11 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//! linear-memory size captured at invocation entry is charged, so the
|
|
3
|
-
//! image's initial allocation is outside the budget.
|
|
1
|
+
//! Hands wasmtime's growth hook to the runtime's per-invocation memory cap.
|
|
4
2
|
|
|
3
|
+
use kobako_runtime::endpoint::MemoryCap;
|
|
5
4
|
use wasmtime::ResourceLimiter;
|
|
6
5
|
|
|
7
|
-
///
|
|
8
|
-
///
|
|
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
|
-
}
|
|
6
|
+
/// wasmtime asks this as `memory.grow` runs; an `Err` becomes the trap
|
|
7
|
+
/// surfaced to the host as a guest invocation failure.
|
|
8
|
+
pub(crate) struct MemoryLimiter(pub(crate) MemoryCap);
|
|
63
9
|
|
|
64
10
|
impl ResourceLimiter for MemoryLimiter {
|
|
65
11
|
fn memory_growing(
|
|
@@ -68,18 +14,7 @@ impl ResourceLimiter for MemoryLimiter {
|
|
|
68
14
|
desired: usize,
|
|
69
15
|
_maximum: Option<usize>,
|
|
70
16
|
) -> wasmtime::Result<bool> {
|
|
71
|
-
|
|
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
|
-
}
|
|
17
|
+
self.0.charge(desired).map_err(wasmtime::Error::new)?;
|
|
83
18
|
Ok(true)
|
|
84
19
|
}
|
|
85
20
|
|
|
@@ -92,162 +27,3 @@ impl ResourceLimiter for MemoryLimiter {
|
|
|
92
27
|
Ok(true)
|
|
93
28
|
}
|
|
94
29
|
}
|
|
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
|
-
}
|
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
//! The differential probe beneath a Mount: each path call, made on the
|
|
2
|
-
//! same host directory through kobako's `
|
|
2
|
+
//! same host directory through kobako's `Bind` Mount and through
|
|
3
3
|
//! wasmtime-wasi's own preopen, must leave the same errno, the same
|
|
4
4
|
//! descriptor, and the same directory behind. A listing of the Mount must
|
|
5
5
|
//! answer the same errno.
|
|
6
6
|
//!
|
|
7
|
-
//!
|
|
8
|
-
//! stat writes only the filetype is
|
|
9
|
-
//! directory carry the rest of the decision.
|
|
7
|
+
//! kobako's inode numbers are its own, and its times differ from the host's
|
|
8
|
+
//! by when each call ran, so of what a stat writes only the filetype is
|
|
9
|
+
//! compared; the errno and the directory carry the rest of the decision.
|
|
10
10
|
//!
|
|
11
11
|
//! Where POSIX lets each host choose, wasmtime-wasi passes on the host's
|
|
12
12
|
//! own answer — unlinking a directory is `ISDIR` on Linux and `PERM` on
|
|
13
13
|
//! macOS. kobako answers one way on every host, the way Linux does, so
|
|
14
14
|
//! the probe holds where CI runs it.
|
|
15
|
+
//!
|
|
16
|
+
//! An open asking to sync, or for a directory it would truncate, is left out:
|
|
17
|
+
//! wasmtime-wasi refuses both as invalid or unsupported, where kobako answers
|
|
18
|
+
//! as Linux does, which `mount_view` holds to literal answers.
|
|
15
19
|
|
|
16
20
|
use std::fs;
|
|
17
21
|
use std::path::{Path, PathBuf};
|
|
18
22
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
19
23
|
use std::sync::{Arc, OnceLock};
|
|
20
24
|
|
|
21
|
-
use kobako_runtime::filesystem::
|
|
22
|
-
use kobako_runtime::filesystem::
|
|
25
|
+
use kobako_runtime::filesystem::Bind;
|
|
26
|
+
use kobako_runtime::filesystem::Filesystem;
|
|
23
27
|
use kobako_runtime::grant::Grant;
|
|
24
|
-
use kobako_runtime::mount::Mount;
|
|
28
|
+
use kobako_runtime::mount::{Access, Mount};
|
|
25
29
|
use kobako_runtime::profile::Profile;
|
|
30
|
+
use kobako_runtime::vfs::PATH_MAX;
|
|
26
31
|
use wasmtime::{Linker, Module, Store};
|
|
27
32
|
use wasmtime_wasi::p1::{self, WasiP1Ctx};
|
|
28
33
|
use wasmtime_wasi::{FsPerms, WasiCtxBuilder};
|
|
29
34
|
|
|
30
|
-
use crate::cache::
|
|
35
|
+
use crate::cache::load_engine;
|
|
31
36
|
use crate::config::Config;
|
|
32
37
|
use crate::imports;
|
|
33
38
|
use crate::invocation::Invocation;
|
|
34
|
-
use crate::preview1::beneath::PATH_MAX;
|
|
35
39
|
use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
|
|
36
40
|
use crate::wasi::Wasi;
|
|
37
41
|
|
|
@@ -89,7 +93,6 @@ const OFLAGS_DIRECTORY: i32 = 1 << 1;
|
|
|
89
93
|
const OFLAGS_EXCL: i32 = 1 << 2;
|
|
90
94
|
const OFLAGS_TRUNC: i32 = 1 << 3;
|
|
91
95
|
const FDFLAGS_NONBLOCK: i32 = 1 << 2;
|
|
92
|
-
const FDFLAGS_SYNC: i32 = 1 << 4;
|
|
93
96
|
const RIGHTS_FD_READ: i64 = 1 << 1;
|
|
94
97
|
const RIGHTS_FD_WRITE: i64 = 1 << 6;
|
|
95
98
|
|
|
@@ -102,6 +105,7 @@ const PATHS: &[&str] = &[
|
|
|
102
105
|
"",
|
|
103
106
|
".",
|
|
104
107
|
"a/",
|
|
108
|
+
"g",
|
|
105
109
|
"l/",
|
|
106
110
|
"n/",
|
|
107
111
|
"g/",
|
|
@@ -193,16 +197,6 @@ const CALLS: &[Call] = &[
|
|
|
193
197
|
rights: RIGHTS_FD_READ,
|
|
194
198
|
fdflags: 0,
|
|
195
199
|
},
|
|
196
|
-
Call::Open {
|
|
197
|
-
oflags: OFLAGS_DIRECTORY | OFLAGS_TRUNC,
|
|
198
|
-
rights: RIGHTS_FD_READ,
|
|
199
|
-
fdflags: 0,
|
|
200
|
-
},
|
|
201
|
-
Call::Open {
|
|
202
|
-
oflags: 0,
|
|
203
|
-
rights: RIGHTS_FD_READ,
|
|
204
|
-
fdflags: FDFLAGS_SYNC,
|
|
205
|
-
},
|
|
206
200
|
Call::Open {
|
|
207
201
|
oflags: 0,
|
|
208
202
|
rights: RIGHTS_FD_READ,
|
|
@@ -378,9 +372,9 @@ fn through_wasmtime_wasi(call: Call, path: &str) -> Observation {
|
|
|
378
372
|
observe(store, &linker, call, path, &host)
|
|
379
373
|
}
|
|
380
374
|
|
|
381
|
-
/// `host` behind a kobako `
|
|
375
|
+
/// `host` behind a kobako `Bind` Mount at `AT`.
|
|
382
376
|
fn kobako_preopen(host: &HostDir) -> (Store<Invocation>, Linker<Invocation>) {
|
|
383
|
-
let engine =
|
|
377
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
384
378
|
let config = Config {
|
|
385
379
|
timeout: None,
|
|
386
380
|
setup_timeout: None,
|
|
@@ -389,12 +383,19 @@ fn kobako_preopen(host: &HostDir) -> (Store<Invocation>, Linker<Invocation>) {
|
|
|
389
383
|
stderr_limit: None,
|
|
390
384
|
open_file_limit: None,
|
|
391
385
|
profile: Profile::Hermetic,
|
|
386
|
+
security: Vec::new(),
|
|
392
387
|
};
|
|
393
|
-
let directory =
|
|
388
|
+
let directory = Bind::new(&host.0).expect("the host directory must open");
|
|
394
389
|
let grant = Grant::Mount(
|
|
395
|
-
Mount::new(
|
|
390
|
+
Mount::new(
|
|
391
|
+
AT,
|
|
392
|
+
Filesystem::new(Arc::new(directory)).expect("the instance must stand"),
|
|
393
|
+
Access::ReadWrite,
|
|
394
|
+
)
|
|
395
|
+
.expect("AT is a guest path"),
|
|
396
396
|
);
|
|
397
|
-
let wasi =
|
|
397
|
+
let wasi =
|
|
398
|
+
Wasi::for_invocation(&config, Vec::new(), &[grant]).expect("WASI context must build");
|
|
398
399
|
let mut store = Store::new(engine, Invocation::new(None, wasi));
|
|
399
400
|
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
400
401
|
let linker = imports::linker().expect("host imports must link");
|
|
@@ -403,7 +404,7 @@ fn kobako_preopen(host: &HostDir) -> (Store<Invocation>, Linker<Invocation>) {
|
|
|
403
404
|
|
|
404
405
|
/// `host` behind wasmtime-wasi's own preopen at `AT`.
|
|
405
406
|
fn wasmtime_wasi_preopen(host: &HostDir) -> (Store<WasiP1Ctx>, Linker<WasiP1Ctx>) {
|
|
406
|
-
let engine =
|
|
407
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
407
408
|
let ctx = WasiCtxBuilder::new()
|
|
408
409
|
.preopened_dir(&host.0, AT, FsPerms::ReadWrite)
|
|
409
410
|
.expect("the host directory must preopen")
|
|
@@ -416,17 +417,17 @@ fn wasmtime_wasi_preopen(host: &HostDir) -> (Store<WasiP1Ctx>, Linker<WasiP1Ctx>
|
|
|
416
417
|
}
|
|
417
418
|
|
|
418
419
|
const ERRNO_BUSY: i32 = 10;
|
|
420
|
+
const ERRNO_EXIST: i32 = 20;
|
|
419
421
|
const ERRNO_INVAL: i32 = 28;
|
|
420
422
|
const ERRNO_ISDIR: i32 = 31;
|
|
421
|
-
const ERRNO_LOOP: i32 = 32;
|
|
422
423
|
const ERRNO_NAMETOOLONG: i32 = 37;
|
|
423
424
|
const ERRNO_NOENT: i32 = 44;
|
|
424
425
|
const ERRNO_NOTDIR: i32 = 54;
|
|
426
|
+
const ERRNO_NOTEMPTY: i32 = 55;
|
|
425
427
|
const ERRNO_NOTSUP: i32 = 58;
|
|
426
428
|
const ERRNO_PERM: i32 = 63;
|
|
427
429
|
const ERRNO_NOTCAPABLE: i32 = 76;
|
|
428
430
|
|
|
429
|
-
/// Whether `path` reaches above the root it is resolved beneath.
|
|
430
431
|
/// Whether `path` takes a `..`, or ends in a `.`, right after one of the
|
|
431
432
|
/// host directory's links.
|
|
432
433
|
fn steps_out_of_a_link(path: &str) -> bool {
|
|
@@ -439,6 +440,17 @@ fn steps_out_of_a_link(path: &str) -> bool {
|
|
|
439
440
|
}
|
|
440
441
|
}
|
|
441
442
|
|
|
443
|
+
/// Whether `call` acts on the last name of `path` rather than land on it, a
|
|
444
|
+
/// `.` or `..` there taking the walk nowhere, as Linux's `LOOKUP_PARENT` leaves
|
|
445
|
+
/// it.
|
|
446
|
+
fn acts_on_a_dot(call: Call, path: &str) -> bool {
|
|
447
|
+
matches!(
|
|
448
|
+
call,
|
|
449
|
+
Call::Mkdir | Call::Rmdir | Call::Unlink | Call::RenameFrom | Call::RenameTo
|
|
450
|
+
) && matches!(path.rsplit('/').next(), Some("." | ".."))
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// Whether `path` reaches above the root it is resolved beneath.
|
|
442
454
|
fn escapes(path: &str) -> bool {
|
|
443
455
|
let mut depth = 0usize;
|
|
444
456
|
path.starts_with('/')
|
|
@@ -458,9 +470,8 @@ fn escapes(path: &str) -> bool {
|
|
|
458
470
|
})
|
|
459
471
|
}
|
|
460
472
|
|
|
461
|
-
/// What kobako answers where it differs from wasmtime-wasi on purpose.
|
|
462
|
-
///
|
|
463
|
-
/// flag and blocks anyway. A path above the root is `NOTCAPABLE`, the code preview1 names for a
|
|
473
|
+
/// What kobako answers where it differs from wasmtime-wasi on purpose. A path
|
|
474
|
+
/// above the root is `NOTCAPABLE`, the code preview1 names for a
|
|
464
475
|
/// capability the guest was never handed, where wasmtime-wasi passes on
|
|
465
476
|
/// cap-std's refusal as `PERM`. A path past `PATH_MAX` is `NAMETOOLONG`
|
|
466
477
|
/// as Linux answers the whole path, where cap-std resolves it one name
|
|
@@ -469,11 +480,13 @@ fn escapes(path: &str) -> bool {
|
|
|
469
480
|
/// which follows a link there, where cap-std's shortcut for one name
|
|
470
481
|
/// drops the `/` and finds the file or the link. A `rmdir` of a link
|
|
471
482
|
/// named with a trailing `/` is `NOTDIR` as Linux answers it, where
|
|
472
|
-
/// cap-std's split turns the `/` into `.` and answers `INVAL`. A
|
|
483
|
+
/// cap-std's split turns the `/` into `.` and answers `INVAL`. A `rmdir`
|
|
484
|
+
/// of a path ending in `..` is `NOTEMPTY` as Linux answers it, where
|
|
485
|
+
/// cap-std answers `INVAL` as it does for a final `.`. A `..` or
|
|
473
486
|
/// a final `.` after a link is `LOOP` and changes nothing, where cap-std
|
|
474
487
|
/// steps out of where the link points; a call refused before its path
|
|
475
488
|
/// resolves answers alike.
|
|
476
|
-
fn
|
|
489
|
+
fn predict(call: Call, path: &str, mut oracle: Observation) -> Observation {
|
|
477
490
|
let refused = |errno| Observation {
|
|
478
491
|
errno: Some(errno),
|
|
479
492
|
opened: [0; 4],
|
|
@@ -482,20 +495,24 @@ fn expected(call: Call, path: &str, mut oracle: Observation) -> Observation {
|
|
|
482
495
|
filetype: 0,
|
|
483
496
|
tree: HostDir::new().tree(),
|
|
484
497
|
};
|
|
485
|
-
if matches!(call, Call::Open { fdflags, .. } if fdflags & FDFLAGS_NONBLOCK != 0) {
|
|
486
|
-
return refused(if path.len() >= PATH_MAX {
|
|
487
|
-
ERRNO_NAMETOOLONG
|
|
488
|
-
} else {
|
|
489
|
-
ERRNO_NOTSUP
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
498
|
let refused_first = matches!(oracle.errno, Some(ERRNO_BUSY | ERRNO_INVAL | ERRNO_NOTSUP));
|
|
493
|
-
if steps_out_of_a_link(path) &&
|
|
494
|
-
|
|
499
|
+
if steps_out_of_a_link(path) && acts_on_a_dot(call, path) {
|
|
500
|
+
match call {
|
|
501
|
+
Call::Mkdir => return refused(ERRNO_EXIST),
|
|
502
|
+
Call::Unlink => return refused(ERRNO_ISDIR),
|
|
503
|
+
_ => {}
|
|
504
|
+
}
|
|
495
505
|
}
|
|
496
506
|
if escapes(path) && oracle.errno == Some(ERRNO_PERM) {
|
|
497
507
|
oracle.errno = Some(ERRNO_NOTCAPABLE);
|
|
498
508
|
}
|
|
509
|
+
// A directory keeps the flags it opened with, as Linux's `F_GETFL`
|
|
510
|
+
// reports them, where wasmtime-wasi reports none on one.
|
|
511
|
+
if let Call::Open { fdflags, .. } = call {
|
|
512
|
+
if oracle.errno == Some(0) && oracle.fdstat[0] == FILETYPE_DIRECTORY {
|
|
513
|
+
oracle.fdstat[2] = fdflags as u8;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
499
516
|
// A trailing `/` makes Linux's `lstat` follow the link at the end,
|
|
500
517
|
// where cap-std leaves it, so the host's own `lstat` answers.
|
|
501
518
|
if matches!(call, Call::StatNoFollow) && path.ends_with('/') {
|
|
@@ -532,7 +549,13 @@ fn expected(call: Call, path: &str, mut oracle: Observation) -> Observation {
|
|
|
532
549
|
_ => {}
|
|
533
550
|
}
|
|
534
551
|
}
|
|
535
|
-
if
|
|
552
|
+
if matches!(call, Call::Rmdir)
|
|
553
|
+
&& path.trim_end_matches('/').ends_with("..")
|
|
554
|
+
&& oracle.errno == Some(ERRNO_INVAL)
|
|
555
|
+
{
|
|
556
|
+
oracle.errno = Some(ERRNO_NOTEMPTY);
|
|
557
|
+
}
|
|
558
|
+
if path.len() >= PATH_MAX && !refused_first {
|
|
536
559
|
oracle.errno = Some(ERRNO_NAMETOOLONG);
|
|
537
560
|
}
|
|
538
561
|
oracle
|
|
@@ -554,13 +577,13 @@ fn every_path_call_beneath_a_mount_answers_as_a_wasmtime_wasi_preopen() {
|
|
|
554
577
|
.flat_map(|&call| paths.iter().map(move |path| (call, path.as_str())))
|
|
555
578
|
.filter_map(|(call, path)| {
|
|
556
579
|
let kobako = through_kobako(call, path);
|
|
557
|
-
let oracle =
|
|
580
|
+
let oracle = predict(call, path, through_wasmtime_wasi(call, path));
|
|
558
581
|
(kobako != oracle).then(|| format!("{call:?} {path:?}: {kobako:?} != {oracle:?}"))
|
|
559
582
|
})
|
|
560
583
|
.collect();
|
|
561
584
|
assert!(
|
|
562
585
|
differing.is_empty(),
|
|
563
|
-
"each path call on a host directory through a kobako
|
|
586
|
+
"each path call on a host directory through a kobako Bind Mount must answer as \
|
|
564
587
|
wasmtime-wasi's preopen of it does:\n{}",
|
|
565
588
|
differing.join("\n")
|
|
566
589
|
);
|
|
@@ -604,6 +627,6 @@ fn a_listing_holding_a_name_that_is_not_utf8_answers_as_a_wasmtime_wasi_preopen(
|
|
|
604
627
|
assert_eq!(
|
|
605
628
|
kobako, oracle,
|
|
606
629
|
"fd_readdir on a host directory holding a name that is not UTF-8 through a kobako \
|
|
607
|
-
|
|
630
|
+
Bind Mount must answer as wasmtime-wasi's preopen of it does"
|
|
608
631
|
);
|
|
609
632
|
}
|