From b5b7542426d7f95d807b8adaaa8b5a6b8e95004a Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:00:35 -0400 Subject: [PATCH 01/24] init: scaffold Rust krun-init crate and wire into build system Replace the C-based build_default_init() in src/devices/build.rs with a Rust crate (init/) compiled via a cargo subprocess. The new build.rs probes whether the active rustc supports the $(uname -m)-unknown-linux-musl target (for a static binary) and falls back to the native target with a user-visible warning if not. The KRUN_INIT_BINARY_PATH override mechanism is preserved so that out-of-tree binaries (e.g. pre-built SEV or TDX images) can still be injected without rebuilding. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 Signed-off-by: Jake Correnti --- Cargo.lock | 340 ++++++++++++++++++------------------ Cargo.toml | 1 + Makefile | 3 + init/Cargo.toml | 22 +++ init/src/main.rs | 3 + src/arch/Cargo.toml | 10 +- src/cpuid/Cargo.toml | 6 +- src/devices/Cargo.toml | 8 +- src/init-blob/Cargo.toml | 5 + src/init-blob/build.rs | 181 ++++++++++++++----- src/kernel/Cargo.toml | 2 +- src/libkrun/Cargo.toml | 10 +- src/rutabaga_gfx/Cargo.toml | 2 +- src/smbios/Cargo.toml | 2 +- src/utils/Cargo.toml | 4 +- src/vmm/Cargo.toml | 10 +- 16 files changed, 373 insertions(+), 236 deletions(-) create mode 100644 init/Cargo.toml create mode 100644 init/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 56332f7f8..2347feff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -46,9 +46,9 @@ checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -92,9 +92,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -127,7 +127,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "cexpr", "clang-sys", "itertools", @@ -135,7 +135,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn", ] @@ -167,9 +167,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "block-buffer" @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bzip2" @@ -216,14 +216,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -339,15 +339,15 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -355,9 +355,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -384,13 +384,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -421,6 +420,30 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[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-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -474,9 +497,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -497,9 +520,9 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "imago" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e8e4b92aa0dd860579cfba776dbf0918a3a7ac5cb601af7d3fc835e71592a5b" +checksum = "ae7cfee876c698a1a2ed9c705ab18f21acbed82110f19b51cc458de73426fe2c" dependencies = [ "async-trait", "bincode", @@ -517,12 +540,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -560,9 +583,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" dependencies = [ "jiff-static", "log", @@ -573,9 +596,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" dependencies = [ "proc-macro2", "quote", @@ -594,10 +617,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -631,7 +656,7 @@ dependencies = [ "linux-loader", "tdx", "vm-memory", - "vmm-sys-util 0.14.0", + "vmm-sys-util", "windows-sys", ] @@ -659,7 +684,7 @@ version = "0.1.0-1.18.0" dependencies = [ "kvm-bindings", "kvm-ioctls", - "vmm-sys-util 0.14.0", + "vmm-sys-util", ] [[package]] @@ -685,13 +710,13 @@ dependencies = [ "log", "lru", "nix 0.30.1", - "rand 0.9.2", + "rand 0.9.4", "thiserror 2.0.18", "vhost", "virtio-bindings", "vm-fdt", "vm-memory", - "vmm-sys-util 0.15.0", + "vmm-sys-util", "zerocopy", ] @@ -700,7 +725,7 @@ name = "krun-display" version = "0.1.0" dependencies = [ "bindgen", - "bitflags 2.11.0", + "bitflags 2.12.1", "log", "static_assertions", "thiserror 2.0.18", @@ -716,12 +741,22 @@ dependencies = [ "log", ] +[[package]] +name = "krun-init" +version = "0.1.0-1.18.1" +dependencies = [ + "libc", + "nix 0.30.1", + "serde", + "serde_json", +] + [[package]] name = "krun-input" version = "0.1.0" dependencies = [ "bindgen", - "bitflags 2.11.0", + "bitflags 2.12.1", "libc", "log", "static_assertions", @@ -756,7 +791,7 @@ dependencies = [ "pkg-config", "remain", "thiserror 1.0.69", - "vmm-sys-util 0.14.0", + "vmm-sys-util", "winapi", "zerocopy", ] @@ -778,7 +813,7 @@ dependencies = [ "libc", "log", "nix 0.30.1", - "vmm-sys-util 0.14.0", + "vmm-sys-util", "windows-sys", ] @@ -787,7 +822,7 @@ name = "krun-vmm" version = "0.1.0-1.18.0" dependencies = [ "bitfield", - "bitflags 2.11.0", + "bitflags 2.12.1", "bzip2", "crossbeam-channel", "flate2", @@ -813,7 +848,7 @@ dependencies = [ "serde_json", "tdx", "vm-memory", - "vmm-sys-util 0.14.0", + "vmm-sys-util", "zstd", ] @@ -827,23 +862,23 @@ dependencies = [ [[package]] name = "kvm-bindings" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" +checksum = "4b3c06ff73c7ce03e780887ec2389d62d2a2a9ddf471ab05c2ff69207cd3f3b4" dependencies = [ - "vmm-sys-util 0.14.0", + "vmm-sys-util", ] [[package]] name = "kvm-ioctls" -version = "0.22.1" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c8f7370330b4f57981e300fa39b02088f2f2a5c2d0f1f994e8090589619c56d" +checksum = "333f77a20344a448f3f70664918135fddeb804e938f28a99d685bd92926e0b19" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "kvm-bindings", "libc", - "vmm-sys-util 0.14.0", + "vmm-sys-util", ] [[package]] @@ -854,9 +889,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libkrun" @@ -893,18 +928,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "libredox" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall", -] - [[package]] name = "linux-loader" version = "0.13.2" @@ -922,24 +945,24 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "log" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "lru" -version = "0.16.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -981,10 +1004,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b5b539a76e3f555fb143c3e67d5e05fa1d5fece02a515f6ecf41b3f1a081f58" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "libc", "nix 0.26.4", - "rand 0.9.2", + "rand 0.9.4", "vsock", ] @@ -994,7 +1017,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6436c562bcdb6f192e0e59f627bff5b0b88f2e1c48264079f4f1d6da42bec2d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "libc", "nix 0.26.4", "vsock", @@ -1019,7 +1042,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", @@ -1028,11 +1051,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "cfg-if", "cfg_aliases", "libc", @@ -1085,15 +1108,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1103,9 +1120,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] @@ -1161,9 +1178,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -1205,15 +1222,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "regex" version = "1.12.3" @@ -1256,9 +1264,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -1275,7 +1283,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "errno", "libc", "linux-raw-sys", @@ -1290,9 +1298,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -1326,9 +1334,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -1354,6 +1362,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -1376,9 +1390,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "sm3" @@ -1429,9 +1449,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -1440,17 +1460,17 @@ dependencies = [ [[package]] name = "tdx" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad59e5bf374211a1fdd8e7439a07d5a5e617fe97f5cf21d03bcd1bf8c82b73af" +checksum = "83943e37cf46979f711ad11489c641fa058fd0fae92c122d1fc26a664e82acab" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "iocuddle", "kvm-bindings", "kvm-ioctls", "libc", "uuid", - "vmm-sys-util 0.12.1", + "vmm-sys-util", ] [[package]] @@ -1495,9 +1515,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "pin-project-lite", ] @@ -1535,9 +1555,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -1565,9 +1585,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -1587,11 +1607,11 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c76d90ce3c6b37d610a5304c9a445cfff580cf8b4b9fd02fb256aaf68552c28a" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "libc", "uuid", "vm-memory", - "vmm-sys-util 0.15.0", + "vmm-sys-util", ] [[package]] @@ -1623,26 +1643,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "vmm-sys-util" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1435039746e20da4f8d507a72ee1b916f7b4b05af7a91c093d2c6561934ede" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "vmm-sys-util" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "vmm-sys-util" version = "0.15.0" @@ -1655,21 +1655,21 @@ dependencies = [ [[package]] name = "vsock" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b82aeb12ad864eb8cd26a6c21175d0bdc66d398584ee6c93c76964c3bcfc78ff" +checksum = "6ba782755fc073877e567c2253c0be48e4aa9a254c232d36d3985dfae0bd5205" dependencies = [ "libc", - "nix 0.31.2", + "nix 0.31.3", ] [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -1678,14 +1678,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -1696,9 +1696,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1706,9 +1706,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -1719,9 +1719,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -1754,7 +1754,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.12.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -1806,6 +1806,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -1855,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.0", + "bitflags 2.12.1", "indexmap", "log", "serde", @@ -1897,18 +1903,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 35b1dbba4..6e85e8c60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "init", "src/libkrun", "src/init-blob", "src/input", diff --git a/Makefile b/Makefile index 6f6a3c4fa..9de26c976 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,9 @@ endif ifeq ($(VHOST_USER),1) FEATURE_FLAGS += --features vhost-user endif +ifeq ($(TIMESYNC),1) + FEATURE_FLAGS += --features timesync +endif ifeq ($(AWS_NITRO),1) VARIANT = -awsnitro FEATURE_FLAGS := --features aws-nitro,net diff --git a/init/Cargo.toml b/init/Cargo.toml new file mode 100644 index 000000000..b1b7d264b --- /dev/null +++ b/init/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "krun-init" +version = "0.1.0-1.18.1" +edition = "2024" +description = "PID-1 init binary for libkrun guest VMs" +license = "Apache-2.0" +repository = "https://github.com/containers/libkrun" + +[[bin]] +name = "krun-init" +path = "src/main.rs" + +[features] +amd-sev = [] +tdx = [] +timesync = [] + +[dependencies] +libc = "0.2" +nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "process", "reboot", "resource", "signal", "socket", "term", "uio"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/init/src/main.rs b/init/src/main.rs new file mode 100644 index 000000000..5b2e5a6c3 --- /dev/null +++ b/init/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("hello, world!"); +} diff --git a/src/arch/Cargo.toml b/src/arch/Cargo.toml index 764b402aa..719c8fb7f 100644 --- a/src/arch/Cargo.toml +++ b/src/arch/Cargo.toml @@ -14,8 +14,8 @@ tdx = [ "tee", "dep:tdx" ] [dependencies] libc = ">=0.2.39" -vm-memory = { version = "0.17", default-features = false, features = ["backend-mmap"] } -vmm-sys-util = "0.14" +vm-memory = { version = "=0.17.1", default-features = false, features = ["backend-mmap"] } +vmm-sys-util = "0.15" arch_gen = { package = "krun-arch-gen", version = "=0.1.0-1.18.0", path = "../arch_gen" } utils = { package = "krun-utils", version = "=0.1.0-1.18.0", path = "../utils" } @@ -24,9 +24,9 @@ utils = { package = "krun-utils", version = "=0.1.0-1.18.0", path = "../utils" } smbios = { package = "krun-smbios", version = "=0.1.0-1.18.0", path = "../smbios" } [target.'cfg(target_os = "linux")'.dependencies] -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" -tdx = { version = "0.1.0", optional = true } +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } +kvm-ioctls = "0.24" +tdx = { version = "0.1.1", optional = true } [target.'cfg(all(target_arch = "x86_64", target_os = "linux"))'.dependencies] linux-loader = { version = "0.13.2", features = ["elf"] } diff --git a/src/cpuid/Cargo.toml b/src/cpuid/Cargo.toml index bbbfa98d5..f3c4ab884 100644 --- a/src/cpuid/Cargo.toml +++ b/src/cpuid/Cargo.toml @@ -11,8 +11,8 @@ repository = "https://github.com/containers/libkrun" tdx = [] [dependencies] -vmm-sys-util = "0.14" +vmm-sys-util = "0.15" [target.'cfg(target_os = "linux")'.dependencies] -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } +kvm-ioctls = "0.24" diff --git a/src/devices/Cargo.toml b/src/devices/Cargo.toml index 9c79a8e58..f314e9127 100644 --- a/src/devices/Cargo.toml +++ b/src/devices/Cargo.toml @@ -33,7 +33,7 @@ thiserror = { version = "2.0", optional = true } vhost = { version = "0.15", optional = true, features = ["vhost-user-frontend"] } vmm-sys-util = { version = "0.15", optional = true } virtio-bindings = "0.2.0" -vm-memory = { version = "0.17", features = ["backend-mmap"] } +vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } zerocopy = { version = "0.8.26", optional = true, features = ["derive"] } krun_display = { package = "krun-display", version = "0.1.0", path = "../display", optional = true, features = ["bindgen_clang_runtime"] } krun_input = { package = "krun-input", version = "0.1.0", path = "../input", features = ["bindgen_clang_runtime"], optional = true } @@ -42,7 +42,7 @@ arch = { package = "krun-arch", version = "=0.1.0-1.18.0", path = "../arch" } utils = { package = "krun-utils", version = "=0.1.0-1.18.0", path = "../utils" } polly = { package = "krun-polly", version = "=0.1.0-1.18.0", path = "../polly" } rutabaga_gfx = { package = "krun-rutabaga-gfx", version = "=0.1.0-1.18.0", path = "../rutabaga_gfx", features = ["virgl_renderer", "virgl_renderer_next"], optional = true } -imago = { version = "0.2.2", features = ["sync-wrappers", "vm-memory"] } +imago = { version = "0.2.3", features = ["sync-wrappers", "vm-memory"] } [target.'cfg(target_os = "macos")'.dependencies] hvf = { package = "krun-hvf", version = "=0.1.0-1.18.0", path = "../hvf" } @@ -51,8 +51,8 @@ lru = ">=0.9" [target.'cfg(target_os = "linux")'.dependencies] rutabaga_gfx = { package = "krun-rutabaga-gfx", version = "=0.1.0-1.18.0", path = "../rutabaga_gfx", features = ["x"], optional = true } caps = "0.5.5" -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } +kvm-ioctls = "0.24" [target.'cfg(any(target_arch = "aarch64", target_arch = "riscv64"))'.dependencies] vm-fdt = ">= 0.2.0" diff --git a/src/init-blob/Cargo.toml b/src/init-blob/Cargo.toml index 6be3f306c..1a32151c2 100644 --- a/src/init-blob/Cargo.toml +++ b/src/init-blob/Cargo.toml @@ -7,5 +7,10 @@ license = "Apache-2.0" repository = "https://github.com/containers/libkrun" build = "build.rs" +[features] +amd-sev = [] +tdx = [] +timesync = [] + [lib] path = "src/lib.rs" diff --git a/src/init-blob/build.rs b/src/init-blob/build.rs index 49a4346d2..eb47e6979 100644 --- a/src/init-blob/build.rs +++ b/src/init-blob/build.rs @@ -1,65 +1,162 @@ -use std::ffi::OsStr; +use std::env; use std::path::PathBuf; use std::process::Command; -fn build_default_init() -> PathBuf { - let manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let libkrun_root = manifest_dir.join("../.."); - let init_src = libkrun_root.join("init/init.c"); - let dhcp_src = libkrun_root.join("init/dhcp.c"); +fn musl_target_for_host() -> &'static str { + let host = env::var("HOST").unwrap_or_default(); + if host.starts_with("aarch64") { + "aarch64-unknown-linux-musl" + } else { + "x86_64-unknown-linux-musl" + } +} + +fn musl_supported(rustc: &str) -> bool { + let musl_target = musl_target_for_host(); + let output = Command::new(rustc) + .args(["--target", musl_target, "--print", "sysroot"]) + .output(); + match output { + Ok(o) if o.status.success() => { + let sysroot = PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()); + sysroot + .join("lib/rustlib") + .join(musl_target) + .join("lib") + .exists() + } + _ => false, + } +} + +/// Return a rustc binary that has the musl target's std library available. +/// +/// Tries the active rustc first. If that fails, searches ~/.rustup/toolchains/ +/// for a stable toolchain that does support musl — covering the common case +/// where the system package manager's rustc (e.g. Fedora's /usr/bin/rustc) +/// is used as the workspace compiler but the user also has a rustup toolchain +/// with musl support installed. +fn find_musl_rustc(default_rustc: &str) -> Option { + if musl_supported(default_rustc) { + return Some(PathBuf::from(default_rustc)); + } + + let home = env::var_os("HOME")?; + let toolchains = PathBuf::from(home).join(".rustup").join("toolchains"); + let mut candidates: Vec = std::fs::read_dir(&toolchains) + .ok()? + .flatten() + .map(|e| e.path().join("bin").join("rustc")) + .filter(|p| p.exists()) + .collect(); - let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap()); + // Prefer stable toolchains over nightly/beta. + candidates.sort_by_key(|p| !p.to_string_lossy().contains("stable")); + + candidates + .into_iter() + .find(|rustc| musl_supported(rustc.to_str().unwrap_or(""))) +} + +fn build_rust_init() -> PathBuf { + let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let workspace_root = manifest_dir.join("../.."); + let init_manifest = workspace_root.join("init/Cargo.toml"); + + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + // Separate target dir avoids conflicting with the parent workspace cargo lock. + let init_target_dir = out_dir.join("init-target"); let init_bin = out_dir.join("init"); - println!("cargo:rerun-if-env-changed=CC_LINUX"); - println!("cargo:rerun-if-env-changed=CC"); - println!("cargo:rerun-if-env-changed=TIMESYNC"); - println!("cargo:rerun-if-changed={}", init_src.display()); - println!("cargo:rerun-if-changed={}", dhcp_src.display()); - println!( - "cargo:rerun-if-changed={}", - libkrun_root.join("init/jsmn.h").display() - ); + let musl_target = musl_target_for_host(); + let profile = env::var("PROFILE").unwrap_or_else(|_| "release".to_string()); + let default_cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let default_rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); + println!( "cargo:rerun-if-changed={}", - libkrun_root.join("init/dhcp.h").display() + workspace_root.join("init/src").display() ); + println!("cargo:rerun-if-changed={}", init_manifest.display()); + println!("cargo:rerun-if-env-changed=TIMESYNC"); + + // Resolve which rustc (and paired cargo) to use for the init binary. + let (rustc, cargo, use_musl) = match find_musl_rustc(&default_rustc) { + Some(musl_rustc) => { + // Use the cargo from the same toolchain bin/ directory so that + // it inherits the same sysroot and target support. + let cargo = musl_rustc + .parent() + .map(|bin| bin.join("cargo")) + .filter(|p| p.exists()) + .map(|p| p.to_string_lossy().into_owned()) + .unwrap_or(default_cargo); + (musl_rustc.to_string_lossy().into_owned(), cargo, true) + } + None => { + println!( + "cargo:warning=musl target not available; krun-init will be dynamically linked. \ + Run `rustup target add $(uname -m)-unknown-linux-musl` for a static binary." + ); + (default_rustc, default_cargo, false) + } + }; - let mut init_cc_flags = vec!["-O2", "-static", "-Wall"]; - if std::env::var_os("TIMESYNC").as_deref() == Some(OsStr::new("1")) { - init_cc_flags.push("-D__TIMESYNC__"); + let mut cmd = Command::new(&cargo); + cmd.arg("build") + .arg("--manifest-path") + .arg(&init_manifest) + .arg("--target-dir") + .arg(&init_target_dir) + .env("RUSTC", &rustc); + + if profile == "release" { + cmd.arg("--release"); } - let cc_value = std::env::var("CC_LINUX") - .or_else(|_| std::env::var("CC")) - .unwrap_or_else(|_| "cc".to_string()); - let mut cc_parts = cc_value.split_ascii_whitespace(); - let cc = cc_parts.next().expect("CC_LINUX/CC must not be empty"); - let status = Command::new(cc) - .args(cc_parts) - .args(&init_cc_flags) - .arg("-o") - .arg(&init_bin) - .arg(&init_src) - .arg(&dhcp_src) - .status() - .unwrap_or_else(|e| panic!("failed to execute {cc}: {e}")); + if use_musl { + cmd.arg("--target").arg(musl_target); + } + let mut features: Vec<&str> = Vec::new(); + if cfg!(feature = "amd-sev") { + features.push("amd-sev"); + } + if cfg!(feature = "tdx") { + features.push("tdx"); + } + if env::var_os("TIMESYNC").is_some_and(|v| v == "1") { + features.push("timesync"); + } + if !features.is_empty() { + cmd.arg("--features").arg(features.join(",")); + } + + let status = cmd + .status() + .unwrap_or_else(|e| panic!("failed to run {cargo}: {e}")); if !status.success() { - panic!("failed to compile init/init.c: {status}"); + panic!("failed to build krun-init"); } + + let built = if use_musl { + // Cross-compilation: cargo places the binary at /// + init_target_dir + .join(musl_target) + .join(&profile) + .join("krun-init") + } else { + init_target_dir.join(&profile).join("krun-init") + }; + std::fs::copy(&built, &init_bin).unwrap_or_else(|e| panic!("failed to copy krun-init: {e}")); + init_bin } fn main() { - let init_binary_path = std::env::var_os("KRUN_INIT_BINARY_PATH") + let init_binary_path = env::var_os("KRUN_INIT_BINARY_PATH") .map(PathBuf::from) - .unwrap_or_else(|| { - let init_path = build_default_init(); - // SAFETY: The build script is single threaded. - unsafe { std::env::set_var("KRUN_INIT_BINARY_PATH", &init_path) }; - init_path - }); + .unwrap_or_else(build_rust_init); println!( "cargo:rustc-env=KRUN_INIT_BINARY_PATH={}", init_binary_path.display() diff --git a/src/kernel/Cargo.toml b/src/kernel/Cargo.toml index 34dde25dd..ca6fec136 100644 --- a/src/kernel/Cargo.toml +++ b/src/kernel/Cargo.toml @@ -7,6 +7,6 @@ license = "Apache-2.0" repository = "https://github.com/containers/libkrun" [dependencies] -vm-memory = { version = "0.17", features = ["backend-mmap"] } +vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } utils = { package = "krun-utils", version = "=0.1.0-1.18.0", path = "../utils" } diff --git a/src/libkrun/Cargo.toml b/src/libkrun/Cargo.toml index 7cc28c0a0..7ab669fbf 100644 --- a/src/libkrun/Cargo.toml +++ b/src/libkrun/Cargo.toml @@ -10,8 +10,8 @@ repository = "https://github.com/containers/libkrun" [features] tee = ["vmm/tee", "devices/tee"] -amd-sev = ["blk", "tee", "vmm/amd-sev", "devices/amd-sev"] -tdx = ["blk", "tee", "vmm/tdx", "devices/tdx"] +amd-sev = ["blk", "tee", "vmm/amd-sev", "devices/amd-sev", "init-blob/amd-sev"] +tdx = ["blk", "tee", "vmm/tdx", "devices/tdx", "init-blob/tdx"] net = ["devices/net", "vmm/net"] blk = ["devices/blk", "vmm/blk"] gpu = ["vmm/gpu", "devices/gpu", "krun_display"] @@ -40,11 +40,11 @@ vmm = { package = "krun-vmm", version = "=0.1.0-1.18.0", path = "../vmm" } hvf = { package = "krun-hvf", version = "=0.1.0-1.18.0", path = "../hvf" } [target.'cfg(target_os = "linux")'.dependencies] -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } +kvm-ioctls = "0.24" aws-nitro = { package = "krun-aws-nitro", version = "=0.1.0-1.18.0", path = "../aws_nitro", optional = true } nitro-enclaves = { version = "0.5.0", optional = true } -vm-memory = { version = "0.17", features = ["backend-mmap"] } +vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } [lib] name = "krun" diff --git a/src/rutabaga_gfx/Cargo.toml b/src/rutabaga_gfx/Cargo.toml index d06bc619a..3bfe820a4 100644 --- a/src/rutabaga_gfx/Cargo.toml +++ b/src/rutabaga_gfx/Cargo.toml @@ -27,7 +27,7 @@ remain = "0.2" thiserror = "1.0.23" zerocopy = { version = "0.8.26", features = ["derive"] } log = "0.4" -vmm-sys-util = "0.14" +vmm-sys-util = "0.15" [target.'cfg(unix)'.dependencies] nix = { version = "0.30.1", features = ["event", "feature", "fs", "mman", "socket", "uio", "ioctl"] } diff --git a/src/smbios/Cargo.toml b/src/smbios/Cargo.toml index ebdf74153..7bd17804f 100644 --- a/src/smbios/Cargo.toml +++ b/src/smbios/Cargo.toml @@ -7,4 +7,4 @@ license = "Apache-2.0" repository = "https://github.com/containers/libkrun" [dependencies] -vm-memory = { version = "0.17", features = ["backend-mmap"] } +vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } diff --git a/src/utils/Cargo.toml b/src/utils/Cargo.toml index 5b2e88757..240b0b983 100644 --- a/src/utils/Cargo.toml +++ b/src/utils/Cargo.toml @@ -14,11 +14,11 @@ log = "0.4.0" [target.'cfg(unix)'.dependencies] libc = ">=0.2.85" nix = "0.30.1" -vmm-sys-util = "0.14" +vmm-sys-util = "0.15" crossbeam-channel = ">=0.5.15" [target.'cfg(target_os = "linux")'.dependencies] -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } [target.'cfg(target_os = "macos")'.dependencies] nix = { version = "0.30.1", features = ["fs"] } diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index c16bd6ff9..299717d3c 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -26,8 +26,8 @@ libc = ">=0.2.39" linux-loader = { version = "0.13.2", features = ["bzimage", "elf", "pe"] } log = "0.4.0" nix = { version = "0.30.1", features = ["fs", "term"] } -vm-memory = { version = "0.17.0", features = ["backend-mmap"] } -vmm-sys-util = "0.14" +vm-memory = { version = "=0.17.1", features = ["backend-mmap"] } +vmm-sys-util = "0.15" krun_display = { package = "krun-display", version = "0.1.0", path = "../display", optional = true, features = ["bindgen_clang_runtime"] } krun_input = { package = "krun-input", version = "0.1.0", path = "../input", optional = true, features = ["bindgen_clang_runtime"] } @@ -52,9 +52,9 @@ cpuid = { package = "krun-cpuid", version = "=0.1.0-1.18.0", path = "../cpuid" } zstd = "0.13" [target.'cfg(target_os = "linux")'.dependencies] -tdx = { version = "0.1.0", optional = true } -kvm-bindings = { version = "0.12", features = ["fam-wrappers"] } -kvm-ioctls = "0.22" +tdx = { version = "0.1.1", optional = true } +kvm-bindings = { version = "0.14", features = ["fam-wrappers"] } +kvm-ioctls = "0.24" [target.'cfg(target_os = "macos")'.dependencies] hvf = { package = "krun-hvf", version = "=0.1.0-1.18.0", path = "../hvf" } From 4de7b4366b63edb0bfa869d1b77b4f8c937c1a53 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:10:26 -0400 Subject: [PATCH 02/24] init: implement filesystem mounting Add init/src/fs.rs with: - mount_or_busy(): helper that treats EBUSY as success - mount_filesystems(): mounts devtmpfs, proc, sysfs, cgroup2, devpts, tmpfs(/dev/shm), and creates the /dev/fd symlink - is_mount_point(): parses /proc/mounts (avoids triggering Podman auto-mounts that stat() would cause) - mount_tmpfs(): mounts a tmpfs at an arbitrary path Implement mount_tee_block_root() function used by both SEV and TDX features to mount /dev/vda and chroot into it. For amd-sev this replaces the previous LUKS/KBS attestation path entirely. The SEV and TDX boot paths are now identical at the init level. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- Cargo.lock | 1 + init/Cargo.toml | 1 + init/src/fs.rs | 114 +++++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 5 ++- 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 init/src/fs.rs diff --git a/Cargo.lock b/Cargo.lock index 2347feff5..fc96a6b37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,7 @@ dependencies = [ name = "krun-init" version = "0.1.0-1.18.1" dependencies = [ + "anyhow", "libc", "nix 0.30.1", "serde", diff --git a/init/Cargo.toml b/init/Cargo.toml index b1b7d264b..fa8af0a98 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -16,6 +16,7 @@ tdx = [] timesync = [] [dependencies] +anyhow = "1" libc = "0.2" nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "process", "reboot", "resource", "signal", "socket", "term", "uio"] } serde = { version = "1", features = ["derive"] } diff --git a/init/src/fs.rs b/init/src/fs.rs new file mode 100644 index 000000000..b4197f817 --- /dev/null +++ b/init/src/fs.rs @@ -0,0 +1,114 @@ +use std::fs::{self, File}; +use std::io::{BufRead, BufReader}; +use std::os::unix::fs as unix_fs; + +use anyhow::Context; +use nix::errno::Errno; +use nix::mount::{self, MsFlags}; +use nix::unistd; + +/// Mount, treating EBUSY (already mounted) as success. +fn mount_or_busy( + src: Option<&str>, + target: &str, + fstype: Option<&str>, + flags: MsFlags, +) -> anyhow::Result<()> { + match mount::mount(src, target, fstype, flags, None::<&str>) { + Ok(()) => Ok(()), + Err(Errno::EBUSY) => Ok(()), + Err(e) => Err(e).with_context(|| format!("mount {target}")), + } +} + +pub fn mount_filesystems() -> anyhow::Result<()> { + let base_flags = MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_RELATIME; + fs::create_dir_all("/dev").context("create /dev")?; + fs::create_dir_all("/proc").context("create /proc")?; + fs::create_dir_all("/sys").context("create /sys")?; + + mount_or_busy( + Some("devtmpfs"), + "/dev", + Some("devtmpfs"), + MsFlags::MS_RELATIME, + )?; + + mount_or_busy( + Some("proc"), + "/proc", + Some("proc"), + MsFlags::MS_NODEV | base_flags, + )?; + + mount_or_busy( + Some("sysfs"), + "/sys", + Some("sysfs"), + MsFlags::MS_NODEV | base_flags, + )?; + + mount_or_busy( + Some("cgroup2"), + "/sys/fs/cgroup", + Some("cgroup2"), + MsFlags::MS_NODEV | base_flags, + )?; + + fs::create_dir_all("/dev/pts").context("create /dev/pts")?; + fs::create_dir_all("/dev/shm").context("create /dev/shm")?; + + mount_or_busy(Some("devpts"), "/dev/pts", Some("devpts"), base_flags)?; + mount_or_busy(Some("tmpfs"), "/dev/shm", Some("tmpfs"), base_flags)?; + + // Best-effort; may already exist. + let _ = unix_fs::symlink("/proc/self/fd", "/dev/fd"); + + Ok(()) +} + +/// Returns true if path is listed as a mount point in /proc/mounts. +/// +/// Uses /proc/mounts instead of stat() because Podman arranges tmpfs +/// auto-mounts that would be triggered by a stat call. +pub fn is_mount_point(path: &str) -> bool { + let Ok(f) = File::open("/proc/mounts") else { + return false; + }; + for line in BufReader::new(f).lines().map_while(Result::ok) { + let mut parts = line.split_whitespace(); + let _ = parts.next(); // device + if parts.next() == Some(path) { + return true; + } + } + false +} + +pub fn mount_tmpfs(path: &str) -> anyhow::Result<()> { + mount::mount( + Some("tmpfs"), + path, + Some("tmpfs"), + MsFlags::MS_NOEXEC | MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_RELATIME, + None::<&str>, + ) + .with_context(|| format!("mount tmpfs at {path}")) +} + +/// Mount /dev/vda as ext4, then pivot root into it. +#[cfg(any(feature = "amd-sev", feature = "tdx"))] +pub fn mount_tee_block_device() -> anyhow::Result<()> { + fs::create_dir_all("/tmp/vda").context("create /tmp/vda")?; + + mount_or_busy( + Some("/dev/vda"), + "/tmp/vda", + Some("ext4"), + MsFlags::MS_RELATIME, + )?; + unistd::chdir("/tmp/vda").context("chdir /tmp/vda")?; + + mount_or_busy(Some("."), "/", None::<&str>, MsFlags::MS_MOVE)?; + unistd::chroot(".").context("chroot .") +} diff --git a/init/src/main.rs b/init/src/main.rs index 5b2e5a6c3..45fb2ec26 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,3 +1,6 @@ +mod fs; + fn main() { - println!("hello, world!"); + #[cfg(any(feature = "amd-sev", feature = "tdx"))] + fs::mount_tee_block_device().expect("mount block root failed"); } From f16b0c961156a3b5288157e76f476f4f73a094d7 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:25:00 -0400 Subject: [PATCH 03/24] init: implement KRUN_BLOCK_ROOT_DEVICE pivot and shared root mount Extend fs.rs with: - try_mount(): mounts with a known fstype, or probes /proc/filesystems when fstype is None - mount_block_root_device(): handles KRUN_BLOCK_ROOT_DEVICE by mounting the block device at /newroot, issuing KRUN_REMOVE_ROOT_DIR_IOCTL to drop the virtiofs temporary root, then pivoting with MS_MOVE - mount_shared_root(): sets MS_REC|MS_SHARED propagation on / Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/fs.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/init/src/fs.rs b/init/src/fs.rs index b4197f817..c68ccd9c6 100644 --- a/init/src/fs.rs +++ b/init/src/fs.rs @@ -1,12 +1,15 @@ +use std::env; use std::fs::{self, File}; use std::io::{BufRead, BufReader}; use std::os::unix::fs as unix_fs; -use anyhow::Context; +use anyhow::{Context, bail}; use nix::errno::Errno; use nix::mount::{self, MsFlags}; use nix::unistd; +const KRUN_REMOVE_ROOT_DIR_IOCTL: libc::c_ulong = 0x7603; + /// Mount, treating EBUSY (already mounted) as success. fn mount_or_busy( src: Option<&str>, @@ -112,3 +115,82 @@ pub fn mount_tee_block_device() -> anyhow::Result<()> { mount_or_busy(Some("."), "/", None::<&str>, MsFlags::MS_MOVE)?; unistd::chroot(".").context("chroot .") } + +/// Mount source onto target, trying each non-virtual filesystem listed in +/// /proc/filesystems when fstype is None. +pub fn try_mount( + source: &str, + target: &str, + fstype: Option<&str>, + flags: MsFlags, + data: Option<&str>, +) -> anyhow::Result<()> { + if let Some(fs) = fstype { + return mount::mount(Some(source), target, Some(fs), flags, data) + .with_context(|| format!("mount {source} -> {target} as {fs}")); + } + + let f = File::open("/proc/filesystems").context("open /proc/filesystems")?; + for line in BufReader::new(f).lines().map_while(Result::ok) { + if line.starts_with("nodev") { + continue; + } + let fs = line.trim(); + if mount::mount(Some(source), target, Some(fs), flags, data).is_ok() { + return Ok(()); + } + } + bail!("no supported filesystem found for {source}") +} + +/// Handle KRUN_BLOCK_ROOT_DEVICE: mount the block device at /newroot, +/// ask the virtiofs device to remove the temporary root, then pivot. +pub fn mount_block_root_device() -> anyhow::Result<()> { + let Some(krun_root) = env::var_os("KRUN_BLOCK_ROOT_DEVICE") else { + return Ok(()); + }; + let krun_root = krun_root.to_string_lossy().into_owned(); + + fs::create_dir_all("/newroot").context("create /newroot")?; + + let fstype = env::var("KRUN_BLOCK_ROOT_FSTYPE").ok(); + let options = env::var("KRUN_BLOCK_ROOT_OPTIONS").ok(); + + try_mount( + &krun_root, + "/newroot", + fstype.as_deref(), + MsFlags::empty(), + options.as_deref(), + )?; + + unistd::chdir("/newroot").context("chdir /newroot")?; + + // Ask the virtiofs device to tear down the temporary root directory. + let fd = unsafe { libc::open(c"/".as_ptr().cast(), libc::O_RDONLY) }; + if fd >= 0 { + unsafe { libc::ioctl(fd, KRUN_REMOVE_ROOT_DIR_IOCTL as _) }; + unsafe { libc::close(fd) }; + } + + mount::mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>) + .context("pivot root MS_MOVE")?; + + unistd::chroot(".").context("chroot after block root pivot")?; + + // Re-mount standard filesystems now that we're in the new root. + mount_filesystems()?; + + Ok(()) +} + +pub fn mount_shared_root() -> anyhow::Result<()> { + mount::mount( + None::<&str>, + "/", + None::<&str>, + MsFlags::MS_REC | MsFlags::MS_SHARED, + None::<&str>, + ) + .context("set MS_SHARED on root mount") +} From a9d9ddfff516779a935d8f29a677d016722eadf2 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:33:40 -0400 Subject: [PATCH 04/24] init: implement DHCP client Port init/dhcp.c to Rust in init/src/dhcp.rs. The public surface is a single do_dhcp(iface) function with the same behaviour as the C version: - Sends DHCPDISCOVER with Rapid Commit (option 80) - On DHCPACK: applies address, route, MTU, and DNS directly - On DHCPOFFER: completes the 4-way handshake, then applies - On no response: returns Ok (VM may be IPv6-only) Netlink structs not exposed by libc (ifinfomsg, ifaddrmsg, rtmsg) are defined locally with #[repr(C)]. sockaddr_nl and sockaddr_in are zero-initialised via mem::zeroed() to handle opaque padding fields. Assisted-by: Claude Code:claude-sonnet-4.6 Signed-off-by: Jake Correnti --- init/Cargo.toml | 2 +- init/src/dhcp.rs | 627 +++++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 1 + 3 files changed, 629 insertions(+), 1 deletion(-) create mode 100644 init/src/dhcp.rs diff --git a/init/Cargo.toml b/init/Cargo.toml index fa8af0a98..1748ddfeb 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -18,6 +18,6 @@ timesync = [] [dependencies] anyhow = "1" libc = "0.2" -nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "process", "reboot", "resource", "signal", "socket", "term", "uio"] } +nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "net", "process", "reboot", "resource", "signal", "socket", "term", "uio"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/init/src/dhcp.rs b/init/src/dhcp.rs new file mode 100644 index 000000000..f76daf7ae --- /dev/null +++ b/init/src/dhcp.rs @@ -0,0 +1,627 @@ +use std::ffi::OsString; +use std::fmt::Write as _; +use std::io::Error as IoError; +use std::mem; +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::slice; + +use anyhow::{Context, bail}; +use nix::errno::Errno; +use nix::net::if_; +use nix::sys::socket::{ + self, AddressFamily, MsgFlags, SockFlag, SockProtocol, SockType, SockaddrIn, sockopt, +}; +use nix::sys::time::{TimeVal, TimeValLike}; +use nix::unistd; + +const DHCP_BUFFER_SIZE: usize = 576; +/// BOOTP vendor-specific area size (64) - magic cookie (4) +const DHCP_OPTIONS_SIZE: usize = 60; +const DHCP_OPTIONS_OFFSET: usize = 240; +const DHCP_OPTIONS_END: u8 = 0xff; +const DHCP_MSG_OFFER: u8 = 2; +const DHCP_MSG_ACK: u8 = 5; +/// RFC 2131: BOOTP/DHCP server +const DHCP_SERVER_PORT: u16 = 67; +/// RFC 2131: BOOTP/DHCP client +const DHCP_CLIENT_PORT: u16 = 68; +/// RFC 2131: client-to-server message +const BOOTREQUEST: u8 = 1; +/// IANA hardware type for Ethernet +const HTYPE_ETHERNET: u8 = 1; +/// Ethernet MAC address length in bytes +const HLEN_ETHERNET: u8 = 6; +/// RFC 2131 §2: request broadcast reply (client has no IP yet) +const DHCP_FLAG_BROADCAST: u16 = 0x8000; +/// RFC 2132: marks options as DHCP format (99.130.83.99) +const DHCP_MAGIC_COOKIE: u32 = 0x6382_5363; + +// libc doesn't expose ifinfomsg, ifaddrmsg, or rtmsg — define them locally. + +#[repr(C)] +struct IfInfoMsg { + ifi_family: u8, + _pad: u8, + ifi_type: u16, + ifi_index: i32, + ifi_flags: u32, + ifi_change: u32, +} + +#[repr(C)] +struct IfAddrMsg { + ifa_family: u8, + ifa_prefixlen: u8, + ifa_flags: u8, + ifa_scope: u8, + ifa_index: u32, +} + +#[repr(C)] +struct RtMsg { + rtm_family: u8, + rtm_dst_len: u8, + rtm_src_len: u8, + rtm_tos: u8, + rtm_table: u8, + rtm_protocol: u8, + rtm_scope: u8, + rtm_type: u8, + rtm_flags: u32, +} + +/// DHCP Packet structure (RFC 2131) +#[repr(C, packed)] +struct DhcpPacket { + /// Message op code / message type + op: u8, + /// Hardware address type + htype: u8, + /// Hardware address length + hlen: u8, + // Client sets to zero + hops: u8, + /// Transaction ID + xid: u32, + /// Seconds elapsed since client began address acquisition + secs: u16, + /// Flags + flags: u16, + /// Client IP address + ciaddr: u32, + /// 'Your' (client) IP address + yiaddr: u32, + /// IP address of next server to use in bootstrap + siaddr: u32, + /// Relay agent IP address + giaddr: u32, + /// Client hardware address + chaddr: [u8; 16], + /// Optional server host name + sname: [u8; 64], + /// Boot file name + file: [u8; 128], + /// Magic cookie + magic: u32, + /// Options + options: [u8; DHCP_OPTIONS_SIZE], +} + +impl DhcpPacket { + fn zeroed() -> Self { + // SAFETY: DhcpPacket is plain-old-data with no padding invariants. + unsafe { mem::zeroed() } + } + + fn as_bytes(&self) -> &[u8] { + // SAFETY: packed repr, no padding; reading as bytes is valid. + unsafe { slice::from_raw_parts(self as *const _ as *const u8, mem::size_of::()) } + } +} + +struct DhcpOptionsWriter<'a> { + buf: &'a mut [u8], + pos: usize, +} + +impl<'a> DhcpOptionsWriter<'a> { + fn new(buf: &'a mut [u8]) -> Self { + Self { buf, pos: 0 } + } + + fn push(&mut self, code: u8, data: &[u8]) { + self.buf[self.pos] = code; + self.buf[self.pos + 1] = data.len() as u8; + self.buf[self.pos + 2..self.pos + 2 + data.len()].copy_from_slice(data); + self.pos += 2 + data.len(); + } + + fn finish(self) { + self.buf[self.pos] = DHCP_OPTIONS_END; + } +} + +/// Iterator over DHCP options in a TLV (type-length-value) encoded buffer (RFC 2132). +/// +/// Each call to `next()` yields `(option_code, data)`. Padding bytes (0x00) +/// are skipped automatically. Iteration stops at the end marker (0xFF) or +/// when the buffer is too short to contain a well-formed option. +struct DhcpOptions<'a> { + remaining: &'a [u8], +} + +impl<'a> DhcpOptions<'a> { + fn new(buf: &'a [u8]) -> Self { + Self { remaining: buf } + } +} + +impl<'a> Iterator for DhcpOptions<'a> { + type Item = (u8, &'a [u8]); + + fn next(&mut self) -> Option { + loop { + let (&opt, rest) = self.remaining.split_first()?; + if opt == DHCP_OPTIONS_END { + self.remaining = &[]; + return None; + } + self.remaining = rest; + if opt == 0 { + continue; + } + let (&len, rest) = self.remaining.split_first()?; + let (data, rest) = rest.split_at_checked(len as usize)?; + self.remaining = rest; + return Some((opt, data)); + } + } +} + +fn struct_as_bytes(v: &T) -> &[u8] { + unsafe { slice::from_raw_parts(v as *const T as *const u8, mem::size_of::()) } +} + +/// Helper function to send netlink message +fn nl_send(sock: libc::c_int, buf: &[u8]) -> anyhow::Result<()> { + // Use mem::zeroed() so the opaque nl_pad field is correctly initialised. + let mut sa: libc::sockaddr_nl = unsafe { mem::zeroed() }; + sa.nl_family = libc::AF_NETLINK as libc::sa_family_t; + + let iov = libc::iovec { + iov_base: buf.as_ptr() as *mut _, + iov_len: buf.len(), + }; + // Use zeroed() rather than a struct literal: musl's msghdr has private + // padding fields (__pad1, __pad2) that cannot be named in a literal. + let mut msg: libc::msghdr = unsafe { mem::zeroed() }; + msg.msg_name = &sa as *const _ as *mut _; + msg.msg_namelen = mem::size_of_val(&sa) as u32; + msg.msg_iov = &iov as *const _ as *mut _; + msg.msg_iovlen = 1; + let ret = unsafe { libc::sendmsg(sock, &msg, 0) }; + if ret < 0 { + bail!("nl_send: {}", IoError::last_os_error()); + } + Ok(()) +} + +/// Helper function to receive netlink response +fn nl_recv(sock: libc::c_int, buf: &mut [u8]) -> anyhow::Result { + let mut sa: libc::sockaddr_nl = unsafe { mem::zeroed() }; + let iov = libc::iovec { + iov_base: buf.as_mut_ptr() as *mut _, + iov_len: buf.len(), + }; + let mut msg: libc::msghdr = unsafe { mem::zeroed() }; + msg.msg_name = &mut sa as *mut _ as *mut _; + msg.msg_namelen = mem::size_of_val(&sa) as u32; + msg.msg_iov = &iov as *const _ as *mut _; + msg.msg_iovlen = 1; + let ret = unsafe { libc::recvmsg(sock, &mut msg, 0) }; + if ret < 0 { + bail!("nl_recv: {}", IoError::last_os_error()); + } + Ok(ret as usize) +} + +/// Add routing attribute to netlink message +fn add_rtattr(buf: &mut [u8], msg_len: &mut usize, rta_type: u16, data: &[u8]) { + let rta_len = (4 + data.len()) as u16; + let aligned_start = (*msg_len + 3) & !3; + let end = aligned_start + ((rta_len as usize + 3) & !3); + assert!(end <= buf.len(), "netlink buffer too small"); + + buf[aligned_start..aligned_start + 2].copy_from_slice(&rta_len.to_ne_bytes()); + buf[aligned_start + 2..aligned_start + 4].copy_from_slice(&rta_type.to_ne_bytes()); + buf[aligned_start + 4..aligned_start + 4 + data.len()].copy_from_slice(data); + buf[aligned_start + 4 + data.len()..end].fill(0); + + *msg_len = end; + buf[0..4].copy_from_slice(&(*msg_len as u32).to_ne_bytes()); +} + +fn nl_check_ack(buf: &[u8], recv_len: usize, op: &str) -> anyhow::Result<()> { + let min = mem::size_of::() + mem::size_of::(); + if recv_len < min { + bail!("{op}: netlink response too short"); + } + let nlh = unsafe { &*(buf.as_ptr() as *const libc::nlmsghdr) }; + if nlh.nlmsg_type != libc::NLMSG_ERROR as u16 { + bail!( + "{op}: expected NLMSG_ERROR ACK, got type {}", + nlh.nlmsg_type + ); + } + let err_offset = mem::size_of::(); + let err = i32::from_ne_bytes(buf[err_offset..err_offset + 4].try_into().unwrap()); + if err != 0 { + bail!("{op}: netlink error {err}"); + } + Ok(()) +} + +fn nl_hdr(buf: &mut [u8], msg_len: usize, nlmsg_type: u16, flags: u16) { + let nlh = libc::nlmsghdr { + nlmsg_len: msg_len as u32, + nlmsg_type, + nlmsg_flags: flags, + nlmsg_seq: 1, + nlmsg_pid: unistd::getpid().as_raw() as u32, + }; + buf[..mem::size_of_val(&nlh)].copy_from_slice(struct_as_bytes(&nlh)); +} + +fn set_mtu(nl_sock: libc::c_int, iface_index: i32, mtu: u32) -> anyhow::Result<()> { + let mut buf = [0u8; 4096]; + let base = mem::size_of::() + mem::size_of::(); + let mut msg_len = base; + + nl_hdr( + &mut buf, + base, + libc::RTM_NEWLINK, + (libc::NLM_F_REQUEST | libc::NLM_F_ACK) as u16, + ); + + let ifi = IfInfoMsg { + ifi_family: libc::AF_UNSPEC as u8, + _pad: 0, + ifi_type: libc::ARPHRD_ETHER, + ifi_index: iface_index, + ifi_flags: 0, + ifi_change: 0, + }; + let ifi_off = mem::size_of::(); + buf[ifi_off..ifi_off + mem::size_of_val(&ifi)].copy_from_slice(struct_as_bytes(&ifi)); + + add_rtattr(&mut buf, &mut msg_len, libc::IFLA_MTU, &mtu.to_ne_bytes()); + + nl_send(nl_sock, &buf[..msg_len])?; + // Receive ACK + let recv_len = nl_recv(nl_sock, &mut buf)?; + nl_check_ack(&buf, recv_len, "set_mtu") +} + +/// Add or delete IPv4 address +fn mod_addr4( + nl_sock: libc::c_int, + iface_index: i32, + cmd: u16, + addr: u32, + prefix_len: u8, +) -> anyhow::Result<()> { + let mut buf = [0u8; 4096]; + let base = mem::size_of::() + mem::size_of::(); + let mut msg_len = base; + + nl_hdr( + &mut buf, + base, + cmd, + (libc::NLM_F_REQUEST | libc::NLM_F_CREATE | libc::NLM_F_ACK) as u16, + ); + + let ifa = IfAddrMsg { + ifa_family: libc::AF_INET as u8, + ifa_prefixlen: prefix_len, + ifa_flags: 0, + ifa_scope: libc::RT_SCOPE_UNIVERSE, + ifa_index: iface_index as u32, + }; + let ifa_off = mem::size_of::(); + buf[ifa_off..ifa_off + mem::size_of_val(&ifa)].copy_from_slice(struct_as_bytes(&ifa)); + + let addr_bytes = addr.to_ne_bytes(); + add_rtattr(&mut buf, &mut msg_len, libc::IFA_LOCAL, &addr_bytes); + add_rtattr(&mut buf, &mut msg_len, libc::IFA_ADDRESS, &addr_bytes); + + nl_send(nl_sock, &buf[..msg_len])?; + // Receive ACK + let recv_len = nl_recv(nl_sock, &mut buf)?; + nl_check_ack(&buf, recv_len, "mod_addr4") +} + +/// Add or delete IPv4 route +fn mod_route4( + nl_sock: libc::c_int, + iface_index: i32, + cmd: u16, + gateway: u32, +) -> anyhow::Result<()> { + let mut buf = [0u8; 4096]; + let base = mem::size_of::() + mem::size_of::(); + let mut msg_len = base; + + nl_hdr( + &mut buf, + base, + cmd, + (libc::NLM_F_REQUEST | libc::NLM_F_CREATE | libc::NLM_F_ACK) as u16, + ); + + let rtm = RtMsg { + rtm_family: libc::AF_INET as u8, + rtm_dst_len: 0, + rtm_src_len: 0, + rtm_tos: 0, + rtm_table: libc::RT_TABLE_MAIN, + rtm_protocol: libc::RTPROT_BOOT, + rtm_scope: libc::RT_SCOPE_UNIVERSE, + rtm_type: libc::RTN_UNICAST, + rtm_flags: 0, + }; + let rtm_off = mem::size_of::(); + buf[rtm_off..rtm_off + mem::size_of_val(&rtm)].copy_from_slice(struct_as_bytes(&rtm)); + + add_rtattr( + &mut buf, + &mut msg_len, + libc::RTA_OIF, + &(iface_index as u32).to_ne_bytes(), + ); + add_rtattr(&mut buf, &mut msg_len, libc::RTA_DST, &0u32.to_ne_bytes()); + add_rtattr( + &mut buf, + &mut msg_len, + libc::RTA_GATEWAY, + &gateway.to_ne_bytes(), + ); + + nl_send(nl_sock, &buf[..msg_len])?; + // Receive ACK + let recv_len = nl_recv(nl_sock, &mut buf)?; + nl_check_ack(&buf, recv_len, "mod_route4") +} + +/// Return the DHCP message type (option 53) from a response, or 0 +fn dhcp_msg_type(response: &[u8]) -> u8 { + DhcpOptions::new(response.get(DHCP_OPTIONS_OFFSET..).unwrap_or(&[])) + .find(|&(code, _)| code == 53) + .and_then(|(_, data)| data.first().copied()) + .unwrap_or(0) +} + +/// Parse a DHCP ACK and configure the interface +fn handle_dhcp_ack(nl_sock: libc::c_int, iface_index: i32, response: &[u8]) -> anyhow::Result<()> { + // Need at least 240 bytes (DHCP header + magic cookie) + 1 for options + if response.len() < DHCP_OPTIONS_OFFSET + 1 { + bail!("DHCPACK too short ({} bytes)", response.len()); + } + + // Parse DHCP response. yiaddr is at offset 16-19 in network byte order + let addr = u32::from_ne_bytes(response[16..20].try_into().unwrap()); + if addr == libc::INADDR_ANY { + bail!("DHCPACK: yiaddr is 0.0.0.0"); + } + + let mut netmask: u32 = 0; + let mut router: u32 = 0; + // Clamp MTU to passt's limit + let mut mtu: u16 = 65520; + let mut resolv_conf = String::new(); + + // Parse DHCP options (start at offset 240 after magic cookie) + for (opt, data) in DhcpOptions::new(response.get(DHCP_OPTIONS_OFFSET..).unwrap_or(&[])) { + match opt { + // Subnet mask + 1 if data.len() >= 4 => { + netmask = u32::from_ne_bytes(data[..4].try_into().unwrap()); + } + // Router + 3 if data.len() >= 4 => { + router = u32::from_ne_bytes(data[..4].try_into().unwrap()); + } + // Domain Name Server + 6 => { + for chunk in data.chunks_exact(4) { + let ip = Ipv4Addr::new(chunk[0], chunk[1], chunk[2], chunk[3]); + let _ = writeln!(resolv_conf, "nameserver {ip}"); + } + } + // Interface MTU + 26 if data.len() >= 2 => { + // We don't know yet if IPv6 is available: don't go below 1280 B + mtu = u16::from_be_bytes(data[..2].try_into().unwrap()).clamp(1280, 65520); + } + _ => {} + } + } + + if let Err(e) = std::fs::write("/etc/resolv.conf", &resolv_conf) { + eprintln!("Warning: couldn't write /etc/resolv.conf: {e}"); + } + + // Calculate the prefix length from netmask + let prefix_len = u32::from_be(netmask).leading_ones() as u8; + + mod_addr4(nl_sock, iface_index, libc::RTM_NEWADDR, addr, prefix_len) + .context("add address from DHCP")?; + mod_route4(nl_sock, iface_index, libc::RTM_NEWROUTE, router) + .context("add default route from DHCP")?; + let _ = set_mtu(nl_sock, iface_index, mtu as u32); + + Ok(()) +} + +/// Perform DHCP discover and configuration for a network interface +/// +/// This function: +/// 1. Binds a UDP socket to the interface using SO_BINDTODEVICE +/// 2. Sends a DHCP DISCOVER message with Rapid Commit option +/// 3. Waits up to 100ms for a response: +/// - If DHCPACK (Rapic Commit): applies configuration directly +/// - If DHCPOFFER: sends DHCPREQUEST and waits for DHCPACK +/// - If no response: returns success (VM may be IPv6-only) +/// 4. Parses the ACK and configures: +/// - IPv4 address with appropriate prefix length +/// - Default gateway route +/// - DNS servers (overwriting /etc/resolv.conf) +/// - Interface MTU +pub fn do_dhcp(iface: &str) -> anyhow::Result<()> { + let iface_index = + if_::if_nametoindex(iface).with_context(|| format!("if_nametoindex({iface})"))?; + + let raw = Errno::result(unsafe { + libc::socket(libc::AF_NETLINK, libc::SOCK_RAW, libc::NETLINK_ROUTE) + }) + .context("socket(AF_NETLINK)")?; + let nl_sock = unsafe { OwnedFd::from_raw_fd(raw) }; + + let mut nl_sa: libc::sockaddr_nl = unsafe { mem::zeroed() }; + nl_sa.nl_family = libc::AF_NETLINK as libc::sa_family_t; + nl_sa.nl_pid = unistd::getpid().as_raw() as u32; + Errno::result(unsafe { + libc::bind( + nl_sock.as_raw_fd(), + &nl_sa as *const _ as *const libc::sockaddr, + mem::size_of_val(&nl_sa) as u32, + ) + }) + .context("bind(netlink)")?; + + // Send Request (DHCPDISCOVER) + let sock = socket::socket( + AddressFamily::Inet, + SockType::Datagram, + SockFlag::empty(), + Some(SockProtocol::Udp), + ) + .context("socket(AF_INET)")?; + + // Allow broadcast + socket::setsockopt(&sock, sockopt::Broadcast, &true).context("setsockopt(SO_BROADCAST)")?; + socket::setsockopt(&sock, sockopt::BindToDevice, &OsString::from(iface)) + .context("setsockopt(SO_BINDTODEVICE)")?; + + // Bind to port 68 (DHCP client) + let bind_addr = SockaddrIn::from(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, DHCP_CLIENT_PORT)); + socket::bind(sock.as_raw_fd(), &bind_addr).context("bind(UDP DHCP client)")?; + + let mut pkt = DhcpPacket::zeroed(); + pkt.op = BOOTREQUEST; + pkt.htype = HTYPE_ETHERNET; + pkt.hlen = HLEN_ETHERNET; + pkt.xid = (unistd::getpid().as_raw() as u32).to_be(); + pkt.flags = DHCP_FLAG_BROADCAST.to_be(); + pkt.magic = DHCP_MAGIC_COOKIE.to_be(); + + // Populate chaddr with the interface's MAC address + let mut mac_ifr: libc::ifreq = unsafe { mem::zeroed() }; + let name_bytes = iface.as_bytes(); + unsafe { + std::ptr::copy_nonoverlapping( + name_bytes.as_ptr() as *const libc::c_char, + mac_ifr.ifr_name.as_mut_ptr(), + name_bytes.len().min(libc::IFNAMSIZ - 1), + ); + } + if unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCGIFHWADDR as _, &mut mac_ifr) } < 0 { + bail!("ioctl(SIOCGIFHWADDR): {}", IoError::last_os_error()); + } + let sa_data = unsafe { mac_ifr.ifr_ifru.ifru_hwaddr.sa_data }; + for (dst, src) in pkt.chaddr.iter_mut().zip(sa_data.iter().take(6)) { + // We need to allow the unnecessary cast, because this will cause clippy to fail on aarch64 + // without it + #[allow(clippy::unnecessary_cast)] + { + *dst = *src as u8; + } + } + + // Build DHCP options + + let mut opts = DhcpOptionsWriter::new(&mut pkt.options); + opts.push(53, &[1]); // Discover + opts.push(80, &[]); // Rapid Commit + opts.finish(); + + let dest = SockaddrIn::from(SocketAddrV4::new(Ipv4Addr::BROADCAST, DHCP_SERVER_PORT)); + + // Keep IPv6-only fast: set receive timeout to 100ms + socket::setsockopt( + &sock, + sockopt::ReceiveTimeout, + &TimeVal::microseconds(100_000), + ) + .context("setsockopt(SO_RCVTIMEO)")?; + + // Send DHCP DISCOVER + let pkt_bytes = pkt.as_bytes(); + socket::sendto(sock.as_raw_fd(), pkt_bytes, &dest, MsgFlags::empty()) + .context("sendto(DISCOVER)")?; + + // Get response: DHCPACK (Rapid Commit) or DHCPOFFER + let mut response = [0u8; DHCP_BUFFER_SIZE]; + let (recv_len, from) = match socket::recvfrom::(sock.as_raw_fd(), &mut response) { + Ok(r) => r, + Err(Errno::EAGAIN) => return Ok(()), // timeout — no DHCP server + Err(e) => bail!("recvfrom: {e}"), + }; + + let msg_type = dhcp_msg_type(&response[..recv_len]); + + match msg_type { + // Rapid Commit - server sent ACK directly + DHCP_MSG_ACK => { + handle_dhcp_ack( + nl_sock.as_raw_fd(), + iface_index as i32, + &response[..recv_len], + )?; + } + // DHCPOFFER - complete the 4-way handshake by sending DHCPREQUEST and waiting for DHCPACK. + // Servers without Rapid Commit (e.g. gvproxy) require this. + DHCP_MSG_OFFER => { + let offered_addr = u32::from_ne_bytes(response[16..20].try_into().unwrap()); + let server_addr = from.map(|a| a.ip().octets()).unwrap_or([0; 4]); + + pkt.options = [0; DHCP_OPTIONS_SIZE]; + let mut opts = DhcpOptionsWriter::new(&mut pkt.options); + opts.push(53, &[3]); // Request + opts.push(50, &offered_addr.to_ne_bytes()); // Requested IP + opts.push(54, &server_addr); // Server ID + opts.finish(); + + let pkt_bytes = pkt.as_bytes(); + socket::sendto(sock.as_raw_fd(), pkt_bytes, &dest, MsgFlags::empty()) + .context("sendto(REQUEST)")?; + + let (recv_len2, _) = socket::recvfrom::(sock.as_raw_fd(), &mut response) + .context("no DHCPACK received")?; + let ack_type = dhcp_msg_type(&response[..recv_len2]); + if ack_type != DHCP_MSG_ACK { + bail!("expected DHCPACK, got type {ack_type}"); + } + handle_dhcp_ack( + nl_sock.as_raw_fd(), + iface_index as i32, + &response[..recv_len2], + )?; + } + _ => bail!("unexpected DHCP message type {msg_type}"), + } + + Ok(()) +} diff --git a/init/src/main.rs b/init/src/main.rs index 45fb2ec26..c0f28e5f2 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,3 +1,4 @@ +mod dhcp; mod fs; fn main() { From dcd6c44aef895f755a5b1b7ba8ccc25f18832c70 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:35:18 -0400 Subject: [PATCH 05/24] init: implement JSON config parsing Add init/src/config.rs, replacing the hand-rolled jsmn-based parser with serde_json. Parses /.krun_config.json (or KRUN_CONFIG env var) and returns a Config struct with: - argv: Entrypoint ++ (args | Cmd), or None if absent - workdir: WorkingDir or Cwd - tmpfs: first tmpfs mount destination not already mounted Environment variables from the Env array are applied during parsing, with HOME and TERM always overwritten, all others set only if unset. A missing or unparseable config file is silently ignored. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/config.rs | 111 +++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 1 + 2 files changed, 112 insertions(+) create mode 100644 init/src/config.rs diff --git a/init/src/config.rs b/init/src/config.rs new file mode 100644 index 000000000..0ede53b4b --- /dev/null +++ b/init/src/config.rs @@ -0,0 +1,111 @@ +#[cfg(target_os = "freebsd")] +use crate::freebsd::ISO_CONFIG_PATH; +use std::env; +use std::fs; +#[cfg(target_os = "freebsd")] +use std::path::Path; + +use anyhow::{Context, Result}; +use serde::Deserialize; + +const CONFIG_FILE_PATH: &str = "/.krun_config.json"; + +// The krun OCI runtime passes a full OCI runtime-spec config.json as the +// config file. The fields we care about live inside "process". +#[derive(Deserialize, Default)] +struct ProcessConfig { + args: Option>, + env: Option>, + cwd: Option, +} + +#[cfg(target_os = "linux")] +#[derive(Deserialize, Default)] +struct Mount { + #[serde(rename = "destination")] + destination: Option, + #[serde(rename = "type")] + mount_type: Option, + #[serde(rename = "source")] + source: Option, +} + +#[derive(Deserialize, Default)] +struct RawConfig { + process: Option, + // Flat format: "args"/"env"/"cwd" at the top level (used by simple configs and tests). + // Only consulted when "process" is absent. + args: Option>, + env: Option>, + cwd: Option, + #[cfg(target_os = "linux")] + mounts: Option>, +} + +#[derive(Default)] +pub struct Config { + pub argv: Option>, + pub workdir: Option, + #[cfg(target_os = "linux")] + pub tmpfs: Option, +} + +pub fn load(#[cfg(target_os = "linux")] is_mount_point: impl Fn(&str) -> bool) -> Config { + let path = env::var("KRUN_CONFIG").unwrap_or_else(|_| { + #[cfg(target_os = "freebsd")] + if Path::new(ISO_CONFIG_PATH).exists() { + return ISO_CONFIG_PATH.to_string(); + } + CONFIG_FILE_PATH.to_string() + }); + + let Ok(raw) = parse_file(&path) else { + return Config::default(); + }; + + let process = raw.process.unwrap_or(ProcessConfig { + args: raw.args, + env: raw.env, + cwd: raw.cwd, + }); + + // Apply environment variables from the process config. + for entry in process.env.unwrap_or_default() { + let Some((key, val)) = entry.split_once('=') else { + continue; + }; + let overwrite = matches!(key, "HOME" | "TERM"); + if env::var(key).is_err() || overwrite { + // SAFETY: single-threaded at this point. + unsafe { env::set_var(key, val) }; + } + } + + let argv = process.args.filter(|v| !v.is_empty()); + let workdir = process.cwd; + + // Find the first tmpfs mount whose destination is not already mounted. + #[cfg(target_os = "linux")] + let tmpfs = raw.mounts.unwrap_or_default().into_iter().find_map(|m| { + let dest = m.destination?; + let ty = m.mount_type.as_deref().unwrap_or(""); + let src = m.source.as_deref().unwrap_or(""); + if ty == "tmpfs" && src == "tmpfs" && !is_mount_point(&dest) { + Some(dest) + } else { + None + } + }); + + Config { + argv, + workdir, + #[cfg(target_os = "linux")] + tmpfs, + } +} + +fn parse_file(path: &str) -> Result { + let data = fs::read(path).with_context(|| format!("read {path}"))?; + serde_json::from_slice(&data).with_context(|| format!("parse {path}")) +} diff --git a/init/src/main.rs b/init/src/main.rs index c0f28e5f2..32565f21f 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,3 +1,4 @@ +mod config; mod dhcp; mod fs; From e1e502a27269dbc74b49fba7a5b035634d4613d9 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:38:19 -0400 Subject: [PATCH 06/24] init: implement network interface setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add setup_network() and setup_dhcp() to env.rs. setup_network() brings up lo unconditionally. setup_dhcp() checks that the interface exists before calling do_dhcp(), and logs a warning on failure rather than aborting (DHCP failure is non-fatal — the VM may be IPv6-only or have no network). Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/env.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 1 + 2 files changed, 67 insertions(+) create mode 100644 init/src/env.rs diff --git a/init/src/env.rs b/init/src/env.rs new file mode 100644 index 000000000..440a11669 --- /dev/null +++ b/init/src/env.rs @@ -0,0 +1,66 @@ +#[cfg(target_os = "linux")] +use std::ffi::CString; +#[cfg(target_os = "linux")] +use std::mem; +#[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; +#[cfg(target_os = "linux")] +use std::ptr; + +#[cfg(target_os = "linux")] +use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; + +#[cfg(target_os = "linux")] +pub fn setup_network(iface: &str) { + let Ok(sock) = socket::socket( + AddressFamily::Inet, + SockType::Datagram, + SockFlag::empty(), + None, + ) else { + return; + }; + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + let lo = c"lo"; + unsafe { + ptr::copy_nonoverlapping( + lo.as_ptr(), + ifr.ifr_name.as_mut_ptr(), + lo.to_bytes_with_nul().len(), + ); + ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short; + libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFFLAGS as _, &ifr); + } + + setup_dhcp(iface, sock.as_raw_fd()); +} + +#[cfg(not(target_os = "linux"))] +pub fn setup_network() {} + +#[cfg(target_os = "linux")] +fn setup_dhcp(iface: &str, sock: i32) { + if std::env::var("KRUN_DHCP").as_deref() != Ok("1") { + return; + } + + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + let name = CString::new(iface).unwrap(); + unsafe { + ptr::copy_nonoverlapping( + name.as_ptr(), + ifr.ifr_name.as_mut_ptr(), + name.as_bytes_with_nul().len().min(libc::IFNAMSIZ), + ); + } + let exists = unsafe { libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) } == 0; + if exists { + unsafe { + ifr.ifr_ifru.ifru_flags |= libc::IFF_UP as libc::c_short; + libc::ioctl(sock, libc::SIOCSIFFLAGS as _, &ifr); + } + if let Err(e) = crate::dhcp::do_dhcp(iface) { + eprintln!("Warning: DHCP configuration for {iface} failed: {e}"); + } + } +} diff --git a/init/src/main.rs b/init/src/main.rs index 32565f21f..5b8124850 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,5 +1,6 @@ mod config; mod dhcp; +mod env; mod fs; fn main() { From 5d75b77bfd8f87a54447097330e4fe984043ae95 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:39:41 -0400 Subject: [PATCH 07/24] init: implement environment and resource setup Extend env.rs with: - apply_hostname(): sets hostname from HOSTNAME env var, defaulting to "localhost" - apply_env(): maps KRUN_HOME -> HOME and KRUN_TERM -> TERM - apply_rlimits(): parses the KRUN_RLIMITS comma-separated list of id,cur,max triples and applies each via setrlimit(2) Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/env.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/init/src/env.rs b/init/src/env.rs index 440a11669..e224ab9e6 100644 --- a/init/src/env.rs +++ b/init/src/env.rs @@ -1,3 +1,4 @@ +use std::env; #[cfg(target_os = "linux")] use std::ffi::CString; #[cfg(target_os = "linux")] @@ -64,3 +65,43 @@ fn setup_dhcp(iface: &str, sock: i32) { } } } + +pub fn apply_hostname() { + let hostname = env::var("HOSTNAME").unwrap_or_else(|_| "localhost".into()); + let _ = nix::unistd::sethostname(&hostname); +} + +pub fn apply_env() { + if let Ok(home) = env::var("KRUN_HOME") { + unsafe { env::set_var("HOME", home) }; + } + if let Ok(term) = env::var("KRUN_TERM") { + unsafe { env::set_var("TERM", term) }; + } +} + +pub fn apply_rlimits() { + let Ok(rlimits) = env::var("KRUN_RLIMITS") else { + return; + }; + for item in rlimits.split(',') { + let Some((id_s, rest)) = item.split_once('=') else { + continue; + }; + let Some((cur_s, max_s)) = rest.split_once(':') else { + continue; + }; + let (Ok(id), Ok(cur), Ok(max)) = ( + id_s.parse::(), + cur_s.parse::(), + max_s.parse::(), + ) else { + continue; + }; + let rlim = libc::rlimit { + rlim_cur: cur, + rlim_max: max, + }; + unsafe { libc::setrlimit(id as _, &rlim) }; + } +} From e4074ddef8268abd85ceddf63a3208f3fb044ea3 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:40:56 -0400 Subject: [PATCH 08/24] init: implement I/O redirects and workload launch Add exec.rs with: - setup_redirects(): walks /sys/class/virtio-ports and dup2s krun-stdin/stdout/stderr onto the corresponding file descriptors - set_exit_code(): reports the workload exit code to the host via KRUN_EXIT_CODE_IOCTL, only when the root fs is virtiofs - run_workload(): forks so PID 1 can reap children; the child calls exec_workload() which sets up redirects and execvp's the argv. Parent waits for the child, reports exit code, syncs, and reboots. KRUN_INIT_PID1=1 skips the fork and exec_workload directly as PID 1. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/exec.rs | 128 +++++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 1 + 2 files changed, 129 insertions(+) create mode 100644 init/src/exec.rs diff --git a/init/src/exec.rs b/init/src/exec.rs new file mode 100644 index 000000000..b9314a2df --- /dev/null +++ b/init/src/exec.rs @@ -0,0 +1,128 @@ +use std::env; +use std::ffi::CString; +#[cfg(target_os = "linux")] +use std::fs; +#[cfg(target_os = "linux")] +use std::mem; +#[cfg(target_os = "linux")] +use std::os::fd::AsRawFd; +#[cfg(target_os = "linux")] +use std::path::Path; +use std::process; + +#[cfg(target_os = "linux")] +use nix::fcntl::{self, OFlag}; +#[cfg(target_os = "linux")] +use nix::sys::reboot::{self, RebootMode}; +#[cfg(target_os = "linux")] +use nix::sys::stat::Mode; +#[cfg(target_os = "linux")] +use nix::sys::statfs::{self, FsType}; +use nix::sys::wait::{self, WaitStatus}; +use nix::unistd::{self, ForkResult}; + +#[cfg(target_os = "linux")] +const KRUN_EXIT_CODE_IOCTL: libc::c_ulong = 0x7602; +#[cfg(target_os = "linux")] +// 0x6573_5546 fits in i32, so the cast to FsType's inner c_long is safe on +// both 32-bit (c_long = i32) and 64-bit (c_long = i64) targets. +const VIRTIOFS_MAGIC: libc::c_long = 0x6573_5546; + +#[cfg(target_os = "linux")] +pub fn setup_redirects() { + let Ok(ports_dir) = fs::read_dir("/sys/class/virtio-ports") else { + return; + }; + for entry in ports_dir.flatten() { + let name_path = entry.path().join("name"); + let Ok(port_name) = fs::read_to_string(&name_path) else { + continue; + }; + let (fd, oflag) = match port_name.trim_end_matches('\n') { + "krun-stdin" => (libc::STDIN_FILENO, OFlag::O_RDONLY), + "krun-stdout" => (libc::STDOUT_FILENO, OFlag::O_WRONLY), + "krun-stderr" => (libc::STDERR_FILENO, OFlag::O_WRONLY), + _ => continue, + }; + let dev_path = format!("/dev/{}", entry.file_name().to_string_lossy()); + let Ok(new_fd) = fcntl::open(Path::new(&dev_path), oflag, Mode::empty()) else { + continue; + }; + if new_fd.as_raw_fd() == fd { + // Device opened directly onto the target fd (happens when + // the target was already closed); prevent OwnedFd from closing it. + mem::forget(new_fd); + continue; + } + let _ = match fd { + libc::STDIN_FILENO => unistd::dup2_stdin(&new_fd), + libc::STDOUT_FILENO => unistd::dup2_stdout(&new_fd), + _ => unistd::dup2_stderr(&new_fd), + }; + } +} + +#[cfg(target_os = "linux")] +pub fn set_exit_code(code: i32) { + let Ok(fs) = statfs::statfs(Path::new("/")) else { + return; + }; + if fs.filesystem_type() != FsType(VIRTIOFS_MAGIC as _) { + return; + } + if let Ok(fd) = fcntl::open(Path::new("/"), OFlag::O_RDONLY, Mode::empty()) { + unsafe { libc::ioctl(fd.as_raw_fd(), KRUN_EXIT_CODE_IOCTL as _, code) }; + } +} + +#[cfg(not(target_os = "linux"))] +pub fn set_exit_code(_code: i32) {} + +pub fn run_workload(argv: &[String]) -> ! { + if env::var("KRUN_INIT_PID1") == Ok("1".to_owned()) { + exec_workload(argv); + } + + match unsafe { unistd::fork() } { + Err(_) => { + set_exit_code(125); + process::exit(125); + } + Ok(ForkResult::Child) => exec_workload(argv), + Ok(ForkResult::Parent { child }) => { + let code = loop { + match wait::waitpid(None, None) { + Ok(WaitStatus::Exited(pid, c)) if pid == child => break c, + Ok(WaitStatus::Signaled(pid, sig, _)) if pid == child => { + break sig as i32 + 128; + } + _ => continue, + } + }; + set_exit_code(code); + unistd::sync(); + #[cfg(target_os = "linux")] + let _ = reboot::reboot(RebootMode::RB_AUTOBOOT); + process::exit(code) + } + } +} + +fn exec_workload(argv: &[String]) -> ! { + #[cfg(target_os = "linux")] + setup_redirects(); + + let c_argv: Vec = argv + .iter() + .map(|s| CString::new(s.as_str()).unwrap()) + .collect(); + + let Err(e) = unistd::execvp(&c_argv[0], &c_argv); + let code = if e == nix::errno::Errno::ENOENT { + 127 + } else { + 126 + }; + eprintln!("Couldn't execute '{}': {e}", argv[0]); + process::exit(code); +} diff --git a/init/src/main.rs b/init/src/main.rs index 5b8124850..e6e10ee66 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,6 +1,7 @@ mod config; mod dhcp; mod env; +mod exec; mod fs; fn main() { From 01da785c70c18c96bd6f3d1518ad057c8a5640aa Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:42:51 -0400 Subject: [PATCH 09/24] init: wire main.rs with full boot sequence Connect all modules in main() in order: 1. mount_block_root() [amd-sev | tdx] 2. mount_filesystems() 3. mount_block_root_device() [KRUN_BLOCK_ROOT_DEVICE] 4. mount_shared_root() 5. setsid + TIOCSCTTY 6. setup_network() 7. config::load() 8. mount_tmpfs() [config tmpfs mount] 9. apply_env / apply_hostname / apply_rlimits 10. chdir to workdir 11. run_workload(argv) Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/main.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/init/src/main.rs b/init/src/main.rs index e6e10ee66..18e2cf8ee 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -1,10 +1,66 @@ mod config; +#[cfg(target_os = "linux")] mod dhcp; mod env; mod exec; +#[cfg(target_os = "linux")] mod fs; -fn main() { +fn main() -> anyhow::Result<()> { #[cfg(any(feature = "amd-sev", feature = "tdx"))] - fs::mount_tee_block_device().expect("mount block root failed"); + fs::mount_tee_block_device()?; + + #[cfg(target_os = "linux")] + { + fs::mount_filesystems()?; + fs::mount_block_root_device()?; + fs::mount_shared_root()?; + } + + let _ = nix::unistd::setsid(); + unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 1i32) }; + + env::setup_network( + #[cfg(target_os = "linux")] + "eth0", + ); + + #[cfg(target_os = "linux")] + let cfg = config::load(fs::is_mount_point); + #[cfg(not(target_os = "linux"))] + let cfg = config::load(); + + #[cfg(target_os = "linux")] + if let Some(ref path) = cfg.tmpfs { + fs::mount_tmpfs(path)?; + } + + env::apply_env(); + env::apply_hostname(); + env::apply_rlimits(); + + if let Some(workdir) = std::env::var("KRUN_WORKDIR").ok().or(cfg.workdir) { + let _ = nix::unistd::chdir(workdir.as_str()); + } + + // The kernel places everything after `--` in the cmdline as this + // process's argv[1..]. The C init built exec_argv by replacing argv[0] + // with KRUN_INIT (or /bin/sh) and keeping argv[1..] in every branch. + let proc_args: Vec = std::env::args().collect(); + + let argv: Vec = if let Ok(init) = std::env::var("KRUN_INIT") { + // KRUN_INIT holds the binary; kernel cmdline args are the arguments. + let mut v = vec![init]; + v.extend_from_slice(&proc_args[1..]); + v + } else if let Some(v) = cfg.argv { + v + } else if proc_args.len() > 1 { + // No KRUN_INIT and no config: treat proc_args[1..] as the command. + proc_args.into_iter().skip(1).collect() + } else { + vec!["/bin/sh".to_string()] + }; + + exec::run_workload(&argv); } From 660403c8896327bb2b49c5990820b407f0c29997 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 16:37:44 -0400 Subject: [PATCH 10/24] init: add FreeBSD platform helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add init/src/freebsd.rs with: - kenv_get(): reads a variable from the FreeBSD kernel environment via kenv(2), which is the source of env vars for init before the process environment is set up - populate_env_from_kenv(): imports the known KRUN_* variables from kenv into std::env at startup so the rest of the code can use std::env::var uniformly on both platforms - open_console(): replicates login_tty(3) without linking libutil — revokes existing opens of /dev/console, opens it, creates a new session via setsid(2), sets the controlling terminal via TIOCSCTTY, and dup2s it onto stdio; falls back to /dev/null + /init.log - mount_config_iso() / unmount_config_iso(): mounts the KRUN_CONFIG ISO 9660 image at /mnt via nmount(2) so the JSON config file can be read, then unmounts it afterwards Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/freebsd.rs | 170 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 init/src/freebsd.rs diff --git a/init/src/freebsd.rs b/init/src/freebsd.rs new file mode 100644 index 000000000..e276b2e84 --- /dev/null +++ b/init/src/freebsd.rs @@ -0,0 +1,170 @@ +use std::ffi::CString; +use std::os::fd::AsRawFd; + +use nix::fcntl::{self, FcntlArg, OFlag}; +use nix::sys::stat::Mode; +use nix::unistd; + +unsafe extern "C" { + fn revoke(path: *const libc::c_char) -> libc::c_int; +} + +const KENV_MVALLEN: usize = 128; +const ISO_DEV: &str = "/dev/iso9660/KRUN_CONFIG"; +const ISO_MOUNT: &str = "/mnt"; +pub const ISO_CONFIG_PATH: &str = "/mnt/krun_config.json"; + +const KENV_VARS: &[&str] = &[ + "HOSTNAME", + "KRUN_CONFIG", + "KRUN_HOME", + "KRUN_INIT", + "KRUN_INIT_PID1", + "KRUN_RLIMITS", + "KRUN_TERM", + "KRUN_WORKDIR", +]; + +fn kenv_get(name: &str) -> Option { + let c_name = CString::new(name).ok()?; + let mut buf = vec![0u8; KENV_MVALLEN + 1]; + let ret = unsafe { + libc::kenv( + libc::KENV_GET, + c_name.as_ptr(), + buf.as_mut_ptr() as *mut libc::c_char, + (KENV_MVALLEN + 1) as i32, + ) + }; + if ret < 0 { + return None; + } + let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char) }; + Some(s.to_string_lossy().into_owned()) +} + +/// Populate the process environment from the FreeBSD kernel environment. +/// +/// On FreeBSD, init runs before the process environment is set up, so +/// variables like KRUN_INIT must be read from kenv(2) rather than getenv(3). +pub fn populate_env_from_kenv() { + for &var in KENV_VARS { + if let Some(val) = kenv_get(var) { + unsafe { std::env::set_var(var, val) }; + } + } +} + +/// Open /dev/console and make it the controlling terminal. +/// +/// Replicates login_tty(3) inline to avoid a libutil dependency: +/// revoke any existing opens, open the device, create a new session, +/// set the controlling terminal via TIOCSCTTY, then dup2 into stdio. +/// Falls back to /dev/null + /init.log if the console cannot be opened. +pub fn open_console() { + let console = c"/dev/console"; + unsafe { revoke(console.as_ptr()) }; + + let Ok(fd) = fcntl::open(console, OFlag::O_RDWR | OFlag::O_NONBLOCK, Mode::empty()) else { + fallback_console(); + return; + }; + + if let Ok(flags) = fcntl::fcntl(&fd, FcntlArg::F_GETFL) { + let _ = fcntl::fcntl( + &fd, + FcntlArg::F_SETFL(OFlag::from_bits_truncate(flags) & !OFlag::O_NONBLOCK), + ); + } + + let _ = unistd::setsid(); + unsafe { libc::ioctl(fd.as_raw_fd(), libc::TIOCSCTTY, 0) }; + let _ = unistd::dup2_stdin(&fd); + let _ = unistd::dup2_stdout(&fd); + let _ = unistd::dup2_stderr(&fd); + if fd.as_raw_fd() <= libc::STDERR_FILENO { + // fd is a stdio slot that dup2 just wrote to — don't close it. + std::mem::forget(fd); + } +} + +fn fallback_console() { + if let Ok(null_fd) = fcntl::open(c"/dev/null", OFlag::O_RDWR, Mode::empty()) { + if null_fd.as_raw_fd() != libc::STDIN_FILENO { + let _ = unistd::dup2_stdin(&null_fd); + } else { + std::mem::forget(null_fd); + } + } + + let log_fd = fcntl::open( + c"/init.log", + OFlag::O_WRONLY | OFlag::O_APPEND | OFlag::O_CREAT, + Mode::from_bits_truncate(0o644), + ); + + // Use the log file for stdout if available, otherwise reuse stdin. + let out_raw = if let Ok(fd) = &log_fd { + let _ = unistd::dup2_stdout(fd); + fd.as_raw_fd() + } else { + unsafe { libc::dup2(libc::STDIN_FILENO, libc::STDOUT_FILENO) }; + -1 + }; + // stderr always mirrors stdout. + unsafe { libc::dup2(libc::STDOUT_FILENO, libc::STDERR_FILENO) }; + // Close the log fd if it wasn't already consumed as stdout. + if out_raw >= 0 && out_raw != libc::STDOUT_FILENO { + drop(log_fd); + } else { + // Either no log fd, or it landed on STDOUT_FILENO — don't double-close. + std::mem::forget(log_fd); + } +} + +/// Mount the KRUN_CONFIG ISO image at /mnt via nmount(2). +/// Returns true on success. +pub fn mount_config_iso() -> bool { + let _ = std::fs::create_dir_all(ISO_MOUNT); + + let fstype_key = c"fstype"; + let fstype_val = c"cd9660"; + let fspath_key = c"fspath"; + let fspath_cstr = CString::new(ISO_MOUNT).unwrap(); + let from_key = c"from"; + let from_cstr = CString::new(ISO_DEV).unwrap(); + + let mut iov = [ + libc::iovec { + iov_base: fstype_key.as_ptr() as *mut _, + iov_len: fstype_key.to_bytes_with_nul().len(), + }, + libc::iovec { + iov_base: fstype_val.as_ptr() as *mut _, + iov_len: fstype_val.to_bytes_with_nul().len(), + }, + libc::iovec { + iov_base: fspath_key.as_ptr() as *mut _, + iov_len: fspath_key.to_bytes_with_nul().len(), + }, + libc::iovec { + iov_base: fspath_cstr.as_ptr() as *mut _, + iov_len: fspath_cstr.as_bytes_with_nul().len(), + }, + libc::iovec { + iov_base: from_key.as_ptr() as *mut _, + iov_len: from_key.to_bytes_with_nul().len(), + }, + libc::iovec { + iov_base: from_cstr.as_ptr() as *mut _, + iov_len: from_cstr.as_bytes_with_nul().len(), + }, + ]; + + unsafe { libc::nmount(iov.as_mut_ptr(), iov.len() as u32, libc::MNT_RDONLY) == 0 } +} + +pub fn unmount_config_iso() { + let mount_cstr = CString::new(ISO_MOUNT).unwrap(); + unsafe { libc::unmount(mount_cstr.as_ptr(), 0) }; +} From fe314a50ecf6fdec91e088e8af52ee281fe8bf10 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 16:38:06 -0400 Subject: [PATCH 11/24] init: wire FreeBSD platform support Connect the FreeBSD helpers into the boot sequence: - open_console() and populate_env_from_kenv() are called at the very start of main() before anything else - setsid/TIOCSCTTY are Linux-only; open_console() handles session setup on FreeBSD - setlogin("root") is called on FreeBSD after console setup - KRUN_DHCP and DHCP setup are Linux-only - If KRUN_CONFIG is not set, mount_config_iso() is attempted; the ISO is unmounted immediately after config::load() returns - fs::* mounts and mount_shared_root are Linux-only - exec_workload() calls open_console() on FreeBSD instead of setup_redirects(), giving the child process a fresh controlling terminal before execvp Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/exec.rs | 2 ++ init/src/main.rs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/init/src/exec.rs b/init/src/exec.rs index b9314a2df..e54f4e2d7 100644 --- a/init/src/exec.rs +++ b/init/src/exec.rs @@ -111,6 +111,8 @@ pub fn run_workload(argv: &[String]) -> ! { fn exec_workload(argv: &[String]) -> ! { #[cfg(target_os = "linux")] setup_redirects(); + #[cfg(target_os = "freebsd")] + crate::freebsd::open_console(); let c_argv: Vec = argv .iter() diff --git a/init/src/main.rs b/init/src/main.rs index 18e2cf8ee..8bef8c1b2 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -3,10 +3,18 @@ mod config; mod dhcp; mod env; mod exec; +#[cfg(target_os = "freebsd")] +mod freebsd; #[cfg(target_os = "linux")] mod fs; fn main() -> anyhow::Result<()> { + #[cfg(target_os = "freebsd")] + freebsd::open_console(); + + #[cfg(target_os = "freebsd")] + freebsd::populate_env_from_kenv(); + #[cfg(any(feature = "amd-sev", feature = "tdx"))] fs::mount_tee_block_device()?; @@ -20,16 +28,29 @@ fn main() -> anyhow::Result<()> { let _ = nix::unistd::setsid(); unsafe { libc::ioctl(0, libc::TIOCSCTTY as _, 1i32) }; + #[cfg(target_os = "freebsd")] + unsafe { + libc::setlogin(c"root".as_ptr()) + }; + env::setup_network( #[cfg(target_os = "linux")] "eth0", ); + #[cfg(target_os = "freebsd")] + let iso_mounted = std::env::var("KRUN_CONFIG").is_err() && freebsd::mount_config_iso(); + #[cfg(target_os = "linux")] let cfg = config::load(fs::is_mount_point); #[cfg(not(target_os = "linux"))] let cfg = config::load(); + #[cfg(target_os = "freebsd")] + if iso_mounted { + freebsd::unmount_config_iso(); + } + #[cfg(target_os = "linux")] if let Some(ref path) = cfg.tmpfs { fs::mount_tmpfs(path)?; From f2872840d4af76eb394df3001acef7b701c3e579 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Fri, 8 May 2026 14:18:54 -0400 Subject: [PATCH 12/24] ci: fix FreeBSD cross-compilation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the C-based BSD init build rule (which referenced the now-deleted init/init.c) with a cargo build rule targeting the correct Rust triple. Makefile: - Remove dead INIT_SRC = init/init.c variable. - Derive FREEBSD_RUST_TARGET from the host ARCH with arm64→aarch64 substitution to get the correct Rust triple. - Set CARGO_BSD_RUSTFLAGS with the clang cross-linker flags (mirroring the existing CC_BSD setup) so cargo can link for FreeBSD. - aarch64-unknown-freebsd is a Tier 3 target with no prebuilt std; use +nightly -Z build-std for that case. setup-build-env: - Add rustup target add x86_64-unknown-freebsd (Tier 2, prebuilt std). - Install nightly toolchain + rust-src for the aarch64 FreeBSD case. cross-compilation.yml: - Add clang to the Linux cross-compilation dependencies so the FreeBSD linker flags resolve correctly on Linux runners. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- .github/actions/setup-build-env/action.yml | 3 +++ .github/workflows/cross-compilation.yml | 4 ++-- Makefile | 25 ++++++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index f407b81c2..fc2aa34f0 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -9,6 +9,9 @@ runs: rustup update stable rustup default stable rustup component add rustfmt clippy + rustup target add x86_64-unknown-freebsd + rustup toolchain install nightly + rustup component add rust-src --toolchain nightly - name: Cache Cargo dependencies uses: actions/cache@v4 diff --git a/.github/workflows/cross-compilation.yml b/.github/workflows/cross-compilation.yml index 40273f800..0fe86dcfe 100644 --- a/.github/workflows/cross-compilation.yml +++ b/.github/workflows/cross-compilation.yml @@ -35,7 +35,7 @@ jobs: - name: Install cross-compilation dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends lld + sudo apt-get install -y --no-install-recommends clang lld - name: Build FreeBSD init on Linux run: make BUILD_BSD_INIT=1 -- init/init-freebsd @@ -52,7 +52,7 @@ jobs: - name: Install cross-compilation dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends lld + sudo apt-get install -y --no-install-recommends clang lld - name: Build FreeBSD init on Linux aarch64 run: make BUILD_BSD_INIT=1 -- init/init-freebsd diff --git a/Makefile b/Makefile index 9de26c976..b80c9ec16 100644 --- a/Makefile +++ b/Makefile @@ -20,8 +20,6 @@ AWS_NITRO_INIT_SRC = \ AWS_NITRO_INIT_LD_FLAGS = -larchive -lnsm -INIT_SRC = init/init.c - ifeq ($(SEV),1) VARIANT = -sev FEATURE_FLAGS := --features amd-sev @@ -143,6 +141,7 @@ else endif # Cross-compile on macOS with the LLVM linker (brew install lld) CC_BSD=$(CLANG) -target $(ARCH)-unknown-freebsd -fuse-ld=lld -stdlib=libc++ -Wl,-strip-debug --sysroot $(SYSROOT_BSD) + CARGO_BSD_RUSTFLAGS = -C linker=$(CLANG) -C link-arg=-target -C link-arg=$(ARCH)-unknown-freebsd -C link-arg=-fuse-ld=lld -C link-arg=-stdlib=libc++ -C link-arg=--sysroot=$(abspath $(SYSROOT_BSD)) -C target-feature=+crt-static else ifeq ($(OS),Linux) # Linux -> FreeBSD cross-compilation ifeq ($(SYSROOT_BSD),) @@ -153,16 +152,34 @@ else endif # Cross-compile on Linux with clang CC_BSD=$(CLANG) -target $(ARCH)-unknown-freebsd -fuse-ld=lld -Wl,-strip-debug --sysroot $(SYSROOT_BSD) + CARGO_BSD_RUSTFLAGS = -C linker=$(CLANG) -C link-arg=-target -C link-arg=$(ARCH)-unknown-freebsd -C link-arg=-fuse-ld=lld -C link-arg=--sysroot=$(abspath $(SYSROOT_BSD)) -C target-feature=+crt-static else # Build on FreeBSD host CC_BSD=$(CC) SYSROOT_BSD_TARGET = + CARGO_BSD_RUSTFLAGS = -C target-feature=+crt-static +endif + +FREEBSD_RUST_TARGET = $(subst arm64,aarch64,$(ARCH))-unknown-freebsd + +# aarch64-unknown-freebsd is Tier 3: no prebuilt std, requires nightly + build-std. +ifeq ($(FREEBSD_RUST_TARGET),aarch64-unknown-freebsd) + CARGO_BSD_TOOLCHAIN = +nightly + CARGO_BSD_EXTRA_FLAGS = -Z build-std +else + CARGO_BSD_TOOLCHAIN = + CARGO_BSD_EXTRA_FLAGS = endif ifeq ($(BUILD_BSD_INIT),1) INIT_BINARY_BSD = init/init-freebsd -$(INIT_BINARY_BSD): $(INIT_SRC) $(SYSROOT_BSD_TARGET) - $(CC_BSD) -std=c23 -O2 -static -Wall -o $@ $(INIT_SRC) -lutil +$(INIT_BINARY_BSD): $(shell find init/src -name '*.rs') init/Cargo.toml $(SYSROOT_BSD_TARGET) + RUSTFLAGS="$(CARGO_BSD_RUSTFLAGS)" \ + cargo $(CARGO_BSD_TOOLCHAIN) build --release \ + $(CARGO_BSD_EXTRA_FLAGS) \ + --manifest-path init/Cargo.toml \ + --target $(FREEBSD_RUST_TARGET) + cp target/$(FREEBSD_RUST_TARGET)/release/krun-init $@ endif # Sysroot preparation rules for cross-compilation on macOS From ec1b733ea397e311b9e1344b48ae14010ec82e77 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Thu, 7 May 2026 15:23:20 -0400 Subject: [PATCH 13/24] init: port timesync clock_worker to Rust Implements the timesync feature behind the `timesync` cargo feature flag. Receives host-side nanosecond timestamps over AF_VSOCK/SOCK_DGRAM on port 123 and applies them via clock_settime when the delta exceeds 100ms. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/src/main.rs | 5 +++++ init/src/timesync.rs | 50 ++++++++++++++++++++++++++++++++++++++++++ src/devices/Cargo.toml | 1 + src/init-blob/build.rs | 4 +--- src/libkrun/Cargo.toml | 1 + 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 init/src/timesync.rs diff --git a/init/src/main.rs b/init/src/main.rs index 8bef8c1b2..ebf1a98a6 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -7,6 +7,8 @@ mod exec; mod freebsd; #[cfg(target_os = "linux")] mod fs; +#[cfg(feature = "timesync")] +mod timesync; fn main() -> anyhow::Result<()> { #[cfg(target_os = "freebsd")] @@ -83,5 +85,8 @@ fn main() -> anyhow::Result<()> { vec!["/bin/sh".to_string()] }; + #[cfg(feature = "timesync")] + timesync::run(); + exec::run_workload(&argv); } diff --git a/init/src/timesync.rs b/init/src/timesync.rs new file mode 100644 index 000000000..b97d53377 --- /dev/null +++ b/init/src/timesync.rs @@ -0,0 +1,50 @@ +use std::mem; + +use nix::sys::socket::{self, VsockAddr}; + +const TSYNC_PORT: u32 = 123; +const NANOS_IN_SECOND: u64 = 1_000_000_000; +const DELTA_SYNC: u64 = 100_000_000; // 100ms — don't bother adjusting for smaller drifts + +pub fn run() { + let sock = unsafe { libc::socket(libc::AF_VSOCK, libc::SOCK_DGRAM, 0) }; + if sock < 0 { + return; + } + + let addr = VsockAddr::new(libc::VMADDR_CID_ANY, TSYNC_PORT); + if socket::bind(sock, &addr).is_err() { + unsafe { libc::close(sock) }; + return; + } + + std::thread::Builder::new() + .name("timesync".into()) + .spawn(move || { + loop { + let mut buf = [0u8; 8]; + let n = unsafe { libc::recv(sock, buf.as_mut_ptr() as *mut _, buf.len(), 0) }; + if n < 0 { + break; + } + if n != 8 { + continue; + } + + let host_ns = u64::from_le_bytes(buf); + + let mut guest_ts: libc::timespec = unsafe { mem::zeroed() }; + unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut guest_ts) }; + let guest_ns = guest_ts.tv_sec as u64 * NANOS_IN_SECOND + guest_ts.tv_nsec as u64; + + if host_ns.abs_diff(guest_ns) > DELTA_SYNC { + let host_ts = libc::timespec { + tv_sec: (host_ns / NANOS_IN_SECOND) as libc::time_t, + tv_nsec: (host_ns % NANOS_IN_SECOND) as libc::c_long, + }; + unsafe { libc::clock_settime(libc::CLOCK_REALTIME, &host_ts) }; + } + } + }) + .unwrap(); +} diff --git a/src/devices/Cargo.toml b/src/devices/Cargo.toml index f314e9127..f0b621d72 100644 --- a/src/devices/Cargo.toml +++ b/src/devices/Cargo.toml @@ -19,6 +19,7 @@ input = ["zerocopy", "krun_input"] virgl_resource_map2 = [] aws-nitro = [] test_utils = [] +timesync = [] vhost-user = ["vhost", "vmm-sys-util"] [dependencies] diff --git a/src/init-blob/build.rs b/src/init-blob/build.rs index eb47e6979..da5afa9d3 100644 --- a/src/init-blob/build.rs +++ b/src/init-blob/build.rs @@ -78,8 +78,6 @@ fn build_rust_init() -> PathBuf { workspace_root.join("init/src").display() ); println!("cargo:rerun-if-changed={}", init_manifest.display()); - println!("cargo:rerun-if-env-changed=TIMESYNC"); - // Resolve which rustc (and paired cargo) to use for the init binary. let (rustc, cargo, use_musl) = match find_musl_rustc(&default_rustc) { Some(musl_rustc) => { @@ -125,7 +123,7 @@ fn build_rust_init() -> PathBuf { if cfg!(feature = "tdx") { features.push("tdx"); } - if env::var_os("TIMESYNC").is_some_and(|v| v == "1") { + if cfg!(feature = "timesync") { features.push("timesync"); } if !features.is_empty() { diff --git a/src/libkrun/Cargo.toml b/src/libkrun/Cargo.toml index 7ab669fbf..711fd3450 100644 --- a/src/libkrun/Cargo.toml +++ b/src/libkrun/Cargo.toml @@ -19,6 +19,7 @@ input = ["krun_input", "vmm/input", "devices/input"] virgl_resource_map2 = ["devices/virgl_resource_map2"] aws-nitro = ["vmm/aws-nitro", "devices/aws-nitro", "dep:aws-nitro", "dep:nitro-enclaves"] vhost-user = ["vmm/vhost-user", "devices/vhost-user"] +timesync = ["devices/timesync", "init-blob/timesync"] [dependencies] crossbeam-channel = ">=0.5.15" From 9ff0a411912b4561bfecc2884685071258741ed4 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 6 May 2026 15:43:04 -0400 Subject: [PATCH 14/24] init: remove C sources and tee/ attestation code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete init/init.c, init/dhcp.c, init/dhcp.h, init/jsmn.h, and the entire init/tee/ directory (snp_attest.c/h and the KBS client). The amd-sev feature no longer performs LUKS unlock or KBS attestation — it mounts /dev/vda as ext4 like the tdx path does. Signed-off-by: Jake Correnti Assisted-by: Claude Code:claude-sonnet-4.6 --- init/dhcp.c | 626 -------------------------------------- init/dhcp.h | 61 ---- init/jsmn.h | 494 ------------------------------ init/tee/kbs/kbs.h | 53 ---- init/tee/kbs/kbs_crypto.c | 332 -------------------- init/tee/kbs/kbs_curl.c | 232 -------------- init/tee/kbs/kbs_types.c | 247 --------------- init/tee/kbs/kbs_util.c | 165 ---------- init/tee/snp_attest.c | 220 -------------- init/tee/snp_attest.h | 107 ------- 10 files changed, 2537 deletions(-) delete mode 100644 init/dhcp.c delete mode 100644 init/dhcp.h delete mode 100644 init/jsmn.h delete mode 100644 init/tee/kbs/kbs.h delete mode 100644 init/tee/kbs/kbs_crypto.c delete mode 100644 init/tee/kbs/kbs_curl.c delete mode 100644 init/tee/kbs/kbs_types.c delete mode 100644 init/tee/kbs/kbs_util.c delete mode 100644 init/tee/snp_attest.c delete mode 100644 init/tee/snp_attest.h diff --git a/init/dhcp.c b/init/dhcp.c deleted file mode 100644 index 3c107e5b6..000000000 --- a/init/dhcp.c +++ /dev/null @@ -1,626 +0,0 @@ -/* - * DHCP Client Implementation - * - * Standalone DHCP client for configuring IPv4 network interfaces. - * Translated from Rust implementation in muvm/src/guest/net.rs - */ - -#include "dhcp.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define DHCP_BUFFER_SIZE 576 -#define DHCP_MSG_OFFER 2 -#define DHCP_MSG_ACK 5 - -/* Helper function to send netlink message */ -static int nl_send(int sock, struct nlmsghdr *nlh) -{ - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - }; - - struct iovec iov = { - .iov_base = nlh, - .iov_len = nlh->nlmsg_len, - }; - - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - - return sendmsg(sock, &msg, 0); -} - -/* Helper function to receive netlink response */ -static int nl_recv(int sock, char *buf, size_t len) -{ - struct sockaddr_nl sa; - struct iovec iov = { - .iov_base = buf, - .iov_len = len, - }; - - struct msghdr msg = { - .msg_name = &sa, - .msg_namelen = sizeof(sa), - .msg_iov = &iov, - .msg_iovlen = 1, - }; - - return recvmsg(sock, &msg, 0); -} - -/* Add routing attribute to netlink message */ -static void add_rtattr(struct nlmsghdr *nlh, int type, const void *data, - int len) -{ - int rtalen = RTA_SPACE(len); - struct rtattr *rta = - (struct rtattr *)(((char *)nlh) + NLMSG_ALIGN(nlh->nlmsg_len)); - rta->rta_type = type; - rta->rta_len = RTA_LENGTH(len); - memcpy(RTA_DATA(rta), data, len); - nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + rtalen; -} - -/* Set MTU */ -static int set_mtu(int nl_sock, int iface_index, unsigned int mtu) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct ifinfomsg *ifi; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); - nlh->nlmsg_type = RTM_NEWLINK; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - ifi = (struct ifinfomsg *)NLMSG_DATA(nlh); - ifi->ifi_family = AF_UNSPEC; - ifi->ifi_type = ARPHRD_ETHER; - ifi->ifi_index = iface_index; - - add_rtattr(nlh, IFLA_MTU, &mtu, sizeof(mtu)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for set_mtu"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for set_mtu"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for set_mtu\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for set_mtu: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Add or delete IPv4 route */ -static int mod_route4(int nl_sock, int iface_index, int cmd, struct in_addr gw) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct rtmsg *rtm; - struct in_addr dst = {.s_addr = INADDR_ANY}; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); - nlh->nlmsg_type = cmd; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - rtm = (struct rtmsg *)NLMSG_DATA(nlh); - rtm->rtm_family = AF_INET; - rtm->rtm_dst_len = 0; - rtm->rtm_src_len = 0; - rtm->rtm_tos = 0; - rtm->rtm_table = RT_TABLE_MAIN; - rtm->rtm_protocol = RTPROT_BOOT; - rtm->rtm_scope = RT_SCOPE_UNIVERSE; - rtm->rtm_type = RTN_UNICAST; - rtm->rtm_flags = 0; - - add_rtattr(nlh, RTA_OIF, &iface_index, sizeof(iface_index)); - add_rtattr(nlh, RTA_DST, &dst, sizeof(dst)); - add_rtattr(nlh, RTA_GATEWAY, &gw, sizeof(gw)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for mod_route4"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for mod_route4"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for mod_route4\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for mod_route4: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Add or delete IPv4 address */ -static int mod_addr4(int nl_sock, int iface_index, int cmd, struct in_addr addr, - unsigned char prefix_len) -{ - char buf[4096]; - struct nlmsghdr *nlh; - struct nlmsgerr *err; - struct ifaddrmsg *ifa; - - memset(buf, 0, sizeof(buf)); - nlh = (struct nlmsghdr *)buf; - nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg)); - nlh->nlmsg_type = cmd; - nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_ACK; - nlh->nlmsg_seq = 1; - nlh->nlmsg_pid = getpid(); - - ifa = (struct ifaddrmsg *)NLMSG_DATA(nlh); - ifa->ifa_family = AF_INET; - ifa->ifa_prefixlen = prefix_len; - ifa->ifa_flags = 0; - ifa->ifa_scope = RT_SCOPE_UNIVERSE; - ifa->ifa_index = iface_index; - - add_rtattr(nlh, IFA_LOCAL, &addr, sizeof(addr)); - add_rtattr(nlh, IFA_ADDRESS, &addr, sizeof(addr)); - - if (nl_send(nl_sock, nlh) < 0) { - perror("nl_send failed for mod_addr4"); - return -1; - } - - /* Receive ACK */ - int len = nl_recv(nl_sock, buf, sizeof(buf)); - if (len < (int)NLMSG_LENGTH(sizeof(struct nlmsgerr))) { - perror("nl_recv failed for mod_addr4"); - return -1; - } - - if (nlh->nlmsg_type != NLMSG_ERROR) { - printf("netlink didn't return a valid answer for mod_addr4\n"); - return -1; - } - - err = (struct nlmsgerr *)NLMSG_DATA(nlh); - if (err->error != 0) { - printf("netlink returned an error for mod_addr4: %d\n", err->error); - return -1; - } - - return 0; -} - -/* Count leading ones in a 32-bit value */ -static unsigned char count_leading_ones(uint32_t val) -{ - unsigned char count = 0; - for (int i = 31; i >= 0; i--) { - if (val & (1U << i)) { - count++; - } else { - break; - } - } - return count; -} - -/* Return the DHCP message type (option 53) from a response, or 0 */ -static unsigned char get_dhcp_msg_type(const unsigned char *response, - ssize_t len) -{ - /* Walk DHCP options (TLV chain starting after the magic cookie) */ - size_t p = 240; - while (p < (size_t)len) { - unsigned char opt = response[p]; - - if (opt == 0xff) /* end */ - break; - if (opt == 0) { /* padding */ - p++; - continue; - } - - if (p + 1 >= (size_t)len) - break; - - unsigned char opt_len = response[p + 1]; - p += 2; - - if (p + opt_len > (size_t)len) - break; - if (opt == 53 && opt_len >= 1) /* Message Type */ - return response[p]; - - p += opt_len; - } - return 0; -} - -/* Parse a DHCP ACK and configure the interface. Returns 0 or -1 on error. */ -static int handle_dhcp_ack(int nl_sock, int iface_index, - const unsigned char *response, ssize_t len) -{ - FILE *resolv = NULL; - bool tried_opening_resolv = false; - - /* Need at least 240 bytes (DHCP header + magic cookie) + 1 for options */ - if (len < 241) { - printf("DHCPACK too short (%zd bytes)\n", len); - return -1; - } - - /* Parse DHCP response */ - struct in_addr addr; - /* yiaddr is at offset 16-19 in network byte order */ - memcpy(&addr.s_addr, &response[16], sizeof(addr.s_addr)); - - if (addr.s_addr == INADDR_ANY) { - printf("DHCPACK has no address (yiaddr is 0.0.0.0)\n"); - return -1; - } - - struct in_addr netmask = {.s_addr = INADDR_ANY}; - struct in_addr router = {.s_addr = INADDR_ANY}; - /* Clamp MTU to passt's limit */ - uint16_t mtu = 65520; - - /* Parse DHCP options (start at offset 240 after magic cookie) */ - size_t p = 240; - while (p < (size_t)len) { - unsigned char opt = response[p]; - - if (opt == 0xff) { - /* Option 255: End (of options) */ - break; - } - - if (opt == 0) { /* Padding */ - p++; - continue; - } - - if (p + 1 >= (size_t)len) - break; - - unsigned char opt_len = response[p + 1]; - p += 2; /* Length doesn't include code and length field itself */ - - if (p + opt_len > (size_t)len) { - /* Malformed packet, option length exceeds packet boundary */ - break; - } - - if (opt == 1 && opt_len >= 4) { - /* Option 1: Subnet Mask */ - memcpy(&netmask.s_addr, &response[p], sizeof(netmask.s_addr)); - } else if (opt == 3 && opt_len >= 4) { - /* Option 3: Router */ - memcpy(&router.s_addr, &response[p], sizeof(router.s_addr)); - } else if (opt == 6 && opt_len >= 4) { - /* Option 6: Domain Name Server */ - if (!resolv && !tried_opening_resolv) { - tried_opening_resolv = true; - resolv = fopen("/etc/resolv.conf", "w"); - if (!resolv) { - perror("Failed to open /etc/resolv.conf"); - } - } - - if (resolv) { - for (int dns_p = p; dns_p + 4 <= p + opt_len; dns_p += 4) { - fprintf(resolv, "nameserver %d.%d.%d.%d\n", response[dns_p], - response[dns_p + 1], response[dns_p + 2], - response[dns_p + 3]); - } - } - } else if (opt == 26 && opt_len >= 2) { - /* Option 26: Interface MTU */ - mtu = (response[p] << 8) | response[p + 1]; - - /* We don't know yet if IPv6 is available: don't go below 1280 B - */ - if (mtu < 1280) - mtu = 1280; - if (mtu > 65520) - mtu = 65520; - } - - p += opt_len; - } - - if (resolv) { - fclose(resolv); - } - - /* Calculate prefix length from netmask */ - unsigned char prefix_len = count_leading_ones(ntohl(netmask.s_addr)); - - if (mod_addr4(nl_sock, iface_index, RTM_NEWADDR, addr, prefix_len) != 0) { - printf("couldn't add the address provided by the DHCP server\n"); - return -1; - } - if (mod_route4(nl_sock, iface_index, RTM_NEWROUTE, router) != 0) { - printf("couldn't add the default route provided by the DHCP server\n"); - return -1; - } - set_mtu(nl_sock, iface_index, mtu); - return 0; -} - -/* Send DISCOVER with Rapid Commit, process ACK, configure address and route */ -int do_dhcp(const char *iface) -{ - struct sockaddr_in bind_addr, dest_addr; - struct dhcp_packet request = {0}; - unsigned char response[DHCP_BUFFER_SIZE]; - struct timeval timeout; - int iface_index; - int broadcast = 1; - int nl_sock = -1; - int sock = -1; - int ret = -1; - - iface_index = if_nametoindex(iface); - if (iface_index == 0) { - perror("Failed to find index for network interface"); - return ret; - } - - nl_sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); - if (nl_sock < 0) { - perror("Failed to create netlink socket"); - return ret; - } - - struct sockaddr_nl sa = { - .nl_family = AF_NETLINK, - .nl_pid = getpid(), - .nl_groups = 0, - }; - - if (bind(nl_sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { - perror("Failed to bind netlink socket"); - goto cleanup; - } - - /* Send request (DHCPDISCOVER) */ - sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (sock < 0) { - perror("socket failed"); - goto cleanup; - } - - /* Allow broadcast */ - if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, - sizeof(broadcast)) < 0) { - perror("setsockopt SO_BROADCAST failed"); - goto cleanup; - } - - if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, iface, - strlen(iface) + 1) < 0) { - perror("setsockopt SO_BINDTODEVICE failed"); - goto cleanup; - } - - /* Bind to port 68 (DHCP client) */ - memset(&bind_addr, 0, sizeof(bind_addr)); - bind_addr.sin_family = AF_INET; - bind_addr.sin_port = htons(68); - bind_addr.sin_addr.s_addr = INADDR_ANY; - - if (bind(sock, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) { - perror("bind failed"); - goto cleanup; - } - - request.op = 1; /* BOOTREQUEST */ - request.htype = 1; /* Hardware address type: Ethernet */ - request.hlen = 6; /* Hardware address length */ - request.hops = 0; /* DHCP relay Hops */ - request.xid = - htonl(getpid()); /* Transaction ID: use PID for some randomness */ - request.secs = - 0; /* Seconds elapsed since beginning of acquisition or renewal */ - request.flags = htons(0x8000); /* DHCP message flags: Broadcast */ - request.ciaddr = 0; /* Client IP address (not set yet) */ - request.yiaddr = 0; /* 'your' IP address (server will fill) */ - request.siaddr = 0; /* Server IP address (not set) */ - request.giaddr = 0; /* Relay agent IP address (not set) */ - request.magic = htonl(0x63825363); /* Magic cookie */ - - /* Populate chaddr with the interface's MAC address */ - struct ifreq mac_ifr; - memset(&mac_ifr, 0, sizeof(mac_ifr)); - strncpy(mac_ifr.ifr_name, iface, IFNAMSIZ); - - if (ioctl(sock, SIOCGIFHWADDR, &mac_ifr) < 0) { - perror("ioctl(SIOCGIFHWADDR) failed"); - goto cleanup; - } - memcpy(request.chaddr, mac_ifr.ifr_hwaddr.sa_data, 6); - - /* Build DHCP options */ - int opt_offset = 0; - - /* Option 53: DHCP Message Type = DISCOVER (1) */ - request.options[opt_offset++] = 53; - request.options[opt_offset++] = 1; - request.options[opt_offset++] = 1; - - /* Option 80: Rapid Commit (RFC 4039) */ - request.options[opt_offset++] = 80; - request.options[opt_offset++] = 0; - - /* Option 255: End of options */ - request.options[opt_offset++] = 0xff; - - /* Remaining bytes are padding (up to 300 bytes) */ - - /* Send DHCP DISCOVER */ - memset(&dest_addr, 0, sizeof(dest_addr)); - dest_addr.sin_family = AF_INET; - dest_addr.sin_port = htons(67); - dest_addr.sin_addr.s_addr = INADDR_BROADCAST; - - if (sendto(sock, &request, sizeof(request), 0, - (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { - perror("sendto failed"); - goto cleanup; - } - - /* Keep IPv6-only fast: set receive timeout to 100ms */ - timeout.tv_sec = 0; - timeout.tv_usec = 100000; - if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < - 0) { - perror("setsockopt SO_RCVTIMEO failed"); - goto cleanup; - } - - /* Get response: DHCPACK (Rapid Commit) or DHCPOFFER */ - struct sockaddr_in from_addr; - socklen_t from_len = sizeof(from_addr); - ssize_t len = recvfrom(sock, response, sizeof(response), 0, - (struct sockaddr *)&from_addr, &from_len); - - if (len <= 0) - goto done; /* No DHCP response — not an error, VM may be IPv6-only */ - - unsigned char msg_type = get_dhcp_msg_type(response, len); - - if (msg_type == DHCP_MSG_ACK) { - /* Rapid Commit — server sent ACK directly */ - close(sock); - sock = -1; - if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) - goto cleanup; - } else if (msg_type == DHCP_MSG_OFFER) { - /* - * DHCPOFFER — complete the 4-way handshake by sending DHCPREQUEST - * and waiting for DHCPACK. Servers without Rapid Commit (e.g. - * gvproxy) require this. - */ - struct in_addr offered_addr; - memcpy(&offered_addr.s_addr, &response[16], - sizeof(offered_addr.s_addr)); - - /* Build DHCPREQUEST */ - memset(request.options, 0, sizeof(request.options)); - opt_offset = 0; - - /* Option 53: DHCP Message Type = REQUEST (3) */ - request.options[opt_offset++] = 53; - request.options[opt_offset++] = 1; - request.options[opt_offset++] = 3; - - /* Option 50: Requested IP Address */ - request.options[opt_offset++] = 50; - request.options[opt_offset++] = 4; - memcpy(&request.options[opt_offset], &offered_addr.s_addr, 4); - opt_offset += 4; - - /* Option 54: Server Identifier (from_addr) */ - request.options[opt_offset++] = 54; - request.options[opt_offset++] = 4; - memcpy(&request.options[opt_offset], &from_addr.sin_addr.s_addr, 4); - opt_offset += 4; - - /* Option 255: End */ - request.options[opt_offset++] = 0xff; - - if (sendto(sock, &request, sizeof(request), 0, - (struct sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { - perror("sendto DHCPREQUEST failed"); - goto cleanup; - } - - from_len = sizeof(from_addr); - len = recvfrom(sock, response, sizeof(response), 0, - (struct sockaddr *)&from_addr, &from_len); - - close(sock); - sock = -1; - - if (len <= 0) { - printf("no DHCPACK received\n"); - goto cleanup; - } - - if (get_dhcp_msg_type(response, len) != DHCP_MSG_ACK) { - printf("expected DHCPACK but got message type %d\n", - get_dhcp_msg_type(response, len)); - goto cleanup; - } - - if (handle_dhcp_ack(nl_sock, iface_index, response, len) != 0) - goto cleanup; - } else { - printf("unexpected DHCP message type %d\n", msg_type); - goto cleanup; - } - -done: - ret = 0; -cleanup: - if (sock >= 0) { - close(sock); - } - if (nl_sock >= 0) { - close(nl_sock); - } - return ret; -} diff --git a/init/dhcp.h b/init/dhcp.h deleted file mode 100644 index 39e20ead7..000000000 --- a/init/dhcp.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * DHCP Client Implementation - * - * Standalone DHCP client for configuring IPv4 network interfaces. - * Translated from Rust implementation in muvm/src/guest/net.rs - */ - -#ifndef DHCP_H -#define DHCP_H - -#include - -/* BOOTP vendor-specific area size (64) - magic cookie (4) */ -#define DHCP_OPTIONS_SIZE 60 - -/* DHCP packet structure (RFC 2131) */ -struct dhcp_packet { - uint8_t op; /* Message op code / message type (1 = BOOTREQUEST) */ - uint8_t htype; /* Hardware address type (1 = Ethernet) */ - uint8_t hlen; /* Hardware address length (6 for Ethernet) */ - uint8_t hops; /* Client sets to zero */ - uint32_t xid; /* Transaction ID */ - uint16_t secs; /* Seconds elapsed since client began address acquisition */ - uint16_t flags; /* Flags (0x8000 = Broadcast) */ - uint32_t ciaddr; /* Client IP address */ - uint32_t yiaddr; /* 'your' (client) IP address */ - uint32_t siaddr; /* IP address of next server to use in bootstrap */ - uint32_t giaddr; /* Relay agent IP address */ - uint8_t chaddr[16]; /* Client hardware address */ - uint8_t sname[64]; /* Optional server host name */ - uint8_t file[128]; /* Boot file name */ - uint32_t magic; /* Magic cookie (0x63825363) */ - uint8_t options[DHCP_OPTIONS_SIZE]; /* Options field */ -} __attribute__((packed)); - -/* - * Perform DHCP discovery and configuration for a network interface - * - * This function: - * 1. Binds a UDP socket to the interface using SO_BINDTODEVICE - * 2. Sends a DHCP DISCOVER message with Rapid Commit option - * 3. Waits up to 100ms for a response: - * - If DHCPACK (Rapid Commit): applies configuration directly - * - If DHCPOFFER: sends DHCPREQUEST and waits for DHCPACK - * - If no response: returns success (VM may be IPv6-only) - * 4. Parses the ACK and configures: - * - IPv4 address with appropriate prefix length - * - Default gateway route - * - DNS servers (overwriting /etc/resolv.conf) - * - Interface MTU - * - * Parameters: - * iface - The name of the network interface to be configured. - * - * Returns: - * 0 on success (whether or not DHCP response was received) - * -1 on error - */ -int do_dhcp(const char *iface); - -#endif /* DHCP_H */ diff --git a/init/jsmn.h b/init/jsmn.h deleted file mode 100644 index 30d37a24a..000000000 --- a/init/jsmn.h +++ /dev/null @@ -1,494 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2010 Serge Zaitsev - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -#ifndef JSMN_H -#define JSMN_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -#define JSMN_API static - -/** - * JSON type identifier. Basic types are: - * o Object - * o Array - * o String - * o Other primitive: number, boolean (true/false) or null - */ -typedef enum { - JSMN_UNDEFINED = 0, - JSMN_OBJECT = 1 << 0, - JSMN_ARRAY = 1 << 1, - JSMN_STRING = 1 << 2, - JSMN_PRIMITIVE = 1 << 3 -} jsmntype_t; - -enum jsmnerr { - /* Not enough tokens were provided */ - JSMN_ERROR_NOMEM = -1, - /* Invalid character inside JSON string */ - JSMN_ERROR_INVAL = -2, - /* The string is not a full JSON packet, more bytes expected */ - JSMN_ERROR_PART = -3 -}; - -/** - * JSON token description. - * type type (object, array, string etc.) - * start start position in JSON data string - * end end position in JSON data string - */ -typedef struct jsmntok { - jsmntype_t type; - int start; - int end; - int size; -#ifdef JSMN_PARENT_LINKS - int parent; -#endif -} jsmntok_t; - -/** - * JSON parser. Contains an array of token blocks available. Also stores - * the string being parsed now and current position in that string. - */ -typedef struct jsmn_parser { - unsigned int pos; /* offset in the JSON string */ - unsigned int toknext; /* next token to allocate */ - int toksuper; /* superior token node, e.g. parent object or array */ -} jsmn_parser; - -/** - * Create JSON parser over an array of tokens - */ -JSMN_API void jsmn_init(jsmn_parser *parser); - -/** - * Run JSON parser. It parses a JSON data string into and array of tokens, each - * describing - * a single JSON object. - */ -JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, - jsmntok_t *tokens, const unsigned int num_tokens); - -#ifndef JSMN_HEADER -/** - * Allocates a fresh unused token from the token pool. - */ -static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, jsmntok_t *tokens, - const size_t num_tokens) -{ - jsmntok_t *tok; - if (parser->toknext >= num_tokens) { - return NULL; - } - tok = &tokens[parser->toknext++]; - tok->start = tok->end = -1; - tok->size = 0; -#ifdef JSMN_PARENT_LINKS - tok->parent = -1; -#endif - return tok; -} - -/** - * Fills token type and boundaries. - */ -static void jsmn_fill_token(jsmntok_t *token, const jsmntype_t type, - const int start, const int end) -{ - token->type = type; - token->start = start; - token->end = end; - token->size = 0; -} - -/** - * Fills next available token with JSON primitive. - */ -static int jsmn_parse_primitive(jsmn_parser *parser, const char *js, - const size_t len, jsmntok_t *tokens, - const size_t num_tokens) -{ - jsmntok_t *token; - int start; - - start = parser->pos; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - switch (js[parser->pos]) { -#ifndef JSMN_STRICT - /* In strict mode primitive must be followed by "," or "}" or "]" */ - case ':': -#endif - case '\t': - case '\r': - case '\n': - case ' ': - case ',': - case ']': - case '}': - goto found; - default: - /* to quiet a warning from gcc*/ - break; - } - /* libkrun: Let's be permissive with non-ASCII bytes - if (js[parser->pos] < 32 || js[parser->pos] >= 127) { - parser->pos = start; - return JSMN_ERROR_INVAL; - } - */ - } -#ifdef JSMN_STRICT - /* In strict mode primitive must be followed by a comma/object/array */ - parser->pos = start; - return JSMN_ERROR_PART; -#endif - -found: - if (tokens == NULL) { - parser->pos--; - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - parser->pos--; - return 0; -} - -/** - * Fills next token with JSON string. - */ -static int jsmn_parse_string(jsmn_parser *parser, char *js, const size_t len, - jsmntok_t *tokens, const size_t num_tokens) -{ - jsmntok_t *token; - - int start = parser->pos; - - /* Skip starting quote */ - parser->pos++; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c = js[parser->pos]; - - /* Quote: end of string */ - if (c == '\"') { - if (tokens == NULL) { - return 0; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - parser->pos = start; - return JSMN_ERROR_NOMEM; - } - jsmn_fill_token(token, JSMN_STRING, start + 1, parser->pos); -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - return 0; - } - - /* Backslash: Quoted symbol expected */ - if (c == '\\' && parser->pos + 1 < len) { - int i; - parser->pos++; - switch (js[parser->pos]) { - /* Allowed escaped symbols */ - case '\"': - case '/': - case '\\': - case 'b': - case 'f': - case 'r': - case 'n': - case 't': - break; - /* Allows escaped symbol \uXXXX */ - case 'u': { - char unicode[5]; - long ascii; - - parser->pos++; - for (i = 0; - i < 4 && parser->pos < len && js[parser->pos] != '\0'; - i++) { - /* If it isn't a hex character we have an error */ - if (!((js[parser->pos] >= 48 && - js[parser->pos] <= 57) || /* 0-9 */ - (js[parser->pos] >= 65 && - js[parser->pos] <= 70) || /* A-F */ - (js[parser->pos] >= 97 && - js[parser->pos] <= 102))) { /* a-f */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - unicode[i] = js[parser->pos]; - parser->pos++; - } - - unicode[4] = '\0'; - ascii = strtol(&unicode[0], NULL, 16); - if (ascii < 0 || ascii > 127) { - /* This unicode char doesn't translate directly to ASCII */ - parser->pos = start; - return JSMN_ERROR_INVAL; - } - - parser->pos--; - js[parser->pos] = (char)ascii; - break; - } - /* Unexpected symbol */ - default: - parser->pos = start; - return JSMN_ERROR_INVAL; - } - } - } - parser->pos = start; - return JSMN_ERROR_PART; -} - -/** - * Parse JSON string and fill tokens. - */ -JSMN_API int jsmn_parse(jsmn_parser *parser, const char *js, const size_t len, - jsmntok_t *tokens, const unsigned int num_tokens) -{ - int r; - int i; - jsmntok_t *token; - int count = parser->toknext; - - for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) { - char c; - jsmntype_t type; - - c = js[parser->pos]; - switch (c) { - case '{': - case '[': - count++; - if (tokens == NULL) { - break; - } - token = jsmn_alloc_token(parser, tokens, num_tokens); - if (token == NULL) { - return JSMN_ERROR_NOMEM; - } - if (parser->toksuper != -1) { - jsmntok_t *t = &tokens[parser->toksuper]; -#ifdef JSMN_STRICT - /* In strict mode an object or array can't become a key */ - if (t->type == JSMN_OBJECT) { - return JSMN_ERROR_INVAL; - } -#endif - t->size++; -#ifdef JSMN_PARENT_LINKS - token->parent = parser->toksuper; -#endif - } - token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); - token->start = parser->pos; - parser->toksuper = parser->toknext - 1; - break; - case '}': - case ']': - if (tokens == NULL) { - break; - } - type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); -#ifdef JSMN_PARENT_LINKS - if (parser->toknext < 1) { - return JSMN_ERROR_INVAL; - } - token = &tokens[parser->toknext - 1]; - for (;;) { - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - token->end = parser->pos + 1; - parser->toksuper = token->parent; - break; - } - if (token->parent == -1) { - if (token->type != type || parser->toksuper == -1) { - return JSMN_ERROR_INVAL; - } - break; - } - token = &tokens[token->parent]; - } -#else - for (i = parser->toknext - 1; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - if (token->type != type) { - return JSMN_ERROR_INVAL; - } - parser->toksuper = -1; - token->end = parser->pos + 1; - break; - } - } - /* Error if unmatched closing bracket */ - if (i == -1) { - return JSMN_ERROR_INVAL; - } - for (; i >= 0; i--) { - token = &tokens[i]; - if (token->start != -1 && token->end == -1) { - parser->toksuper = i; - break; - } - } -#endif - break; - case '\"': - r = jsmn_parse_string(parser, (char *)js, len, tokens, num_tokens); - if (r < 0) { - return r; - } - count++; - if (parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - case '\t': - case '\r': - case '\n': - case ' ': - break; - case ':': - parser->toksuper = parser->toknext - 1; - break; - case ',': - if (tokens != NULL && parser->toksuper != -1 && - tokens[parser->toksuper].type != JSMN_ARRAY && - tokens[parser->toksuper].type != JSMN_OBJECT) { -#ifdef JSMN_PARENT_LINKS - parser->toksuper = tokens[parser->toksuper].parent; -#else - for (i = parser->toknext - 1; i >= 0; i--) { - if (tokens[i].type == JSMN_ARRAY || - tokens[i].type == JSMN_OBJECT) { - if (tokens[i].start != -1 && tokens[i].end == -1) { - parser->toksuper = i; - break; - } - } - } -#endif - } - break; -#ifdef JSMN_STRICT - /* In strict mode primitives are: numbers and booleans */ - case '-': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case 't': - case 'f': - case 'n': - /* And they must not be keys of the object */ - if (tokens != NULL && parser->toksuper != -1) { - const jsmntok_t *t = &tokens[parser->toksuper]; - if (t->type == JSMN_OBJECT || - (t->type == JSMN_STRING && t->size != 0)) { - return JSMN_ERROR_INVAL; - } - } -#else - /* In non-strict mode every unquoted value is a primitive */ - default: -#endif - r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens); - if (r < 0) { - return r; - } - count++; - if (parser->toksuper != -1 && tokens != NULL) { - tokens[parser->toksuper].size++; - } - break; - -#ifdef JSMN_STRICT - /* Unexpected char in strict mode */ - default: - return JSMN_ERROR_INVAL; -#endif - } - } - - if (tokens != NULL) { - for (i = parser->toknext - 1; i >= 0; i--) { - /* Unmatched opened object or array */ - if (tokens[i].start != -1 && tokens[i].end == -1) { - return JSMN_ERROR_PART; - } - } - } - - return count; -} - -/** - * Creates a new parser based over a given buffer with an array of tokens - * available. - */ -JSMN_API void jsmn_init(jsmn_parser *parser) -{ - parser->pos = 0; - parser->toknext = 0; - parser->toksuper = -1; -} - -#endif /* JSMN_HEADER */ - -#ifdef __cplusplus -} -#endif - -#endif /* JSMN_H */ diff --git a/init/tee/kbs/kbs.h b/init/tee/kbs/kbs.h deleted file mode 100644 index 9549a76a4..000000000 --- a/init/tee/kbs/kbs.h +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#ifndef _KBS -#define _KBS - -#include -#include -#include - -#include "../snp_attest.h" - -/* - * Identifiers for all possible TEE architectures. - */ -enum tee { - TEE_SEV, - TEE_SGX, - TEE_SNP, - TEE_TDX, -}; - -/* - * The type of KBS operation to be performed. - */ -enum curl_post_type { - KBS_CURL_REQ, - KBS_CURL_ATTEST, - KBS_CURL_GET_KEY, -}; - -// kbs_util.c -char *tee_str(int); -char *find_cookie(char *, char *); -int read_cookie_val(char *, char *); -int json_parse_str(char *, char *, char *); - -// kbs_types.c -int kbs_request_marshal(char *, int, char *); -int kbs_challenge(CURL *, char *, char *, char *); -int kbs_attest(CURL *, char *, struct snp_report *, BIGNUM *, BIGNUM *, char *); -int kbs_get_key(CURL *, char *, char *, EVP_PKEY *, char *); - -// kbs_curl.c -int kbs_curl_post(CURL *, char *, char *, char *, int); -int kbs_curl_get(CURL *, char *, char *, char *, int); - -// kbs_crypto.c -int kbs_tee_pubkey_create(EVP_PKEY **, BIGNUM **, BIGNUM **); -int kbs_nonce_pubkey_hash(char *, EVP_PKEY *, unsigned char **, unsigned int *); -void BN_b64(BIGNUM *, char *); -int rsa_pkey_decrypt(EVP_PKEY *, char *, char **); - -#endif /* _KBS */ diff --git a/init/tee/kbs/kbs_crypto.c b/init/tee/kbs/kbs_crypto.c deleted file mode 100644 index 516fa2c5e..000000000 --- a/init/tee/kbs/kbs_crypto.c +++ /dev/null @@ -1,332 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include - -#include -#include -#include -#include -#include -#include - -#include "kbs.h" - -/* - * Create an OpenSSL TEE public/private key pair. - */ -int kbs_tee_pubkey_create(EVP_PKEY **pkey, BIGNUM **n, BIGNUM **e) -{ - int ret, rc; - EVP_PKEY_CTX *ctx; - - rc = -1; - ctx = NULL; - - /* - * The public/private key pair will use an RSA algorithm. Generate the - * keys' context. - */ - ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); - if (ctx == NULL) { - printf("ERROR: creating TEE public key context\n"); - - return rc; - } - - ret = EVP_PKEY_keygen_init(ctx); - if (ret < 1) { - printf("ERROR: initializing TEE public key generation\n"); - - goto ctx_free; - } - - /* - * Set key generation bits to 2048 and generate the key pair. - */ - ret = EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048); - if (ret < 1) { - printf("ERROR: setting RSA keygen bits\n"); - - goto ctx_free; - } - - *pkey = NULL; - ret = EVP_PKEY_keygen(ctx, pkey); - if (ret < 1) { - printf("ERROR: generating RSA key\n"); - - goto ctx_free; - } - - /* - * Get the modulus and exponents of the key pair. - */ - ret = EVP_PKEY_get_bn_param(*pkey, OSSL_PKEY_PARAM_RSA_N, n); - if (ret < 0 || n == NULL) { - printf("ERROR: getting public key modulus\n"); - - goto ctx_free; - } - - ret = EVP_PKEY_get_bn_param(*pkey, OSSL_PKEY_PARAM_RSA_E, e); - if (ret < 0 || e == NULL) { - printf("ERROR: getting public key exponent\n"); - - goto ctx_free; - } - - rc = 0; - -ctx_free: - EVP_PKEY_CTX_free(ctx); - - return rc; -} - -/* - * Create a SHA512 hash of the nonce and TEE public key to send to the - * attestation server. - */ -int kbs_nonce_pubkey_hash(char *nonce, EVP_PKEY *pkey, unsigned char **hash, - unsigned int *size) -{ - int rc; - EVP_MD_CTX *md_ctx; - BIGNUM *n, *e; - char n_b64[512], e_b64[512]; - - rc = -1; - - /* - * Initialize an MD context and initialize the SHA512 digest. - */ - md_ctx = EVP_MD_CTX_new(); - if (md_ctx == NULL) { - printf("ERROR: generating SHA512 context\n"); - - return rc; - } - - if (EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL) < 1) { - printf("ERROR: initializing SHA512 hash\n"); - - goto md_ctx_free; - } - - /* - * Update the digest with the data from the nonce. - */ - if (EVP_DigestUpdate(md_ctx, (void *)nonce, strlen(nonce)) < 1) { - printf("ERROR: updating SHA512 digest with nonce\n"); - - goto md_ctx_free; - } - - /* - * Update the digest with the data from the TEE public key. - * - * To do this, we will write the base64 encoding of the TEE public - * key's modulus and exponent. - */ - n = e = NULL; - if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_N, &n) == 0) { - printf("ERROR: unable to retrieve public key modulus\n"); - - goto md_ctx_free; - } - - if (EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_RSA_E, &e) == 0) { - printf("ERROR: unable to retrieve public key exponent\n"); - - goto md_ctx_free; - } - - /* - * base64-encode the modulus and exponents, and hash the base64 strings - * into the SHA512 digest. - */ - BN_b64(n, n_b64); - BN_b64(e, e_b64); - - if (EVP_DigestUpdate(md_ctx, (void *)n_b64, strlen(n_b64)) < 1) { - printf("ERROR: updating SHA512 digest with public key N\n"); - - goto md_ctx_free; - } - - if (EVP_DigestUpdate(md_ctx, (void *)e_b64, strlen(e_b64)) < 1) { - printf("ERROR: updating SHA512 digest with public key E\n"); - - goto md_ctx_free; - } - - /* - * Allocate the memory to hold the SHA512 hash, and write the SHA512 - * hash to the "hash" byte array. - */ - *hash = (unsigned char *)OPENSSL_malloc(EVP_MD_size(EVP_sha512())); - if (*hash == NULL) { - printf("ERROR: allocating memory for SHA512 hash\n"); - - goto md_ctx_free; - } - - if (EVP_DigestFinal_ex(md_ctx, *hash, size) < 1) { - printf("ERROR: finalizing the SHA512 hash\n"); - - goto hash_free; - } - - rc = 0; - - goto md_ctx_free; - -hash_free: - OPENSSL_free((void *)*hash); - -md_ctx_free: - EVP_MD_CTX_free(md_ctx); - - return rc; -} - -/* - * Using a given RSA public/private key pair, decrypt an encrypted and hex - * encoded string of text. Store the plaintext of the encrypted text into a - * buffer and point "plain_ptr" to said buffer. - */ -int rsa_pkey_decrypt(EVP_PKEY *pkey, char *enc, char **plain_ptr) -{ - int rc; - EVP_PKEY_CTX *ctx; - char enc_bin[4096], *plain; - size_t enc_bin_len, secret_plain_len = 4096; - - rc = -1; - - /* - * Decode the hex-encoded string to its byte format. - */ - if (OPENSSL_hexstr2buf_ex((unsigned char *)enc_bin, 4096, &enc_bin_len, enc, - '\0') != 1) { - printf("Error converting hex to buf\n"); - - return rc; - } - - /* - * Initialize the public key decryption context. - */ - ctx = EVP_PKEY_CTX_new(pkey, NULL); - if (ctx == NULL) { - printf("ERROR: creation of pkey context for decryption\n"); - - return rc; - } - - if (EVP_PKEY_decrypt_init(ctx) <= 0) { - printf("ERROR: creation of decryption context for pkey\n"); - - goto ctx_free; - } - - if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) { - printf("Error setting RSA padding\n"); - - goto ctx_free; - } - - /* - * To first get the length that the plain secret buffer should be, call - * EVP_PKEY_decrypt() with a NULL output buffer argument. Then, - * "secret_plain_len" will contain the proper amount of bytes to - * allocate for the output buffer. - */ - rc = EVP_PKEY_decrypt(ctx, NULL, &secret_plain_len, - (unsigned char *)enc_bin, enc_bin_len); - if (rc <= 0) { - printf("ERROR: finding plaintext passphrase length: %d\n", rc); - - goto ctx_free; - } - - /* - * Allocate the output buffer using "secret_plain_len". - */ - plain = OPENSSL_malloc(secret_plain_len); - if (plain == NULL) - goto ctx_free; - - /* - * Decrypt the string using the OpenSSL RSA public key. - */ - rc = EVP_PKEY_decrypt(ctx, (unsigned char *)plain, &secret_plain_len, - (unsigned char *)enc_bin, enc_bin_len); - if (rc <= 0) { - printf("ERROR: decrypting RSA-encrypted passphrase: %d\n", rc); - OPENSSL_free(plain); - - goto ctx_free; - } - plain[secret_plain_len] = '\0'; - - /* - * Set the "plain_ptr" arg to the plaintext passphrase". - */ - *plain_ptr = plain; - - rc = 0; - -ctx_free: - EVP_PKEY_CTX_free(ctx); - - return rc; -} - -/* - * base64-encode the contents of an OpenSSL BIGNUM. - */ -void BN_b64(BIGNUM *bn, char *str) -{ - BIO *bio; - BIO *b64; - char *bn_bin; - char *bn_b64; - int bn_binlen; - int bn_b64len; - - /* - * Encode the BIGNUM contents to binary. - */ - bn_binlen = BN_num_bytes(bn); - bn_bin = malloc(bn_binlen); - BN_bn2bin(bn, (unsigned char *)bn_bin); - - /* - * Write the binary-encoded string to to a base64-configured OpenSSL - * BIO. - */ - b64 = BIO_new(BIO_f_base64()); - BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); - bio = BIO_new(BIO_s_mem()); - BIO_push(b64, bio); - BIO_write(b64, bn_bin, bn_binlen); - BIO_flush(b64); - - /* - * Retrieve the base64-encoded contents of the BIO, null-terminate the - * string, and copy those contents to the output string. - */ - bn_b64len = BIO_get_mem_data(b64, &bn_b64); - bn_b64[bn_b64len] = '\0'; - - strcpy(str, bn_b64); - - /* - * Cleanup OpenSSL data structures. - */ - BIO_free(b64); - BIO_free(bio); - free(bn_bin); -} diff --git a/init/tee/kbs/kbs_curl.c b/init/tee/kbs/kbs_curl.c deleted file mode 100644 index eb639db72..000000000 --- a/init/tee/kbs/kbs_curl.c +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include -#include - -#include "kbs.h" - -#define KBS_CURL_ERR(x) \ - printf("%s: %s\n", __func__, x); \ - return -1; - -static CURLcode kbs_curl_set_headers(CURL *, char *); -size_t cwrite(void *, size_t, size_t, void *); - -/* - * Complete a cURL POST request. POST the "in" string and retrieve the contents - * of the POST request "out" string. - * - * Depending on the type of request, some extra headers may need to be set. - * For example, on a KBS REQUEST, no session ID has been retrieved from the - * attestation server so far. Yet, during a KBS_ATTEST request, a session ID - * has been given from the server and must be added to the headers. - */ -int kbs_curl_post(CURL *curl, char *url, char *in, char *out, int type) -{ - CURLcode code; - struct curl_slist *cks; - char full_url[256], *session_id_label, session_id[256]; - - /* - * Neither the input or output strings should be invalid/NULL. - */ - if (!in) { - KBS_CURL_ERR("Input argument NULL"); - } - - if (!out) { - KBS_CURL_ERR("Output argument NULL"); - } - - if (curl_easy_setopt(curl, CURLOPT_POST, 1L) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_POST"); - } - - if (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cwrite) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_WRITEFUNCTION"); - } - - /* - * If the operation being completed is a KBS REQUEST, then this is the - * initial request to the attestation server, and there is no session - * ID to make note of. Otherwise, the session ID has been established - * and must be parsed from the cURL cookies data. - */ - cks = NULL; - if (type == KBS_CURL_REQ) { - sprintf(full_url, "%s/kbs/v0/auth", url); - - if (curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "") != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_COOKIEFILE"); - } - - if (kbs_curl_set_headers(curl, NULL) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_HTTPHEADER"); - } - } else { - sprintf(full_url, "%s/kbs/v0/attest", url); - - if (curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cks) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_COOKIELIST"); - } - - session_id_label = NULL; - while (cks) { - session_id_label = find_cookie(cks->data, "session_id"); - - if (session_id_label) - break; - cks = cks->next; - } - - if (session_id_label == NULL) { - KBS_CURL_ERR("No session_id cookie found"); - } - - if (read_cookie_val(session_id_label, session_id) < 0) { - KBS_CURL_ERR("No session_id value for cookie"); - } - - if (kbs_curl_set_headers(curl, (char *)session_id) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_HTTPHEADER"); - } - } - - if (curl_easy_setopt(curl, CURLOPT_URL, full_url) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_URL"); - } - - /* - * This is a cURL POST request that will write data to the "out" - * argument. "out" is expected to have been allocated beforehand and - * able to hold the full response from the attestation server. - */ - if (curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(in)) != - CURLE_OK) { - KBS_CURL_ERR("CURLOPT_POSTFIELDSIZE"); - } - - if (curl_easy_setopt(curl, CURLOPT_POSTFIELDS, in) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_POSTFIELDS"); - } - - if (curl_easy_setopt(curl, CURLOPT_WRITEDATA, out) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_WRITEDATA"); - } - - code = curl_easy_perform(curl); - if (code != CURLE_OK && code != CURLE_WRITE_ERROR) { - KBS_CURL_ERR("CURL_EASY_PERFORM"); - } - - return 0; -} - -/* - * A cURL GET request. No input is given, and we are simply retrieving data - * from the KBS attestation server. - */ -int kbs_curl_get(CURL *curl, char *url, char *wid, char *out, int type) -{ - CURLcode code; - char full_url[100], *session_id_label, session_id[100]; - struct curl_slist *cookies; - - if (type != KBS_CURL_GET_KEY) { - KBS_CURL_ERR("Invalid KBS operation"); - } - - code = curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies); - if (code != CURLE_OK) { - KBS_CURL_ERR("Cannot retrieve cURL cookies"); - } - - /* - * This API is used by kbs_get_key(), therefore we are expected to have - * a valid session ID by this point. Parse the cURL cookies data to find - * this session ID. - */ - while (cookies != NULL) { - session_id_label = find_cookie(cookies->data, "session_id"); - if (session_id_label) - break; - - cookies = cookies->next; - } - - if (session_id_label == NULL) { - KBS_CURL_ERR("Couldn't find cookie labeled\n"); - } - - /* - * Read the session ID and include it in the cURL headers. - */ - if (read_cookie_val(session_id_label, session_id) < 0) { - KBS_CURL_ERR("Couldn't read cookie value\n"); - } - - if (kbs_curl_set_headers(curl, (char *)session_id) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_HTTPHEADER"); - } - - /* - * The location of the KBS key is located at - * $ATTESTATION_URL/kbs/v0/key/$WORKLOAD_ID. - */ - sprintf(full_url, "%s/kbs/v0/key/%s", url, wid); - - if (curl_easy_setopt(curl, CURLOPT_URL, full_url) != CURLE_OK) { - KBS_CURL_ERR("CURLOPT_URL"); - } - - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cwrite); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, out); - - code = curl_easy_perform(curl); - if (code != CURLE_OK && code != CURLE_WRITE_ERROR) { - KBS_CURL_ERR("CURL_EASY_PERFORM"); - } - - return 0; -} - -/* - * Set the cURL headers. If the session args is not NULL, that indicates that - * the session ID has been retrieved from attestation server before, and that - * session ID should be included in the headers. - */ -static CURLcode kbs_curl_set_headers(CURL *curl, char *session) -{ - struct curl_slist *slist; - char session_buf[512]; - - slist = NULL; - slist = curl_slist_append(slist, "Accept: application/json"); - slist = curl_slist_append(slist, - "Content-Type: application/json; charset=utf-8"); - - /* - * Add the session ID cookie if the session ID exists. - */ - if (session) { - sprintf(session_buf, "Cookie: session_id=%s", session); - curl_slist_append(slist, session_buf); - } - - /* - * Set the headers. - */ - return curl_easy_setopt(curl, CURLOPT_HTTPHEADER, slist); -} - -/* - * Simple strcpy() for attestation server responses. Required by a cURL - * operation that writes data. - */ -size_t cwrite(void *data, size_t size, size_t nmemb, void *userp) -{ - strcpy((char *)userp, (char *)data); - - return size; -} diff --git a/init/tee/kbs/kbs_types.c b/init/tee/kbs/kbs_types.c deleted file mode 100644 index b9f512d01..000000000 --- a/init/tee/kbs/kbs_types.c +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include -#include - -#include -#include -#include -#include - -#include "kbs.h" - -#include "../snp_attest.h" - -static void kbs_attestation_marshal(struct snp_report *, char *, BIGNUM *, - BIGNUM *, char *); -static void kbs_attestation_marshal_tee_pubkey(char *, BIGNUM *, BIGNUM *); - -/* - * Given a TEE architecture and workload ID, write the JSON string of the - * KBS REQUEST. - */ -int kbs_request_marshal(char *json_request, int tee, char *workload_id) -{ - char *teestr; - - /* - * Retrieve the KBS string equivalent of the TEE enum value. - */ - teestr = tee_str(tee); - if (teestr == NULL) - return -1; - - /* - * Build the KBS REQUEST JSON string. - */ - sprintf(json_request, - "{\"extra-params\":\"{\\\"workload_id\\\":\\\"%s\\\"}\",\"tee\":\"%" - "s\",\"version\":\"0.0.0\"}", - workload_id, teestr); - - return 0; -} - -/* - * Peform a KBS CHALLENGE. - * - * "json_request" is the JSON string of the KBS REQUEST. - * "nonce" is the output argument to be retrieved from the attestation server. - */ -int kbs_challenge(CURL *curl, char *url, char *json_request, char *nonce) -{ - int ret, rc; - char *nonce_json; - - rc = -1; - - nonce_json = (char *)malloc(0x2000); - if (nonce_json == NULL) { - printf("ERROR: unable to allocate JSON nonce buffer\n"); - - return rc; - } - - ret = kbs_curl_post(curl, url, (void *)json_request, (void *)nonce_json, - KBS_CURL_REQ); - if (ret < 0) { - printf("ERROR: could not complete KBS challenge\n"); - - goto out; - } - - /* - * Parse the JSON response from the KBS server to retrieve the nonce. - */ - if (json_parse_str(nonce, "nonce", nonce_json) < 0) { - printf("ERROR: unable to parse nonce from server response\n"); - - goto out; - } - - rc = 0; - -out: - free(nonce_json); - - return rc; -} - -/* - * Send all required materials (attestation report, certificate chain, etc..) - * to the attestation server for attestation. - */ -int kbs_attest(CURL *curl, char *url, struct snp_report *report, BIGNUM *mod, - BIGNUM *exp, char *gen) -{ - int rc; - char *json, errmsg[200]; - - rc = -1; - json = (char *)malloc(0x1000); - if (json == NULL) { - printf("ERROR: unable to allocate JSON buffer\n"); - - return rc; - } - - /* - * Marshal the kbs_types Attestation JSON struct with the given - * attestation report and certificate chain. - */ - kbs_attestation_marshal(report, json, mod, exp, gen); - - /* - * Ensure the error messaging string is empty, because we will - * eventually read this string as indicator of a cURL attestation - * server error. - */ - strcpy(errmsg, ""); - - if (kbs_curl_post(curl, url, json, errmsg, KBS_CURL_ATTEST) < 0) { - printf("ERROR: could not complete KBS attestation\n"); - - rc = -1; - goto out; - } - - /* - * If there is no error message, it can be assumed that the attestation - * was completed successfully. - */ - if (strcmp(errmsg, "") != 0) { - rc = -1; - printf("ATTESTATION ERROR: %s\n", errmsg); - - goto out; - } - - rc = 0; - -out: - free((void *)json); - - return rc; -} - -/* - * Retrieve the secret from the KBS attestation server. - */ -int kbs_get_key(CURL *curl, char *url, char *wid, EVP_PKEY *pkey, char *pass) -{ - int end_idx; - char json[4096]; - char encrypted[4096], *plain; - - /* - * The key is represented as a JSON byte list, copy this JSON list - * string to "json". - */ - if (kbs_curl_get(curl, url, wid, json, KBS_CURL_GET_KEY) < 0) { - printf("ERROR: could not complete KBS passphrase retrieval\n"); - - return -1; - } - - end_idx = strlen(json) - 2; - - memcpy(encrypted, json + 1, end_idx); - encrypted[end_idx] = '\0'; - - if (rsa_pkey_decrypt(pkey, encrypted, &plain) < 0) { - printf("ERROR: could not decrypt passphrase from KBS server\n"); - - return -1; - } - - strcpy(pass, plain); - - OPENSSL_free(plain); - - return 0; -} - -/* - * Marshal a JSON string of the kbs_types Attestation struct from the given - * attestation report and certificate data. - */ -static void kbs_attestation_marshal(struct snp_report *report, char *json, - BIGNUM *mod, BIGNUM *exp, char *gen) -{ - char buf[4096], *report_hexstr; - size_t report_hexstr_len; - - report_hexstr = (char *)malloc(0x1000); - if (report_hexstr == NULL) - return; - - sprintf(buf, "{"); - strcpy(json, buf); - - kbs_attestation_marshal_tee_pubkey(json, mod, exp); - - sprintf(buf, "\"tee-evidence\":\"{"); - strcat(json, buf); - - sprintf(buf, "\\\"gen\\\":\\\"%s\\\",", gen); - strcat(json, buf); - - OPENSSL_buf2hexstr_ex(report_hexstr, 0x1000, &report_hexstr_len, - (unsigned char *)report, sizeof(*report), '\0'); - report_hexstr[report_hexstr_len] = '\0'; - sprintf(buf, "\\\"report\\\":\\\"%s\\\",", report_hexstr); - strcat(json, buf); - - strcat(json, "\\\"cert_chain\\\":\\\"[]\\\"}"); - - strcat(json, "\"}"); -} - -/* - * Marshal a JSON string of the KBS TEE public key. - */ -static void kbs_attestation_marshal_tee_pubkey(char *json, BIGNUM *mod, - BIGNUM *exp) -{ - char mod_b64[512], exp_b64[512]; - char buf[1024]; - - if (mod == NULL || exp == NULL) - return; - - BN_b64(mod, mod_b64); - BN_b64(exp, exp_b64); - - sprintf(buf, "\"tee-pubkey\":{"); - strcat(json, buf); - - sprintf(buf, "\"alg\":\"RSA\","); - strcat(json, buf); - - sprintf(buf, "\"k-mod\":\"%s\",", mod_b64); - strcat(json, buf); - - sprintf(buf, "\"k-exp\":\"%s\"},", exp_b64); - strcat(json, buf); -} diff --git a/init/tee/kbs/kbs_util.c b/init/tee/kbs/kbs_util.c deleted file mode 100644 index 6c0b7c878..000000000 --- a/init/tee/kbs/kbs_util.c +++ /dev/null @@ -1,165 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include - -#include "../../jsmn.h" -#include "kbs.h" - -#define MAX_TOKENS 16384 - -static int label_find(char *, char *); - -/* - * Return the string identifier of the inputted TEE architecture. - */ -char *tee_str(int tee) -{ - switch (tee) { - case TEE_SEV: - return "sev"; - case TEE_SGX: - return "sgx"; - case TEE_SNP: - return "snp"; - case TEE_TDX: - return "tdx"; - - /* - * No other TEE architecture is supported. - */ - default: - printf("ERROR: tee_str(): Invalid input\n"); - return NULL; - } -} - -/* - * Parse a given string of cURL cookie data and find the label indicated by the - * "label" argument. This function is essentially a search of a substring - * within a given string. - */ -char *find_cookie(char *cookie_data, char *label) -{ - char *cookie_ptr; - size_t label_len, cookie_len; - - label_len = strlen(label); - cookie_len = strlen(cookie_data); - - cookie_ptr = cookie_data; - for (int i = 0; i < (cookie_len - label_len); i++, cookie_ptr++) { - if (strncmp(cookie_ptr, label, label_len) == 0) - return cookie_ptr; - } - - return NULL; -} - -/* - * From a label in a cURL cookie string, parse its associated value. - */ -int read_cookie_val(char *label, char *buf) -{ - char *ptr; - int ws; - - ws = 0; - ptr = label; - for (ptr = label; *ptr != '\0'; ptr++) { - if (*ptr == ' ' || *ptr == '\t') - ws = 1; - else if (ws == 1) { - strcpy(buf, ptr); - - return 0; - } - } - - return -1; -} - -/* - * Given a JSON string and a "label", parse the string associated with that - * label and write the contents to "out". - */ -int json_parse_str(char *out, char *label, char *json) -{ - int ntokens, eq, rc; - jsmn_parser parser; - jsmntok_t *tokens, *curr, *next; - char *val; - int len; - - rc = -1; - - tokens = (jsmntok_t *)malloc(MAX_TOKENS * sizeof(jsmntok_t)); - if (tokens == NULL) { - printf("ERROR: unable to allocate JSON string\n"); - - return rc; - } - - jsmn_init(&parser); - - ntokens = jsmn_parse(&parser, json, strlen(json), tokens, MAX_TOKENS); - if (ntokens <= 0) { - printf("ERROR: unable to find any tokens in KBS challenge\n"); - - goto out; - } - - /* - * Traverse each token of the JSON string. - */ - for (int i = 0; i < ntokens - 1; i++) { - curr = &tokens[i]; - next = &tokens[i + 1]; - - /* - * Only interested in reading a string. - */ - if (curr->type != JSMN_STRING) - continue; - - /* - * Compare the current token with the label being searched for. - */ - eq = label_find(label, json + curr->start); - if (eq && next->type == JSMN_STRING) { - /* - * Found the string associated with the label, calculate - * its beginning and ending indexes within the JSON - * string and copy the contents over to "out". - */ - val = json + next->start; - len = next->end - next->start; - - memcpy((void *)out, (void *)val, len); - rc = 0; - - goto out; - } - } - -out: - free((void *)tokens); - - return rc; -} - -static int label_find(char *label, char *str) -{ - size_t label_sz; - - label_sz = strlen(label); - - for (int i = 0; i < label_sz; i++) { - if (label[i] != str[i]) - return 0; - if (label[i] != '\0') - continue; - } - - return 1; -} diff --git a/init/tee/snp_attest.c b/init/tee/snp_attest.c deleted file mode 100644 index 8303705ff..000000000 --- a/init/tee/snp_attest.c +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include - -#include "kbs/kbs.h" -#include "snp_attest.h" - -#define NONCE_MAX 1024 -#define JSON_MAX 1024 -#define GEN_MAX 32 - -static int snp_get_report(const uint8_t *, size_t, struct snp_report *); -static int SNP_ATTEST_ERR(char *); -static void json_fmt(char *); - -int snp_attest(char *pass, char *url, char *wid, char *tee_data) -{ - CURL *curl; - char nonce[NONCE_MAX], json[JSON_MAX], gen[GEN_MAX]; - struct snp_report report; - EVP_PKEY *pkey; - BIGNUM *n, *e; - unsigned int hash_size; - uint8_t *hash; - - if (kbs_request_marshal(json, TEE_SNP, wid) < 0) - return SNP_ATTEST_ERR("Unable to marshal KBS REQUEST"); - - curl = curl_easy_init(); - if (curl == NULL) - return SNP_ATTEST_ERR("Unable to initialize cURL instance"); - - if (kbs_challenge(curl, url, json, nonce) < 0) - return SNP_ATTEST_ERR("Unable to retrieve nonce from server"); - - json_fmt(tee_data); - if (json_parse_str(gen, "gen", tee_data) < 0) - return SNP_ATTEST_ERR("Unable to retrieve SNP generation"); - - n = e = NULL; - if (kbs_tee_pubkey_create(&pkey, &n, &e) < 0) - return SNP_ATTEST_ERR("Unable to create TEE public key"); - - if (kbs_nonce_pubkey_hash(nonce, pkey, &hash, &hash_size) < 0) - return SNP_ATTEST_ERR("Unable to hash nonce and public key"); - - if (snp_get_report(hash, hash_size, &report) != EXIT_SUCCESS) - return SNP_ATTEST_ERR("Unable to retrieve attestation report"); - - if (kbs_attest(curl, url, &report, n, e, gen) < 0) - return SNP_ATTEST_ERR("Unable to complete KBS ATTESTATION"); - - curl_easy_reset(curl); - - if (kbs_get_key(curl, url, wid, pkey, pass) < 0) - return SNP_ATTEST_ERR("Unable to retrieve passphrase"); - - return 0; -} - -/* - * A function for the SNP_GET_REPORT ioctl. - * - * SNP_GET_REPORT fills both the attestation report and the certificate - * data. - */ -static int snp_get_report(const uint8_t *data, size_t data_sz, - struct snp_report *report) -{ - int rc = EXIT_FAILURE; - int fd = -1; - struct snp_report_req req; - struct snp_report_resp resp; - struct snp_guest_request_ioctl guest_req; - struct msg_report_resp *report_resp = (struct msg_report_resp *)&resp.data; - - /* - * The kernel will attempt to fill the report, certs, and certs_size, - * Therefore, none of these values can be NULL. - */ - if (report == NULL) { - printf("report is NULL\n"); - rc = EINVAL; - - goto out; - } - - /* - * We will be filling the user_data field of the request with "data". - * Ensure that the data is valid and can fit in the user_data field. - */ - if (data && (data_sz > sizeof(req.user_data) || data_sz == 0)) { - rc = EINVAL; - - goto out; - } - - /* - * Initialize data structures. - */ - memset(&req, 0, sizeof(req)); - - /* - * Copy the data into user_data if it exists. - */ - if (data) - memcpy(&req.user_data, data, data_sz); - - memset(&resp, 0, sizeof(resp)); - - memset(&guest_req, 0, sizeof(guest_req)); - guest_req.msg_version = 1; - guest_req.req_data = (__u64)&req; - guest_req.resp_data = (__u64)&resp; - - /* - * Open the SEV guest device. - */ - errno = 0; - fd = open(SEV_GUEST_DEV, O_RDWR); - if (fd == -1) { - rc = errno; - perror("open"); - - goto out; - } - - /* - * Retrieve the SNP attestation report. - */ - errno = 0; - rc = ioctl(fd, SNP_GET_REPORT, &guest_req); - if (rc == -1) { - rc = errno; - perror("ioctl"); - fprintf(stderr, "errno is %u\n", errno); - fprintf(stderr, "firmware error %#llx\n", guest_req.fw_err); - fprintf(stderr, "report error %x\n", report_resp->status); - - goto out_close; - } - - /* - * Ensure that the report was successfully generated. - */ - if (report_resp->status != 0) { - fprintf(stderr, "firmware error %x\n", report_resp->status); - rc = report_resp->status; - - goto out_close; - } else if (report_resp->report_size > sizeof(*report)) { - fprintf(stderr, "report size is %u bytes (expected %lu)!\n", - report_resp->report_size, sizeof(*report)); - rc = EFBIG; - - goto out_close; - } - - /* - * Copy the report + certs data. - */ - memcpy(report, &report_resp->report, report_resp->report_size); - rc = EXIT_SUCCESS; - -out_close: - if (fd > 0) { - close(fd); - fd = -1; - } -out: - return rc; -} - -static int SNP_ATTEST_ERR(char *errmsg) -{ - printf("SNP ATTEST ERROR: %s\n", errmsg); - - return -1; -} - -/* - * String format an unformatted JSON string: - * - * For example, this string: - * "{\"test\":\"123\"}" - * - * Would become: - * "{"test":"123"}" - */ -static void json_fmt(char *str) -{ - char cpy[strlen(str)]; - size_t sz, cpy_idx; - - sz = strlen(str); - cpy_idx = 0; - - for (int i = 0; i < sz; i++) { - if (str[i] != '\\') - cpy[cpy_idx++] = str[i]; - } - cpy[cpy_idx] = '\0'; - - strcpy(str, cpy); -} diff --git a/init/tee/snp_attest.h b/init/tee/snp_attest.h deleted file mode 100644 index d923d9b76..000000000 --- a/init/tee/snp_attest.h +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -#ifndef _SNP_ATTEST -#define _SNP_ATTEST - -#include - -#include - -#define SEV_GUEST_DEV "/dev/sev-guest" - -/* - * Cryptographic signature (should be signed by the VCEK). - */ -struct signature { - uint8_t r[72]; - uint8_t s[72]; - uint8_t reserved[512 - 144]; -}; - -/* - * Structure containing the security version numbers of each component in the - * Trusted Computing Base (TCB) of the SNP firmware. - */ -union tcb_version { - struct { - uint8_t boot_loader; - uint8_t tee; - uint8_t reserved[4]; - uint8_t snp; - uint8_t microcode; - }; - uint64_t raw; -}; - -/* - * An array of certificates. Consult the AMD SEV GHCB document to understand how - * this table should be built and parsed. - */ -struct cert_table { - struct cert_table_entry { - uuid_t guid; - uint32_t offset; - uint32_t len; - } *entry; -}; - -/* - * SNP attestation report structure. Based off of the attestation report - * structure described in firmware version 1.52. - */ -struct snp_report { - uint32_t version; - uint32_t guest_svn; - uint64_t policy; - uint8_t family_id[16]; - uint8_t image_id[16]; - uint32_t vmpl; - uint32_t signature_algo; - union tcb_version current_tcb; - - /* - * TODO: Change to a "struct platform_info". - */ - uint64_t platform_info; - - uint32_t author_key_en : 1; - uint32_t _reserved_0 : 31; - uint32_t _reserved_1; - uint8_t report_data[64]; - uint8_t measurement[48]; - uint8_t host_data[32]; - uint8_t id_key_digest[48]; - uint8_t author_key_digest[48]; - uint8_t report_id[32]; - uint8_t report_id_ma[32]; - union tcb_version reported_tcb; - uint8_t _reserved_2[24]; - uint8_t chip_id[64]; - union tcb_version committed_tcb; - uint8_t current_build; - uint8_t current_minor; - uint8_t current_major; - uint8_t _reserved_3; - uint8_t committed_build; - uint8_t committed_minor; - uint8_t committed_major; - uint8_t _reserved_4; - union tcb_version launch_tcb; - uint8_t _reserved_5[168]; - struct signature signature; -}; - -/* - * Response from the SNP_GET_EXT_REPORT ioctl. - */ -struct msg_report_resp { - uint32_t status; - uint32_t report_size; - uint8_t reserved[0x20 - 0x8]; - struct snp_report report; -}; - -// snp_attest.c -int snp_attest(char *, char *, char *, char *); - -#endif /* _SNP_ATTEST */ From ca8148ad16f0e5b09b667a5908bcb3bea2f92d01 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Thu, 28 May 2026 16:22:02 -0400 Subject: [PATCH 15/24] init: remove KRUN_REMOVE_ROOT_DIR_IOCTL Port of cd8b2be0af316fe4c38b6749a888c56ca7699277. The temporary root directory hack has been replaced by NullFs, so the ioctl that cleaned it up is no longer needed. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/fs.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/init/src/fs.rs b/init/src/fs.rs index c68ccd9c6..5cf1626fb 100644 --- a/init/src/fs.rs +++ b/init/src/fs.rs @@ -8,8 +8,6 @@ use nix::errno::Errno; use nix::mount::{self, MsFlags}; use nix::unistd; -const KRUN_REMOVE_ROOT_DIR_IOCTL: libc::c_ulong = 0x7603; - /// Mount, treating EBUSY (already mounted) as success. fn mount_or_busy( src: Option<&str>, @@ -166,13 +164,6 @@ pub fn mount_block_root_device() -> anyhow::Result<()> { unistd::chdir("/newroot").context("chdir /newroot")?; - // Ask the virtiofs device to tear down the temporary root directory. - let fd = unsafe { libc::open(c"/".as_ptr().cast(), libc::O_RDONLY) }; - if fd >= 0 { - unsafe { libc::ioctl(fd, KRUN_REMOVE_ROOT_DIR_IOCTL as _) }; - unsafe { libc::close(fd) }; - } - mount::mount(Some("."), "/", None::<&str>, MsFlags::MS_MOVE, None::<&str>) .context("pivot root MS_MOVE")?; From 1cdd4f76119163084391e2f25b2d706fb49cb052 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Thu, 28 May 2026 16:23:17 -0400 Subject: [PATCH 16/24] init: set up a dummy network interface with TSI Port of 2593accd9b57da23912d1824216bcc53866c838d. When TSI is active, brings up dummy0 and assigns it 10.0.0.1/8 so applications that probe for network availability see a configured interface. Silently skips setup if the dummy driver is absent in the kernel. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- examples/Cargo.lock | 44 +++++++++++++------------- init/src/env.rs | 77 +++++++++++++++++++++++++++++++++++++++++++++ init/src/main.rs | 5 +++ 3 files changed, 104 insertions(+), 22 deletions(-) diff --git a/examples/Cargo.lock b/examples/Cargo.lock index c4c63539f..93ceff859 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -80,9 +80,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bindgen" @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.20.7" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6b04e07d8080154ed4ac03546d9a2b303cc2fe1901ba0b35b301516e289368" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" dependencies = [ "smallvec", "target-lexicon", @@ -240,9 +240,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "env_logger" @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -717,9 +717,9 @@ dependencies = [ [[package]] name = "kvm-bindings" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a537873e15e8daabb416667e606d9b0abc2a8fb9a45bd5853b888ae0ead82f9" +checksum = "4b3c06ff73c7ce03e780887ec2389d62d2a2a9ddf471ab05c2ff69207cd3f3b4" dependencies = [ "vmm-sys-util", ] @@ -732,15 +732,15 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memoffset" @@ -983,9 +983,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "termcolor" @@ -1042,9 +1042,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ "indexmap", "toml_datetime", @@ -1087,9 +1087,9 @@ checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "vmm-sys-util" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d21f366bf22bfba3e868349978766a965cbe628c323d58e026be80b8357ab789" +checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f" dependencies = [ "bitflags 1.3.2", "libc", @@ -1143,9 +1143,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] diff --git a/init/src/env.rs b/init/src/env.rs index e224ab9e6..edf1a0ff6 100644 --- a/init/src/env.rs +++ b/init/src/env.rs @@ -8,6 +8,8 @@ use std::os::fd::AsRawFd; #[cfg(target_os = "linux")] use std::ptr; +#[cfg(target_os = "linux")] +use nix::errno::Errno; #[cfg(target_os = "linux")] use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; @@ -66,6 +68,81 @@ fn setup_dhcp(iface: &str, sock: i32) { } } +/// Returns true if `tsi_hijack` appears in the kernel command line before any +/// `--` delimiter. Mirrors `tsi_enabled()` in init.c. +#[cfg(target_os = "linux")] +pub fn tsi_enabled() -> bool { + let Ok(cmdline) = std::fs::read_to_string("/proc/cmdline") else { + return false; + }; + cmdline + .split_whitespace() + .take_while(|tok| *tok != "--") + .any(|tok| tok == "tsi_hijack") +} + +/// Brings up `dummy0` and assigns it 203.0.113.1/24 (IANA TEST-NET-3) so +/// that applications probing for network availability see a configured +/// interface when TSI is in use. Silently succeeds when the dummy driver is +/// absent. +/// Mirrors `enable_dummy_interface()` in init.c. +#[cfg(target_os = "linux")] +pub fn enable_dummy_interface() { + use std::net::Ipv4Addr; + + let Ok(sock) = socket::socket( + AddressFamily::Inet, + SockType::Datagram, + SockFlag::empty(), + None, + ) else { + eprintln!("Warning: dummy interface socket failed"); + return; + }; + + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + let name = c"dummy0"; + unsafe { + ptr::copy_nonoverlapping( + name.as_ptr(), + ifr.ifr_name.as_mut_ptr(), + name.to_bytes_with_nul().len(), + ); + ifr.ifr_ifru.ifru_flags = libc::IFF_UP as libc::c_short; + } + + let ret = unsafe { libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFFLAGS as _, &ifr) }; + if ret < 0 { + if Errno::last() != Errno::ENODEV { + eprintln!("Warning: dummy interface up failed"); + } + return; + } + + // Set IP address to 203.0.113.1 (IANA TEST-NET-3). + let mut sin: libc::sockaddr_in = unsafe { mem::zeroed() }; + sin.sin_family = libc::AF_INET as libc::sa_family_t; + sin.sin_addr.s_addr = u32::from_ne_bytes(Ipv4Addr::new(203, 0, 113, 1).octets()); + unsafe { + ifr.ifr_ifru.ifru_addr = *(&sin as *const libc::sockaddr_in as *const libc::sockaddr); + if libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFADDR as _, &ifr) < 0 { + eprintln!("Warning: dummy interface address failed"); + return; + } + } + + // Set netmask to 255.255.255.0. + let mut sin: libc::sockaddr_in = unsafe { mem::zeroed() }; + sin.sin_family = libc::AF_INET as libc::sa_family_t; + sin.sin_addr.s_addr = u32::from_ne_bytes(Ipv4Addr::new(255, 255, 255, 0).octets()); + unsafe { + ifr.ifr_ifru.ifru_netmask = *(&sin as *const libc::sockaddr_in as *const libc::sockaddr); + if libc::ioctl(sock.as_raw_fd(), libc::SIOCSIFNETMASK as _, &ifr) < 0 { + eprintln!("Warning: dummy interface mask failed"); + } + } +} + pub fn apply_hostname() { let hostname = env::var("HOSTNAME").unwrap_or_else(|_| "localhost".into()); let _ = nix::unistd::sethostname(&hostname); diff --git a/init/src/main.rs b/init/src/main.rs index ebf1a98a6..66d7b5e24 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -40,6 +40,11 @@ fn main() -> anyhow::Result<()> { "eth0", ); + #[cfg(target_os = "linux")] + if env::tsi_enabled() { + env::enable_dummy_interface(); + } + #[cfg(target_os = "freebsd")] let iso_mounted = std::env::var("KRUN_CONFIG").is_err() && freebsd::mount_config_iso(); From 4a43a81d7370418e27093e577f4a386844c6d1af Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:30:11 -0400 Subject: [PATCH 17/24] init: bring up loopback interface on FreeBSD The non-Linux setup_network() stub was empty, so the lo interface was never raised inside FreeBSD guests. The C init unconditionally brought up lo on all platforms (the #if __linux__ guard covered only the DHCP block, not lo setup). Use nix::sys::socket to open an AF_INET/SOCK_DGRAM socket and issue SIOCSIFFLAGS / IFF_UP, matching the C behaviour. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/env.rs | 43 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/init/src/env.rs b/init/src/env.rs index edf1a0ff6..5c3943b0e 100644 --- a/init/src/env.rs +++ b/init/src/env.rs @@ -1,16 +1,16 @@ use std::env; #[cfg(target_os = "linux")] use std::ffi::CString; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::mem; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::os::fd::AsRawFd; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "freebsd"))] use std::ptr; #[cfg(target_os = "linux")] use nix::errno::Errno; -#[cfg(target_os = "linux")] +#[cfg(any(target_os = "linux", target_os = "freebsd"))] use nix::sys::socket::{self, AddressFamily, SockFlag, SockType}; #[cfg(target_os = "linux")] @@ -38,7 +38,40 @@ pub fn setup_network(iface: &str) { setup_dhcp(iface, sock.as_raw_fd()); } -#[cfg(not(target_os = "linux"))] +#[cfg(target_os = "freebsd")] +pub fn setup_network() { + // Bring up the loopback interface on FreeBSD. + // + // libc does not export SIOCSIFFLAGS for FreeBSD targets; the ioctl number + // is _IOW('i', 16, struct ifreq) = 0x80206910 on both aarch64 and x86_64 + // (sizeof(struct ifreq) == 32 on both). + const SIOCSIFFLAGS: libc::c_ulong = 0x80206910; + + let Ok(sock) = socket::socket( + AddressFamily::Inet, + SockType::Datagram, + SockFlag::empty(), + None, + ) else { + return; + }; + let mut ifr: libc::ifreq = unsafe { mem::zeroed() }; + let lo = c"lo"; + unsafe { + ptr::copy_nonoverlapping( + lo.as_ptr(), + ifr.ifr_name.as_mut_ptr(), + lo.to_bytes_with_nul().len(), + ); + // On FreeBSD, ifru_flags is [c_short; 2]; index 0 holds the flags value. + ifr.ifr_ifru.ifru_flags[0] |= libc::IFF_UP as libc::c_short; + libc::ioctl(sock.as_raw_fd(), SIOCSIFFLAGS as _, &ifr); + } +} + +// On macOS host builds and other non-Linux, non-FreeBSD platforms the +// loopback interface is already configured by the OS. +#[cfg(all(not(target_os = "linux"), not(target_os = "freebsd")))] pub fn setup_network() {} #[cfg(target_os = "linux")] From baeb715171ebf9fea577c62e8459d8493d11c200 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:32:50 -0400 Subject: [PATCH 18/24] init: add Entrypoint field and Docker-format config key aliases The C init accepted "Cmd", "Env", "WorkingDir"/"Cwd", and "Entrypoint" keys via case-insensitive comparison; the Rust port only handled OCI runtime-spec keys ("args", "env", "cwd" inside "process"). Add serde aliases so RawConfig's flat fields also accept the Docker image config capitalisation: - "Cmd" aliases "args" - "Env" aliases "env" - "WorkingDir"/"Cwd" alias "cwd" - new "Entrypoint" field (top-level, Docker format only) When Entrypoint is present it is prepended to the resolved args vector, matching the C init's concat_entrypoint_argv() behaviour. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/config.rs | 117 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/init/src/config.rs b/init/src/config.rs index 0ede53b4b..4797798f2 100644 --- a/init/src/config.rs +++ b/init/src/config.rs @@ -33,11 +33,18 @@ struct Mount { #[derive(Deserialize, Default)] struct RawConfig { process: Option, - // Flat format: "args"/"env"/"cwd" at the top level (used by simple configs and tests). - // Only consulted when "process" is absent. + // OCI runtime-spec flat format ("args"/"env"/"cwd") and Docker image config + // aliases ("Cmd"/"Env"/"WorkingDir"/"Cwd") are all accepted. Only + // consulted when "process" is absent. + #[serde(alias = "Cmd")] args: Option>, + #[serde(alias = "Env")] env: Option>, + #[serde(alias = "WorkingDir", alias = "Cwd")] cwd: Option, + // Docker image config: Entrypoint is prepended to args/Cmd to form argv. + #[serde(rename = "Entrypoint")] + entrypoint: Option>, #[cfg(target_os = "linux")] mounts: Option>, } @@ -63,6 +70,9 @@ pub fn load(#[cfg(target_os = "linux")] is_mount_point: impl Fn(&str) -> bool) - return Config::default(); }; + // Extract Entrypoint before partially moving raw into process below. + let entrypoint = raw.entrypoint.filter(|v| !v.is_empty()); + let process = raw.process.unwrap_or(ProcessConfig { args: raw.args, env: raw.env, @@ -81,7 +91,17 @@ pub fn load(#[cfg(target_os = "linux")] is_mount_point: impl Fn(&str) -> bool) - } } - let argv = process.args.filter(|v| !v.is_empty()); + // Prepend Entrypoint (Docker image config) to args when both are present. + let base_args = process.args.filter(|v| !v.is_empty()); + let argv = match entrypoint { + Some(mut ep) => { + if let Some(args) = base_args { + ep.extend(args); + } + Some(ep) + } + None => base_args, + }; let workdir = process.cwd; // Find the first tmpfs mount whose destination is not already mounted. @@ -109,3 +129,94 @@ fn parse_file(path: &str) -> Result { let data = fs::read(path).with_context(|| format!("read {path}"))?; serde_json::from_slice(&data).with_context(|| format!("parse {path}")) } + +#[cfg(test)] +mod tests { + use super::*; + + fn raw(json: &str) -> RawConfig { + serde_json::from_str(json).expect("parse") + } + + #[test] + fn cmd_alias_maps_to_args() { + let r = raw(r#"{"Cmd": ["bash", "-c", "echo hi"]}"#); + assert_eq!( + r.args, + Some(vec!["bash".into(), "-c".into(), "echo hi".into()]) + ); + } + + #[test] + fn env_alias_maps_to_env() { + let r = raw(r#"{"Env": ["FOO=bar", "BAZ=qux"]}"#); + assert_eq!(r.env, Some(vec!["FOO=bar".into(), "BAZ=qux".into()])); + } + + #[test] + fn working_dir_alias_maps_to_cwd() { + let r = raw(r#"{"WorkingDir": "/app"}"#); + assert_eq!(r.cwd, Some("/app".into())); + } + + #[test] + fn cwd_alias_maps_to_cwd() { + let r = raw(r#"{"Cwd": "/work"}"#); + assert_eq!(r.cwd, Some("/work".into())); + } + + #[test] + fn entrypoint_is_parsed() { + let r = raw(r#"{"Entrypoint": ["/ep.sh", "--flag"]}"#); + assert_eq!(r.entrypoint, Some(vec!["/ep.sh".into(), "--flag".into()])); + } + + #[test] + fn entrypoint_prepended_to_args() { + // Simulate what load() does to merge entrypoint + base_args. + let ep = Some(vec!["/ep.sh".to_string()]); + let base = Some(vec!["nginx".to_string()]); + let argv = match ep.filter(|v| !v.is_empty()) { + Some(mut e) => { + if let Some(a) = base { + e.extend(a); + } + Some(e) + } + None => base, + }; + assert_eq!(argv, Some(vec!["/ep.sh".into(), "nginx".into()])); + } + + #[test] + fn entrypoint_alone_when_no_args() { + let ep = Some(vec!["/ep.sh".to_string()]); + let base: Option> = None; + let argv = match ep.filter(|v| !v.is_empty()) { + Some(mut e) => { + if let Some(a) = base { + e.extend(a); + } + Some(e) + } + None => base, + }; + assert_eq!(argv, Some(vec!["/ep.sh".into()])); + } + + #[test] + fn args_used_when_no_entrypoint() { + let ep: Option> = None; + let base = Some(vec!["myapp".to_string()]); + let argv = match ep.filter(|v| !v.is_empty()) { + Some(mut e) => { + if let Some(a) = base { + e.extend(a); + } + Some(e) + } + None => base, + }; + assert_eq!(argv, Some(vec!["myapp".into()])); + } +} From 3b90e52e5ef8af614e3d381abdd4923ef952539a Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:34:23 -0400 Subject: [PATCH 19/24] init: accept colon-separated rlimits and strip outer quotes Two issues with apply_rlimits(): 1. The C init parsed KRUN_RLIMITS with strtoull() and a single-char skip, so any separator character between ID, CUR, and MAX worked (e.g. "7:1024:4096"). The Rust code required "ID=CUR:MAX" and silently skipped entries using the historical colon-only format. 2. krun_set_rlimits() wraps the entire value in double-quotes (format!("\"{}\"", ...)), so the env var received by init is "\"7=1024:4096\"". Neither the old Rust nor the C parser handled this correctly. Fix both by extracting parse_rlimit_entry() which strips outer '"' chars and splits on the first two occurrences of '=' or ':' via splitn(3). Both formats and the quoted form now parse correctly. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/env.rs | 79 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/init/src/env.rs b/init/src/env.rs index 5c3943b0e..ec6bd0990 100644 --- a/init/src/env.rs +++ b/init/src/env.rs @@ -194,24 +194,65 @@ pub fn apply_rlimits() { let Ok(rlimits) = env::var("KRUN_RLIMITS") else { return; }; - for item in rlimits.split(',') { - let Some((id_s, rest)) = item.split_once('=') else { - continue; - }; - let Some((cur_s, max_s)) = rest.split_once(':') else { - continue; - }; - let (Ok(id), Ok(cur), Ok(max)) = ( - id_s.parse::(), - cur_s.parse::(), - max_s.parse::(), - ) else { - continue; - }; - let rlim = libc::rlimit { - rlim_cur: cur, - rlim_max: max, - }; - unsafe { libc::setrlimit(id as _, &rlim) }; + // krun_set_rlimits() wraps the value in outer double-quotes; strip them. + let s = rlimits.trim_matches('"'); + for item in s.split(',') { + if let Some((id, cur, max)) = parse_rlimit_entry(item) { + let rlim = libc::rlimit { + rlim_cur: cur, + rlim_max: max, + }; + unsafe { libc::setrlimit(id as _, &rlim) }; + } + } +} + +// Accept both "ID=CUR:MAX" (Rust format) and "ID:CUR:MAX" (C format) by +// splitting on the first two occurrences of either '=' or ':'. +fn parse_rlimit_entry(item: &str) -> Option<(u32, libc::rlim_t, libc::rlim_t)> { + let item = item.trim_matches('"'); + let parts: Vec<&str> = item.splitn(3, ['=', ':']).collect(); + let [id, cur, max] = parts.as_slice() else { + return None; + }; + let id = id.parse::().ok()?; + let cur = cur.parse::().ok()?; + let max = max.parse::().ok()?; + Some((id, cur, max)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rlimit_equals_format() { + assert_eq!(parse_rlimit_entry("7=1024:4096"), Some((7, 1024, 4096))); + } + + #[test] + fn rlimit_colon_format() { + assert_eq!(parse_rlimit_entry("7:1024:4096"), Some((7, 1024, 4096))); + } + + #[test] + fn rlimit_outer_quotes_stripped() { + assert_eq!(parse_rlimit_entry("\"7=1024:4096\""), Some((7, 1024, 4096))); + } + + #[test] + fn rlimit_trailing_quote_stripped() { + // Last item in a quoted list has a trailing '"'. + assert_eq!(parse_rlimit_entry("11=512:1024\""), Some((11, 512, 1024))); + } + + #[test] + fn rlimit_too_few_parts_is_none() { + assert_eq!(parse_rlimit_entry("7:1024"), None); + } + + #[test] + fn rlimit_non_numeric_is_none() { + assert_eq!(parse_rlimit_entry("invalid"), None); } } From 91ca6288eaa520ab4d8f188172990328b4c56a9c Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:35:14 -0400 Subject: [PATCH 20/24] init: document argv fallback divergence from C init The C init replaced argv[0] with "/bin/sh" when neither KRUN_INIT nor a config file was present, forwarding remaining cmdline tokens as shell arguments. The Rust init instead treats proc_args[1] as the executable directly. Add a comment explaining the rationale: callers that omit both KRUN_INIT and a config file intend the cmdline argument to be the command, not a shell script path, making the Rust behaviour more intuitive. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/main.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/init/src/main.rs b/init/src/main.rs index 66d7b5e24..58659a2b1 100644 --- a/init/src/main.rs +++ b/init/src/main.rs @@ -85,6 +85,17 @@ fn main() -> anyhow::Result<()> { v } else if proc_args.len() > 1 { // No KRUN_INIT and no config: treat proc_args[1..] as the command. + // + // Intentional divergence from the C init: the C init substituted + // argv[0] with "/bin/sh" and forwarded the remaining args as shell + // arguments ("/bin/sh arg1 arg2 ..."). That made sense when krun + // callers relied on the shell to interpret cmdline tokens, but it + // means proc_args[1] is treated as a script path rather than a binary. + // + // The Rust init treats proc_args[1] as the executable directly. The + // typical krun caller that omits both KRUN_INIT and a config file + // intends the cmdline argument to be the command, not a shell script, + // so this behaviour is more useful and less surprising. proc_args.into_iter().skip(1).collect() } else { vec!["/bin/sh".to_string()] From 6bca7dae97c9aa199d2393148726b02fbada08bd Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:41:26 -0400 Subject: [PATCH 21/24] init: fork timesync child process instead of spawning a thread A thread is destroyed when the parent calls execvp() (in PID1 mode). The C init ran clock_worker() in a forked child process, which survives exec. Match that behaviour: create the vsock socket, fork, and run the recv loop in the child; the parent closes its copy of the socket and returns immediately. Also switch to nix wrappers throughout: socket::socket(), socket::recv(), time::clock_gettime(), and time::clock_settime() replace the equivalent unsafe libc calls. Add the nix "time" feature to support the clock functions. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/Cargo.toml | 2 +- init/src/timesync.rs | 94 +++++++++++++++++++++++++++----------------- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/init/Cargo.toml b/init/Cargo.toml index 1748ddfeb..43dfc0374 100644 --- a/init/Cargo.toml +++ b/init/Cargo.toml @@ -18,6 +18,6 @@ timesync = [] [dependencies] anyhow = "1" libc = "0.2" -nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "net", "process", "reboot", "resource", "signal", "socket", "term", "uio"] } +nix = { version = "0.30", features = ["fs", "hostname", "ioctl", "mount", "net", "process", "reboot", "resource", "signal", "socket", "term", "time", "uio"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/init/src/timesync.rs b/init/src/timesync.rs index b97d53377..3c1f73aee 100644 --- a/init/src/timesync.rs +++ b/init/src/timesync.rs @@ -1,50 +1,70 @@ -use std::mem; - -use nix::sys::socket::{self, VsockAddr}; +use nix::sys::socket::{self, AddressFamily, MsgFlags, SockFlag, SockType, VsockAddr}; +use nix::sys::time::TimeSpec; +use nix::time::{self, ClockId}; +use nix::unistd::{self, ForkResult}; +use std::os::fd::AsRawFd; const TSYNC_PORT: u32 = 123; const NANOS_IN_SECOND: u64 = 1_000_000_000; const DELTA_SYNC: u64 = 100_000_000; // 100ms — don't bother adjusting for smaller drifts +/// Spawn a child process that synchronises the guest clock from the host. +/// +/// Uses fork() rather than a thread so the sync loop survives when the parent +/// calls execvp() in PID1 mode (a thread would be destroyed by exec; a +/// separate process is not). Safe to call here because run() is invoked +/// before any other threads exist in the process. pub fn run() { - let sock = unsafe { libc::socket(libc::AF_VSOCK, libc::SOCK_DGRAM, 0) }; - if sock < 0 { + let Ok(sock) = socket::socket( + AddressFamily::Vsock, + SockType::Datagram, + SockFlag::empty(), + None, + ) else { return; - } + }; let addr = VsockAddr::new(libc::VMADDR_CID_ANY, TSYNC_PORT); - if socket::bind(sock, &addr).is_err() { - unsafe { libc::close(sock) }; + if socket::bind(sock.as_raw_fd(), &addr).is_err() { return; } - std::thread::Builder::new() - .name("timesync".into()) - .spawn(move || { - loop { - let mut buf = [0u8; 8]; - let n = unsafe { libc::recv(sock, buf.as_mut_ptr() as *mut _, buf.len(), 0) }; - if n < 0 { - break; - } - if n != 8 { - continue; - } - - let host_ns = u64::from_le_bytes(buf); - - let mut guest_ts: libc::timespec = unsafe { mem::zeroed() }; - unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, &mut guest_ts) }; - let guest_ns = guest_ts.tv_sec as u64 * NANOS_IN_SECOND + guest_ts.tv_nsec as u64; - - if host_ns.abs_diff(guest_ns) > DELTA_SYNC { - let host_ts = libc::timespec { - tv_sec: (host_ns / NANOS_IN_SECOND) as libc::time_t, - tv_nsec: (host_ns % NANOS_IN_SECOND) as libc::c_long, - }; - unsafe { libc::clock_settime(libc::CLOCK_REALTIME, &host_ts) }; - } - } - }) - .unwrap(); + match unsafe { unistd::fork() } { + Ok(ForkResult::Child) => { + // Child: run the sync loop until the socket errors, then exit. + clock_worker(sock.as_raw_fd()); + unsafe { libc::_exit(1) }; + } + _ => { + // Parent or fork error: sock drops here, closing the parent's copy. + // The child retains its inherited fd. + } + } +} + +fn clock_worker(sock: libc::c_int) { + loop { + let mut buf = [0u8; 8]; + let Ok(n) = socket::recv(sock, &mut buf, MsgFlags::empty()) else { + break; + }; + if n != 8 { + continue; + } + + let host_ns = u64::from_le_bytes(buf); + + let Ok(guest_ts) = time::clock_gettime(ClockId::CLOCK_REALTIME) else { + break; + }; + let guest_ns = guest_ts.tv_sec() as u64 * NANOS_IN_SECOND + guest_ts.tv_nsec() as u64; + + if host_ns.abs_diff(guest_ns) > DELTA_SYNC { + let host_ts = TimeSpec::new( + (host_ns / NANOS_IN_SECOND) as libc::time_t, + (host_ns % NANOS_IN_SECOND) as libc::c_long, + ); + let _ = time::clock_settime(ClockId::CLOCK_REALTIME, host_ts); + } + } } From 81f63ef70da34d4602dc71a78f7999bccd0d1526 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:41:56 -0400 Subject: [PATCH 22/24] init: exit 125 when setup_redirects cannot open virtio-ports The C init called exit(125) if setup_redirects() returned a negative value (which it did when opendir("/sys/class/virtio-ports") failed). The Rust port returned silently, letting the workload run with unredirected stdio and no diagnostic. Match the C behaviour: print an error and exit(125) so callers get a visible signal that the redirects could not be set up. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/exec.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/init/src/exec.rs b/init/src/exec.rs index e54f4e2d7..03b196f3d 100644 --- a/init/src/exec.rs +++ b/init/src/exec.rs @@ -31,7 +31,8 @@ const VIRTIOFS_MAGIC: libc::c_long = 0x6573_5546; #[cfg(target_os = "linux")] pub fn setup_redirects() { let Ok(ports_dir) = fs::read_dir("/sys/class/virtio-ports") else { - return; + eprintln!("Unable to open virtio-ports directory"); + process::exit(125); }; for entry in ports_dir.flatten() { let name_path = entry.path().join("name"); From 350de653fdb6207016ab2167de3261f2e0dc9650 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Tue, 2 Jun 2026 11:43:42 -0400 Subject: [PATCH 23/24] init: match KRUN_INIT_PID1 by prefix rather than exact equality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C init checked *env_init_pid1 == '1' (first-byte comparison), accepting any value starting with '1' — including "10" or "1\n" (which can appear when the value originates from a file read). The Rust port used exact equality with "1", silently ignoring those variants. Replace with is_ok_and(|v| v.starts_with('1')). Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/exec.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/init/src/exec.rs b/init/src/exec.rs index 03b196f3d..dacecf48d 100644 --- a/init/src/exec.rs +++ b/init/src/exec.rs @@ -80,7 +80,10 @@ pub fn set_exit_code(code: i32) { pub fn set_exit_code(_code: i32) {} pub fn run_workload(argv: &[String]) -> ! { - if env::var("KRUN_INIT_PID1") == Ok("1".to_owned()) { + // Match the C init which checked *env_init_pid1 == '1' (first-byte prefix), + // accepting "1", "10", "1\n", etc. Exact equality with "1" would reject + // values that arrive with a trailing newline. + if env::var("KRUN_INIT_PID1").is_ok_and(|v| v.starts_with('1')) { exec_workload(argv); } From 1ccddcacb648f135f9d0d8fea74796ba37a88258 Mon Sep 17 00:00:00 2001 From: Jake Correnti Date: Wed, 3 Jun 2026 11:23:24 -0400 Subject: [PATCH 24/24] init/dhcp: only overwrite resolv.conf with DNS Port of upstream commit 378e5249 ("init/dhcp: only overwrite resolv.conf with DNS"). Only write /etc/resolv.conf when the DHCP server provides nameservers, preserving any pre-existing content. Assisted-by: Claude Code: claude-sonnet-4-6 Signed-off-by: Jake Correnti --- init/src/dhcp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/init/src/dhcp.rs b/init/src/dhcp.rs index f76daf7ae..439d1c2f3 100644 --- a/init/src/dhcp.rs +++ b/init/src/dhcp.rs @@ -449,7 +449,9 @@ fn handle_dhcp_ack(nl_sock: libc::c_int, iface_index: i32, response: &[u8]) -> a } } - if let Err(e) = std::fs::write("/etc/resolv.conf", &resolv_conf) { + if !resolv_conf.is_empty() + && let Err(e) = std::fs::write("/etc/resolv.conf", &resolv_conf) + { eprintln!("Warning: couldn't write /etc/resolv.conf: {e}"); }