kobako 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. data/sig/kobako/snippet/binary.rbs +0 -12
data/ROADMAP.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # Roadmap
2
2
 
3
- kobako is a Ruby gem providing an in-process Wasm sandbox for untrusted mruby
4
- code: a wasmtime host runs a precompiled `kobako.wasm` guest, with host↔guest
5
- Transport over a MessagePack wire. Features cover one-shot `#eval`, preload +
6
- `#run` dispatch, Service injection at constant-path names, opaque
7
- Capability Handles, block yield re-entry, three-class error attribution,
8
- output capture, and a warm Sandbox pool.
3
+ kobako runs untrusted mruby code in an in-process Wasm sandbox. A wasmtime
4
+ host drives a precompiled `kobako.wasm` guest over a MessagePack wire. Each
5
+ row below links a feature to its scenarios and to where its code starts.
9
6
 
10
- | Feature | Entry Points | Notes |
11
- |---------|-------------|-------|
12
- | ✅ [F-01 Sandbox instantiation](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
13
- | ✅ [F-02 Service binding](docs/spec/behavior/services.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) | — |
14
- | ✅ [F-04 Synchronous mruby source execution (`#eval`)](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
15
- | ✅ [F-05 Guest-initiated Transport dispatch](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) | — |
16
- | ✅ [F-06 Capability Handle encoding and referencing](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/catalog/handles.rb](lib/kobako/catalog/handles.rb) | — |
17
- | ✅ [F-07 Three-class error attribution and raising](docs/spec/behavior/outcome.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) | A guest-entry decode failure is exercised only through its payload half ([`CD-003`](docs/spec/behavior/codec.md)); a Run envelope that does not frame is not reachable through the public API |
18
- | ✅ [F-08 Guest output capture](docs/spec/behavior/sandbox.md) | [lib/kobako/capture.rb](lib/kobako/capture.rb) | — |
19
- | ✅ [F-09 Host–guest message codec](docs/wire-codec.md) | [crates/kobako-transport/](crates/kobako-transport/) (core envelope + ABI, one implementation), [lib/kobako/codec/](lib/kobako/codec/) (host payload codec) | The payload layer has a second implementation in `crates/kobako-codec`; the envelope layer is pinned by golden vectors instead |
20
- | ✅ [F-10 Reproducible build pipeline](SPEC.md#code-organization) | [tasks/wasm/build.rake](tasks/wasm/build.rake) | Verified by build-time gates (double-bake byte-identity, gemspec whitelist), not `test/` |
21
- | ✅ [F-11 Multi-layer test and benchmark suite](SPEC.md#testing-style) | [test/](test/) | Benchmarks live in [benchmark/](benchmark/) with the gate in `tasks/bench/`; the anchor baseline advances only by deliberate re-bless |
22
- | ✅ [F-12 Guest block reception and yield re-entry](docs/spec/behavior/transport-yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) | — |
23
- | ✅ [F-13 Snippet preloading (`#preload`)](docs/spec/behavior/sandbox.md) | [lib/kobako/catalog/snippets.rb](lib/kobako/catalog/snippets.rb) | — |
24
- | ✅ [F-14 Synchronous entrypoint dispatch (`#run`)](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) | — |
25
- | ✅ [F-15 Warm Sandbox pool checkout (`Kobako::Pool`)](docs/spec/behavior/pool.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) | — |
7
+ | Feature | Entry Points |
8
+ |---------|-------------|
9
+ | ✅ [Sandbox](docs/spec/behavior/sandbox.md) | [lib/kobako/sandbox.rb](lib/kobako/sandbox.rb) |
10
+ | ✅ [Service registration](docs/spec/behavior/services.md) | [lib/kobako/catalog/services.rb](lib/kobako/catalog/services.rb) |
11
+ | ✅ [Runtime](docs/spec/behavior/runtime.md) | [lib/kobako/runtime.rb](lib/kobako/runtime.rb) |
12
+ | ✅ [Pool](docs/spec/behavior/pool.md) | [lib/kobako/pool.rb](lib/kobako/pool.rb) |
13
+ | ✅ [Extension](docs/spec/behavior/extension.md) | [lib/kobako/extension.rb](lib/kobako/extension.rb) |
14
+ | ✅ [Outcome attribution](docs/spec/behavior/outcome.md) | [lib/kobako/outcome.rb](lib/kobako/outcome.rb) |
15
+ | ✅ [Transport dispatch](docs/spec/behavior/transport-dispatch.md) | [lib/kobako/transport/dispatcher.rb](lib/kobako/transport/dispatcher.rb) |
16
+ | ✅ [Yield re-entry](docs/spec/behavior/transport-yield.md) | [lib/kobako/transport/yielder.rb](lib/kobako/transport/yielder.rb) |
17
+ | ✅ [Dispatch boundary](docs/spec/behavior/transport-boundary.md) | [lib/kobako/transport/exposure.rb](lib/kobako/transport/exposure.rb) |
18
+ | ✅ [Payload encoding](docs/spec/behavior/payload-encoding.md) | [lib/kobako/payload/](lib/kobako/payload/) |
19
+ | ✅ [Payload wire](docs/spec/behavior/codec.md) | [lib/kobako/codec/](lib/kobako/codec/) |
20
+ | ✅ [Core envelope](docs/spec/behavior/envelope.md) | [crates/kobako-transport/](crates/kobako-transport/) |
21
+ | ✅ [mruby guest](docs/spec/behavior/mruby.md) | [wasm/kobako-mruby/](wasm/kobako-mruby/) |
22
+ | ✅ [Guest IO](docs/spec/behavior/io.md) | [wasm/kobako-io/](wasm/kobako-io/) |
23
+ | ✅ [Guest JSON](docs/spec/behavior/json.md) | [wasm/kobako-json/](wasm/kobako-json/) |
24
+ | ✅ [Regexp](docs/spec/behavior/regexp.md) | [wasm/kobako-regexp/src/regexp.rs](wasm/kobako-regexp/src/regexp.rs) |
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
+ | ✅ [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
@@ -3,7 +3,7 @@
3
3
  kobako runs untrusted guest code inside an in-process Wasm sandbox, so a break in
4
4
  its isolation boundary is treated as a security issue. This file is about **reporting
5
5
  such an issue**; for how the boundary is meant to work and where your
6
- responsibilities as a host begin, see [`docs/security-model.md`](docs/security-model.md).
6
+ responsibilities as a host begin, see [`docs/guides/security.md`](docs/guides/security.md).
7
7
 
8
8
  ## Supported versions
9
9
 
@@ -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,122 @@
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
+
89
+ ## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.17.0...kobako-runtime-v0.18.0) (2026-10-13)
90
+
91
+
92
+ ### ⚠ BREAKING CHANGES
93
+
94
+ * **transport:** the stdin layout a Guest Binary reads changed within ABI 4. kobako_transport::envelope::Frames is replaced by Preamble, which is #[non_exhaustive] and built from Default; Bindings and Snippets no longer have their own decode and encode. Guest::eval and Guest::run, MrbGuest::eval and MrbGuest::run, and Runtime::invoke take a Preamble in place of Frames. kobako_core re-exports Preamble and Flag in place of Frames.
95
+ * **transport:** the ABI version is 4, so a Guest Binary built for ABI 3 is refused at Sandbox construction. kobako_core::export_guest! emits __kobako_downcall in place of __kobako_eval, __kobako_run, __kobako_yield_to_block and __kobako_take_outcome, and the host import is __kobako_upcall. kobako_core::abi::{take_outcome, write_outcome, write_reply, decode_run} are replaced by serve and answer, and the frames module is no longer public. Reply, YieldReply and Outcome gain an Unsupported arm and take their tags from the shared registry, and kobako_core::DispatchError and kobako::YieldError gain an Unsupported arm. A yield to a guest without block support raises Kobako::UnsupportedError at the Service's yield instead of failing the invocation as a trap.
96
+ * **runtime:** Runtime::invoke takes &kobako_transport::envelope::Frames in place of kobako_runtime::Frames, whose byte fields are gone. Guest::eval and Guest::run (and their MrbGuest flows) receive the decoded Frames, eval also its source; kobako_core::frames::read_frame is replaced by read_eval_frames and read_frames.
97
+ * **runtime:** kobako_runtime::snapshot::Completion::Outcome holds a decoded kobako_transport::envelope::Outcome instead of Vec<u8>; an engine decodes the result itself and reports an unframed one as
98
+ * **runtime:** kobako_runtime::runtime::Entry::Run holds { entrypoint: &str, payload: Vec<u8> } instead of { envelope: &[u8] }; a frontend passes the fields, and an engine encodes the Run envelope itself.
99
+ * **runtime:** kobako_runtime::yielder::Yielder::yield_to_block returns Result<YieldReply, Trap> instead of the encoded bytes; an engine implementing it decodes the reply itself.
100
+
101
+ ### Features
102
+
103
+ * **runtime:** compile a Preamble's snippets on an instance of their own ([303432d](https://github.com/elct9620/kobako/commit/303432dd90ed62dc378391515e68e784e1b473a4)), closes [#75](https://github.com/elct9620/kobako/issues/75)
104
+ * **transport:** carry each operation as a tag on one channel per direction ([c7d574b](https://github.com/elct9620/kobako/commit/c7d574b59a42e58632f2bdc46cc9a8e4c7ac6121)), closes [#73](https://github.com/elct9620/kobako/issues/73)
105
+ * **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
106
+
107
+
108
+ ### Bug Fixes
109
+
110
+ * keep a compile's bytecode only when the Preamble still fits it ([765db08](https://github.com/elct9620/kobako/commit/765db08ad31c8f5163794c7e144355f3237af93b))
111
+
112
+
113
+ ### Code Refactoring
114
+
115
+ * **runtime:** hand a frontend the decoded Outcome ([0da6aac](https://github.com/elct9620/kobako/commit/0da6aac0f7943d694a5a4ab9439b2d6c527ae1b0)), closes [#73](https://github.com/elct9620/kobako/issues/73)
116
+ * **runtime:** hand a frontend the decoded Yield Reply ([e80ab54](https://github.com/elct9620/kobako/commit/e80ab54e36c2a80b0f746c675f85bd1238fd4331)), closes [#73](https://github.com/elct9620/kobako/issues/73)
117
+ * **runtime:** let the ABI endpoints frame and read the Frames ([dea069a](https://github.com/elct9620/kobako/commit/dea069ac49a59c12a6f7102ed3cf2925f316cbff)), closes [#73](https://github.com/elct9620/kobako/issues/73)
118
+ * **runtime:** let the engine frame a run's entrypoint and payload ([62f9c94](https://github.com/elct9620/kobako/commit/62f9c9407ff3ce690723625bea075fa49d2dda47)), closes [#73](https://github.com/elct9620/kobako/issues/73)
119
+
3
120
  ## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-runtime-v0.16.0...kobako-runtime-v0.17.0) (2026-09-20)
4
121
 
5
122
 
@@ -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`, ...) and the
6
- # dispatch / yield re-entry traits a frontend supplies. Engine
7
- # implementations (`kobako-wasmtime`) and host frontends (the Ruby
8
- # ext) meet on this surface, so the engine stays swappable.
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.17.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.17.0", path = "../kobako-transport" }
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,28 +4,34 @@ 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; this crate
8
- is the surface where the two meet, free of any engine or frontend
9
- type, so the engine stays swappable.
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
  |---|---|
13
13
  | `runtime` | the `Runtime` trait: one guest invocation on a fresh instance in, its observable `Snapshot` out |
14
- | `snapshot` | the per-invocation observables — `Completion` (outcome or trap), the two output `Capture`s, and resource `Usage`, uniform across success and trap |
15
- | `error` | the neutral failure channels: `Trap` (engine fault) and `SetupError` (the invocation never started) |
14
+ | `snapshot` | each invocation's `Completion` (outcome or trap), both output `Capture`s, and `Usage` |
15
+ | `error` | the neutral failure types: `Trap`, `SetupError`, and `InvokeError` over both |
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 |
16
20
  | `dispatch` / `yielder` | the `DispatchHandler` and `Yielder` traits a frontend supplies for guest→host dispatch and block-yield re-entry |
17
21
 
18
- Engine implementations (such as `kobako-wasmtime`) implement
19
- `Runtime`; host frontends (such as the kobako Ruby gem's native ext)
20
- map the neutral types onto their own language surface.
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.
21
25
 
22
26
  ## Usage
23
27
 
28
+ Add the crate to an engine or a frontend that speaks the contract.
29
+
24
30
  ```toml
25
31
  [dependencies]
26
- kobako-runtime = "0.17.0" # x-release-please-version
32
+ kobako-runtime = "0.19.0" # x-release-please-version
27
33
  ```
28
34
 
29
35
  ## License
30
36
 
31
- 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
  }
@@ -1,9 +1,9 @@
1
- //! Engine-neutral host error channels, free of any frontend dependency.
1
+ //! Engine-neutral host error types, free of any frontend dependency.
2
2
  //!
3
3
  //! The run path produces these instead of constructing host-language
4
4
  //! exceptions directly; each frontend's boundary is the single place that
5
5
  //! maps them onto its own error classes (the Ruby ext does so in its error
6
- //! mapper). Keeping the channels frontend-free lets any engine
6
+ //! mapper). Keeping the types frontend-free lets any engine
7
7
  //! implementation produce them unchanged.
8
8
 
9
9
  use std::fmt;
@@ -26,11 +26,10 @@ pub enum Trap {
26
26
  }
27
27
 
28
28
  /// A failure that yields no invocation outcome. The discriminant records
29
- /// the runtime's state so a frontend can attribute the failure per SPEC:
29
+ /// the runtime's state so a frontend can attribute the failure:
30
30
  /// `ModuleNotBuilt` (the guest artifact is absent), `Dead` (the runtime
31
31
  /// could not be constructed), and `Intact` (the runtime is live but a
32
- /// host-side pre-call step failed, so no discard-and-recreate recovery is
33
- /// owed).
32
+ /// host-side pre-call step failed before the engine ran).
34
33
  ///
35
34
  /// Non-exhaustive, as `Trap`: a frontend maps each state onto its own
36
35
  /// class, and a later state must not break that match.
@@ -45,14 +44,14 @@ pub enum SetupError {
45
44
  /// A failure that prevented the invocation from starting — a pre-call
46
45
  /// engine fault (`Trap`) or a host-side setup fault (`Setup`) — unified so
47
46
  /// the run mechanics can propagate both with `?`; a frontend destructures
48
- /// it back into the two channels. Faults after the guest export starts
47
+ /// it back into the two. Faults after the guest export starts
49
48
  /// ride in `Completion::Trap` instead, so captures and usage survive them.
50
49
  ///
51
50
  /// Named for where it comes from — the `Err` of `Runtime::invoke` — so a
52
51
  /// host that also has its own `Error` in scope can hold both.
53
52
  ///
54
- /// Non-exhaustive, as its two channels are: a later way an invocation can
55
- /// fail to start must not break the frontends already destructuring it.
53
+ /// Non-exhaustive: a later way an invocation can fail to start must not
54
+ /// break the frontends already destructuring it.
56
55
  #[derive(Debug)]
57
56
  #[non_exhaustive]
58
57
  pub enum InvokeError {
@@ -96,7 +95,7 @@ impl fmt::Display for SetupError {
96
95
  }
97
96
  }
98
97
 
99
- // Both channels sit on the seam a third-party engine implements against,
98
+ // Both types sit on the seam a third-party engine implements against,
100
99
  // where a caller wants to lift them into `Box<dyn Error>` or `anyhow` with
101
100
  // `?`. `Trap` carries no source of its own, so neither reports one.
102
101
  impl std::error::Error for SetupError {}
@@ -0,0 +1,194 @@
1
+ //! How a host directory spells a name it holds. A host that folds case or
2
+ //! Unicode form reaches one entry by many names, so a Policy judging the
3
+ //! name as asked would see a spelling the host never stored.
4
+ //!
5
+ //! macOS answers the stored name for any spelling. Elsewhere a listing
6
+ //! finds the entry by its inode, skipped on a Linux filesystem known to
7
+ //! match names exactly; without an inode, a spelling the listing lacks is
8
+ //! refused.
9
+
10
+ use std::io;
11
+
12
+ use cap_std::fs::Dir;
13
+
14
+ /// The name `parent` stores for the entry `name` reaches, a link reached
15
+ /// as itself, or `None` when nothing is there.
16
+ pub(super) fn stored_name(parent: &Dir, name: &str) -> io::Result<Option<String>> {
17
+ #[cfg(target_os = "macos")]
18
+ {
19
+ macos::stored_name(parent, name)
20
+ }
21
+ #[cfg(not(target_os = "macos"))]
22
+ {
23
+ #[cfg(target_os = "linux")]
24
+ if linux::matches_exactly(parent) {
25
+ return match parent.symlink_metadata(name) {
26
+ Ok(_) => Ok(Some(name.to_string())),
27
+ Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
28
+ Err(err) => Err(err),
29
+ };
30
+ }
31
+ stored_name_by_listing(parent, name)
32
+ }
33
+ }
34
+
35
+ /// The name a listing of `parent` holds for `name`: itself when listed,
36
+ /// else the entry sharing its inode.
37
+ #[cfg(not(target_os = "macos"))]
38
+ fn stored_name_by_listing(parent: &Dir, name: &str) -> io::Result<Option<String>> {
39
+ let found = match parent.symlink_metadata(name) {
40
+ Ok(found) => found,
41
+ Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
42
+ Err(err) => return Err(err),
43
+ };
44
+ let mut names = Vec::new();
45
+ for entry in parent.entries()? {
46
+ let entry = entry?;
47
+ if entry.file_name() == name {
48
+ return Ok(Some(name.to_string()));
49
+ }
50
+ names.push(entry);
51
+ }
52
+ #[cfg(unix)]
53
+ {
54
+ use cap_std::fs::MetadataExt;
55
+ use std::os::unix::fs::DirEntryExt;
56
+ for entry in names {
57
+ if entry.ino() == found.ino() {
58
+ return entry
59
+ .file_name()
60
+ .into_string()
61
+ .map(Some)
62
+ .map_err(|_| io::ErrorKind::InvalidData.into());
63
+ }
64
+ }
65
+ }
66
+ #[cfg(not(unix))]
67
+ let _ = (found, names);
68
+ Err(io::ErrorKind::PermissionDenied.into())
69
+ }
70
+
71
+ #[cfg(target_os = "macos")]
72
+ mod macos {
73
+ use std::ffi::CString;
74
+ use std::io;
75
+ use std::os::fd::AsRawFd;
76
+
77
+ use cap_std::fs::Dir;
78
+
79
+ /// The reply `getattrlist(2)` writes for `ATTR_CMN_NAME`: its length,
80
+ /// then a reference to the name stored behind it.
81
+ #[repr(C, packed(4))]
82
+ struct NameReply {
83
+ length: u32,
84
+ name: libc::attrreference_t,
85
+ data: [u8; 1024],
86
+ }
87
+
88
+ pub(super) fn stored_name(parent: &Dir, name: &str) -> io::Result<Option<String>> {
89
+ let path = CString::new(name).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
90
+ let mut request = libc::attrlist {
91
+ bitmapcount: libc::ATTR_BIT_MAP_COUNT,
92
+ reserved: 0,
93
+ commonattr: libc::ATTR_CMN_NAME,
94
+ volattr: 0,
95
+ dirattr: 0,
96
+ fileattr: 0,
97
+ forkattr: 0,
98
+ };
99
+ let mut reply = NameReply {
100
+ length: 0,
101
+ name: libc::attrreference_t {
102
+ attr_dataoffset: 0,
103
+ attr_length: 0,
104
+ },
105
+ data: [0; 1024],
106
+ };
107
+ // SAFETY: every pointer names a live, correctly sized buffer, and
108
+ // the reply buffer's size bounds what the call writes.
109
+ let answered = unsafe {
110
+ libc::getattrlistat(
111
+ parent.as_raw_fd(),
112
+ path.as_ptr(),
113
+ (&raw mut request).cast(),
114
+ (&raw mut reply).cast(),
115
+ size_of::<NameReply>(),
116
+ libc::FSOPT_NOFOLLOW as libc::c_ulong,
117
+ )
118
+ };
119
+ if answered != 0 {
120
+ let err = io::Error::last_os_error();
121
+ return match err.kind() {
122
+ io::ErrorKind::NotFound => Ok(None),
123
+ _ => Err(err),
124
+ };
125
+ }
126
+ // The reference counts from itself, and `data` follows it; the
127
+ // length counts the terminating NUL.
128
+ let start = usize::try_from(reply.name.attr_dataoffset)
129
+ .ok()
130
+ .and_then(|offset| offset.checked_sub(size_of::<libc::attrreference_t>()));
131
+ let len = (reply.name.attr_length as usize).checked_sub(1);
132
+ let stored = start
133
+ .zip(len)
134
+ .and_then(|(start, len)| reply.data.get(start..start.checked_add(len)?))
135
+ .ok_or(io::ErrorKind::InvalidData)?;
136
+ String::from_utf8(stored.to_vec())
137
+ .map(Some)
138
+ .map_err(|_| io::ErrorKind::InvalidData.into())
139
+ }
140
+ }
141
+
142
+ #[cfg(target_os = "linux")]
143
+ mod linux {
144
+ use cap_std::fs::Dir;
145
+ use rustix::fs::{fstatfs, ioctl_getflags};
146
+
147
+ /// The 32-bit magic numbers of filesystems matching names byte for
148
+ /// byte unless a directory is marked to fold case: ext4, Btrfs, tmpfs,
149
+ /// overlayfs, and F2FS. XFS is left to the listing, as a volume made
150
+ /// ASCII case-insensitive marks no directory.
151
+ const EXACT: [u32; 5] = [0xEF53, 0x9123_683E, 0x0102_1994, 0x794C_7630, 0xF2F5_2010];
152
+
153
+ /// The inode flag marking a directory whose names fold case, from
154
+ /// Linux's `FS_CASEFOLD_FL`.
155
+ const CASEFOLD: u32 = 0x4000_0000;
156
+
157
+ pub(super) fn matches_exactly(parent: &Dir) -> bool {
158
+ let exact = fstatfs(parent).is_ok_and(|stats| EXACT.contains(&(stats.f_type as u32)));
159
+ exact && ioctl_getflags(parent).is_ok_and(|flags| flags.bits() & CASEFOLD == 0)
160
+ }
161
+
162
+ #[cfg(test)]
163
+ mod tests {
164
+ use cap_std::ambient_authority;
165
+
166
+ use super::super::stored_name_by_listing;
167
+ use super::Dir;
168
+
169
+ // A Linux host whose filesystem is not known to match exactly,
170
+ // XFS among them, spells by listing; the host here matches
171
+ // exactly, so the listing must answer as the lookup would.
172
+ // @behavior FS-081
173
+ #[test]
174
+ fn a_listing_spells_names_it_matches_exactly_as_given() {
175
+ let path = std::env::temp_dir().join(format!("kobako-listed-{}", std::process::id()));
176
+ let _ = std::fs::remove_dir_all(&path);
177
+ std::fs::create_dir_all(&path).expect("the host directory must be created");
178
+ std::fs::write(path.join("file"), "x").expect("the host file must be written");
179
+ let dir = Dir::open_ambient_dir(&path, ambient_authority())
180
+ .expect("the host directory must open");
181
+ let answers = (
182
+ stored_name_by_listing(&dir, "file").ok(),
183
+ stored_name_by_listing(&dir, "missing").ok(),
184
+ );
185
+ let _ = std::fs::remove_dir_all(&path);
186
+ assert_eq!(
187
+ answers,
188
+ (Some(Some("file".to_string())), Some(None)),
189
+ "a name the host holds, spelled by listing its directory, must come back as \
190
+ given, and a name it lacks as none"
191
+ );
192
+ }
193
+ }
194
+ }