kobako 0.28.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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
//! The differential probe beneath a Mount: each path call, made on the
|
|
2
|
+
//! same host directory through kobako's `Bind` Mount and through
|
|
3
|
+
//! wasmtime-wasi's own preopen, must leave the same errno, the same
|
|
4
|
+
//! descriptor, and the same directory behind. A listing of the Mount must
|
|
5
|
+
//! answer the same errno.
|
|
6
|
+
//!
|
|
7
|
+
//! kobako's inode numbers are its own, and its times differ from the host's
|
|
8
|
+
//! by when each call ran, so of what a stat writes only the filetype is
|
|
9
|
+
//! compared; the errno and the directory carry the rest of the decision.
|
|
10
|
+
//!
|
|
11
|
+
//! Where POSIX lets each host choose, wasmtime-wasi passes on the host's
|
|
12
|
+
//! own answer — unlinking a directory is `ISDIR` on Linux and `PERM` on
|
|
13
|
+
//! macOS. kobako answers one way on every host, the way Linux does, so
|
|
14
|
+
//! the probe holds where CI runs it.
|
|
15
|
+
//!
|
|
16
|
+
//! An open asking to sync, or for a directory it would truncate, is left out:
|
|
17
|
+
//! wasmtime-wasi refuses both as invalid or unsupported, where kobako answers
|
|
18
|
+
//! as Linux does, which `mount_view` holds to literal answers.
|
|
19
|
+
|
|
20
|
+
use std::fs;
|
|
21
|
+
use std::path::{Path, PathBuf};
|
|
22
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
23
|
+
use std::sync::{Arc, OnceLock};
|
|
24
|
+
|
|
25
|
+
use kobako_runtime::filesystem::Bind;
|
|
26
|
+
use kobako_runtime::filesystem::Filesystem;
|
|
27
|
+
use kobako_runtime::grant::Grant;
|
|
28
|
+
use kobako_runtime::mount::{Access, Mount};
|
|
29
|
+
use kobako_runtime::profile::Profile;
|
|
30
|
+
use kobako_runtime::vfs::PATH_MAX;
|
|
31
|
+
use wasmtime::{Linker, Module, Store};
|
|
32
|
+
use wasmtime_wasi::p1::{self, WasiP1Ctx};
|
|
33
|
+
use wasmtime_wasi::{FsPerms, WasiCtxBuilder};
|
|
34
|
+
|
|
35
|
+
use crate::cache::load_engine;
|
|
36
|
+
use crate::config::Config;
|
|
37
|
+
use crate::imports;
|
|
38
|
+
use crate::invocation::Invocation;
|
|
39
|
+
use crate::trap::NO_TIMEOUT_EPOCH_DELTA;
|
|
40
|
+
use crate::wasi::Wasi;
|
|
41
|
+
|
|
42
|
+
/// The guest path both sides preopen the host directory at.
|
|
43
|
+
const AT: &str = "/data";
|
|
44
|
+
|
|
45
|
+
/// Each export calls one path import on descriptor 3 with the path the
|
|
46
|
+
/// host placed at 1024, and the second path at 8192 where a call takes
|
|
47
|
+
/// two. An opened descriptor lands at 128, its fdstat at 320, and a stat
|
|
48
|
+
/// at 256.
|
|
49
|
+
const PROBE_WAT: &str = r#"
|
|
50
|
+
(module
|
|
51
|
+
(import "wasi_snapshot_preview1" "path_open" (func $open (param i32 i32 i32 i32 i32 i64 i64 i32 i32) (result i32)))
|
|
52
|
+
(import "wasi_snapshot_preview1" "path_filestat_get" (func $stat (param i32 i32 i32 i32 i32) (result i32)))
|
|
53
|
+
(import "wasi_snapshot_preview1" "path_create_directory" (func $mkdir (param i32 i32 i32) (result i32)))
|
|
54
|
+
(import "wasi_snapshot_preview1" "path_remove_directory" (func $rmdir (param i32 i32 i32) (result i32)))
|
|
55
|
+
(import "wasi_snapshot_preview1" "path_unlink_file" (func $unlink (param i32 i32 i32) (result i32)))
|
|
56
|
+
(import "wasi_snapshot_preview1" "path_rename" (func $rename (param i32 i32 i32 i32 i32 i32) (result i32)))
|
|
57
|
+
(import "wasi_snapshot_preview1" "fd_fdstat_get" (func $fdstat (param i32 i32) (result i32)))
|
|
58
|
+
(memory (export "memory") 1)
|
|
59
|
+
(func (export "open") (param $len i32) (param $oflags i32) (param $rights i64) (param $fdflags i32) (result i32)
|
|
60
|
+
(local $errno i32)
|
|
61
|
+
(local.set $errno (call $open (i32.const 3) (i32.const 1) (i32.const 1024) (local.get $len) (local.get $oflags)
|
|
62
|
+
(local.get $rights) (i64.const 0) (local.get $fdflags) (i32.const 128)))
|
|
63
|
+
(if (i32.eqz (local.get $errno))
|
|
64
|
+
(then (drop (call $fdstat (i32.load (i32.const 128)) (i32.const 320)))))
|
|
65
|
+
(local.get $errno))
|
|
66
|
+
(func (export "open_nofollow") (param $len i32) (result i32)
|
|
67
|
+
(call $open (i32.const 3) (i32.const 0) (i32.const 1024) (local.get $len) (i32.const 0)
|
|
68
|
+
(i64.const 2) (i64.const 0) (i32.const 0) (i32.const 128)))
|
|
69
|
+
(func (export "stat") (param $len i32) (result i32)
|
|
70
|
+
(call $stat (i32.const 3) (i32.const 1) (i32.const 1024) (local.get $len) (i32.const 256)))
|
|
71
|
+
(func (export "stat_nofollow") (param $len i32) (result i32)
|
|
72
|
+
(call $stat (i32.const 3) (i32.const 0) (i32.const 1024) (local.get $len) (i32.const 256)))
|
|
73
|
+
(func (export "mkdir") (param $len i32) (result i32)
|
|
74
|
+
(call $mkdir (i32.const 3) (i32.const 1024) (local.get $len)))
|
|
75
|
+
(func (export "rmdir") (param $len i32) (result i32)
|
|
76
|
+
(call $rmdir (i32.const 3) (i32.const 1024) (local.get $len)))
|
|
77
|
+
(func (export "unlink") (param $len i32) (result i32)
|
|
78
|
+
(call $unlink (i32.const 3) (i32.const 1024) (local.get $len)))
|
|
79
|
+
(func (export "rename") (param $len i32) (param $to i32) (result i32)
|
|
80
|
+
(call $rename (i32.const 3) (i32.const 1024) (local.get $len) (i32.const 3) (i32.const 8192) (local.get $to))))
|
|
81
|
+
"#;
|
|
82
|
+
|
|
83
|
+
const PATH_AT: usize = 1024;
|
|
84
|
+
const SECOND_PATH_AT: usize = 8192;
|
|
85
|
+
const OPENED_AT: usize = 128;
|
|
86
|
+
const FDSTAT_AT: usize = 320;
|
|
87
|
+
/// Where a stat's filetype lands, past its device and inode.
|
|
88
|
+
const FILETYPE_AT: usize = 256 + 16;
|
|
89
|
+
const FILETYPE_DIRECTORY: u8 = 3;
|
|
90
|
+
|
|
91
|
+
const OFLAGS_CREAT: i32 = 1 << 0;
|
|
92
|
+
const OFLAGS_DIRECTORY: i32 = 1 << 1;
|
|
93
|
+
const OFLAGS_EXCL: i32 = 1 << 2;
|
|
94
|
+
const OFLAGS_TRUNC: i32 = 1 << 3;
|
|
95
|
+
const FDFLAGS_NONBLOCK: i32 = 1 << 2;
|
|
96
|
+
const RIGHTS_FD_READ: i64 = 1 << 1;
|
|
97
|
+
const RIGHTS_FD_WRITE: i64 = 1 << 6;
|
|
98
|
+
|
|
99
|
+
/// Paths that stay beneath the root, climb above it, pass through what
|
|
100
|
+
/// does not exist or is no directory, or name the root itself.
|
|
101
|
+
const PATHS: &[&str] = &[
|
|
102
|
+
"a",
|
|
103
|
+
"l",
|
|
104
|
+
"missing",
|
|
105
|
+
"",
|
|
106
|
+
".",
|
|
107
|
+
"a/",
|
|
108
|
+
"g",
|
|
109
|
+
"l/",
|
|
110
|
+
"n/",
|
|
111
|
+
"g/",
|
|
112
|
+
"missing/",
|
|
113
|
+
"d",
|
|
114
|
+
"d/f",
|
|
115
|
+
"d/..",
|
|
116
|
+
"d/../a",
|
|
117
|
+
"a/..",
|
|
118
|
+
"missing/..",
|
|
119
|
+
"missing/../a",
|
|
120
|
+
"../a",
|
|
121
|
+
"d/../../a",
|
|
122
|
+
"/a",
|
|
123
|
+
"n/..",
|
|
124
|
+
"n/../f",
|
|
125
|
+
"n/.",
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
/// Paths at Linux's limits: a name of `NAME_MAX` bytes and one past it,
|
|
129
|
+
/// and a path that fits `PATH_MAX` with its terminating NUL and one that
|
|
130
|
+
/// does not.
|
|
131
|
+
fn paths_at_the_limits() -> Vec<String> {
|
|
132
|
+
let nested = |len: usize| {
|
|
133
|
+
let mut path = "d/".repeat(len / 2);
|
|
134
|
+
path.truncate(len - 1);
|
|
135
|
+
path.push('x');
|
|
136
|
+
path
|
|
137
|
+
};
|
|
138
|
+
vec![
|
|
139
|
+
"x".repeat(255),
|
|
140
|
+
"x".repeat(256),
|
|
141
|
+
format!("d/{}", "x".repeat(256)),
|
|
142
|
+
nested(4095),
|
|
143
|
+
nested(4096),
|
|
144
|
+
]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// One path call, as the probe makes it.
|
|
148
|
+
#[derive(Clone, Copy, Debug)]
|
|
149
|
+
enum Call {
|
|
150
|
+
Open {
|
|
151
|
+
oflags: i32,
|
|
152
|
+
rights: i64,
|
|
153
|
+
fdflags: i32,
|
|
154
|
+
},
|
|
155
|
+
/// Open the path to read, following no symbolic link at its end.
|
|
156
|
+
OpenNoFollow,
|
|
157
|
+
Stat,
|
|
158
|
+
/// Stat the path, following no symbolic link at its end.
|
|
159
|
+
StatNoFollow,
|
|
160
|
+
Mkdir,
|
|
161
|
+
Rmdir,
|
|
162
|
+
Unlink,
|
|
163
|
+
/// Rename the path to `b`.
|
|
164
|
+
RenameFrom,
|
|
165
|
+
/// Rename `a` to the path.
|
|
166
|
+
RenameTo,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const CALLS: &[Call] = &[
|
|
170
|
+
Call::Open {
|
|
171
|
+
oflags: 0,
|
|
172
|
+
rights: RIGHTS_FD_READ,
|
|
173
|
+
fdflags: 0,
|
|
174
|
+
},
|
|
175
|
+
Call::Open {
|
|
176
|
+
oflags: 0,
|
|
177
|
+
rights: RIGHTS_FD_WRITE,
|
|
178
|
+
fdflags: 0,
|
|
179
|
+
},
|
|
180
|
+
Call::Open {
|
|
181
|
+
oflags: OFLAGS_CREAT | OFLAGS_TRUNC,
|
|
182
|
+
rights: RIGHTS_FD_WRITE,
|
|
183
|
+
fdflags: 0,
|
|
184
|
+
},
|
|
185
|
+
Call::Open {
|
|
186
|
+
oflags: OFLAGS_CREAT | OFLAGS_EXCL,
|
|
187
|
+
rights: RIGHTS_FD_WRITE,
|
|
188
|
+
fdflags: 0,
|
|
189
|
+
},
|
|
190
|
+
Call::Open {
|
|
191
|
+
oflags: OFLAGS_DIRECTORY,
|
|
192
|
+
rights: RIGHTS_FD_READ,
|
|
193
|
+
fdflags: 0,
|
|
194
|
+
},
|
|
195
|
+
Call::Open {
|
|
196
|
+
oflags: OFLAGS_DIRECTORY | OFLAGS_CREAT,
|
|
197
|
+
rights: RIGHTS_FD_READ,
|
|
198
|
+
fdflags: 0,
|
|
199
|
+
},
|
|
200
|
+
Call::Open {
|
|
201
|
+
oflags: 0,
|
|
202
|
+
rights: RIGHTS_FD_READ,
|
|
203
|
+
fdflags: FDFLAGS_NONBLOCK,
|
|
204
|
+
},
|
|
205
|
+
Call::OpenNoFollow,
|
|
206
|
+
Call::Stat,
|
|
207
|
+
Call::StatNoFollow,
|
|
208
|
+
Call::Mkdir,
|
|
209
|
+
Call::Rmdir,
|
|
210
|
+
Call::Unlink,
|
|
211
|
+
Call::RenameFrom,
|
|
212
|
+
Call::RenameTo,
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
/// A host directory of the probe's own, holding `a`, `d/f`, `d/e/`, a
|
|
216
|
+
/// link `l` to `a`, and a link `n` to `d/e`, whose parent is not the
|
|
217
|
+
/// root, removed when it drops.
|
|
218
|
+
struct HostDir(PathBuf);
|
|
219
|
+
|
|
220
|
+
impl HostDir {
|
|
221
|
+
fn new() -> Self {
|
|
222
|
+
static NEXT: AtomicUsize = AtomicUsize::new(0);
|
|
223
|
+
let path = std::env::temp_dir().join(format!(
|
|
224
|
+
"kobako-differential-{}-{}",
|
|
225
|
+
std::process::id(),
|
|
226
|
+
NEXT.fetch_add(1, Ordering::Relaxed)
|
|
227
|
+
));
|
|
228
|
+
fs::create_dir_all(path.join("d/e")).expect("the host directory must be created");
|
|
229
|
+
fs::write(path.join("a"), "hello").expect("the host file must be written");
|
|
230
|
+
fs::write(path.join("d/f"), "inner").expect("the host file must be written");
|
|
231
|
+
#[cfg(unix)]
|
|
232
|
+
for (target, link) in [("a", "l"), ("d/e", "n"), ("nowhere", "g")] {
|
|
233
|
+
std::os::unix::fs::symlink(target, path.join(link))
|
|
234
|
+
.expect("the host link must be made");
|
|
235
|
+
}
|
|
236
|
+
Self(path)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Every entry beneath the directory, with a file's content.
|
|
240
|
+
fn tree(&self) -> Vec<(String, Option<Vec<u8>>)> {
|
|
241
|
+
fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, Option<Vec<u8>>)>) {
|
|
242
|
+
for entry in fs::read_dir(dir).expect("the host directory must list") {
|
|
243
|
+
let path = entry.expect("the host entry must read").path();
|
|
244
|
+
let name = path
|
|
245
|
+
.strip_prefix(root)
|
|
246
|
+
.expect("an entry lies beneath its root")
|
|
247
|
+
.to_string_lossy()
|
|
248
|
+
.into_owned();
|
|
249
|
+
if path.is_symlink() {
|
|
250
|
+
let target = fs::read_link(&path).expect("the host link must read");
|
|
251
|
+
out.push((
|
|
252
|
+
name,
|
|
253
|
+
Some(target.to_string_lossy().into_owned().into_bytes()),
|
|
254
|
+
));
|
|
255
|
+
} else if path.is_dir() {
|
|
256
|
+
out.push((name, None));
|
|
257
|
+
walk(root, &path, out);
|
|
258
|
+
} else {
|
|
259
|
+
out.push((
|
|
260
|
+
name,
|
|
261
|
+
Some(fs::read(&path).expect("the host file must read")),
|
|
262
|
+
));
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
let mut out = Vec::new();
|
|
267
|
+
walk(&self.0, &self.0, &mut out);
|
|
268
|
+
out.sort();
|
|
269
|
+
out
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
impl Drop for HostDir {
|
|
274
|
+
fn drop(&mut self) {
|
|
275
|
+
let _ = fs::remove_dir_all(&self.0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// What one call leaves behind; `None` for a call that trapped.
|
|
280
|
+
#[derive(Debug, PartialEq)]
|
|
281
|
+
struct Observation {
|
|
282
|
+
errno: Option<i32>,
|
|
283
|
+
opened: [u8; 4],
|
|
284
|
+
/// An opened descriptor's filetype and flags.
|
|
285
|
+
fdstat: [u8; 4],
|
|
286
|
+
/// Whether an opened descriptor's rights read and write it.
|
|
287
|
+
rights: u8,
|
|
288
|
+
filetype: u8,
|
|
289
|
+
tree: Vec<(String, Option<Vec<u8>>)>,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn observe<T>(
|
|
293
|
+
mut store: Store<T>,
|
|
294
|
+
linker: &Linker<T>,
|
|
295
|
+
call: Call,
|
|
296
|
+
path: &str,
|
|
297
|
+
host: &HostDir,
|
|
298
|
+
) -> Observation {
|
|
299
|
+
static MODULE: OnceLock<Module> = OnceLock::new();
|
|
300
|
+
let module = MODULE
|
|
301
|
+
.get_or_init(|| Module::new(store.engine(), PROBE_WAT).expect("probe module must compile"));
|
|
302
|
+
let instance = linker
|
|
303
|
+
.instantiate(&mut store, module)
|
|
304
|
+
.expect("probe module must instantiate");
|
|
305
|
+
let memory = instance
|
|
306
|
+
.get_memory(&mut store, "memory")
|
|
307
|
+
.expect("probe exports its memory");
|
|
308
|
+
let (first, second) = match call {
|
|
309
|
+
Call::RenameTo => ("a", path),
|
|
310
|
+
Call::RenameFrom => (path, "b"),
|
|
311
|
+
_ => (path, ""),
|
|
312
|
+
};
|
|
313
|
+
memory.data_mut(&mut store)[PATH_AT..PATH_AT + first.len()].copy_from_slice(first.as_bytes());
|
|
314
|
+
memory.data_mut(&mut store)[SECOND_PATH_AT..SECOND_PATH_AT + second.len()]
|
|
315
|
+
.copy_from_slice(second.as_bytes());
|
|
316
|
+
let len = first.len() as i32;
|
|
317
|
+
let errno = match call {
|
|
318
|
+
Call::Open {
|
|
319
|
+
oflags,
|
|
320
|
+
rights,
|
|
321
|
+
fdflags,
|
|
322
|
+
} => instance
|
|
323
|
+
.get_typed_func::<(i32, i32, i64, i32), i32>(&mut store, "open")
|
|
324
|
+
.and_then(|f| f.call(&mut store, (len, oflags, rights, fdflags))),
|
|
325
|
+
Call::RenameFrom | Call::RenameTo => instance
|
|
326
|
+
.get_typed_func::<(i32, i32), i32>(&mut store, "rename")
|
|
327
|
+
.and_then(|f| f.call(&mut store, (len, second.len() as i32))),
|
|
328
|
+
Call::OpenNoFollow
|
|
329
|
+
| Call::Stat
|
|
330
|
+
| Call::StatNoFollow
|
|
331
|
+
| Call::Mkdir
|
|
332
|
+
| Call::Rmdir
|
|
333
|
+
| Call::Unlink => {
|
|
334
|
+
let name = match call {
|
|
335
|
+
Call::OpenNoFollow => "open_nofollow",
|
|
336
|
+
Call::Stat => "stat",
|
|
337
|
+
Call::StatNoFollow => "stat_nofollow",
|
|
338
|
+
Call::Mkdir => "mkdir",
|
|
339
|
+
Call::Rmdir => "rmdir",
|
|
340
|
+
_ => "unlink",
|
|
341
|
+
};
|
|
342
|
+
instance
|
|
343
|
+
.get_typed_func::<i32, i32>(&mut store, name)
|
|
344
|
+
.and_then(|f| f.call(&mut store, len))
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
.ok();
|
|
348
|
+
let mut opened = [0; 4];
|
|
349
|
+
opened.copy_from_slice(&memory.data(&store)[OPENED_AT..OPENED_AT + 4]);
|
|
350
|
+
let data = memory.data(&store);
|
|
351
|
+
let mut fdstat = [0; 4];
|
|
352
|
+
fdstat.copy_from_slice(&data[FDSTAT_AT..FDSTAT_AT + 4]);
|
|
353
|
+
Observation {
|
|
354
|
+
errno,
|
|
355
|
+
opened,
|
|
356
|
+
fdstat,
|
|
357
|
+
rights: data[FDSTAT_AT + 8] & (RIGHTS_FD_READ | RIGHTS_FD_WRITE) as u8,
|
|
358
|
+
filetype: data[FILETYPE_AT],
|
|
359
|
+
tree: host.tree(),
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
fn through_kobako(call: Call, path: &str) -> Observation {
|
|
364
|
+
let host = HostDir::new();
|
|
365
|
+
let (store, linker) = kobako_preopen(&host);
|
|
366
|
+
observe(store, &linker, call, path, &host)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
fn through_wasmtime_wasi(call: Call, path: &str) -> Observation {
|
|
370
|
+
let host = HostDir::new();
|
|
371
|
+
let (store, linker) = wasmtime_wasi_preopen(&host);
|
|
372
|
+
observe(store, &linker, call, path, &host)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// `host` behind a kobako `Bind` Mount at `AT`.
|
|
376
|
+
fn kobako_preopen(host: &HostDir) -> (Store<Invocation>, Linker<Invocation>) {
|
|
377
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
378
|
+
let config = Config {
|
|
379
|
+
timeout: None,
|
|
380
|
+
setup_timeout: None,
|
|
381
|
+
memory_limit: None,
|
|
382
|
+
stdout_limit: None,
|
|
383
|
+
stderr_limit: None,
|
|
384
|
+
open_file_limit: None,
|
|
385
|
+
profile: Profile::Hermetic,
|
|
386
|
+
security: Vec::new(),
|
|
387
|
+
};
|
|
388
|
+
let directory = Bind::new(&host.0).expect("the host directory must open");
|
|
389
|
+
let grant = Grant::Mount(
|
|
390
|
+
Mount::new(
|
|
391
|
+
AT,
|
|
392
|
+
Filesystem::new(Arc::new(directory)).expect("the instance must stand"),
|
|
393
|
+
Access::ReadWrite,
|
|
394
|
+
)
|
|
395
|
+
.expect("AT is a guest path"),
|
|
396
|
+
);
|
|
397
|
+
let wasi =
|
|
398
|
+
Wasi::for_invocation(&config, Vec::new(), &[grant]).expect("WASI context must build");
|
|
399
|
+
let mut store = Store::new(engine, Invocation::new(None, wasi));
|
|
400
|
+
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
401
|
+
let linker = imports::linker().expect("host imports must link");
|
|
402
|
+
(store, linker)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// `host` behind wasmtime-wasi's own preopen at `AT`.
|
|
406
|
+
fn wasmtime_wasi_preopen(host: &HostDir) -> (Store<WasiP1Ctx>, Linker<WasiP1Ctx>) {
|
|
407
|
+
let engine = load_engine().expect("shared engine must be constructible");
|
|
408
|
+
let ctx = WasiCtxBuilder::new()
|
|
409
|
+
.preopened_dir(&host.0, AT, FsPerms::ReadWrite)
|
|
410
|
+
.expect("the host directory must preopen")
|
|
411
|
+
.build_p1();
|
|
412
|
+
let mut store = Store::new(engine, ctx);
|
|
413
|
+
store.set_epoch_deadline(NO_TIMEOUT_EPOCH_DELTA);
|
|
414
|
+
let mut linker = Linker::new(engine);
|
|
415
|
+
p1::add_to_linker_sync(&mut linker, |ctx: &mut WasiP1Ctx| ctx).expect("preview1 must link");
|
|
416
|
+
(store, linker)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const ERRNO_BUSY: i32 = 10;
|
|
420
|
+
const ERRNO_EXIST: i32 = 20;
|
|
421
|
+
const ERRNO_INVAL: i32 = 28;
|
|
422
|
+
const ERRNO_ISDIR: i32 = 31;
|
|
423
|
+
const ERRNO_NAMETOOLONG: i32 = 37;
|
|
424
|
+
const ERRNO_NOENT: i32 = 44;
|
|
425
|
+
const ERRNO_NOTDIR: i32 = 54;
|
|
426
|
+
const ERRNO_NOTEMPTY: i32 = 55;
|
|
427
|
+
const ERRNO_NOTSUP: i32 = 58;
|
|
428
|
+
const ERRNO_PERM: i32 = 63;
|
|
429
|
+
const ERRNO_NOTCAPABLE: i32 = 76;
|
|
430
|
+
|
|
431
|
+
/// Whether `path` takes a `..`, or ends in a `.`, right after one of the
|
|
432
|
+
/// host directory's links.
|
|
433
|
+
fn steps_out_of_a_link(path: &str) -> bool {
|
|
434
|
+
let mut components = path.split('/');
|
|
435
|
+
matches!(components.next(), Some("l" | "n"))
|
|
436
|
+
&& match components.next() {
|
|
437
|
+
Some("..") => true,
|
|
438
|
+
Some(".") => components.next().is_none(),
|
|
439
|
+
_ => false,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Whether `call` acts on the last name of `path` rather than land on it, a
|
|
444
|
+
/// `.` or `..` there taking the walk nowhere, as Linux's `LOOKUP_PARENT` leaves
|
|
445
|
+
/// it.
|
|
446
|
+
fn acts_on_a_dot(call: Call, path: &str) -> bool {
|
|
447
|
+
matches!(
|
|
448
|
+
call,
|
|
449
|
+
Call::Mkdir | Call::Rmdir | Call::Unlink | Call::RenameFrom | Call::RenameTo
|
|
450
|
+
) && matches!(path.rsplit('/').next(), Some("." | ".."))
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/// Whether `path` reaches above the root it is resolved beneath.
|
|
454
|
+
fn escapes(path: &str) -> bool {
|
|
455
|
+
let mut depth = 0usize;
|
|
456
|
+
path.starts_with('/')
|
|
457
|
+
|| path.split('/').any(|component| match component {
|
|
458
|
+
"" | "." => false,
|
|
459
|
+
".." => match depth.checked_sub(1) {
|
|
460
|
+
Some(up) => {
|
|
461
|
+
depth = up;
|
|
462
|
+
false
|
|
463
|
+
}
|
|
464
|
+
None => true,
|
|
465
|
+
},
|
|
466
|
+
_ => {
|
|
467
|
+
depth += 1;
|
|
468
|
+
false
|
|
469
|
+
}
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// What kobako answers where it differs from wasmtime-wasi on purpose. A path
|
|
474
|
+
/// above the root is `NOTCAPABLE`, the code preview1 names for a
|
|
475
|
+
/// capability the guest was never handed, where wasmtime-wasi passes on
|
|
476
|
+
/// cap-std's refusal as `PERM`. A path past `PATH_MAX` is `NAMETOOLONG`
|
|
477
|
+
/// as Linux answers the whole path, where cap-std resolves it one name
|
|
478
|
+
/// at a time and never hands Linux the whole of it. A stat following no
|
|
479
|
+
/// link of a path named with a trailing `/` answers as Linux's `lstat`,
|
|
480
|
+
/// which follows a link there, where cap-std's shortcut for one name
|
|
481
|
+
/// drops the `/` and finds the file or the link. A `rmdir` of a link
|
|
482
|
+
/// named with a trailing `/` is `NOTDIR` as Linux answers it, where
|
|
483
|
+
/// cap-std's split turns the `/` into `.` and answers `INVAL`. A `rmdir`
|
|
484
|
+
/// of a path ending in `..` is `NOTEMPTY` as Linux answers it, where
|
|
485
|
+
/// cap-std answers `INVAL` as it does for a final `.`. A `..` or
|
|
486
|
+
/// a final `.` after a link is `LOOP` and changes nothing, where cap-std
|
|
487
|
+
/// steps out of where the link points; a call refused before its path
|
|
488
|
+
/// resolves answers alike.
|
|
489
|
+
fn predict(call: Call, path: &str, mut oracle: Observation) -> Observation {
|
|
490
|
+
let refused = |errno| Observation {
|
|
491
|
+
errno: Some(errno),
|
|
492
|
+
opened: [0; 4],
|
|
493
|
+
fdstat: [0; 4],
|
|
494
|
+
rights: 0,
|
|
495
|
+
filetype: 0,
|
|
496
|
+
tree: HostDir::new().tree(),
|
|
497
|
+
};
|
|
498
|
+
let refused_first = matches!(oracle.errno, Some(ERRNO_BUSY | ERRNO_INVAL | ERRNO_NOTSUP));
|
|
499
|
+
if steps_out_of_a_link(path) && acts_on_a_dot(call, path) {
|
|
500
|
+
match call {
|
|
501
|
+
Call::Mkdir => return refused(ERRNO_EXIST),
|
|
502
|
+
Call::Unlink => return refused(ERRNO_ISDIR),
|
|
503
|
+
_ => {}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if escapes(path) && oracle.errno == Some(ERRNO_PERM) {
|
|
507
|
+
oracle.errno = Some(ERRNO_NOTCAPABLE);
|
|
508
|
+
}
|
|
509
|
+
// A directory keeps the flags it opened with, as Linux's `F_GETFL`
|
|
510
|
+
// reports them, where wasmtime-wasi reports none on one.
|
|
511
|
+
if let Call::Open { fdflags, .. } = call {
|
|
512
|
+
if oracle.errno == Some(0) && oracle.fdstat[0] == FILETYPE_DIRECTORY {
|
|
513
|
+
oracle.fdstat[2] = fdflags as u8;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// A trailing `/` makes Linux's `lstat` follow the link at the end,
|
|
517
|
+
// where cap-std leaves it, so the host's own `lstat` answers.
|
|
518
|
+
if matches!(call, Call::StatNoFollow) && path.ends_with('/') {
|
|
519
|
+
let host = HostDir::new();
|
|
520
|
+
match fs::symlink_metadata(host.0.join(path)) {
|
|
521
|
+
Ok(metadata) if metadata.is_dir() => {
|
|
522
|
+
if oracle.errno == Some(0) {
|
|
523
|
+
oracle.filetype = FILETYPE_DIRECTORY;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
found => {
|
|
527
|
+
oracle.errno = Some(match found {
|
|
528
|
+
Err(err) if err.kind() == std::io::ErrorKind::NotFound => ERRNO_NOENT,
|
|
529
|
+
_ => ERRNO_NOTDIR,
|
|
530
|
+
});
|
|
531
|
+
oracle.filetype = 0;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Linux's `unlink` and `rmdir` follow no link at the end, so the name
|
|
536
|
+
// itself answers a trailing `/`, where cap-std follows the link first.
|
|
537
|
+
if matches!(call, Call::Unlink | Call::Rmdir)
|
|
538
|
+
&& path.ends_with('/')
|
|
539
|
+
&& oracle.errno.is_some_and(|errno| errno != 0)
|
|
540
|
+
{
|
|
541
|
+
let host = HostDir::new();
|
|
542
|
+
match (
|
|
543
|
+
call,
|
|
544
|
+
fs::symlink_metadata(host.0.join(path.trim_end_matches('/'))),
|
|
545
|
+
) {
|
|
546
|
+
(_, Ok(metadata)) if !metadata.is_dir() => oracle.errno = Some(ERRNO_NOTDIR),
|
|
547
|
+
(Call::Unlink, Ok(_)) => oracle.errno = Some(ERRNO_ISDIR),
|
|
548
|
+
(Call::Unlink, Err(_)) => oracle.errno = Some(ERRNO_NOENT),
|
|
549
|
+
_ => {}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if matches!(call, Call::Rmdir)
|
|
553
|
+
&& path.trim_end_matches('/').ends_with("..")
|
|
554
|
+
&& oracle.errno == Some(ERRNO_INVAL)
|
|
555
|
+
{
|
|
556
|
+
oracle.errno = Some(ERRNO_NOTEMPTY);
|
|
557
|
+
}
|
|
558
|
+
if path.len() >= PATH_MAX && !refused_first {
|
|
559
|
+
oracle.errno = Some(ERRNO_NAMETOOLONG);
|
|
560
|
+
}
|
|
561
|
+
oracle
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
#[test]
|
|
565
|
+
#[cfg_attr(
|
|
566
|
+
not(target_os = "linux"),
|
|
567
|
+
ignore = "wasmtime-wasi passes on the host's own answer, and kobako answers as Linux does"
|
|
568
|
+
)]
|
|
569
|
+
fn every_path_call_beneath_a_mount_answers_as_a_wasmtime_wasi_preopen() {
|
|
570
|
+
let paths: Vec<String> = PATHS
|
|
571
|
+
.iter()
|
|
572
|
+
.map(|&path| path.to_string())
|
|
573
|
+
.chain(paths_at_the_limits())
|
|
574
|
+
.collect();
|
|
575
|
+
let differing: Vec<String> = CALLS
|
|
576
|
+
.iter()
|
|
577
|
+
.flat_map(|&call| paths.iter().map(move |path| (call, path.as_str())))
|
|
578
|
+
.filter_map(|(call, path)| {
|
|
579
|
+
let kobako = through_kobako(call, path);
|
|
580
|
+
let oracle = predict(call, path, through_wasmtime_wasi(call, path));
|
|
581
|
+
(kobako != oracle).then(|| format!("{call:?} {path:?}: {kobako:?} != {oracle:?}"))
|
|
582
|
+
})
|
|
583
|
+
.collect();
|
|
584
|
+
assert!(
|
|
585
|
+
differing.is_empty(),
|
|
586
|
+
"each path call on a host directory through a kobako Bind Mount must answer as \
|
|
587
|
+
wasmtime-wasi's preopen of it does:\n{}",
|
|
588
|
+
differing.join("\n")
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Only Linux keeps a name that is not UTF-8; macOS refuses to make one.
|
|
593
|
+
#[test]
|
|
594
|
+
#[cfg(target_os = "linux")]
|
|
595
|
+
fn a_listing_holding_a_name_that_is_not_utf8_answers_as_a_wasmtime_wasi_preopen() {
|
|
596
|
+
use std::os::unix::ffi::OsStrExt;
|
|
597
|
+
|
|
598
|
+
/// Lists descriptor 3 into the 256 bytes at 1024 from the first entry,
|
|
599
|
+
/// the bytes used landing at 128.
|
|
600
|
+
const READDIR_WAT: &str = r#"
|
|
601
|
+
(module
|
|
602
|
+
(import "wasi_snapshot_preview1" "fd_readdir" (func $readdir (param i32 i32 i32 i64 i32) (result i32)))
|
|
603
|
+
(memory (export "memory") 1)
|
|
604
|
+
(func (export "readdir") (result i32)
|
|
605
|
+
(call $readdir (i32.const 3) (i32.const 1024) (i32.const 256) (i64.const 0) (i32.const 128))))
|
|
606
|
+
"#;
|
|
607
|
+
|
|
608
|
+
/// The errno the listing answers; `None` for a call that trapped.
|
|
609
|
+
fn readdir_errno<T>(mut store: Store<T>, linker: &Linker<T>) -> Option<i32> {
|
|
610
|
+
let module = Module::new(store.engine(), READDIR_WAT).expect("probe module must compile");
|
|
611
|
+
let instance = linker
|
|
612
|
+
.instantiate(&mut store, &module)
|
|
613
|
+
.expect("probe module must instantiate");
|
|
614
|
+
instance
|
|
615
|
+
.get_typed_func::<(), i32>(&mut store, "readdir")
|
|
616
|
+
.and_then(|f| f.call(&mut store, ()))
|
|
617
|
+
.ok()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
let host = HostDir::new();
|
|
621
|
+
fs::write(host.0.join(std::ffi::OsStr::from_bytes(b"\xff")), "")
|
|
622
|
+
.expect("the host file must be written");
|
|
623
|
+
let (store, linker) = kobako_preopen(&host);
|
|
624
|
+
let kobako = readdir_errno(store, &linker);
|
|
625
|
+
let (store, linker) = wasmtime_wasi_preopen(&host);
|
|
626
|
+
let oracle = readdir_errno(store, &linker);
|
|
627
|
+
assert_eq!(
|
|
628
|
+
kobako, oracle,
|
|
629
|
+
"fd_readdir on a host directory holding a name that is not UTF-8 through a kobako \
|
|
630
|
+
Bind Mount must answer as wasmtime-wasi's preopen of it does"
|
|
631
|
+
);
|
|
632
|
+
}
|