kobako 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -1,13 +1,10 @@
1
1
  //! Per-path cache of pre-instantiated wasmtime artifacts.
2
2
  //!
3
- //! The `Linker` wiring (the WASI preview1 import set plus the
4
- //! `__kobako_upcall` host import) and its type-check against the
5
- //! compiled Module are identical for every `Kobako::Runtime` on the
6
- //! same Guest Binary — both host closures read all their state from
7
- //! the `Invocation` inside the calling Store, never from the Runtime.
8
- //! Caching the resolved `InstancePre` per path — together with the ABI
9
- //! version check it guards (`crate::abi`) — takes both off the
10
- //! `Driver::new` hot path.
3
+ //! The host imports (`crate::imports`) and their type-check against the
4
+ //! compiled Module are identical for every `Kobako::Runtime` on the same
5
+ //! Guest Binary. Caching the resolved `InstancePre` per path — together
6
+ //! with the ABI version check it guards (`crate::abi`) — takes both off
7
+ //! the `Driver::new` hot path.
11
8
  //!
12
9
  //! Concurrency: the same as `crate::cache` — the Mutex covers lookup and
13
10
  //! insert only, and a concurrent miss builds an equivalent `InstancePre`.
@@ -15,19 +12,24 @@
15
12
  use std::collections::HashMap;
16
13
  use std::path::{Path, PathBuf};
17
14
  use std::sync::{Mutex, OnceLock};
15
+ use std::time::Duration;
18
16
 
19
- use wasmtime::{Caller, InstancePre, Linker};
20
- use wasmtime_wasi::p1;
17
+ use wasmtime::InstancePre;
21
18
 
22
- use crate::cache::{cached_module, shared_engine};
19
+ use crate::cache::load_module;
23
20
  use crate::invocation::Invocation;
24
- use crate::{abi, dispatch, trap};
21
+ use crate::{abi, imports, trap};
25
22
  use kobako_runtime::error::SetupError;
26
23
 
27
24
  static INSTANCE_PRE_CACHE: OnceLock<Mutex<HashMap<PathBuf, InstancePre<Invocation>>>> =
28
25
  OnceLock::new();
29
26
 
30
- pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>, SetupError> {
27
+ /// The probe runs only on the first load of a path, so only that load's
28
+ /// `setup_timeout` bounds it; a path already verified is not set up again.
29
+ pub(crate) fn load_instance_pre(
30
+ path: &Path,
31
+ setup_timeout: Option<Duration>,
32
+ ) -> Result<InstancePre<Invocation>, SetupError> {
31
33
  let cache = INSTANCE_PRE_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
32
34
 
33
35
  if let Some(pre) = cache
@@ -39,47 +41,17 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
39
41
  return Ok(pre);
40
42
  }
41
43
 
42
- let module = cached_module(path)?;
43
- let linker = build_linker()?;
44
+ let module = load_module(path)?;
45
+ let linker = imports::linker()?;
44
46
  let pre = linker
45
47
  .instantiate_pre(&module)
46
48
  .map_err(trap::instantiate_err)?;
47
49
  // Verify before the insert, so a rejected artifact is never
48
50
  // remembered as a usable template.
49
- abi::verify(&pre)?;
51
+ abi::verify(&pre, setup_timeout)?;
50
52
  cache
51
53
  .lock()
52
54
  .expect("instance_pre cache mutex poisoned")
53
55
  .insert(path.to_path_buf(), pre.clone());
54
56
  Ok(pre)
55
57
  }
56
-
57
- fn build_linker() -> Result<Linker<Invocation>, SetupError> {
58
- let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
59
-
60
- // Wire the wasmtime-wasi preview1 WASI imports. Routes guest fd 1/2
61
- // to the MemoryOutputPipes set up before each run via
62
- // `Driver::invoke`. The closure pulls a `&mut WasiP1Ctx` out of
63
- // Invocation; the panic semantics live inside `Invocation::wasi_mut`
64
- // so the wiring stays honest about its precondition.
65
- p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| state.wasi_mut())
66
- .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
67
-
68
- // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
69
- // docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
70
- // answers.
71
- linker
72
- .func_wrap(
73
- "env",
74
- "__kobako_upcall",
75
- |mut caller: Caller<'_, Invocation>,
76
- req_ptr: i32,
77
- req_len: i32|
78
- -> wasmtime::Result<i64> {
79
- dispatch::handle(&mut caller, req_ptr, req_len)
80
- },
81
- )
82
- .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
83
-
84
- Ok(linker)
85
- }
@@ -4,9 +4,8 @@
4
4
  //! Owned as the data of each per-invocation `wasmtime::Store`
