From 770307ae072ef3db74df9e36488d3aa40068dcec Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 16 Oct 2026 20:11:32 -0400 Subject: [PATCH] Fix chroot for wasi 0.3 Properly handle async operations inside sync methods that return a future. Formalizes the jail root and path resolution inside the jail. Adds tests for common paths including jail escapes and symlinks. Signed-off-by: Scott Andrews --- Cargo.lock | 2484 +++++++++++++++++++++-- Cargo.toml | 9 +- components/chroot/Cargo.toml | 7 + components/chroot/README.md | 16 +- components/chroot/build.rs | 7 + components/chroot/src/lib.rs | 345 ++-- components/chroot/src/path.rs | 255 +++ components/chroot/tests/chroot.rs | 1297 ++++++++++++ crates/componentized-rt/Cargo.toml | 12 + crates/componentized-rt/src/executor.rs | 79 + crates/componentized-rt/src/lib.rs | 60 + crates/test-harness/Cargo.toml | 11 + crates/test-harness/src/lib.rs | 359 ++++ 13 files changed, 4597 insertions(+), 344 deletions(-) create mode 100644 components/chroot/build.rs create mode 100644 components/chroot/src/path.rs create mode 100644 components/chroot/tests/chroot.rs create mode 100644 crates/componentized-rt/Cargo.toml create mode 100644 crates/componentized-rt/src/executor.rs create mode 100644 crates/componentized-rt/src/lib.rs create mode 100644 crates/test-harness/Cargo.toml create mode 100644 crates/test-harness/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2549905..bbd8e6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,27 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -19,9 +40,32 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" + +[[package]] +name = "async-trait" +version = "0.1.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" @@ -29,11 +73,47 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cap-primitives" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] [[package]] name = "cc" @@ -42,6 +122,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -51,6 +133,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + [[package]] name = "chrono" version = "0.5.0-alpha.1" @@ -58,12 +151,33 @@ source = "git+https://github.com/chronotope/chrono.git?branch=0.5.x#16c45fa8eab2 dependencies = [ "android-tzdata", "iana-time-zone", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] name = "chroot" version = "0.1.0" +dependencies = [ + "componentized-rt", + "tempfile", + "test-harness", + "tokio", + "wasmtime", + "wit-bindgen", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "componentized-rt" +version = "0.1.0" dependencies = [ "wit-bindgen", ] @@ -75,389 +189,2043 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "equivalent" -version = "1.0.1" +name = "core_detect" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "cpp_demangle" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "0667304c32ea56cb4cd6d2d7c0cfe9a2f8041229db8c033af7f8d69492429def" +dependencies = [ + "cfg-if", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "cpufeatures" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] [[package]] -name = "futures-core" -version = "0.3.32" +name = "cpufeatures" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "cranelift-assembler-x64" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "f6d504c8c7ba3f3accaf8e98b7096beb98e1a271404357744c7e9e730c6865ff" +dependencies = [ + "cranelift-assembler-x64-meta", +] [[package]] -name = "futures-util" -version = "0.3.32" +name = "cranelift-assembler-x64-meta" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "c065757905596bc06e82969ebd7b92d691b7fd710a7e9e8181ae0c30dab588d0" dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", + "cranelift-srcgen", ] [[package]] -name = "hashbrown" -version = "0.17.0" +name = "cranelift-bforest" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "5ca7162cd4d4fc5f9f2f88d1459d939882203d93a8c5a19ef2c405b2e82ef13f" dependencies = [ - "foldhash", + "cranelift-entity", + "wasmtime-internal-core", ] [[package]] -name = "heck" -version = "0.5.0" +name = "cranelift-bitset" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "0ec2ba2d4f3d959ec4023a10266c3dcfb28381d1938d507a9a5e1abb78bbcc88" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "cranelift-codegen" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "51a64cb821d5fed56bdd6053acd36fa928cd82b70bf6b19b8b14b8b27d047b9a" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.17.0", + "libm", "log", - "wasm-bindgen", - "windows-core", + "postcard", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "cranelift-codegen-meta" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "142be1e267a7497ba3c2b5a829eaf5387b92b9bcdba6e0a6020f1ffccae7f2b8" dependencies = [ - "cc", + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "cranelift-codegen-shared" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "0e41f75410da6cce8601330137d73718f679db81cb790a33de3871b5c90a0de3" [[package]] -name = "indexmap" -version = "2.14.0" +name = "cranelift-control" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "0c2616c6d01c14921ac9289ef65eb7702d9c760dfdc4537a8ee5bf38ea2832c6" dependencies = [ - "equivalent", - "hashbrown", + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a03e8b44c4877cf2fb21e5788d514af6fd291a066ed9ae37adf114a25fecb4aa" +dependencies = [ + "cranelift-bitset", "serde", - "serde_core", + "serde_derive", + "wasmtime-internal-core", ] [[package]] -name = "itoa" -version = "1.0.14" +name = "cranelift-frontend" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "366ff7cb014bd08752210eba55a650969f8db9b52670d69d2ea0b23e1e9e3edf" +dependencies = [ + "cranelift-codegen", + "hashbrown 0.17.0", + "log", + "smallvec", + "target-lexicon", +] [[package]] -name = "js-sys" -version = "0.3.100" +name = "cranelift-isle" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +checksum = "9ea6526ebf9064fff8ce3b1e26afad7027f9e6bb0e45d2b9200e4cbab8343345" + +[[package]] +name = "cranelift-native" +version = "0.136.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b44d323f3e5108a8a5bc61f3acd227285ece066e19afe9aece211d66b34a64ea" dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", + "cranelift-codegen", + "libc", + "target-lexicon", ] [[package]] -name = "leb128fmt" -version = "0.1.0" +name = "cranelift-srcgen" +version = "0.136.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "fc173168f291ff3b95b5d0769d6a05b1eaef5e6d74c2600affab8396b4e7b420" [[package]] -name = "libc" -version = "0.2.186" +name = "crc32fast" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] [[package]] -name = "log" -version = "0.4.25" +name = "crossbeam-deque" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] [[package]] -name = "macro-string" -version = "0.3.0" +name = "crossbeam-epoch" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", + "crossbeam-utils", ] [[package]] -name = "memchr" -version = "2.7.4" +name = "crossbeam-utils" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] -name = "once_cell" -version = "1.21.4" +name = "crypto-common" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "debugid" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] [[package]] -name = "prettyplease" -version = "0.3.0" +name = "digest" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "proc-macro2", - "syn 3.0.5", + "block-buffer", + "crypto-common", ] [[package]] -name = "proc-macro2" -version = "1.0.93" +name = "directories-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" dependencies = [ - "unicode-ident", + "cfg-if", + "dirs-sys-next", ] [[package]] -name = "quote" -version = "1.0.38" +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", + "quote", + "syn 3.0.5", ] [[package]] -name = "readonly" -version = "0.1.0" +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ - "wit-bindgen", + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "ryu" -version = "1.0.19" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] [[package]] -name = "semver" -version = "1.0.25" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "serde" -version = "1.0.228" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "serde_core", + "percent-encoding", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "fs-set-times" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ - "serde_derive", + "io-lifetimes 2.0.4", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.60.2", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "leb128" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "mach2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" + +[[package]] +name = "macro-string" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7b3a5bbb2f63aaa223a671ea1bd0e7bc16bd30ce387324cf63995deaddbbd2" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memfd" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" +dependencies = [ + "rustix", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "object" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd229a0361b9d0d4396176e02d65897f487eebeab7caa6d443855ee152ca0b9c" +dependencies = [ + "crc32fast", + "hashbrown 0.17.0", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.5", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def59f28ccaf2cb4f8dfaac83197fd7d56a4470a70e4f2589454c731370101c5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1834d3b3ef994f266a0c9ed29a8214f2c2faaa9b1c44babfa57566bb05af83" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "readonly" +version = "0.1.0" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regalloc2" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757712e8e61590d6d4f5d563483755538b5aa13467837a3b41cd9832509a7f85" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", + "log", + "rustc-hash", + "serde", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "test-harness" +version = "0.1.0" +dependencies = [ + "tokio", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "toml" +version = "1.1.8+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" + +[[package]] +name = "tracing" +version = "0.1.0" +dependencies = [ + "chrono", + "heck", + "wit-bindgen", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27dc71f96d21a8b9153da9005c603f4f20d5781b7e6d2675ea0ca5def8713b75" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "log", + "petgraph", + "smallvec", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64765e43c000fae008a48db8b288e175e237fc9ce20a26586836878c1bee89f" +dependencies = [ + "leb128fmt", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-encoder" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" +dependencies = [ + "leb128fmt", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4495a6f85dd94c411c749303efcbc5defade0b0cbdbc0541495241ab917a9b5a" +dependencies = [ + "leb128fmt", + "wasmparser 0.262.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e10808eb27efde692f1b1706089fe19296728bf872291f5b4e0d50fba88f2f" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasm-metadata" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.259.0", + "wasmparser 0.259.0", +] + +[[package]] +name = "wasmparser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9269dd817e0e7021bf534ccd9761bc2d64778b6708e41228b8439d75eaa0b1" +dependencies = [ + "bitflags", + "hashbrown 0.17.0", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.259.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" +dependencies = [ + "bitflags", + "hashbrown 0.17.0", + "indexmap", + "semver", +] + +[[package]] +name = "wasmparser" +version = "0.262.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974d643b125b272607e2fff8012ea14a8328acd7621e4bafa7afb32500052b1b" +dependencies = [ + "bitflags", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a457ba8670a3db3d357c1aeac3ed4fe378a76054a221789f0df0ea507e292b" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.258.3", +] + +[[package]] +name = "wasmtime" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbdd39f5c66f63ba6b5b31391b324b7ade7237d29c866cdb561330118ef436e" +dependencies = [ + "addr2line", + "async-trait", + "bitflags", + "bumpalo", + "bytes", + "cc", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wat", + "windows-sys 0.61.2", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-environ" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e915c27826de4f7b4411b61fd6db32ca448657a5136b8c8e8874a15e90cecbf4" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.17.0", + "indexmap", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.258.3", + "wasmparser 0.258.3", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "952086551d2041430a159fe4bda3471cfea3d52846b93c825829cea9dd01954d" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f531a0b809fd9fd158be6133c97bd2fc628ac47f5812aff3e3b5624eb02c2" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.258.3", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e754a3ff5690c34eb4713bd7c65f60dd607ead4e39042cbf52ac8011de14455b" + +[[package]] +name = "wasmtime-internal-core" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba00c5db77adb0749ee059068e5a417bfac3d8969f5a3479d9bd1ca8c1ba248d" +dependencies = [ + "anyhow", + "hashbrown 0.17.0", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ceeb4325712a253f463e50a69b5a547fbbecd52e45612360c543eaa25a03ff" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.21", + "wasmparser 0.258.3", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb3f21bc32b5382ea95f83ce9a5f2f62b68f1b25a37d0802a41a65236eae5ae" +dependencies = [ + "cc", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e786f9925126e0d2e466c5308e572e8b6226f4047596228a3d63d99528a78949" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92cb1dd5da78f5b21d65e91063bdd8c2eb343c1f077a1dcbc29003d4a794abb0" +dependencies = [ + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "49.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0243d244f6dd07ccd114e6bb099e8dd1197b47de5c054576adc9483f80f65d8" +dependencies = [ + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "20fa5616cd875ec78f5b92020fa47ced7c8bb923079185c8475751098b8c4a69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.119", ] [[package]] -name = "serde_json" -version = "1.0.138" +name = "wasmtime-internal-wit-bindgen" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +checksum = "cad5e8cef56790d4741b62f7b6c971af53b2ad4c2d1603f8ffe86872763147aa" dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-component 0.258.3", + "wit-parser 0.258.3", ] [[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "slab" -version = "0.4.12" +name = "wasmtime-wasi" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "284555904d42017e049fd57c05810b7975075c322481c045f99b07385fb7a5ba" +dependencies = [ + "async-trait", + "bitflags", + "bytes", + "cap-primitives", + "futures", + "rand", + "rustix", + "rustix-linux-procfs", + "thiserror 2.0.21", + "tokio", + "tracing 0.1.44", + "url", + "wasmtime", + "wasmtime-wasi-io", + "wiggle", + "windows-sys 0.61.2", + "winx", +] [[package]] -name = "syn" -version = "2.0.98" +name = "wasmtime-wasi-io" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "ebaf573aae85636595bdb976b6c76e9277e1f81db067aff6764cb256e0c17dff" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "async-trait", + "bytes", + "futures", + "tracing 0.1.44", + "wasmtime", ] [[package]] -name = "syn" -version = "3.0.5" +name = "wast" +version = "35.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "leb128", ] [[package]] -name = "tracing" -version = "0.1.0" +name = "wast" +version = "262.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30612a0c29249b5f185888a107e8a519d920226b7d07f9d351fd094e9f75eb23" dependencies = [ - "chrono", - "heck", - "wit-bindgen", + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.262.0", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "wat" +version = "1.262.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "e228027f2b07bbaf946980ebbe2437fca73be6f54c40c2a8d4f42f277075f8d1" +dependencies = [ + "wast 262.0.0", +] [[package]] -name = "wasm-bindgen" -version = "0.2.123" +name = "wiggle" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +checksum = "ad3dcdc2980b91d0e7db6975caee4f553c6e37fd8e1464a38b09baceed3fe568" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "bitflags", + "thiserror 2.0.21", + "tracing 0.1.44", + "wasmtime", + "wasmtime-environ", + "wiggle-macro", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.123" +name = "wiggle-generate" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +checksum = "4765433ad668e225812b956e0f598011e5cdab4d1a57b2d295ab6863e36ff231" dependencies = [ + "heck", + "proc-macro2", "quote", - "wasm-bindgen-macro-support", + "syn 2.0.119", + "wasmtime-environ", + "witx", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.123" +name = "wiggle-macro" +version = "49.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +checksum = "d94c6623099d11a3fee39986492e803dd0db6676c9d40ab9553a7edd29f317e5" dependencies = [ - "bumpalo", "proc-macro2", "quote", - "syn 2.0.98", - "wasm-bindgen-shared", + "syn 2.0.119", + "wiggle-generate", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.123" +name = "winapi" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "unicode-ident", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "wasm-encoder" -version = "0.259.0" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d0246511d901aacf25d2dc9111f0054947de5e093fe662099e709ff7530dc3" -dependencies = [ - "leb128fmt", - "wasmparser", -] +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] -name = "wasm-metadata" -version = "0.259.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7bf119eb01f4a246864c10bd87c8f26c566350abb25aa4d5273c415bbab338" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "windows-sys 0.61.2", ] [[package]] -name = "wasmparser" -version = "0.259.0" +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7c12eac7bb587801590f6a67ff0bc84d0748513864b71108e4b311cc3df694" -dependencies = [ - "bitflags", - "hashbrown", - "indexmap", - "semver", -] +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" @@ -480,7 +2248,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.119", ] [[package]] @@ -491,7 +2259,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.98", + "syn 2.0.119", ] [[package]] @@ -518,20 +2286,64 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -540,48 +2352,112 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.62.0" @@ -589,6 +2465,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53cb4b5556c3a791e86838ea287782bdafa704d55b0e68b5b81a3a16b9ea5f4b" dependencies = [ "bitflags", + "futures", "wit-bindgen-rust-macro", ] @@ -600,7 +2477,7 @@ checksum = "72ad22a37ecbc0e1fdca34d2b670ba41368cb0920735643e00fdf39a16ee5431" dependencies = [ "anyhow", "heck", - "wit-parser", + "wit-parser 0.259.0", ] [[package]] @@ -614,9 +2491,9 @@ dependencies = [ "indexmap", "prettyplease", "syn 3.0.5", - "wasm-metadata", + "wasm-metadata 0.259.0", "wit-bindgen-core", - "wit-component", + "wit-component 0.259.0", ] [[package]] @@ -635,6 +2512,25 @@ dependencies = [ "wit-bindgen-rust", ] +[[package]] +name = "wit-component" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fca88950af0c1f42bc77e17315e48798b8d68340171b6d204f62bc19a878a6" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.258.3", + "wasm-metadata 0.258.3", + "wasmparser 0.258.3", + "wit-parser 0.258.3", +] + [[package]] name = "wit-component" version = "0.259.0" @@ -648,10 +2544,29 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", + "wasm-encoder 0.259.0", + "wasm-metadata 0.259.0", + "wasmparser 0.259.0", + "wit-parser 0.259.0", +] + +[[package]] +name = "wit-parser" +version = "0.258.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2127663dcf3ee16614fb0e3c33d5c6c66fec9dad317a8b0fdd3869487a533" +dependencies = [ + "anyhow", + "hashbrown 0.17.0", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-ident", + "wasmparser 0.258.3", ] [[package]] @@ -661,7 +2576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6390f4f02493ce678d509c859cf1698e38929fb75c8012a6ee2f7d6b6cb66cd7" dependencies = [ "anyhow", - "hashbrown", + "hashbrown 0.17.0", "id-arena", "indexmap", "log", @@ -670,5 +2585,128 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser", + "wasmparser 0.259.0", +] + +[[package]] +name = "witx" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" +dependencies = [ + "anyhow", + "log", + "thiserror 1.0.69", + "wast 35.0.2", +] + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.2+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" +dependencies = [ + "cc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 4429d78..03d6605 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,19 @@ members = [ "components/chroot", "components/readonly", "components/tracing", + "crates/test-harness", ] [workspace.dependencies] chrono = { git = "https://github.com/chronotope/chrono.git", branch = "0.5.x" } +componentized-rt = { path = "./crates/componentized-rt" } heck = "0.5" -wit-bindgen = "0.62.0" +tempfile = "3" +test-harness = { path = "./crates/test-harness" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] } +wasmtime = { version = "49", features = ["component-model-async"] } +wasmtime-wasi = { version = "49", features = ["p3"] } +wit-bindgen = { version = "0.62.0", features = ["async-spawn"] } [profile.release] opt-level = "s" diff --git a/components/chroot/Cargo.toml b/components/chroot/Cargo.toml index d31d916..b9bbb55 100644 --- a/components/chroot/Cargo.toml +++ b/components/chroot/Cargo.toml @@ -8,4 +8,11 @@ license = "Apache-2.0" crate-type = ["cdylib"] [dependencies] +componentized-rt = { workspace = true, features = ["executor"] } wit-bindgen = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +test-harness = { workspace = true } +tokio = { workspace = true } +wasmtime = { workspace = true } diff --git a/components/chroot/README.md b/components/chroot/README.md index e91cfcf..71af8fb 100644 --- a/components/chroot/README.md +++ b/components/chroot/README.md @@ -1,3 +1,17 @@ # `chroot` -Virtualizes the wasi:filesystem interfaces limiting all access to a specific directory defined by the 'path' key in a wasi:config/store, mimicking the Unix chroot command. +Virtualizes the `wasi:filesystem` interfaces limiting all access to a specific directory defined by the 'path' key in a `wasi:config/store`, mimicking the GNU chroot command. + +The chroot is exported as a single preopened directory, `.`, a virtual scope rather than a root, since a wasi preopen need not be rooted. The `path` is a path within one of the imported preopens, the preopen whose name is the longest prefix of the path, e.g. `/srv/jail` with a preopen `/` is the directory `srv/jail` of that preopen. The path must not contain `..`. The directory is opened by the first operation on the root that needs it, an operation on the root fails when the directory doesn't exist. + +Paths are resolved like a GNU chroot: + +- an absolute path is resolved from the root of the chroot, the exported `.`, from any descriptor +- `..` of the root is the root, e.g. `/../../etc/passwd` is `/etc/passwd` within the chroot +- `..` is resolved lexically before the path is given to the host, `a/b/..` is `a` even when `b` is a symlink +- a descriptor below the root doesn't know where it is, it may have been moved, so like any wasi descriptor a path naming one of its parents, e.g. `../file`, fails with `not-permitted` +- a path containing a NUL byte fails with `illegal-byte-sequence` + +Symlinks are followed by the host, which confines them to the descriptor they are followed from, a symlink leading out of the chroot can be created and read, but not followed. A symlink with an absolute target is not followed, it would be resolved from the host's root rather than the chroot's. + +The synchronous `read-via-stream`, `write-via-stream`, `append-via-stream` and `read-directory` can't wait for the root to be opened, until it is they run on a background thread and their results are forwarded, so the host must support component model threads, e.g. `wasmtime -W component-model-threading`. diff --git a/components/chroot/build.rs b/components/chroot/build.rs new file mode 100644 index 0000000..cd4e5a1 --- /dev/null +++ b/components/chroot/build.rs @@ -0,0 +1,7 @@ +fn main() { + // componentized_rt::executor's thread is started with thread.new-indirect, which calls through + // the exported function table + if std::env::var("CARGO_CFG_TARGET_FAMILY").as_deref() == Ok("wasm") { + println!("cargo:rustc-link-arg-cdylib=--export-table"); + } +} diff --git a/components/chroot/src/lib.rs b/components/chroot/src/lib.rs index 11a1551..d862de0 100644 --- a/components/chroot/src/lib.rs +++ b/components/chroot/src/lib.rs @@ -1,4 +1,5 @@ -use std::path::{Component, Path, PathBuf}; +use std::cell::OnceCell; +use std::rc::Rc; use exports::wasi::filesystem::preopens::Guest as Preopens; use exports::wasi::filesystem::types::{ @@ -8,47 +9,42 @@ use exports::wasi::filesystem::types::{ }; use wasi::filesystem::preopens; use wasi::filesystem::types; +use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; -const PATH_KEY: &str = "path"; - -fn prefix_path(path: String) -> String { - let path_prefix = wasi::config::store::get(PATH_KEY) - .expect("Config must resolve") - .expect(format!("Config must contain '{}'", PATH_KEY).as_str()); +mod path; - let path = match path.strip_prefix("/") { - Some(p) => String::from(p), - None => path, - }; +use path::Base; - // TODO unsafe, path traversals are possible - String::from(Path::new("").join(path_prefix).join(path).to_str().unwrap()) -} +const PATH_KEY: &str = "path"; struct FilesystemChroot {} impl Preopens for FilesystemChroot { fn get_directories() -> Vec<(Descriptor, String)> { - let dirs = preopens::get_directories(); - // TODO find the correct preopen directory, for now assume the first is correct - let (fd, path) = dirs.first().expect("Must have a preopened directory"); - - let path_flags = types::PathFlags::SYMLINK_FOLLOW; - let open_flags = types::OpenFlags::DIRECTORY; - let flags = types::DescriptorFlags::READ; - - let root = prefix_path(String::from(path)); - - let fd = wit_bindgen::block_on(async { - fd.open_at(path_flags, root.clone(), open_flags, flags) - .await - // TODO should we create the directory if it doesn't exist - .expect(format!("chroot directory '{}' must exist", root.clone()).as_str()) + let config = wasi::config::store::get(PATH_KEY) + .expect("Config must resolve") + .unwrap_or_else(|| panic!("Config must contain '{PATH_KEY}'")); + let components = path::config_components(&config) + .unwrap_or_else(|| panic!("chroot path '{config}' must not contain '..'")); + + let mut dirs = preopens::get_directories(); + let names: Vec<&str> = dirs.iter().map(|(_, name)| name.as_str()).collect(); + let (index, path) = path::find_preopen(&components, &names).unwrap_or_else(|| { + panic!("chroot path '{config}' must be within a preopened directory") }); + let (preopen, _) = dirs.swap_remove(index); - let chroot_fd = - Descriptor::new(FilesystemChrootDescriptor::new(fd, root.into(), "/".into())); - vec![(chroot_fd, String::from("/"))] + let root = FilesystemChrootDescriptor { + fd: None, + jail: Rc::new(Jail { + preopen, + path, + root: OnceCell::new(), + }), + at_root: true, + }; + // a virtual scope rather than the guest's root, wasi preopens need not be rooted + vec![(Descriptor::new(root), String::from("."))] } } @@ -56,38 +52,152 @@ impl Types for FilesystemChroot { type Descriptor = FilesystemChrootDescriptor; } +/// The directory the chroot is confined to. +/// +/// It is opened by the first operation that needs it, `get-directories` is synchronous and can't +/// wait for the host to open it. +struct Jail { + /// The preopen containing the jail. + preopen: types::Descriptor, + /// The path of the jail within the preopen, without `..`. + path: String, + root: OnceCell, +} + +impl Jail { + /// The root of the chroot, opened when first needed. + async fn root(&self) -> Result<&types::Descriptor, ErrorCode> { + if let Some(root) = self.root.get() { + return Ok(root); + } + // the chroot is no more capable than the preopen it is in + let flags = self.preopen.get_flags().await? + & (types::DescriptorFlags::READ | types::DescriptorFlags::MUTATE_DIRECTORY); + let root = self + .preopen + .open_at( + types::PathFlags::SYMLINK_FOLLOW, + self.path.clone(), + types::OpenFlags::DIRECTORY, + flags, + ) + .await?; + // another operation may have opened the root while this one waited, either will do + Ok(self.root.get_or_init(|| root)) + } +} + struct FilesystemChrootDescriptor { - fd: types::Descriptor, - root: PathBuf, - path: PathBuf, + /// The host descriptor, `None` for the root of the chroot, which is opened by the jail. + fd: Option, + jail: Rc, + /// Whether the descriptor is known to be the root, `..` from the root is the root. + at_root: bool, } impl FilesystemChrootDescriptor { - fn new(fd: types::Descriptor, root: PathBuf, path: PathBuf) -> Self { - Self { fd, root, path } - } - - fn internal_path(&self, path: String) -> Result { - let mut internal = self.path.clone(); - for component in PathBuf::from(path).components() { - if component == Component::RootDir { - // treat root dir as a relative path - internal = PathBuf::from(".") - } else if component == Component::ParentDir { - if !internal.pop() { - // attempt to escape root - return Err(ErrorCode::NotPermitted); - }; - } else if let Component::Normal(c) = component { - internal.push(c); - } + async fn fd(&self) -> Result<&types::Descriptor, ErrorCode> { + match &self.fd { + Some(fd) => Ok(fd), + None => self.jail.root().await, } - Ok(internal) } - fn external_path(&self, path: String) -> Result { - let path = self.root.clone().join(self.internal_path(path)?); - Ok(path.to_string_lossy().into_owned()) + /// Resolve a path into the host descriptor to use and a path relative to it, which is never + /// absolute and never contains `..`. + async fn resolve(&self, path: String) -> Result<(&types::Descriptor, Base, String), ErrorCode> { + let (base, path) = path::resolve(&path, self.at_root).map_err(|err| match err { + path::Error::Empty => ErrorCode::NoEntry, + path::Error::Escape => ErrorCode::NotPermitted, + path::Error::Nul => ErrorCode::IllegalByteSequence, + })?; + let fd = match base { + Base::Root => self.jail.root().await?, + Base::Descriptor => self.fd().await?, + }; + Ok((fd, base, path)) + } + + /// Run a synchronous operation that returns a future for its result. + /// + /// A synchronous function can't wait for the root to be opened, once it is open the operation + /// is passed through, until then the operation runs in the background and its result is + /// forwarded. + fn with_fd( + &self, + op: impl FnOnce(&types::Descriptor) -> FutureReader> + 'static, + ) -> FutureReader> { + if let Some(fd) = self.opened_fd() { + return op(fd); + } + let jail = self.jail.clone(); + let (tx, rx) = result_future(); + componentized_rt::executor::spawn(async move { + let result = match jail.root().await { + Ok(root) => op(root).await, + Err(err) => Err(err), + }; + let _ = tx.write(result).await; + }); + rx + } + + /// Run a synchronous operation that returns a stream and a future for its result, see + /// [`Self::with_fd`]. + fn with_fd_stream( + &self, + new_stream: fn() -> (StreamWriter, StreamReader), + op: impl FnOnce(&types::Descriptor) -> (StreamReader, FutureReader>) + + 'static, + ) -> (StreamReader, FutureReader>) { + if let Some(fd) = self.opened_fd() { + return op(fd); + } + let jail = self.jail.clone(); + let (mut stream_tx, stream_rx) = new_stream(); + let (tx, rx) = result_future(); + componentized_rt::executor::spawn(async move { + let result = match jail.root().await { + Ok(root) => { + let (stream, result) = op(root); + forward(stream, &mut stream_tx).await; + result.await + } + Err(err) => Err(err), + }; + // the stream ends before the result is known + drop(stream_tx); + let _ = tx.write(result).await; + }); + (stream_rx, rx) + } + + /// The host descriptor, when it is open. + fn opened_fd(&self) -> Option<&types::Descriptor> { + self.fd.as_ref().or_else(|| self.jail.root.get()) + } +} + +fn result_future() -> ( + wit_bindgen::FutureWriter>, + FutureReader>, +) { + // the default is only written if the write is cancelled + wit_future::new(|| Err(ErrorCode::Other(None))) +} + +/// Forward the items of one stream to another, until either end is dropped. +async fn forward(from: StreamReader, to: &mut StreamWriter) { + let mut from = from; + loop { + let (result, items) = from.read(Vec::with_capacity(4096)).await; + if !items.is_empty() && !to.write_all(items).await.is_empty() { + // the reader was dropped + return; + } + if !matches!(result, StreamResult::Complete(_)) { + return; + } } } @@ -99,7 +209,7 @@ impl GuestDescriptor for FilesystemChrootDescriptor { wit_bindgen::StreamReader, wit_bindgen::FutureReader>, ) { - self.fd.read_via_stream(offset) + self.with_fd_stream(wit_stream::new, move |fd| fd.read_via_stream(offset)) } fn write_via_stream( @@ -107,14 +217,14 @@ impl GuestDescriptor for FilesystemChrootDescriptor { data: wit_bindgen::StreamReader, offset: Filesize, ) -> wit_bindgen::FutureReader> { - self.fd.write_via_stream(data, offset) + self.with_fd(move |fd| fd.write_via_stream(data, offset)) } fn append_via_stream( &self, data: wit_bindgen::StreamReader, ) -> wit_bindgen::FutureReader> { - self.fd.append_via_stream(data) + self.with_fd(move |fd| fd.append_via_stream(data)) } async fn advise( @@ -123,23 +233,23 @@ impl GuestDescriptor for FilesystemChrootDescriptor { length: Filesize, advice: Advice, ) -> Result<(), ErrorCode> { - self.fd.advise(offset, length, advice).await + self.fd().await?.advise(offset, length, advice).await } async fn sync_data(&self) -> Result<(), ErrorCode> { - self.fd.sync_data().await + self.fd().await?.sync_data().await } async fn get_flags(&self) -> Result { - self.fd.get_flags().await + self.fd().await?.get_flags().await } async fn get_type(&self) -> Result { - self.fd.get_type().await + self.fd().await?.get_type().await } async fn set_size(&self, size: Filesize) -> Result<(), ErrorCode> { - self.fd.set_size(size).await + self.fd().await?.set_size(size).await } async fn set_times( @@ -147,7 +257,8 @@ impl GuestDescriptor for FilesystemChrootDescriptor { data_access_timestamp: NewTimestamp, data_modification_timestamp: NewTimestamp, ) -> Result<(), ErrorCode> { - self.fd + self.fd() + .await? .set_times(data_access_timestamp, data_modification_timestamp) .await } @@ -158,20 +269,20 @@ impl GuestDescriptor for FilesystemChrootDescriptor { wit_bindgen::StreamReader, wit_bindgen::FutureReader>, ) { - self.fd.read_directory() + self.with_fd_stream(wit_stream::new, |fd| fd.read_directory()) } async fn sync(&self) -> Result<(), ErrorCode> { - self.fd.sync().await + self.fd().await?.sync().await } async fn create_directory_at(&self, path: String) -> Result<(), ErrorCode> { - let path = self.external_path(path)?; - self.fd.create_directory_at(path).await + let (fd, _, path) = self.resolve(path).await?; + fd.create_directory_at(path).await } async fn stat(&self) -> Result { - self.fd.stat().await + self.fd().await?.stat().await } async fn stat_at( @@ -179,8 +290,8 @@ impl GuestDescriptor for FilesystemChrootDescriptor { path_flags: PathFlags, path: String, ) -> Result { - let path = self.external_path(path)?; - self.fd.stat_at(path_flags, path).await + let (fd, _, path) = self.resolve(path).await?; + fd.stat_at(path_flags, path).await } async fn set_times_at( @@ -190,15 +301,14 @@ impl GuestDescriptor for FilesystemChrootDescriptor { data_access_timestamp: NewTimestamp, data_modification_timestamp: NewTimestamp, ) -> Result<(), ErrorCode> { - let path = self.external_path(path)?; - self.fd - .set_times_at( - path_flags, - path, - data_access_timestamp, - data_modification_timestamp, - ) - .await + let (fd, _, path) = self.resolve(path).await?; + fd.set_times_at( + path_flags, + path, + data_access_timestamp, + data_modification_timestamp, + ) + .await } async fn link_at( @@ -208,11 +318,11 @@ impl GuestDescriptor for FilesystemChrootDescriptor { new_descriptor: DescriptorBorrow<'_>, new_path: String, ) -> Result<(), ErrorCode> { - let old_path = self.external_path(old_path)?; - let new_descriptor: &FilesystemChrootDescriptor = new_descriptor.get(); - let new_path = self.external_path(new_path)?; - self.fd - .link_at(old_path_flags, old_path, &new_descriptor.fd, new_path) + let (old_fd, _, old_path) = self.resolve(old_path).await?; + let new_descriptor: &Self = new_descriptor.get(); + let (new_fd, _, new_path) = new_descriptor.resolve(new_path).await?; + old_fd + .link_at(old_path_flags, old_path, new_fd, new_path) .await } @@ -223,28 +333,25 @@ impl GuestDescriptor for FilesystemChrootDescriptor { open_flags: OpenFlags, flags: DescriptorFlags, ) -> Result { - let internal_path = self.internal_path(path.clone())?; - let external_path = self.external_path(path.clone())?; - self.fd - .open_at(path_flags, external_path, open_flags, flags) - .await - .map(|fd| { - Descriptor::new(FilesystemChrootDescriptor::new( - fd, - self.root.clone(), - internal_path, - )) - }) + let (fd, base, path) = self.resolve(path).await?; + // the root itself, or the descriptor itself when it is the root + let at_root = path == "." && (base == Base::Root || self.at_root); + let fd = fd.open_at(path_flags, path, open_flags, flags).await?; + Ok(Descriptor::new(FilesystemChrootDescriptor { + fd: Some(fd), + jail: self.jail.clone(), + at_root, + })) } async fn readlink_at(&self, path: String) -> Result { - let path = self.external_path(path)?; - self.fd.readlink_at(path).await + let (fd, _, path) = self.resolve(path).await?; + fd.readlink_at(path).await } async fn remove_directory_at(&self, path: String) -> Result<(), ErrorCode> { - let path = self.external_path(path)?; - self.fd.remove_directory_at(path).await + let (fd, _, path) = self.resolve(path).await?; + fd.remove_directory_at(path).await } async fn rename_at( @@ -253,34 +360,34 @@ impl GuestDescriptor for FilesystemChrootDescriptor { new_descriptor: DescriptorBorrow<'_>, new_path: String, ) -> Result<(), ErrorCode> { - let old_path = self.external_path(old_path)?; + let (old_fd, _, old_path) = self.resolve(old_path).await?; let new_descriptor: &Self = new_descriptor.get(); - let new_path = self.external_path(new_path)?; - - self.fd - .rename_at(old_path, &new_descriptor.fd, new_path) - .await + let (new_fd, _, new_path) = new_descriptor.resolve(new_path).await?; + old_fd.rename_at(old_path, new_fd, new_path).await } async fn symlink_at(&self, old_path: String, new_path: String) -> Result<(), ErrorCode> { - let old_path = self.external_path(old_path)?; - let new_path = self.external_path(new_path)?; - self.fd.symlink_at(old_path, new_path).await + // the target is stored as given, it is resolved when the link is followed and the host + // confines it to the descriptor the link is followed from + let (fd, _, new_path) = self.resolve(new_path).await?; + fd.symlink_at(old_path, new_path).await } async fn unlink_file_at(&self, path: String) -> Result<(), ErrorCode> { - let path = self.external_path(path)?; - self.fd.unlink_file_at(path).await + let (fd, _, path) = self.resolve(path).await?; + fd.unlink_file_at(path).await } async fn is_same_object(&self, other: DescriptorBorrow<'_>) -> bool { let other: &Self = other.get(); - - self.fd.is_same_object(&other.fd).await + match (self.fd().await, other.fd().await) { + (Ok(fd), Ok(other)) => fd.is_same_object(other).await, + _ => false, + } } async fn metadata_hash(&self) -> Result { - self.fd.metadata_hash().await + self.fd().await?.metadata_hash().await } async fn metadata_hash_at( @@ -288,8 +395,8 @@ impl GuestDescriptor for FilesystemChrootDescriptor { path_flags: PathFlags, path: String, ) -> Result { - let path = self.external_path(path)?; - self.fd.metadata_hash_at(path_flags, path).await + let (fd, _, path) = self.resolve(path).await?; + fd.metadata_hash_at(path_flags, path).await } } diff --git a/components/chroot/src/path.rs b/components/chroot/src/path.rs new file mode 100644 index 0000000..ea2ab79 --- /dev/null +++ b/components/chroot/src/path.rs @@ -0,0 +1,255 @@ +//! Lexical path resolution for the chroot, independent of the filesystem. +//! +//! Every path the guest passes is normalized before it reaches the host, so no path given to the +//! host contains a `..` or is absolute. The host's descriptors confine symlinks, which can't be +//! resolved here without racing the filesystem. + +/// The descriptor a path is resolved relative to. +#[derive(Debug, PartialEq, Eq)] +pub enum Base { + /// The root of the chroot. + Root, + /// The descriptor the path was given to. + Descriptor, +} + +/// Why a path can't be resolved. +#[derive(Debug, PartialEq, Eq)] +pub enum Error { + /// The path is empty. + Empty, + /// The path names a parent of a descriptor that is not the root of the chroot. + Escape, + /// The path contains a NUL byte, which ends the path for C, but not for wasi. + Nul, +} + +/// Resolve a path given to a descriptor into a path relative to `Base`. +/// +/// Like the root of a GNU chroot, the parent of the root is the root itself. An absolute path is +/// resolved from the root, as is a relative path given to a descriptor at the root, and `..` never +/// goes above the root. A descriptor below the root doesn't know where it is, it may have been moved, +/// so like any wasi descriptor, a path naming one of its parents is an error. +/// +/// `..` is resolved lexically, `a/b/..` is `a` even when `b` is a symlink. +pub fn resolve(path: &str, at_root: bool) -> Result<(Base, String), Error> { + if path.is_empty() { + return Err(Error::Empty); + } + if path.contains('\0') { + return Err(Error::Nul); + } + let absolute = path.starts_with('/'); + let base = if absolute || at_root { + Base::Root + } else { + Base::Descriptor + }; + + let mut components: Vec<&str> = vec![]; + for component in path.split('/') { + match component { + "" | "." => {} + ".." => { + if components.pop().is_none() && base == Base::Descriptor { + return Err(Error::Escape); + } + } + component => components.push(component), + } + } + + if components.is_empty() { + return Ok((base, String::from("."))); + } + let mut resolved = components.join("/"); + // a trailing slash, `.` or `..` requires the path to be a directory + if path.ends_with('/') || path.ends_with("/.") || path.ends_with("/..") { + resolved.push('/'); + } + Ok((base, resolved)) +} + +/// Split the configured path of the chroot into its components, it must not contain `..`. +pub fn config_components(path: &str) -> Option> { + let mut components = vec![]; + for component in path.split('/') { + match component { + "" | "." => {} + ".." => return None, + component => components.push(component), + } + } + Some(components) +} + +/// Find the preopen containing the chroot, the preopen with the longest name the configured path +/// starts with. Returns the index of the preopen and the path of the chroot within it. +pub fn find_preopen<'a>(config: &[&str], preopens: &[&'a str]) -> Option<(usize, String)> { + preopens + .iter() + .enumerate() + .filter_map(|(index, name)| { + // `/` and `.` both name the root + let name = config_components(name)?; + config.starts_with(&name).then(|| (index, name.len())) + }) + .max_by_key(|(_, len)| *len) + .map(|(index, len)| { + let path = config[len..].join("/"); + (index, if path.is_empty() { ".".into() } else { path }) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn root(path: &str) -> Result<(Base, String), Error> { + resolve(path, true) + } + + fn child(path: &str) -> Result<(Base, String), Error> { + resolve(path, false) + } + + fn ok(base: Base, path: &str) -> Result<(Base, String), Error> { + Ok((base, path.into())) + } + + #[test] + fn plain_paths() { + assert_eq!(root("a"), ok(Base::Root, "a")); + assert_eq!(root("a/b/c.txt"), ok(Base::Root, "a/b/c.txt")); + assert_eq!(child("a/b"), ok(Base::Descriptor, "a/b")); + } + + #[test] + fn empty_path() { + assert_eq!(root(""), Err(Error::Empty)); + assert_eq!(child(""), Err(Error::Empty)); + } + + #[test] + fn root_itself() { + for path in ["/", ".", "./", "//", "/.", "/./.", ".."] { + assert_eq!(root(path), ok(Base::Root, "."), "{path}"); + } + assert_eq!(child("."), ok(Base::Descriptor, ".")); + assert_eq!(child("/"), ok(Base::Root, ".")); + } + + #[test] + fn redundant_separators_and_dots() { + assert_eq!(root("a//b"), ok(Base::Root, "a/b")); + assert_eq!(root("./a/./b"), ok(Base::Root, "a/b")); + assert_eq!(root("a/b/../c"), ok(Base::Root, "a/c")); + assert_eq!(child("a/../b"), ok(Base::Descriptor, "b")); + } + + #[test] + fn absolute_paths_resolve_from_root() { + assert_eq!(root("/a/b"), ok(Base::Root, "a/b")); + assert_eq!(child("/a/b"), ok(Base::Root, "a/b")); + assert_eq!(child("///a"), ok(Base::Root, "a")); + } + + #[test] + fn parent_of_root_is_root() { + assert_eq!(root("../a"), ok(Base::Root, "a")); + assert_eq!(root("../../../../a"), ok(Base::Root, "a")); + assert_eq!(root("/../a"), ok(Base::Root, "a")); + assert_eq!(root("a/../../b"), ok(Base::Root, "b")); + assert_eq!(root("a/b/../../../../c"), ok(Base::Root, "c")); + assert_eq!(child("/../../etc/passwd"), ok(Base::Root, "etc/passwd")); + } + + #[test] + fn parent_of_child_is_escape() { + assert_eq!(child(".."), Err(Error::Escape)); + assert_eq!(child("../a"), Err(Error::Escape)); + assert_eq!(child("a/../../b"), Err(Error::Escape)); + assert_eq!(child("./.."), Err(Error::Escape)); + } + + #[test] + fn trailing_directory_markers_are_kept() { + assert_eq!(root("a/"), ok(Base::Root, "a/")); + assert_eq!(root("a/."), ok(Base::Root, "a/")); + assert_eq!(root("a/b/.."), ok(Base::Root, "a/")); + assert_eq!(root("a//"), ok(Base::Root, "a/")); + } + + #[test] + fn no_resolved_path_is_absolute_or_has_parents() { + let paths = [ + "/", + "..", + "../", + "/..", + "a/..", + "../a/../..", + "//..//a", + "a/./../..", + "/a/b/../../..", + ]; + for path in paths { + let (_, resolved) = root(path).unwrap(); + assert!(!resolved.starts_with('/'), "{path} -> {resolved}"); + assert!( + !resolved.split('/').any(|c| c == ".."), + "{path} -> {resolved}" + ); + } + } + + #[test] + fn other_bytes_are_not_separators() { + // only `/` separates components in wasi, the host rejects what it can't name + assert_eq!(root("..\\a"), ok(Base::Root, "..\\a")); + assert_eq!(root("..."), ok(Base::Root, "...")); + assert_eq!(root(".. /a"), ok(Base::Root, ".. /a")); + } + + #[test] + fn nul_bytes_are_rejected() { + // `a\0/../b` is `a` to C, but `b` once resolved + assert_eq!(root("a\0/../b"), Err(Error::Nul)); + assert_eq!(root("a\0"), Err(Error::Nul)); + assert_eq!(child("\0"), Err(Error::Nul)); + } + + #[test] + fn config_rejects_parents() { + assert_eq!(config_components("jail"), Some(vec!["jail"])); + assert_eq!(config_components("/srv/./jail/"), Some(vec!["srv", "jail"])); + assert_eq!(config_components("/"), Some(vec![])); + assert_eq!(config_components(".."), None); + assert_eq!(config_components("jail/../.."), None); + assert_eq!(config_components("/srv/../etc"), None); + } + + #[test] + fn preopen_is_longest_match() { + let find = |config: &str, preopens: &[&str]| { + find_preopen(&config_components(config).unwrap(), preopens) + }; + assert_eq!(find("jail", &["/"]), Some((0, "jail".into()))); + assert_eq!(find("/jail", &["."]), Some((0, "jail".into()))); + assert_eq!( + find("/data/jail", &["/", "/data"]), + Some((1, "jail".into())) + ); + assert_eq!( + find("/data/jail", &["/data", "/"]), + Some((0, "jail".into())) + ); + assert_eq!(find("/data", &["/", "/data"]), Some((1, ".".into()))); + // a preopen matches whole components, `/data` doesn't contain `/database` + assert_eq!(find("/database/jail", &["/data"]), None); + assert_eq!(find("/jail", &["/data"]), None); + assert_eq!(find("/jail", &[]), None); + // a preopen named with `..` is never used + assert_eq!(find("/jail", &["/..", "/"]), Some((1, "jail".into()))); + } +} diff --git a/components/chroot/tests/chroot.rs b/components/chroot/tests/chroot.rs new file mode 100644 index 0000000..e6e556f --- /dev/null +++ b/components/chroot/tests/chroot.rs @@ -0,0 +1,1297 @@ +//! The chroot confines every operation to the configured directory. +//! +//! Each test preopens a fresh copy of this tree as `/`, with the chroot at `/jail`: +//! +//! ```text +//! secret.txt "outside secret", must never be reachable +//! jail-sibling/secret.txt shares the jail's name as a prefix +//! jail/ +//! secret.txt "jail", the file `/secret.txt` in the chroot +//! inside.txt "inside" +//! sub/nested.txt "nested!" +//! sub/deeper/ +//! sub/link-up -> ../inside.txt +//! sub/link-escape -> ../../secret.txt +//! link-inside -> inside.txt +//! link-outside -> ../secret.txt +//! link-absolute -> /secret.txt +//! link-host-absolute -> /secret.txt +//! link-dir-outside -> .. +//! ``` +//! +//! Files have distinct sizes, a stat tells which file was reached. + +#![cfg(unix)] + +use std::fs; +use std::os::unix::fs::symlink; +use std::path::{Path, PathBuf}; + +use tempfile::TempDir; +use test_harness::{ + DescriptorFlags, DescriptorType, ErrorCode, Exports, Harness, OpenFlags, PathFlags, Perms, + TestSubject, +}; +use wasmtime::component::{Accessor, ResourceAny}; + +const OUTSIDE_SECRET: &str = "outside secret"; +const JAIL_SECRET: &str = "jail"; +const INSIDE: &str = "inside"; +const NESTED: &str = "nested!"; + +fn size(content: &str) -> u64 { + content.len() as u64 +} + +struct Fixture { + dir: TempDir, +} + +impl Fixture { + fn new() -> Self { + let dir = tempfile::tempdir().expect("tempdir"); + let root = dir.path(); + fs::write(root.join("secret.txt"), OUTSIDE_SECRET).unwrap(); + fs::create_dir_all(root.join("jail-sibling")).unwrap(); + fs::write(root.join("jail-sibling/secret.txt"), OUTSIDE_SECRET).unwrap(); + + let jail = root.join("jail"); + fs::create_dir_all(jail.join("sub/deeper")).unwrap(); + fs::write(jail.join("secret.txt"), JAIL_SECRET).unwrap(); + fs::write(jail.join("inside.txt"), INSIDE).unwrap(); + fs::write(jail.join("sub/nested.txt"), NESTED).unwrap(); + symlink("../inside.txt", jail.join("sub/link-up")).unwrap(); + symlink("../../secret.txt", jail.join("sub/link-escape")).unwrap(); + symlink("inside.txt", jail.join("link-inside")).unwrap(); + symlink("../secret.txt", jail.join("link-outside")).unwrap(); + symlink("/secret.txt", jail.join("link-absolute")).unwrap(); + symlink(root.join("secret.txt"), jail.join("link-host-absolute")).unwrap(); + symlink("..", jail.join("link-dir-outside")).unwrap(); + Self { dir } + } + + fn host(&self, path: &str) -> PathBuf { + self.dir.path().join(path) + } + + fn jail(&self, path: &str) -> PathBuf { + self.dir.path().join("jail").join(path) + } + + /// The files outside the jail are untouched. + fn assert_outside_untouched(&self) { + assert_eq!( + fs::read_to_string(self.host("secret.txt")).unwrap(), + OUTSIDE_SECRET + ); + assert_eq!( + fs::read_to_string(self.host("jail-sibling/secret.txt")).unwrap(), + OUTSIDE_SECRET + ); + let mut entries: Vec = fs::read_dir(self.dir.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .collect(); + entries.sort(); + assert_eq!(entries, ["jail", "jail-sibling", "secret.txt"]); + } + + fn harness(&self) -> Harness { + Harness::new("chroot").preopen(self.dir.path(), "/", Perms::ReadWrite) + } + + async fn chroot(&self) -> TestSubject { + self.harness() + .config("path", "/jail") + .build() + .await + .expect("chroot") + } +} + +/// The single preopen of the chroot, its root. +async fn root( + accessor: &Accessor, + exports: &Exports, +) -> wasmtime::Result { + let dirs = exports + .wasi_filesystem_preopens() + .call_get_directories(accessor) + .await?; + assert_eq!(dirs.len(), 1); + let (fd, name) = dirs.into_iter().next().unwrap(); + assert_eq!(name, "."); + Ok(fd) +} + +/// Stat a path from the root, following symlinks. +async fn stat_size( + accessor: &Accessor, + exports: &Exports, + fd: ResourceAny, + path: &str, +) -> wasmtime::Result> { + Ok(exports + .wasi_filesystem_types() + .descriptor() + .call_stat_at(accessor, fd, PathFlags::SYMLINK_FOLLOW, path.into()) + .await? + .map(|stat| stat.size)) +} + +/// Stat each path from the root, returning the size of the file reached or the error. +async fn stat_all(fixture: &Fixture, paths: &[&str]) -> Vec<(String, Result)> { + let paths: Vec = paths.iter().map(|path| path.to_string()).collect(); + let mut chroot = fixture.chroot().await; + chroot + .run(async move |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut results = vec![]; + for path in paths { + let result = stat_size(accessor, exports, fd, &path).await?; + results.push((path, result)); + } + Ok(results) + }) + .await + .expect("run") +} + +fn expect_all(results: Vec<(String, Result)>, expected: Result) { + for (path, result) in results { + assert_eq!(result, expected, "{path:?}"); + } +} + +/// Open a path from the root. +async fn open( + accessor: &Accessor, + exports: &Exports, + fd: ResourceAny, + path: &str, + open_flags: OpenFlags, + flags: DescriptorFlags, +) -> wasmtime::Result> { + exports + .wasi_filesystem_types() + .descriptor() + .call_open_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + path.into(), + open_flags, + flags, + ) + .await +} + +// --- lookups --- + +#[tokio::test(flavor = "multi_thread")] +async fn files_in_the_jail_are_reachable() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &[ + "inside.txt", + "/inside.txt", + "./inside.txt", + "sub/nested.txt", + "/sub/nested.txt", + ], + ) + .await; + assert_eq!( + results, + [ + ("inside.txt".into(), Ok(size(INSIDE))), + ("/inside.txt".into(), Ok(size(INSIDE))), + ("./inside.txt".into(), Ok(size(INSIDE))), + ("sub/nested.txt".into(), Ok(size(NESTED))), + ("/sub/nested.txt".into(), Ok(size(NESTED))), + ] + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn redundant_separators_and_dots() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &[ + "sub//nested.txt", + "//sub///nested.txt", + "./sub/./nested.txt", + "sub/deeper/../nested.txt", + "sub/deeper/../../sub/nested.txt", + ], + ) + .await; + expect_all(results, Ok(size(NESTED))); +} + +#[tokio::test(flavor = "multi_thread")] +async fn parent_of_root_is_root() { + let fixture = Fixture::new(); + // each names `/secret.txt` in the chroot, the jail's secret, never the host's + let results = stat_all( + &fixture, + &[ + "secret.txt", + "/secret.txt", + "../secret.txt", + "/../secret.txt", + "../../../../../../secret.txt", + "/../../../../../../secret.txt", + "sub/../../secret.txt", + "sub/deeper/../../../../secret.txt", + "./../secret.txt", + "..//..//secret.txt", + ], + ) + .await; + expect_all(results, Ok(size(JAIL_SECRET))); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn parent_of_root_is_root_for_directories() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &["..", "/..", "../..", "sub/../..", "../jail/secret.txt"], + ) + .await; + // `..` is the jail, there is no `jail` directory in the jail + assert_eq!(results[3].1, results[0].1); + assert_eq!( + results[4], + ("../jail/secret.txt".into(), Err(ErrorCode::NoEntry)) + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn sibling_with_a_shared_prefix_is_unreachable() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &[ + "../jail-sibling/secret.txt", + "/../jail-sibling/secret.txt", + "../jail-sibling", + ], + ) + .await; + expect_all(results, Err(ErrorCode::NoEntry)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn host_paths_are_not_reachable() { + let fixture = Fixture::new(); + let host_secret = fixture.host("secret.txt").to_string_lossy().into_owned(); + let host_jail = fixture.jail("inside.txt").to_string_lossy().into_owned(); + let results = stat_all( + &fixture, + &[&host_secret, &host_jail, "/etc/passwd", "/jail/inside.txt"], + ) + .await; + // absolute paths resolve within the jail, where none of these exist + expect_all(results, Err(ErrorCode::NoEntry)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn odd_paths_are_not_traversals() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &["..\\secret.txt", "...", ".../secret.txt", ".. /secret.txt"], + ) + .await; + expect_all(results, Err(ErrorCode::NoEntry)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn empty_path_is_an_error() { + let fixture = Fixture::new(); + let results = stat_all(&fixture, &[""]).await; + expect_all(results, Err(ErrorCode::NoEntry)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn nul_byte_is_an_error() { + let fixture = Fixture::new(); + // C would see `inside.txt`, resolving the rest would reach `secret.txt` + let results = stat_all( + &fixture, + &["inside.txt\0/../secret.txt", "inside.txt\0", "\0"], + ) + .await; + expect_all(results, Err(ErrorCode::IllegalByteSequence)); +} + +#[tokio::test(flavor = "multi_thread")] +async fn trailing_slash_requires_a_directory() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &[ + "inside.txt/", + "inside.txt/.", + "sub/", + "sub/.", + "sub/deeper/..", + ], + ) + .await; + assert!(results[0].1.is_err(), "{:?}", results[0]); + assert!(results[1].1.is_err(), "{:?}", results[1]); + assert!(results[2].1.is_ok(), "{:?}", results[2]); + assert!(results[3].1.is_ok(), "{:?}", results[3]); + assert!(results[4].1.is_ok(), "{:?}", results[4]); +} + +// --- symlinks --- + +#[tokio::test(flavor = "multi_thread")] +async fn symlinks_within_the_jail_are_followed() { + let fixture = Fixture::new(); + let results = stat_all(&fixture, &["link-inside", "sub/link-up", "/sub/link-up"]).await; + expect_all(results, Ok(size(INSIDE))); +} + +#[tokio::test(flavor = "multi_thread")] +async fn symlinks_out_of_the_jail_are_not_followed() { + let fixture = Fixture::new(); + let results = stat_all( + &fixture, + &[ + "link-outside", + "link-absolute", + "link-host-absolute", + "sub/link-escape", + "link-dir-outside", + "link-dir-outside/secret.txt", + "link-dir-outside/jail/inside.txt", + "sub/../link-outside", + ], + ) + .await; + for (path, result) in results { + assert!( + result != Ok(size(OUTSIDE_SECRET)), + "{path:?} reached the secret outside the jail" + ); + assert!(result.is_err(), "{path:?} -> {result:?}"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn symlinks_are_not_followed_without_the_flag() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let types = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut types = vec![]; + for path in ["link-outside", "link-absolute", "link-dir-outside"] { + let stat = exports + .wasi_filesystem_types() + .descriptor() + .call_stat_at(accessor, fd, PathFlags::empty(), path.into()) + .await? + .expect("lstat"); + types.push(stat.type_); + } + Ok(types) + }) + .await + .unwrap(); + for type_ in types { + assert_eq!(type_, DescriptorType::SymbolicLink); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn readlink_returns_the_target_as_stored() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let targets = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut targets = vec![]; + for path in ["link-inside", "/sub/link-up", "../link-outside"] { + targets.push( + exports + .wasi_filesystem_types() + .descriptor() + .call_readlink_at(accessor, fd, path.into()) + .await?, + ); + } + Ok(targets) + }) + .await + .unwrap(); + assert_eq!( + targets, + [ + Ok("inside.txt".into()), + Ok("../inside.txt".into()), + Ok("../secret.txt".into()) + ] + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn created_symlinks_store_the_target_unchanged() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let (created, followed) = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + let mut created = vec![]; + for (target, link) in [ + ("inside.txt", "new-inside"), + ("../secret.txt", "new-outside"), + ("/secret.txt", "../new-absolute"), + ] { + created.push( + descriptor + .call_symlink_at(accessor, fd, target.into(), link.into()) + .await?, + ); + } + let mut followed = vec![]; + for link in ["new-inside", "new-outside", "new-absolute"] { + followed.push(stat_size(accessor, exports, fd, link).await?); + } + Ok((created, followed)) + }) + .await + .unwrap(); + assert_eq!(created[0], Ok(())); + assert_eq!(created[1], Ok(())); + // the host refuses absolute targets + assert!(created[2].is_err(), "{:?}", created[2]); + // the targets are not rewritten with the jail's host path + assert_eq!( + fs::read_link(fixture.jail("new-inside")).unwrap(), + Path::new("inside.txt") + ); + assert_eq!( + fs::read_link(fixture.jail("new-outside")).unwrap(), + Path::new("../secret.txt") + ); + assert!(fs::symlink_metadata(fixture.jail("new-absolute")).is_err()); + assert!(fs::symlink_metadata(fixture.host("new-absolute")).is_err()); + // a planted link out of the jail is never followed out of it + assert_eq!(followed[0], Ok(size(INSIDE))); + assert!(followed[1].is_err(), "{:?}", followed[1]); + assert_eq!(followed[2], Err(ErrorCode::NoEntry)); + fixture.assert_outside_untouched(); +} + +// --- descriptors --- + +#[tokio::test(flavor = "multi_thread")] +async fn opened_directories_resolve_relative_to_themselves() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let sub = open( + accessor, + exports, + fd, + "sub", + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await? + .expect("open sub"); + let deeper = open( + accessor, + exports, + sub, + "deeper", + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await? + .expect("open deeper"); + Ok(vec![ + stat_size(accessor, exports, sub, "nested.txt").await?, + stat_size(accessor, exports, sub, "./nested.txt").await?, + stat_size(accessor, exports, sub, "deeper/../nested.txt").await?, + // the link names a parent of the descriptor it is followed from + stat_size(accessor, exports, sub, "link-up").await?, + // absolute paths resolve from the root of the chroot + stat_size(accessor, exports, sub, "/inside.txt").await?, + stat_size(accessor, exports, deeper, "/sub/nested.txt").await?, + stat_size(accessor, exports, deeper, "/../../secret.txt").await?, + ]) + }) + .await + .unwrap(); + assert_eq!( + results, + [ + Ok(size(NESTED)), + Ok(size(NESTED)), + Ok(size(NESTED)), + Err(ErrorCode::NotPermitted), + Ok(size(INSIDE)), + Ok(size(NESTED)), + Ok(size(JAIL_SECRET)), + ] + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn parent_of_opened_directory_is_not_permitted() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let sub = open( + accessor, + exports, + fd, + "sub", + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await? + .expect("open sub"); + let mut results = vec![]; + for path in [ + "..", + "../inside.txt", + "../../secret.txt", + "deeper/../../inside.txt", + ] { + results.push(stat_size(accessor, exports, sub, path).await?); + } + Ok(results) + }) + .await + .unwrap(); + expect_all( + results.into_iter().map(|r| (String::new(), r)).collect(), + Err(ErrorCode::NotPermitted), + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn reopened_root_is_still_the_root() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut results = vec![]; + for path in [".", "/", "..", "sub/.."] { + let reopened = open( + accessor, + exports, + fd, + path, + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await? + .expect("reopen root"); + results.push(stat_size(accessor, exports, reopened, "../secret.txt").await?); + } + Ok(results) + }) + .await + .unwrap(); + // `sub/..` resolves lexically to the root of the chroot + expect_all( + results.into_iter().map(|r| (String::new(), r)).collect(), + Ok(size(JAIL_SECRET)), + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn symlinked_directory_out_of_the_jail_cannot_be_opened() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let result = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + open( + accessor, + exports, + fd, + "link-dir-outside", + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await + }) + .await + .unwrap(); + assert!(result.is_err(), "{result:?}"); +} + +// --- mutations --- + +#[tokio::test(flavor = "multi_thread")] +async fn files_are_created_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut results = vec![]; + for path in [ + "created.txt", + "/created-absolute.txt", + "../created-parent.txt", + "sub/../../created-sub.txt", + ] { + results.push( + open( + accessor, + exports, + fd, + path, + OpenFlags::CREATE, + DescriptorFlags::READ | DescriptorFlags::WRITE, + ) + .await? + .map(|_| ()), + ); + } + Ok(results) + }) + .await + .unwrap(); + assert_eq!(results, [Ok(()), Ok(()), Ok(()), Ok(())]); + for name in [ + "created.txt", + "created-absolute.txt", + "created-parent.txt", + "created-sub.txt", + ] { + assert!(fixture.jail(name).is_file(), "{name}"); + } + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn files_are_not_created_through_symlinks_out_of_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let mut results = vec![]; + for path in ["link-dir-outside/created.txt", "link-outside"] { + results.push( + open( + accessor, + exports, + fd, + path, + OpenFlags::CREATE | OpenFlags::TRUNCATE, + DescriptorFlags::WRITE, + ) + .await? + .map(|_| ()), + ); + } + Ok(results) + }) + .await + .unwrap(); + for result in results { + assert!(result.is_err(), "{result:?}"); + } + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn directories_are_created_and_removed_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + Ok(vec![ + descriptor + .call_create_directory_at(accessor, fd, "../escaped".into()) + .await?, + descriptor + .call_create_directory_at(accessor, fd, "/../../escaped-too".into()) + .await?, + descriptor + .call_create_directory_at(accessor, fd, "link-dir-outside/escaped".into()) + .await?, + descriptor + .call_remove_directory_at(accessor, fd, "../escaped-too".into()) + .await?, + ]) + }) + .await + .unwrap(); + assert_eq!(results[0], Ok(())); + assert_eq!(results[1], Ok(())); + assert!(results[2].is_err(), "{:?}", results[2]); + assert_eq!(results[3], Ok(())); + assert!(fixture.jail("escaped").is_dir()); + assert!(!fixture.jail("escaped-too").exists()); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn the_jail_itself_cannot_be_removed() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + let mut results = vec![]; + for path in ["/", ".", "..", "../jail", "/../.."] { + results.push( + descriptor + .call_remove_directory_at(accessor, fd, path.into()) + .await?, + ); + } + Ok(results) + }) + .await + .unwrap(); + for result in results { + assert!(result.is_err(), "{result:?}"); + } + assert!(fixture.jail("inside.txt").is_file()); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn unlink_stays_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + Ok(vec![ + // the jail's secret, not the host's + descriptor + .call_unlink_file_at(accessor, fd, "../secret.txt".into()) + .await?, + // removes the link itself, not its target + descriptor + .call_unlink_file_at(accessor, fd, "link-outside".into()) + .await?, + descriptor + .call_unlink_file_at(accessor, fd, "link-dir-outside/secret.txt".into()) + .await?, + descriptor + .call_unlink_file_at(accessor, fd, "../jail-sibling/secret.txt".into()) + .await?, + ]) + }) + .await + .unwrap(); + assert_eq!(results[0], Ok(())); + assert_eq!(results[1], Ok(())); + assert!(results[2].is_err(), "{:?}", results[2]); + assert_eq!(results[3], Err(ErrorCode::NoEntry)); + assert!(!fixture.jail("secret.txt").exists()); + assert!(fs::symlink_metadata(fixture.jail("link-outside")).is_err()); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn rename_stays_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let sub = open( + accessor, + exports, + fd, + "sub", + OpenFlags::DIRECTORY, + DescriptorFlags::READ | DescriptorFlags::MUTATE_DIRECTORY, + ) + .await? + .expect("open sub"); + let descriptor = exports.wasi_filesystem_types().descriptor(); + Ok(vec![ + descriptor + .call_rename_at(accessor, fd, "inside.txt".into(), fd, "../moved.txt".into()) + .await?, + // the new path is relative to the new descriptor + descriptor + .call_rename_at(accessor, fd, "moved.txt".into(), sub, "moved.txt".into()) + .await?, + descriptor + .call_rename_at( + accessor, + sub, + "moved.txt".into(), + fd, + "/../../moved-back.txt".into(), + ) + .await?, + descriptor + .call_rename_at( + accessor, + fd, + "moved-back.txt".into(), + fd, + "link-dir-outside/stolen.txt".into(), + ) + .await?, + descriptor + .call_rename_at( + accessor, + fd, + "../secret.txt".into(), + fd, + "link-dir-outside/secret.txt".into(), + ) + .await?, + descriptor + .call_rename_at(accessor, sub, "../inside.txt".into(), fd, "x.txt".into()) + .await?, + ]) + }) + .await + .unwrap(); + assert_eq!(results[0], Ok(())); + assert_eq!(results[1], Ok(())); + assert_eq!(results[2], Ok(())); + assert!(results[3].is_err(), "{:?}", results[3]); + assert!(results[4].is_err(), "{:?}", results[4]); + assert_eq!(results[5], Err(ErrorCode::NotPermitted)); + assert_eq!( + fs::read_to_string(fixture.jail("moved-back.txt")).unwrap(), + INSIDE + ); + assert_eq!( + fs::read_to_string(fixture.jail("secret.txt")).unwrap(), + JAIL_SECRET + ); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn hard_links_stay_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + Ok(vec![ + descriptor + .call_link_at( + accessor, + fd, + PathFlags::empty(), + "../secret.txt".into(), + fd, + "../hard.txt".into(), + ) + .await?, + descriptor + .call_link_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "link-outside".into(), + fd, + "hard-outside.txt".into(), + ) + .await?, + descriptor + .call_link_at( + accessor, + fd, + PathFlags::empty(), + "inside.txt".into(), + fd, + "link-dir-outside/hard.txt".into(), + ) + .await?, + ]) + }) + .await + .unwrap(); + assert_eq!(results[0], Ok(())); + assert!(results[1].is_err(), "{:?}", results[1]); + assert!(results[2].is_err(), "{:?}", results[2]); + // a link to the jail's secret, not the host's + assert_eq!( + fs::read_to_string(fixture.jail("hard.txt")).unwrap(), + JAIL_SECRET + ); + assert!(!fixture.jail("hard-outside.txt").exists()); + fixture.assert_outside_untouched(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn path_metadata_operations_stay_in_the_jail() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let (set_times, hashes) = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + use test_harness::bindings::exports::wasi::filesystem::types::NewTimestamp; + let set_times = vec![ + descriptor + .call_set_times_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "link-outside".into(), + NewTimestamp::Now, + NewTimestamp::Now, + ) + .await?, + descriptor + .call_set_times_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "../secret.txt".into(), + NewTimestamp::Now, + NewTimestamp::Now, + ) + .await?, + ]; + let hashes = vec![ + descriptor + .call_metadata_hash_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "../secret.txt".into(), + ) + .await?, + descriptor + .call_metadata_hash_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "secret.txt".into(), + ) + .await?, + descriptor + .call_metadata_hash_at( + accessor, + fd, + PathFlags::SYMLINK_FOLLOW, + "link-outside".into(), + ) + .await?, + ]; + Ok((set_times, hashes)) + }) + .await + .unwrap(); + assert!(set_times[0].is_err(), "{:?}", set_times[0]); + assert_eq!(set_times[1], Ok(())); + assert!(hashes[0].is_ok()); + assert_eq!(hashes[0], hashes[1]); + assert!(hashes[2].is_err(), "{:?}", hashes[2]); +} + +// --- configuration --- + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_may_be_relative() { + let fixture = Fixture::new(); + let mut chroot = fixture + .harness() + .config("path", "jail") + .build() + .await + .unwrap(); + let result = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + stat_size(accessor, exports, fd, "/inside.txt").await + }) + .await + .unwrap(); + assert_eq!(result, Ok(size(INSIDE))); +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_may_be_within_a_relative_preopen() { + let fixture = Fixture::new(); + let mut chroot = Harness::new("chroot") + .preopen(fixture.dir.path(), ".", Perms::ReadWrite) + .config("path", "jail") + .build() + .await + .unwrap(); + let result = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + stat_size(accessor, exports, fd, "../secret.txt").await + }) + .await + .unwrap(); + assert_eq!(result, Ok(size(JAIL_SECRET))); +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_selects_the_longest_preopen() { + let fixture = Fixture::new(); + let mut chroot = fixture + .harness() + .preopen(fixture.host("jail"), "/data", Perms::ReadWrite) + .config("path", "/data/sub") + .build() + .await + .unwrap(); + let result = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + stat_size(accessor, exports, fd, "/nested.txt").await + }) + .await + .unwrap(); + assert_eq!(result, Ok(size(NESTED))); +} + +#[tokio::test(flavor = "multi_thread")] +async fn chroot_is_no_more_capable_than_its_preopen() { + let fixture = Fixture::new(); + let mut chroot = Harness::new("chroot") + .preopen(fixture.dir.path(), "/", Perms::ReadOnly) + .config("path", "/jail") + .build() + .await + .unwrap(); + let (flags, created) = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + let flags = descriptor.call_get_flags(accessor, fd).await?; + let created = descriptor + .call_create_directory_at(accessor, fd, "new".into()) + .await?; + Ok((flags, created)) + }) + .await + .unwrap(); + assert_eq!(flags, Ok(DescriptorFlags::READ)); + assert_eq!(created, Err(ErrorCode::NotPermitted)); + assert!(!fixture.jail("new").exists()); +} + +/// The chroot traps rather than expose a directory it was not configured with. +async fn assert_config_traps(harness: Harness) { + let mut chroot = harness.build().await.unwrap(); + let result = chroot + .run(async |accessor, exports| { + exports + .wasi_filesystem_preopens() + .call_get_directories(accessor) + .await + }) + .await; + assert!( + result.is_err(), + "expected a trap, got {} preopens", + result.unwrap().len() + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_must_not_contain_parents() { + let fixture = Fixture::new(); + for path in ["..", "/jail/..", "/jail/../jail-sibling", "jail/../../"] { + assert_config_traps(fixture.harness().config("path", path)).await; + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_must_be_within_a_preopen() { + let fixture = Fixture::new(); + assert_config_traps(fixture.harness()).await; + assert_config_traps( + Harness::new("chroot") + .preopen(fixture.host("jail"), "/data", Perms::ReadWrite) + .config("path", "/jail"), + ) + .await; +} + +/// The root of a chroot whose directory can't be opened fails every operation, the directory is +/// opened by the first operation that needs it. +async fn assert_root_unusable(harness: Harness) { + let mut chroot = harness.build().await.unwrap(); + let results = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + Ok(vec![ + stat_size(accessor, exports, fd, "secret.txt") + .await? + .map(|_| ()), + stat_size(accessor, exports, fd, "/inside.txt") + .await? + .map(|_| ()), + descriptor.call_stat(accessor, fd).await?.map(|_| ()), + open( + accessor, + exports, + fd, + ".", + OpenFlags::DIRECTORY, + DescriptorFlags::READ, + ) + .await? + .map(|_| ()), + ]) + }) + .await + .unwrap(); + for result in results { + assert!(result.is_err(), "{result:?}"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_must_be_a_directory() { + let fixture = Fixture::new(); + assert_root_unusable(fixture.harness().config("path", "/missing")).await; + assert_root_unusable(fixture.harness().config("path", "/jail/inside.txt")).await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn config_path_must_not_leave_the_preopen_through_a_symlink() { + let fixture = Fixture::new(); + assert_root_unusable( + Harness::new("chroot") + .preopen(fixture.host("jail"), "/", Perms::ReadWrite) + .config("path", "/link-dir-outside"), + ) + .await; +} + +/// List the root before anything opened it, the listing is forwarded by the chroot rather than +/// passed through from the host. +async fn list_unopened_root( + fixture: &Fixture, + harness: Harness, +) -> (Vec, Result<(), ErrorCode>) { + let _ = fixture; + let mut chroot = harness.build().await.unwrap(); + chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let (entries, result) = exports + .wasi_filesystem_types() + .descriptor() + .call_read_directory(accessor, fd) + .await?; + let mut entries = test_harness::collect(accessor, entries)?; + let result = test_harness::resolve(accessor, result).await?; + let mut names = vec![]; + while let Some(entry) = entries.recv().await { + names.push(entry.name); + } + names.sort(); + Ok((names, result)) + }) + .await + .unwrap() +} + +#[tokio::test(flavor = "multi_thread")] +async fn root_is_listed_before_it_is_opened() { + let fixture = Fixture::new(); + let (names, result) = + list_unopened_root(&fixture, fixture.harness().config("path", "/jail")).await; + assert_eq!(result, Ok(())); + assert_eq!( + names, + [ + "inside.txt", + "link-absolute", + "link-dir-outside", + "link-host-absolute", + "link-inside", + "link-outside", + "secret.txt", + "sub", + ] + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn missing_root_fails_to_list() { + let fixture = Fixture::new(); + let (names, result) = + list_unopened_root(&fixture, fixture.harness().config("path", "/missing")).await; + assert_eq!(result, Err(ErrorCode::NoEntry)); + assert!(names.is_empty(), "{names:?}"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn root_is_not_a_file_before_it_is_opened() { + // `write-via-stream` and `append-via-stream` are forwarded the same way, but the harness can't + // give them data, a stream passed through to wasmtime-wasi needs its own Rust type, see + // `test_harness::resolve` + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + let (data, result) = chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let (data, result) = exports + .wasi_filesystem_types() + .descriptor() + .call_read_via_stream(accessor, fd, 0) + .await?; + let mut data = test_harness::collect(accessor, data)?; + let result = test_harness::resolve(accessor, result).await?; + let mut bytes = vec![]; + while let Some(byte) = data.recv().await { + bytes.push(byte); + } + Ok((bytes, result)) + }) + .await + .unwrap(); + // the root is a directory, reading it fails rather than trapping + assert!(result.is_err(), "{result:?}"); + assert!(data.is_empty(), "{data:?}"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn synchronous_operations_work_once_the_root_is_opened() { + let fixture = Fixture::new(); + let mut chroot = fixture.chroot().await; + chroot + .run(async |accessor, exports| { + let fd = root(accessor, exports).await?; + let descriptor = exports.wasi_filesystem_types().descriptor(); + descriptor.call_stat(accessor, fd).await?.expect("stat"); + descriptor.call_read_directory(accessor, fd).await?; + Ok(()) + }) + .await + .unwrap(); +} diff --git a/crates/componentized-rt/Cargo.toml b/crates/componentized-rt/Cargo.toml new file mode 100644 index 0000000..ad51231 --- /dev/null +++ b/crates/componentized-rt/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "componentized-rt" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +[features] +# background executor for futures started by sync exports, see `componentized_rt::executor` +executor = ["dep:wit-bindgen", "wit-bindgen/inter-task-wakeup"] + +[dependencies] +wit-bindgen = { workspace = true, optional = true } diff --git a/crates/componentized-rt/src/executor.rs b/crates/componentized-rt/src/executor.rs new file mode 100644 index 0000000..2853153 --- /dev/null +++ b/crates/componentized-rt/src/executor.rs @@ -0,0 +1,79 @@ +//! Runs background futures that outlive the export which started them. +//! +//! Sync exports, like `wasi:sockets/types#tcp-socket.listen`, have no component model task to +//! spawn futures into. A single cooperative thread runs `wit_bindgen::block_on` for the life of +//! the component and polls every background future. It is the only thread that suspends with +//! frames on the component's shadow stack, which is what makes sharing that stack sound, see +//! [`crate::thread::spawn`]. +//! +//! Requires the `executor` feature. Like [`crate::thread::spawn`], the component must be linked +//! with `--export-table`. + +use core::cell::RefCell; +use core::future::{Future, poll_fn}; +use core::pin::Pin; +use core::pin::pin; +use core::task::{Poll, Waker}; + +struct Executor { + /// Futures waiting to be picked up by the executor thread. + queue: Vec>>>, + /// `None` until the executor thread is started, then the waker to call when the queue grows. + waker: Option>, +} + +struct State(RefCell); + +// components are single threaded, cooperative threads only switch at explicit points +unsafe impl Sync for State {} + +static EXECUTOR: State = State(RefCell::new(Executor { + queue: Vec::new(), + waker: None, +})); + +/// Run `future` in the background until it completes. +/// +/// The component may not spawn any other threads, and its exports must either run to completion +/// or be callback based async exports, see [`crate::thread::spawn`]. +pub fn spawn(future: impl Future + 'static) { + let mut executor = EXECUTOR.0.borrow_mut(); + executor.queue.push(Box::pin(future)); + match &mut executor.waker { + None => { + executor.waker = Some(None); + // SAFETY: the executor thread is the only thread the component spawns, and every + // other task either runs to completion or is a callback based async export + unsafe { crate::thread::spawn(|| wit_bindgen::block_on(run())) }; + } + Some(waker) => { + if let Some(waker) = waker.take() { + drop(executor); + waker.wake(); + } + } + } +} + +/// Hand queued futures to wit-bindgen's local task set, forever. +async fn run() { + // `wit_bindgen::block_on` unwraps its waitable set when the task yields, and the set is only + // created once a waitable is registered. Spawning a future wakes the executor, so if every + // spawned future completes without registering a waitable the next yield panics. A read + // which never completes, the writer is never written or dropped, keeps a waitable registered + // from the first poll. + let (_writer, mut reader) = wit_bindgen::UnitStreamOps::new(); + let mut idle = pin!(reader.read(Vec::new())); + + poll_fn(|cx| { + let _ = idle.as_mut().poll(cx); + let mut executor = EXECUTOR.0.borrow_mut(); + for future in executor.queue.drain(..) { + // the future runs until it completes + wit_bindgen::spawn_local(future); + } + executor.waker = Some(Some(cx.waker().clone())); + Poll::<()>::Pending + }) + .await +} diff --git a/crates/componentized-rt/src/lib.rs b/crates/componentized-rt/src/lib.rs new file mode 100644 index 0000000..dd05f02 --- /dev/null +++ b/crates/componentized-rt/src/lib.rs @@ -0,0 +1,60 @@ +//! Runtime support for components. +//! +//! Cooperative component model threads, and a background executor for futures started by sync +//! exports. + +#[cfg(feature = "executor")] +pub mod executor; + +/// Cooperative component model threads. +/// +/// Threads are created with the component model's `thread.new-indirect` builtin, without any +/// support from a libc. Every thread runs on the component's one shadow stack, which is only +/// sound while at most one thread ever suspends with frames on that stack. Tasks which run to +/// completion, or which use wit-bindgen's callback based async exports, never do. +pub mod thread { + #[cfg(target_family = "wasm")] + #[link(wasm_import_module = "$root")] + unsafe extern "C" { + #[link_name = "[thread-new-indirect-v0]"] + fn thread_new_indirect(start: unsafe extern "C" fn(*mut u8), arg: *mut u8) -> u32; + #[link_name = "[thread-resume-later]"] + fn thread_resume_later(thread: u32); + } + + #[cfg(target_family = "wasm")] + unsafe extern "C" fn start(arg: *mut u8) { + let f = unsafe { Box::from_raw(arg as *mut Box) }; + f() + } + + /// Spawn a thread running `f`, it starts once the current thread yields or returns. + /// + /// Starting later means the current thread's frames have been popped from the shared stack + /// before the new thread pushes its own. + /// + /// `thread.new-indirect` calls through the module's function table, so the component must be + /// linked with `--export-table`, e.g. from its build script: + /// `cargo:rustc-link-arg-cdylib=--export-table`. + /// + /// # Safety + /// + /// The new thread shares the component's shadow stack. If it may suspend with frames on the + /// stack, for example by calling `wit_bindgen::block_on`, no other thread may do the same + /// while it is alive, and every task must run to completion or use callback based async + /// exports. + pub unsafe fn spawn(f: impl FnOnce() + 'static) { + let f: Box> = Box::new(Box::new(f)); + let arg = Box::into_raw(f) as *mut u8; + #[cfg(target_family = "wasm")] + unsafe { + let thread = thread_new_indirect(start, arg); + thread_resume_later(thread); + } + #[cfg(not(target_family = "wasm"))] + { + let _ = arg; + unimplemented!("component model threads are only available on wasm") + } + } +} diff --git a/crates/test-harness/Cargo.toml b/crates/test-harness/Cargo.toml new file mode 100644 index 0000000..12d41de --- /dev/null +++ b/crates/test-harness/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "test-harness" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" +publish = false + +[dependencies] +tokio = { workspace = true } +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } diff --git a/crates/test-harness/src/lib.rs b/crates/test-harness/src/lib.rs new file mode 100644 index 0000000..35ff044 --- /dev/null +++ b/crates/test-harness/src/lib.rs @@ -0,0 +1,359 @@ +//! Test harness for the filesystem components. +//! +//! A [`Harness`] instantiates a component (`target/components/*/*.wasm`) with real directories +//! preopened from the host by wasmtime-wasi, values for `wasi:config/store`, and captured +//! `wasi:logging` output. Tests call the component's exported `wasi:filesystem` interfaces. + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::process::Command; +use std::sync::{Arc, Mutex}; +use std::task::Poll; + +use tokio::sync::{mpsc, oneshot}; +use wasmtime::component::{ + Accessor, Component, FutureConsumer, FutureReader, HasSelf, Lift, Linker, ResourceTable, + Source, StreamConsumer, StreamReader, StreamResult, +}; +use wasmtime::error::Context as _; +use wasmtime::{Config, Engine, Result, Store, StoreContextMut, bail, format_err}; +use wasmtime_wasi::{FsPerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; + +use crate::bindings::exports::wasi::filesystem::{preopens, types}; +use crate::bindings::wasi::config::store; +use crate::bindings::wasi::logging::logging; + +pub mod bindings { + wasmtime::component::bindgen!({ + path: "../../components/wit", + inline: " + package componentized:test-harness; + + world harness { + import wasi:config/store@0.2.0-rc.1; + import wasi:logging/logging@0.1.0-draft; + export wasi:filesystem/types@0.3.0; + export wasi:filesystem/preopens@0.3.0; + } + ", + world: "componentized:test-harness/harness", + exports: { default: async | store }, + additional_derives: [PartialEq], + }); +} + +pub use bindings::exports::wasi::filesystem::types::{ + DescriptorFlags, DescriptorStat, DescriptorType, ErrorCode, OpenFlags, PathFlags, +}; +pub use bindings::wasi::logging::logging::Level; +pub use wasmtime_wasi::FsPerms as Perms; + +/// Receive the items written to a guest stream on a channel. +/// +/// Only for streams the guest writes itself, see [`resolve`]. +pub fn collect( + accessor: &Accessor, + stream: StreamReader, +) -> Result> { + let (tx, rx) = mpsc::unbounded_channel(); + accessor.with(|store| stream.pipe(store, ChannelConsumer(tx)))?; + Ok(rx) +} + +/// Wait for the value of a guest future. +/// +/// Only for futures the guest writes itself. A future the guest passes through from a wasmtime-wasi +/// host import is transferred host to host, which requires the same Rust type on both ends, and +/// the harness' exported types are generated separately from wasmtime-wasi's. +pub async fn resolve( + accessor: &Accessor, + future: FutureReader, +) -> Result { + let (tx, rx) = oneshot::channel(); + accessor.with(|store| future.pipe(store, OneshotConsumer(Some(tx))))?; + rx.await + .map_err(|_| format_err!("future closed without a value")) +} + +struct OneshotConsumer(Option>); + +impl FutureConsumer for OneshotConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let (Some(item), Some(tx)) = (item, self.get_mut().0.take()) { + // the receiver is only dropped when the test stopped waiting + let _ = tx.send(item); + } + Poll::Ready(Ok(())) + } +} + +struct ChannelConsumer(mpsc::UnboundedSender); + +impl StreamConsumer for ChannelConsumer { + type Item = T; + + fn poll_consume( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + store: StoreContextMut, + mut source: Source<'_, T>, + _finish: bool, + ) -> Poll> { + let mut item = None; + source.read(store, &mut item)?; + if let Some(item) = item { + if self.0.send(item).is_err() { + return Poll::Ready(Ok(StreamResult::Dropped)); + } + } + Poll::Ready(Ok(StreamResult::Completed)) + } +} + +/// Root of the workspace, where the Makefile lives. +fn workspace_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +/// Path containing the built component. +pub fn component_path(name: &str) -> PathBuf { + workspace_dir().join(format!("target/components/{name}/{name}.wasm")) +} + +/// Rebuild the named component in `target/components/` with make, unless it was already built by +/// this process. +fn ensure_built(name: &str) -> Result<()> { + static BUILT: Mutex>> = Mutex::new(None); + + // hold the lock while building so concurrent tests don't run make over each other + let mut built = BUILT.lock().unwrap_or_else(|err| err.into_inner()); + let built = built.get_or_insert_with(HashSet::new); + if built.contains(name) { + return Ok(()); + } + + let target = format!("target/components/{name}/{name}.wasm"); + let output = Command::new("make") + .arg("-C") + .arg(workspace_dir()) + .arg(&target) + .output() + .context("failed to run make")?; + if !output.status.success() { + bail!( + "failed to build {target}:\n{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + built.insert(name.to_string()); + Ok(()) +} + +/// A message logged by the test subject via `wasi:logging`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LogEntry { + pub level: Level, + pub context: String, + pub message: String, +} + +/// Observations recorded while the test subject runs. +#[derive(Clone, Default)] +pub struct Recorder { + logs: Arc>>, +} + +impl Recorder { + /// Messages logged by the test subject, in order. + pub fn logs(&self) -> Vec { + self.logs.lock().unwrap().clone() + } +} + +pub struct Ctx { + wasi: WasiCtx, + table: ResourceTable, + config: Vec<(String, String)>, + recorder: Recorder, +} + +impl WasiView for Ctx { + fn ctx(&mut self) -> WasiCtxView<'_> { + WasiCtxView { + ctx: &mut self.wasi, + table: &mut self.table, + } + } +} + +impl store::Host for Ctx { + fn get(&mut self, key: String) -> Result, store::Error> { + Ok(self + .config + .iter() + .rev() + .find(|(k, _)| *k == key) + .map(|(_, v)| v.clone())) + } + + fn get_all(&mut self) -> Result, store::Error> { + Ok(self.config.clone()) + } +} + +impl logging::Host for Ctx { + fn log(&mut self, level: Level, context: String, message: String) { + self.recorder.logs.lock().unwrap().push(LogEntry { + level, + context, + message, + }); + } +} + +/// Builds a subject instance for a test. +pub struct Harness { + subject: String, + config: Vec<(String, String)>, + preopens: Vec<(PathBuf, String, FsPerms)>, +} + +impl Harness { + /// Test the named component from `target/components/`, e.g. `chroot`. + pub fn new(component_name: &str) -> Self { + Self { + subject: component_name.to_string(), + config: vec![], + preopens: vec![], + } + } + + /// Add a `wasi:config/store` value. + pub fn config(mut self, key: &str, value: &str) -> Self { + self.config.push((key.to_string(), value.to_string())); + self + } + + /// Preopen a host directory for the test subject under the guest path. + pub fn preopen( + mut self, + host_path: impl AsRef, + guest_path: &str, + perms: FsPerms, + ) -> Self { + self.preopens.push(( + host_path.as_ref().to_path_buf(), + guest_path.to_string(), + perms, + )); + self + } + + /// Instantiate the test subject. + pub async fn build(self) -> Result { + ensure_built(&self.subject)?; + + let mut config = Config::new(); + config.wasm_component_model_async(true); + // componentized-rt's background executor runs on a component model thread + config.wasm_component_model_threading(true); + let engine = Engine::new(&config)?; + + let path = component_path(&self.subject); + let component = Component::from_file(&engine, &path) + .with_context(|| format!("failed to read {}", path.display()))?; + + let mut linker = Linker::new(&engine); + wasmtime_wasi::p3::add_to_linker(&mut linker)?; + store::add_to_linker::<_, HasSelf>(&mut linker, |ctx| ctx)?; + logging::add_to_linker::<_, HasSelf>(&mut linker, |ctx| ctx)?; + + let mut wasi = WasiCtxBuilder::new(); + wasi.inherit_stdio(); + for (host_path, guest_path, perms) in &self.preopens { + wasi.preopened_dir(host_path, guest_path, *perms) + .with_context(|| format!("failed to preopen {}", host_path.display()))?; + } + + let recorder = Recorder::default(); + let mut store = Store::new( + &engine, + Ctx { + wasi: wasi.build(), + table: ResourceTable::new(), + config: self.config, + recorder: recorder.clone(), + }, + ); + let instance_pre = linker.instantiate_pre(&component)?; + let instance = instance_pre + .instantiate_async(&mut store) + .await + .with_context(|| format!("failed to instantiate {}", self.subject))?; + let exports = Exports { + types: types::GuestIndices::new(&instance_pre)?.load(&mut store, &instance)?, + preopens: preopens::GuestIndices::new(&instance_pre)?.load(&mut store, &instance)?, + }; + + Ok(TestSubject { + store, + exports, + recorder, + }) + } +} + +/// The test subject's exported interfaces. +pub struct Exports { + types: types::Guest, + preopens: preopens::Guest, +} + +impl Exports { + /// The exported `wasi:filesystem/types` interface. + pub fn wasi_filesystem_types(&self) -> &types::Guest { + &self.types + } + + /// The exported `wasi:filesystem/preopens` interface. + pub fn wasi_filesystem_preopens(&self) -> &preopens::Guest { + &self.preopens + } +} + +/// An instantiated test subject. +pub struct TestSubject { + store: Store, + exports: Exports, + recorder: Recorder, +} + +impl TestSubject { + /// Observations recorded by the logger. + pub fn recorder(&self) -> Recorder { + self.recorder.clone() + } + + /// Run a test body against the test subject's exports. + pub async fn run( + &mut self, + f: impl AsyncFnOnce(&Accessor, &Exports) -> Result + Send, + ) -> Result { + let exports = &self.exports; + self.store + .run_concurrent(async move |accessor| f(accessor, exports).await) + .await? + } +}