kobako 0.28.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -3,21 +3,19 @@
3
3
  //! A request under a tag the host does not serve is answered as
4
4
  //! unsupported, so a guest asking for a later operation can fall back.
5
5
  //!
6
- //! A failure lands on whoever is answerable for it. A trap the guest
7
- //! raises during a callback into it ends the invocation as that trap; any
8
- //! other failure is the guest's or the wire's, so the import answers 0,
9
- //! which the guest receives as a wire failure.
10
- //!
11
- //! Each 0 also writes one `[kobako-dispatch] <reason>` line to stderr,
12
- //! since the 0 itself carries no reason. It is the one place the driver
13
- //! logs, and normal operation never reaches it.
6
+ //! A failure lands on whoever is answerable for it, and always says why.
7
+ //! A trap the guest raises during a callback into it ends the invocation
8
+ //! as that trap. A Call the host could not answer is answered as an
9
+ //! internal failure naming why, and an answer too large for a message as
10
+ //! the Service's own failure. Only an answer that cannot be written into
11
+ //! guest memory at all ends the invocation, as a trap naming why.
14
12
 
15
13
  use wasmtime::Caller;
16
14
 
17
15
  use kobako_runtime::error::Trap;
18
16
  use kobako_runtime::yielder::Yielder;
19
17
  use kobako_transport::abi::pack_ptr_len;
20
- use kobako_transport::envelope::{Unsupported, Upcall, YieldReply};
18
+ use kobako_transport::envelope::{Fault, FaultKind, Reply, Unsupported, Upcall, YieldReply};
21
19
 
22
20
  use crate::guest_mem;
23
21
  use crate::invocation::Invocation;
@@ -27,39 +25,58 @@ use crate::invocation::Invocation;
27
25
  /// stack on the Rust call stack with no shared slot.
28
26
  struct CallerYielder<'a, 'c> {
29
27
  caller: &'a mut Caller<'c, Invocation>,
28
+ frame: u32,
30
29
  }
31
30
 
