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,105 @@
|
|
|
1
|
+
//! A Mount: the Grant handing a Filesystem to the guest at an absolute
|
|
2
|
+
//! guest path, with the access it allows there and the Policy that may refuse
|
|
3
|
+
//! more, which the VFS asks through its Cred, so a Filesystem of a third
|
|
4
|
+
//! party's checks nothing.
|
|
5
|
+
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use crate::filesystem::Filesystem;
|
|
10
|
+
use crate::vfs::VfsMount;
|
|
11
|
+
|
|
12
|
+
/// What a Mount allows a guest beneath it, and what a Policy is asked to allow
|
|
13
|
+
/// for one path.
|
|
14
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
15
|
+
pub enum Access {
|
|
16
|
+
Read,
|
|
17
|
+
ReadWrite,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// The Host App's decision for one path a guest opens or changes, made after
|
|
21
|
+
/// a Mount's access allows it, so it can only refuse more.
|
|
22
|
+
pub trait Policy: Send + Sync {
|
|
23
|
+
/// Whether the guest may reach `path`, the absolute guest path, with
|
|
24
|
+
/// `access`.
|
|
25
|
+
fn allows(&self, path: &str, access: Access) -> bool;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// A Filesystem instance at an absolute guest path. Its path bounds what a
|
|
29
|
+
/// Policy can allow either way.
|
|
30
|
+
#[derive(Clone)]
|
|
31
|
+
pub struct Mount {
|
|
32
|
+
at: String,
|
|
33
|
+
policy: Option<Arc<dyn Policy>>,
|
|
34
|
+
mnt: Arc<VfsMount>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// A path no Mount can stand at: one that is not an absolute guest path.
|
|
38
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
39
|
+
pub struct MountPathError(String);
|
|
40
|
+
|
|
41
|
+
impl fmt::Display for MountPathError {
|
|
42
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
43
|
+
write!(
|
|
44
|
+
f,
|
|
45
|
+
"mount path must be an absolute guest path, got {:?}",
|
|
46
|
+
self.0
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl std::error::Error for MountPathError {}
|
|
52
|
+
|
|
53
|
+
/// An absolute guest path has no empty, `.`, or `..` component; the root
|
|
54
|
+
/// is `/` alone.
|
|
55
|
+
fn is_guest_path(at: &str) -> bool {
|
|
56
|
+
at == "/"
|
|
57
|
+
|| at.strip_prefix('/').is_some_and(|rest| {
|
|
58
|
+
rest.split('/')
|
|
59
|
+
.all(|part| !part.is_empty() && part != "." && part != "..")
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl Mount {
|
|
64
|
+
/// A Mount of `filesystem` at `at`, refused unless `at` is an absolute
|
|
65
|
+
/// guest path, which every path the Mount answers for is built on.
|
|
66
|
+
pub fn new(
|
|
67
|
+
at: impl Into<String>,
|
|
68
|
+
filesystem: Arc<Filesystem>,
|
|
69
|
+
access: Access,
|
|
70
|
+
) -> Result<Self, MountPathError> {
|
|
71
|
+
let at = at.into();
|
|
72
|
+
if !is_guest_path(&at) {
|
|
73
|
+
return Err(MountPathError(at));
|
|
74
|
+
}
|
|
75
|
+
Ok(Self {
|
|
76
|
+
at,
|
|
77
|
+
mnt: VfsMount::new(filesystem, access == Access::Read),
|
|
78
|
+
policy: None,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// This Mount with `policy` also judging each open and each change its
|
|
83
|
+
/// access allows; it can only refuse more, as an LSM can.
|
|
84
|
+
pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
|
|
85
|
+
Self {
|
|
86
|
+
policy: Some(policy),
|
|
87
|
+
..self
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The Filesystem as the VFS walks it.
|
|
92
|
+
pub fn mnt(&self) -> &Arc<VfsMount> {
|
|
93
|
+
&self.mnt
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// The Policy deciding beneath this Mount, once one was given.
|
|
97
|
+
pub(crate) fn policy(&self) -> Option<&Arc<dyn Policy>> {
|
|
98
|
+
self.policy.as_ref()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// The absolute guest path the Filesystem appears at.
|
|
102
|
+
pub fn at(&self) -> &str {
|
|
103
|
+
&self.at
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -6,13 +6,20 @@
|
|
|
6
6
|
//! actually built, and the frontend refuses a declaration below the
|
|
7
7
|
//! request — so the request is also the floor.
|
|
8
8
|
|
|
9
|
+
use std::hash::{BuildHasher, RandomState};
|
|
10
|
+
use std::sync::LazyLock;
|
|
11
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
12
|
+
|
|
13
|
+
use crate::vfs::{FileType, Identity, Kstat};
|
|
14
|
+
|
|
9
15
|
/// The ordered isolation ladder a runtime builds one rung of.
|
|
10
16
|
///
|
|
11
17
|
/// `Hermetic` is the full ambient-denial posture: ambient time and
|
|
12
|
-
/// entropy denied at the WASI boundary, no filesystem /
|
|
13
|
-
/// network reachability, and no host import beyond the wire ABI's
|
|
14
|
-
/// `__kobako_upcall`. `Permissive` differs in
|
|
15
|
-
/// live
|
|
18
|
+
/// entropy denied at the WASI boundary, no ambient filesystem /
|
|
19
|
+
/// environment / network reachability, and no host import beyond the wire ABI's
|
|
20
|
+
/// `__kobako_upcall`. `Permissive` differs only in the ambient
|
|
21
|
+
/// authority it grants — live time and entropy at the WASI boundary — and in
|
|
22
|
+
/// the times and inode numbers it shows of an entry. Ordering
|
|
16
23
|
/// follows strength (`Permissive < Hermetic`), so a floor check is a
|
|
17
24
|
/// plain comparison.
|
|
18
25
|
///
|
|
@@ -29,10 +36,139 @@ pub enum Profile {
|
|
|
29
36
|
Hermetic,
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
impl Profile {
|
|
40
|
+
/// The inode number a guest on this posture sees for `identity`. A
|
|
41
|
+
/// hermetic guest sees none, as a Filesystem's device number depends on
|
|
42
|
+
/// how many were made before it in the process. Any other sees a hash of
|
|
43
|
+
/// it under a key the process draws once, so it can tell entries apart
|
|
44
|
+
/// and cannot work back to a host's inode number.
|
|
45
|
+
pub fn guest_ino(self, identity: Option<Identity>) -> u64 {
|
|
46
|
+
static KEY: LazyLock<RandomState> = LazyLock::new(RandomState::new);
|
|
47
|
+
match (self, identity) {
|
|
48
|
+
(Self::Hermetic, _) | (_, None) => 0,
|
|
49
|
+
(Self::Permissive, Some(identity)) => KEY.hash_one(identity),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// `stat` as a guest on this posture sees it, whichever ABI a Wasm
|
|
54
|
+
/// Driver encodes it into. A Filesystem records the host's real time whatever
|
|
55
|
+
/// the invocation writing to it runs under, so one shared across
|
|
56
|
+
/// invocations keeps one record; a hermetic guest sees every time at the
|
|
57
|
+
/// Unix epoch, where its wall clock stands, so a time a write left behind
|
|
58
|
+
/// is no clock it can read. A link reports no size, so nothing of where it
|
|
59
|
+
/// points reaches the guest, and an entry is told apart only by what
|
|
60
|
+
/// `guest_ino` answers.
|
|
61
|
+
pub fn guest_kstat(self, stat: Kstat) -> Kstat {
|
|
62
|
+
let ino = self.guest_ino(Identity::of(&stat));
|
|
63
|
+
let masked = match self {
|
|
64
|
+
Self::Permissive => stat,
|
|
65
|
+
Self::Hermetic => {
|
|
66
|
+
let epoch = |time: Option<SystemTime>| time.map(|_| UNIX_EPOCH);
|
|
67
|
+
stat.with_times(epoch(stat.atime), epoch(stat.mtime), epoch(stat.ctime))
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
Kstat {
|
|
71
|
+
size: match masked.file_type {
|
|
72
|
+
FileType::Symlink => 0,
|
|
73
|
+
_ => masked.size,
|
|
74
|
+
},
|
|
75
|
+
..masked.with_identity(0, ino)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
32
80
|
#[cfg(test)]
|
|
33
81
|
mod tests {
|
|
34
82
|
use super::*;
|
|
35
83
|
|
|
84
|
+
// The Wasm Driver only encodes this, so another one shows the same, and a
|
|
85
|
+
// guest reads a link's size and a time here or nowhere.
|
|
86
|
+
// @behavior RT-173 RT-183
|
|
87
|
+
#[test]
|
|
88
|
+
fn a_guest_sees_no_size_for_a_link_and_only_what_its_posture_allows() {
|
|
89
|
+
let reported = Kstat::new(FileType::Symlink, 42)
|
|
90
|
+
.with_identity(7, 9)
|
|
91
|
+
.with_times(Some(SystemTime::now()), None, None);
|
|
92
|
+
let (hermetic, permissive) = (
|
|
93
|
+
Profile::Hermetic.guest_kstat(reported),
|
|
94
|
+
Profile::Permissive.guest_kstat(reported),
|
|
95
|
+
);
|
|
96
|
+
assert_eq!(
|
|
97
|
+
(hermetic.size, permissive.size, hermetic.ino, hermetic.atime),
|
|
98
|
+
(0, 0, 0, Some(UNIX_EPOCH)),
|
|
99
|
+
"Profile::guest_kstat of a link must report no size on any posture, and at the \
|
|
100
|
+
hermetic rung no inode number and every time at the Unix epoch"
|
|
101
|
+
);
|
|
102
|
+
assert!(
|
|
103
|
+
permissive.ino != 0 && permissive.ino != 9 && permissive.atime == reported.atime,
|
|
104
|
+
"Profile::guest_kstat at the permissive rung must hash the inode number and keep \
|
|
105
|
+
the time"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A device number depends on how many Filesystems the process made before,
|
|
110
|
+
// so the inode number a guest reads folds it in and the device stays out.
|
|
111
|
+
// @behavior RT-283
|
|
112
|
+
#[test]
|
|
113
|
+
fn a_guest_sees_one_device_for_every_entry() {
|
|
114
|
+
let reported = Kstat::new(FileType::RegFile, 1).with_identity(7, 9);
|
|
115
|
+
assert_eq!(
|
|
116
|
+
[Profile::Hermetic, Profile::Permissive]
|
|
117
|
+
.map(|profile| profile.guest_kstat(reported).dev),
|
|
118
|
+
[0, 0],
|
|
119
|
+
"Profile::guest_kstat of an entry a Filesystem reports on device 7 must report \
|
|
120
|
+
device 0 on either posture"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// An unkeyed hash answers what anyone can compute from a host's numbers,
|
|
125
|
+
// so the hash a guest reads must differ from it.
|
|
126
|
+
// @behavior RT-194
|
|
127
|
+
#[test]
|
|
128
|
+
fn a_guest_cannot_compute_the_inode_number_it_reads() {
|
|
129
|
+
use std::collections::hash_map::DefaultHasher;
|
|
130
|
+
use std::hash::{Hash, Hasher};
|
|
131
|
+
|
|
132
|
+
let reported = Kstat::new(FileType::RegFile, 0).with_identity(7, 9);
|
|
133
|
+
let identity = Identity::of(&reported);
|
|
134
|
+
let mut unkeyed = DefaultHasher::new();
|
|
135
|
+
identity
|
|
136
|
+
.expect("an inode number names an identity")
|
|
137
|
+
.hash(&mut unkeyed);
|
|
138
|
+
let seen = Profile::Permissive.guest_ino(identity);
|
|
139
|
+
assert_eq!(
|
|
140
|
+
seen,
|
|
141
|
+
Profile::Permissive.guest_ino(identity),
|
|
142
|
+
"Profile::guest_ino of one identity must answer one number within a process"
|
|
143
|
+
);
|
|
144
|
+
assert_ne!(
|
|
145
|
+
seen,
|
|
146
|
+
unkeyed.finish(),
|
|
147
|
+
"Profile::guest_ino at the permissive rung must not answer a hash anyone can compute \
|
|
148
|
+
from the device and inode numbers"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// A guest reads the count on any ABI, so the count starts in the runtime
|
|
153
|
+
// rather than in what a Wasm Driver encodes.
|
|
154
|
+
// @behavior RT-193
|
|
155
|
+
#[test]
|
|
156
|
+
fn an_entry_whose_filesystem_keeps_no_count_is_named_once() {
|
|
157
|
+
let reported = Kstat::new(FileType::RegFile, 0);
|
|
158
|
+
assert_eq!(
|
|
159
|
+
(
|
|
160
|
+
Profile::Hermetic.guest_kstat(reported).nlink,
|
|
161
|
+
Profile::Permissive.guest_kstat(reported).nlink,
|
|
162
|
+
Profile::Permissive
|
|
163
|
+
.guest_kstat(reported.with_nlink(3))
|
|
164
|
+
.nlink,
|
|
165
|
+
),
|
|
166
|
+
(1, 1, 3),
|
|
167
|
+
"Profile::guest_kstat of an entry reporting no count must show one name on each \
|
|
168
|
+
posture, and of one reporting three must show three"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
36
172
|
// @behavior RT-014
|
|
37
173
|
#[test]
|
|
38
174
|
fn a_declaration_satisfies_any_floor_at_or_below_it() {
|
|
@@ -13,6 +13,7 @@ use kobako_transport::envelope::Preamble;
|
|
|
13
13
|
|
|
14
14
|
use crate::dispatch::DispatchHandler;
|
|
15
15
|
use crate::error::InvokeError;
|
|
16
|
+
use crate::grant::{Grant, GrantKind};
|
|
16
17
|
use crate::profile::Profile;
|
|
17
18
|
use crate::snapshot::Snapshot;
|
|
18
19
|
|
|
@@ -49,13 +50,24 @@ pub enum Entry<'a> {
|
|
|
49
50
|
/// wrapper and GC-marking it; the runtime itself touches no frontend
|
|
50
51
|
/// value.
|
|
51
52
|
pub trait Runtime {
|
|
53
|
+
/// `grants` are this Invocation's, each of a kind `supports` declared;
|
|
54
|
+
/// the runtime presents them to the guest and keeps none afterwards.
|
|
52
55
|
fn invoke(
|
|
53
56
|
&self,
|
|
54
57
|
entry: Entry<'_>,
|
|
55
58
|
preamble: &Preamble,
|
|
56
59
|
handler: Option<Arc<dyn DispatchHandler>>,
|
|
60
|
+
grants: &[Grant],
|
|
57
61
|
) -> Result<Snapshot, InvokeError>;
|
|
58
62
|
|
|
63
|
+
/// Whether this runtime presents a Grant of `kind` to the guest. A
|
|
64
|
+
/// frontend refuses to register one it does not, before any guest runs.
|
|
65
|
+
/// The default supports none, so an engine that has not learned Grants
|
|
66
|
+
/// still serves every invocation without one.
|
|
67
|
+
fn supports(&self, _kind: GrantKind) -> bool {
|
|
68
|
+
false
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/// The isolation profile this runtime provides. Deliberately
|
|
60
72
|
/// without a default: every engine states its posture explicitly,
|
|
61
73
|
/// and a frontend refuses construction when the declaration falls
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
//! Who is running and how it waits, beneath every lock, as Linux's
|
|
2
|
+
//! `kernel/sched` is: a lock asks only for its wait to happen, so nothing
|
|
3
|
+
//! above it knows what waiting means to the frontend. A run past its deadline,
|
|
4
|
+
//! or one its frontend interrupted, stops waiting, as a fatal signal ends
|
|
5
|
+
//! Linux's killable waits, so one Sandbox holding a lock never holds another's
|
|
6
|
+
//! thread past that one's own end.
|
|
7
|
+
|
|
8
|
+
use std::cell::RefCell;
|
|
9
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
10
|
+
use std::sync::{Arc, OnceLock};
|
|
11
|
+
use std::thread::{self, ThreadId};
|
|
12
|
+
use std::time::{Duration, Instant};
|
|
13
|
+
|
|
14
|
+
/// How a thread waits for a lock another thread holds. A frontend whose
|
|
15
|
+
/// threads share a lock of their own, as Ruby's threads share the GVL,
|
|
16
|
+
/// gives that lock up for the wait, or a holder that needs it to finish
|
|
17
|
+
/// never could.
|
|
18
|
+
pub trait Blocking: Send + Sync {
|
|
19
|
+
/// Run `wait`, which returns once the lock is taken.
|
|
20
|
+
fn wait(&self, wait: &mut dyn FnMut());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static BLOCKING: OnceLock<Box<dyn Blocking>> = OnceLock::new();
|
|
24
|
+
|
|
25
|
+
/// Have every thread of this process wait through `blocking`, as a frontend
|
|
26
|
+
/// says once at startup; a later one is handed back, as
|
|
27
|
+
/// `register_filesystem` refuses a second. Without one a thread just waits.
|
|
28
|
+
pub fn register_blocking(blocking: Box<dyn Blocking>) -> Result<(), Box<dyn Blocking>> {
|
|
29
|
+
BLOCKING.set(blocking)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Run `wait` as this process's threads wait, as a lock's slow path calls
|
|
33
|
+
/// `schedule`.
|
|
34
|
+
pub(crate) fn schedule(wait: &mut dyn FnMut()) {
|
|
35
|
+
match BLOCKING.get() {
|
|
36
|
+
Some(blocking) => blocking.wait(wait),
|
|
37
|
+
None => wait(),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
thread_local! {
|
|
42
|
+
static CURRENT: ThreadId = thread::current().id();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Who is running, as Linux's `current`.
|
|
46
|
+
pub(crate) fn current() -> ThreadId {
|
|
47
|
+
CURRENT.with(|current| *current)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// A thread as its frontend may interrupt it from another thread, as a signal
|
|
51
|
+
/// reaches a Linux task.
|
|
52
|
+
#[derive(Clone, Default)]
|
|
53
|
+
pub struct Task {
|
|
54
|
+
interrupted: Arc<AtomicBool>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl Task {
|
|
58
|
+
/// The calling thread.
|
|
59
|
+
pub fn current() -> Self {
|
|
60
|
+
TASK.with(|task| task.borrow().task.clone())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Have every wait of the run this thread carries out stop, as a fatal
|
|
64
|
+
/// signal ends a killable wait; it holds until that run ends.
|
|
65
|
+
pub fn interrupt(&self) {
|
|
66
|
+
self.interrupted.store(true, Ordering::SeqCst);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// The task this thread carries out, as Linux's `current`, and when its run is
|
|
71
|
+
/// to stop.
|
|
72
|
+
#[derive(Default)]
|
|
73
|
+
struct Current {
|
|
74
|
+
task: Task,
|
|
75
|
+
deadline: Option<Instant>,
|
|
76
|
+
/// How many runs the thread is inside, one within another where a
|
|
77
|
+
/// Filesystem's own call starts another.
|
|
78
|
+
depth: usize,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
thread_local! {
|
|
82
|
+
static TASK: RefCell<Current> = RefCell::default();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Run `run` as a run that stops waiting once `deadline` passes or its thread
|
|
86
|
+
/// is interrupted, as a Linux task stops a killable wait on a fatal signal. A
|
|
87
|
+
/// run within another keeps the outer one's interrupt.
|
|
88
|
+
pub fn run_task<T>(deadline: Option<Instant>, run: impl FnOnce() -> T) -> T {
|
|
89
|
+
/// Puts back the deadline of the run around this one, however `run` ends,
|
|
90
|
+
/// and lets an interrupt go once no run is left.
|
|
91
|
+
struct Outer(Option<Instant>);
|
|
92
|
+
|
|
93
|
+
impl Drop for Outer {
|
|
94
|
+
fn drop(&mut self) {
|
|
95
|
+
TASK.with(|task| {
|
|
96
|
+
let mut running = task.borrow_mut();
|
|
97
|
+
running.deadline = self.0;
|
|
98
|
+
running.depth -= 1;
|
|
99
|
+
if running.depth == 0 {
|
|
100
|
+
running.task.interrupted.store(false, Ordering::SeqCst);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let _outer = Outer(TASK.with(|task| {
|
|
107
|
+
let mut running = task.borrow_mut();
|
|
108
|
+
running.depth += 1;
|
|
109
|
+
std::mem::replace(&mut running.deadline, deadline)
|
|
110
|
+
}));
|
|
111
|
+
run()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Whether the calling thread's run is to stop waiting, as Linux's
|
|
115
|
+
/// `fatal_signal_pending(current)` answers.
|
|
116
|
+
pub(crate) fn fatal_signal_pending() -> bool {
|
|
117
|
+
TASK.with(|task| {
|
|
118
|
+
let running = task.borrow();
|
|
119
|
+
running.task.interrupted.load(Ordering::SeqCst)
|
|
120
|
+
|| running.deadline.is_some_and(|at| Instant::now() >= at)
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// The most a wait sleeps before it looks again whether its run is to stop.
|
|
125
|
+
const MAX_SLEEP: Duration = Duration::from_millis(10);
|
|
126
|
+
|
|
127
|
+
/// How long a wait sleeps before it looks again whether its run is to stop:
|
|
128
|
+
/// never past its deadline.
|
|
129
|
+
pub(crate) fn sleep_slice() -> Duration {
|
|
130
|
+
TASK.with(|task| match task.borrow().deadline {
|
|
131
|
+
Some(at) => at.saturating_duration_since(Instant::now()).min(MAX_SLEEP),
|
|
132
|
+
None => MAX_SLEEP,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// A process has one Blocking, so a test counts only the waits of the thread
|
|
137
|
+
/// that asked to.
|
|
138
|
+
#[cfg(test)]
|
|
139
|
+
pub(crate) mod testing {
|
|
140
|
+
use std::cell::RefCell;
|
|
141
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
142
|
+
use std::sync::Arc;
|
|
143
|
+
use std::time::{Duration, Instant};
|
|
144
|
+
|
|
145
|
+
/// The longest a test waits for another thread, so what never arrives fails
|
|
146
|
+
/// the test instead of hanging the suite.
|
|
147
|
+
pub(crate) const MAX_WAIT: Duration = Duration::from_secs(10);
|
|
148
|
+
|
|
149
|
+
/// Wait until `arrived`, failing the test with `what` past `MAX_WAIT`.
|
|
150
|
+
pub(crate) fn wait_until(what: &str, mut arrived: impl FnMut() -> bool) {
|
|
151
|
+
let started = Instant::now();
|
|
152
|
+
while !arrived() {
|
|
153
|
+
assert!(
|
|
154
|
+
started.elapsed() < MAX_WAIT,
|
|
155
|
+
"{what} did not happen within {MAX_WAIT:?}"
|
|
156
|
+
);
|
|
157
|
+
std::thread::yield_now();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// What the thread `handle` runs answers, failing the test with `what` past
|
|
162
|
+
/// `MAX_WAIT`, so a thread stuck on a lock fails rather than hangs.
|
|
163
|
+
pub(crate) fn join<T>(what: &str, handle: std::thread::JoinHandle<T>) -> T {
|
|
164
|
+
wait_until(what, || handle.is_finished());
|
|
165
|
+
handle
|
|
166
|
+
.join()
|
|
167
|
+
.unwrap_or_else(|_| panic!("{what} must not panic"))
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// What the scoped thread `handle` runs answers, as `join` waits for one.
|
|
171
|
+
pub(crate) fn join_scoped<T>(what: &str, handle: std::thread::ScopedJoinHandle<'_, T>) -> T {
|
|
172
|
+
wait_until(what, || handle.is_finished());
|
|
173
|
+
handle
|
|
174
|
+
.join()
|
|
175
|
+
.unwrap_or_else(|_| panic!("{what} must not panic"))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// What `run` answers, each wait for a lock in it ending past `MAX_WAIT`, so a
|
|
179
|
+
/// test thread waiting on a lock itself fails rather than hangs.
|
|
180
|
+
pub(crate) fn within_max_wait<T>(run: impl FnOnce() -> T) -> T {
|
|
181
|
+
super::run_task(Some(Instant::now() + MAX_WAIT), run)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
use super::{Blocking, BLOCKING};
|
|
185
|
+
|
|
186
|
+
thread_local! {
|
|
187
|
+
static COUNTS: RefCell<Option<Arc<AtomicUsize>>> = const { RefCell::new(None) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
struct CountsWaits;
|
|
191
|
+
|
|
192
|
+
impl Blocking for CountsWaits {
|
|
193
|
+
fn wait(&self, wait: &mut dyn FnMut()) {
|
|
194
|
+
COUNTS.with(|counts| {
|
|
195
|
+
if let Some(counts) = counts.borrow().as_ref() {
|
|
196
|
+
counts.fetch_add(1, Ordering::SeqCst);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
wait();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// Counts each wait this thread makes through the registered Blocking.
|
|
204
|
+
pub(crate) fn count_waits() -> Arc<AtomicUsize> {
|
|
205
|
+
BLOCKING.get_or_init(|| Box::new(CountsWaits));
|
|
206
|
+
let counts = Arc::<AtomicUsize>::default();
|
|
207
|
+
COUNTS.with(|held| *held.borrow_mut() = Some(counts.clone()));
|
|
208
|
+
counts
|
|
209
|
+
}
|
|
210
|
+
}
|