kobako 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
@@ -1,11 +1,10 @@
1
- //! The Ruby face of the Filesystem: `Kobako::Filesystem::Memory`,
2
- //! `Temporary`, `Directory`, and `Overlay`, each standing for the
3
- //! `kobako_runtime` Filesystem of that kind.
1
+ //! The Ruby face of the Filesystem: `Kobako::Filesystem::Tmpfs`,
2
+ //! `Bind`, and `Overlay`, each standing for the `kobako_runtime`
3
+ //! Filesystem of that kind.
4
4
  //!
5
5
  //! A kind kobako ships runs in Rust; Ruby only picks it and the access.
6
- //! Ruby is asked again while the guest runs through a mount's block, at
7
- //! most once per open or change, and through a Filesystem written in
8
- //! Ruby, once per call.
6
+ //! Ruby is asked again while the guest runs, through a mount's block and
7
+ //! through a Filesystem written in Ruby.
9
8
 
10
9
  mod grants;
11
10
  mod ruby;
@@ -14,38 +13,36 @@ use std::sync::Arc;
14
13
 
15
14
  use magnus::value::{Opaque, ReprValue};
16
15
  use magnus::{
17
- function, gc, method, prelude::*, scan_args::get_kwargs, scan_args::scan_args,
18
- DataTypeFunctions, Error as MagnusError, RHash, RModule, RString, Ruby, Symbol, TypedData,
19
- Value,
16
+ function, gc, prelude::*, scan_args::get_kwargs, scan_args::scan_args, DataTypeFunctions,
17
+ Error as MagnusError, RArray, RHash, RModule, RString, Ruby, Symbol, TypedData, Value,
20
18
  };
21
19
 
22
20
  use ruby::RubyFilesystem;
23
21
 
24
- use kobako_runtime::filesystem::{
25
- Access, Blocking, Directory, Filesystem, Memory, Overlay, Temporary,
26
- };
22
+ use kobako_runtime::filesystem::{Bind, Filesystem, FilesystemError, Overlay, Tmpfs};
27
23
 
28
24
  pub(super) use grants::{to_grants, validate_mount};
29
25
 
26
+ /// Each kind kobako ships is one Filesystem instance, made once with its
27
+ /// Ruby object, so every Mount of that object shares it as a bind mount
28
+ /// shares a super block.
30
29
  macro_rules! filesystem_class {
31
30
  ($name:ident, $class:literal) => {
32
31
  #[magnus::wrap(class = $class, free_immediately, size)]
33
- pub(super) struct $name(Arc<dyn Filesystem>);
32
+ pub(super) struct $name(Arc<Filesystem>);
34
33
  };
35
34
  }
36
35
 
37
- filesystem_class!(MemoryFs, "Kobako::Filesystem::Memory");
38
- filesystem_class!(TemporaryFs, "Kobako::Filesystem::Temporary");
39
- filesystem_class!(DirectoryFs, "Kobako::Filesystem::Directory");
36
+ filesystem_class!(TmpfsFs, "Kobako::Filesystem::Tmpfs");
37
+ filesystem_class!(BindFs, "Kobako::Filesystem::Bind");
40
38
 
41
- /// The Overlay itself rather than a shared handle, so `layer` can answer a
42
- /// new one and leave this one as it was. It marks the Ruby object of
39
+ /// The Overlay's one Filesystem instance. It marks the Ruby object of
43
40
  /// every Layer, since a Filesystem written in Ruby has nothing else
44
41
  /// keeping it alive.
45
42
  #[derive(magnus::TypedData)]
46
43
  #[magnus(class = "Kobako::Filesystem::Overlay", free_immediately, size, mark)]
47
44
  pub(super) struct OverlayFs {
48
- overlay: Overlay,
45
+ filesystem: Arc<Filesystem>,
49
46
  layers: Vec<Opaque<Value>>,
50
47
  }
51
48
 
