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.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. 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
- //! Module layout (one responsibility per file):
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
+ }