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.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /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
- on_dispatch: Option<Arc<dyn DispatchHandler>>,
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
- open_calls: u32,
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
- on_dispatch: None,
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
- open_calls: 0,
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 first trap wins: the upcall import ends the invocation with it,
70
- /// so the trap keeps its own kind.
71
- pub(crate) fn record_reentry_trap(&mut self, trap: wasmtime::Error) {
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.on_dispatch = Some(handler);
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.on_dispatch
109
- .as_ref()
110
- .is_some_and(|handler| handler.abandoned())
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 guest_mem;
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
- //! 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.
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
- /// 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
- }
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
- 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
- }
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 `Directory` Mount and through
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
- //! A Filesystem reports no timestamps or inode numbers, so of what a
8
- //! stat writes only the filetype is compared; the errno and the
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::Access;
22
- use kobako_runtime::filesystem::Directory;
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::shared_engine;
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 `Directory` Mount at `AT`.
375
+ /// `host` behind a kobako `Bind` Mount at `AT`.
382
376
  fn kobako_preopen(host: &HostDir) -> (Store<Invocation>, Linker<Invocation>) {
383
- let engine = shared_engine().expect("shared engine must be constructible");
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 = Directory::new(&host.0).expect("the host directory must open");
388
+ let directory = Bind::new(&host.0).expect("the host directory must open");
394
389
  let grant = Grant::Mount(
395
- Mount::new(AT, Arc::new(directory), Access::ReadWrite).expect("AT is a guest path"),
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 = Wasi::for_invocation(&config, &[], &[grant]).expect("WASI context must build");
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 = shared_engine().expect("shared engine must be constructible");
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. An
462
- /// open asking not to block is `NOTSUP`, where wasmtime-wasi records the
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 `..` or
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 expected(call: Call, path: &str, mut oracle: Observation) -> Observation {
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) && !refused_first {
494
- return refused(ERRNO_LOOP);
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 path.len() >= PATH_MAX {
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 = expected(call, path, through_wasmtime_wasi(call, path));
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 Directory Mount must answer as \
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
- Directory Mount must answer as wasmtime-wasi's preopen of it does"
630
+ Bind Mount must answer as wasmtime-wasi's preopen of it does"
608
631
  );
609
632
  }