kobako 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,372 @@
1
+ //! The differential probe: every call kobako answers, made on descriptors
2
+ //! 0 to 3 against kobako's linker and against plain wasmtime-wasi preview1,
3
+ //! must leave the same errno, the same guest memory, and the same captures.
4
+ //!
5
+ //! It guards each wasmtime-wasi upgrade: a change in what preview1 answers
6
+ //! shows up here rather than in a guest. The deliberate differences — a
7
+ //! clock wait under the hermetic rung — are witnessed in `poll`.
8
+
9
+ mod mount;
10
+
11
+ use std::sync::OnceLock;
12
+
13
+ use wasmtime::{Linker, Module, Store};
14
+ use wasmtime_wasi::p1::{self, WasiP1Ctx};
15
+ use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
16
+ use wasmtime_wasi::WasiCtxBuilder;
17
+
18
+ use crate::cache::shared_engine;
19
+ use crate::config::Config;
20
+ use crate::imports;
21
+ use crate::invocation::Invocation;
22
+ use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
23
+ use crate::wasi::Wasi;
24
+ use kobako_runtime::profile::Profile;
25
+
26
+ /// The Preamble bytes stdin carries after its length prefix.
27
+ const PREAMBLE: &[u8] = b"hello";
28
+
29
+ /// Every export takes a descriptor and answers the call's errno. Memory
30
+ /// holds an `iovec` at 64 over 512..528, an empty one at 72, a `ciovec` at 80 over "hello" at
31
+ /// 600, and the paths "a" and "b" at 700 and 702; results land at 128 and
32
+ /// 256, subscriptions are built at 800 and events written at 896.
33
+ const PROBE_WAT: &str = r#"
34
+ (module
35
+ (import "wasi_snapshot_preview1" "fd_advise" (func $fd_advise (param i32 i64 i64 i32) (result i32)))
36
+ (import "wasi_snapshot_preview1" "fd_allocate" (func $fd_allocate (param i32 i64 i64) (result i32)))
37
+ (import "wasi_snapshot_preview1" "fd_close" (func $fd_close (param i32) (result i32)))
38
+ (import "wasi_snapshot_preview1" "fd_datasync" (func $fd_datasync (param i32) (result i32)))
39
+ (import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fd_fdstat_get (param i32 i32) (result i32)))
40
+ (import "wasi_snapshot_preview1" "fd_fdstat_set_flags" (func $fd_fdstat_set_flags (param i32 i32) (result i32)))
41
+ (import "wasi_snapshot_preview1" "fd_fdstat_set_rights" (func $fd_fdstat_set_rights (param i32 i64 i64) (result i32)))
42
+ (import "wasi_snapshot_preview1" "fd_filestat_get" (func $fd_filestat_get (param i32 i32) (result i32)))
43
+ (import "wasi_snapshot_preview1" "fd_filestat_set_size" (func $fd_filestat_set_size (param i32 i64) (result i32)))
44
+ (import "wasi_snapshot_preview1" "fd_filestat_set_times" (func $fd_filestat_set_times (param i32 i64 i64 i32) (result i32)))
45
+ (import "wasi_snapshot_preview1" "fd_pread" (func $fd_pread (param i32 i32 i32 i64 i32) (result i32)))
46
+ (import "wasi_snapshot_preview1" "fd_prestat_get" (func $fd_prestat_get (param i32 i32) (result i32)))
47
+ (import "wasi_snapshot_preview1" "fd_prestat_dir_name" (func $fd_prestat_dir_name (param i32 i32 i32) (result i32)))
48
+ (import "wasi_snapshot_preview1" "fd_pwrite" (func $fd_pwrite (param i32 i32 i32 i64 i32) (result i32)))
49
+ (import "wasi_snapshot_preview1" "fd_read" (func $fd_read (param i32 i32 i32 i32) (result i32)))
50
+ (import "wasi_snapshot_preview1" "fd_readdir" (func $fd_readdir (param i32 i32 i32 i64 i32) (result i32)))
51
+ (import "wasi_snapshot_preview1" "fd_renumber" (func $fd_renumber (param i32 i32) (result i32)))
52
+ (import "wasi_snapshot_preview1" "fd_seek" (func $fd_seek (param i32 i64 i32 i32) (result i32)))
53
+ (import "wasi_snapshot_preview1" "fd_sync" (func $fd_sync (param i32) (result i32)))
54
+ (import "wasi_snapshot_preview1" "fd_tell" (func $fd_tell (param i32 i32) (result i32)))
55
+ (import "wasi_snapshot_preview1" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
56
+ (import "wasi_snapshot_preview1" "path_create_directory" (func $path_create_directory (param i32 i32 i32) (result i32)))
57
+ (import "wasi_snapshot_preview1" "path_filestat_get" (func $path_filestat_get (param i32 i32 i32 i32 i32) (result i32)))
58
+ (import "wasi_snapshot_preview1" "path_filestat_set_times" (func $path_filestat_set_times (param i32 i32 i32 i32 i64 i64 i32) (result i32)))
59
+ (import "wasi_snapshot_preview1" "path_link" (func $path_link (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
60
+ (import "wasi_snapshot_preview1" "path_open" (func $path_open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
61
+ (import "wasi_snapshot_preview1" "path_readlink" (func $path_readlink (param i32 i32 i32 i32 i32 i32) (result i32)))
62
+ (import "wasi_snapshot_preview1" "path_remove_directory" (func $path_remove_directory (param i32 i32 i32) (result i32)))
63
+ (import "wasi_snapshot_preview1" "path_rename" (func $path_rename (param i32 i32 i32 i32 i32 i32) (result i32)))
64
+ (import "wasi_snapshot_preview1" "path_symlink" (func $path_symlink (param i32 i32 i32 i32 i32) (result i32)))
65
+ (import "wasi_snapshot_preview1" "path_unlink_file" (func $path_unlink_file (param i32 i32 i32) (result i32)))
66
+ (import "wasi_snapshot_preview1" "poll_oneoff" (func $poll_oneoff (param i32 i32 i32 i32) (result i32)))
67
+ (import "wasi_snapshot_preview1" "sock_accept" (func $sock_accept (param i32 i32 i32) (result i32)))
68
+ (import "wasi_snapshot_preview1" "sock_recv" (func $sock_recv (param i32 i32 i32 i32 i32 i32) (result i32)))
69
+ (import "wasi_snapshot_preview1" "sock_send" (func $sock_send (param i32 i32 i32 i32 i32) (result i32)))
70
+ (import "wasi_snapshot_preview1" "sock_shutdown" (func $sock_shutdown (param i32 i32) (result i32)))
71
+ (memory (export "memory") 1)
72
+ (data (i32.const 64) "\00\02\00\00\10\00\00\00")
73
+ (data (i32.const 72) "\00\02\00\00\00\00\00\00")
74
+ (data (i32.const 80) "\58\02\00\00\05\00\00\00")
75
+ (data (i32.const 88) "\00\00\02\00\10\00\00\00")
76
+ (data (i32.const 600) "hello")
77
+ (data (i32.const 700) "a\00b")
78
+
79
+ ;; One subscription at 800: userdata 7, the tag, and the descriptor.
80
+ (func $subscribe (param $tag i32) (param $fd i32)
81
+ (i64.store (i32.const 800) (i64.const 7))
82
+ (i32.store8 (i32.const 808) (local.get $tag))
83
+ (i32.store (i32.const 816) (local.get $fd)))
84
+
85
+ (func (export "fd_advise") (param i32) (result i32) (call $fd_advise (local.get 0) (i64.const 0) (i64.const 0) (i32.const 0)))
86
+ (func (export "fd_allocate") (param i32) (result i32) (call $fd_allocate (local.get 0) (i64.const 0) (i64.const 0)))
87
+ (func (export "fd_close") (param i32) (result i32) (call $fd_close (local.get 0)))
88
+ (func (export "fd_datasync") (param i32) (result i32) (call $fd_datasync (local.get 0)))
89
+ (func (export "fd_fdstat_get") (param i32) (result i32) (call $fd_fdstat_get (local.get 0) (i32.const 256)))
90
+ (func (export "fd_fdstat_set_flags") (param i32) (result i32) (call $fd_fdstat_set_flags (local.get 0) (i32.const 0)))
91
+ (func (export "fd_fdstat_set_rights") (param i32) (result i32) (call $fd_fdstat_set_rights (local.get 0) (i64.const 0) (i64.const 0)))
92
+ (func (export "fd_filestat_get") (param i32) (result i32) (call $fd_filestat_get (local.get 0) (i32.const 256)))
93
+ (func (export "fd_filestat_set_size") (param i32) (result i32) (call $fd_filestat_set_size (local.get 0) (i64.const 0)))
94
+ (func (export "fd_filestat_set_times") (param i32) (result i32) (call $fd_filestat_set_times (local.get 0) (i64.const 0) (i64.const 0) (i32.const 0)))
95
+ (func (export "fd_pread") (param i32) (result i32) (call $fd_pread (local.get 0) (i32.const 64) (i32.const 1) (i64.const 0) (i32.const 128)))
96
+ (func (export "fd_prestat_get") (param i32) (result i32) (call $fd_prestat_get (local.get 0) (i32.const 256)))
97
+ (func (export "fd_prestat_dir_name") (param i32) (result i32) (call $fd_prestat_dir_name (local.get 0) (i32.const 512) (i32.const 16)))
98
+ (func (export "fd_pwrite") (param i32) (result i32) (call $fd_pwrite (local.get 0) (i32.const 80) (i32.const 1) (i64.const 0) (i32.const 128)))
99
+ (func (export "fd_read") (param i32) (result i32) (call $fd_read (local.get 0) (i32.const 64) (i32.const 1) (i32.const 128)))
100
+ (func (export "fd_readdir") (param i32) (result i32) (call $fd_readdir (local.get 0) (i32.const 512) (i32.const 64) (i64.const 0) (i32.const 128)))
101
+ (func (export "fd_renumber") (param i32) (result i32) (call $fd_renumber (local.get 0) (local.get 0)))
102
+ (func (export "fd_seek") (param i32) (result i32) (call $fd_seek (local.get 0) (i64.const 0) (i32.const 0) (i32.const 128)))
103
+ (func (export "fd_sync") (param i32) (result i32) (call $fd_sync (local.get 0)))
104
+ (func (export "fd_tell") (param i32) (result i32) (call $fd_tell (local.get 0) (i32.const 128)))
105
+ (func (export "fd_write") (param i32) (result i32) (call $fd_write (local.get 0) (i32.const 80) (i32.const 1) (i32.const 128)))
106
+ (func (export "path_create_directory") (param i32) (result i32) (call $path_create_directory (local.get 0) (i32.const 700) (i32.const 1)))
107
+ (func (export "path_filestat_get") (param i32) (result i32) (call $path_filestat_get (local.get 0) (i32.const 0) (i32.const 700) (i32.const 1) (i32.const 256)))
108
+ (func (export "path_filestat_set_times") (param i32) (result i32) (call $path_filestat_set_times (local.get 0) (i32.const 0) (i32.const 700) (i32.const 1) (i64.const 0) (i64.const 0) (i32.const 0)))
109
+ (func (export "path_link") (param i32) (result i32) (call $path_link (local.get 0) (i32.const 0) (i32.const 700) (i32.const 1) (local.get 0) (i32.const 702) (i32.const 1)))
110
+ (func (export "path_open") (param i32) (result i32) (call $path_open (local.get 0) (i32.const 0) (i32.const 700) (i32.const 1) (i32.const 0) (i64.const 0) (i64.const 0) (i32.const 0) (i32.const 128)))
111
+ (func (export "path_readlink") (param i32) (result i32) (call $path_readlink (local.get 0) (i32.const 700) (i32.const 1) (i32.const 512) (i32.const 16) (i32.const 128)))
112
+ (func (export "path_remove_directory") (param i32) (result i32) (call $path_remove_directory (local.get 0) (i32.const 700) (i32.const 1)))
113
+ (func (export "path_rename") (param i32) (result i32) (call $path_rename (local.get 0) (i32.const 700) (i32.const 1) (local.get 0) (i32.const 702) (i32.const 1)))
114
+ (func (export "path_symlink") (param i32) (result i32) (call $path_symlink (i32.const 700) (i32.const 1) (local.get 0) (i32.const 702) (i32.const 1)))
115
+ (func (export "path_unlink_file") (param i32) (result i32) (call $path_unlink_file (local.get 0) (i32.const 700) (i32.const 1)))
116
+ (func (export "poll_oneoff_read") (param i32) (result i32)
117
+ (call $subscribe (i32.const 1) (local.get 0))
118
+ (call $poll_oneoff (i32.const 800) (i32.const 896) (i32.const 1) (i32.const 128)))
119
+ (func (export "poll_oneoff_write") (param i32) (result i32)
120
+ (call $subscribe (i32.const 2) (local.get 0))
121
+ (call $poll_oneoff (i32.const 800) (i32.const 896) (i32.const 1) (i32.const 128)))
122
+ ;; A lone relative wait of no time on the clock the descriptor numbers.
123
+ (func (export "poll_oneoff_clock") (param i32) (result i32)
124
+ (call $subscribe (i32.const 0) (local.get 0))
125
+ (call $poll_oneoff (i32.const 800) (i32.const 896) (i32.const 1) (i32.const 128)))
126
+ (func (export "poll_oneoff_none") (param i32) (result i32)
127
+ (call $poll_oneoff (i32.const 800) (i32.const 896) (i32.const 0) (i32.const 128)))
128
+ (func (export "sock_accept") (param i32) (result i32) (call $sock_accept (local.get 0) (i32.const 0) (i32.const 128)))
129
+ (func (export "sock_recv") (param i32) (result i32) (call $sock_recv (local.get 0) (i32.const 64) (i32.const 1) (i32.const 0) (i32.const 128) (i32.const 132)))
130
+ (func (export "sock_send") (param i32) (result i32) (call $sock_send (local.get 0) (i32.const 80) (i32.const 1) (i32.const 0) (i32.const 128)))
131
+ (func (export "sock_shutdown") (param i32) (result i32) (call $sock_shutdown (local.get 0) (i32.const 0)))
132
+
133
+ ;; A buffer past the one page of memory, through the iovec at 88.
134
+ (func (export "fd_read_far") (param i32) (result i32) (call $fd_read (local.get 0) (i32.const 88) (i32.const 1) (i32.const 128)))
135
+ (func (export "fd_pread_far") (param i32) (result i32) (call $fd_pread (local.get 0) (i32.const 88) (i32.const 1) (i64.const 0) (i32.const 128)))
136
+ (func (export "fd_write_far") (param i32) (result i32) (call $fd_write (local.get 0) (i32.const 88) (i32.const 1) (i32.const 128)))
137
+ (func (export "fd_pwrite_far") (param i32) (result i32) (call $fd_pwrite (local.get 0) (i32.const 88) (i32.const 1) (i64.const 0) (i32.const 128)))
138
+ (func (export "fd_readdir_far") (param i32) (result i32) (call $fd_readdir (local.get 0) (i32.const 0x20000) (i32.const 0) (i64.const 0) (i32.const 128)))
139
+ (func (export "path_open_far") (param i32) (result i32) (call $path_open (local.get 0) (i32.const 0) (i32.const 0x20000) (i32.const 1) (i32.const 0) (i64.const 0) (i64.const 0) (i32.const 0) (i32.const 128)))
140
+ ;; A path longer than one call's hostcall fuel.
141
+ (func (export "path_open_huge") (param i32) (result i32) (call $path_open (local.get 0) (i32.const 0) (i32.const 700) (i32.const 0x0c800000) (i32.const 0) (i64.const 0) (i64.const 0) (i32.const 0) (i32.const 128)))
142
+ ;; Each argument one past what the witx defines.
143
+ (func (export "fd_advise_undefined") (param i32) (result i32) (call $fd_advise (local.get 0) (i64.const 0) (i64.const 0) (i32.const 6)))
144
+ (func (export "fd_seek_undefined") (param i32) (result i32) (call $fd_seek (local.get 0) (i64.const 0) (i32.const 3) (i32.const 128)))
145
+ (func (export "fd_fdstat_set_flags_undefined") (param i32) (result i32) (call $fd_fdstat_set_flags (local.get 0) (i32.const 0x20)))
146
+ (func (export "fd_fdstat_set_rights_undefined") (param i32) (result i32) (call $fd_fdstat_set_rights (local.get 0) (i64.const 0x40000000) (i64.const 0)))
147
+ (func (export "fd_filestat_set_times_undefined") (param i32) (result i32) (call $fd_filestat_set_times (local.get 0) (i64.const 0) (i64.const 0) (i32.const 0x10)))
148
+ (func (export "path_filestat_get_undefined") (param i32) (result i32) (call $path_filestat_get (local.get 0) (i32.const 2) (i32.const 700) (i32.const 1) (i32.const 256)))
149
+ (func (export "path_open_undefined") (param i32) (result i32) (call $path_open (local.get 0) (i32.const 0) (i32.const 700) (i32.const 1) (i32.const 0x10) (i64.const 0) (i64.const 0) (i32.const 0) (i32.const 128)))
150
+ (func (export "path_link_undefined") (param i32) (result i32) (call $path_link (local.get 0) (i32.const 2) (i32.const 700) (i32.const 1) (local.get 0) (i32.const 702) (i32.const 1)))
151
+ (func (export "sock_recv_undefined") (param i32) (result i32) (call $sock_recv (local.get 0) (i32.const 64) (i32.const 1) (i32.const 4) (i32.const 128) (i32.const 132)))
152
+ (func (export "sock_shutdown_undefined") (param i32) (result i32) (call $sock_shutdown (local.get 0) (i32.const 4)))
153
+
154
+ (func (export "fd_read_empty") (param i32) (result i32) (call $fd_read (local.get 0) (i32.const 72) (i32.const 1) (i32.const 128)))
155
+ (func (export "fd_write_empty") (param i32) (result i32) (call $fd_write (local.get 0) (i32.const 72) (i32.const 1) (i32.const 128)))
156
+ (func (export "close_twice") (param i32) (result i32)
157
+ (drop (call $fd_close (local.get 0)))
158
+ (call $fd_close (local.get 0)))
159
+ (func (export "renumber_onto_stdin") (param i32) (result i32)
160
+ (drop (call $fd_renumber (local.get 0) (i32.const 0)))
161
+ (call $fd_fdstat_get (i32.const 0) (i32.const 256)))
162
+ (func (export "read_twice") (param i32) (result i32)
163
+ (drop (call $fd_read (local.get 0) (i32.const 64) (i32.const 1) (i32.const 128)))
164
+ (call $fd_read (local.get 0) (i32.const 64) (i32.const 1) (i32.const 132)))
165
+ (func (export "write_twice") (param i32) (result i32)
166
+ (drop (call $fd_write (local.get 0) (i32.const 80) (i32.const 1) (i32.const 128)))
167
+ (call $fd_write (local.get 0) (i32.const 80) (i32.const 1) (i32.const 132))))
168
+ "#;
169
+
170
+ const CALLS: &[&str] = &[
171
+ "fd_advise",
172
+ "fd_allocate",
173
+ "fd_close",
174
+ "fd_datasync",
175
+ "fd_fdstat_get",
176
+ "fd_fdstat_set_flags",
177
+ "fd_fdstat_set_rights",
178
+ "fd_filestat_get",
179
+ "fd_filestat_set_size",
180
+ "fd_filestat_set_times",
181
+ "fd_pread",
182
+ "fd_prestat_get",
183
+ "fd_prestat_dir_name",
184
+ "fd_pwrite",
185
+ "fd_read",
186
+ "fd_readdir",
187
+ "fd_renumber",
188
+ "fd_seek",
189
+ "fd_sync",
190
+ "fd_tell",
191
+ "fd_write",
192
+ "path_create_directory",
193
+ "path_filestat_get",
194
+ "path_filestat_set_times",
195
+ "path_link",
196
+ "path_open",
197
+ "path_readlink",
198
+ "path_remove_directory",
199
+ "path_rename",
200
+ "path_symlink",
201
+ "path_unlink_file",
202
+ "poll_oneoff_read",
203
+ "poll_oneoff_write",
204
+ "poll_oneoff_clock",
205
+ "poll_oneoff_none",
206
+ "sock_accept",
207
+ "sock_recv",
208
+ "sock_send",
209
+ "sock_shutdown",
210
+ "fd_read_far",
211
+ "fd_pread_far",
212
+ "fd_write_far",
213
+ "fd_pwrite_far",
214
+ "fd_readdir_far",
215
+ "path_open_far",
216
+ "path_open_huge",
217
+ "fd_advise_undefined",
218
+ "fd_seek_undefined",
219
+ "fd_fdstat_set_flags_undefined",
220
+ "fd_fdstat_set_rights_undefined",
221
+ "fd_filestat_set_times_undefined",
222
+ "path_filestat_get_undefined",
223
+ "path_open_undefined",
224
+ "path_link_undefined",
225
+ "sock_recv_undefined",
226
+ "sock_shutdown_undefined",
227
+ "fd_read_empty",
228
+ "fd_write_empty",
229
+ "close_twice",
230
+ "renumber_onto_stdin",
231
+ "read_twice",
232
+ "write_twice",
233
+ ];
234
+
235
+ /// What one call leaves behind; `None` for a call that trapped.
236
+ #[derive(Debug, PartialEq)]
237
+ struct Observation {
238
+ errno: Option<i32>,
239
+ memory: Vec<u8>,
240
+ stdout: Vec<u8>,
241
+ stderr: Vec<u8>,
242
+ }
243
+
244
+ /// A capped Channel exercises the pipe filling mid-write.
245
+ const CHANNEL_LIMITS: [Option<usize>; 2] = [None, Some(3)];
246
+
247
+ fn stdin_bytes() -> Vec<u8> {
248
+ let mut bytes = (PREAMBLE.len() as u32).to_be_bytes().to_vec();
249
+ bytes.extend_from_slice(PREAMBLE);
250
+ bytes
251
+ }
252
+
253
+ fn observe<T>(
254
+ mut store: Store<T>,
255
+ linker: &Linker<T>,
256
+ call: &str,
257
+ fd: i32,
258
+ ) -> (Observation, Store<T>) {
259
+ static MODULE: OnceLock<Module> = OnceLock::new();
260
+ let module = MODULE
261
+ .get_or_init(|| Module::new(store.engine(), PROBE_WAT).expect("probe module must compile"));
262
+ let instance = linker
263
+ .instantiate(&mut store, module)
264
+ .expect("probe module must instantiate");
265
+ let errno = instance
266
+ .get_typed_func::<i32, i32>(&mut store, call)
267
+ .expect("probe export must resolve")
268
+ .call(&mut store, fd)
269
+ .ok();
270
+ let memory = instance
271
+ .get_memory(&mut store, "memory")
272
+ .expect("probe exports its memory")
273
+ .data(&store)[..1024]
274
+ .to_vec();
275
+ let observation = Observation {
276
+ errno,
277
+ memory,
278
+ stdout: Vec::new(),
279
+ stderr: Vec::new(),
280
+ };
281
+ (observation, store)
282
+ }
283
+
284
+ fn through_kobako(call: &str, fd: i32, limit: Option<usize>) -> Observation {
285
+ let engine = shared_engine().expect("shared engine must be constructible");
286
+ let config = Config {
287
+ timeout: None,
288
+ setup_timeout: None,
289
+ memory_limit: None,
290
+ stdout_limit: limit,
291
+ stderr_limit: limit,
292
+ open_file_limit: None,
293
+ profile: Profile::Hermetic,
294
+ };
295
+ let wasi = Wasi::for_invocation(&config, PREAMBLE, &[]).expect("WASI context must build");
296
+ let mut store = Store::new(engine, Invocation::new(None, wasi));
297
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
298
+ let linker = imports::linker().expect("host imports must link");
299
+ let (mut observation, store) = observe(store, &linker, call, fd);
300
+ observation.stdout = store.data().wasi().stdout_bytes();
301
+ observation.stderr = store.data().wasi().stderr_bytes();
302
+ observation
303
+ }
304
+
305
+ /// Plain wasmtime-wasi preview1 over the same stdin and pipes.
306
+ struct Oracle {
307
+ ctx: WasiP1Ctx,
308
+ stdout: MemoryOutputPipe,
309
+ stderr: MemoryOutputPipe,
310
+ }
311
+
312
+ fn through_wasmtime_wasi(call: &str, fd: i32, limit: Option<usize>) -> Observation {
313
+ let engine = shared_engine().expect("shared engine must be constructible");
314
+ let stdout = MemoryOutputPipe::new(crate::capture::pipe_capacity(limit));
315
+ let stderr = MemoryOutputPipe::new(crate::capture::pipe_capacity(limit));
316
+ let ctx = WasiCtxBuilder::new()
317
+ .stdin(MemoryInputPipe::new(stdin_bytes()))
318
+ .stdout(stdout.clone())
319
+ .stderr(stderr.clone())
320
+ .build_p1();
321
+ let mut store = Store::new(
322
+ engine,
323
+ Oracle {
324
+ ctx,
325
+ stdout,
326
+ stderr,
327
+ },
328
+ );
329
+ store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
330
+ let mut linker = Linker::new(engine);
331
+ p1::add_to_linker_sync(&mut linker, |oracle: &mut Oracle| &mut oracle.ctx)
332
+ .expect("preview1 must link");
333
+ let (mut observation, store) = observe(store, &linker, call, fd);
334
+ observation.stdout = store.data().stdout.contents().to_vec();
335
+ observation.stderr = store.data().stderr.contents().to_vec();
336
+ observation
337
+ }
338
+
339
+ // Descriptor 3 is not open, so each call can only answer by the order its
340
+ // checks run in.
341
+ // @behavior RT-138
342
+ #[test]
343
+ fn an_argument_preview1_cannot_hold_answers_in_wasmtime_wasis_order() {
344
+ let errno = |call| through_kobako(call, 3, None).errno;
345
+ assert_eq!(
346
+ (
347
+ errno("fd_seek_undefined"),
348
+ errno("fd_read_far"),
349
+ errno("path_open_huge"),
350
+ ),
351
+ (None, Some(8), Some(48)),
352
+ "through kobako's preview1 imports on a descriptor not open, an undefined whence must \
353
+ trap, a buffer past memory must answer EBADF before it is bounded, and a path past the \
354
+ call's fuel must answer ENOMEM before it is read, as wasmtime-wasi answers each"
355
+ );
356
+ }
357
+
358
+ #[test]
359
+ fn every_descriptor_call_answers_as_wasmtime_wasi_preview1() {
360
+ for limit in CHANNEL_LIMITS {
361
+ for call in CALLS {
362
+ for fd in 0..=3 {
363
+ assert_eq!(
364
+ through_kobako(call, fd, limit),
365
+ through_wasmtime_wasi(call, fd, limit),
366
+ "{call} on descriptor {fd} with Channels capped at {limit:?} through \
367
+ kobako's preview1 imports must answer as wasmtime-wasi's preview1 does"
368
+ );
369
+ }
370
+ }
371
+ }
372
+ }
@@ -0,0 +1,267 @@
1
+ //! What a preview1 call answers when it does not succeed: an error code
2
+ //! the guest reads, or a trap that ends the invocation.
3
+
4
+ use kobako_runtime::filesystem::FilesystemError;
5
+ use kobako_runtime::mount::Denial;
6
+
7
+ /// One preview1 `errno`, numbered as the witx `$errno` enum numbers it.
8
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
9
+ #[repr(u16)]
10
+ pub(crate) enum Errno {
11
+ TooBig = 1,
12
+ Acces = 2,
13
+ Addrinuse = 3,
14
+ Addrnotavail = 4,
15
+ Afnosupport = 5,
16
+ Again = 6,
17
+ Already = 7,
18
+ Badf = 8,
19
+ Badmsg = 9,
20
+ Busy = 10,
21
+ Canceled = 11,
22
+ Child = 12,
23
+ Connaborted = 13,
24
+ Connrefused = 14,
25
+ Connreset = 15,
26
+ Deadlk = 16,
27
+ Destaddrreq = 17,
28
+ Dom = 18,
29
+ Dquot = 19,
30
+ Exist = 20,
31
+ Fault = 21,
32
+ Fbig = 22,
33
+ Hostunreach = 23,
34
+ Idrm = 24,
35
+ Ilseq = 25,
36
+ Inprogress = 26,
37
+ Intr = 27,
38
+ Inval = 28,
39
+ Io = 29,
40
+ Isconn = 30,
41
+ Isdir = 31,
42
+ Loop = 32,
43
+ Mfile = 33,
44
+ Mlink = 34,
45
+ Msgsize = 35,
46
+ Multihop = 36,
47
+ Nametoolong = 37,
48
+ Netdown = 38,
49
+ Netreset = 39,
50
+ Netunreach = 40,
51
+ Nfile = 41,
52
+ Nobufs = 42,
53
+ Nodev = 43,
54
+ Noent = 44,
55
+ Noexec = 45,
56
+ Nolck = 46,
57
+ Nolink = 47,
58
+ Nomem = 48,
59
+ Nomsg = 49,
60
+ Noprotoopt = 50,
61
+ Nospc = 51,
62
+ Nosys = 52,
63
+ Notconn = 53,
64
+ Notdir = 54,
65
+ Notempty = 55,
66
+ Notrecoverable = 56,
67
+ Notsock = 57,
68
+ Notsup = 58,
69
+ Notty = 59,
70
+ Nxio = 60,
71
+ Overflow = 61,
72
+ Ownerdead = 62,
73
+ Perm = 63,
74
+ Pipe = 64,
75
+ Proto = 65,
76
+ Protonosupport = 66,
77
+ Prototype = 67,
78
+ Range = 68,
79
+ Rofs = 69,
80
+ Spipe = 70,
81
+ Srch = 71,
82
+ Stale = 72,
83
+ Timedout = 73,
84
+ Txtbsy = 74,
85
+ Xdev = 75,
86
+ Notcapable = 76,
87
+ }
88
+
89
+ impl Errno {
90
+ /// The code a POSIX error name stands for: the witx case, `E`
91
+ /// prefixed and upper-cased. `ENOTCAPABLE` is WASI's own.
92
+ fn from_posix_name(name: &str) -> Option<Self> {
93
+ Some(match name {
94
+ "E2BIG" => Self::TooBig,
95
+ "EACCES" => Self::Acces,
96
+ "EADDRINUSE" => Self::Addrinuse,
97
+ "EADDRNOTAVAIL" => Self::Addrnotavail,
98
+ "EAFNOSUPPORT" => Self::Afnosupport,
99
+ "EAGAIN" => Self::Again,
100
+ "EALREADY" => Self::Already,
101
+ "EBADF" => Self::Badf,
102
+ "EBADMSG" => Self::Badmsg,
103
+ "EBUSY" => Self::Busy,
104
+ "ECANCELED" => Self::Canceled,
105
+ "ECHILD" => Self::Child,
106
+ "ECONNABORTED" => Self::Connaborted,
107
+ "ECONNREFUSED" => Self::Connrefused,
108
+ "ECONNRESET" => Self::Connreset,
109
+ "EDEADLK" => Self::Deadlk,
110
+ "EDESTADDRREQ" => Self::Destaddrreq,
111
+ "EDOM" => Self::Dom,
112
+ "EDQUOT" => Self::Dquot,
113
+ "EEXIST" => Self::Exist,
114
+ "EFAULT" => Self::Fault,
115
+ "EFBIG" => Self::Fbig,
116
+ "EHOSTUNREACH" => Self::Hostunreach,
117
+ "EIDRM" => Self::Idrm,
118
+ "EILSEQ" => Self::Ilseq,
119
+ "EINPROGRESS" => Self::Inprogress,
120
+ "EINTR" => Self::Intr,
121
+ "EINVAL" => Self::Inval,
122
+ "EIO" => Self::Io,
123
+ "EISCONN" => Self::Isconn,
124
+ "EISDIR" => Self::Isdir,
125
+ "ELOOP" => Self::Loop,
126
+ "EMFILE" => Self::Mfile,
127
+ "EMLINK" => Self::Mlink,
128
+ "EMSGSIZE" => Self::Msgsize,
129
+ "EMULTIHOP" => Self::Multihop,
130
+ "ENAMETOOLONG" => Self::Nametoolong,
131
+ "ENETDOWN" => Self::Netdown,
132
+ "ENETRESET" => Self::Netreset,
133
+ "ENETUNREACH" => Self::Netunreach,
134
+ "ENFILE" => Self::Nfile,
135
+ "ENOBUFS" => Self::Nobufs,
136
+ "ENODEV" => Self::Nodev,
137
+ "ENOENT" => Self::Noent,
138
+ "ENOEXEC" => Self::Noexec,
139
+ "ENOLCK" => Self::Nolck,
140
+ "ENOLINK" => Self::Nolink,
141
+ "ENOMEM" => Self::Nomem,
142
+ "ENOMSG" => Self::Nomsg,
143
+ "ENOPROTOOPT" => Self::Noprotoopt,
144
+ "ENOSPC" => Self::Nospc,
145
+ "ENOSYS" => Self::Nosys,
146
+ "ENOTCONN" => Self::Notconn,
147
+ "ENOTDIR" => Self::Notdir,
148
+ "ENOTEMPTY" => Self::Notempty,
149
+ "ENOTRECOVERABLE" => Self::Notrecoverable,
150
+ "ENOTSOCK" => Self::Notsock,
151
+ "ENOTSUP" => Self::Notsup,
152
+ "ENOTTY" => Self::Notty,
153
+ "ENXIO" => Self::Nxio,
154
+ "EOVERFLOW" => Self::Overflow,
155
+ "EOWNERDEAD" => Self::Ownerdead,
156
+ "EPERM" => Self::Perm,
157
+ "EPIPE" => Self::Pipe,
158
+ "EPROTO" => Self::Proto,
159
+ "EPROTONOSUPPORT" => Self::Protonosupport,
160
+ "EPROTOTYPE" => Self::Prototype,
161
+ "ERANGE" => Self::Range,
162
+ "EROFS" => Self::Rofs,
163
+ "ESPIPE" => Self::Spipe,
164
+ "ESRCH" => Self::Srch,
165
+ "ESTALE" => Self::Stale,
166
+ "ETIMEDOUT" => Self::Timedout,
167
+ "ETXTBSY" => Self::Txtbsy,
168
+ "EXDEV" => Self::Xdev,
169
+ "ENOTCAPABLE" => Self::Notcapable,
170
+ _ => return None,
171
+ })
172
+ }
173
+ }
174
+
175
+ /// A call that did not succeed. preview1 traps rather than answering
176
+ /// for a pointer outside guest memory or an argument outside what the
177
+ /// witx defines, as wasmtime-wasi does.
178
+ pub(crate) enum Failure {
179
+ Errno(Errno),
180
+ Trap(wasmtime::Error),
181
+ }
182
+
183
+ impl Failure {
184
+ /// A trap saying what the guest handed over that preview1 cannot hold.
185
+ pub(crate) fn trap(message: &'static str) -> Self {
186
+ Self::Trap(wasmtime::Error::msg(message))
187
+ }
188
+ }
189
+
190
+ impl From<Errno> for Failure {
191
+ fn from(errno: Errno) -> Self {
192
+ Self::Errno(errno)
193
+ }
194
+ }
195
+
196
+ /// The code a Filesystem's failure reaches the guest as: the one its
197
+ /// POSIX name stands for, or `IO` for a failure without one.
198
+ impl From<FilesystemError> for Failure {
199
+ fn from(error: FilesystemError) -> Self {
200
+ Self::Errno(
201
+ error
202
+ .posix_name()
203
+ .and_then(Errno::from_posix_name)
204
+ .unwrap_or(Errno::Io),
205
+ )
206
+ }
207
+ }
208
+
209
+ /// A change beneath a read-only Mount is `ROFS`; a Policy's refusal is
210
+ /// `ACCES`.
211
+ impl From<Denial> for Failure {
212
+ fn from(denial: Denial) -> Self {
213
+ Self::Errno(match denial {
214
+ Denial::ReadOnly => Errno::Rofs,
215
+ Denial::Refused => Errno::Acces,
216
+ })
217
+ }
218
+ }
219
+
220
+ pub(crate) type Answer<T = ()> = Result<T, Failure>;
221
+
222
+ #[cfg(test)]
223
+ mod tests {
224
+ use super::*;
225
+
226
+ #[test]
227
+ fn each_filesystem_failure_reaches_the_guest_as_its_own_code() {
228
+ let cases = [
229
+ (FilesystemError::NotFound, 44),
230
+ (FilesystemError::AlreadyExists, 20),
231
+ (FilesystemError::NotADirectory, 54),
232
+ (FilesystemError::IsADirectory, 31),
233
+ (FilesystemError::DirectoryNotEmpty, 55),
234
+ (FilesystemError::PermissionDenied, 2),
235
+ (FilesystemError::ReadOnlyFilesystem, 69),
236
+ (FilesystemError::InvalidInput, 28),
237
+ (FilesystemError::Unsupported, 58),
238
+ (FilesystemError::StorageFull, 51),
239
+ (FilesystemError::FileTooLarge, 22),
240
+ (FilesystemError::CrossesDevices, 75),
241
+ (FilesystemError::Deadlock, 16),
242
+ (FilesystemError::FilesystemLoop, 32),
243
+ (FilesystemError::NotPermitted, 63),
244
+ (FilesystemError::BadDescriptor, 8),
245
+ (FilesystemError::ResourceBusy, 10),
246
+ (FilesystemError::IllegalSequence, 25),
247
+ (FilesystemError::Interrupted, 27),
248
+ (FilesystemError::TooManyOpenFiles, 33),
249
+ (FilesystemError::InvalidFilename, 37),
250
+ (FilesystemError::OutOfMemory, 48),
251
+ (FilesystemError::NotASocket, 57),
252
+ (FilesystemError::ValueTooLarge, 61),
253
+ (FilesystemError::NotSeekable, 70),
254
+ (FilesystemError::Other("x".into()), 29),
255
+ ];
256
+ for (error, code) in cases {
257
+ let label = format!("{error:?}");
258
+ let Failure::Errno(errno) = Failure::from(error) else {
259
+ panic!("{label} must answer an error code, not a trap");
260
+ };
261
+ assert_eq!(
262
+ errno as u16, code,
263
+ "{label} through Failure::from must reach the guest as preview1 errno {code}"
264
+ );
265
+ }
266
+ }
267
+ }