32
31
  impl Yielder for CallerYielder<'_, '_> {
33
- fn yield_to_block(&mut self, args: &[u8]) -> Result<YieldReply, Trap> {
34
- let bytes = guest_mem::drive_yield(self.caller, args)
32
+ fn frame(&self) -> u32 {
33
+ self.frame
34
+ }
35
+
36
+ fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap> {
37
+ let bytes = guest_mem::drive_yield(self.caller, frame, args)
35
38
  .map_err(|msg| Trap::Other(msg.to_string()))?;
36
39
  YieldReply::decode(&bytes).map_err(|err| Trap::Other(err.to_string()))
37
40
  }
38
41
  }
39
42
 
40
- /// Answer one `__kobako_upcall` call: the packed `(ptr<<32)|len` of the
41
- /// Reply, 0 for a failure the guest or the wire is answerable for, or the
42
- /// trap a callback into the guest raised.
43
+ /// Answer one `__kobako_upcall` call with the packed `(ptr<<32)|len` of
44
+ /// the Reply, or the trap a callback into the guest raised.
43
45
  pub(crate) fn handle(
44
46
  caller: &mut Caller<'_, Invocation>,
45
47
  req_ptr: i32,
46
48
  req_len: i32,
47
49
  ) -> wasmtime::Result<i64> {
48
- let answer = try_handle(caller, req_ptr, req_len);
50
+ let reply = reply_to(caller, req_ptr, req_len)
51
+ .unwrap_or_else(|reason| Reply::Fault(Fault::new(FaultKind::Internal, reason)));
52
+ let written = write_reply(caller, &fit_in_message(reply).encode());
49
53
  if let Some(trap) = caller.data_mut().take_reentry_trap() {
50
54
  return Err(trap);
51
55
  }
52
- Ok(answer.unwrap_or_else(|reason| {
53
- eprintln!("[kobako-dispatch] {reason}");
54
- 0
55
- }))
56
+ if let Some(trap) = caller.data().abandonment() {
57
+ return Err(trap);
58
+ }
59
+ written.map_err(wasmtime::Error::msg)
56
60
  }
57
61
 
58
- fn try_handle(
62
+ /// An answer past the message cap is the Service's to change, as any other
63
+ /// answer the host cannot write is.
64
+ fn fit_in_message(reply: Reply) -> Reply {
65
+ let bytes = reply.encode().len();
66
+ match guest_mem::checked_payload_len(bytes) {
67
+ Ok(_) => reply,
68
+ Err(limit) => Reply::Fault(Fault::new(
69
+ FaultKind::Runtime,
70
+ format!("Sandbox could not write the Service's answer: {limit}"),
71
+ )),
72
+ }
73
+ }
74
+
75
+ fn reply_to(
59
76
  caller: &mut Caller<'_, Invocation>,
60
77
  req_ptr: i32,
61
78
  req_len: i32,
62
- ) -> Result<i64, &'static str> {
79
+ ) -> Result<Reply, &'static str> {
63
80
  let req_bytes = guest_mem::read_request(caller, req_ptr, req_len)?;
64
81
  // The driver decodes the core envelope, here and in `CallerYielder`, so
65
82
  // the frontend never sees a frame; the payload inside it stays bytes
@@ -67,8 +84,9 @@ fn try_handle(
67
84
  let call = match Upcall::decode(&req_bytes) {
68
85
  Ok(Upcall::Call(call)) => call,
69
86
  Ok(_) => {
70
- let refusal = Unsupported::new("the host does not serve this request");
71
- return write_reply(caller, &refusal.encode());
87
+ return Ok(Reply::Unsupported(Unsupported::new(
88
+ "the host does not serve this request",
89
+ )))
72
90
  }
73
91
  Err(_) => {
74
92
  return Err(
@@ -89,15 +107,15 @@ fn try_handle(
89
107
  // handler. The borrow ends with the block, freeing the Caller for
90
108
  // `write_reply`; nested dispatch frames each build their own, so
91
109
  // the LIFO re-entry lives on the Rust stack — no shared slot.
110
+ let frame = caller.data_mut().open_call();
92
111
  let reply = {
93
- let mut yielder = CallerYielder { caller };
112
+ let mut yielder = CallerYielder { caller, frame };
94
113
  handler.dispatch(call, &mut yielder)
95
- }
96
- .ok_or(
114
+ };
115
+ caller.data_mut().close_call();
116
+ reply.ok_or(
97
117
  "a Sandbox callback raised an exception instead of returning a fault — please report this as a kobako bug",
98
- )?;
99
-
100
- write_reply(caller, &reply.encode())
118
+ )
101
119
  }
102
120
 
103
121
  fn write_reply(caller: &mut Caller<'_, Invocation>, bytes: &[u8]) -> Result<i64, &'static str> {
@@ -9,7 +9,6 @@
9
9
 
10
10
  use std::path::Path;
11
11
  use std::sync::Arc;
12
- use std::time::Instant;
13
12
 
14
13
  use wasmtime::{
15
14
  AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
@@ -19,9 +18,11 @@ use crate::cache::shared_engine;
19
18
  use crate::config::Config;
20
19
  use crate::exports::{self, Exports};
21
20
  use crate::invocation::Invocation;
21
+ use crate::wasi::Wasi;
22
22
  use crate::{capture, guest_mem, instance_pre, io, trap};
23
23
  use kobako_runtime::dispatch::DispatchHandler;
24
24
  use kobako_runtime::error::{InvokeError, SetupError, Trap};
25
+ use kobako_runtime::grant::{Grant, GrantKind};
25
26
  use kobako_runtime::profile::Profile;
26
27
  use kobako_runtime::runtime::{Entry, Runtime as ContractRuntime};
27
28
  use kobako_runtime::snapshot::{Capture, Completion, Snapshot, Usage};
@@ -45,15 +46,22 @@ impl Driver {
45
46
  /// neither leaves the driver.
46
47
  pub fn new(path: &Path, config: Config) -> Result<Self, SetupError> {
47
48
  Ok(Self {
48
- instance_pre: instance_pre::cached_instance_pre(path)?,
49
+ instance_pre: instance_pre::cached_instance_pre(path, config.setup_timeout)?,
49
50
  config,
50
51
  })
51
52
  }
52
53
 
53
- fn new_store(&self) -> Result<WtStore<Invocation>, SetupError> {
54
- let mut store = WtStore::new(shared_engine()?, Invocation::new(self.config.memory_limit));
54
+ /// The deadline and the `wall_time` bracket open with the Store, so
55
+ /// instantiating the guest and writing its request count against the
56
+ /// `timeout` as the export call does.
57
+ fn new_store(&self, wasi: Wasi) -> Result<WtStore<Invocation>, SetupError> {
58
+ let mut store = WtStore::new(
59
+ shared_engine()?,
60
+ Invocation::new(self.config.memory_limit, wasi),
61
+ );
55
62
  store.limiter(|state: &mut Invocation| -> &mut dyn ResourceLimiter { state.limiter_mut() });
56
- store.epoch_deadline_callback(trap::epoch_deadline_callback);
63
+ trap::arm_deadline(&mut store, self.config.timeout);
64
+ store.data_mut().start_wall_clock();
57
65
  Ok(store)
58
66
  }
59
67
 
@@ -63,7 +71,11 @@ impl Driver {
63
71
  let instance = self
64
72
  .instance_pre
65
73
  .instantiate(store.as_context_mut())
66
- .map_err(|e| Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}")))?;
74
+ .map_err(|e| {
75
+ trap::cap_trap(&e).unwrap_or_else(|| {
76
+ Trap::Other(format!("failed to instantiate the Sandbox runtime: {e}"))
77
+ })
78
+ })?;
67
79
  let exports = Exports::resolve(&instance, store.as_context_mut());
68
80
  store.data_mut().set_exports(exports.clone());
69
81
  Ok(exports)
@@ -83,58 +95,41 @@ impl Driver {
83
95
  Params: wasmtime::WasmParams,
84
96
  Results: wasmtime::WasmResults,
85
97
  {
86
- self.prime_caps(store, exports);
98
+ self.arm_memory_cap(store, exports);
87
99
  let result = export.call(store.as_context_mut(), params);
88
100
  disarm_caps(store);
89
101
  result
90
102
  }
91
103
 
92
104
  /// The pre-initialized image's allocation is folded into the memory
93
- /// baseline rather than the budget.
94
- fn prime_caps(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
95
- match self.config.timeout {
96
- Some(timeout) => {
97
- let deadline = Instant::now() + timeout;
98
- store.data_mut().set_deadline(Some(deadline));
99
- store.set_epoch_deadline(1);
100
- }
101
- None => {
102
- store.data_mut().set_deadline(None);
103
- store.set_epoch_deadline(trap::NO_TIMEOUT_EPOCH_DELTA);
104
- }
105
- }
105
+ /// baseline rather than the budget, so the cap arms only once the
106
+ /// image is in place.
107
+ fn arm_memory_cap(&self, store: &mut WtStore<Invocation>, exports: &Exports) {
106
108
  let baseline = match exports.memory {
107
109
  Some(m) => m.data_size(store.as_context_mut()),
108
110
  None => 0,
109
111
  };
110
112
  store.data_mut().arm_memory_cap(baseline);
111
- store.data_mut().start_wall_clock();
112
113
  }
113
114
 
114
115
  /// Built the same for every `completion`: captures and usage must
115
116
  /// survive a trap just as they do an outcome.
116
117
  fn build_snapshot(&self, store: &WtStore<Invocation>, completion: Completion) -> Snapshot {
117
118
  let data = store.data();
118
- let usage = Usage {
119
- wall_time: data.wall_time().as_secs_f64(),
120
- memory_peak: data.memory_peak(),
119
+ let mut usage = Usage::default();
120
+ usage.wall_time = data.wall_time().as_secs_f64();
121
+ usage.memory_peak = data.memory_peak();
122
+ let clip = |bytes, limit| {
123
+ let mut capture = Capture::default();
124
+ (capture.bytes, capture.truncated) = capture::clip_capture(bytes, limit);
125
+ capture
121
126
  };
122
- let (stdout_bytes, stdout_truncated) =
123
- capture::clip_capture(data.stdout_bytes(), self.config.stdout_limit);
124
- let (stderr_bytes, stderr_truncated) =
125
- capture::clip_capture(data.stderr_bytes(), self.config.stderr_limit);
126
- Snapshot {
127
+ Snapshot::new(
127
128
  completion,
128
- stdout: Capture {
129
- bytes: stdout_bytes,
130
- truncated: stdout_truncated,
131
- },
132
- stderr: Capture {
133
- bytes: stderr_bytes,
134
- truncated: stderr_truncated,
135
- },
129
+ clip(data.wasi().stdout_bytes(), self.config.stdout_limit),
130
+ clip(data.wasi().stderr_bytes(), self.config.stderr_limit),
136
131
  usage,
137
- }
132
+ )
138
133
  }
139
134
  }
140
135
 
@@ -148,12 +143,13 @@ impl ContractRuntime for Driver {
148
143
  entry: Entry<'_>,
149
144
  preamble: &Preamble,
150
145
  handler: Option<Arc<dyn DispatchHandler>>,
146
+ grants: &[Grant],
151
147
  ) -> Result<Snapshot, InvokeError> {
152
- let mut store = self.new_store()?;
148
+ let wasi = Wasi::for_invocation(&self.config, &preamble.encode(), grants)?;
149
+ let mut store = self.new_store(wasi)?;
153
150
  if let Some(handler) = handler {
154
151
  store.data_mut().bind_on_dispatch(handler);
155
152
  }
156
- io::install_wasi(&mut store, &self.config, &preamble.encode())?;
157
153
  let exports = self.instantiate(&mut store)?;
158
154
  let downcall = exports::require(exports.downcall.as_ref())?;
159
155
  let request = match entry {
@@ -174,24 +170,30 @@ impl ContractRuntime for Driver {
174
170
  Ok(outcome) => Completion::Outcome(outcome),
175
171
  Err(t) => Completion::Trap(t),
176
172
  },
177
- Err(e) => Completion::Trap(trap::trap_from(e)),
173
+ Err(e) => Completion::Trap(trap::to_trap(e)),
178
174
  };
179
175
  Ok(self.build_snapshot(&store, completion))
180
176
  }
181
177
 
182
178
  /// Exactly the rung `Config` requested. `Hermetic` freezes ambient time
183
- /// and entropy; both rungs wire no filesystem, environment, or network,
179
+ /// and entropy; both rungs wire no ambient filesystem, environment, or network,
184
180
  /// and the linker adds only `__kobako_upcall` beyond that WASI surface.
185
181
  fn profile(&self) -> Profile {
186
182
  self.config.profile
187
183
  }
188
184
 
185
+ /// A Mount becomes a preopen of kobako's own descriptor table, which
186
+ /// answers every preview1 call beneath it.
187
+ fn supports(&self, kind: GrantKind) -> bool {
188
+ matches!(kind, GrantKind::Mount)
189
+ }
190
+
189
191
  /// Bounded by the same caps as an invocation, on an instance of its
190
192
  /// own that no script ever runs on, so what it answers carries
191
193
  /// nothing a script could have written.
192
194
  fn compile(&self, preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
193
- let mut store = self.new_store().ok()?;
194
- io::install_wasi(&mut store, &self.config, &preamble.encode()).ok()?;
195
+ let wasi = Wasi::for_compile(&self.config, &preamble.encode()).ok()?;
196
+ let mut store = self.new_store(wasi).ok()?;
195
197
  let exports = self.instantiate(&mut store).ok()?;
196
198
  let downcall = exports::require(exports.downcall.as_ref()).ok()?;
197
199
  let (ptr, len) =
@@ -1,6 +1,7 @@
1
- //! Guest linear-memory I/O for every host↔guest buffer handoff, whether
2
- //! the invocation body drives it through its `Store` or a host import
3
- //! drives it through its `Caller`.
1
+ //! Guest linear-memory I/O for the ABI's buffer handoffs, whether the
2
+ //! invocation body drives one through its `Store` or a dispatch or yield
3
+ //! import drives one through its `Caller`. preview1 reads and writes its
4
+ //! arguments through its own `GuestMemory`.
4
5
  //!
5
6
  //! The two shared steps answer an error kind, and each caller words it,
6
7
  //! because depth decides what the same problem means: before the export
@@ -92,7 +93,7 @@ fn callback_exports(caller: &Caller<'_, Invocation>) -> Result<Exports, &'static
92
93
 
93
94
  /// Keep a trap the guest raised during a callback into it for the dispatch
94
95
  /// import to end the invocation with, and answer the callback's own reason.
95
- fn trapped(
96
+ fn keep_trap(
96
97
  caller: &mut Caller<'_, Invocation>,
97
98
  trap: wasmtime::Error,
98
99
  reason: &'static str,
@@ -136,7 +137,7 @@ pub(crate) fn write_from_callback(
136
137
  alloc_and_write(&mut *caller, &exports, bytes).map_err(|err| match err {
137
138
  WriteError::TooLarge => "payload exceeds the 16 MiB limit",
138
139
  WriteError::Incompatible(message) => message,
139
- WriteError::Trapped(trap) => trapped(
140
+ WriteError::Trapped(trap) => keep_trap(
140
141
  caller,
141
142
  trap,
142
143
  "the Sandbox trapped while allocating memory for the request",
@@ -177,9 +178,14 @@ pub(crate) fn unpack_guest_buffer(packed: u64) -> (usize, usize) {
177
178
 
178
179
  pub(crate) fn drive_yield(
179
180
  caller: &mut Caller<'_, Invocation>,
181
+ frame: u32,
180
182
  args: &[u8],
181
183
  ) -> Result<Vec<u8>, &'static str> {
182
- let request = Downcall::Yield(args.to_vec()).encode();
184
+ let request = Downcall::Yield {
185
+ frame,
186
+ args: args.to_vec(),
187
+ }
188
+ .encode();
183
189
  let len_i32 = checked_payload_len(request.len())?;
184
190
  let req_ptr = write_from_callback(caller, &request)? as i32;
185
191
 
@@ -188,7 +194,7 @@ pub(crate) fn drive_yield(
188
194
  let memory = exports.memory.ok_or(SANDBOX_RUNTIME_NOT_KOBAKO)?;
189
195
  let packed = yield_fn
190
196
  .call(&mut *caller, (req_ptr, len_i32))
191
- .map_err(|trap| trapped(caller, trap, "the Sandbox trapped while invoking a block"))?;
197
+ .map_err(|trap| keep_trap(caller, trap, "the Sandbox trapped while invoking a block"))?;
192
198
  let (reply_ptr, reply_len) = unpack_guest_buffer(packed);
193
199
  if reply_len == 0 {
194
200
  return Err("the Sandbox returned an empty block result");
@@ -0,0 +1,46 @@
1
+ //! The host imports a Guest Binary resolves: the WASI preview1 set and
2
+ //! the wire ABI's `__kobako_upcall`.
3
+ //!
4
+ //! Both closures read all their state from the `Invocation` inside the
5
+ //! calling Store, never from a Runtime, so the wiring is the same for
6
+ //! every Sandbox and `crate::instance_pre` can cache what it resolves.
7
+
8
+ use wasmtime::{Caller, Linker};
9
+ use wasmtime_wasi::p1;
10
+
11
+ use crate::cache::shared_engine;
12
+ use crate::invocation::Invocation;
13
+ use crate::{dispatch, preview1};
14
+ use kobako_runtime::error::SetupError;
15
+
16
+ pub(crate) fn linker() -> Result<Linker<Invocation>, SetupError> {
17
+ let mut linker: Linker<Invocation> = Linker::new(shared_engine()?);
18
+
19
+ // Wire the wasmtime-wasi preview1 WASI imports against each
20
+ // invocation's own `Wasi` context, then answer every call that reads
21
+ // a descriptor from kobako's own table, which routes guest fd 1/2 to
22
+ // its capture pipes.
23
+ p1::add_to_linker_sync(&mut linker, |state: &mut Invocation| {
24
+ state.wasi_mut().ctx_mut()
25
+ })
26
+ .and_then(|_| preview1::shadow(&mut linker))
27
+ .map_err(|e| SetupError::Dead(format!("failed to set up the WASI runtime: {e}")))?;
28
+
29
+ // `__kobako_upcall`, `(ptr: i32, len: i32) -> i64` per
30
+ // docs/wire/abi.md § ABI Signatures; `dispatch::handle` owns what it
31
+ // answers.
32
+ linker
33
+ .func_wrap(
34
+ "env",
35
+ "__kobako_upcall",
36
+ |mut caller: Caller<'_, Invocation>,
37
+ req_ptr: i32,
38
+ req_len: i32|
39
+ -> wasmtime::Result<i64> {
40
+ dispatch::handle(&mut caller, req_ptr, req_len)
41
+ },
42
+ )
43
+ .map_err(|e| SetupError::Dead(format!("failed to set up the host callback bridge: {e}")))?;
44
+
45
+ Ok(linker)
46
+ }
@@ -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::cached_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 cached_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
@@ -40,46 +42,16 @@ pub(crate) fn cached_instance_pre(path: &Path) -> Result<InstancePre<Invocation>
40
42
  }
41
43
 
42
44
  let module = cached_module(path)?;
43
- let linker = build_linker()?;
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,28 +15,22 @@
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;
23
19
 
24
20
  use crate::exports::Exports;
25
21
  use crate::limiter::MemoryLimiter;
22
+ use crate::wasi::Wasi;
26
23
 
27
24
  /// Per-invocation host state, owned by the invocation's own Store.
28
25
  /// Threaded through every host import callback.
29
26
  ///
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.
27
+ /// All field access is mediated by methods on this type — the `Wasi`
28
+ /// context arrives with the invocation, the dispatch handler is set once
29
+ /// via `Invocation::bind_on_dispatch`, and the captures are read back
30
+ /// through `Invocation::wasi` after the run. The fields are private so
31
+ /// the mutation surface stays narrow.
37
32
  pub(crate) struct Invocation {
38
- wasi: Option<WasiP1Ctx>,
39
- stdout_pipe: Option<MemoryOutputPipe>,
40
- stderr_pipe: Option<MemoryOutputPipe>,
33
+ wasi: Wasi,
41
34
  on_dispatch: Option<Arc<dyn DispatchHandler>>,
42
35
  deadline: Option<Instant>,
43
36
  limiter: MemoryLimiter,
@@ -45,14 +38,13 @@ pub(crate) struct Invocation {
45
38
  wall_time: Duration,
46
39
  reentry_trap: Option<wasmtime::Error>,
47
40
  exports: Option<Exports>,
41
+ open_calls: u32,
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,
47
+ wasi,
56
48
  on_dispatch: None,
57
49
  deadline: None,
58
50
  limiter: MemoryLimiter::new(memory_limit),
@@ -60,6 +52,7 @@ impl Invocation {
60
52
  wall_time: Duration::ZERO,
61
53
  reentry_trap: None,
62
54
  exports: None,
55
+ open_calls: 0,
63
56
  }
64
57
  }
65
58
 
@@ -89,45 +82,41 @@ impl Invocation {
89
82
  self.reentry_trap.take()
90
83
  }
91
84
 
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);
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;
101
94
  }
102
95
 
103
96
  pub(crate) fn bind_on_dispatch(&mut self, handler: Arc<dyn DispatchHandler>) {
104
97
  self.on_dispatch = Some(handler);
105
98
  }
106
99
 
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()
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()
112
104
  }
113
105
 
114
- pub(crate) fn stderr_bytes(&self) -> Vec<u8> {
115
- self.stderr_pipe
106
+ /// The trap ending an invocation its host gave up on.
107
+ pub(crate) fn abandonment(&self) -> Option<wasmtime::Error> {
108
+ self.on_dispatch
116
109
  .as_ref()
117
- .map(|p| p.contents().to_vec())
118
- .unwrap_or_default()
110
+ .is_some_and(|handler| handler.abandoned())
111
+ .then(|| wasmtime::Error::msg("the host abandoned the invocation"))
119
112
  }
120
113
 
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()
114
+ pub(crate) fn wasi(&self) -> &Wasi {
115
+ &self.wasi
125
116
  }
126
117
 
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")
118
+ pub(crate) fn wasi_mut(&mut self) -> &mut Wasi {
119
+ &mut self.wasi
131
120
  }
132
121
 
133
122
  pub(crate) fn set_deadline(&mut self, deadline: Option<Instant>) {