5
5
  //! and threaded through every host import —
6
6
  //! the `__kobako_upcall` dispatcher reads the bound dispatch handler,
7
- //! while `Driver::invoke` installs the invocation's WASI
8
- //! context + pipes (via `io::install_wasi`) before the guest
9
- //! export call.
7
+ //! and the WASI imports read the invocation's `Wasi` context, which the
8
+ //! Store is created with.
10
9
  //!
11
10
  //! The slot also carries the per-invocation wall-clock deadline and the
12
11
  //! memory cap `MemoryLimiter`, both read from the wasmtime
@@ -16,53 +15,56 @@
16
15
  use std::sync::Arc;
17
16
  use std::time::{Duration, Instant};
18
17
 
19
- use wasmtime_wasi::p1::WasiP1Ctx;
20
- use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
21
-
22
18
  use kobako_runtime::dispatch::DispatchHandler;
19
+ use kobako_runtime::endpoint::{Calls, MemoryCap, ABANDONED};
23
20
 
24
21
  use crate::exports::Exports;
25
22
  use crate::limiter::MemoryLimiter;
23
+ use crate::wasi::Wasi;
26
24
 
27
25
  /// Per-invocation host state, owned by the invocation's own Store.
28
26
  /// Threaded through every host import callback.
29
27
  ///
30
- /// All field access is mediated by methods on this type — the WASI ctx
31
- /// is rebuilt fresh before each invocation via
32
- /// `Invocation::install_wasi`, the dispatch handler is set once via
33
- /// `Invocation::bind_on_dispatch`, and captured stdout/stderr bytes
34
- /// are read after the invocation via `Invocation::stdout_bytes` /
35
- /// `Invocation::stderr_bytes`. The fields are private so the mutation
36
- /// surface stays narrow.
28
+ /// All field access is mediated by methods on this type — the `Wasi`
29
+ /// context arrives with the invocation, the dispatch handler is set once
30
+ /// via `Invocation::bind_on_dispatch`, and the captures are read back
31
+ /// through `Invocation::wasi` after the run. The fields are private so
32
+ /// the mutation surface stays narrow.
37
33
  pub(crate) struct Invocation {
38
- wasi: Option<WasiP1Ctx>,
39
- stdout_pipe: Option<MemoryOutputPipe>,
40
- stderr_pipe: Option<MemoryOutputPipe>,
41
- on_dispatch: Option<Arc<dyn DispatchHandler>>,
34
+ wasi: Wasi,
35
+ calls: Calls<wasmtime::Error>,
42
36
  deadline: Option<Instant>,
43
37
  limiter: MemoryLimiter,
44
38
  wall_entry: Option<Instant>,
45
39
  wall_time: Duration,
46
- reentry_trap: Option<wasmtime::Error>,
47
40
  exports: Option<Exports>,
41
+ hostcall_fuel: usize,
48
42
  }
49
43
 
