From ff651c5955b84d15209e486bee6327bbbe663cb7 Mon Sep 17 00:00:00 2001 From: Luca Stocchi Date: Tue, 29 Sep 2026 11:03:57 +0200 Subject: [PATCH] Add support for Windows by using WHP with libkrun 2.0 This PR adds experimental support for launching virtual machines on Windows hosts using libkrun 2.0 and the Windows Hypervisor Platform (WHP). 1. Added build.rs link instructions using LIBKRUN_LIB_DIR for krun.dll.lib lookup on Windows and a build.ps1 helper script for local development on Windows. 2. Restricted timesync module to macOS only 3. Splitted context/virtio/status to have platform-specific implementations. The macOS part remained unchanged. This patch misses the firmware for x86_64 systems. It depends on https://github.com/slp/edk2/pull/2 It will be added in following PR Assisted-by: OpenCode: GPT-5.6 Terra Signed-off-by: Luca Stocchi --- Cargo.lock | 339 ++++++++++++++++++---------- Cargo.toml | 8 + README.md | 10 +- build.ps1 | 66 ++++++ build.rs | 11 +- docs/usage.md | 77 ++++++- src/context/mod.rs | 11 + src/{context.rs => context/unix.rs} | 6 +- src/context/windows.rs | 279 +++++++++++++++++++++++ src/main.rs | 1 + src/status/mod.rs | 178 +++++++++++++++ src/{status.rs => status/unix.rs} | 212 ++--------------- src/{virtio.rs => virtio/mod.rs} | 278 ++--------------------- src/virtio/unix.rs | 244 ++++++++++++++++++++ src/virtio/windows.rs | 158 +++++++++++++ 15 files changed, 1305 insertions(+), 573 deletions(-) create mode 100644 build.ps1 create mode 100644 src/context/mod.rs rename src/{context.rs => context/unix.rs} (98%) create mode 100644 src/context/windows.rs create mode 100644 src/status/mod.rs rename src/{status.rs => status/unix.rs} (68%) rename src/{virtio.rs => virtio/mod.rs} (64%) create mode 100644 src/virtio/unix.rs create mode 100644 src/virtio/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 7ac8040..852cb3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,72 +4,74 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.11" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", + "once_cell_polyfill", "windows-sys", ] [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "bitflags" @@ -79,9 +81,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block2" @@ -93,25 +95,22 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.0.83" +name = "cfg-if" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "cfg-if" -version = "1.0.0" +name = "cfg_aliases" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "clap" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -119,9 +118,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -131,27 +130,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core-foundation" @@ -171,9 +170,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -181,41 +180,72 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "block2", "objc2", ] [[package]] name = "either" -version = "1.13.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "env_filter" -version = "0.1.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" dependencies = [ "log", "regex", @@ -223,9 +253,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" dependencies = [ "anstream", "anstyle", @@ -236,9 +266,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "httparse" @@ -246,6 +276,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" @@ -254,26 +290,39 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.10" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6" +checksum = "0ab1baf72f08796de0260609515130699b890ac25f30e610ad894bc5856cafdb" dependencies = [ + "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt", + "log", ] [[package]] name = "jiff-static" -version = "0.2.10" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254" +checksum = "378268a1116ad67ae6228701118ac9f491d78fda38a40a1f1a9e1348de6f7212" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -292,25 +341,26 @@ dependencies = [ "serde", "serde_json", "sysinfo", + "windows-sys", ] [[package]] name = "libc" -version = "0.2.153" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "log" -version = "0.4.27" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mac_address" -version = "1.1.5" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4863ee94f19ed315bf3bc00299338d857d4b5bc856af375cc97d237382ad3856" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ "nix", "winapi", @@ -318,46 +368,46 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "nix" -version = "0.23.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 1.3.2", - "cc", + "bitflags 2.13.2", "cfg-if", + "cfg_aliases", "libc", "memoffset", ] [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -368,7 +418,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "dispatch2", "objc2", ] @@ -385,7 +435,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.13.2", "block2", "dispatch2", "libc", @@ -393,44 +443,50 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" dependencies = [ "portable-atomic", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -438,9 +494,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -448,9 +504,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -460,9 +516,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -471,15 +527,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -487,29 +543,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.6", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -520,15 +576,26 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "2.0.101" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -549,17 +616,37 @@ dependencies = [ "windows", ] +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "winapi" @@ -613,7 +700,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -624,9 +711,15 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.1.2" @@ -638,11 +731,11 @@ dependencies = [ [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] @@ -711,6 +804,6 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index abd8d9f..4088c1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,16 @@ sysinfo = "0.31.4" log = "0.4.0" env_logger = "0.11.8" regex = "1.11.1" +[target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10.1" objc2-io-kit = "0.3.2" httparse = "1.10.1" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { version = "0.61.2", features = [ + "Win32_Foundation", + "Win32_Networking_WinSock", + "Win32_System_Console", +] } diff --git a/README.md b/README.md index 155fa9f..2d883fe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ `krunkit` is a tool to launch configurable virtual machines using the [libkrun](https://github.com/containers/libkrun) platform. > [!IMPORTANT] -> krunkit is only supported on hosts running macOS 14 or newer. +> The established krunkit path supports macOS 14 or newer. Experimental Windows support uses libkrun 2.0 and WHP. ## Installation @@ -39,6 +39,14 @@ make sudo make install ``` +On Windows, `LIBKRUN_LIB_DIR` must point to the directory containing `krun.dll.lib` when invoking Cargo. +Place the matching `krun.dll` beside `krunkit.exe` to run it. For local development, `build.ps1` builds a +libkrun 2.0 tree and copies its DLL beside the executable: + +```powershell +.\build.ps1 -LibkrunSource C:\src\libkrun +``` + To build with `libkrun` from *Homebrew* or *MacPorts* use the appropriate `PREFIX`: ``` diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..4c0f337 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,66 @@ +param ( + [ValidateSet("all", "debug", "install", "clean")] + [string]$Task = "all", + + [string]$Prefix = $env:PREFIX, + + [string]$LibkrunSource = $env:LIBKRUN_SOURCE_DIR +) + +$ErrorActionPreference = "Stop" +$BinName = "krunkit.exe" +$ReleasePath = "target\release\$BinName" +$DebugPath = "target\debug\$BinName" + +function Build-Krunkit([string]$Profile) { + if ([string]::IsNullOrWhiteSpace($LibkrunSource)) { + throw "Specify -LibkrunSource or set LIBKRUN_SOURCE_DIR" + } + + $ReleaseArg = @() + if ($Profile -eq "release") { + $ReleaseArg += "--release" + } + cargo build --manifest-path (Join-Path $LibkrunSource "Cargo.toml") -p libkrun --features "ffi blk net" @ReleaseArg + if ($LASTEXITCODE -ne 0) { throw "libkrun build failed" } + + $env:LIBKRUN_LIB_DIR = Join-Path $LibkrunSource "target\$Profile" + cargo build @ReleaseArg + if ($LASTEXITCODE -ne 0) { throw "krunkit build failed" } + + Copy-Item -Path (Join-Path $env:LIBKRUN_LIB_DIR "krun.dll") -Destination "target\$Profile\krun.dll" -Force +} + +switch ($Task) { + "all" { + Build-Krunkit "release" + } + + "debug" { + Build-Krunkit "debug" + } + + "install" { + if ([string]::IsNullOrWhiteSpace($Prefix)) { + throw "Specify -Prefix or set PREFIX" + } + + if (-not (Test-Path $ReleasePath)) { + Build-Krunkit "release" + } + + $TargetBinDir = Join-Path $Prefix "bin" + + if (-not (Test-Path $TargetBinDir)) { + New-Item -ItemType Directory -Path $TargetBinDir -Force | Out-Null + } + + Copy-Item -Path $ReleasePath -Destination $TargetBinDir -Force + Copy-Item -Path "target\release\krun.dll" -Destination $TargetBinDir -Force + Write-Host "Successfully installed to: $TargetBinDir" -ForegroundColor Green + } + + "clean" { + cargo clean + } +} diff --git a/build.rs b/build.rs index 73247a9..cc35b3d 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,4 @@ -// SDPX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 fn main() { #[cfg(target_os = "macos")] @@ -11,4 +11,13 @@ fn main() { println!("cargo:rerun-if-env-changed=PREFIX"); } + + #[cfg(target_os = "windows")] + { + let lib_dir = std::env::var("LIBKRUN_LIB_DIR") + .expect("LIBKRUN_LIB_DIR must point to the directory containing krun.dll.lib"); + println!("cargo:rustc-link-search={lib_dir}"); + + println!("cargo:rerun-if-env-changed=LIBKRUN_LIB_DIR"); + } } diff --git a/docs/usage.md b/docs/usage.md index 3ea2e2b..fb34e6e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,7 +1,8 @@ # krunkit Command Line -`krunkit` can launch configurable virtual machines using macOS's hypervisor framework and the `libkrun` virtual -machine monitor library. The `libkrun` virtual machine configuration can be specified from command line arguments. +`krunkit` can launch configurable virtual machines using macOS's hypervisor framework or Windows Hypervisor +Platform and the `libkrun` virtual machine monitor library. The `libkrun` virtual machine configuration can be +specified from command line arguments. Specifying a virtual machine's vCPU and RAM allocation is required. Adding devices is optional, yet most workloads will require a root disk to be useful. @@ -64,11 +65,9 @@ This configures a virtual machine to use two vCPUs and 2048 MiB of RAM: --cpus 2 --memory 2048 ``` -## Bootloader Configuration - ### EFI bootloader -`--bootloader efi` allows booting a disk image using EFI, which removes the need for providing external kernel/initrd/... jthe disk image bootloader will be started by the EFI firmware, which will in turn know which kernel it should be booting. +`--bootloader efi` allows booting a disk image using EFI, which removes the need for providing external kernel/initrd/... the disk image bootloader will be started by the EFI firmware, which will in turn know which kernel it should be booting. #### Arguments - `variable-store`: path to a file which EFI can use to store its variables @@ -271,3 +270,71 @@ The table below provides some data on how offloading effects the gvproxy and vmn | gvproxy | krunkit | false | 1.47 Gbits/s | 2.58 Gbits/s | | gvproxy | vfkit | false | 1.43 Gbits/s | 2.84 Gbits/s | +## Bootloader Configuration on Windows + +On Windows, krunkit boots `edk2/OVMF.fd` by default. Use `--firmware-path PATH` to select another UEFI image. + +The Windows path connects `ttyS0` to the host terminal. It supports raw and QCOW2 `virtio-blk` images, +path-based `virtio-net,type=unixstream` devices, and `virtio-vsock` Unix socket mappings. + +### Windows Limitations + +On Windows, only the following devices can be specified with `--device`: + +- `virtio-blk` +- `virtio-net,type=unixstream,path=...`; `unixgram`, `unixSocketPath`, and `fd` backends are unsupported. +- `virtio-vsock` +- `virtio-fs` (requires `permissionSemantics=complete`; `simplified` is not supported on Windows) + +`virtio-serial`, `virtio-gpu`, and `virtio-input` are unsupported. `virtio-rng` is added +automatically, but must not be specified with `--device`. + +The RESTful service (`--restful-uri`), `--timesync`, and nested virtualization (`--nested`) are not +supported on Windows. The default console is connected directly to the host terminal; use it instead of a +`virtio-serial` device for guest text output. + +```powershell +krunkit.exe --cpus 2 --memory 2G ` + --firmware-path C:\vm\OVMF.fd ` + --device virtio-blk,path=C:\vm\disk.raw,format=raw +``` + +### SSH on Windows + +Start gvproxy in a separate terminal. Its default virtual network assigns the guest `192.168.127.2`, and +`-ssh-port 2222` forwards host port 2222 to guest port 22: + +```powershell +gvproxy.exe -listen-qemu unix://C:/vm/network.sock -ssh-port 2222 +``` + +Add the matching network device when starting krunkit: + +```powershell +krunkit.exe --cpus 2 --memory 2G ` + --firmware-path C:\vm\OVMF.fd ` + --device virtio-blk,path=C:\vm\rootfs.raw,format=raw ` + --device virtio-net,type=unixstream,path=C:\vm\network.sock,mac=5a:94:ef:e4:0c:ee,offloading=true +``` + +The guest must run DHCP on the virtio network interface and have `sshd` listening on port 22. Connect with: + +```powershell +ssh.exe -p 2222 USER@127.0.0.1 +``` + +### vsock on Windows + +The default `listen` action handles guest-initiated connections: libkrun connects guest port 1024 to an existing +host Unix socket. + +```powershell +--device virtio-vsock,port=1024,socketURL=C:\vm\guest-to-host.sock,listen +``` + +Use `connect` for host-initiated connections. Libkrun listens at the host socket path and forwards accepted +connections to a service listening on guest CID 3, port 1024. + +```powershell +--device virtio-vsock,port=1024,socketURL=C:\vm\host-to-guest.sock,connect +``` diff --git a/src/context/mod.rs b/src/context/mod.rs new file mode 100644 index 0000000..8b6ea39 --- /dev/null +++ b/src/context/mod.rs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(not(windows))] +mod unix; +#[cfg(windows)] +mod windows; + +#[cfg(not(windows))] +pub use unix::KrunContext; +#[cfg(windows)] +pub use windows::KrunContext; diff --git a/src/context.rs b/src/context/unix.rs similarity index 98% rename from src/context.rs rename to src/context/unix.rs index eed1a6e..5a43734 100644 --- a/src/context.rs +++ b/src/context/unix.rs @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 -use super::*; - use crate::{ + cmdline::Args, status::{get_shutdown_eventfd, status_listener, RestfulUri}, virtio::KrunContextSet, }; @@ -16,7 +15,9 @@ use std::{ io, }; +#[cfg(target_os = "macos")] use crate::timesync::timesync_listener; +#[cfg(target_os = "macos")] use crate::virtio::{VsockAction, VsockConfig}; use anyhow::{anyhow, Context}; use env_logger::{Builder, Env, Target}; @@ -199,6 +200,7 @@ impl TryFrom for KrunContext { } } + #[cfg(target_os = "macos")] if let Some(timesync_port) = args.timesync { let vsock_config = VsockConfig { port: timesync_port, diff --git a/src/context/windows.rs b/src/context/windows.rs new file mode 100644 index 0000000..5abad27 --- /dev/null +++ b/src/context/windows.rs @@ -0,0 +1,279 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{cmdline::Args, status::RestfulUri, virtio::windows::attach_devices}; +use anyhow::{anyhow, Context}; +use std::{ + ffi::{c_char, c_void, CStr}, + fs::OpenOptions, + io, + os::windows::io::AsRawHandle, + path::{Path, PathBuf}, + ptr, +}; + +type KrunObject = *mut c_void; +type KrunError = *mut c_void; +type KrunResult = u64; + +const KRUN_SUCCESS: KrunResult = 0; +const KRUN_LOG_STYLE_AUTO: u32 = 0; +const KRUN_LOG_OPTION_ENV: u32 = 0; +const KRUN_LOG_OPTION_NO_ENV: u32 = 1; + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunStr { + data: *const c_char, + len: usize, +} +impl KrunStr { + fn new(value: &str) -> Self { + Self { + data: value.as_ptr().cast(), + len: value.len(), + } + } +} + +#[link(name = "krun.dll")] +unsafe extern "C" { + fn krun_init_log( + target: u64, + level: u32, + style: u32, + options: u32, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_mmio_device_manager_new() -> KrunObject; + fn krun_mmio_device_manager_add(manager: KrunObject, device: KrunObject); + fn krun_console_device_builder() -> KrunObject; + fn krun_console_builder_add_default_console( + builder: KrunObject, + stdin: *mut c_void, + stdout: *mut c_void, + stderr: *mut c_void, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_console_builder_build(builder: KrunObject, err_out: *mut KrunError) -> KrunObject; + fn krun_payload_load_firmware( + path: KrunStr, + cmdline: KrunStr, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_vmm_builder_new() -> KrunObject; + fn krun_vmm_builder_vcpus( + builder: *mut KrunObject, + count: u8, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_ram_mib( + builder: *mut KrunObject, + mib: u32, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_payload(builder: *mut KrunObject, payload: KrunObject); + fn krun_vmm_builder_devices(builder: *mut KrunObject, devices: KrunObject); + fn krun_vmm_builder_acpi( + builder: *mut KrunObject, + enabled: bool, + err_out: *mut KrunError, + ) -> KrunResult; + fn krun_vmm_builder_build(builder: *mut KrunObject, err_out: *mut KrunError) -> KrunObject; + fn krun_vmm_run(vmm: KrunObject); + fn krun_balloon_device_new(err_out: *mut KrunError) -> KrunObject; + fn krun_rng_device_new(err_out: *mut KrunError) -> KrunObject; + fn krun_error_result(error: KrunError) -> KrunResult; + fn krun_error_destroy(error: KrunError); + fn krun_result_name_cstr(result: KrunResult) -> *const c_char; +} + +pub struct KrunContext { + vmm: KrunObject, + args: Args, +} + +impl TryFrom for KrunContext { + type Error = anyhow::Error; + fn try_from(args: Args) -> Result { + if args.cpus == 0 { + return Err(anyhow!("vcpus must be a minimum of 1 (0 is invalid)")); + } + if args.memory == 0 { + return Err(anyhow!("zero MiB RAM inputted (invalid)")); + } + if args + .restful_uri + .as_ref() + .is_some_and(|uri| *uri != RestfulUri::None) + { + return Err(anyhow!("the RESTful service is not supported on Windows")); + } + init_logging(&args)?; + let devices = unsafe { krun_mmio_device_manager_new() }; + if devices.is_null() { + return Err(anyhow!("unable to create libkrun device manager")); + } + + let console_builder = unsafe { krun_console_device_builder() }; + let mut error = ptr::null_mut(); + check_result( + unsafe { + krun_console_builder_add_default_console( + console_builder, + io::stdin().as_raw_handle().cast(), + io::stdout().as_raw_handle().cast(), + io::stderr().as_raw_handle().cast(), + &mut error, + ) + }, + error, + "unable to configure virtio console", + )?; + let mut error = ptr::null_mut(); + let console = unsafe { krun_console_builder_build(console_builder, &mut error) }; + check_object(console, error, "unable to build virtio console")?; + unsafe { krun_mmio_device_manager_add(devices, console) }; + add_device( + devices, + unsafe { krun_balloon_device_new(ptr::null_mut()) }, + "balloon", + )?; + add_device( + devices, + unsafe { krun_rng_device_new(ptr::null_mut()) }, + "RNG", + )?; + + unsafe { attach_devices(devices, &args.devices, check_object) }?; + let firmware = args + .firmware_path + .clone() + .or_else(get_firmware_path) + .ok_or_else(|| anyhow!("can't find a firmware to load"))?; + let mut error = ptr::null_mut(); + let payload = unsafe { + krun_payload_load_firmware(KrunStr::new(path_str(&firmware, "firmware")?), KrunStr::new("reboot=k panic=-1 panic_print=0 nomodule console=hvc0 rootfstype=virtiofs rw quiet no_timer_check"), &mut error) + }; + let payload = check_object(payload, error, "unable to load firmware")?; + let mut builder = unsafe { krun_vmm_builder_new() }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_vcpus(&mut builder, args.cpus, &mut error) }, + error, + "unable to configure vCPUs", + )?; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_ram_mib(&mut builder, args.memory, &mut error) }, + error, + "unable to configure RAM", + )?; + unsafe { + krun_vmm_builder_payload(&mut builder, payload); + krun_vmm_builder_devices(&mut builder, devices) + }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_vmm_builder_acpi(&mut builder, true, &mut error) }, + error, + "unable to enable ACPI", + )?; + let mut error = ptr::null_mut(); + let vmm = unsafe { krun_vmm_builder_build(&mut builder, &mut error) }; + Ok(Self { + vmm: check_object(vmm, error, "unable to build libkrun VMM")?, + args, + }) + } +} + +impl KrunContext { + pub fn run(&self) -> Result<(), anyhow::Error> { + if let Some(pidfile) = &self.args.pidfile { + std::fs::write(pidfile, std::process::id().to_string())?; + } + unsafe { krun_vmm_run(self.vmm) }; + Ok(()) + } +} + +fn add_device( + manager: KrunObject, + device: KrunObject, + description: &str, +) -> Result<(), anyhow::Error> { + if device.is_null() { + return Err(anyhow!("unable to create {description} device")); + } + unsafe { krun_mmio_device_manager_add(manager, device) }; + Ok(()) +} +fn init_logging(args: &Args) -> Result<(), anyhow::Error> { + let (level, options) = args + .krun_log_level + .map(|level| (level, KRUN_LOG_OPTION_NO_ENV)) + .unwrap_or((3, KRUN_LOG_OPTION_ENV)); + let log_file; + let handle = match &args.log_file { + Some(path) => { + log_file = OpenOptions::new().append(true).create(true).open(path)?; + log_file.as_raw_handle() as u64 + } + None => io::stderr().as_raw_handle() as u64, + }; + let mut error = ptr::null_mut(); + check_result( + unsafe { krun_init_log(handle, level, KRUN_LOG_STYLE_AUTO, options, &mut error) }, + error, + "unable to initialize libkrun logging", + ) +} +fn check_object( + object: KrunObject, + error: KrunError, + context: &str, +) -> Result { + check_error(error, context)?; + if object.is_null() { + Err(anyhow!("{context}")) + } else { + Ok(object) + } +} +fn check_result(result: KrunResult, error: KrunError, context: &str) -> Result<(), anyhow::Error> { + check_error(error, context)?; + if result == KRUN_SUCCESS { + Ok(()) + } else { + Err(anyhow!("{context}: libkrun result {result:#x}")) + } +} +fn check_error(error: KrunError, context: &str) -> Result<(), anyhow::Error> { + if error.is_null() { + return Ok(()); + } + let result = unsafe { krun_error_result(error) }; + let name = unsafe { + let name = krun_result_name_cstr(result); + (!name.is_null()).then(|| CStr::from_ptr(name).to_string_lossy().into_owned()) + } + .unwrap_or_else(|| format!("result {result:#x}")); + unsafe { krun_error_destroy(error) }; + Err(anyhow!("{context}: {name}")) +} +fn path_str<'a>(path: &'a Path, description: &str) -> Result<&'a str, anyhow::Error> { + path.to_str() + .with_context(|| format!("{description} path is not valid UTF-8: {}", path.display())) +} +fn get_firmware_path() -> Option { + let executable = std::env::current_exe().ok()?; + let directory = executable.parent()?; + [ + directory.join("OVMF.fd"), + directory.join("edk2/OVMF.fd"), + directory.parent()?.join("edk2/OVMF.fd"), + directory.parent()?.parent()?.join("edk2/OVMF.fd"), + ] + .into_iter() + .find(|path| path.is_file()) +} diff --git a/src/main.rs b/src/main.rs index 96e3170..3e8b3f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ mod cmdline; mod context; mod status; +#[cfg(target_os = "macos")] mod timesync; mod virtio; diff --git a/src/status/mod.rs b/src/status/mod.rs new file mode 100644 index 0000000..4ac16f0 --- /dev/null +++ b/src/status/mod.rs @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::{net::Ipv4Addr, str::FromStr}; + +use anyhow::{anyhow, Context}; + +#[cfg(unix)] +mod unix; + +#[cfg(unix)] +pub use unix::{get_shutdown_eventfd, status_listener}; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum UriScheme { + Tcp, + Unix, + #[default] + None, +} + +impl FromStr for UriScheme { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "tcp" => Ok(Self::Tcp), + "unix" => Ok(Self::Unix), + "none" => Ok(Self::None), + _ => Err(anyhow!("invalid scheme")), + } + } +} + +/// Socket address in which the restful URI socket should listen on. Identical to Rust's +/// SocketAddrV4, but requires a modified FromStr implementation due to how the address is +/// presented on the command line. +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RestfulUri { + Tcp(Ipv4Addr, u16), + Unix(String), + #[default] + None, +} + +impl FromStr for RestfulUri { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let expression = regex::Regex::new(r"^(?Pnone|tcp|unix)://(?P.*)").unwrap(); + let Some(cap) = expression.captures(s) else { + return Err(anyhow!("invalid scheme input")); + }; + let scheme = &cap["scheme"]; + let value = &cap["value"]; + match UriScheme::from_str(scheme)? { + UriScheme::Tcp => { + let (ip_addr, port) = parse_tcp_input(value)?; + Ok(Self::Tcp(ip_addr, port)) + } + UriScheme::Unix => { + if value.is_empty() { + return Err(anyhow!("empty unix socket path")); + } + Ok(Self::Unix(value.to_string())) + } + UriScheme::None => Ok(Self::None), + } + } +} + +fn parse_tcp_input(input: &str) -> Result<(Ipv4Addr, u16), anyhow::Error> { + let mut parts: Vec = input.split(':').map(|s| s.to_string()).collect(); + if parts.len() != 2 { + return Err(anyhow!("restful URI formatted incorrectly")); + } + + // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to + // 127.0.0.1, this must be manually translated. + if &parts[0][..] == "localhost" { + parts[0] = String::from("127.0.0.1"); + } + + let ip_addr = + Ipv4Addr::from_str(&parts[0]).context("restful URI IP address formatted incorrectly")?; + let port = u16::from_str(&parts[1]).context("restful URI port number formatted incorrectly")?; + Ok((ip_addr, port)) +} + +#[allow(unused_imports)] +mod tests { + use super::*; + + #[test] + fn parse_valid_unix_scheme() { + assert_eq!( + RestfulUri::Unix("/tmp/path".to_string()), + RestfulUri::from_str("unix:///tmp/path").unwrap() + ); + } + + #[test] + fn parse_unix_scheme_missing_path() { + assert_eq!( + anyhow!("empty unix socket path").to_string(), + RestfulUri::from_str("unix://").err().unwrap().to_string() + ); + } + + #[test] + fn parse_unix_scheme_missing_slashes() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("unix:").err().unwrap().to_string() + ); + } + + #[test] + fn parse_unix_scheme_misspelling() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("uni://path") + .err() + .unwrap() + .to_string() + ); + } + + #[test] + fn parse_valid_tcp_scheme() { + assert_eq!( + RestfulUri::Tcp(Ipv4Addr::new(127, 0, 0, 1), 8080), + RestfulUri::from_str("tcp://localhost:8080").unwrap(), + ); + } + + #[test] + fn parse_tcp_scheme_missing_port() { + assert_eq!( + anyhow!("restful URI formatted incorrectly").to_string(), + RestfulUri::from_str("tcp://localhost") + .err() + .unwrap() + .to_string() + ); + } + + #[test] + fn parse_tcp_scheme_with_unix_path() { + assert_eq!( + anyhow!("restful URI formatted incorrectly").to_string(), + RestfulUri::from_str("tcp:///tmp/path") + .err() + .unwrap() + .to_string(), + ); + } + + #[test] + fn parse_valid_none_scheme() { + assert_eq!(RestfulUri::None, RestfulUri::from_str("none://").unwrap()); + } + + #[test] + fn parse_none_scheme_missing_postfix() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("none").err().unwrap().to_string(), + ); + } + + #[test] + fn parse_random_string_scheme() { + assert_eq!( + anyhow!("invalid scheme input").to_string(), + RestfulUri::from_str("foobar").err().unwrap().to_string(), + ); + } +} \ No newline at end of file diff --git a/src/status.rs b/src/status/unix.rs similarity index 68% rename from src/status.rs rename to src/status/unix.rs index ab6a961..080644d 100644 --- a/src/status.rs +++ b/src/status/unix.rs @@ -3,17 +3,18 @@ use std::{ fs::File, io::{ErrorKind, Read, Write}, - net::{Ipv4Addr, TcpListener}, + net::TcpListener, os::{ fd::{FromRawFd, RawFd}, unix::net::UnixListener, }, - str::FromStr, }; -use anyhow::{anyhow, Context}; +use anyhow::anyhow; use serde::{Deserialize, Serialize}; +use super::RestfulUri; + #[link(name = "krun")] extern "C" { fn krun_get_shutdown_eventfd(ctx_id: u32) -> i32; @@ -21,84 +22,8 @@ extern "C" { const VM_STATE_PATH: &str = "/vm/state"; -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub enum UriScheme { - Tcp, - Unix, - #[default] - None, -} - -impl FromStr for UriScheme { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - match s { - "tcp" => Ok(Self::Tcp), - "unix" => Ok(Self::Unix), - "none" => Ok(Self::None), - _ => Err(anyhow!("invalid scheme")), - } - } -} - -/// Socket address in which the restful URI socket should listen on. Identical to Rust's -/// SocketAddrV4, but requires a modified FromStr implementation due to how the address is -/// presented on the command line. -#[derive(Clone, Debug, Default, PartialEq)] -pub enum RestfulUri { - Tcp(Ipv4Addr, u16), - Unix(String), - #[default] - None, -} - -impl FromStr for RestfulUri { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let expression = regex::Regex::new(r"^(?Pnone|tcp|unix)://(?P.*)").unwrap(); - let Some(cap) = expression.captures(s) else { - return Err(anyhow!("invalid scheme input")); - }; - let scheme = &cap["scheme"]; - let value = &cap["value"]; - match UriScheme::from_str(scheme)? { - UriScheme::Tcp => { - let (ip_addr, port) = parse_tcp_input(value)?; - Ok(Self::Tcp(ip_addr, port)) - } - UriScheme::Unix => { - if value.is_empty() { - return Err(anyhow!("empty unix socket path")); - } - Ok(Self::Unix(value.to_string())) - } - UriScheme::None => Ok(Self::None), - } - } -} - -fn parse_tcp_input(input: &str) -> Result<(Ipv4Addr, u16), anyhow::Error> { - let mut parts: Vec = input.split(':').map(|s| s.to_string()).collect(); - if parts.len() != 2 { - return Err(anyhow!("restful URI formatted incorrectly")); - } - - // Ipv4Address's FromStr does not understand that the "localhost" IP address translates to - // 127.0.0.1, this must be manually translated. - if &parts[0][..] == "localhost" { - parts[0] = String::from("127.0.0.1"); - } - - let ip_addr = - Ipv4Addr::from_str(&parts[0]).context("restful URI IP address formatted incorrectly")?; - let port = u16::from_str(&parts[1]).context("restful URI port number formatted incorrectly")?; - Ok((ip_addr, port)) -} - -/// Retrieve the shutdown event file descriptor initialized by libkrun. -pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> i32 { +/// Retrieve the shutdown event file descriptor / handle initialized by libkrun. +pub unsafe fn get_shutdown_eventfd(ctx_id: u32) -> RawFd { let fd = krun_get_shutdown_eventfd(ctx_id); if fd < 0 { panic!("unable to retrieve krun shutdown file descriptor"); @@ -232,7 +157,7 @@ fn handle_incoming_stream( let body = &buf[header_len..body_end]; let state_req: VmStateRequest = match serde_json::from_slice(body) { - Ok(r) => r, + Ok(request) => request, Err(_) => { write_http_response( stream, @@ -263,9 +188,7 @@ fn handle_incoming_stream( } } } - _ => { - write_http_error(stream, 405, "Method Not Allowed"); - } + _ => write_http_error(stream, 405, "Method Not Allowed"), } } @@ -311,20 +234,21 @@ pub fn status_listener( #[cfg(test)] mod tests { + use std::{io::Cursor, os::fd::AsRawFd}; + use super::*; - use std::io::Cursor; fn make_request(method: &str, path: &str, body: Option<&str>) -> Vec { - let mut req = format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\n"); - if let Some(b) = body { - req.push_str(&format!("Content-Length: {}\r\n", b.len())); - req.push_str("Content-Type: application/json\r\n"); + let mut request = format!("{method} {path} HTTP/1.1\r\nHost: localhost\r\n"); + if let Some(body) = body { + request.push_str(&format!("Content-Length: {}\r\n", body.len())); + request.push_str("Content-Type: application/json\r\n"); } - req.push_str("\r\n"); - if let Some(b) = body { - req.push_str(b); + request.push_str("\r\n"); + if let Some(body) = body { + request.push_str(body); } - req.into_bytes() + request.into_bytes() } struct MockStream { @@ -332,17 +256,18 @@ mod tests { written: Vec, } - impl std::io::Read for MockStream { + impl Read for MockStream { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.read.read(buf) } } - impl std::io::Write for MockStream { + impl Write for MockStream { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.written.extend_from_slice(buf); Ok(buf.len()) } + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } @@ -353,13 +278,10 @@ mod tests { read: Cursor::new(request.to_vec()), written: Vec::new(), }; - - let (sock_a, _sock_b) = std::os::unix::net::UnixStream::pair().unwrap(); - let mut shutdown_fd = - unsafe { File::from_raw_fd(std::os::fd::AsRawFd::as_raw_fd(&sock_a)) }; + let (socket, _peer) = std::os::unix::net::UnixStream::pair().unwrap(); + let mut shutdown_fd = unsafe { File::from_raw_fd(socket.as_raw_fd()) }; handle_incoming_stream(&mut stream, &mut shutdown_fd, stopping); - std::mem::forget(shutdown_fd); String::from_utf8(stream.written).unwrap() @@ -369,7 +291,7 @@ mod tests { response .split_whitespace() .nth(1) - .and_then(|s| s.parse().ok()) + .and_then(|status| status.parse().ok()) .unwrap() } @@ -493,90 +415,4 @@ mod tests { assert_eq!(json["canHardStop"], true); assert_eq!(json["canPause"], false); } - - #[test] - fn parse_valid_unix_scheme() { - assert_eq!( - RestfulUri::Unix("/tmp/path".to_string()), - RestfulUri::from_str("unix:///tmp/path").unwrap() - ); - } - - #[test] - fn parse_unix_scheme_missing_path() { - assert_eq!( - anyhow!("empty unix socket path").to_string(), - RestfulUri::from_str("unix://").err().unwrap().to_string() - ); - } - - #[test] - fn parse_unix_scheme_missing_slashes() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("unix:").err().unwrap().to_string() - ); - } - - #[test] - fn parse_unix_scheme_misspelling() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("uni://path") - .err() - .unwrap() - .to_string() - ); - } - - #[test] - fn parse_valid_tcp_scheme() { - assert_eq!( - RestfulUri::Tcp(Ipv4Addr::new(127, 0, 0, 1), 8080), - RestfulUri::from_str("tcp://localhost:8080").unwrap(), - ); - } - - #[test] - fn parse_tcp_scheme_missing_port() { - assert_eq!( - anyhow!("restful URI formatted incorrectly").to_string(), - RestfulUri::from_str("tcp://localhost") - .err() - .unwrap() - .to_string() - ); - } - - #[test] - fn parse_tcp_scheme_with_unix_path() { - assert_eq!( - anyhow!("restful URI formatted incorrectly").to_string(), - RestfulUri::from_str("tcp:///tmp/path") - .err() - .unwrap() - .to_string(), - ); - } - - #[test] - fn parse_valid_none_scheme() { - assert_eq!(RestfulUri::None, RestfulUri::from_str("none://").unwrap()); - } - - #[test] - fn parse_none_scheme_missing_postfix() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("none").err().unwrap().to_string(), - ); - } - - #[test] - fn parse_random_string_scheme() { - assert_eq!( - anyhow!("invalid scheme input").to_string(), - RestfulUri::from_str("foobar").err().unwrap().to_string(), - ); - } } diff --git a/src/virtio.rs b/src/virtio/mod.rs similarity index 64% rename from src/virtio.rs rename to src/virtio/mod.rs index 1d3baf3..6aeee6f 100644 --- a/src/virtio.rs +++ b/src/virtio/mod.rs @@ -1,19 +1,15 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::cmdline::{ - check_required_args, check_unknown_args, cstring_to_ptr, parse_args, parse_boolean, -}; - -use std::{ - ffi::{c_char, c_int, CString}, - os::fd::RawFd, - os::unix::ffi::OsStrExt, - path::{Path, PathBuf}, - str::FromStr, -}; +use crate::cmdline::{check_required_args, check_unknown_args, parse_args, parse_boolean}; + +use std::{path::PathBuf, str::FromStr}; use anyhow::{anyhow, Context, Result}; use mac_address::MacAddress; +#[cfg(unix)] +pub type PlatformSocket = std::os::fd::RawFd; +#[cfg(windows)] +pub type PlatformSocket = std::os::windows::io::RawSocket; /// Taken from https://github.com/containers/libkrun/blob/7116644749c7b1028a970c9e8bd2d0163745a225/include/libkrun.h#L269 const NET_FEATURE_CSUM: u32 = 1 << 0; @@ -26,7 +22,7 @@ const NET_FEATURE_HOST_TSO6: u32 = 1 << 12; const NET_FEATURE_HOST_UFO: u32 = 1 << 14; /// These are the features enabled by krun_set_passt_fd and krun_set_gvproxy_path. -const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM +pub(crate) const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM | NET_FEATURE_GUEST_CSUM | NET_FEATURE_GUEST_TSO4 | NET_FEATURE_GUEST_UFO @@ -35,49 +31,18 @@ const COMPAT_NET_FEATURES: u32 = NET_FEATURE_CSUM /// Send the VFKIT magic after establishing the connection, /// as required by gvproxy in vfkit mode. -const NET_FLAG_VFKIT: u32 = 1 << 0; +pub(crate) const NET_FLAG_VFKIT: u32 = 1 << 0; const SOCK_TYPE_UNIX_SOCKET_PATH: &str = "unixSocketPath"; const SOCK_TYPE_UNIXGRAM: &str = "unixgram"; const SOCK_TYPE_UNIXSTREAM: &str = "unixstream"; -#[link(name = "krun")] -extern "C" { - fn krun_add_disk2( - ctx_id: u32, - c_block_id: *const c_char, - c_disk_path: *const c_char, - disk_format: u32, - read_only: bool, - ) -> i32; - fn krun_add_vsock_port2(ctx_id: u32, port: u32, c_filepath: *const c_char, listen: bool) - -> i32; - fn krun_add_virtiofs4( - ctx_id: u32, - c_tag: *const c_char, - c_path: *const c_char, - shm_size: u64, - read_only: bool, - semantics: u32, - ) -> i32; - fn krun_set_console_output(ctx_id: u32, c_filepath: *const c_char) -> i32; - fn krun_add_net_unixgram( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, - ) -> i32; - fn krun_add_net_unixstream( - ctx_id: u32, - c_path: *const c_char, - fd: c_int, - c_mac: *const u8, - features: u32, - flags: u32, - ) -> i32; -} +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::KrunContextSet; +#[cfg(windows)] +pub(crate) mod windows; #[repr(u32)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] @@ -118,12 +83,6 @@ impl FromStr for FsPermissions { } } -/// Each virito device configures itself with krun differently. This is used by each virtio device -/// to set their respective configurations with libkrun. -pub trait KrunContextSet { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error>; -} - /// virtio device configurations. #[derive(Clone, Debug, PartialEq)] pub enum VirtioDeviceConfig { @@ -169,23 +128,6 @@ impl FromStr for VirtioDeviceConfig { } } -/// Configure the device in the krun context based on which underlying device is contained. -impl KrunContextSet for VirtioDeviceConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - match self { - Self::Blk(blk) => blk.krun_ctx_set(id), - Self::Vsock(vsock) => vsock.krun_ctx_set(id), - Self::Net(net) => net.krun_ctx_set(id), - Self::Fs(fs) => fs.krun_ctx_set(id), - Self::Serial(serial) => serial.krun_ctx_set(id), - - // virtio-input, virtio-gpu, and virtio-rng devices are currently not configured in - // krun. - _ => Ok(()), - } - } -} - /// Configuration of a virtio-blk device. #[derive(Clone, Debug, Default, PartialEq)] pub struct BlkConfig { @@ -194,6 +136,8 @@ pub struct BlkConfig { /// Format of the disk image. pub format: DiskImageFormat, + + pub serial: Option, } impl FromStr for BlkConfig { @@ -212,40 +156,18 @@ impl FromStr for BlkConfig { blk_config.format = DiskImageFormat::from_str(f.as_str())?; } + // Parse custom serial / id if provided. + if let Some(s) = args.remove("serial") { + blk_config.serial = Some(s); + } else if let Some(s) = args.remove("id") { + blk_config.serial = Some(s); + } check_unknown_args(args, "virtio-blk")?; Ok(blk_config) } } -/// Set the virtio-blk device to be the krun VM's root disk. -impl KrunContextSet for BlkConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let basename = match self.path.file_name() { - Some(osstr) => osstr.to_str().unwrap_or("disk"), - None => "disk", - }; - let block_id_cstr = CString::new(basename).context("can't convert basename to cstring")?; - let path_cstr = path_to_cstring(&self.path)?; - - if krun_add_disk2( - id, - block_id_cstr.as_ptr(), - path_cstr.as_ptr(), - self.format as u32, - false, - ) < 0 - { - return Err(anyhow!(format!( - "unable to set virtio-blk disk for {}", - self.path.display() - ))); - } - - Ok(()) - } -} - /// Configuration of a virtio-serial device. #[derive(Clone, Debug, PartialEq)] pub struct SerialConfig { @@ -270,21 +192,6 @@ impl FromStr for SerialConfig { } } -/// Set the krun console output to be written to the virtio-serial's log file. -impl KrunContextSet for SerialConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let path_cstr = path_to_cstring(&self.log_file_path)?; - - if krun_set_console_output(id, path_cstr.as_ptr()) < 0 { - return Err(anyhow!( - "unable to set krun console output redirection to virtio-serial log file" - )); - } - - Ok(()) - } -} - /// Configuration of a virtio-vsock device. #[derive(Clone, Debug, Default, PartialEq)] pub struct VsockConfig { @@ -341,31 +248,6 @@ impl FromStr for VsockConfig { } } -/// Map the virtio-vsock's guest port and host path to enable the krun VM to communicate with the -/// socket on the host. -impl KrunContextSet for VsockConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let path_cstr = path_to_cstring(&self.socket_url)?; - - // libkrun's `listen` parameter means "guest expects connections from host" which is true when VsockAction::Connect. - if krun_add_vsock_port2( - id, - self.port, - path_cstr.as_ptr(), - self.action == VsockAction::Connect, - ) < 0 - { - return Err(anyhow!(format!( - "unable to add vsock port {} for path {}", - self.port, - &self.socket_url.display() - ))); - } - - Ok(()) - } -} - /// virtio-vsock action. #[derive(Clone, Debug, Default, PartialEq)] pub enum VsockAction { @@ -393,7 +275,7 @@ impl FromStr for VsockAction { #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct SocketConfig { pub path: Option, - pub fd: Option, + pub fd: Option, pub offloading: bool, pub send_vfkit_magic: bool, } @@ -501,7 +383,7 @@ fn parse_socket_config( if let Some(fd) = args.remove("fd") { socket_config.fd = Some( - fd.parse::() + fd.parse::() .context("virtio-net unable to convert \"fd\" value {fd} to a file descriptor")?, ); } @@ -528,80 +410,6 @@ fn parse_socket_config( Ok((socket_type, socket_config)) } -/// Set the gvproxy's path and network MAC address. -impl KrunContextSet for NetConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - match &self.socket_type { - SocketType::UnixGram => { - let features = if self.socket_config.offloading { - COMPAT_NET_FEATURES - } else { - 0 - }; - - let path = match &self.socket_config.path { - Some(path) => path_to_cstring(path)?, - None => path_to_cstring(&PathBuf::new())?, - }; - - let flags = if self.socket_config.send_vfkit_magic { - NET_FLAG_VFKIT - } else { - 0 - }; - - if krun_add_net_unixgram( - id, - cstring_to_ptr(&path), - self.socket_config.fd.unwrap_or(-1), - self.mac_address.bytes().as_ptr(), - features, - flags, - ) < 0 - { - // TODO(jakecorrenti): if this fails, we should display all of the values the - // user provided to the virtio-net cmdline - return Err(anyhow!(format!( - "virtio-net unable to add device with unix datagram backend {:#?}", - self.socket_config - ))); - } - } - SocketType::UnixStream => { - let features = if self.socket_config.offloading { - COMPAT_NET_FEATURES - } else { - 0 - }; - - let path = match &self.socket_config.path { - Some(path) => path_to_cstring(path)?, - None => path_to_cstring(&PathBuf::new())?, - }; - - if krun_add_net_unixstream( - id, - cstring_to_ptr(&path), - self.socket_config.fd.unwrap_or(-1), - self.mac_address.bytes().as_ptr(), - features, - 0, - ) < 0 - { - // TODO(jakecorrenti): if this fails, we should display all of the values the - // user provided to the virtio-net cmdline - return Err(anyhow!(format!( - "virtio-net unable to add device with unix stream backend {:#?}", - self.socket_config - ))); - } - } - } - - Ok(()) - } -} - /// Configuration of a virtio-fs device. #[derive(Clone, Debug, Default, PartialEq)] pub struct FsConfig { @@ -640,32 +448,6 @@ impl FromStr for FsConfig { } } -/// Set the shared directory with its guest mount tag. -impl KrunContextSet for FsConfig { - unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { - let shared_dir_cstr = path_to_cstring(&self.shared_dir)?; - let mount_tag_cstr = path_to_cstring(&self.mount_tag)?; - - if krun_add_virtiofs4( - id, - mount_tag_cstr.as_ptr(), - shared_dir_cstr.as_ptr(), - 0, - false, - self.permission_semantics.clone() as u32, - ) < 0 - { - return Err(anyhow!(format!( - "unable to add virtiofs shared directory {} with mount tag {}", - &self.shared_dir.display(), - &self.mount_tag.display() - ))); - } - - Ok(()) - } -} - /// Configuration of a virtio-gpu device. #[derive(Clone, Debug, Default, PartialEq)] pub struct GpuConfig { @@ -733,13 +515,3 @@ impl FromStr for InputConfig { } } } - -/// Construct a NULL-terminated C string from a Rust Path object. -fn path_to_cstring(path: &Path) -> Result { - let cstring = CString::new(path.as_os_str().as_bytes()).context(format!( - "unable to convert path {} into NULL-terminated C string", - path.display() - ))?; - - Ok(cstring) -} diff --git a/src/virtio/unix.rs b/src/virtio/unix.rs new file mode 100644 index 0000000..4984dea --- /dev/null +++ b/src/virtio/unix.rs @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::cmdline::cstring_to_ptr; +use std::{ + ffi::{c_char, c_int, CString}, + os::unix::ffi::OsStrExt, + path::Path, +}; + +#[link(name = "krun")] +unsafe extern "C" { + fn krun_add_disk2( + ctx_id: u32, + block_id: *const c_char, + path: *const c_char, + format: u32, + read_only: bool, + ) -> i32; + fn krun_add_vsock_port2(ctx_id: u32, port: u32, path: *const c_char, listen: bool) -> i32; + fn krun_add_virtiofs4( + ctx_id: u32, + tag: *const c_char, + path: *const c_char, + shm_size: u64, + read_only: bool, + semantics: u32, + ) -> i32; + fn krun_set_console_output(ctx_id: u32, path: *const c_char) -> i32; + fn krun_add_net_unixgram( + ctx_id: u32, + path: *const c_char, + fd: c_int, + mac: *const u8, + features: u32, + flags: u32, + ) -> i32; + fn krun_add_net_unixstream( + ctx_id: u32, + path: *const c_char, + fd: c_int, + mac: *const u8, + features: u32, + flags: u32, + ) -> i32; +} + +/// Each virito device configures itself with krun differently. This is used by each virtio device +/// to set their respective configurations with libkrun. +pub trait KrunContextSet { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error>; +} + +/// Configure the device in the krun context based on which underlying device is contained +impl KrunContextSet for VirtioDeviceConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + match self { + Self::Blk(blk) => blk.krun_ctx_set(id), + Self::Vsock(vsock) => vsock.krun_ctx_set(id), + Self::Net(net) => net.krun_ctx_set(id), + Self::Fs(fs) => fs.krun_ctx_set(id), + Self::Serial(serial) => serial.krun_ctx_set(id), + + // virtio-input, virtio-gpu, and virtio-rng devices are currently not configured in + // krun. + _ => Ok(()), + } + } +} + +impl KrunContextSet for BlkConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let basename = match self.path.file_name() { + Some(osstr) => osstr.to_str().unwrap_or("disk"), + None => "disk", + }; + let block_id_cstr = CString::new(basename).context("can't convert basename to cstring")?; + let path_cstr = path_to_cstring(&self.path)?; + + if krun_add_disk2( + id, + block_id_cstr.as_ptr(), + path_cstr.as_ptr(), + self.format as u32, + false, + ) < 0 + { + return Err(anyhow!(format!( + "unable to set virtio-blk disk for {}", + self.path.display() + ))); + } + + Ok(()) + } +} + +/// Set the krun console output to be written to the virtio-serial's log file. +impl KrunContextSet for SerialConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let path = path_to_cstring(&self.log_file_path)?; + + if unsafe { krun_set_console_output(id, path.as_ptr()) } < 0 { + return Err(anyhow!( + "unable to set krun console output redirection to virtio-serial log file" + )); + } + + Ok(()) + } +} + +/// Map the virtio-vsock's guest port and host path to enable the krun VM to communicate with the +/// socket on the host. +impl KrunContextSet for VsockConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let path_cstr = path_to_cstring(&self.socket_url)?; + + // libkrun's `listen` parameter means "guest expects connections from host" which is true when VsockAction::Connect. + if krun_add_vsock_port2( + id, + self.port, + path_cstr.as_ptr(), + self.action == VsockAction::Connect, + ) < 0 + { + return Err(anyhow!(format!( + "unable to add vsock port {} for path {}", + self.port, + self.socket_url.display() + ))); + } + + Ok(()) + } +} + +impl KrunContextSet for NetConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + match &self.socket_type { + SocketType::UnixGram => { + let features = if self.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + + let path = match &self.socket_config.path { + Some(path) => path_to_cstring(path)?, + None => path_to_cstring(&PathBuf::new())?, + }; + + let flags = if self.socket_config.send_vfkit_magic { + NET_FLAG_VFKIT + } else { + 0 + }; + + if krun_add_net_unixgram( + id, + cstring_to_ptr(&path), + self.socket_config.fd.unwrap_or(-1), + self.mac_address.bytes().as_ptr(), + features, + flags, + ) < 0 + { + // TODO(jakecorrenti): if this fails, we should display all of the values the + // user provided to the virtio-net cmdline + return Err(anyhow!(format!( + "virtio-net unable to add device with unix datagram backend {:#?}", + self.socket_config + ))); + } + } + SocketType::UnixStream => { + let features = if self.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + + let path = match &self.socket_config.path { + Some(path) => path_to_cstring(path)?, + None => path_to_cstring(&PathBuf::new())?, + }; + + if krun_add_net_unixstream( + id, + cstring_to_ptr(&path), + self.socket_config.fd.unwrap_or(-1), + self.mac_address.bytes().as_ptr(), + features, + 0, + ) < 0 + { + // TODO(jakecorrenti): if this fails, we should display all of the values the + // user provided to the virtio-net cmdline + return Err(anyhow!(format!( + "virtio-net unable to add device with unix stream backend {:#?}", + self.socket_config + ))); + } + } + } + + Ok(()) + } +} + +impl KrunContextSet for FsConfig { + unsafe fn krun_ctx_set(&self, id: u32) -> Result<(), anyhow::Error> { + let shared_dir_cstr = path_to_cstring(&self.shared_dir)?; + let mount_tag_cstr = path_to_cstring(&self.mount_tag)?; + + if krun_add_virtiofs4( + id, + mount_tag_cstr.as_ptr(), + shared_dir_cstr.as_ptr(), + 0, + false, + self.permission_semantics.clone() as u32, + ) < 0 + { + return Err(anyhow!(format!( + "unable to add virtiofs shared directory {} with mount tag {}", + self.shared_dir.display(), + self.mount_tag.display() + ))); + } + + Ok(()) + } +} + +/// Construct a NULL-terminated C string from a Rust Path object. +fn path_to_cstring(path: &Path) -> Result { + let cstring = CString::new(path.as_os_str().as_bytes()).context(format!( + "unable to convert path {} into NULL-terminated C string", + path.display() + ))?; + + Ok(cstring) +} diff --git a/src/virtio/windows.rs b/src/virtio/windows.rs new file mode 100644 index 0000000..d03b57b --- /dev/null +++ b/src/virtio/windows.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use anyhow::{anyhow, Context, Result}; +use std::{ + ffi::{c_char, c_void}, + path::Path, + ptr, +}; + +type KrunObject = *mut c_void; +type KrunError = *mut c_void; + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunStr { + data: *const c_char, + len: usize, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct KrunBytes { + data: *const u8, + len: usize, +} + +impl KrunStr { + fn new(value: &str) -> Self { + Self { + data: value.as_ptr().cast(), + len: value.len(), + } + } +} + +#[link(name = "krun.dll")] +unsafe extern "C" { + fn krun_mmio_device_manager_add(manager: KrunObject, device: KrunObject); + fn krun_block_device_new( + id: KrunStr, + path: KrunStr, + format: u32, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_fs_device_new(tag: KrunStr, host_path: KrunStr, err_out: *mut KrunError) -> KrunObject; + fn krun_net_device_new_unixstream_path( + id: KrunStr, + path: KrunStr, + mac: KrunBytes, + features: u32, + flags: u32, + err_out: *mut KrunError, + ) -> KrunObject; + fn krun_vsock_device_new(cid: u64, tsi_features: u32, err_out: *mut KrunError) -> KrunObject; + fn krun_vsock_device_add_unix_port(vsock: KrunObject, port: u32, path: KrunStr, listen: bool); +} + +pub(crate) unsafe fn attach_devices( + manager: *mut c_void, + configs: &[VirtioDeviceConfig], + check_object: fn(KrunObject, KrunError, &str) -> Result, +) -> Result<()> { + for (index, config) in configs.iter().enumerate() { + let device = match config { + VirtioDeviceConfig::Blk(block) => { + let generated_id; + let id = match block.serial.as_deref() { + Some(id) => id, + None => { + generated_id = format!("disk{index}"); + &generated_id + } + }; + let mut error = ptr::null_mut(); + let device = unsafe { + krun_block_device_new( + KrunStr::new(id), + KrunStr::new(path_str(&block.path, "block device")?), + block.format as u32, + &mut error, + ) + }; + check_object(device, error, "unable to create block device")? + } + VirtioDeviceConfig::Fs(fs) => { + let mut error = ptr::null_mut(); + let device = unsafe { + krun_fs_device_new( + KrunStr::new(path_str(&fs.mount_tag, "virtio-fs mount tag")?), + KrunStr::new(path_str(&fs.shared_dir, "virtio-fs shared directory")?), + &mut error, + ) + }; + check_object(device, error, "unable to create virtio-fs device")? + } + VirtioDeviceConfig::Net(net) => { + if net.socket_type != SocketType::UnixStream || net.socket_config.fd.is_some() { + return Err(anyhow!( + "Windows supports only path-based type=unixstream networking" + )); + } + let path = + net.socket_config.path.as_deref().ok_or_else(|| { + anyhow!("virtio-net type=unixstream requires path=") + })?; + let id = format!("net{index}"); + let features = if net.socket_config.offloading { + COMPAT_NET_FEATURES + } else { + 0 + }; + let flags = if net.socket_config.send_vfkit_magic { + NET_FLAG_VFKIT + } else { + 0 + }; + let mut error = ptr::null_mut(); + let device = unsafe { + krun_net_device_new_unixstream_path( + KrunStr::new(&id), + KrunStr::new(path_str(path, "network socket")?), + KrunBytes { + data: net.mac_address.bytes().as_ptr(), + len: 6, + }, + features, + flags, + &mut error, + ) + }; + check_object(device, error, "unable to create network device")? + } + VirtioDeviceConfig::Vsock(config) => { + let mut error = ptr::null_mut(); + let device = unsafe { krun_vsock_device_new(3, 0, &mut error) }; + let device = check_object(device, error, "unable to create vsock device")?; + unsafe { + krun_vsock_device_add_unix_port( + device, + config.port, + KrunStr::new(path_str(&config.socket_url, "vsock socket")?), + config.action == VsockAction::Connect, + ) + }; + device + } + _ => return Err(anyhow!("unsupported device on Windows: {config:?}")), + }; + unsafe { krun_mmio_device_manager_add(manager, device) }; + } + Ok(()) +} + +fn path_str<'a>(path: &'a Path, description: &str) -> Result<&'a str> { + path.to_str() + .with_context(|| format!("{description} path is not valid UTF-8: {}", path.display())) +}