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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +170 -0
- data/Cargo.lock +111 -82
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +11 -1
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +32 -14
- data/crates/kobako-transport/src/envelope/request.rs +18 -8
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/Cargo.toml +21 -16
- data/crates/kobako-wasmtime/README.md +5 -3
- data/crates/kobako-wasmtime/src/abi.rs +66 -32
- data/crates/kobako-wasmtime/src/ambient.rs +63 -4
- data/crates/kobako-wasmtime/src/cache.rs +185 -74
- data/crates/kobako-wasmtime/src/config.rs +58 -1
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
- data/crates/kobako-wasmtime/src/driver.rs +46 -44
- data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
- data/crates/kobako-wasmtime/src/invocation.rs +35 -46
- data/crates/kobako-wasmtime/src/io.rs +5 -202
- data/crates/kobako-wasmtime/src/lib.rs +4 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +39 -19
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
- data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +7 -1
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +120 -199
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +60 -7
- data/sig/kobako/snippet/binary.rbs +0 -12
data/README.md
CHANGED
|
@@ -45,7 +45,7 @@ gem install kobako
|
|
|
45
45
|
|
|
46
46
|
## Quick Start
|
|
47
47
|
|
|
48
|
-
The same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and
|
|
48
|
+
The same untrusted script runs identically from either host frontend. It executes inside the Wasm guest and reaches no file you have not mounted for it, no socket, and none of your environment.
|
|
49
49
|
|
|
50
50
|
```ruby
|
|
51
51
|
require "kobako"
|
|
@@ -129,12 +129,13 @@ These terms name the concepts the rest of this README builds on.
|
|
|
129
129
|
| Invocation | — | One run of guest code, by `#eval` or `#run`. |
|
|
130
130
|
| Execution | `Kobako::Execution` | The frozen record an Invocation leaves. |
|
|
131
131
|
| Service | — | A host object the guest reaches by name, such as `MyService::KV`. |
|
|
132
|
+
| Mount | — | A `Kobako::Filesystem` placed at a guest path by `Sandbox#mount`, for the guest's `File`. |
|
|
132
133
|
| Context | `Kobako::Context` | The per-Invocation object the optional block receives. |
|
|
133
134
|
| Snippet | — | Guest source or bytecode replayed at the start of every Invocation. |
|
|
134
135
|
| Handle | `Kobako::Handle` | An opaque reference to a host object the wire cannot carry by value. |
|
|
135
136
|
| Block | — | Guest code passed alongside a Service call. |
|
|
136
137
|
|
|
137
|
-
A Sandbox keeps nothing from one Invocation to the next. An Execution holds what the run produced, wrote, and consumed: `#value`, output captures, and `#usage`. A failed run raises, carrying its Execution on the error's `#execution`. A Service is the guest's only route to a host resource. A Context's `ctx.bind` supplies a Service object for that one run. A Handle names its object only within the Invocation that issued it. A Block stays in the guest; each `yield` to it is a synchronous round-trip.
|
|
138
|
+
A Sandbox keeps nothing from one Invocation to the next. An Execution holds what the run produced, wrote, and consumed: `#value`, output captures, and `#usage`. A failed run raises, carrying its Execution on the error's `#execution`. A Service or a Mount is the guest's only route to a host resource. A Context's `ctx.bind` supplies a Service object for that one run. A Handle names its object only within the Invocation that issued it. A Block stays in the guest; each `yield` to it is a synchronous round-trip.
|
|
138
139
|
|
|
139
140
|
## Usage
|
|
140
141
|
|
|
@@ -143,6 +144,7 @@ Each section below stands on its own; this is the order they build in.
|
|
|
143
144
|
| If you want to | Read |
|
|
144
145
|
|---|---|
|
|
145
146
|
| let guest code call into your host | [Services](#services), [Per-Invocation Bindings](#per-invocation-bindings), [Service Blocks](#service-blocks) |
|
|
147
|
+
| let guest code read and write files | [Mounts](#mounts) |
|
|
146
148
|
| see what a run produced or why it failed | [Output Capture](#output-capture), [Error Handling](#error-handling), [Invocation Lifecycle](#invocation-lifecycle) |
|
|
147
149
|
| bound what a run may consume | [Resource Limits](#resource-limits), [Concurrency](#concurrency), [Pooling](#pooling) |
|
|
148
150
|
| hand host objects across the boundary | [Handle Management](#handle-management) |
|
|
@@ -150,7 +152,7 @@ Each section below stands on its own; this is the order they build in.
|
|
|
150
152
|
|
|
151
153
|
### Services
|
|
152
154
|
|
|
153
|
-
`bind` any Ruby object as a Service at a constant-path name. The guest reaches it as a `MyService::KV` (or top-level `
|
|
155
|
+
`bind` any Ruby object as a Service at a constant-path name. The guest reaches it as a `MyService::KV` (or top-level `Clock`) proxy. Through the Transport wire, the guest invokes the public methods the object's own class defines.
|
|
154
156
|
|
|
155
157
|
```ruby
|
|
156
158
|
class User
|
|
@@ -170,7 +172,7 @@ sandbox.eval(<<~RUBY)
|
|
|
170
172
|
RUBY
|
|
171
173
|
```
|
|
172
174
|
|
|
173
|
-
Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.
|
|
175
|
+
Each `::`-separated path segment must match `/\A[A-Z]\w{0,65533}\z/`. A path under `Kobako`, which the Guest Binary keeps for itself, raises `ArgumentError`. Symbol kwargs travel transparently to the host method's keyword arguments. The registry seals at the first invocation (see [Invocation Lifecycle](#invocation-lifecycle)); later `#bind` raises `ArgumentError`.
|
|
174
176
|
|
|
175
177
|
### Per-Invocation Bindings
|
|
176
178
|
|
|
@@ -184,6 +186,84 @@ sandbox.eval("Req::Current.user_id") { |ctx| ctx.bind("Req::Current", request) }
|
|
|
184
186
|
|
|
185
187
|
`ctx.bind` also shadows an already-bound path for that one run. The Context is spent once the block returns, so a `ctx` captured out of it raises `ArgumentError`. Choosing the object per invocation lets concurrent Threads invoke a single Sandbox and still keep their identities apart. The [multi-tenant example](examples/multi-tenant/) runs that shape end to end.
|
|
186
188
|
|
|
189
|
+
### Mounts
|
|
190
|
+
|
|
191
|
+
`mount` places a `Kobako::Filesystem` at an absolute guest path, where the guest's `File` reads it. The default access is `:read`, so a write beneath it raises `Errno::EROFS` in the guest.
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
sandbox.mount("/config", Kobako::Filesystem::Memory.new("app.yml" => yaml))
|
|
195
|
+
sandbox.mount("/tmp", Kobako::Filesystem::Temporary.new, access: :read_write)
|
|
196
|
+
sandbox.mount("/data", Kobako::Filesystem::Directory.new("var/data")) { |path, _access| path.end_with?(".csv") }
|
|
197
|
+
|
|
198
|
+
sandbox.eval('File.read("/config/app.yml")')
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
| Kind | Holds |
|
|
202
|
+
|---|---|
|
|
203
|
+
| `Memory.new(files = {}, limit:, inodes:)` | files in memory, shared by every invocation |
|
|
204
|
+
| `Temporary.new(limit:, inodes:)` | files in memory, empty at every invocation |
|
|
205
|
+
| `Directory.new(path)` | a host directory; nothing outside it is reachable |
|
|
206
|
+
| `Overlay.new.layer(fs, access:)` | Layers stacked into one view, the first added on top |
|
|
207
|
+
|
|
208
|
+
A Memory or Temporary is bounded as a tmpfs mount is. Its files hold 64 MiB unless `limit:` gives another size in bytes. It holds one file or directory for each 4 KiB of that limit, and its root, unless `inodes:` gives another count. `nil` lifts either bound, and the guest raises `Errno::ENOSPC` past one.
|
|
209
|
+
|
|
210
|
+
A block decides each open and each change that reaches the Mount in the access's place, asked at most once for each. A falsy answer or a `StandardError` fails that call as `Errno::EACCES`. Mounts seal with the Sandbox like a `bind`, and `ctx.mount` replaces one for a single run.
|
|
211
|
+
|
|
212
|
+
#### Guest File
|
|
213
|
+
|
|
214
|
+
The guest's `File.open` takes a mode String such as `"r"`, `"w+"`, or `"wx"`, or an Integer of `File::Constants` flags, valued as CRuby's on Linux. An exclusive create lets Sandboxes sharing a directory agree on a lock file. The critical flag `disable_file` removes `File` from the guest altogether.
|
|
215
|
+
|
|
216
|
+
Beneath a mount, `File.open` and `IO.new` answer as CRuby does over a Linux directory. Where they differ, they only refuse more:
|
|
217
|
+
|
|
218
|
+
| When | The guest sees |
|
|
219
|
+
|---|---|
|
|
220
|
+
| nothing is mounted | `NotImplementedError` |
|
|
221
|
+
| a block refuses a change | `Errno::EACCES` where a read-only mount says `Errno::EROFS` |
|
|
222
|
+
| a Ruby Filesystem's file without `append` is asked to append | `Errno::ENOTSUP` on the write |
|
|
223
|
+
| an Integer mode holds a flag kobako does not honor, or `WRONLY` beside `RDWR` | `Errno::EINVAL` |
|
|
224
|
+
| a mode names an encoding, such as `"r:utf-8"` | `ArgumentError` |
|
|
225
|
+
| `IO.new` wraps stdout or stderr in a mode other than `"w"` | `ArgumentError` |
|
|
226
|
+
| `disable_file` is set | no `File` constant |
|
|
227
|
+
|
|
228
|
+
#### Overlays
|
|
229
|
+
|
|
230
|
+
`layer` adds a Filesystem beneath the Layers already there and answers a new Overlay. Each Layer is `:read`, the default, or `:read_write` on its own.
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
overlay = Kobako::Filesystem::Overlay.new
|
|
234
|
+
.layer(Kobako::Filesystem::Temporary.new, access: :read_write) # empty every run
|
|
235
|
+
.layer(Kobako::Filesystem::Directory.new("app")) # never written
|
|
236
|
+
sandbox.mount("/app", overlay, access: :read_write)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
A change to a file in a writable Layer stays there. A read-only Layer's file is first copied up to the nearest writable Layer above it, and a new or renamed file lands in the topmost one. A lower writable Layer's file does not move, raising `Errno::EXDEV`, as across devices. A removal hides what a lower Layer keeps with an [OCI whiteout](https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts), `.wh.<name>`, kept in the topmost writable Layer. Names starting with `.wh.`, in any case, are therefore reserved.
|
|
240
|
+
|
|
241
|
+
#### Filesystems in Ruby
|
|
242
|
+
|
|
243
|
+
Any object answering `open` and `stat` mounts as a Filesystem, directly or as a Layer. Its methods follow Ruby's `File`, `Dir`, and `IO`, so a real `::File` can answer as an open file.
|
|
244
|
+
|
|
245
|
+
| Method | Answers |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `open(path, flags)` | an open file, below |
|
|
248
|
+
| `stat(path)` | an object answering `directory?`, `size`, and `ino`, as `File::Stat` does |
|
|
249
|
+
| `children(path)` | entry names, as `Dir.children` gives them |
|
|
250
|
+
| `mkdir` · `rmdir` · `unlink` · `rename(from, to, directory:)` | optional; without one, that change raises `Errno::EROFS` |
|
|
251
|
+
|
|
252
|
+
A `stat` may also answer `dev`; without one, the Filesystem stands for a single device. An `ino` names one directory while the guest holds it open, and names match exactly, as a block judges them. A path is relative to the mount, the empty path being its root, and `flags` are `File::Constants`. A `rename` told `directory: false` moves only a file and `true` only a directory; `nil` moves either. An `Errno` the guest has a class for reaches the script as itself, and any other `StandardError` as `Errno::EIO`. Every call re-enters Ruby, and its time counts toward `timeout:` without being interrupted.
|
|
253
|
+
|
|
254
|
+
##### Open Files
|
|
255
|
+
|
|
256
|
+
The file `open` answers takes the guest's reads and writes, each method named as `IO` names it.
|
|
257
|
+
|
|
258
|
+
| Method | Answers | Without it |
|
|
259
|
+
|---|---|---|
|
|
260
|
+
| `pread(length, offset)` · `size` | bytes, `nil` or `EOFError` at the end; the length | required |
|
|
261
|
+
| `pwrite(data, offset)` · `truncate(length)` | bytes written; nothing | that change raises `Errno::EROFS` |
|
|
262
|
+
| `append(data)` | where the file then ends, as `IO#pos` does | an append raises `Errno::ENOTSUP` |
|
|
263
|
+
| `fsync` · `close` | nothing | nothing to flush or release |
|
|
264
|
+
|
|
265
|
+
An `append` writes all of `data` at the end in one step, so another writer's append never lands over it.
|
|
266
|
+
|
|
187
267
|
### Output Capture
|
|
188
268
|
|
|
189
269
|
Guest writes through `puts` / `print` / `p` / `$stdout` / `$stderr` are buffered per-channel and read off the run's Execution, independently of its `#value`. Each invocation captures its own; overflow is clipped at the cap and flagged by `#stdout_truncated?` / `#stderr_truncated?`.
|
|
@@ -225,7 +305,7 @@ Each of these carries the failed run's Execution on `#execution`. A rescue reads
|
|
|
225
305
|
| `Kobako::TimeoutError` | `TrapError` | Per-invocation `timeout` exhausted |
|
|
226
306
|
| `Kobako::MemoryLimitError` | `TrapError` | Per-invocation `memory_limit` exhausted |
|
|
227
307
|
| `Kobako::NoServiceError` | `ServiceError` | Nothing bound there, a dead Handle, or a method the guest may not call |
|
|
228
|
-
| `Kobako::ServiceArgumentError` | `ServiceError` | The
|
|
308
|
+
| `Kobako::ServiceArgumentError` | `ServiceError` | The call would not bind, or the Service raised `ArgumentError` |
|
|
229
309
|
| `Kobako::HandleExhaustedError` | `SandboxError` | Handle counter reached its 2³¹ − 1 cap |
|
|
230
310
|
| `Kobako::BytecodeError` | `SandboxError` | `#preload(binary:)` failed RITE validation at replay |
|
|
231
311
|
| `Kobako::UnsupportedFlagError` | `SandboxError` | The Guest Binary does not recognise a critical `#flag`; no script ran |
|
|
@@ -233,6 +313,15 @@ Each of these carries the failed run's Execution on `#execution`. A rescue reads
|
|
|
233
313
|
|
|
234
314
|
`SandboxError` and `ServiceError` carry structured `origin` / `klass` / `backtrace_lines` fields when the guest produced a panic envelope.
|
|
235
315
|
|
|
316
|
+
#### Host Exceptions
|
|
317
|
+
|
|
318
|
+
A Service, a mount's block, or a Filesystem written in Ruby is your code running inside the run. What it raises splits by class.
|
|
319
|
+
|
|
320
|
+
| Raised by your code | Result |
|
|
321
|
+
|-----------------------------------------|-----------------------------------------------|
|
|
322
|
+
| a `StandardError` | the guest sees a failure it may rescue |
|
|
323
|
+
| `Interrupt`, `exit`, `throw`, `Timeout` | the run ends; the invocation raises it as is |
|
|
324
|
+
|
|
236
325
|
### Resource Limits
|
|
237
326
|
|
|
238
327
|
Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memory `memory_limit`; exhaustion raises a `TrapError` subclass. Pass `nil` to `timeout` / `memory_limit` to disable that cap. Read [`Execution#usage`](lib/kobako/execution.rb) for actual consumption — populated on every outcome, so a rescued trap reports it just as a completed run does.
|
|
@@ -240,22 +329,36 @@ Each invocation enforces a wall-clock `timeout` and a per-invocation linear-memo
|
|
|
240
329
|
```ruby
|
|
241
330
|
sandbox = Kobako::Sandbox.new(
|
|
242
331
|
timeout: 5.0, # seconds, default 60.0
|
|
243
|
-
|
|
332
|
+
setup_timeout: 2.0, # seconds, default 10.0
|
|
333
|
+
memory_limit: 10 * 1024 * 1024, # bytes, default 4 MiB
|
|
244
334
|
stdout_limit: 64 * 1024, # bytes, default 1 MiB
|
|
245
|
-
stderr_limit: 64 * 1024
|
|
335
|
+
stderr_limit: 64 * 1024,
|
|
336
|
+
open_file_limit: 16 # descriptors, default 1024
|
|
246
337
|
)
|
|
247
338
|
```
|
|
248
339
|
|
|
249
|
-
| Cap
|
|
250
|
-
|
|
251
|
-
| `timeout`
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
| `
|
|
340
|
+
| Cap | Raises | Default |
|
|
341
|
+
|-------------------|--------------------------------|---------|
|
|
342
|
+
| `timeout` | `Kobako::TimeoutError` | 60.0 s |
|
|
343
|
+
| `setup_timeout` | `Kobako::SetupError` at `new` | 10.0 s |
|
|
344
|
+
| `memory_limit` | `Kobako::MemoryLimitError` | 4 MiB |
|
|
345
|
+
| `stdout_limit` | output clipped (no raise) | 1 MiB |
|
|
346
|
+
| `stderr_limit` | output clipped (no raise) | 1 MiB |
|
|
347
|
+
| `open_file_limit` | `Errno::EMFILE` in the guest | 1024 |
|
|
255
348
|
|
|
256
|
-
`memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget.
|
|
349
|
+
`setup_timeout` bounds instantiating a Guest Binary and reading its ABI version, not compiling it. It applies the first time a path is loaded; later Sandboxes on that path skip the check. A Pool builds each Sandbox at its first checkout, so its `SetupError` raises from `with`. `memory_limit` covers the per-invocation `memory.grow` delta from the entry baseline, so a Sandbox reused across invocations does not silently accumulate against a global budget. `open_file_limit` counts every descriptor one invocation holds, the three standard ones and each Mount's included, so a guest cannot exhaust the host process's own.
|
|
257
350
|
|
|
258
|
-
|
|
351
|
+
#### Isolation Profile
|
|
352
|
+
|
|
353
|
+
Beyond the caps, `profile:` requests the Sandbox's isolation posture on a two-rung ladder.
|
|
354
|
+
|
|
355
|
+
```
|
|
356
|
+
:permissive < :hermetic (default)
|
|
357
|
+
live clock frozen clock
|
|
358
|
+
live entropy constant entropy
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`:hermetic` denies the guest ambient time and entropy; `:permissive` lets the guest's `wasi:clocks` / `wasi:random` read live host sources, an explicit trade of reproducibility. The guest reaches no ambient filesystem, environment, or network under either. The request is also a floor: construction fails with `Kobako::SetupError` on a runtime that declares a weaker posture than requested. See [`docs/guides/security.md`](docs/guides/security.md) § Isolation Profiles.
|
|
259
362
|
|
|
260
363
|
### Concurrency
|
|
261
364
|
|
|
@@ -358,7 +461,7 @@ A Sandbox holds no state from any run, so Threads sharing one are equally safe.
|
|
|
358
461
|
|
|
359
462
|
### Service Blocks
|
|
360
463
|
|
|
361
|
-
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest
|
|
464
|
+
A Service method can accept a guest-supplied block via `&blk` and `yield` into it. The block body runs inside the Wasm guest. Its `break` / `next` / `return` / exceptions follow normal Ruby semantics: an exit lands where it would through a plain method.
|
|
362
465
|
|
|
363
466
|
```ruby
|
|
364
467
|
sandbox.bind("Seq::Map", ->(items, &blk) { items.map(&blk) })
|
|
@@ -367,13 +470,28 @@ sandbox.eval('Seq::Map.call([1, 2, 3]) { |x| x * 2 }').value
|
|
|
367
470
|
# => [2, 4, 6]
|
|
368
471
|
```
|
|
369
472
|
|
|
370
|
-
A `yield` that cannot complete raises at the Service's call site, so the Service may rescue it. Left unrescued, a `BlockError` hands the guest its own exception back; the
|
|
473
|
+
A `yield` that cannot complete raises at the Service's call site, so the Service may rescue it. Left unrescued, a `BlockError` hands the guest its own exception back; the others fail the Service call.
|
|
371
474
|
|
|
372
475
|
| Class | Trigger at `yield` |
|
|
373
476
|
|----------------------------|---------------------------------------|
|
|
374
477
|
| `Kobako::BlockError` | The guest block raised |
|
|
375
478
|
| `Kobako::YieldValueError` | An argument cannot cross to the guest |
|
|
376
479
|
| `Kobako::UnsupportedError` | The Guest Binary serves no block |
|
|
480
|
+
| `ThreadError` | The block runs off the calling Fiber |
|
|
481
|
+
|
|
482
|
+
#### Block Fiber
|
|
483
|
+
|
|
484
|
+
A block runs only on the Fiber that called the Service. The guest's Wasm stack lives on that Fiber's native stack, so entering it from another raises `ThreadError` before the block runs. CRuby itself lets a block run anywhere; kobako narrows that to keep the guest's stack checks sound.
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
Service on the calling Fiber
|
|
488
|
+
├─ blk.call / yield ─> runs in the guest
|
|
489
|
+
├─ Thread.new { blk.call } ─> ThreadError
|
|
490
|
+
└─ Enumerator.new { |y| y << blk.call }
|
|
491
|
+
.next (runs its body on a Fiber) ─> ThreadError
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`Enumerator#next` counts as another Fiber, because it runs the enumerator's body on one. Internal iteration such as `each` or `map` stays on the calling Fiber and runs the block normally.
|
|
377
495
|
|
|
378
496
|
### Handle Management
|
|
379
497
|
|
|
@@ -391,7 +509,7 @@ sandbox.eval('Factory::Make.call("Bob").greet').value # => "hi, Bob" (Handle r
|
|
|
391
509
|
sandbox.eval('Factory::Make.call("Bob")').value # => #<Greeter @name="Bob"> (Handle restoration)
|
|
392
510
|
```
|
|
393
511
|
|
|
394
|
-
A `break` value from a guest block
|
|
512
|
+
A `break` value from a guest block never reaches host code. It stays in the guest while the Service unwinds, so a Handle in it stays a Handle.
|
|
395
513
|
|
|
396
514
|
#### Per-dispatch Handles
|
|
397
515
|
|
|
@@ -461,8 +579,8 @@ Use the source form for snippets authored in your repo; use the bytecode form wh
|
|
|
461
579
|
An Extension teaches the guest a native-style constant by pairing a guest idiom (`source`) with an optional host `backend`. `Sandbox#install` composes the two through the existing `#preload` and `#bind` verbs, adding no wire or Guest Binary surface. Pure operations run in-guest with no round-trip. The rest dispatch to the backend under the same isolation and reflection guarantees as any bound Service ([`docs/guides/extensions.md`](docs/guides/extensions.md)).
|
|
462
580
|
|
|
463
581
|
```ruby
|
|
464
|
-
|
|
465
|
-
class
|
|
582
|
+
VFS = <<~'MRUBY'
|
|
583
|
+
class Vfs
|
|
466
584
|
extend Kobako::Proxy
|
|
467
585
|
def self.basename(path) = path.split("/").last || "" # pure: runs in-guest
|
|
468
586
|
# read / write are undefined here, so they dispatch to the backend
|
|
@@ -471,16 +589,16 @@ MRUBY
|
|
|
471
589
|
|
|
472
590
|
sandbox.install(
|
|
473
591
|
Kobako::Extension.new(
|
|
474
|
-
name: :
|
|
475
|
-
source:
|
|
592
|
+
name: :Vfs,
|
|
593
|
+
source: VFS,
|
|
476
594
|
backend: Kobako::Extension::Backend.new(
|
|
477
|
-
path: "
|
|
478
|
-
provider: -> {
|
|
595
|
+
path: "Vfs",
|
|
596
|
+
provider: -> { CopyOnWriteFiles.new(root) } # invoked once per invocation
|
|
479
597
|
)
|
|
480
598
|
)
|
|
481
599
|
)
|
|
482
600
|
|
|
483
|
-
sandbox.eval('
|
|
601
|
+
sandbox.eval('Vfs.read("sample.txt")') # dispatches to the backend's #read
|
|
484
602
|
```
|
|
485
603
|
|
|
486
604
|
A backend declares the bound object's lifetime by keyword, never by inference — so a static object that happens to be callable stays unambiguous.
|
|
@@ -491,12 +609,12 @@ A backend declares the bound object's lifetime by keyword, never by inference
|
|
|
491
609
|
| `provider:` | A no-argument callable invoked per invocation |
|
|
492
610
|
| neither | A fillable, standing for `Kobako::Unresolved` until `ctx.bind` supplies the run's object |
|
|
493
611
|
|
|
494
|
-
A writable backend takes `provider:`, so its state cannot leak across runs. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked
|
|
612
|
+
A writable backend takes `provider:`, so its state cannot leak across runs. kobako ships no concrete Extension; the idiom and backend are yours. The [overlay VFS example](examples/vfs/) is a worked virtual file that reads through to disk while protecting it from guest writes.
|
|
495
613
|
|
|
496
614
|
## Security
|
|
497
615
|
|
|
498
|
-
kobako isolates the guest, but **what it may reach is whatever you `bind
|
|
499
|
-
exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
|
|
616
|
+
kobako isolates the guest, but **what it may reach is whatever you `bind` or `mount`**.
|
|
617
|
+
`bind` exposes every public method the object's own class defines. Inherited, mixed-in, and built-in
|
|
500
618
|
methods stay out of reach, but the class's own surface does not. Bind a purpose-built object
|
|
501
619
|
scoped to the task, not a capable one whose other methods leak more than you intend.
|
|
502
620
|
|
|
@@ -524,23 +642,23 @@ Order-of-magnitude figures on macOS arm64, Ruby 3.4.7, YJIT off. Absolute values
|
|
|
524
642
|
| Phase | Cost |
|
|
525
643
|
|--------------------------------------------------------------|-----------------------|
|
|
526
644
|
| First `Sandbox.new` ever for a Guest Binary (Module JIT, then disk-cached) | ~500 ms once per machine |
|
|
527
|
-
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~
|
|
645
|
+
| First `Sandbox.new` in a fresh process (`.cwasm` cache warm) | ~1.4 ms one-time |
|
|
528
646
|
| Subsequent `Sandbox.new` (caches warm) | ~3 µs |
|
|
529
|
-
| Warm `#eval` with one Service call on a reused Sandbox | ~
|
|
530
|
-
| Warm `#run(:Entrypoint, ...)` dispatch | ~
|
|
531
|
-
| Service call amortized inside one invocation | ~
|
|
532
|
-
| Snippet replay per invocation | ~7
|
|
647
|
+
| Warm `#eval` with one Service call on a reused Sandbox | ~62 µs |
|
|
648
|
+
| Warm `#run(:Entrypoint, ...)` dispatch | ~57 µs |
|
|
649
|
+
| Service call amortized inside one invocation | ~4.9 µs |
|
|
650
|
+
| Snippet replay per invocation | ~2.7 µs each |
|
|
533
651
|
| Per additional idle Sandbox (RSS) | < 1 KB |
|
|
534
652
|
|
|
535
|
-
The Cranelift JIT runs once per machine and
|
|
653
|
+
The Cranelift JIT runs once per machine and wasmtime release — the compiled artifact persists in a `.cwasm` disk cache, so later processes deserialize in milliseconds. An idle Sandbox holds no wasm instance, which is why a thousand idle tenants cost ~37 MB total. The canonical boot state is baked into the artifact and instantiated per invocation. A +10% regression on any gated benchmark blocks release.
|
|
536
654
|
|
|
537
655
|
#### Throughput Levers
|
|
538
656
|
|
|
539
657
|
| Choice | What it changes |
|
|
540
658
|
|---|---|
|
|
541
|
-
| `gvl: :hold` (default) | ~
|
|
659
|
+
| `gvl: :hold` (default) | ~15–20k `#eval`/s in aggregate, at any Thread count |
|
|
542
660
|
| `gvl: :release` | lifts that ceiling for compute-bound scripts (see [Concurrency](#concurrency)) |
|
|
543
|
-
| `match?` over `=~` | `=~` (~5 µs/match) costs about
|
|
661
|
+
| `match?` over `=~` | `=~` (~5.6 µs/match) costs about 6× `match?` (~0.9 µs) |
|
|
544
662
|
|
|
545
663
|
Under `:hold` the GVL serializes wasm work, though Ruby-side `#eval` setup still overlaps. `=~` eagerly builds the `MatchData` and match globals, so prefer `match?` for boolean tests. Regexp is an opt-in capability gem, excluded from the default binary and the gated set. Its throughput is tracked in a separate non-gated characterization (`#11` in [`benchmark/README.md`](benchmark/README.md)).
|
|
546
664
|
|
data/ROADMAP.md
CHANGED
|
@@ -24,3 +24,8 @@ row below links a feature to its scenarios and to where its code starts.
|
|
|
24
24
|
| ✅ [Regexp](docs/spec/behavior/regexp.md) | [wasm/kobako-regexp/src/regexp.rs](wasm/kobako-regexp/src/regexp.rs) |
|
|
25
25
|
| ✅ [Regexp over String](docs/spec/behavior/regexp-string.md) | [wasm/kobako-regexp/src/string_ext.rs](wasm/kobako-regexp/src/string_ext.rs) |
|
|
26
26
|
| ✅ [MatchData](docs/spec/behavior/regexp-matchdata.md) | [wasm/kobako-regexp/src/matchdata.rs](wasm/kobako-regexp/src/matchdata.rs) |
|
|
27
|
+
| ✅ [Guest File](docs/spec/behavior/file.md) | [wasm/kobako-io/src/file.rs](wasm/kobako-io/src/file.rs) |
|
|
28
|
+
| ✅ [Filesystem](docs/spec/behavior/filesystem.md) | [crates/kobako-runtime/src/filesystem.rs](crates/kobako-runtime/src/filesystem.rs) |
|
|
29
|
+
| ✅ [Mount](docs/spec/behavior/mount.md) | [lib/kobako/catalog/mount.rb](lib/kobako/catalog/mount.rb) |
|
|
30
|
+
| ✅ [Ruby Filesystem](docs/spec/behavior/ruby-filesystem.md) | [ext/kobako/src/runtime/filesystem/ruby.rs](ext/kobako/src/runtime/filesystem/ruby.rs) |
|
|
31
|
+
| ✅ [Journeys](docs/spec/behavior/journeys.md) | [test/e2e/test_journeys.rb](test/e2e/test_journeys.rb) |
|
data/SECURITY.md
CHANGED
|
@@ -24,7 +24,7 @@ ask to stay anonymous.
|
|
|
24
24
|
## Scope
|
|
25
25
|
|
|
26
26
|
In scope is anything that lets guest code cross the isolation boundary it should not:
|
|
27
|
-
reaching host memory, the filesystem, the network, or `ENV`; obtaining ambient time or
|
|
27
|
+
reaching host memory, the filesystem beyond what you mounted, the network, or `ENV`; obtaining ambient time or
|
|
28
28
|
entropy the host froze; reaching a Service you never bound; or a
|
|
29
29
|
memory-safety fault in the host codec or wasmtime driver.
|
|
30
30
|
|
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.18.0...kobako-runtime-v0.19.0) (2026-10-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **runtime:** a struct literal no longer builds OpenOptions, Metadata, DirEntry, Snapshot, Capture, or Usage outside kobako-runtime; use their `new` or `Default` and assign each field.
|
|
9
|
+
* the Guest ABI is version 5, so a Guest Binary built against version 4 is refused at Sandbox construction until rebuilt. The Yielder trait of kobako-runtime gains frame and takes the frame in yield_to_block; the Guest trait of kobako-core and the MrbGuest trait of kobako-mruby take the frame in yield_to_block; Downcall::Yield carries frame and args.
|
|
10
|
+
* **runtime:** the Runtime trait's invoke takes the invocation's Grants as a fourth argument, so every engine implementing it must accept them.
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* check what a rename moves as it moves, refusing a directory beneath a Policy ([23794bf](https://github.com/elct9620/kobako/commit/23794bfcb54e148126e29873fa9b52d9e3de8d8c))
|
|
15
|
+
* **runtime:** bound the files and directories a Memory or Temporary holds ([8d0be0a](https://github.com/elct9620/kobako/commit/8d0be0ab3dc8c11bcb96865b0bf1a0bbb8dd3b16))
|
|
16
|
+
* **runtime:** bound what a Memory or Temporary holds, 64 MiB by default ([2557d1b](https://github.com/elct9620/kobako/commit/2557d1ba7530aacfddf908aeddac561c4fbc5c8a))
|
|
17
|
+
* **runtime:** draw every device a host does not name from one source ([210d55b](https://github.com/elct9620/kobako/commit/210d55b1b4e0699dc5ef56b4b6930be01658a3c6))
|
|
18
|
+
* **runtime:** hand each invocation its Grants and let an engine declare the kinds it supports ([cba90ee](https://github.com/elct9620/kobako/commit/cba90eed09c8afcb23080cef1c9cbb3650648472)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
19
|
+
* **runtime:** keep a directory descriptor's identity its own while open ([6267642](https://github.com/elct9620/kobako/commit/6267642ea831917be76a0101e8d206ccfa8834c1))
|
|
20
|
+
* **runtime:** let a dispatch handler abandon its invocation ([179da9e](https://github.com/elct9620/kobako/commit/179da9ee1f31fed9e6d0c149faee0ad55f8db487))
|
|
21
|
+
* **runtime:** let a Filesystem's file refuse its close ([af16007](https://github.com/elct9620/kobako/commit/af1600797b098796dd6f329672c7d04e2e1cd2fa)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
22
|
+
* **runtime:** let a Mount decide its own access as the one kind of Grant ([5c9189e](https://github.com/elct9620/kobako/commit/5c9189eea8ac61cf165dca1b38e459c4f14acdd0)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
23
|
+
* **runtime:** let a Mount's Policy judge the spelling the host stores ([5281057](https://github.com/elct9620/kobako/commit/52810573f22d2a3edfae9b34b7767a5f440fba15))
|
|
24
|
+
* **runtime:** let the records a Filesystem or an engine builds gain fields ([c0ef88d](https://github.com/elct9620/kobako/commit/c0ef88d0b6df16d651e4c49226e41667307d772f))
|
|
25
|
+
* **runtime:** name a file grown past what a Filesystem keeps ([7bac3e6](https://github.com/elct9620/kobako/commit/7bac3e65534097b85d93f5967b6c73a979869a78))
|
|
26
|
+
* **runtime:** name each code a Filesystem may fail with ([1d980fa](https://github.com/elct9620/kobako/commit/1d980fa4328bad42e8a8665adfec9fc844690e97))
|
|
27
|
+
* **runtime:** name the POSIX error each FilesystemError stands for ([01c301b](https://github.com/elct9620/kobako/commit/01c301b4af9f489594eb7a98ab48c508e5b61ca4))
|
|
28
|
+
* **runtime:** refuse a Memory seed path through `..` ([b31434d](https://github.com/elct9620/kobako/commit/b31434d386ebde838c15755619c67da95311ecf6))
|
|
29
|
+
* **runtime:** refuse a Mount at a path that is no absolute guest path ([a525758](https://github.com/elct9620/kobako/commit/a5257589f9d831b9a3b6d1952df6e5c8469630e1))
|
|
30
|
+
* **runtime:** report a link a lookup does not follow as a link ([8c1e0b3](https://github.com/elct9620/kobako/commit/8c1e0b3e63fbc91b10f11cd91817ce94de24c7e6))
|
|
31
|
+
* **runtime:** report the identity of what a path holds ([392bc0e](https://github.com/elct9620/kobako/commit/392bc0e9f2fb935934a0031bf7d64dec0b27df6a))
|
|
32
|
+
* **runtime:** say whether a path beneath a Mount may pass a link ([7706a25](https://github.com/elct9620/kobako/commit/7706a2537bf8e5975b2b227bef0c05cb48c09f4b))
|
|
33
|
+
* **runtime:** seed a Memory from a list that may not hold, answering why ([8ac2dfd](https://github.com/elct9620/kobako/commit/8ac2dfdf73b84fe2e53405a653d861ddca9ea98c))
|
|
34
|
+
* **runtime:** ship the Memory, Temporary, Directory, and Overlay filesystems ([c60c453](https://github.com/elct9620/kobako/commit/c60c4535e966ffd501e03d7be7fef0a44d860b0b)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
35
|
+
* **runtime:** stack an Overlay from Layers, each read-only or writable ([6c88188](https://github.com/elct9620/kobako/commit/6c88188cd7992c218f8c5a3be30981e685804078)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* append through a Filesystem's file only in one step ([0509ca7](https://github.com/elct9620/kobako/commit/0509ca70d2d5a832303973ba0f7280be0731fd3d))
|
|
41
|
+
* ask a mount's block once for a read ([f89bd16](https://github.com/elct9620/kobako/commit/f89bd16c1a71ed1d385ab9d3ac5974ac3f0a9103))
|
|
42
|
+
* follow no link swapped in after a Policy judges a path ([ed93213](https://github.com/elct9620/kobako/commit/ed9321303dd45e113fc413e99e1240118cc4bbe4))
|
|
43
|
+
* hold an Overlay's lock across each call, waiting without the GVL ([b8e7342](https://github.com/elct9620/kobako/commit/b8e734273fdc577a40be97c43c46d9548d66c625))
|
|
44
|
+
* keep a link within the directory a descriptor opened ([952dd05](https://github.com/elct9620/kobako/commit/952dd0556969683f44798da94f9fc7a17c1f6d55))
|
|
45
|
+
* run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
|
|
46
|
+
* **runtime:** answer a host failure as the error naming it ([effd43a](https://github.com/elct9620/kobako/commit/effd43a85a3ba69aaa068d2c384c3b5cb466161c))
|
|
47
|
+
* **runtime:** append to a Directory file in one host write ([4a5450a](https://github.com/elct9620/kobako/commit/4a5450a67e202e45299175c4fe7528cf50489f0e))
|
|
48
|
+
* **runtime:** copy a file up whole when its Layer writes in parts ([89d87f7](https://github.com/elct9620/kobako/commit/89d87f7225611786467968ec34eba5a8223b5fc5))
|
|
49
|
+
* **runtime:** fail an Overlay call a Layer cannot answer instead of guessing ([5ba0db3](https://github.com/elct9620/kobako/commit/5ba0db33c56ed837401bcb62345f81b4b50c317a))
|
|
50
|
+
* **runtime:** hold each Memory entry under its own name ([8f1ba8a](https://github.com/elct9620/kobako/commit/8f1ba8a50d3d8ca3a695115d51b605be1f18e29e))
|
|
51
|
+
* **runtime:** judge a link an Overlay's Layer holds as the link itself ([959c1f3](https://github.com/elct9620/kobako/commit/959c1f3dab6f6948baabbd914bbb86dfff090f7b))
|
|
52
|
+
* **runtime:** keep an Overlay rename to one Layer and answer a landed change as done ([e3405b7](https://github.com/elct9620/kobako/commit/e3405b7139809ada202ffdb0d6bf1eef3dcbf056))
|
|
53
|
+
* **runtime:** leave a Memory file as it was on a write of no bytes ([9f8fd23](https://github.com/elct9620/kobako/commit/9f8fd237b9b0042590b149f9b2051e0773d8b9af))
|
|
54
|
+
* **runtime:** leave an Overlay unchanged when a removal or rename is refused ([a7bee4b](https://github.com/elct9620/kobako/commit/a7bee4b2a465a0ef0ad2e376c281dca82a0df0e1))
|
|
55
|
+
* **runtime:** let a file or link in a Layer hide what lies below it ([fd579f6](https://github.com/elct9620/kobako/commit/fd579f679cfa8a83ec7d139066bc1a4570af398b))
|
|
56
|
+
* **runtime:** make every Filesystem answer the methods that keep a guarantee ([00f5af6](https://github.com/elct9620/kobako/commit/00f5af63f34ac7722249746d0b0eb566da618803))
|
|
57
|
+
* **runtime:** move a directory made again as the writable Layer's own ([ac3e864](https://github.com/elct9620/kobako/commit/ac3e864bbb0ce5d61def45e0403239be405481d3))
|
|
58
|
+
* **runtime:** move a directory onto one that holds only markers ([934f0c3](https://github.com/elct9620/kobako/commit/934f0c39e0ad1189bdb1cb0461671245dea32f93))
|
|
59
|
+
* **runtime:** open a Directory file the runtime neither reads nor writes ([6b30858](https://github.com/elct9620/kobako/commit/6b308583383d6803894b2c7af1a9ef05d72b0e7d)), closes [#78](https://github.com/elct9620/kobako/issues/78)
|
|
60
|
+
* **runtime:** open a file already there without writing to create it ([7f76a1a](https://github.com/elct9620/kobako/commit/7f76a1af575739ae16c5daeb1df8268af44cea63))
|
|
61
|
+
* **runtime:** panic on files a Memory cannot be seeded with ([cf5c835](https://github.com/elct9620/kobako/commit/cf5c835d7df8bf82972487280c6a6c79be955a68))
|
|
62
|
+
* **runtime:** put an Overlay's markers back when the change fails ([78ddeed](https://github.com/elct9620/kobako/commit/78ddeeddda8b9f797a80ec3796b28e04cf99ffc9))
|
|
63
|
+
* **runtime:** refuse a backslash in a Directory path on Windows ([88b5719](https://github.com/elct9620/kobako/commit/88b57195325b168395248a82b94a5ebf7e5f4142))
|
|
64
|
+
* **runtime:** refuse a Directory append on Windows rather than take two steps ([a528cc9](https://github.com/elct9620/kobako/commit/a528cc9577f1b2e1359699e2400be93c1f3a9be7))
|
|
65
|
+
* **runtime:** refuse a file moved beneath itself as no directory ([bfabb52](https://github.com/elct9620/kobako/commit/bfabb52b27080ee9e5000bd5524ea201a7e33d80))
|
|
66
|
+
* **runtime:** refuse a host name that is not UTF-8 as an illegal sequence ([c4ff030](https://github.com/elct9620/kobako/commit/c4ff030ffa8e29fa1b10df4aaf6af65162c76353))
|
|
67
|
+
* **runtime:** refuse a Memory file grown past what can be allocated ([302e2d4](https://github.com/elct9620/kobako/commit/302e2d4fcd5b31fee4e953e8993bb47d9a2a673a))
|
|
68
|
+
* **runtime:** refuse an Overlay change landing beneath a read-only Layer ([e7b2f19](https://github.com/elct9620/kobako/commit/e7b2f19156df9773f669cbe8a7aaefc17591cca3))
|
|
69
|
+
* **runtime:** refuse to open a FIFO, socket, or device beneath a Directory ([bdcaf78](https://github.com/elct9620/kobako/commit/bdcaf78bf131998648c65ac51ced4a588a79211f))
|
|
70
|
+
* **runtime:** remove no directory as a file on any host ([c9c0820](https://github.com/elct9620/kobako/commit/c9c0820fd39c0744b9d9c367b977f6587d1f9589))
|
|
71
|
+
* **runtime:** rename and copy up through an Overlay as overlayfs does ([ada5a86](https://github.com/elct9620/kobako/commit/ada5a86a8647d89666e9de808eb13ba638882e51))
|
|
72
|
+
* **runtime:** replace neither a FIFO nor a directory with the other through an Overlay ([8fed062](https://github.com/elct9620/kobako/commit/8fed062438e105c6a784bf4bde1582fda9e3c9c2))
|
|
73
|
+
* **runtime:** report a merged directory's identity alike through an Overlay's lstat ([ded3b4a](https://github.com/elct9620/kobako/commit/ded3b4aba78efa678318c38b139b5c9c76b5d3fd))
|
|
74
|
+
* **runtime:** require cap-std 4.0.3 for the trailing-slash lookup fix ([549cfe0](https://github.com/elct9620/kobako/commit/549cfe07fc8b9affdc8a1765a9e4bc5963ef546e))
|
|
75
|
+
* **runtime:** reserve an Overlay's marker name in any case ([ef39923](https://github.com/elct9620/kobako/commit/ef399230eabe01ad6eed632b2753880ae1fc322c))
|
|
76
|
+
* **runtime:** reserve exactly what a Memory file grows by ([96f8141](https://github.com/elct9620/kobako/commit/96f8141442cac2170a8bf8a76055d4f4659b1100))
|
|
77
|
+
* **runtime:** spell a Directory path's last name without following a link there ([c5e5ae7](https://github.com/elct9620/kobako/commit/c5e5ae74ec1f6ee07e24da08d7b097fb6a882961))
|
|
78
|
+
* **runtime:** spell a path on XFS by listing rather than trusting exact names ([f89495d](https://github.com/elct9620/kobako/commit/f89495dcfd7c6d05d46ebc5dbae0039856cc15c6))
|
|
79
|
+
* **wasmtime:** hold the whole invocation to its deadline ([231e325](https://github.com/elct9620/kobako/commit/231e32592699008adf3105050bee8b5528ea27a8))
|
|
80
|
+
* **wasmtime:** place an append at the end as the file stands when it lands ([d7291c4](https://github.com/elct9620/kobako/commit/d7291c4f32fb1f5f2494c69c76796476bbcf4d43))
|
|
81
|
+
* **wasmtime:** refuse a link an open asks not to follow ([123a01b](https://github.com/elct9620/kobako/commit/123a01b01f59cc69c0e3a23f9aeb01aa89132c07))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
### Performance Improvements
|
|
85
|
+
|
|
86
|
+
* **runtime:** list an Overlay directory in time linear to its entries ([be3f446](https://github.com/elct9620/kobako/commit/be3f446f782cbd72b114f7b6072e3b50c391f093))
|
|
87
|
+
* **runtime:** stop an Overlay's marker lookup at a directory a Layer lacks ([494fa09](https://github.com/elct9620/kobako/commit/494fa090e90806c50e4e8baaa4f1001efc388b45))
|
|
88
|
+
|
|
3
89
|
## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.17.0...kobako-runtime-v0.18.0) (2026-10-13)
|
|
4
90
|
|
|
5
91
|
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
#
|
|
3
3
|
# The contract a wasm engine must satisfy to drive a kobako guest,
|
|
4
4
|
# free of any engine or frontend type: the `Runtime` trait plus the
|
|
5
|
-
# neutral value types (`Snapshot`, `Trap`, `SetupError`, ...)
|
|
6
|
-
# dispatch / yield re-entry traits a frontend supplies
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# neutral value types (`Snapshot`, `Trap`, `SetupError`, ...), the
|
|
6
|
+
# dispatch / yield re-entry traits a frontend supplies, and the Grants
|
|
7
|
+
# a frontend hands each invocation, the Mount and the Filesystems it
|
|
8
|
+
# places. Engine implementations (`kobako-wasmtime`) and host frontends
|
|
9
|
+
# (the Ruby ext) meet on this surface, so the engine stays swappable.
|
|
10
|
+
# `cap-std` confines the Directory Filesystem to its host directory.
|
|
9
11
|
#
|
|
10
12
|
# Self-contained by design: the Ruby gem ships this crate without the
|
|
11
13
|
# `crates/` workspace manifest, so no `workspace = true` inheritance
|
|
@@ -13,7 +15,7 @@
|
|
|
13
15
|
|
|
14
16
|
[package]
|
|
15
17
|
name = "kobako-runtime"
|
|
16
|
-
version = "0.
|
|
18
|
+
version = "0.19.0"
|
|
17
19
|
edition = "2021"
|
|
18
20
|
description = "Engine-neutral host runtime contract for embedding kobako Wasm guests."
|
|
19
21
|
license = "Apache-2.0"
|
|
@@ -27,4 +29,23 @@ categories = ["wasm", "virtualization"]
|
|
|
27
29
|
# in-tree builds (and the Ruby gem, which ships both crates) resolving
|
|
28
30
|
# locally.
|
|
29
31
|
[dependencies]
|
|
30
|
-
kobako-transport = { version = "0.
|
|
32
|
+
kobako-transport = { version = "0.19.0", path = "../kobako-transport" }
|
|
33
|
+
# cap-std resolves every path of the Directory Filesystem beneath its host
|
|
34
|
+
# directory, refusing a `..` or a symbolic link that would lead out of it.
|
|
35
|
+
# 4.0.3 is the first release whose trailing-slash lookup keeps that
|
|
36
|
+
# refusal (GHSA-hp8f-xmx4-4qrg).
|
|
37
|
+
cap-std = "4.0.3"
|
|
38
|
+
# cap-fs-ext opens a Directory's path one directory at a time without
|
|
39
|
+
# following a link, so a Mount's Policy judges the path its call reaches.
|
|
40
|
+
cap-fs-ext = "4.0.3"
|
|
41
|
+
|
|
42
|
+
# rustix switches a Directory file to append mode for one write, so the
|
|
43
|
+
# host places it at the file's end in one step; std cannot set file flags.
|
|
44
|
+
[target.'cfg(unix)'.dependencies]
|
|
45
|
+
rustix = { version = "1.1.4", features = ["fs"] }
|
|
46
|
+
|
|
47
|
+
# libc asks macOS for the name a directory stores, which a Mount's Policy
|
|
48
|
+
# judges in place of a spelling the host folds; rustix has no
|
|
49
|
+
# getattrlist.
|
|
50
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
51
|
+
libc = "0.2.191"
|
|
@@ -4,9 +4,9 @@ Engine-neutral host runtime contract for
|
|
|
4
4
|
[kobako](https://github.com/elct9620/kobako), an in-process Wasm
|
|
5
5
|
sandbox for running untrusted mruby scripts.
|
|
6
6
|
|
|
7
|
-
A kobako host drives a Guest Binary through a wasm engine
|
|
8
|
-
is
|
|
9
|
-
|
|
7
|
+
A kobako host drives a Guest Binary through a wasm engine. This crate
|
|
8
|
+
is where the two meet, free of any engine or frontend type, so the
|
|
9
|
+
engine stays swappable.
|
|
10
10
|
|
|
11
11
|
| Module | What it declares |
|
|
12
12
|
|---|---|
|
|
@@ -14,19 +14,24 @@ type, so the engine stays swappable.
|
|
|
14
14
|
| `snapshot` | each invocation's `Completion` (outcome or trap), both output `Capture`s, and `Usage` |
|
|
15
15
|
| `error` | the neutral failure types: `Trap`, `SetupError`, and `InvokeError` over both |
|
|
16
16
|
| `profile` | the isolation `Profile` a runtime declares |
|
|
17
|
+
| `grant` | the `Grant` a frontend hands each invocation, and the `GrantKind`s a runtime supports |
|
|
18
|
+
| `mount` | the `Mount` at a guest path, and the `Policy` judging its paths |
|
|
19
|
+
| `filesystem` | the `Filesystem` trait, its `Access`, and the four kinds kobako ships |
|
|
17
20
|
| `dispatch` / `yielder` | the `DispatchHandler` and `Yielder` traits a frontend supplies for guest→host dispatch and block-yield re-entry |
|
|
18
21
|
|
|
19
|
-
Engine implementations
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
Engine implementations such as `kobako-wasmtime` implement `Runtime`.
|
|
23
|
+
Host frontends such as the kobako Ruby gem's native ext map the neutral
|
|
24
|
+
types onto their own language surface.
|
|
22
25
|
|
|
23
26
|
## Usage
|
|
24
27
|
|
|
28
|
+
Add the crate to an engine or a frontend that speaks the contract.
|
|
29
|
+
|
|
25
30
|
```toml
|
|
26
31
|
[dependencies]
|
|
27
|
-
kobako-runtime = "0.
|
|
32
|
+
kobako-runtime = "0.19.0" # x-release-please-version
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
## License
|
|
31
36
|
|
|
32
|
-
Apache-2.0
|
|
37
|
+
Licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
|
@@ -25,4 +25,12 @@ use kobako_transport::envelope::{Call, Reply};
|
|
|
25
25
|
/// the payload onward without copying it first.
|
|
26
26
|
pub trait DispatchHandler: Send + Sync {
|
|
27
27
|
fn dispatch(&self, call: Call<'_>, yielder: &mut dyn Yielder) -> Option<Reply>;
|
|
28
|
+
|
|
29
|
+
/// Whether the host gave up on this invocation, as when one of its
|
|
30
|
+
/// callbacks raised something meant for the host rather than the
|
|
31
|
+
/// guest. The engine ends the invocation at the next host call it
|
|
32
|
+
/// answers, so the guest cannot go on as if it had been answered.
|
|
33
|
+
fn abandoned(&self) -> bool {
|
|
34
|
+
false
|
|
35
|
+
}
|
|
28
36
|
}
|