50
44
  impl Invocation {
51
- pub(crate) fn new(memory_limit: Option<usize>) -> Self {
45
+ pub(crate) fn new(memory_limit: Option<usize>, wasi: Wasi) -> Self {
52
46
  Self {
53
- wasi: None,
54
- stdout_pipe: None,
55
- stderr_pipe: None,
56
- on_dispatch: None,
47
+ wasi,
48
+ calls: Calls::default(),
57
49
  deadline: None,
58
- limiter: MemoryLimiter::new(memory_limit),
50
+ limiter: MemoryLimiter(MemoryCap::new(memory_limit)),
59
51
  wall_entry: None,
60
52
  wall_time: Duration::ZERO,
61
- reentry_trap: None,
62
53
  exports: None,
54
+ hostcall_fuel: 0,
63
55
  }
64
56
  }
65
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
+
66
68
  pub(crate) fn set_exports(&mut self, exports: Exports) {
67
69
  self.exports = Some(exports);
68
70
  }
@@ -73,61 +75,28 @@ impl Invocation {
73
75
  self.exports.clone()
74
76
  }
75
77
 
76
- /// The first trap wins: the upcall import ends the invocation with it,
77
- /// so the trap keeps its own kind.
78
- pub(crate) fn record_reentry_trap(&mut self, trap: wasmtime::Error) {
79
- self.reentry_trap.get_or_insert(trap);
80
- }
81
-
82
- /// Once a callback into the guest has trapped, the guest is past
83
- /// resuming.
84
- pub(crate) fn reentry_trapped(&self) -> bool {
85
- self.reentry_trap.is_some()
86
- }
87
-
88
- pub(crate) fn take_reentry_trap(&mut self) -> Option<wasmtime::Error> {
89
- self.reentry_trap.take()
90
- }
91
-
92
- pub(crate) fn install_wasi(
93
- &mut self,
94
- wasi: WasiP1Ctx,
95
- stdout: MemoryOutputPipe,
96
- stderr: MemoryOutputPipe,
97
- ) {
98
- self.wasi = Some(wasi);
99
- self.stdout_pipe = Some(stdout);
100
- self.stderr_pipe = Some(stderr);
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
101
81
  }
102
82
 
103
83
  pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
104
- self.on_dispatch = Some(handler);
105
- }
106
-
107
- pub(crate) fn stdout_bytes(&self) -> Vec<u8> {
108
- self.stdout_pipe
109
- .as_ref()
110
- .map(|p| p.contents().to_vec())
111
- .unwrap_or_default()
84
+ self.calls.bind(handler);
112
85
  }
113
86
 
114
- pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
115
- self.stderr_pipe
116
- .as_ref()
117
- .map(|p| p.contents().to_vec())
118
- .unwrap_or_default()
87
+ /// The trap ending an invocation its host gave up on.
88
+ pub(crate) fn abandonment(&self) -> Option<wasmtime::Error> {
89
+ self.calls
90
+ .abandoned()
91
+ .then(|| wasmtime::Error::msg(ABANDONED))
119
92
  }
120
93
 
121
- /// A clone, so the borrow on the `Caller` is released and the
122
- /// dispatcher can re-borrow it to write the Reply.
123
- pub(crate) fn on_dispatch(&self) -> Option<Arc<dyn DispatchHandler>> {
124
- self.on_dispatch.clone()
94
+ pub(crate) fn wasi(&self) -> &Wasi {
95
+ &self.wasi
125
96
  }
126
97
 
127
- pub(crate) fn wasi_mut(&mut self) -> &mut WasiP1Ctx {
128
- self.wasi
129
- .as_mut()
130
- .expect("WASI context not initialised — the driver must install it before any WASI use")
98
+ pub(crate) fn wasi_mut(&mut self) -> &mut Wasi {
99
+ &mut self.wasi
131
100
  }
132
101
 
133
102
  pub(crate) fn set_deadline(&mut self, deadline: Option<Instant>) {
@@ -146,13 +115,13 @@ impl Invocation {
146
115
  /// image's initial allocation and the high-water mark of earlier
147
116
  /// invocations do not consume the budget.
148
117
  pub(crate) fn arm_memory_cap(&mut self, baseline: usize) {
149
- self.limiter.activate(baseline);
118
+ self.limiter.0.activate(baseline);
150
119
  }
151
120
 
152
121
  /// Disarmed once the export returns, so post-run host bookkeeping such
153
122
  /// as reading the answer back is not charged to the script.
154
123
  pub(crate) fn disarm_memory_cap(&mut self) {
155
- self.limiter.deactivate();
124
+ self.limiter.0.deactivate();
156
125
  }
157
126
 
158
127
  /// Stamped right before the guest export call, so the bracket matches
@@ -172,6 +141,6 @@ impl Invocation {
172
141
  }
173
142
 
174
143
  pub(crate) fn memory_peak(&self) -> usize {
175
- self.limiter.peak()
144
+ self.limiter.0.peak()
176
145
  }
177
146
  }
@@ -13,17 +13,18 @@
13
13
  mod abi;
14
14
  mod ambient;
15
15
  mod cache;
16
- mod capture;
17
16
  mod config;
18
- mod dispatch;
17
+ mod descriptors;
19
18
  mod driver;
20
19
  mod exports;
21
- mod guest_mem;
20
+ mod guest;
21
+ mod imports;
22
22
  mod instance_pre;
23
23
  mod invocation;
24
- mod io;
25
24
  mod limiter;
25
+ mod preview1;
26
26
  mod trap;
27
+ mod wasi;
27
28
 
28
29
  pub use config::Config;
29
30
  pub use driver::Driver;
@@ -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
- }