diff --git a/firecracker-container-vm/.gitignore b/firecracker-container-vm/.gitignore
new file mode 100644
index 0000000..c236ddc
--- /dev/null
+++ b/firecracker-container-vm/.gitignore
@@ -0,0 +1,4 @@
+/target/
+.deps/
+**/*.rs.bk
+Cargo.lock
diff --git a/firecracker-container-vm/Cargo.lock b/firecracker-container-vm/Cargo.lock
new file mode 100644
index 0000000..c313317
--- /dev/null
+++ b/firecracker-container-vm/Cargo.lock
@@ -0,0 +1,2724 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anes"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[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"
+
+[[package]]
+name = "bytes"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
+
+[[package]]
+name = "caps"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cast"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
+
+[[package]]
+name = "cc"
+version = "1.2.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
+dependencies = [
+ "find-msvc-tools",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "ciborium"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
+dependencies = [
+ "ciborium-io",
+ "ciborium-ll",
+ "serde",
+]
+
+[[package]]
+name = "ciborium-io"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
+
+[[package]]
+name = "ciborium-ll"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
+dependencies = [
+ "ciborium-io",
+ "half",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "criterion"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
+dependencies = [
+ "anes",
+ "cast",
+ "ciborium",
+ "clap",
+ "criterion-plot",
+ "is-terminal",
+ "itertools",
+ "num-traits",
+ "once_cell",
+ "oorandom",
+ "plotters",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "tinytemplate",
+ "walkdir",
+]
+
+[[package]]
+name = "criterion-plot"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
+dependencies = [
+ "cast",
+ "itertools",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "defmt"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
+dependencies = [
+ "bitflags 1.3.2",
+ "defmt-macros",
+]
+
+[[package]]
+name = "defmt-macros"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
+dependencies = [
+ "defmt-parser",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "defmt-parser"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
+dependencies = [
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "docker_credential"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d"
+dependencies = [
+ "base64 0.22.1",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "endian-type"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
+
+[[package]]
+name = "env_filter"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "jiff",
+ "log",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fc-oci-fs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.22.1",
+ "bytes",
+ "criterion",
+ "docker_credential",
+ "flate2",
+ "fuse-backend-rs",
+ "hex",
+ "http 1.4.2",
+ "indexed_deflate",
+ "libc",
+ "log",
+ "oci-distribution",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "tar",
+ "tempfile",
+ "thiserror 2.0.18",
+ "tokio",
+]
+
+[[package]]
+name = "fc-runner"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "env_logger",
+ "fc-oci-fs",
+ "fc-vhostfsd",
+ "flate2",
+ "fuse-backend-rs",
+ "log",
+ "nix 0.30.1",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tar",
+ "tempfile",
+ "tokio",
+ "vhost",
+ "vhost-user-backend",
+ "vm-memory",
+]
+
+[[package]]
+name = "fc-vhostfsd"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "env_logger",
+ "fc-oci-fs",
+ "fuse-backend-rs",
+ "log",
+ "nix 0.30.1",
+ "tokio",
+ "vhost",
+ "vhost-user-backend",
+ "virtio-bindings",
+ "virtio-queue",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fuse-backend-rs"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d8148185e731fa601a078e4ae8c742bf4b69e86e19136848780deaef08fd7d"
+dependencies = [
+ "arc-swap",
+ "bitflags 1.3.2",
+ "caps",
+ "lazy_static",
+ "libc",
+ "log",
+ "mio 0.8.11",
+ "nix 0.24.3",
+ "radix_trie",
+ "virtio-queue",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[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 = "gzip-header"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86848f4fd157d91041a62c78046fb7b248bcc2dce78376d436a1756e9a038577"
+dependencies = [
+ "crc32fast",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-auth"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+dependencies = [
+ "bytes",
+ "http 0.2.12",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.5.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
+dependencies = [
+ "futures-util",
+ "http 0.2.12",
+ "hyper",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+]
+
+[[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.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[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 = "indexed_deflate"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94285e6105b4fd185b5a9672737911a793c3a0b23ad7ac939b4ff9f9c2c637"
+dependencies = [
+ "gzip-header",
+ "miniz_oxide",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jiff"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34f877a98676d2fb664698d74cc6a51ce6c484ce8c770f05d0108ec9090aeb46"
+dependencies = [
+ "defmt",
+ "jiff-static",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde_core",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0666b5ab5ecaca213fc2a85b8c0083d9004e84ee2d5f9a7e0017aaf50986f25f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jwt"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f"
+dependencies = [
+ "base64 0.13.1",
+ "crypto-common",
+ "digest",
+ "hmac",
+ "serde",
+ "serde_json",
+ "sha2",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "memchr"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nibble_vec"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "nix"
+version = "0.24.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069"
+dependencies = [
+ "bitflags 1.3.2",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.13.0",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "oci-distribution"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a635cabf7a6eb4e5f13e9e82bd9503b7c2461bf277132e38638a935ebd684b4"
+dependencies = [
+ "bytes",
+ "chrono",
+ "futures-util",
+ "http 0.2.12",
+ "http-auth",
+ "jwt",
+ "lazy_static",
+ "olpc-cjson",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2",
+ "thiserror 1.0.69",
+ "tokio",
+ "tracing",
+ "unicase",
+]
+
+[[package]]
+name = "olpc-cjson"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083"
+dependencies = [
+ "serde",
+ "serde_json",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "oorandom"
+version = "11.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "plotters"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
+dependencies = [
+ "num-traits",
+ "plotters-backend",
+ "plotters-svg",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "plotters-backend"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
+
+[[package]]
+name = "plotters-svg"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
+dependencies = [
+ "plotters-backend",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
+dependencies = [
+ "portable-atomic",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+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 = "radix_trie"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
+dependencies = [
+ "endian-type",
+ "nibble_vec",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
+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 = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.13.0",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.11.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
+dependencies = [
+ "base64 0.21.7",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http 0.2.12",
+ "http-body",
+ "hyper",
+ "hyper-rustls",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls",
+ "rustls-pemfile",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "system-configuration",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+ "winreg",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags 2.13.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64 0.21.7",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[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.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[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",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[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 = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+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 = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[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 = "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.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[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",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinytemplate"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio 1.2.1",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.4",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "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",
+]
+
+[[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 = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[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 = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
+dependencies = [
+ "getrandom 0.4.3",
+ "js-sys",
+ "rand",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "vhost"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee90657203a8644e9a0860a0db6a7887d8ef0c7bc09fc22dfa4ae75df65bac86"
+dependencies = [
+ "bitflags 2.13.0",
+ "libc",
+ "uuid",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "vhost-user-backend"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5925983d8fb537752ad3e26604c0a17abfa5de77cb6773a096c8a959c9eca0f"
+dependencies = [
+ "libc",
+ "log",
+ "vhost",
+ "virtio-bindings",
+ "virtio-queue",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "virtio-bindings"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868"
+
+[[package]]
+name = "virtio-queue"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e358084f32ed165fddb41d98ff1b7ff3c08b9611d8d6114a1b422e2e85688baf"
+dependencies = [
+ "libc",
+ "log",
+ "virtio-bindings",
+ "vm-memory",
+ "vmm-sys-util",
+]
+
+[[package]]
+name = "vm-memory"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f39348a049689cabd3377cdd9182bf526ec76a6f823b79903896452e9d7a7380"
+dependencies = [
+ "arc-swap",
+ "libc",
+ "thiserror 2.0.18",
+ "winapi",
+]
+
+[[package]]
+name = "vmm-sys-util"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f"
+dependencies = [
+ "bitflags 1.3.2",
+ "libc",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[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.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "0.25.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[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.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "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_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[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_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[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_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winreg"
+version = "0.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
+dependencies = [
+ "cfg-if",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/firecracker-container-vm/Cargo.toml b/firecracker-container-vm/Cargo.toml
new file mode 100644
index 0000000..33c052f
--- /dev/null
+++ b/firecracker-container-vm/Cargo.toml
@@ -0,0 +1,46 @@
+[workspace]
+resolver = "2"
+members = [
+ "crates/fc-oci-fs",
+ "crates/fc-vhostfsd",
+ "crates/fc-runner",
+]
+
+[workspace.package]
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+authors = ["Example Contributors"]
+
+[workspace.dependencies]
+anyhow = "1"
+async-trait = "0.1"
+base64 = "0.22"
+bytes = "1"
+clap = { version = "4", features = ["derive"] }
+env_logger = "0.11"
+flate2 = "1"
+fuse-backend-rs = { version = "0.14", default-features = false, features = ["virtiofs"] }
+futures = "0.3"
+hex = "0.4"
+http = "1"
+indexed_deflate = "0.1"
+log = "0.4"
+oci-distribution = { version = "0.10", default-features = false, features = ["rustls-tls"] }
+reqwest = { version = "0.11", default-features = false, features = ["rustls-tls", "json", "stream", "blocking"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+sha2 = "0.10"
+tar = "0.4"
+tempfile = "3"
+thiserror = "2"
+tokio = { version = "1", features = ["full"] }
+vhost = "0.16"
+vhost-user-backend = "0.22"
+virtio-bindings = "0.2"
+virtio-queue = "0.17"
+vm-memory = "0.17"
+vmm-sys-util = "0.15"
+libc = "0.2"
+docker_credential = "1.4"
+criterion = { version = "0.5", features = ["html_reports"] }
diff --git a/firecracker-container-vm/README.md b/firecracker-container-vm/README.md
new file mode 100644
index 0000000..26c7ec2
--- /dev/null
+++ b/firecracker-container-vm/README.md
@@ -0,0 +1,326 @@
+# firecracker-container-vm
+
+Example **Firecracker VM runner** that uses a container image reference as the guest root filesystem, loading files **on demand** at runtime over HTTP range requests into gzip-compressed OCI layers.
+
+This implements the lazy-pull approach described in [dagdotdev registry explorer](https://github.com/jonjohnsonjr/dagdotdev/blob/main/pkg/explore/README.md):
+
+1. Build a **gzip zran-style index** (`indexed_deflate`) over each `tar+gzip` layer.
+2. Record a **tar table of contents** mapping paths to uncompressed byte offsets.
+3. On file read, fetch only the **compressed byte range** needed from the registry, seek in the gzip stream, and return file bytes.
+4. Serve the merged overlay via **virtio-fs** using [`vhost-user-backend`](https://crates.io/crates/vhost-user-backend) + [`fuse-backend-rs`](https://crates.io/crates/fuse-backend-rs).
+
+## Architecture
+
+```
+┌─────────────────────┐ vhost-user UDS ┌──────────────────────┐
+│ Firecracker (guest) │ ◄──────────────────────► │ fc-vhostfsd │
+│ virtio-fs driver │ │ vhost-user-backend │
+└─────────────────────┘ │ fuse-backend-rs │
+ │ OverlayFs (lazy) │
+ └──────────┬───────────┘
+ │ HTTP Range
+ ▼
+ ┌──────────────────────┐
+ │ OCI registry blobs │
+ │ (tar+gzip layers) │
+ └──────────────────────┘
+```
+
+### Crates
+
+| Crate | Role |
+|-------|------|
+| `fc-oci-fs` | Registry client, gzip index, tar TOC, overlay `FileSystem` |
+| `fc-vhostfsd` | vhost-user virtio-fs daemon |
+| `fc-runner` | Spawns `fc-vhostfsd` and configures Firecracker |
+
+## Prerequisites
+
+| Requirement | Notes |
+|-------------|-------|
+| Rust 1.96+ | `source /usr/local/cargo/env` in the devcontainer |
+| `/dev/kvm` read/write | `[ -w /dev/kvm ] && echo OK` — add yourself to the `kvm` group if needed |
+| Docker config (optional) | `~/.docker/config.json` for private registries |
+| Host tools for VM path | `git`, `curl`, `docker`, `sudo` (kernel + Firecracker builds use Firecracker's `devtool`) |
+
+**Important:** virtio-fs needs two things that official Firecracker release artifacts do not provide out of the box:
+
+1. A **guest `vmlinux` with `CONFIG_VIRTIO_FS=y`** — the CI kernels from S3 do not enable virtio-fs.
+2. A **Firecracker binary with generic vhost-user** ([PR #5773](https://github.com/firecracker-microvm/firecracker/pull/5773)) — release builds only expose vhost-user **block**, not virtio-fs.
+
+The scripts under `scripts/` automate both builds into `.deps/`.
+
+## Build this project
+
+```bash
+source /usr/local/cargo/env
+cd firecracker-container-vm
+cargo build --release
+```
+
+## Quickstart (full path)
+
+### 1. Host dependencies (Firecracker + vmlinux)
+
+One-shot (builds from source; needs Docker and sudo):
+
+```bash
+cd firecracker-container-vm
+./scripts/setup-host.sh
+```
+
+Or step by step:
+
+```bash
+export DEPS_DIR="$PWD/.deps"
+
+# Firecracker with PUT /vhost-user-devices/{id} (PR #5773 branch)
+./scripts/build-firecracker-virtiofs.sh
+
+# Guest kernel with virtio-fs enabled (patches Firecracker's 6.1 CI config)
+./scripts/build-vmlinux-virtiofs.sh
+```
+
+To download a **stock** Firecracker release only (no virtio-fs frontend):
+
+```bash
+./scripts/download-firecracker.sh # installs .deps/firecracker
+```
+
+### 2. Verify KVM
+
+```bash
+[ -r /dev/kvm ] && [ -w /dev/kvm ] && echo "KVM OK" || echo "Fix KVM access first"
+```
+
+### 3. Run the lazy rootfs daemon
+
+```bash
+./target/release/fc-vhostfsd \
+ --image docker.io/library/alpine:3.20 \
+ --socket /tmp/fc-vhostfs.sock \
+ --cache-dir /tmp/fc-oci-cache \
+ --tag rootfs \
+ --metrics-addr 127.0.0.1:9100
+```
+
+In another terminal, confirm metrics and that the socket exists:
+
+```bash
+curl -s localhost:9100/metrics | rg '^fc_startup_ready_milliseconds'
+ls -l /tmp/fc-vhostfs.sock
+```
+
+### 4. Boot Firecracker with the container image as rootfs
+
+```bash
+./target/release/fc-runner \
+ --image docker.io/library/alpine:3.20 \
+ --firecracker "$PWD/.deps/firecracker-virtiofs" \
+ --kernel "$PWD/.deps/vmlinux-virtiofs" \
+ --cache-dir /tmp/fc-oci-cache \
+ --vhost-socket /tmp/fc-vhostfs.sock \
+ --tag rootfs \
+ --memory-mib 512
+```
+
+Use `--dry-run` to print the Firecracker API payload without starting the VM (still starts `fc-vhostfsd`).
+
+Guest cmdline (set by `fc-runner`):
+
+```
+console=ttyS0 reboot=k panic=1 pci=off init=/bin/sh rootfstype=virtiofs root=/ root=rootfs
+```
+
+Alpine provides `/bin/sh` on the merged root. You should get a shell on the serial console when the VM starts.
+
+### 5. Smoke test without a VM (daemon only)
+
+If you only want to validate lazy pulls + vhost-user handshake:
+
+```bash
+cargo test
+# vhost-user protocol test against a local fixture layer (no KVM/registry)
+cargo test -p fc-runner --test vhost_e2e
+```
+
+## Run the vhost-fs daemon (reference)
+
+```bash
+./target/release/fc-vhostfsd \
+ --image docker.io/library/alpine:3.20 \
+ --socket /tmp/fc-vhostfs.sock \
+ --cache-dir /tmp/fc-oci-cache \
+ --tag rootfs
+```
+
+On first access to a layer, the daemon downloads the blob (if not cached), builds a gzip index + tar TOC, then serves files via range reads.
+
+### Registry authentication
+
+`fc-vhostfsd` / `fc-oci-fs` read credentials the same way the Docker CLI does:
+
+- `~/.docker/config.json` (or `$DOCKER_CONFIG/config.json`)
+- `auths` entries (inline base64 user/pass)
+- per-registry `credHelpers`
+- global `credsStore` (e.g. `docker-credential-gcr`, `osxkeychain`, `pass`)
+
+Run `docker login` (or your cloud provider's helper) once; private images should work without extra flags.
+
+## Metrics
+
+`fc-vhostfsd` exposes Prometheus text on `http://127.0.0.1:9100/metrics` by default (`--metrics-addr`).
+
+| Metric | Meaning |
+|--------|---------|
+| `fc_bytes_fetched_from_registry` | Compressed bytes actually pulled from the registry |
+| `fc_bytes_saved_vs_full_pull` | `sum(layer sizes) - fetched` (data you did **not** download vs a full `docker pull`) |
+| `fc_layer_compressed_bytes_total` | Total compressed layer bytes in the resolved image |
+| `fc_registry_range_requests_total` | HTTP range requests to blob URLs |
+| `fc_full_blob_downloads_total` | Layers fully copied to local cache (first-time index build) |
+| `fc_gzip_index_builds_total` | Gzip index builds |
+| `fc_fuse_requests_total` / `fc_fuse_reads_total` | virtio-fs / FUSE traffic |
+| `fc_startup_ready_milliseconds` | Image open → vhost socket listening |
+| `fc_process_rss_bytes` | Daemon resident memory |
+| `fc_cache_dir_bytes_on_disk` | Local cache size (blobs + indexes) |
+
+```bash
+curl -s localhost:9100/metrics | rg '^fc_'
+```
+
+## Which VMM can use this? (virtio-fs frontends)
+
+`fc-vhostfsd` is a **vhost-user backend**. Something in the VMM must act as the **vhost-user frontend** and connect to `--socket`. Your options:
+
+| Frontend | Status | Notes |
+|----------|--------|-------|
+| **Firecracker** (generic vhost-user) | Needs recent build | [PR #5773](https://github.com/firecracker-microvm/firecracker/pull/5773) adds `PUT /vhost-user-devices/{id}` so virtio-fs works without native Firecracker virtio-fs code. `fc-runner` targets this API. |
+| **Cloud Hypervisor** | Works today | First-class virtio-fs + vhost-user; point `--socket` at the same path. No Firecracker-specific API. |
+| **QEMU** | Works today | `virtiofsd` / custom daemon via `-chardev socket` + `vhost-user-fs-pci` device. |
+| **crosvm** | Works today | Can run vhost-user fs backends against a virtio-fs device. |
+| **Stock Firecracker (released)** | No virtio-fs | Only block/net/vsock unless you build from the generic vhost-user branch. |
+
+**You do not need to change `fc-vhostfsd` between these** — only the VMM configuration differs. `fc-runner` is Firecracker-specific; for Cloud Hypervisor or QEMU, run `fc-vhostfsd` manually and wire the socket in that VMM's config.
+
+## Guest kernel (`vmlinux`)
+
+You need a **Linux guest kernel with virtio-fs over virtio-mmio** (Firecracker's bus).
+
+### Do not use the stock CI vmlinux for this example
+
+Firecracker's [getting-started guide](https://github.com/firecracker-microvm/firecracker/blob/main/docs/getting-started.md) downloads kernels from S3:
+
+```bash
+ARCH="$(uname -m)"
+release_url="https://github.com/firecracker-microvm/firecracker/releases"
+latest_version=$(basename "$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${release_url}/latest")")
+CI_VERSION=${latest_version%.*}
+latest_kernel_key=$(curl "http://spec.ccfc.min.s3.amazonaws.com/?prefix=firecracker-ci/$CI_VERSION/$ARCH/vmlinux-&list-type=2" \
+ | grep -oP "(?<=)(firecracker-ci/$CI_VERSION/$ARCH/vmlinux-[0-9]+\.[0-9]+\.[0-9]{1,3})(?=)" \
+ | sort -V | tail -1)
+wget -O vmlinux-ci "https://s3.amazonaws.com/spec.ccfc.min/${latest_kernel_key}"
+```
+
+That kernel is fine for block-device rootfs smoke tests, but its config has `# CONFIG_VIRTIO_FS is not set`. Use `./scripts/build-vmlinux-virtiofs.sh` instead.
+
+### Recommended: build virtio-fs-enabled vmlinux
+
+```bash
+./scripts/build-vmlinux-virtiofs.sh
+# output: .deps/vmlinux-virtiofs
+```
+
+This clones Firecracker, enables `CONFIG_VIRTIO_FS=y` in the `6.1` CI guest config, and runs `./tools/devtool build_ci_artifacts kernels 6.1`.
+
+Minimum kernel options (already present in Firecracker CI configs except virtio-fs):
+
+- `CONFIG_VIRTIO_MMIO=y`
+- `CONFIG_VIRTIO_FS=y`
+- `CONFIG_FUSE_FS=y`
+- `CONFIG_SERIAL_8250_CONSOLE=y` for `console=ttyS0`
+
+Manual build details: [Firecracker rootfs and kernel setup](https://github.com/firecracker-microvm/firecracker/blob/main/docs/rootfs-and-kernel-setup.md).
+
+## Firecracker binary
+
+### For virtio-fs (`fc-runner`)
+
+Released binaries from [GitHub releases](https://github.com/firecracker-microvm/firecracker/releases) do **not** include `PUT /vhost-user-devices/{id}` yet. Build from PR #5773:
+
+```bash
+./scripts/build-firecracker-virtiofs.sh
+# output: .deps/firecracker-virtiofs
+```
+
+### Download latest official release (block/net only)
+
+```bash
+./scripts/download-firecracker.sh
+# output: .deps/firecracker
+curl -fsSL https://github.com/firecracker-microvm/firecracker/releases/latest/download/firecracker-$(curl -fsSLI -o /dev/null -w '%{url_effective}' https://github.com/firecracker-microvm/firecracker/releases/latest | xargs basename)-$(uname -m).tgz | tar -xz
+```
+
+Or manually (from upstream docs):
+
+```bash
+ARCH="$(uname -m)"
+release_url="https://github.com/firecracker-microvm/firecracker/releases"
+latest=$(basename "$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${release_url}/latest")")
+curl -fsSL "${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz" | tar -xz
+mv "release-${latest}-${ARCH}/firecracker-${latest}-${ARCH}" firecracker
+```
+
+## Run the Firecracker example (reference)
+
+```bash
+./target/release/fc-runner \
+ --image docker.io/library/alpine:3.20 \
+ --firecracker "$PWD/.deps/firecracker-virtiofs" \
+ --kernel "$PWD/.deps/vmlinux-virtiofs" \
+ --cache-dir /tmp/fc-oci-cache \
+ --dry-run
+```
+
+Without `--dry-run`, `fc-runner` configures Firecracker via its HTTP API:
+
+- `PUT /boot-source`
+- `PUT /machine-config`
+- `PUT /vhost-user-devices/rootfs` (virtio-fs via generic vhost-user frontend)
+- `PUT /actions` (`InstanceStart`)
+
+Guest kernel cmdline includes `rootfstype=virtiofs root=/ root=rootfs`.
+
+## Tests
+
+```bash
+cargo test
+```
+
+Integration test `vhost_user_virtiofs_daemon_handshake` verifies the vhost-user protocol end-to-end against a local fixture layer (no registry/KVM required).
+
+## Benchmarks
+
+```bash
+cargo bench -p fc-oci-fs
+```
+
+Benchmarks build a synthetic `tar.gz` layer with hundreds of files and measure:
+
+- **read_last_file_1kb** — random read of the last tar entry (exercises gzip seek + range-style decompression)
+- **lookup_opt_data_file** — FUSE lookup through the overlay
+
+## Design notes & limitations (example code)
+
+This is an **example** implementation, not production hardened:
+
+- **Read-only** rootfs; no whiteout/opaque-dir completeness guarantees beyond basic `.wh.*` handling
+- **Single-platform** manifests only (no OCI index platform selection)
+- Layer blobs are cached to disk on first full fetch; subsequent reads use the on-disk gzip index
+- `fc-runner` is Firecracker-specific; other VMMs run `fc-vhostfsd` directly (see table above)
+
+## References
+
+- [dagdotdev explore README](https://github.com/jonjohnsonjr/dagdotdev/blob/main/pkg/explore/README.md) — gzip zran / range request strategy
+- [`indexed_deflate`](https://crates.io/crates/indexed_deflate) — gzip random access indexes
+- [`vhost-user-backend`](https://crates.io/crates/vhost-user-backend) — Rust vhost-user daemon framework
+- [Firecracker generic vhost-user](https://github.com/firecracker-microvm/firecracker/pull/5773)
diff --git a/firecracker-container-vm/crates/fc-oci-fs/Cargo.toml b/firecracker-container-vm/crates/fc-oci-fs/Cargo.toml
new file mode 100644
index 0000000..5ec3424
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "fc-oci-fs"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+description = "On-demand OCI container image filesystem with gzip zran-style seeking"
+
+[dependencies]
+anyhow.workspace = true
+async-trait.workspace = true
+base64.workspace = true
+bytes.workspace = true
+flate2.workspace = true
+fuse-backend-rs = { workspace = true, default-features = false, features = ["virtiofs"] }
+hex.workspace = true
+http.workspace = true
+docker_credential.workspace = true
+indexed_deflate.workspace = true
+libc.workspace = true
+log.workspace = true
+oci-distribution.workspace = true
+reqwest.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+sha2.workspace = true
+tar.workspace = true
+thiserror.workspace = true
+tokio.workspace = true
+
+[dev-dependencies]
+criterion.workspace = true
+flate2.workspace = true
+tempfile.workspace = true
+tokio = { workspace = true, features = ["test-util"] }
+
+[[bench]]
+name = "layer_read"
+harness = false
diff --git a/firecracker-container-vm/crates/fc-oci-fs/benches/layer_read.rs b/firecracker-container-vm/crates/fc-oci-fs/benches/layer_read.rs
new file mode 100644
index 0000000..a9d180b
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/benches/layer_read.rs
@@ -0,0 +1,88 @@
+use std::path::Path;
+use std::time::Instant;
+
+use criterion::{black_box, criterion_group, criterion_main, Criterion};
+use fc_oci_fs::{ImageFs, IndexedLayer, LayerSource};
+use fc_oci_fs::registry::LocalBlob;
+use flate2::write::GzEncoder;
+use flate2::Compression;
+use tar::Builder;
+use tempfile::tempdir;
+
+fn build_large_layer(dir: &Path, files: usize) -> std::path::PathBuf {
+ let tar_path = dir.join("layer.tar");
+ let gz_path = dir.join("layer.tar.gz");
+ {
+ let tar = std::fs::File::create(&tar_path).unwrap();
+ let mut builder = Builder::new(tar);
+ for i in 0..files {
+ let path = format!("opt/data/file_{i:05}.txt");
+ let payload = format!("payload-{i}-{}\n", "x".repeat(1024));
+ let bytes = payload.as_bytes();
+ let mut header = tar::Header::new_gnu();
+ header.set_size(bytes.len() as u64);
+ header.set_mode(0o644);
+ header.set_cksum();
+ builder.append_data(&mut header, path, bytes).unwrap();
+ }
+ builder.finish().unwrap();
+ }
+ let mut gz = GzEncoder::new(Vec::new(), Compression::default());
+ std::io::copy(
+ &mut std::fs::File::open(&tar_path).unwrap(),
+ &mut gz,
+ )
+ .unwrap();
+ std::fs::write(&gz_path, gz.finish().unwrap()).unwrap();
+ gz_path
+}
+
+fn bench_layer_read(c: &mut Criterion) {
+ let dir = tempdir().unwrap();
+ let gz = build_large_layer(dir.path(), 200);
+ let layer = IndexedLayer::open(
+ "bench",
+ LayerSource::Local(LocalBlob::open(&gz).unwrap()),
+ dir.path().join("cache"),
+ )
+ .unwrap();
+ let last = layer
+ .toc()
+ .entries()
+ .max_by_key(|e| e.offset)
+ .unwrap()
+ .clone();
+
+ c.bench_function("read_last_file_1kb", |b| {
+ b.iter(|| {
+ let mut buf = vec![0u8; 1024];
+ let n = layer.read_file(black_box(&last), 0, &mut buf).unwrap();
+ black_box(n);
+ })
+ });
+}
+
+fn bench_overlay_lookup(c: &mut Criterion) {
+ let dir = tempdir().unwrap();
+ let gz = build_large_layer(dir.path(), 50);
+ let fs = ImageFs::open_local_layer("bench", &gz, dir.path().join("cache")).unwrap();
+ use fuse_backend_rs::api::filesystem::{Context, FileSystem, ROOT_ID};
+ use std::ffi::CString;
+ let ctx = Context {
+ uid: 0,
+ gid: 0,
+ pid: 1,
+ };
+ c.bench_function("lookup_opt_data_file", |b| {
+ b.iter(|| {
+ let entry = fs
+ .overlay
+ .lookup(&ctx, ROOT_ID, &CString::new("opt").unwrap())
+ .unwrap();
+ black_box(entry.inode);
+ })
+ });
+}
+
+criterion_group!(benches, bench_layer_read, bench_overlay_lookup);
+criterion_main!(benches);
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/docker_auth.rs b/firecracker-container-vm/crates/fc-oci-fs/src/docker_auth.rs
new file mode 100644
index 0000000..ca880f3
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/docker_auth.rs
@@ -0,0 +1,88 @@
+use docker_credential::DockerCredential;
+use oci_distribution::secrets::RegistryAuth;
+use oci_distribution::Reference;
+
+use crate::error::{Error, Result};
+
+/// Resolved registry credentials suitable for both `oci-distribution` and raw HTTP.
+#[derive(Debug, Clone)]
+pub enum ResolvedAuth {
+ Anonymous,
+ Basic { username: String, password: String },
+ Bearer(String),
+}
+
+impl ResolvedAuth {
+ pub fn for_reference(reference: &Reference) -> Result {
+ let keys = registry_lookup_keys(reference);
+ for key in keys {
+ match docker_credential::get_credential(&key) {
+ Ok(DockerCredential::UsernamePassword(username, password)) => {
+ return Ok(ResolvedAuth::Basic { username, password });
+ }
+ Ok(DockerCredential::IdentityToken(token)) => {
+ return Ok(ResolvedAuth::Bearer(token));
+ }
+ Err(docker_credential::CredentialRetrievalError::ConfigNotFound)
+ | Err(docker_credential::CredentialRetrievalError::NoCredentialConfigured) => {
+ continue
+ }
+ Err(e) => return Err(Error::Registry(format!("docker credential {key}: {e}"))),
+ }
+ }
+ Ok(ResolvedAuth::Anonymous)
+ }
+
+ pub fn to_oci_auth(&self) -> RegistryAuth {
+ match self {
+ ResolvedAuth::Anonymous => RegistryAuth::Anonymous,
+ ResolvedAuth::Basic { username, password } => {
+ RegistryAuth::Basic(username.clone(), password.clone())
+ }
+ // oci-distribution only supports Basic; identity tokens are applied on HTTP directly.
+ ResolvedAuth::Bearer(token) => RegistryAuth::Basic(String::new(), token.clone()),
+ }
+ }
+
+ pub fn authorization_header(&self) -> Option {
+ match self {
+ ResolvedAuth::Anonymous => None,
+ ResolvedAuth::Basic { username, password } => Some(basic_auth_header(username, password)),
+ ResolvedAuth::Bearer(token) => Some(format!("Bearer {token}")),
+ }
+ }
+}
+
+fn basic_auth_header(username: &str, password: &str) -> String {
+ use base64::Engine as _;
+ let raw = format!("{username}:{password}");
+ format!(
+ "Basic {}",
+ base64::engine::general_purpose::STANDARD.encode(raw.as_bytes())
+ )
+}
+
+/// Keys tried when looking up `~/.docker/config.json` / cred helpers for a reference.
+fn registry_lookup_keys(reference: &Reference) -> Vec {
+ let registry = reference.resolve_registry();
+ let mut keys = Vec::new();
+ if registry == "docker.io" || registry == "index.docker.io" {
+ keys.push("https://index.docker.io/v1/".to_string());
+ }
+ keys.push(format!("https://{registry}"));
+ keys.push(format!("https://{registry}/v1/"));
+ keys.push(registry.to_string());
+ keys
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn docker_hub_lookup_keys() {
+ let reference: Reference = "docker.io/library/alpine:latest".parse().unwrap();
+ let keys = registry_lookup_keys(&reference);
+ assert!(keys.iter().any(|k| k.contains("index.docker.io")));
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/error.rs b/firecracker-container-vm/crates/fc-oci-fs/src/error.rs
new file mode 100644
index 0000000..f95b75a
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/error.rs
@@ -0,0 +1,35 @@
+use thiserror::Error;
+
+#[derive(Debug, Error)]
+pub enum Error {
+ #[error("registry error: {0}")]
+ Registry(String),
+
+ #[error("oci distribution error: {0}")]
+ Oci(#[from] oci_distribution::errors::OciDistributionError),
+
+ #[error("http error: {0}")]
+ Http(#[from] reqwest::Error),
+
+ #[error("io error: {0}")]
+ Io(#[from] std::io::Error),
+
+ #[error("gzip index error: {0}")]
+ GzIndex(#[from] indexed_deflate::Error),
+
+ #[error("invalid image reference: {0}")]
+ InvalidReference(String),
+
+ #[error("path not found: {0}")]
+ NotFound(String),
+
+ #[error("unsupported layer media type: {0}")]
+ UnsupportedMediaType(String),
+
+ #[error("json error: {0}")]
+ Json(#[from] serde_json::Error),
+ #[error("{0}")]
+ Other(String),
+}
+
+pub type Result = std::result::Result;
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/gz/layer.rs b/firecracker-container-vm/crates/fc-oci-fs/src/gz/layer.rs
new file mode 100644
index 0000000..fa72810
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/gz/layer.rs
@@ -0,0 +1,235 @@
+use std::fs::File;
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+use std::time::Instant;
+
+use indexed_deflate::{AccessPointSpan, GzDecoder, GzIndexBuilder};
+use serde::{Deserialize, Serialize};
+
+use crate::error::{Error, Result};
+use crate::metrics;
+use crate::registry::{LocalBlob, RangeBlob};
+use crate::tar_index::{TarEntry, TarIndex};
+
+/// Source bytes for a single OCI image layer.
+pub enum LayerSource {
+ Remote(RangeBlob),
+ Local(LocalBlob),
+}
+
+impl LayerSource {
+ pub fn len(&self) -> u64 {
+ match self {
+ LayerSource::Remote(blob) => blob.len(),
+ LayerSource::Local(blob) => blob.len(),
+ }
+ }
+}
+
+impl Read for LayerSource {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ match self {
+ LayerSource::Remote(blob) => blob.read(buf),
+ LayerSource::Local(blob) => blob.read(buf),
+ }
+ }
+}
+
+impl Seek for LayerSource {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result {
+ match self {
+ LayerSource::Remote(blob) => blob.seek(pos),
+ LayerSource::Local(blob) => blob.seek(pos),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct CachedLayerIndex {
+ digest: String,
+ toc: Vec,
+}
+
+/// A gzip-indexed OCI layer with a tar table-of-contents for random file access.
+///
+/// This follows the dagdotdev explore approach: build a zran-style gzip index
+/// (via `indexed_deflate`) and record tar entry offsets in the uncompressed stream.
+pub struct IndexedLayer {
+ digest: String,
+ blob_path: PathBuf,
+ index_path: PathBuf,
+ toc_path: PathBuf,
+ toc: TarIndex,
+}
+
+impl IndexedLayer {
+ pub fn digest(&self) -> &str {
+ &self.digest
+ }
+
+ pub fn toc(&self) -> &TarIndex {
+ &self.toc
+ }
+
+ pub fn open(
+ digest: impl Into,
+ source: LayerSource,
+ cache_dir: impl AsRef,
+ ) -> Result {
+ let digest = digest.into();
+ let cache_dir = cache_dir.as_ref().to_path_buf();
+ std::fs::create_dir_all(&cache_dir)?;
+ let index_path = cache_dir.join(format!("{digest}.gz.idx"));
+ let toc_path = cache_dir.join(format!("{digest}.toc.json"));
+
+ let toc = if index_path.exists() && toc_path.exists() {
+ let data = std::fs::read_to_string(&toc_path)?;
+ let cached: CachedLayerIndex = serde_json::from_str(&data)?;
+ TarIndex::from_entries(cached.toc)
+ } else {
+ build_index_and_toc(&digest, source, &index_path, &toc_path)?
+ };
+
+ Ok(Self {
+ digest,
+ blob_path: index_path.with_extension("blob"),
+ index_path,
+ toc_path,
+ toc,
+ })
+ }
+
+ pub fn read_file(&self, entry: &TarEntry, offset: u64, buf: &mut [u8]) -> Result {
+ if offset >= entry.size {
+ return Ok(0);
+ }
+ let to_read = buf.len().min((entry.size - offset) as usize);
+ let gz = File::open(&self.blob_path)?;
+ let index = File::open(&self.index_path)?;
+ let mut decoder = GzDecoder::new(gz, index)?;
+ decoder.seek(SeekFrom::Start(entry.offset + offset))?;
+ decoder
+ .read_exact(&mut buf[..to_read])
+ .map_err(Error::from)?;
+ Ok(to_read)
+ }
+}
+
+fn build_index_and_toc(
+ digest: &str,
+ mut source: LayerSource,
+ index_path: &Path,
+ toc_path: &Path,
+) -> Result {
+ let blob_path = index_path.with_extension("blob");
+ if !blob_path.exists() {
+ let copied = std::io::copy(&mut source, &mut File::create(&blob_path)?)?;
+ metrics::record_full_blob_download(copied);
+ }
+
+ let index_started = Instant::now();
+ let gz = File::open(&blob_path)?;
+ let mut index_file = File::options()
+ .create(true)
+ .truncate(true)
+ .read(true)
+ .write(true)
+ .open(index_path)?;
+
+ let mut builder = GzIndexBuilder::new(gz, &mut index_file, AccessPointSpan::default())?;
+
+ let mut entries = Vec::new();
+ let mut archive = tar::Archive::new(&mut builder);
+ for entry in archive.entries_with_seek()? {
+ let entry = entry?;
+ let path = entry
+ .path()
+ .map_err(|e| Error::Other(e.to_string()))?
+ .to_string_lossy()
+ .into_owned();
+ let offset = entry.raw_file_position();
+ let size = entry.header().size()?;
+ let mode = entry.header().mode()?;
+ let entry_type = entry.header().entry_type();
+ let link_target = entry.header().link_name()?.map(|p| p.to_string_lossy().into_owned());
+ entries.push(TarEntry {
+ path: normalize_tar_path(&path),
+ offset,
+ size,
+ mode,
+ entry_type: format!("{entry_type:?}"),
+ link_target,
+ });
+ }
+ builder.finish()?;
+ metrics::record_index_build(index_started.elapsed());
+
+ let toc = TarIndex::from_entries(entries);
+ let cached = CachedLayerIndex {
+ digest: digest.to_string(),
+ toc: toc.entries_cloned(),
+ };
+ std::fs::write(toc_path, serde_json::to_string_pretty(&cached)?)?;
+ Ok(toc)
+}
+
+fn normalize_tar_path(path: &str) -> String {
+ let trimmed = path.trim_start_matches("./");
+ if trimmed.starts_with('/') {
+ trimmed.to_string()
+ } else {
+ format!("/{trimmed}")
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use flate2::write::GzEncoder;
+ use flate2::Compression;
+ use tar::Builder;
+ use tempfile::tempdir;
+
+ fn sample_layer(dir: &Path) -> PathBuf {
+ let tar_path = dir.join("layer.tar");
+ let gz_path = dir.join("layer.tar.gz");
+ {
+ let tar = File::create(&tar_path).unwrap();
+ let mut builder = Builder::new(tar);
+ let data = b"hello from lazy layer";
+ let mut header = tar::Header::new_gnu();
+ header.set_size(data.len() as u64);
+ header.set_mode(0o644);
+ header.set_cksum();
+ builder
+ .append_data(&mut header, "etc/hostname", &data[..])
+ .unwrap();
+ builder.finish().unwrap();
+ }
+ {
+ let mut gz = GzEncoder::new(Vec::new(), Compression::default());
+ let mut input = File::open(&tar_path).unwrap();
+ std::io::copy(&mut input, &mut gz).unwrap();
+ std::fs::write(&gz_path, gz.finish().unwrap()).unwrap();
+ }
+ gz_path
+ }
+
+ #[test]
+ fn builds_index_and_reads_file() {
+ let dir = tempdir().unwrap();
+ let gz_path = sample_layer(dir.path());
+ let cache = dir.path().join("cache");
+ let layer = IndexedLayer::open(
+ "deadbeef",
+ LayerSource::Local(LocalBlob::open(&gz_path).unwrap()),
+ &cache,
+ )
+ .unwrap();
+ let entry = layer.toc().get("/etc/hostname").expect("entry");
+ let mut buf = vec![0u8; entry.size as usize];
+ let n = layer.read_file(entry, 0, &mut buf).unwrap();
+ assert_eq!(n, buf.len());
+ assert_eq!(String::from_utf8_lossy(&buf), "hello from lazy layer");
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/gz/mod.rs b/firecracker-container-vm/crates/fc-oci-fs/src/gz/mod.rs
new file mode 100644
index 0000000..688c02b
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/gz/mod.rs
@@ -0,0 +1,3 @@
+mod layer;
+
+pub use layer::{IndexedLayer, LayerSource};
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/image.rs b/firecracker-container-vm/crates/fc-oci-fs/src/image.rs
new file mode 100644
index 0000000..a51ab26
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/image.rs
@@ -0,0 +1,84 @@
+use std::path::Path;
+use std::sync::Arc;
+
+use oci_distribution::Reference;
+
+use crate::error::Result;
+use crate::gz::{IndexedLayer, LayerSource};
+use crate::metrics;
+use crate::overlay::OverlayFs;
+use crate::registry::{LocalBlob, RegistryClient};
+
+/// Parsed container image reference.
+#[derive(Debug, Clone)]
+pub struct ImageRef {
+ pub reference: Reference,
+ pub layer_digests: Vec,
+}
+
+/// High-level API: resolve an OCI image and build a lazy overlay filesystem.
+pub struct ImageFs {
+ pub image: ImageRef,
+ pub overlay: Arc,
+}
+
+impl ImageFs {
+ pub async fn open(
+ image: &str,
+ cache_dir: impl AsRef,
+ registry: &mut RegistryClient,
+ ) -> Result {
+ metrics::mark_startup_begin();
+ let (reference, layer_digests) = registry.resolve_layers(image).await?;
+ let mut total_layer_bytes = 0u64;
+ for digest in &layer_digests {
+ total_layer_bytes = total_layer_bytes.saturating_add(
+ registry
+ .layer_compressed_size(&reference, digest)
+ .await?,
+ );
+ }
+ metrics::record_layer_sizes(total_layer_bytes);
+
+ let mut layers = Vec::new();
+ for digest in &layer_digests {
+ let blob = registry
+ .open_blob(&reference, digest)
+ .await?;
+ let layer = IndexedLayer::open(
+ digest,
+ LayerSource::Remote(blob),
+ cache_dir.as_ref(),
+ )?;
+ layers.push(Arc::new(layer));
+ }
+ let overlay = OverlayFs::from_layers(layers)?;
+ Ok(Self {
+ image: ImageRef {
+ reference,
+ layer_digests,
+ },
+ overlay,
+ })
+ }
+
+ pub fn open_local_layer(
+ digest: &str,
+ blob_path: impl AsRef,
+ cache_dir: impl AsRef,
+ ) -> Result {
+ let layer = Arc::new(IndexedLayer::open(
+ digest,
+ LayerSource::Local(LocalBlob::open(blob_path)?),
+ cache_dir.as_ref(),
+ )?);
+ let overlay = OverlayFs::from_layers(vec![layer])?;
+ Ok(Self {
+ image: ImageRef {
+ reference: "local/fixture:latest".parse().unwrap(),
+ layer_digests: vec![digest.to_string()],
+ },
+ overlay,
+ })
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/lib.rs b/firecracker-container-vm/crates/fc-oci-fs/src/lib.rs
new file mode 100644
index 0000000..f8b9709
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/lib.rs
@@ -0,0 +1,17 @@
+pub mod docker_auth;
+pub mod error;
+pub mod gz;
+pub mod image;
+pub mod metrics;
+pub mod overlay;
+pub mod registry;
+pub mod tar_index;
+
+pub use docker_auth::ResolvedAuth;
+pub use gz::{IndexedLayer, LayerSource};
+pub use image::{ImageFs, ImageRef};
+pub use metrics::{
+ mark_startup_begin, mark_startup_ready, process_started, render_prometheus,
+};
+pub use overlay::OverlayFs;
+pub use registry::RegistryClient;
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/metrics.rs b/firecracker-container-vm/crates/fc-oci-fs/src/metrics.rs
new file mode 100644
index 0000000..9bce863
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/metrics.rs
@@ -0,0 +1,195 @@
+use std::path::Path;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::OnceLock;
+use std::time::{Duration, Instant};
+
+static STARTUP: OnceLock = OnceLock::new();
+static PROCESS_START: OnceLock = OnceLock::new();
+
+static BYTES_FETCHED: AtomicU64 = AtomicU64::new(0);
+static BYTES_SAVED: AtomicU64 = AtomicU64::new(0);
+static RANGE_REQUESTS: AtomicU64 = AtomicU64::new(0);
+static FULL_BLOB_DOWNLOADS: AtomicU64 = AtomicU64::new(0);
+static INDEX_BUILDS: AtomicU64 = AtomicU64::new(0);
+static FUSE_REQUESTS: AtomicU64 = AtomicU64::new(0);
+static FUSE_READS: AtomicU64 = AtomicU64::new(0);
+static LAYER_BYTES_TOTAL: AtomicU64 = AtomicU64::new(0);
+static STARTUP_READY_MS: AtomicU64 = AtomicU64::new(0);
+
+pub fn process_started() {
+ let _ = PROCESS_START.set(Instant::now());
+}
+
+pub fn mark_startup_begin() {
+ let _ = STARTUP.set(Instant::now());
+}
+
+pub fn mark_startup_ready() {
+ if let Some(t0) = STARTUP.get() {
+ STARTUP_READY_MS.store(t0.elapsed().as_millis() as u64, Ordering::Relaxed);
+ }
+}
+
+pub fn record_layer_sizes(total_compressed_bytes: u64) {
+ LAYER_BYTES_TOTAL.store(total_compressed_bytes, Ordering::Relaxed);
+}
+
+pub fn record_bytes_fetched(n: u64) {
+ BYTES_FETCHED.fetch_add(n, Ordering::Relaxed);
+ recompute_bytes_saved();
+}
+
+pub fn record_range_request() {
+ RANGE_REQUESTS.fetch_add(1, Ordering::Relaxed);
+}
+
+pub fn record_full_blob_download(bytes: u64) {
+ FULL_BLOB_DOWNLOADS.fetch_add(1, Ordering::Relaxed);
+ record_bytes_fetched(bytes);
+}
+
+pub fn record_index_build(duration: Duration) {
+ INDEX_BUILDS.fetch_add(1, Ordering::Relaxed);
+ let _ = duration;
+}
+
+pub fn record_fuse_request() {
+ FUSE_REQUESTS.fetch_add(1, Ordering::Relaxed);
+}
+
+pub fn record_fuse_read(bytes: u64) {
+ FUSE_READS.fetch_add(1, Ordering::Relaxed);
+ let _ = bytes;
+}
+
+fn recompute_bytes_saved() {
+ let total = LAYER_BYTES_TOTAL.load(Ordering::Relaxed);
+ let fetched = BYTES_FETCHED.load(Ordering::Relaxed);
+ if total > fetched {
+ BYTES_SAVED.store(total - fetched, Ordering::Relaxed);
+ }
+}
+
+pub fn cache_dir_bytes(cache_dir: &Path) -> u64 {
+ dir_size(cache_dir).unwrap_or(0)
+}
+
+fn dir_size(path: &Path) -> std::io::Result {
+ let mut total = 0u64;
+ if !path.exists() {
+ return Ok(0);
+ }
+ for entry in std::fs::read_dir(path)? {
+ let entry = entry?;
+ let meta = entry.metadata()?;
+ if meta.is_dir() {
+ total = total.saturating_add(dir_size(&entry.path())?);
+ } else {
+ total = total.saturating_add(meta.len());
+ }
+ }
+ Ok(total)
+}
+
+pub fn rss_bytes() -> u64 {
+ std::fs::read_to_string("/proc/self/status")
+ .ok()
+ .and_then(|s| {
+ s.lines()
+ .find(|l| l.starts_with("VmRSS:"))
+ .and_then(|l| l.split_whitespace().nth(1))
+ .and_then(|kb| kb.parse::().ok())
+ })
+ .map(|kb| kb * 1024)
+ .unwrap_or(0)
+}
+
+pub fn uptime_seconds() -> f64 {
+ PROCESS_START
+ .get()
+ .map(|t| t.elapsed().as_secs_f64())
+ .unwrap_or(0.0)
+}
+
+pub fn render_prometheus(cache_dir: &Path) -> String {
+ let fetched = BYTES_FETCHED.load(Ordering::Relaxed);
+ let saved = BYTES_SAVED.load(Ordering::Relaxed);
+ let layer_total = LAYER_BYTES_TOTAL.load(Ordering::Relaxed);
+ let cache_bytes = cache_dir_bytes(cache_dir);
+ let rss = rss_bytes();
+ let startup_ms = STARTUP_READY_MS.load(Ordering::Relaxed);
+
+ format!(
+ r#"# HELP fc_bytes_fetched_from_registry Total compressed bytes read from the OCI registry.
+# TYPE fc_bytes_fetched_from_registry counter
+fc_bytes_fetched_from_registry {fetched}
+# HELP fc_bytes_saved_vs_full_pull Bytes not downloaded compared to pulling full layer blobs.
+# TYPE fc_bytes_saved_vs_full_pull gauge
+fc_bytes_saved_vs_full_pull {saved}
+# HELP fc_layer_compressed_bytes_total Sum of compressed layer blob sizes in the resolved image.
+# TYPE fc_layer_compressed_bytes_total gauge
+fc_layer_compressed_bytes_total {layer_total}
+# HELP fc_registry_range_requests_total HTTP range requests issued to registry blob URLs.
+# TYPE fc_registry_range_requests_total counter
+fc_registry_range_requests_total {range_reqs}
+# HELP fc_full_blob_downloads_total Number of full layer blobs copied to local cache.
+# TYPE fc_full_blob_downloads_total counter
+fc_full_blob_downloads_total {full_downloads}
+# HELP fc_gzip_index_builds_total Gzip index builds performed for layers.
+# TYPE fc_gzip_index_builds_total counter
+fc_gzip_index_builds_total {index_builds}
+# HELP fc_fuse_requests_total FUSE/virtio-fs requests handled by the daemon.
+# TYPE fc_fuse_requests_total counter
+fc_fuse_requests_total {fuse_reqs}
+# HELP fc_fuse_reads_total File read operations served from lazy layers.
+# TYPE fc_fuse_reads_total counter
+fc_fuse_reads_total {fuse_reads}
+# HELP fc_startup_ready_milliseconds Time from image open start until vhost socket is listening.
+# TYPE fc_startup_ready_milliseconds gauge
+fc_startup_ready_milliseconds {startup_ms}
+# HELP fc_process_uptime_seconds Daemon uptime.
+# TYPE fc_process_uptime_seconds gauge
+fc_process_uptime_seconds {uptime}
+# HELP fc_process_rss_bytes Resident set size of the vhost-fs daemon.
+# TYPE fc_process_rss_bytes gauge
+fc_process_rss_bytes {rss}
+# HELP fc_cache_dir_bytes_on_disk Bytes used by layer blobs and gzip indexes on disk.
+# TYPE fc_cache_dir_bytes_on_disk gauge
+fc_cache_dir_bytes_on_disk {cache_bytes}
+"#,
+ fetched = fetched,
+ saved = saved,
+ layer_total = layer_total,
+ range_reqs = RANGE_REQUESTS.load(Ordering::Relaxed),
+ full_downloads = FULL_BLOB_DOWNLOADS.load(Ordering::Relaxed),
+ index_builds = INDEX_BUILDS.load(Ordering::Relaxed),
+ fuse_reqs = FUSE_REQUESTS.load(Ordering::Relaxed),
+ fuse_reads = FUSE_READS.load(Ordering::Relaxed),
+ startup_ms = startup_ms,
+ uptime = uptime_seconds(),
+ rss = rss,
+ cache_bytes = cache_bytes,
+ )
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ mark_startup_begin, mark_startup_ready, process_started, record_bytes_fetched,
+ record_layer_sizes, render_prometheus,
+ };
+ use tempfile::tempdir;
+
+ #[test]
+ fn prometheus_includes_saved_bytes() {
+ process_started();
+ mark_startup_begin();
+ record_layer_sizes(1_000_000);
+ record_bytes_fetched(250_000);
+ mark_startup_ready();
+ let dir = tempdir().unwrap();
+ let body = render_prometheus(dir.path());
+ assert!(body.contains("fc_bytes_saved_vs_full_pull 750000"));
+ assert!(body.contains("fc_startup_ready_milliseconds"));
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/overlay.rs b/firecracker-container-vm/crates/fc-oci-fs/src/overlay.rs
new file mode 100644
index 0000000..5bedd59
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/overlay.rs
@@ -0,0 +1,490 @@
+use std::any::Any;
+use std::collections::{BTreeMap, HashMap};
+use std::ffi::CStr;
+use std::io;
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+
+use fuse_backend_rs::abi::fuse_abi::stat64;
+use fuse_backend_rs::api::filesystem::{Context, DirEntry, Entry, FileSystem, FsOptions, OpenOptions, SetattrValid, ZeroCopyWriter};
+use fuse_backend_rs::api::filesystem::ROOT_ID;
+use fuse_backend_rs::api::BackendFileSystem;
+
+use crate::error::Result;
+use crate::gz::IndexedLayer;
+use crate::metrics;
+use crate::tar_index::TarEntry;
+
+const ATTR_TIMEOUT: Duration = Duration::from_secs(3600);
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum NodeKind {
+ Directory,
+ File,
+ Symlink,
+}
+
+#[derive(Debug, Clone)]
+struct Node {
+ inode: u64,
+ parent: u64,
+ name: String,
+ kind: NodeKind,
+ mode: u32,
+ size: u64,
+ link_target: Option>,
+ location: Option,
+ children: Vec,
+}
+
+#[derive(Debug, Clone)]
+struct FileLocation {
+ layer: usize,
+ entry: TarEntry,
+}
+
+/// Read-only overlay filesystem backed by indexed OCI layers.
+pub struct OverlayFs {
+ root: u64,
+ nodes: Mutex>,
+ path_to_inode: Mutex>,
+ next_inode: AtomicU64,
+ layers: Vec>,
+}
+
+impl OverlayFs {
+ pub fn from_layers(layers: Vec>) -> Result> {
+ let fs = Arc::new(Self {
+ root: ROOT_ID,
+ nodes: Mutex::new(HashMap::new()),
+ path_to_inode: Mutex::new(HashMap::new()),
+ next_inode: AtomicU64::new(ROOT_ID + 1),
+ layers,
+ });
+ fs.build_tree()?;
+ Ok(fs)
+ }
+
+ fn alloc_inode(&self) -> u64 {
+ self.next_inode.fetch_add(1, Ordering::Relaxed)
+ }
+
+ fn build_tree(&self) -> Result<()> {
+ let mut nodes = self.nodes.lock().unwrap();
+ let mut path_to_inode = self.path_to_inode.lock().unwrap();
+ nodes.insert(
+ ROOT_ID,
+ Node {
+ inode: ROOT_ID,
+ parent: ROOT_ID,
+ name: String::new(),
+ kind: NodeKind::Directory,
+ mode: 0o755,
+ size: 0,
+ link_target: None,
+ location: None,
+ children: Vec::new(),
+ },
+ );
+ path_to_inode.insert("/".to_string(), ROOT_ID);
+
+ let mut visible: BTreeMap = BTreeMap::new();
+ let mut whiteouts: std::collections::HashSet = std::collections::HashSet::new();
+
+ for (layer_idx, layer) in self.layers.iter().enumerate() {
+ for entry in layer.toc().entries() {
+ let name = entry.path.rsplit('/').next().unwrap_or("");
+ if name.starts_with(".wh.") {
+ let hidden = entry
+ .path
+ .rsplit_once('/')
+ .map(|(parent, wh)| format!("{}/{}", parent, &wh[4..]))
+ .unwrap_or_else(|| entry.path.trim_start_matches(".wh.").to_string());
+ whiteouts.insert(hidden);
+ continue;
+ }
+ if entry.entry_type.contains("Directory") {
+ visible.insert(entry.path.clone(), (layer_idx, entry.clone()));
+ } else if entry.entry_type.contains("Regular") || entry.entry_type.contains("Link")
+ {
+ if !whiteouts.contains(&entry.path) {
+ visible.insert(entry.path.clone(), (layer_idx, entry.clone()));
+ }
+ } else if entry.entry_type.contains("Symlink") {
+ if !whiteouts.contains(&entry.path) {
+ visible.insert(entry.path.clone(), (layer_idx, entry.clone()));
+ }
+ }
+ }
+ }
+
+ for (path, (layer_idx, entry)) in visible {
+ self.ensure_path(
+ &mut nodes,
+ &mut path_to_inode,
+ &path,
+ layer_idx,
+ &entry,
+ )?;
+ }
+ Ok(())
+ }
+
+ fn ensure_path(
+ &self,
+ nodes: &mut HashMap,
+ path_to_inode: &mut HashMap,
+ path: &str,
+ layer_idx: usize,
+ entry: &TarEntry,
+ ) -> Result<()> {
+ if path_to_inode.contains_key(path) {
+ return Ok(());
+ }
+ let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
+ let mut current_path = String::from("/");
+ let mut parent = ROOT_ID;
+ for (idx, part) in parts.iter().enumerate() {
+ if part.is_empty() {
+ continue;
+ }
+ if current_path != "/" {
+ current_path.push('/');
+ }
+ current_path.push_str(part);
+ if let Some(&inode) = path_to_inode.get(¤t_path) {
+ parent = inode;
+ continue;
+ }
+ let inode = self.alloc_inode();
+ let is_last = idx == parts.len() - 1;
+ let kind = if is_last {
+ if entry.entry_type.contains("Directory") {
+ NodeKind::Directory
+ } else if entry.entry_type.contains("Symlink") {
+ NodeKind::Symlink
+ } else {
+ NodeKind::File
+ }
+ } else {
+ NodeKind::Directory
+ };
+ let node = Node {
+ inode,
+ parent,
+ name: part.to_string(),
+ kind,
+ mode: if is_last { entry.mode } else { 0o755 },
+ size: if is_last { entry.size } else { 0 },
+ link_target: if is_last {
+ entry.link_target.as_ref().map(|s| s.as_bytes().to_vec())
+ } else {
+ None
+ },
+ location: if is_last && matches!(kind, NodeKind::File) {
+ Some(FileLocation {
+ layer: layer_idx,
+ entry: entry.clone(),
+ })
+ } else {
+ None
+ },
+ children: Vec::new(),
+ };
+ nodes.get_mut(&parent).unwrap().children.push(part.to_string());
+ nodes.insert(inode, node);
+ path_to_inode.insert(current_path.clone(), inode);
+ parent = inode;
+ }
+ Ok(())
+ }
+
+ fn node_to_entry(&self, node: &Node) -> Entry {
+ let mut attr: stat64 = unsafe { std::mem::zeroed() };
+ attr.st_ino = node.inode;
+ attr.st_mode = match node.kind {
+ NodeKind::Directory => libc::S_IFDIR | (node.mode & 0o777),
+ NodeKind::File => libc::S_IFREG | (node.mode & 0o777),
+ NodeKind::Symlink => libc::S_IFLNK | (node.mode & 0o777),
+ };
+ attr.st_size = node.size as i64;
+ attr.st_nlink = 1;
+ Entry {
+ inode: node.inode,
+ generation: 0,
+ attr,
+ attr_flags: 0,
+ attr_timeout: ATTR_TIMEOUT,
+ entry_timeout: ATTR_TIMEOUT,
+ }
+ }
+}
+
+impl FileSystem for OverlayFs {
+ type Inode = u64;
+ type Handle = u64;
+
+ fn init(&self, capable: FsOptions) -> io::Result {
+ let mut opts = FsOptions::empty();
+ if capable.contains(FsOptions::ZERO_MESSAGE_OPEN) {
+ opts |= FsOptions::ZERO_MESSAGE_OPEN;
+ }
+ Ok(opts)
+ }
+
+ fn lookup(&self, _ctx: &Context, parent: Self::Inode, name: &CStr) -> io::Result {
+ metrics::record_fuse_request();
+ let name = name.to_string_lossy();
+ let nodes = self.nodes.lock().unwrap();
+ let parent_node = nodes
+ .get(&parent)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ if !parent_node.children.iter().any(|c| c == name.as_ref()) {
+ return Err(io::Error::from_raw_os_error(libc::ENOENT));
+ }
+ let path = if parent == ROOT_ID {
+ format!("/{name}")
+ } else {
+ let mut parts = vec![];
+ let mut cur = parent;
+ while cur != ROOT_ID {
+ let n = &nodes[&cur];
+ parts.push(n.name.clone());
+ cur = n.parent;
+ }
+ parts.reverse();
+ format!("/{}/{}", parts.join("/"), name)
+ };
+ let inode = *self
+ .path_to_inode
+ .lock()
+ .unwrap()
+ .get(&path)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ Ok(self.node_to_entry(&nodes[&inode]))
+ }
+
+ fn getattr(
+ &self,
+ _ctx: &Context,
+ inode: Self::Inode,
+ _handle: Option,
+ ) -> io::Result<(stat64, Duration)> {
+ let nodes = self.nodes.lock().unwrap();
+ let node = nodes
+ .get(&inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ let entry = self.node_to_entry(node);
+ Ok((entry.attr, ATTR_TIMEOUT))
+ }
+
+ fn read(
+ &self,
+ _ctx: &Context,
+ inode: Self::Inode,
+ _handle: Self::Handle,
+ w: &mut dyn ZeroCopyWriter,
+ size: u32,
+ offset: u64,
+ _lock_owner: Option,
+ _flags: u32,
+ ) -> io::Result {
+ let nodes = self.nodes.lock().unwrap();
+ let node = nodes
+ .get(&inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ let location = node
+ .location
+ .as_ref()
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::EISDIR))?;
+ let layer = &self.layers[location.layer];
+ let mut buf = vec![0u8; size as usize];
+ let n = layer
+ .read_file(&location.entry, offset, &mut buf)
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
+ metrics::record_fuse_read(n as u64);
+ w.write(&buf[..n])?;
+ Ok(n)
+ }
+
+ fn readdir(
+ &self,
+ _ctx: &Context,
+ inode: Self::Inode,
+ _handle: Self::Handle,
+ mut size: u32,
+ offset: u64,
+ add_entry: &mut dyn FnMut(DirEntry) -> io::Result,
+ ) -> io::Result<()> {
+ let nodes = self.nodes.lock().unwrap();
+ let node = nodes
+ .get(&inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ if !matches!(node.kind, NodeKind::Directory) {
+ return Err(io::Error::from_raw_os_error(libc::ENOTDIR));
+ }
+ let mut entries: Vec<(u64, String)> = node
+ .children
+ .iter()
+ .filter_map(|child| {
+ let path = if inode == ROOT_ID {
+ format!("/{child}")
+ } else {
+ let mut parts = vec![];
+ let mut cur = inode;
+ while cur != ROOT_ID {
+ let n = &nodes[&cur];
+ parts.push(n.name.clone());
+ cur = n.parent;
+ }
+ parts.reverse();
+ format!("/{}/{}", parts.join("/"), child)
+ };
+ let ino = *self.path_to_inode.lock().unwrap().get(&path)?;
+ Some((ino, child.clone()))
+ })
+ .collect();
+ entries.sort_by(|a, b| a.1.cmp(&b.1));
+ let start = offset.saturating_sub(1) as usize;
+ for (idx, (ino, name)) in entries.into_iter().enumerate().skip(start) {
+ if size == 0 {
+ break;
+ }
+ let dirent = DirEntry {
+ ino,
+ offset: (idx + 2) as u64,
+ type_: libc::DT_UNKNOWN as u32,
+ name: name.as_bytes(),
+ };
+ let consumed = add_entry(dirent)?;
+ size = size.saturating_sub(consumed as u32);
+ }
+ Ok(())
+ }
+
+ fn readlink(&self, _ctx: &Context, inode: Self::Inode) -> io::Result> {
+ let nodes = self.nodes.lock().unwrap();
+ let node = nodes
+ .get(&inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ node.link_target
+ .clone()
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::EINVAL))
+ }
+
+ fn open(
+ &self,
+ _ctx: &Context,
+ inode: Self::Inode,
+ _flags: u32,
+ _fuse_flags: u32,
+ ) -> io::Result<(Option, OpenOptions, Option)> {
+ let nodes = self.nodes.lock().unwrap();
+ let node = nodes
+ .get(&inode)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ if !matches!(node.kind, NodeKind::File) {
+ return Err(io::Error::from_raw_os_error(libc::EISDIR));
+ }
+ Ok((Some(inode), OpenOptions::empty(), None))
+ }
+
+ fn opendir(
+ &self,
+ _ctx: &Context,
+ inode: Self::Inode,
+ _flags: u32,
+ ) -> io::Result<(Option, OpenOptions)> {
+ Ok((Some(inode), OpenOptions::empty()))
+ }
+
+ fn setattr(
+ &self,
+ _ctx: &Context,
+ _inode: Self::Inode,
+ _attr: stat64,
+ _handle: Option,
+ _valid: SetattrValid,
+ ) -> io::Result<(stat64, Duration)> {
+ Err(io::Error::from_raw_os_error(libc::EROFS))
+ }
+}
+
+impl BackendFileSystem for OverlayFs {
+ fn mount(&self) -> io::Result<(Entry, u64)> {
+ let nodes = self.nodes.lock().unwrap();
+ let root = nodes
+ .get(&ROOT_ID)
+ .ok_or_else(|| io::Error::from_raw_os_error(libc::ENOENT))?;
+ let max_ino = self.next_inode.load(Ordering::Relaxed);
+ Ok((self.node_to_entry(root), max_ino))
+ }
+
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::gz::{IndexedLayer, LayerSource};
+ use crate::registry::LocalBlob;
+ use flate2::write::GzEncoder;
+ use flate2::Compression;
+ use std::ffi::CString;
+ use tar::Builder;
+ use tempfile::tempdir;
+
+ fn fixture_layer(dir: &std::path::Path) -> std::path::PathBuf {
+ let tar_path = dir.join("layer.tar");
+ let gz_path = dir.join("layer.tar.gz");
+ {
+ let tar = std::fs::File::create(&tar_path).unwrap();
+ let mut builder = Builder::new(tar);
+ for (path, content) in [("bin/sh", b"#!/bin/sh\n".as_slice()), ("etc/motd", b"welcome\n".as_slice())] {
+ let mut header = tar::Header::new_gnu();
+ header.set_size(content.len() as u64);
+ header.set_mode(0o755);
+ header.set_cksum();
+ builder
+ .append_data(&mut header, path, &content[..])
+ .unwrap();
+ }
+ builder.finish().unwrap();
+ }
+ let mut gz = GzEncoder::new(Vec::new(), Compression::default());
+ std::io::copy(
+ &mut std::fs::File::open(&tar_path).unwrap(),
+ &mut gz,
+ )
+ .unwrap();
+ std::fs::write(&gz_path, gz.finish().unwrap()).unwrap();
+ gz_path
+ }
+
+ #[test]
+ fn overlay_lookup_and_getattr() {
+ let dir = tempdir().unwrap();
+ let gz = fixture_layer(dir.path());
+ let layer = Arc::new(
+ IndexedLayer::open(
+ "layer0",
+ LayerSource::Local(LocalBlob::open(&gz).unwrap()),
+ dir.path().join("cache"),
+ )
+ .unwrap(),
+ );
+ let fs = OverlayFs::from_layers(vec![layer]).unwrap();
+ let ctx = Context {
+ uid: 0,
+ gid: 0,
+ pid: 1,
+ };
+ let entry = fs
+ .lookup(&ctx, ROOT_ID, &CString::new("etc").unwrap())
+ .unwrap();
+ assert_eq!(entry.attr.st_mode & libc::S_IFDIR as u32, libc::S_IFDIR as u32);
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/registry.rs b/firecracker-container-vm/crates/fc-oci-fs/src/registry.rs
new file mode 100644
index 0000000..a3791dc
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/registry.rs
@@ -0,0 +1,331 @@
+use std::collections::HashMap;
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+use std::sync::{Arc, Mutex};
+
+use oci_distribution::client::Client;
+use oci_distribution::Reference;
+use reqwest::blocking::Client as HttpClient;
+use reqwest::header::{AUTHORIZATION, RANGE};
+use reqwest::StatusCode;
+
+use crate::docker_auth::ResolvedAuth;
+use crate::error::{Error, Result};
+use crate::metrics;
+
+const GZIP_LAYER_TYPES: &[&str] = &[
+ "application/vnd.oci.image.layer.v1.tar+gzip",
+ "application/vnd.docker.image.rootfs.diff.tar.gzip",
+];
+
+/// Cached redirect + range capability for a blob, following the dagdotdev explore approach.
+#[derive(Debug, Clone)]
+struct BlobEndpoint {
+ url: String,
+ size: u64,
+ supports_range: bool,
+ auth_header: Option,
+}
+
+/// HTTP `Read + Seek` over an OCI blob using range requests.
+pub struct RangeBlob {
+ endpoint: BlobEndpoint,
+ http: HttpClient,
+ pos: u64,
+ cache: Vec,
+ cache_start: u64,
+}
+
+impl RangeBlob {
+ pub fn len(&self) -> u64 {
+ self.endpoint.size
+ }
+
+ fn fetch_range(&mut self, start: u64, end: u64) -> Result> {
+ metrics::record_range_request();
+ let range = format!("bytes={}-{}", start, end.saturating_sub(1));
+ let mut req = self.http.get(&self.endpoint.url).header(RANGE, range);
+ if let Some(auth) = &self.endpoint.auth_header {
+ req = req.header(AUTHORIZATION, auth);
+ }
+ let resp = req.send()?.error_for_status()?;
+ let bytes = resp.bytes()?.to_vec();
+ metrics::record_bytes_fetched(bytes.len() as u64);
+ Ok(bytes)
+ }
+
+ fn refill_cache(&mut self, pos: u64) -> Result<()> {
+ let chunk = 256 * 1024;
+ let end = (pos + chunk).min(self.endpoint.size);
+ if pos >= self.endpoint.size {
+ self.cache.clear();
+ self.cache_start = pos;
+ return Ok(());
+ }
+ let data = if self.endpoint.supports_range {
+ self.fetch_range(pos, end)?
+ } else {
+ let mut req = self.http.get(&self.endpoint.url);
+ if let Some(auth) = &self.endpoint.auth_header {
+ req = req.header(AUTHORIZATION, auth);
+ }
+ let full = req.send()?.error_for_status()?;
+ let bytes = full.bytes()?.to_vec();
+ metrics::record_bytes_fetched(bytes.len() as u64);
+ bytes[pos as usize..end as usize].to_vec()
+ };
+ self.cache = data;
+ self.cache_start = pos;
+ Ok(())
+ }
+}
+
+impl Read for RangeBlob {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ if self.pos >= self.endpoint.size {
+ return Ok(0);
+ }
+ if self.cache.is_empty()
+ || self.pos < self.cache_start
+ || self.pos >= self.cache_start + self.cache.len() as u64
+ {
+ self.refill_cache(self.pos)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
+ }
+ let offset = (self.pos - self.cache_start) as usize;
+ let n = buf.len().min(self.cache.len() - offset);
+ buf[..n].copy_from_slice(&self.cache[offset..offset + n]);
+ self.pos += n as u64;
+ Ok(n)
+ }
+}
+
+impl Seek for RangeBlob {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result {
+ self.pos = match pos {
+ SeekFrom::Start(off) => off,
+ SeekFrom::End(off) => {
+ if off >= 0 {
+ self.endpoint.size + off as u64
+ } else {
+ self.endpoint.size.saturating_sub((-off) as u64)
+ }
+ }
+ SeekFrom::Current(off) => {
+ if off >= 0 {
+ self.pos + off as u64
+ } else {
+ self.pos.saturating_sub((-off) as u64)
+ }
+ }
+ };
+ Ok(self.pos)
+ }
+}
+
+/// Registry client with docker config auth, ping/token caching, and range-aware blob access.
+#[derive(Clone)]
+pub struct RegistryClient {
+ oci: Arc>,
+ http: HttpClient,
+ auth: Arc>>,
+ blob_cache: Arc>>,
+ cache_dir: PathBuf,
+}
+
+impl RegistryClient {
+ pub fn new(cache_dir: impl AsRef) -> Self {
+ metrics::process_started();
+ Self {
+ oci: Arc::new(Mutex::new(Client::new(oci_distribution::client::ClientConfig {
+ protocol: oci_distribution::client::ClientProtocol::Https,
+ ..Default::default()
+ }))),
+ http: HttpClient::new(),
+ auth: Arc::new(Mutex::new(HashMap::new())),
+ blob_cache: Arc::new(Mutex::new(HashMap::new())),
+ cache_dir: cache_dir.as_ref().to_path_buf(),
+ }
+ }
+
+ pub fn cache_dir(&self) -> &Path {
+ &self.cache_dir
+ }
+
+ fn auth_for(&self, reference: &Reference) -> Result {
+ let key = reference.resolve_registry().to_string();
+ if let Some(auth) = self.auth.lock().unwrap().get(&key).cloned() {
+ return Ok(auth);
+ }
+ let auth = ResolvedAuth::for_reference(reference)?;
+ self.auth.lock().unwrap().insert(key, auth.clone());
+ Ok(auth)
+ }
+
+ pub async fn resolve_layers(&mut self, image: &str) -> Result<(Reference, Vec)> {
+ let reference: Reference = image
+ .parse::()
+ .map_err(|e| Error::InvalidReference(format!("{e}")))?;
+ let auth = self.auth_for(&reference)?;
+ let (manifest, _) = self
+ .oci
+ .lock()
+ .unwrap()
+ .pull_manifest(&reference, &auth.to_oci_auth())
+ .await?;
+ let image_manifest = match manifest {
+ oci_distribution::manifest::OciManifest::Image(m) => m,
+ oci_distribution::manifest::OciManifest::ImageIndex(_) => {
+ return Err(Error::Other(
+ "multi-arch indexes require platform selection (not implemented in example)"
+ .into(),
+ ));
+ }
+ };
+
+ let mut digests = Vec::new();
+ for layer in image_manifest.layers {
+ if !GZIP_LAYER_TYPES.contains(&layer.media_type.as_str()) {
+ return Err(Error::UnsupportedMediaType(layer.media_type));
+ }
+ let digest = layer
+ .digest
+ .strip_prefix("sha256:")
+ .unwrap_or(&layer.digest)
+ .to_string();
+ digests.push(digest);
+ }
+ Ok((reference, digests))
+ }
+
+ pub async fn open_blob(&mut self, reference: &Reference, digest: &str) -> Result {
+ let key = format!("{}@{}", reference.repository(), digest);
+ let auth = self.auth_for(reference)?;
+ let endpoint = {
+ let cached = self.blob_cache.lock().unwrap().get(&key).cloned();
+ if let Some(ep) = cached {
+ ep
+ } else {
+ let ep = self.probe_blob(reference, digest, &auth).await?;
+ self.blob_cache.lock().unwrap().insert(key, ep.clone());
+ ep
+ }
+ };
+ Ok(RangeBlob {
+ endpoint,
+ http: self.http.clone(),
+ pos: 0,
+ cache: Vec::new(),
+ cache_start: 0,
+ })
+ }
+
+ pub async fn layer_compressed_size(
+ &mut self,
+ reference: &Reference,
+ digest: &str,
+ ) -> Result {
+ let blob = self.open_blob(reference, digest).await?;
+ Ok(blob.len())
+ }
+
+ async fn probe_blob(
+ &mut self,
+ reference: &Reference,
+ digest: &str,
+ auth: &ResolvedAuth,
+ ) -> Result {
+ let digest = format!("sha256:{digest}");
+ let url = format!(
+ "https://{}/v2/{}/blobs/{}",
+ reference.resolve_registry(),
+ reference.repository(),
+ digest
+ );
+
+ let _ = self
+ .oci
+ .lock()
+ .unwrap()
+ .pull_manifest(reference, &auth.to_oci_auth())
+ .await?;
+
+ let auth_header = auth.authorization_header();
+
+ let mut req = self.http.head(&url);
+ if let Some(ref header) = auth_header {
+ req = req.header(AUTHORIZATION, header);
+ }
+ let head = req.send()?;
+ let head = head.error_for_status()?;
+
+ let final_url = head.url().as_str().trim_end_matches('/').to_string();
+ let size = head
+ .headers()
+ .get(reqwest::header::CONTENT_LENGTH)
+ .and_then(|v| v.to_str().ok())
+ .and_then(|v| v.parse().ok())
+ .unwrap_or(0);
+ let supports_range = head
+ .headers()
+ .get(reqwest::header::ACCEPT_RANGES)
+ .and_then(|v| v.to_str().ok())
+ .map(|v| v.contains("bytes"))
+ .unwrap_or(false);
+
+ let supports_range = if supports_range {
+ true
+ } else {
+ let mut probe = self.http.get(&final_url).header(RANGE, "bytes=0-0");
+ if let Some(ref header) = auth_header {
+ probe = probe.header(AUTHORIZATION, header);
+ }
+ let resp = probe.send()?;
+ let ok = resp.status() == StatusCode::PARTIAL_CONTENT;
+ if ok {
+ metrics::record_range_request();
+ if let Ok(body) = resp.bytes() {
+ metrics::record_bytes_fetched(body.len() as u64);
+ }
+ }
+ ok
+ };
+
+ Ok(BlobEndpoint {
+ url: final_url,
+ size,
+ supports_range,
+ auth_header,
+ })
+ }
+}
+
+/// Convenience wrapper for local fixture blobs used in tests and benchmarks.
+pub struct LocalBlob {
+ inner: std::fs::File,
+ len: u64,
+}
+
+impl LocalBlob {
+ pub fn open(path: impl AsRef) -> Result {
+ let file = std::fs::File::open(path.as_ref())?;
+ let len = file.metadata()?.len();
+ Ok(Self { inner: file, len })
+ }
+
+ pub fn len(&self) -> u64 {
+ self.len
+ }
+}
+
+impl Read for LocalBlob {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result {
+ self.inner.read(buf)
+ }
+}
+
+impl Seek for LocalBlob {
+ fn seek(&mut self, pos: SeekFrom) -> std::io::Result {
+ self.inner.seek(pos)
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-oci-fs/src/tar_index.rs b/firecracker-container-vm/crates/fc-oci-fs/src/tar_index.rs
new file mode 100644
index 0000000..c4334e6
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-oci-fs/src/tar_index.rs
@@ -0,0 +1,44 @@
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub struct TarEntry {
+ pub path: String,
+ pub offset: u64,
+ pub size: u64,
+ pub mode: u32,
+ pub entry_type: String,
+ pub link_target: Option,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct TarIndex {
+ by_path: HashMap,
+}
+
+impl TarIndex {
+ pub fn from_entries(entries: Vec) -> Self {
+ let mut by_path = HashMap::new();
+ for entry in entries {
+ by_path.insert(entry.path.clone(), entry);
+ }
+ Self { by_path }
+ }
+
+ pub fn entries(&self) -> impl Iterator- {
+ self.by_path.values()
+ }
+
+ pub fn entries_cloned(&self) -> Vec {
+ self.by_path.values().cloned().collect()
+ }
+
+ pub fn get(&self, path: &str) -> Option<&TarEntry> {
+ self.by_path.get(path)
+ }
+
+ pub fn insert(&mut self, entry: TarEntry) {
+ self.by_path.insert(entry.path.clone(), entry);
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-runner/Cargo.toml b/firecracker-container-vm/crates/fc-runner/Cargo.toml
new file mode 100644
index 0000000..09c540a
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "fc-runner"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+description = "Example Firecracker runner using on-demand OCI image rootfs via virtio-fs"
+
+[[bin]]
+name = "fc-runner"
+path = "src/main.rs"
+
+[dependencies]
+anyhow.workspace = true
+clap.workspace = true
+env_logger.workspace = true
+fc-oci-fs = { path = "../fc-oci-fs" }
+log.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tempfile.workspace = true
+tokio.workspace = true
+
+[dev-dependencies]
+fc-vhostfsd = { path = "../fc-vhostfsd" }
+fuse-backend-rs = { workspace = true, default-features = false, features = ["virtiofs"] }
+reqwest = { workspace = true, features = ["blocking"] }
+tempfile.workspace = true
+flate2.workspace = true
+tar.workspace = true
+vhost = { workspace = true, features = ["vhost-user-frontend"] }
+vhost-user-backend.workspace = true
+nix = { version = "0.30", features = ["fs"] }
+vm-memory.workspace = true
diff --git a/firecracker-container-vm/crates/fc-runner/src/firecracker.rs b/firecracker-container-vm/crates/fc-runner/src/firecracker.rs
new file mode 100644
index 0000000..ab2b3cd
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/src/firecracker.rs
@@ -0,0 +1,139 @@
+use std::fs;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+
+use anyhow::Context as _;
+use serde_json::json;
+
+use crate::Args;
+
+/// Firecracker API configuration for a microVM with generic vhost-user virtio-fs.
+///
+/// Requires a Firecracker build that includes the generic vhost-user frontend
+/// (`PUT /vhost-user-devices/{id}`), as described in firecracker PR #5773.
+pub fn build_config(args: &Args) -> anyhow::Result {
+ let api_sock = args
+ .cache_dir
+ .join("firecracker.api.sock");
+ let log_path = args.cache_dir.join("firecracker.log");
+ fs::create_dir_all(&args.cache_dir)?;
+
+ Ok(json!({
+ "boot-source": {
+ "kernel_image_path": args.kernel,
+ "boot_args": format!(
+ "console=ttyS0 reboot=k panic=1 pci=off init=/bin/sh rootfstype=virtiofs root=/ {}",
+ if args.tag.is_empty() { "root=rootfs".into() } else { format!("root={}", args.tag) }
+ )
+ },
+ "drives": [],
+ "machine-config": {
+ "vcpu_count": args.vcpus,
+ "mem_size_mib": args.memory_mib,
+ "smt": false
+ },
+ "network-interfaces": [],
+ "vsock": null,
+ "_runner_meta": {
+ "api_socket": api_sock,
+ "log_path": log_path,
+ "vhost_user_device": {
+ "id": "rootfs",
+ "socket": args.vhost_socket,
+ "num_queues": 2,
+ "queue_size": 1024
+ }
+ }
+ }))
+}
+
+pub fn run_vm(args: &Args) -> anyhow::Result<()> {
+ if !args.firecracker.exists() {
+ anyhow::bail!(
+ "firecracker binary not found at {} — install Firecracker or pass --firecracker",
+ args.firecracker.display()
+ );
+ }
+ if !args.kernel.exists() {
+ anyhow::bail!("kernel not found at {}", args.kernel.display());
+ }
+
+ let api_sock = args.cache_dir.join("firecracker.api.sock");
+ let log_path = args.cache_dir.join("firecracker.log");
+ fs::create_dir_all(&args.cache_dir)?;
+ let _ = fs::remove_file(&api_sock);
+
+ let mut fc = Command::new(&args.firecracker)
+ .arg("--api-sock")
+ .arg(&api_sock)
+ .arg("--log-path")
+ .arg(&log_path)
+ .arg("--level")
+ .arg("Info")
+ .stdin(Stdio::null())
+ .stdout(Stdio::null())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .context("spawn firecracker")?;
+
+ std::thread::sleep(std::time::Duration::from_millis(200));
+
+ let boot = json!({
+ "kernel_image_path": args.kernel,
+ "boot_args": format!(
+ "console=ttyS0 reboot=k panic=1 pci=off init=/bin/sh rootfstype=virtiofs root=/ {}",
+ if args.tag.is_empty() { "root=rootfs".into() } else { format!("root={}", args.tag) }
+ )
+ });
+ firecracker_put(&api_sock, "/boot-source", &boot)?;
+
+ let machine = json!({
+ "vcpu_count": args.vcpus,
+ "mem_size_mib": args.memory_mib,
+ "smt": false
+ });
+ firecracker_put(&api_sock, "/machine-config", &machine)?;
+
+ let device = json!({
+ "socket": args.vhost_socket,
+ "num_queues": 2,
+ "queue_size": 1024
+ });
+ firecracker_put(&api_sock, "/vhost-user-devices/rootfs", &device)?;
+
+ firecracker_put(&api_sock, "/actions", &json!({"action_type": "InstanceStart"}))?;
+ let status = fc.wait().context("wait for firecracker")?;
+ if !status.success() {
+ anyhow::bail!("firecracker exited with {status}");
+ }
+ Ok(())
+}
+
+fn firecracker_put(
+ api_sock: &PathBuf,
+ path: &str,
+ body: &serde_json::Value,
+) -> anyhow::Result<()> {
+ let output = Command::new("curl")
+ .arg("--silent")
+ .arg("--show-error")
+ .arg("--fail")
+ .arg("--unix-socket")
+ .arg(api_sock)
+ .arg("-X")
+ .arg("PUT")
+ .arg(format!("http://localhost{path}"))
+ .arg("-H")
+ .arg("Content-Type: application/json")
+ .arg("-d")
+ .arg(body.to_string())
+ .output()
+ .context("curl firecracker api")?;
+ if !output.status.success() {
+ anyhow::bail!(
+ "firecracker API PUT {path} failed: {}",
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+ Ok(())
+}
diff --git a/firecracker-container-vm/crates/fc-runner/src/main.rs b/firecracker-container-vm/crates/fc-runner/src/main.rs
new file mode 100644
index 0000000..c217753
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/src/main.rs
@@ -0,0 +1,74 @@
+//! Example Firecracker runner that boots a microVM with a lazy OCI rootfs via virtio-fs.
+
+mod firecracker;
+mod vhostfs;
+
+use std::path::PathBuf;
+
+use anyhow::Context as _;
+use clap::Parser;
+use log::info;
+
+#[derive(Debug, Parser)]
+#[command(
+ name = "fc-runner",
+ about = "Boot Firecracker with on-demand OCI container image rootfs"
+)]
+struct Args {
+ /// Container image reference used as the guest root filesystem
+ #[arg(long)]
+ image: String,
+
+ /// Path to the Firecracker binary
+ #[arg(long, default_value = "firecracker")]
+ firecracker: PathBuf,
+
+ /// Path to the guest kernel (vmlinux or bzImage with virtio_fs support)
+ #[arg(long)]
+ kernel: PathBuf,
+
+ /// Guest memory in MiB
+ #[arg(long, default_value_t = 512)]
+ memory_mib: u32,
+
+ /// Number of vCPUs
+ #[arg(long, default_value_t = 1)]
+ vcpus: u32,
+
+ /// vhost-user socket for virtio-fs
+ #[arg(long, default_value = "/tmp/fc-vhostfs.sock")]
+ vhost_socket: PathBuf,
+
+ /// OCI layer/index cache directory
+ #[arg(long, default_value = "/tmp/fc-oci-cache")]
+ cache_dir: PathBuf,
+
+ /// virtio-fs mount tag inside the guest
+ #[arg(long, default_value = "rootfs")]
+ tag: String,
+
+ /// Only generate the Firecracker configuration JSON and start the vhost-fs daemon
+ #[arg(long)]
+ dry_run: bool,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ env_logger::init();
+ let args = Args::parse();
+
+ let mut vhostfs = vhostfs::spawn(&args).context("start vhost-fs daemon")?;
+ info!("vhost-fs daemon pid={}", vhostfs.child.id());
+
+ if args.dry_run {
+ let config = firecracker::build_config(&args)?;
+ println!("{}", serde_json::to_string_pretty(&config)?);
+ info!("dry-run: vhost-fs daemon running, firecracker not started");
+ vhostfs.child.wait().context("wait for vhostfsd")?;
+ return Ok(());
+ }
+
+ firecracker::run_vm(&args).context("run firecracker vm")?;
+ let _ = vhostfs.child.kill();
+ Ok(())
+}
diff --git a/firecracker-container-vm/crates/fc-runner/src/vhostfs.rs b/firecracker-container-vm/crates/fc-runner/src/vhostfs.rs
new file mode 100644
index 0000000..cb90218
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/src/vhostfs.rs
@@ -0,0 +1,43 @@
+use std::path::PathBuf;
+use std::process::{Child, Command, Stdio};
+
+use anyhow::{bail, Context as _};
+
+use crate::Args;
+
+pub struct VhostFsProcess {
+ pub child: Child,
+}
+
+pub fn spawn(args: &Args) -> anyhow::Result {
+ let bin = std::env::current_exe()
+ .ok()
+ .and_then(|p| p.parent().map(|d| d.join("fc-vhostfsd")))
+ .filter(|p| p.exists())
+ .unwrap_or_else(|| PathBuf::from("fc-vhostfsd"));
+
+ if let Some(parent) = args.vhost_socket.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ let _ = std::fs::remove_file(&args.vhost_socket);
+
+ let child = Command::new(bin)
+ .arg("--image")
+ .arg(&args.image)
+ .arg("--socket")
+ .arg(&args.vhost_socket)
+ .arg("--cache-dir")
+ .arg(&args.cache_dir)
+ .arg("--tag")
+ .arg(&args.tag)
+ .stdout(Stdio::inherit())
+ .stderr(Stdio::inherit())
+ .spawn()
+ .context("spawn fc-vhostfsd")?;
+
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ if !args.vhost_socket.exists() {
+ bail!("vhost-user socket was not created at {}", args.vhost_socket.display());
+ }
+ Ok(VhostFsProcess { child })
+}
diff --git a/firecracker-container-vm/crates/fc-runner/tests/support/mod.rs b/firecracker-container-vm/crates/fc-runner/tests/support/mod.rs
new file mode 100644
index 0000000..7b13e30
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/tests/support/mod.rs
@@ -0,0 +1,31 @@
+use std::path::{Path, PathBuf};
+
+use flate2::write::GzEncoder;
+use flate2::Compression;
+use tar::Builder;
+
+pub fn build_fixture_image(dir: &Path) -> PathBuf {
+ let tar_path = dir.join("layer.tar");
+ let gz_path = dir.join("layer.tar.gz");
+ {
+ let tar = std::fs::File::create(&tar_path).unwrap();
+ let mut builder = Builder::new(tar);
+ let payload = b"#!/bin/sh\necho hello-from-lazy-rootfs\n";
+ let mut header = tar::Header::new_gnu();
+ header.set_size(payload.len() as u64);
+ header.set_mode(0o755);
+ header.set_cksum();
+ builder
+ .append_data(&mut header, "bin/hello", &payload[..])
+ .unwrap();
+ builder.finish().unwrap();
+ }
+ let mut gz = GzEncoder::new(Vec::new(), Compression::default());
+ std::io::copy(
+ &mut std::fs::File::open(&tar_path).unwrap(),
+ &mut gz,
+ )
+ .unwrap();
+ std::fs::write(&gz_path, gz.finish().unwrap()).unwrap();
+ gz_path
+}
diff --git a/firecracker-container-vm/crates/fc-runner/tests/vhost_e2e.rs b/firecracker-container-vm/crates/fc-runner/tests/vhost_e2e.rs
new file mode 100644
index 0000000..93b78bd
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-runner/tests/vhost_e2e.rs
@@ -0,0 +1,71 @@
+use std::path::Path;
+use std::sync::{Arc, Barrier, Mutex};
+use std::thread;
+
+use std::fs::File;
+use std::os::unix::io::AsRawFd;
+
+use fc_oci_fs::ImageFs;
+use fc_vhostfsd::backend::FsBackendHandler;
+use fuse_backend_rs::api::server::Server;
+use vhost::vhost_user::message::VhostUserHeaderFlag;
+use vhost::vhost_user::{Frontend, Listener, VhostUserFrontend};
+use vhost::VhostBackend;
+use vhost_user_backend::VhostUserDaemon;
+use vm_memory::{FileOffset, GuestAddress, GuestMemoryAtomic, GuestMemoryMmap};
+
+mod support;
+
+fn vhost_client(path: &Path, barrier: Arc) {
+ barrier.wait();
+ let mut frontend = Frontend::connect(path, 1).unwrap();
+ frontend.set_hdr_flags(VhostUserHeaderFlag::NEED_REPLY);
+ barrier.wait();
+
+ let features = frontend.get_features().unwrap();
+ let proto = frontend.get_protocol_features().unwrap();
+ frontend.set_features(features).unwrap();
+ frontend.set_protocol_features(proto).unwrap();
+
+ assert_eq!(frontend.get_queue_num().unwrap(), 2);
+ frontend.set_owner().unwrap();
+
+ let (_cfg, data) = frontend
+ .get_config(0, 36, vhost::vhost_user::message::VhostUserConfigFlags::empty(), &[0u8; 36])
+ .unwrap();
+ assert!(data.starts_with(b"rootfs"));
+}
+
+#[test]
+fn vhost_user_virtiofs_daemon_handshake() {
+ let dir = tempfile::tempdir().unwrap();
+ let image = support::build_fixture_image(dir.path());
+ let overlay = ImageFs::open_local_layer("fixture", &image, dir.path().join("cache"))
+ .unwrap()
+ .overlay;
+ let server = Arc::new(Server::new(overlay));
+ let backend = Arc::new(Mutex::new(
+ FsBackendHandler::new(server, "rootfs".to_string()).unwrap(),
+ ));
+ let memfd = nix::sys::memfd::memfd_create("test", nix::sys::memfd::MFdFlags::empty()).unwrap();
+ let file = File::from(memfd);
+ file.set_len(0x100000).unwrap();
+ let file_offset = FileOffset::new(file, 0);
+ let mem = GuestMemoryAtomic::new(
+ GuestMemoryMmap::from_ranges_with_files(&[(GuestAddress(0x100000), 0x100000, Some(file_offset))])
+ .unwrap(),
+ );
+ let mut daemon = VhostUserDaemon::new("test".to_string(), backend, mem).unwrap();
+
+ let barrier = Arc::new(Barrier::new(2));
+ let socket = dir.path().join("vhost.sock");
+ let barrier2 = barrier.clone();
+ let socket2 = socket.clone();
+ let client = thread::spawn(move || vhost_client(&socket2, barrier2));
+
+ let mut listener = Listener::new(&socket, false).unwrap();
+ barrier.wait();
+ daemon.start(&mut listener).unwrap();
+ barrier.wait();
+ client.join().unwrap();
+}
diff --git a/firecracker-container-vm/crates/fc-vhostfsd/Cargo.toml b/firecracker-container-vm/crates/fc-vhostfsd/Cargo.toml
new file mode 100644
index 0000000..34fc60a
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-vhostfsd/Cargo.toml
@@ -0,0 +1,31 @@
+[package]
+name = "fc-vhostfsd"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+description = "vhost-user virtio-fs daemon serving OCI container image layers on demand"
+
+[lib]
+name = "fc_vhostfsd"
+path = "src/lib.rs"
+
+[[bin]]
+name = "fc-vhostfsd"
+path = "src/main.rs"
+
+[dependencies]
+anyhow.workspace = true
+clap.workspace = true
+env_logger.workspace = true
+fc-oci-fs = { path = "../fc-oci-fs" }
+fuse-backend-rs = { workspace = true, default-features = false, features = ["virtiofs"] }
+log.workspace = true
+tokio.workspace = true
+vhost.workspace = true
+vhost-user-backend.workspace = true
+virtio-bindings.workspace = true
+virtio-queue.workspace = true
+vm-memory.workspace = true
+vmm-sys-util.workspace = true
+nix = { version = "0.30", features = ["fs"] }
diff --git a/firecracker-container-vm/crates/fc-vhostfsd/src/backend.rs b/firecracker-container-vm/crates/fc-vhostfsd/src/backend.rs
new file mode 100644
index 0000000..f5e8186
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-vhostfsd/src/backend.rs
@@ -0,0 +1,191 @@
+use std::io::Result;
+use std::sync::{Arc, Mutex};
+
+use fc_oci_fs::OverlayFs;
+use fuse_backend_rs::api::server::Server;
+use fuse_backend_rs::transport::{FsCacheReqHandler, Reader, VirtioFsWriter};
+use vhost::vhost_user::message::VhostUserProtocolFeatures;
+use vhost::vhost_user::Backend;
+use vhost_user_backend::{VhostUserBackendMut, VringMutex, VringT};
+use virtio_bindings::bindings::virtio_ring::{
+ VIRTIO_RING_F_EVENT_IDX, VIRTIO_RING_F_INDIRECT_DESC,
+};
+use virtio_queue::DescriptorChain;
+use virtio_queue::QueueOwnedT;
+use vm_memory::{GuestAddressSpace, GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap};
+use vmm_sys_util::epoll::EventSet;
+use vmm_sys_util::event::{EventConsumer, EventNotifier};
+
+const VIRTIO_F_VERSION_1: u32 = 32;
+const QUEUE_SIZE: usize = 1024;
+const NUM_QUEUES: usize = 2;
+const HIPRIO_QUEUE_EVENT: u16 = 0;
+const REQ_QUEUE_EVENT: u16 = 1;
+
+/// virtio-fs config space: 36-byte tag + reserved fields.
+fn build_config(tag: &str) -> Vec {
+ let mut cfg = vec![0u8; 128];
+ let bytes = tag.as_bytes();
+ let n = bytes.len().min(36);
+ cfg[..n].copy_from_slice(&bytes[..n]);
+ cfg
+}
+
+pub struct FsBackend {
+ event_idx: bool,
+ kill_evt: (EventConsumer, EventNotifier),
+ mem: Option>,
+ server: Arc>>,
+ config: Vec,
+}
+
+impl FsBackend {
+ pub fn new(server: Arc>>, tag: String) -> Result {
+ Ok(Self {
+ event_idx: false,
+ kill_evt: vmm_sys_util::event::new_event_consumer_and_notifier(
+ vmm_sys_util::event::EventFlag::NONBLOCK,
+ )?,
+ mem: None,
+ server,
+ config: build_config(&tag),
+ })
+ }
+
+ fn process_queue(&mut self, vring_state: &mut vhost_user_backend::VringState) -> Result<()> {
+ let guest_mem = self
+ .mem
+ .as_ref()
+ .ok_or_else(|| std::io::Error::from(std::io::ErrorKind::NotConnected))?;
+ let avail_chains: Vec>> = vring_state
+ .get_queue_mut()
+ .iter(guest_mem.memory())
+ .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))?
+ .collect();
+
+ for chain in avail_chains {
+ let head_index = chain.head_index();
+ let mem = chain.memory();
+ let reader = Reader::from_descriptor_chain(mem, chain.clone())
+ .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))?;
+ let writer = VirtioFsWriter::new(mem, chain.clone())
+ .map_err(|_| std::io::Error::from(std::io::ErrorKind::InvalidData))?;
+
+ self.server
+ .handle_message(
+ reader,
+ fuse_backend_rs::transport::Writer::VirtioFs(writer),
+ None as Option<&mut dyn FsCacheReqHandler>,
+ None,
+ )
+ .map_err(|e| std::io::Error::other(e))?;
+ fc_oci_fs::metrics::record_fuse_request();
+
+ if self.event_idx {
+ if vring_state.add_used(head_index, 0).is_err() {
+ log::warn!("failed to add used descriptor");
+ }
+ match vring_state.needs_notification() {
+ Err(_) => {
+ vring_state.signal_used_queue().ok();
+ }
+ Ok(true) => {
+ vring_state.signal_used_queue().ok();
+ }
+ Ok(false) => {}
+ }
+ } else {
+ let _ = vring_state.add_used(head_index, 0);
+ vring_state.signal_used_queue().ok();
+ }
+ }
+ Ok(())
+ }
+}
+
+pub struct FsBackendHandler {
+ backend: Mutex,
+}
+
+impl FsBackendHandler {
+ pub fn new(server: Arc>>, tag: String) -> Result {
+ Ok(Self {
+ backend: Mutex::new(FsBackend::new(server, tag)?),
+ })
+ }
+}
+
+impl VhostUserBackendMut for FsBackendHandler {
+ type Bitmap = ();
+ type Vring = VringMutex;
+
+ fn num_queues(&self) -> usize {
+ NUM_QUEUES
+ }
+
+ fn max_queue_size(&self) -> usize {
+ QUEUE_SIZE
+ }
+
+ fn features(&self) -> u64 {
+ 1 << VIRTIO_F_VERSION_1
+ | 1 << VIRTIO_RING_F_INDIRECT_DESC
+ | 1 << VIRTIO_RING_F_EVENT_IDX
+ | vhost::vhost_user::message::VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
+ }
+
+ fn protocol_features(&self) -> VhostUserProtocolFeatures {
+ VhostUserProtocolFeatures::MQ
+ | VhostUserProtocolFeatures::CONFIG
+ | VhostUserProtocolFeatures::BACKEND_REQ
+ }
+
+ fn set_event_idx(&mut self, _enabled: bool) {
+ self.backend.lock().unwrap().event_idx = true;
+ }
+
+ fn get_config(&self, offset: u32, size: u32) -> Vec {
+ let backend = self.backend.lock().unwrap();
+ let start = offset as usize;
+ let end = (start + size as usize).min(backend.config.len());
+ backend.config[start..end].to_vec()
+ }
+
+ fn update_memory(&mut self, mem: GuestMemoryAtomic) -> Result<()> {
+ self.backend.lock().unwrap().mem = Some(mem);
+ Ok(())
+ }
+
+ fn set_backend_req_fd(&mut self, backend: Backend) {
+ let _ = backend;
+ }
+
+ fn exit_event(&self, _thread_index: usize) -> Option<(EventConsumer, EventNotifier)> {
+ let backend = self.backend.lock().unwrap();
+ Some((
+ backend.kill_evt.0.try_clone().ok()?,
+ backend.kill_evt.1.try_clone().ok()?,
+ ))
+ }
+
+ fn handle_event(
+ &mut self,
+ device_event: u16,
+ evset: EventSet,
+ vrings: &[VringMutex],
+ _thread_id: usize,
+ ) -> Result<()> {
+ if evset != EventSet::IN {
+ return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput));
+ }
+ let mut vring_state = match device_event {
+ HIPRIO_QUEUE_EVENT => vrings[0].get_mut(),
+ REQ_QUEUE_EVENT => vrings[1].get_mut(),
+ _ => return Err(std::io::Error::from(std::io::ErrorKind::InvalidInput)),
+ };
+ self.backend
+ .lock()
+ .unwrap()
+ .process_queue(&mut vring_state)
+ }
+}
diff --git a/firecracker-container-vm/crates/fc-vhostfsd/src/lib.rs b/firecracker-container-vm/crates/fc-vhostfsd/src/lib.rs
new file mode 100644
index 0000000..fceb141
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-vhostfsd/src/lib.rs
@@ -0,0 +1 @@
+pub mod backend;
diff --git a/firecracker-container-vm/crates/fc-vhostfsd/src/main.rs b/firecracker-container-vm/crates/fc-vhostfsd/src/main.rs
new file mode 100644
index 0000000..daed9aa
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-vhostfsd/src/main.rs
@@ -0,0 +1,91 @@
+//! vhost-user virtio-fs daemon exposing an on-demand OCI image rootfs.
+
+mod backend;
+mod metrics_server;
+
+use std::path::PathBuf;
+use std::sync::Arc;
+
+use anyhow::Context as _;
+use clap::Parser;
+use fc_oci_fs::{metrics, ImageFs, RegistryClient};
+use fuse_backend_rs::api::server::Server;
+use log::info;
+use tokio::runtime::Runtime;
+use vhost::vhost_user::Listener;
+use vhost_user_backend::VhostUserDaemon;
+use vm_memory::{GuestAddress, GuestMemoryAtomic, GuestMemoryMmap};
+
+use crate::backend::FsBackendHandler;
+
+#[derive(Debug, Parser)]
+#[command(name = "fc-vhostfsd", about = "vhost-user virtio-fs daemon for lazy OCI rootfs")]
+struct Args {
+ /// Container image reference (e.g. docker.io/library/alpine:3.20)
+ #[arg(long)]
+ image: String,
+
+ /// Unix socket path for the vhost-user frontend (Firecracker, Cloud Hypervisor, etc.)
+ #[arg(long, default_value = "/tmp/fc-vhostfs.sock")]
+ socket: PathBuf,
+
+ /// Cache directory for gzip indexes and downloaded layer blobs
+ #[arg(long, default_value = "/tmp/fc-oci-cache")]
+ cache_dir: PathBuf,
+
+ /// virtio-fs tag visible inside the guest
+ #[arg(long, default_value = "rootfs")]
+ tag: String,
+
+ /// Prometheus metrics listen address (GET /metrics)
+ #[arg(long, default_value = "127.0.0.1:9100")]
+ metrics_addr: String,
+}
+
+fn main() -> anyhow::Result<()> {
+ env_logger::init();
+ let args = Args::parse();
+
+ let cache_dir: Arc = Arc::from(args.cache_dir.as_path());
+ let _metrics = metrics_server::spawn_metrics_server(&args.metrics_addr, cache_dir.clone());
+
+ let rt = Runtime::new().context("create tokio runtime")?;
+ let mut registry = RegistryClient::new(&args.cache_dir);
+ let image_fs = rt
+ .block_on(ImageFs::open(&args.image, &args.cache_dir, &mut registry))
+ .context("open OCI image filesystem")?;
+
+ info!(
+ "serving {} ({} layers) on {}",
+ args.image,
+ image_fs.image.layer_digests.len(),
+ args.socket.display()
+ );
+
+ let server = Arc::new(Server::new(image_fs.overlay));
+ let backend = FsBackendHandler::new(server, args.tag)?;
+ let memfd = nix::sys::memfd::memfd_create("fc-vhostfs", nix::sys::memfd::MFdFlags::empty())
+ .map_err(|e| anyhow::anyhow!("memfd_create: {e}"))?;
+ let file = std::fs::File::from(memfd);
+ file.set_len(0x100000)
+ .map_err(|e| anyhow::anyhow!("set_len: {e}"))?;
+ let file_offset = vm_memory::FileOffset::new(file, 0);
+ let mem = GuestMemoryAtomic::new(
+ GuestMemoryMmap::from_ranges_with_files(&[(GuestAddress(0x100000), 0x100000, Some(file_offset))])
+ .map_err(|e| anyhow::anyhow!("guest memory: {e:?}"))?,
+ );
+ let backend = std::sync::Arc::new(std::sync::Mutex::new(backend));
+ let mut daemon = VhostUserDaemon::new("fc-vhostfsd".to_string(), backend, mem)
+ .map_err(|e| anyhow::anyhow!("create vhost-user daemon: {e:?}"))?;
+
+ if let Some(parent) = args.socket.parent() {
+ std::fs::create_dir_all(parent)?;
+ }
+ let _ = std::fs::remove_file(&args.socket);
+ let mut listener = Listener::new(&args.socket, true).context("bind vhost-user socket")?;
+ daemon.start(&mut listener).map_err(|e| anyhow::anyhow!("start daemon: {e:?}"))?;
+ metrics::mark_startup_ready();
+ info!("vhost-user virtio-fs daemon listening on {}", args.socket.display());
+ daemon.wait().map_err(|e| anyhow::anyhow!("daemon exited: {e:?}"))?;
+ Ok(())
+}
diff --git a/firecracker-container-vm/crates/fc-vhostfsd/src/metrics_server.rs b/firecracker-container-vm/crates/fc-vhostfsd/src/metrics_server.rs
new file mode 100644
index 0000000..0ea83a3
--- /dev/null
+++ b/firecracker-container-vm/crates/fc-vhostfsd/src/metrics_server.rs
@@ -0,0 +1,55 @@
+use std::io::{Read, Write};
+use std::net::{TcpListener, TcpStream};
+use std::path::Path;
+use std::sync::Arc;
+use std::thread;
+
+use fc_oci_fs::render_prometheus;
+use log::{error, info};
+
+pub fn spawn_metrics_server(addr: &str, cache_dir: Arc) -> thread::JoinHandle<()> {
+ let addr = addr.to_string();
+ thread::Builder::new()
+ .name("fc-metrics".into())
+ .spawn(move || {
+ let listener = match TcpListener::bind(&addr) {
+ Ok(l) => l,
+ Err(e) => {
+ error!("metrics server failed to bind {addr}: {e}");
+ return;
+ }
+ };
+ info!("prometheus metrics on http://{addr}/metrics");
+ for stream in listener.incoming() {
+ match stream {
+ Ok(mut stream) => {
+ if let Err(e) = handle_connection(&mut stream, &cache_dir) {
+ error!("metrics connection error: {e}");
+ }
+ }
+ Err(e) => error!("metrics accept error: {e}"),
+ }
+ }
+ })
+ .expect("spawn metrics thread")
+}
+
+fn handle_connection(stream: &mut TcpStream, cache_dir: &Path) -> std::io::Result<()> {
+ let mut buf = [0u8; 1024];
+ let n = stream.read(&mut buf)?;
+ let req = String::from_utf8_lossy(&buf[..n]);
+ let is_metrics = req.lines().next().is_some_and(|l| l.contains("GET /metrics"));
+ if !is_metrics {
+ let resp = "HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n";
+ stream.write_all(resp.as_bytes())?;
+ return Ok(());
+ }
+ let body = render_prometheus(cache_dir);
+ let resp = format!(
+ "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nContent-Length: {}\r\n\r\n{}",
+ body.len(),
+ body
+ );
+ stream.write_all(resp.as_bytes())?;
+ Ok(())
+}
diff --git a/firecracker-container-vm/scripts/build-firecracker-virtiofs.sh b/firecracker-container-vm/scripts/build-firecracker-virtiofs.sh
new file mode 100755
index 0000000..7848e5b
--- /dev/null
+++ b/firecracker-container-vm/scripts/build-firecracker-virtiofs.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+# Build Firecracker from the generic vhost-user frontend PR branch.
+# fc-runner needs PUT /vhost-user-devices/{id}, which is not in release builds yet.
+# Requires: docker, git, sudo (for devtool).
+set -euo pipefail
+
+DEPS_DIR="${DEPS_DIR:-$(cd "$(dirname "$0")/.." && pwd)/.deps}"
+FC_SRC="${DEPS_DIR}/firecracker-src"
+PR_REF="${FC_VHOST_PR_REF:-refs/pull/5773/head}"
+ARCH="$(uname -m)"
+
+mkdir -p "$DEPS_DIR"
+
+if [ ! -d "$FC_SRC/.git" ]; then
+ git clone https://github.com/firecracker-microvm/firecracker "$FC_SRC"
+fi
+
+cd "$FC_SRC"
+git fetch origin "$PR_REF:feat/generic-vhost-user" || {
+ echo "Failed to fetch ${PR_REF}. Check that PR #5773 is still available." >&2
+ exit 1
+}
+git checkout feat/generic-vhost-user
+
+echo "Building Firecracker (generic vhost-user) — this may take several minutes..."
+sudo systemctl start docker 2>/dev/null || true
+sudo ./tools/devtool build --release
+
+TARGET="build/cargo_target/${ARCH}-unknown-linux-musl/release/firecracker"
+install -m 0755 "$TARGET" "${DEPS_DIR}/firecracker-virtiofs"
+echo "Installed: ${DEPS_DIR}/firecracker-virtiofs"
+"${DEPS_DIR}/firecracker-virtiofs" --version
diff --git a/firecracker-container-vm/scripts/build-vmlinux-virtiofs.sh b/firecracker-container-vm/scripts/build-vmlinux-virtiofs.sh
new file mode 100755
index 0000000..e40bd92
--- /dev/null
+++ b/firecracker-container-vm/scripts/build-vmlinux-virtiofs.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+# Build a Firecracker-compatible guest vmlinux with CONFIG_VIRTIO_FS=y.
+#
+# Official CI vmlinux artifacts from S3 do NOT enable virtio-fs; you need this
+# step (or an equivalent custom kernel build) for fc-runner.
+#
+# Requires: docker, git, sudo. Expect roughly 15–30 minutes on a typical dev VM.
+set -euo pipefail
+
+DEPS_DIR="${DEPS_DIR:-$(cd "$(dirname "$0")/.." && pwd)/.deps}"
+FC_SRC="${DEPS_DIR}/firecracker-src-kernel"
+ARCH="$(uname -m)"
+KERNEL_VERSION="${KERNEL_VERSION:-6.1}"
+
+mkdir -p "$DEPS_DIR"
+
+if [ ! -d "$FC_SRC/.git" ]; then
+ git clone --depth 1 https://github.com/firecracker-microvm/firecracker "$FC_SRC"
+fi
+
+cd "$FC_SRC"
+
+CFG="resources/guest_configs/microvm-kernel-ci-${ARCH}-${KERNEL_VERSION}.config"
+if [ ! -f "$CFG" ]; then
+ echo "Kernel config not found: $CFG" >&2
+ exit 1
+fi
+
+# Enable virtio-fs in the guest kernel (FUSE_FS is already enabled in CI configs).
+if grep -q '^CONFIG_VIRTIO_FS=y' "$CFG"; then
+ echo "virtio-fs already enabled in $CFG"
+else
+ echo "Patching $CFG to enable CONFIG_VIRTIO_FS=y"
+ sed -i 's/# CONFIG_VIRTIO_FS is not set/CONFIG_VIRTIO_FS=y/' "$CFG"
+fi
+
+echo "Building guest kernel ${KERNEL_VERSION} via firecracker devtool..."
+sudo systemctl start docker 2>/dev/null || true
+sudo ./tools/devtool build_ci_artifacts kernels "${KERNEL_VERSION}"
+
+OUT_DIR="resources/${ARCH}"
+KERNEL="$(ls -1 "${OUT_DIR}"/vmlinux-"${KERNEL_VERSION}"* 2>/dev/null | grep -v debug | head -1)"
+if [ -z "$KERNEL" ]; then
+ echo "Kernel build finished but no vmlinux found under ${OUT_DIR}" >&2
+ ls -la "$OUT_DIR" || true
+ exit 1
+fi
+
+install -m 0644 "$KERNEL" "${DEPS_DIR}/vmlinux-virtiofs"
+echo "Installed: ${DEPS_DIR}/vmlinux-virtiofs"
+file "${DEPS_DIR}/vmlinux-virtiofs"
diff --git a/firecracker-container-vm/scripts/download-firecracker.sh b/firecracker-container-vm/scripts/download-firecracker.sh
new file mode 100755
index 0000000..c9ecb32
--- /dev/null
+++ b/firecracker-container-vm/scripts/download-firecracker.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# Download the latest official Firecracker release binary.
+# Note: released Firecracker does not yet include generic vhost-user / virtio-fs
+# (see scripts/build-firecracker-virtiofs.sh). Use this binary for block/net
+# workflows or as a baseline install.
+set -euo pipefail
+
+DEPS_DIR="${DEPS_DIR:-$(cd "$(dirname "$0")/.." && pwd)/.deps}"
+mkdir -p "$DEPS_DIR"
+cd "$DEPS_DIR"
+
+ARCH="$(uname -m)"
+release_url="https://github.com/firecracker-microvm/firecracker/releases"
+latest="$(basename "$(curl -fsSLI -o /dev/null -w '%{url_effective}' "${release_url}/latest")")"
+
+echo "Downloading Firecracker ${latest} for ${ARCH}..."
+curl -fsSL "${release_url}/download/${latest}/firecracker-${latest}-${ARCH}.tgz" | tar -xz
+
+BIN="release-${latest}-${ARCH}/firecracker-${latest}-${ARCH}"
+install -m 0755 "$BIN" "${DEPS_DIR}/firecracker"
+echo "Installed: ${DEPS_DIR}/firecracker"
+"${DEPS_DIR}/firecracker" --version
diff --git a/firecracker-container-vm/scripts/setup-host.sh b/firecracker-container-vm/scripts/setup-host.sh
new file mode 100755
index 0000000..a4270d4
--- /dev/null
+++ b/firecracker-container-vm/scripts/setup-host.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+# Fetch/build host dependencies for a full fc-runner smoke test.
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+export DEPS_DIR="${DEPS_DIR:-$ROOT/.deps}"
+
+echo "==> Firecracker with generic vhost-user (for virtio-fs)"
+"$ROOT/scripts/build-firecracker-virtiofs.sh"
+
+echo "==> Guest vmlinux with virtio-fs"
+"$ROOT/scripts/build-vmlinux-virtiofs.sh"
+
+echo
+echo "Done. Use:"
+echo " export PATH=\"$DEPS_DIR:\$PATH\""
+echo " fc-runner --firecracker $DEPS_DIR/firecracker-virtiofs --kernel $DEPS_DIR/vmlinux-virtiofs ..."