@@ -57,40 +54,36 @@ impl DataTypeFunctions for OverlayFs {
57
54
 
58
55
  pub(super) fn register(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
59
56
  let module = kobako.define_module("Filesystem")?;
60
- let memory = module.define_class("Memory", ruby.class_object())?;
61
- memory.define_singleton_method("new", function!(memory_new, -1))?;
62
- let temporary = module.define_class("Temporary", ruby.class_object())?;
63
- temporary.define_singleton_method("new", function!(temporary_new, -1))?;
64
- let directory = module.define_class("Directory", ruby.class_object())?;
65
- directory.define_singleton_method("new", function!(directory_new, 1))?;
57
+ let tmpfs = module.define_class("Tmpfs", ruby.class_object())?;
58
+ tmpfs.define_singleton_method("new", function!(tmpfs_new, -1))?;
59
+ let bind = module.define_class("Bind", ruby.class_object())?;
60
+ bind.define_singleton_method("new", function!(bind_new, 1))?;
66
61
  let overlay = module.define_class("Overlay", ruby.class_object())?;
67
- overlay.define_singleton_method("new", function!(overlay_new, 0))?;
68
- overlay.define_method("layer", method!(overlay_layer, -1))?;
69
- MemoryFs::class(ruby);
70
- TemporaryFs::class(ruby);
71
- DirectoryFs::class(ruby);
62
+ overlay.define_singleton_method("new", function!(overlay_new, -1))?;
63
+ TmpfsFs::class(ruby);
64
+ BindFs::class(ruby);
72
65
  OverlayFs::class(ruby);
73
66
  Ok(())
74
67
  }
75
68
 
76
- /// `Memory.new(files = {}, limit: 64 MiB, inodes:)`: each
69
+ /// `Tmpfs.new(files = {}, size: 64 MiB, nr_inodes:)`: each
77
70
  /// `path => contents` pair becomes a file, its directories created along
78
71
  /// the way. Ruby hands a bare `"path" => contents` over as keywords, so
79
- /// every String keyword is a file too. Without `inodes:` the entries
80
- /// follow the limit.
81
- fn memory_new(args: &[Value]) -> Result<MemoryFs, MagnusError> {
72
+ /// every String keyword is a file too. Without `nr_inodes:` the entries
73
+ /// follow the size.
74
+ fn tmpfs_new(args: &[Value]) -> Result<TmpfsFs, MagnusError> {
82
75
  let ruby = Ruby::get().expect("Ruby thread");
83
76
  let args = scan_args::<(), (Option<RHash>,), (), (), RHash, ()>(args)?;
84
77
  let mut files = Vec::new();
85
- let mut limit = Some(Memory::DEFAULT_LIMIT);
86
- let mut inodes = None;
87
- let limit_key = ruby.to_symbol("limit");
88
- let inodes_key = ruby.to_symbol("inodes");
78
+ let mut size = Some(Tmpfs::DEFAULT_SIZE);
79
+ let mut nr_inodes = None;
80
+ let size_key = ruby.to_symbol("size");
81
+ let nr_inodes_key = ruby.to_symbol("nr_inodes");
89
82
  for hash in args.optional.0.into_iter().chain([args.keywords]) {
90
83
  hash.foreach(|key: Value, value: Value| {
91
84
  match Symbol::from_value(key) {
92
- Some(key) if key.equal(limit_key)? => limit = to_limit(value)?,
93
- Some(key) if key.equal(inodes_key)? => inodes = Some(to_limit(value)?),
85
+ Some(key) if key.equal(size_key)? => size = to_count(value)?,
86
+ Some(key) if key.equal(nr_inodes_key)? => nr_inodes = Some(to_count(value)?),
94
87
  Some(key) => {
95
88
  return Err(MagnusError::new(
96
89
  ruby.exception_arg_error(),
@@ -105,125 +98,104 @@ fn memory_new(args: &[Value]) -> Result<MemoryFs, MagnusError> {
105
98
  Ok(magnus::r_hash::ForEach::Continue)
106
99
  })?;
107
100
  }
108
- // A list no Memory holds is an `ArgumentError` the Host App can rescue.
109
- let memory = Memory::try_from_iter(files)
101
+ // A list no Tmpfs holds is an `ArgumentError` the Host App can rescue.
102
+ let tmpfs = Tmpfs::try_from_iter(files)
110
103
  .map_err(|error| MagnusError::new(ruby.exception_arg_error(), error.to_string()))?
111
- .with_limit(limit);
112
- Ok(MemoryFs(Arc::new(match inodes {
113
- Some(inodes) => memory.with_inodes(inodes),
114
- None => memory,
115
- })))
116
- }
117
-
118
- /// `Temporary.new(limit: 64 MiB, inodes:)`.
119
- fn temporary_new(args: &[Value]) -> Result<TemporaryFs, MagnusError> {
120
- let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
121
- let kwargs = get_kwargs::<_, (), (Option<Value>, Option<Value>), ()>(
122
- args.keywords,
123
- &[],
124
- &["limit", "inodes"],
125
- )?;
126
- let (limit, inodes) = kwargs.optional;
127
- let limit = match limit {
128
- Some(limit) => to_limit(limit)?,
129
- None => Some(Memory::DEFAULT_LIMIT),
130
- };
131
- let temporary = Temporary::new().with_limit(limit);
132
- Ok(TemporaryFs(Arc::new(match inodes {
133
- Some(inodes) => temporary.with_inodes(to_limit(inodes)?),
134
- None => temporary,
135
- })))
104
+ .with_size(size);
105
+ let filesystem = Filesystem::new(Arc::new(match nr_inodes {
106
+ Some(nr_inodes) => tmpfs.with_nr_inodes(nr_inodes),
107
+ None => tmpfs,
108
+ }));
109
+ filesystem.map(TmpfsFs).map_err(cannot_stand)
136
110
  }
137
111
 
138
- /// A `limit:` or `inodes:` count, or `nil` for none.
139
- fn to_limit(limit: Value) -> Result<Option<usize>, MagnusError> {
140
- Option::<usize>::try_convert(limit)
141
- }
142
-
143
- fn directory_new(path: String) -> Result<DirectoryFs, MagnusError> {
112
+ /// A Filesystem whose instance cannot stand, an `ArgumentError` the Host App
113
+ /// can rescue.
114
+ fn cannot_stand(error: FilesystemError) -> MagnusError {
144
115
  let ruby = Ruby::get().expect("Ruby thread");
145
- Directory::new(&path)
146
- .map(|directory| DirectoryFs(Arc::new(directory)))
147
- .map_err(|err| {
148
- MagnusError::new(
149
- ruby.exception_arg_error(),
150
- format!("cannot open host directory {path}: {err}"),
151
- )
152
- })
116
+ MagnusError::new(
117
+ ruby.exception_arg_error(),
118
+ format!("cannot make a Filesystem: {error:?}"),
119
+ )
153
120
  }
154
121
 
155
- /// `Overlay.new`, with no Layer yet.
156
- fn overlay_new() -> OverlayFs {
157
- OverlayFs {
158
- overlay: Overlay::new().with_blocking(Arc::new(GvlFreeWait)),
159
- layers: Vec::new(),
160
- }
122
+ /// A `size:` or `nr_inodes:` count, or `nil` for none.
123
+ fn to_count(count: Value) -> Result<Option<usize>, MagnusError> {
124
+ Option::<usize>::try_convert(count)
161
125
  }
162
126
 
163
- /// An Overlay's thread waits for its lock without the GVL, so a holder
164
- /// asking a Layer written in Ruby can take the GVL and finish.
165
- struct GvlFreeWait;
166
-
167
- impl Blocking for GvlFreeWait {
168
- fn wait(&self, wait: &mut dyn FnMut()) {
169
- super::gvl::region(super::gvl::holds(), wait);
170
- }
127
+ fn bind_new(path: String) -> Result<BindFs, MagnusError> {
128
+ let ruby = Ruby::get().expect("Ruby thread");
129
+ let directory = Bind::new(&path).map_err(|err| {
130
+ MagnusError::new(
131
+ ruby.exception_arg_error(),
132
+ format!("cannot open host directory {path}: {err}"),
133
+ )
134
+ })?;
135
+ Filesystem::new(Arc::new(directory))
136
+ .map(BindFs)
137
+ .map_err(cannot_stand)
171
138
  }
172
139
 
173
- /// `layer(filesystem, access: :read)`, a new Overlay with `filesystem`
174
- /// beneath this one's Layers.
175
- fn overlay_layer(overlay: &OverlayFs, args: &[Value]) -> Result<OverlayFs, MagnusError> {
176
- let args = scan_args::<(Value,), (), (), (), RHash, ()>(args)?;
177
- let kwargs = get_kwargs::<_, (), (Option<Value>,), ()>(args.keywords, &[], &["access"])?;
178
- let (filesystem,) = args.required;
179
- let access = match kwargs.optional.0 {
180
- Some(access) => to_access(access)?,
181
- None => Access::Read,
140
+ /// `Overlay.new(lower:, upper: nil)`: the Filesystems in `lower`, topmost
141
+ /// first, under `upper`, a Tmpfs. Without `upper` it takes two lower
142
+ /// Layers at least.
143
+ fn overlay_new(args: &[Value]) -> Result<OverlayFs, MagnusError> {
144
+ let ruby = Ruby::get().expect("Ruby thread");
145
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
146
+ let kwargs =
147
+ get_kwargs::<_, (RArray,), (Option<Value>,), ()>(args.keywords, &["lower"], &["upper"])?;
148
+ let lower = (0..kwargs.required.0.len())
149
+ .map(|index| kwargs.required.0.entry::<Value>(index as isize))
150
+ .collect::<Result<Vec<_>, _>>()?;
151
+ let upper = kwargs.optional.0.filter(|upper| !upper.is_nil());
152
+ let upper_fs = match upper {
153
+ Some(upper) => match <&TmpfsFs>::try_convert(upper) {
154
+ Ok(tmpfs) => Some(tmpfs.0.clone()),
155
+ Err(_) => {
156
+ return Err(MagnusError::new(
157
+ ruby.exception_type_error(),
158
+ format!(
159
+ "upper must be a Kobako::Filesystem::Tmpfs, got {}",
160
+ upper.class()
161
+ ),
162
+ ))
163
+ }
164
+ },
165
+ None => None,
182
166
  };
183
- let mut layers = overlay.layers.clone();
184
- layers.push(Opaque::from(filesystem));
167
+ let lower_fs = lower
168
+ .iter()
169
+ .map(|layer| to_filesystem(*layer))
170
+ .collect::<Result<Vec<_>, _>>()?;
171
+ let overlay = Overlay::new(lower_fs, upper_fs).map_err(|_| {
172
+ MagnusError::new(
173
+ ruby.exception_arg_error(),
174
+ "an Overlay needs a lower Layer under its upper, or two lower Layers without one, \
175
+ each Filesystem at most once",
176
+ )
177
+ })?;
185
178
  Ok(OverlayFs {
186
- overlay: overlay
187
- .overlay
188
- .clone()
189
- .layer(to_filesystem(filesystem)?, access),
190
- layers,
179
+ filesystem: Filesystem::new(Arc::new(overlay)).map_err(cannot_stand)?,
180
+ layers: lower.into_iter().chain(upper).map(Opaque::from).collect(),
191
181
  })
192
182
  }
193
183
 
194
- fn to_access(access: Value) -> Result<Access, MagnusError> {
195
- let name = Symbol::from_value(access)
196
- .map(|symbol| symbol.name())
197
- .transpose()?;
198
- match name.as_deref() {
199
- Some("read") => Ok(Access::Read),
200
- Some("read_write") => Ok(Access::ReadWrite),
201
- _ => Err(MagnusError::new(
202
- Ruby::get().expect("Ruby thread").exception_arg_error(),
203
- format!(
204
- "access must be :read or :read_write, got {}",
205
- access.inspect()
206
- ),
207
- )),
208
- }
209
- }
210
-
211
- /// The Filesystem a Ruby handle of any kobako kind stands for.
212
- fn to_filesystem(value: Value) -> Result<Arc<dyn Filesystem>, MagnusError> {
213
- if let Ok(fs) = <&MemoryFs>::try_convert(value) {
214
- return Ok(fs.0.clone());
215
- }
216
- if let Ok(fs) = <&TemporaryFs>::try_convert(value) {
184
+ /// The Filesystem instance a Ruby handle of any kobako kind stands for. A
185
+ /// Filesystem written in Ruby has no instance of its own to share, so each
186
+ /// call makes one.
187
+ fn to_filesystem(value: Value) -> Result<Arc<Filesystem>, MagnusError> {
188
+ if let Ok(fs) = <&TmpfsFs>::try_convert(value) {
217
189
  return Ok(fs.0.clone());
218
190
  }
219
- if let Ok(fs) = <&DirectoryFs>::try_convert(value) {
191
+ if let Ok(fs) = <&BindFs>::try_convert(value) {
220
192
  return Ok(fs.0.clone());
221
193
  }
222
194
  if let Ok(fs) = <&OverlayFs>::try_convert(value) {
223
- return Ok(Arc::new(fs.overlay.clone()));
195
+ return Ok(fs.filesystem.clone());
224
196
  }
225
197
  if let Some(fs) = RubyFilesystem::from_value(value) {
226
- return Ok(Arc::new(fs));
198
+ return Filesystem::new(Arc::new(fs)).map_err(cannot_stand);
227
199
  }
228
200
  let ruby = Ruby::get().expect("Ruby thread");
229
201
  Err(MagnusError::new(
@@ -6,16 +6,24 @@
6
6
  //! callback, which is ordinary Ruby and must hold the lock. A thread-local flag
7
7
  //! stops a nested dispatch from re-acquiring a second time, and both directions
8
8
  //! run their closure through a `catch_unwind` trampoline because a panic must
9
- //! never cross the C frame of `rb_thread_call_without_gvl` /
9
+ //! never cross the C frame of `rb_thread_call_without_gvl2` /
10
10
  //! `rb_thread_call_with_gvl` (undefined behaviour). The flag is per-thread,
11
11
  //! matching the one-thread-one-Sandbox contract, and is restored through a drop
12
12
  //! guard so an unwinding panic cannot leave it corrupt for the next invocation.
13
+ //!
14
+ //! A release never raises where it returns, as `rb_thread_call_without_gvl`
15
+ //! would, since that jumps over every Rust frame of the run and what they hold:
16
+ //! a lock's guard, this flag. An interrupt waits for the run to end instead.
13
17
 
14
18
  use std::cell::Cell;
15
19
  use std::os::raw::c_void;
16
20
  use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
17
21
  use std::ptr;
18
22
 
23
+ use kobako_runtime::sched::{Blocking, Task};
24
+
25
+ use super::escape;
26
+
19
27
  thread_local! {
20
28
  // True while this thread sits inside a region whose GVL we released and
21
29
  // have not yet re-acquired. Drives `reenter`'s one-shot re-acquire.
@@ -30,17 +38,39 @@ where
30
38
  return f();
31
39
  }
32
40
  let _guard = ReleasedGuard::set(true);
41
+ // An interrupt the thread receives meanwhile stops each wait for a lock the
42
+ // run meets; the interrupt itself still waits for the run to end.
43
+ let task = Task::current();
44
+ let task_ptr = ptr::addr_of!(task).cast_mut().cast::<c_void>();
33
45
  call_via(f, |func, data| unsafe {
34
- rb_sys::rb_thread_call_without_gvl(Some(func), data, None, ptr::null_mut())
46
+ rb_sys::rb_thread_call_without_gvl2(Some(func), data, Some(interrupt_task), task_ptr)
35
47
  })
36
48
  }
37
49
 
50
+ /// Have the thread's run stop waiting for locks, called by Ruby without the GVL
51
+ /// as the region's unblocking function.
52
+ unsafe extern "C" fn interrupt_task(task: *mut c_void) {
53
+ let task = unsafe { &*task.cast::<Task>() };
54
+ task.interrupt();
55
+ }
56
+
38
57
  /// Whether this thread holds the GVL: outside any released region, or
39
58
  /// inside a dispatch that re-acquired it.
40
- pub(crate) fn holds() -> bool {
59
+ fn holds() -> bool {
41
60
  !GVL_RELEASED.with(|g| g.get())
42
61
  }
43
62
 
63
+ /// A thread waits for a lock without the GVL, so a holder asking a
64
+ /// Filesystem written in Ruby, or a mount's block, can take the GVL and
65
+ /// finish.
66
+ pub(crate) struct GvlFreeWait;
67
+
68
+ impl Blocking for GvlFreeWait {
69
+ fn wait(&self, wait: &mut dyn FnMut()) {
70
+ region(holds(), wait);
71
+ }
72
+ }
73
+
44
74
  /// Runs inline when the GVL is already held — hold mode, or a nested
45
75
  /// dispatch whose outer frame re-acquired it.
46
76
  pub(crate) fn reenter<F, R>(f: F) -> R
@@ -95,7 +125,7 @@ where
95
125
 
96
126
  fn call_via<F, R>(
97
127
  f: F,
98
- invoke: impl FnOnce(unsafe extern "C" fn(*mut c_void) -> *mut c_void, *mut c_void) -> *mut c_void,
128
+ mut invoke: impl FnMut(unsafe extern "C" fn(*mut c_void) -> *mut c_void, *mut c_void) -> *mut c_void,
99
129
  ) -> R
100
130
  where
101
131
  F: FnOnce() -> R,
@@ -104,9 +134,14 @@ where
104
134
  f: Some(f),
105
135
  result: None,
106
136
  };
107
- invoke(trampoline::<F, R>, ptr::addr_of_mut!(ctx).cast::<c_void>());
108
- match ctx.result.take().expect("gvl trampoline stores a result") {
109
- Ok(value) => value,
110
- Err(payload) => resume_unwind(payload),
137
+ loop {
138
+ invoke(trampoline::<F, R>, ptr::addr_of_mut!(ctx).cast::<c_void>());
139
+ match ctx.result.take() {
140
+ Some(Ok(value)) => return value,
141
+ Some(Err(payload)) => resume_unwind(payload),
142
+ // `rb_thread_call_without_gvl2` runs nothing while an interrupt
143
+ // is pending, so that one is taken off the thread first.
144
+ None => escape::hold_interrupts(),
145
+ }
111
146
  }
112
147
  }
@@ -46,7 +46,7 @@ impl Snapshot {
46
46
  Self(snapshot, verb)
47
47
  }
48
48
 
49
- /// Already split off the core envelope, so the Ruby side maps a failure
49
+ /// Already split off the Envelope, so the Ruby side maps a failure
50
50
  /// onto its error taxonomy without decoding a payload byte: the ok
51
51
  /// arm's payload, or a Panic's fields beside an empty payload. A trap
52
52
  /// has no Outcome; `#trap_error` is the discriminator there.
@@ -111,13 +111,17 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
111
111
  // directly into those classes through the constructors and mappers
112
112
  // in `runtime/errors.rs` — no intermediate hierarchy is registered.
113
113
 
114
+ // This extension is the only frontend in its process, so the first
115
+ // registration is the one that stands.
116
+ let _ = kobako_runtime::sched::register_blocking(Box::new(gvl::GvlFreeWait));
117
+
114
118
  let runtime = kobako.define_class("Runtime", ruby.class_object())?;
115
119
  runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
116
120
  runtime.define_method("eval", method!(Runtime::eval, 3))?;
117
121
  runtime.define_method("run", method!(Runtime::run, 4))?;
118
122
  runtime.define_method("profile", method!(Runtime::profile, 0))?;
119
123
  runtime.define_method("compile", method!(Runtime::compile, 1))?;
120
- runtime.define_singleton_method("validate_mount!", function!(filesystem::validate_mount, 3))?;
124
+ runtime.define_singleton_method("validate_mount", function!(filesystem::validate_mount, 3))?;
121
125
  // The guest re-enters for a block yield through a frame-scoped
122
126
  // `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
123
127
  // Runtime.
@@ -331,6 +335,7 @@ impl Runtime {
331
335
  let ruby = Ruby::get().expect("Ruby thread");
332
336
  let preamble = to_preamble(&ruby, preamble)?;
333
337
  let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
338
+ escape::resume()?;
334
339
  Ok(bytecodes.map(|bytecodes| {
335
340
  ruby.ary_from_iter(
336
341
  bytecodes
@@ -16,10 +16,10 @@ module Kobako
16
16
  # The Extension readers are duck-typed, so a malformed +name+ or
17
17
  # +backend.path+ surfaces through the registry it routes to.
18
18
  def install(extension, snippets:, services:)
19
- validate!(extension)
19
+ validate(extension)
20
20
  snippets.register(code: extension.source, name: extension.name)
21
21
  backend = extension.backend
22
- services.bind(backend.path, install_object(backend)) if backend
22
+ services.bind(backend.path, object_of(backend)) if backend
23
23
  @entries << extension
24
24
  self
25
25
  end
@@ -27,7 +27,7 @@ module Kobako
27
27
  # The asserted flag flips only on success, so a check that failed
28
28
  # re-runs on the next invocation rather than silently passing a broken
29
29
  # Sandbox.
30
- def assert_dependencies!
30
+ def assert_dependencies
31
31
  return self if @asserted
32
32
 
33
33
  extension, dependency = unmet_dependency
@@ -52,7 +52,7 @@ module Kobako
52
52
 
53
53
  private
54
54
 
55
- def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
55
+ def object_of(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
56
56
 
57
57
  def resolve_backend(extension, resolved, by_provider)
58
58
  backend = extension.backend
@@ -64,7 +64,7 @@ module Kobako
64
64
  resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
65
65
  end
66
66
 
67
- def validate!(extension)
67
+ def validate(extension)
68
68
  source = extension.source
69
69
  raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
70
70
 
@@ -18,10 +18,10 @@ module Kobako
18
18
  end
19
19
  raise ArgumentError, "flag value must be a String, got #{value.class}" unless value.is_a?(String)
20
20
 
21
- key = Catalog.to_utf8!(name.to_s, "flag name")
21
+ key = Catalog.to_utf8(name.to_s, "flag name")
22
22
  raise ArgumentError, "flag #{key.inspect} already set" if @entries.key?(key)
23
23
 
24
- @entries[key] = [Catalog.to_utf8!(value, "flag value"), !advisory]
24
+ @entries[key] = [Catalog.to_utf8(value, "flag value"), !advisory]
25
25
  nil
26
26
  end
27
27
  end
@@ -23,8 +23,8 @@ module Kobako
23
23
  # call made through this Handle is authorized against the reference
24
24
  # the guest was given.
25
25
  def alloc(object)
26
- reject_unwrappable!(object)
27
- ensure_capacity!
26
+ reject_unwrappable(object)
27
+ ensure_capacity
28
28
  id = @next_id
29
29
  @entries[id] = Kobako::Transport::Exposure.of(object, @surfaces)
30
30
  @next_id = id + 1
@@ -36,7 +36,7 @@ module Kobako
36
36
  end
37
37
 
38
38
  def exposure(id)
39
- require_bound!(id)
39
+ require_bound(id)
40
40
  @entries[id]
41
41
  end
42
42
 
@@ -61,7 +61,7 @@ module Kobako
61
61
  # a surface the floor reads as ordinary Service behaviour. Raising here
62
62
  # keeps the rule at the single mint point, so it holds on both the
63
63
  # Service-return and the +#run+ host→guest auto-wrap paths.
64
- def reject_unwrappable!(object)
64
+ def reject_unwrappable(object)
65
65
  case object
66
66
  when Binding, Method, UnboundMethod, Module, Delegator
67
67
  # Delegator < BasicObject exposes no static +#class+, so name the
@@ -71,7 +71,7 @@ module Kobako
71
71
  end
72
72
  end
73
73
 
74
- def ensure_capacity!
74
+ def ensure_capacity
75
75
  cap = Kobako::Handle::MAX_ID
76
76
  return unless @next_id > cap
77
77
 
@@ -80,7 +80,7 @@ module Kobako
80
80
  "in a single invocation (limit #{cap})"
81
81
  end
82
82
 
83
- def require_bound!(id)
83
+ def require_bound(id)
84
84
  return if @entries.key?(id)
85
85
 
86
86
  raise SandboxError, "unknown Handle id: #{id.inspect}"
@@ -7,21 +7,13 @@ module Kobako
7
7
  def key = at
8
8
 
9
9
  # Why +other+ cannot be mounted beside this Mount, or +nil+: a path
10
- # means one Mount, and never sits inside another.
10
+ # means one Mount, as Docker refuses a duplicate mount point.
11
11
  def conflict(other)
12
- return "path #{at.inspect} is already mounted" if other.at == at
13
-
14
- "path #{at.inspect} nests with the mounted path #{other.at.inspect}" if nested?(other.at)
12
+ "path #{at.inspect} is already mounted" if other.at == at
15
13
  end
16
14
 
17
15
  # The Mount as the native side reads it.
18
16
  def to_native = [at, access, filesystem, policy]
19
-
20
- private
21
-
22
- def nested?(other)
23
- [at, other].include?("/") || at.start_with?("#{other}/") || other.start_with?("#{at}/")
24
- end
25
17
  end
26
18
  end
27
19
  end
@@ -16,7 +16,7 @@ module Kobako
16
16
 
17
17
  # The binding records +object+'s Exposure as it stands now.
18
18
  def bind(path, object)
19
- path_str = validate_path!(path)
19
+ path_str = validate_path(path)
20
20
  if reserved?(path_str)
21
21
  raise ArgumentError,
22
22
  "Service path #{path_str} is in the reserved #{RESERVED_NAMESPACE} namespace"
@@ -46,7 +46,7 @@ module Kobako
46
46
 
47
47
  private
48
48
 
49
- def validate_path!(path)
49
+ def validate_path(path)
50
50
  path_str = path.to_s
51
51
  segments = path_str.split("::", -1)
52
52
  return path_str if !segments.empty? && segments.all? { |seg| CONSTANT_NAME.match?(seg) }
@@ -40,17 +40,17 @@ module Kobako
40
40
  if binary
41
41
  raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
42
42
 
43
- register_binary!(binary)
43
+ register_binary(binary)
44
44
  else
45
- register_source!(code, name)
45
+ register_source(code, name)
46
46
  end
47
47
  end
48
48
 
49
49
  private
50
50
 
51
- def register_source!(code, name)
52
- code, name = ensure_source_args!(code, name)
53
- body = Catalog.to_utf8!(code, "code")
51
+ def register_source(code, name)
52
+ code, name = ensure_source_args(code, name)
53
+ body = Catalog.to_utf8(code, "code")
54
54
 
55
55
  name_sym = normalize_name(name)
56
56
  if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
@@ -63,7 +63,7 @@ module Kobako
63
63
 
64
64
  # The +code:+ type check runs first so an explicit +code: nil+ reads
65
65
  # as a type error rather than a missing keyword.
66
- def ensure_source_args!(code, name)
66
+ def ensure_source_args(code, name)
67
67
  raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
68
68
  raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
69
69
  raise ArgumentError, "missing keyword: name:" if name.nil?
@@ -73,7 +73,7 @@ module Kobako
73
73
 
74
74
  # Kept as bytes whatever encoding the caller tagged them with, since
75
75
  # the guest reads them as bytecode rather than text.
76
- def register_binary!(bytes)
76
+ def register_binary(bytes)
77
77
  raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
78
78
 
79
79
  @entries << Snippet::Bytecode.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
@@ -13,7 +13,7 @@ module Kobako
13
13
  # The native side carries a registered text only as UTF-8, so text read
14
14
  # as bytes is re-tagged, and bytes that are not UTF-8 are refused at
15
15
  # registration rather than failing every invocation.
16
- def self.to_utf8!(text, what)
16
+ def self.to_utf8(text, what)
17
17
  utf8 = text.dup.force_encoding(Encoding::UTF_8)
18
18
  raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
19
19