diff options
72 files changed, 1213 insertions, 617 deletions
diff --git a/.gitignore b/.gitignore index e9858b9..0491ebb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ # SPDX-License-Identifier: CC0-1.0 -# SPDX-FileCopyrightText: 2021-2022 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> /config.nix build/ result result-* + +**/subprojects/* +!**/subprojects/*.wrap +!**/subprojects/packagefiles diff --git a/host/rootfs/Makefile b/host/rootfs/Makefile index a108a29..fc61c09 100644 --- a/host/rootfs/Makefile +++ b/host/rootfs/Makefile @@ -78,6 +78,8 @@ S6_RC_FILES = \ etc/s6-rc/ext-rc/type \ etc/s6-rc/ext/type \ etc/s6-rc/ext/up \ + etc/s6-rc/kvm/type \ + etc/s6-rc/kvm/up \ etc/s6-rc/mdevd-coldplug/dependencies \ etc/s6-rc/mdevd-coldplug/type \ etc/s6-rc/mdevd-coldplug/up \ diff --git a/host/rootfs/default.nix b/host/rootfs/default.nix index 5bd2488..c6664bd 100644 --- a/host/rootfs/default.nix +++ b/host/rootfs/default.nix @@ -6,7 +6,7 @@ import ../../lib/call-package.nix ( { callSpectrumPackage, lseek, src, pkgsMusl, pkgsStatic, linux_latest }: pkgsStatic.callPackage ( -{ start-vm +{ start-vmm , lib, stdenvNoCC, nixos, runCommand, writeReferencesToFile, erofs-utils, s6-rc , busybox, cloud-hypervisor, cryptsetup, execline, e2fsprogs, jq, kmod , mdevd, s6, s6-linux-init, socat, util-linuxMinimal, virtiofsd, xorg @@ -44,7 +44,7 @@ let packages = [ cloud-hypervisor e2fsprogs execline jq kmod mdevd - s6 s6-linux-init s6-rc socat start-vm virtiofsd + s6 s6-linux-init s6-rc socat start-vmm virtiofsd (cryptsetup.override { programs = { diff --git a/host/rootfs/etc/mdev.conf b/host/rootfs/etc/mdev.conf index 1d6b630..31af4d1 100644 --- a/host/rootfs/etc/mdev.conf +++ b/host/rootfs/etc/mdev.conf @@ -3,5 +3,5 @@ -$MODALIAS=.* 0:0 660 +/etc/mdev/modalias.sh -$DEVTYPE=(disk|partition) 0:0 660 +/etc/mdev/block/add -kvm 0:0 660 +kvm 0:0 660 +background { /etc/mdev/listen kvm } dri/card0 0:0 660 +background { /etc/mdev/listen card0 } diff --git a/host/rootfs/etc/s6-rc/ext-rc-init/dependencies b/host/rootfs/etc/s6-rc/ext-rc-init/dependencies index ba3cd47..c8fb026 100644 --- a/host/rootfs/etc/s6-rc/ext-rc-init/dependencies +++ b/host/rootfs/etc/s6-rc/ext-rc-init/dependencies @@ -3,3 +3,5 @@ # core ext +kvm +static-nodes diff --git a/host/rootfs/etc/s6-rc/ext-rc-init/up b/host/rootfs/etc/s6-rc/ext-rc-init/up index 7c5764a..9fa003d 100644 --- a/host/rootfs/etc/s6-rc/ext-rc-init/up +++ b/host/rootfs/etc/s6-rc/ext-rc-init/up @@ -2,7 +2,8 @@ # SPDX-FileCopyrightText: 2021-2023 Alyssa Ross <hi@alyssa.is> # SPDX-FileCopyrightText: 2022 Unikie -if { mkdir -p /run/s6-rc.ext.src } +if { mkdir -p /run/s6-rc.ext.src/ok-vmm/contents.d } +if { redirfd -w 1 /run/s6-rc.ext.src/ok-vmm/type echo bundle } cd /run/s6-rc.ext.src if { @@ -15,8 +16,9 @@ if { if { mkdir vm-${name} vm-${name}/dependencies.d vm-${name}/env } if { redirfd -w 1 vm-${name}/type echo longrun } if { redirfd -w 1 vm-${name}/notification-fd echo 3 } - if { redirfd -w 1 vm-${name}/run printf "#!/bin/execlineb -P\n/bin/start-vm" } + if { redirfd -w 1 vm-${name}/run printf "#!/bin/execlineb -P\n/bin/start-vmm" } if { chmod +x vm-${name}/run } + if { touch ok-vmm/contents.d/vm-${name} } if { elglob -0 paths ${dir}/shared-dirs/* @@ -42,4 +44,5 @@ if { } if { s6-rc-compile /run/s6-rc.ext.db /run/s6-rc.ext.src } -s6-rc-init -c /run/s6-rc.ext.db -l /run/s6-rc.ext -p ext- /run/service +if { s6-rc-init -c /run/s6-rc.ext.db -l /run/s6-rc.ext -p ext- /run/service } +s6-rc -ul /run/s6-rc.ext change ok-vmm diff --git a/host/rootfs/etc/s6-rc/kvm/type b/host/rootfs/etc/s6-rc/kvm/type new file mode 100644 index 0000000..bdd22a1 --- /dev/null +++ b/host/rootfs/etc/s6-rc/kvm/type @@ -0,0 +1 @@ +oneshot diff --git a/host/rootfs/etc/s6-rc/kvm/type.license b/host/rootfs/etc/s6-rc/kvm/type.license new file mode 100644 index 0000000..a941ca4 --- /dev/null +++ b/host/rootfs/etc/s6-rc/kvm/type.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC0-1.0 +SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> diff --git a/host/rootfs/etc/s6-rc/kvm/up b/host/rootfs/etc/s6-rc/kvm/up new file mode 100644 index 0000000..c02e3f9 --- /dev/null +++ b/host/rootfs/etc/s6-rc/kvm/up @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +/etc/mdev/wait kvm diff --git a/host/rootfs/usr/bin/lsvm b/host/rootfs/usr/bin/lsvm index b5a979e..a2b2b2b 100755 --- a/host/rootfs/usr/bin/lsvm +++ b/host/rootfs/usr/bin/lsvm @@ -1,17 +1,21 @@ #!/bin/execlineb -P # SPDX-License-Identifier: EUPL-1.2+ -# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> -foreground { s6-rc -bu change ext } foreground { printf "NAME \tSTATUS\n" } cd /ext/svc/data elglob -0 vms * forx -E vm { $vms } if { printf "%-20s\t" $vm } -ifte { - ifte { echo "[31mSTOPPED[0m" } - { echo "[32;1mRUNNING[0m" } - test -f /run/service/ext-vm-${vm}/down +if -n { + redirfd -w 2 /dev/null + backtick -E state { + pipeline -w { jq -r .state } + ch-remote --api-socket /run/service/ext-vm-${vm}/env/cloud-hypervisor.sock info + } + case -s $state { + Created { echo "[31mSTOPPED[0m" } + } + echo "[32;1mRUNNING[0m" } -{ echo "[33mUNKNOWN[0m" } -test -d /run/service/ext-vm-${vm} +echo "[33mUNKNOWN[0m" diff --git a/host/rootfs/usr/bin/vm-start b/host/rootfs/usr/bin/vm-start index 46668eb..effc65d 100755 --- a/host/rootfs/usr/bin/vm-start +++ b/host/rootfs/usr/bin/vm-start @@ -1,6 +1,15 @@ #!/bin/execlineb -S1 # SPDX-License-Identifier: EUPL-1.2+ -# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> foreground { s6-rc -bu change ext-rc } -s6-rc -l /run/s6-rc.ext -u change vm-${1} + +foreground { + redirfd -w 2 /dev/null + cd /ext/svc/data/${1}/providers/net + elglob -0 providers * + forx -pE provider { $providers } + vm-start $provider +} + +ch-remote --api-socket /run/service/ext-vm-${1}/env/cloud-hypervisor.sock boot diff --git a/host/rootfs/usr/bin/vm-stop b/host/rootfs/usr/bin/vm-stop index 2322003..db2e9f3 100755 --- a/host/rootfs/usr/bin/vm-stop +++ b/host/rootfs/usr/bin/vm-stop @@ -1,5 +1,5 @@ #!/bin/execlineb -S1 # SPDX-License-Identifier: EUPL-1.2+ -# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> -s6-rc -l /run/s6-rc.ext -d change vm-${1} +ch-remote --api-socket /run/service/ext-vm-${1}/env/cloud-hypervisor.sock shutdown diff --git a/host/start-vm/ch.rs b/host/start-vm/ch.rs deleted file mode 100644 index 876a6ed..0000000 --- a/host/start-vm/ch.rs +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> - -use std::ffi::{CStr, OsStr, OsString}; -use std::num::NonZeroI32; -use std::os::raw::{c_char, c_int}; -use std::os::unix::prelude::*; -use std::process::{Command, Stdio}; - -use crate::format_mac; - -// Trivially safe. -const EPERM: NonZeroI32 = unsafe { NonZeroI32::new_unchecked(1) }; -const EPROTO: NonZeroI32 = unsafe { NonZeroI32::new_unchecked(71) }; - -fn command(vm_name: &OsStr, s: impl AsRef<OsStr>) -> Command { - let mut api_socket_path = OsString::from("/run/service/ext-vm-"); - api_socket_path.push(vm_name); - api_socket_path.push("/env/cloud-hypervisor.sock"); - - let mut command = Command::new("ch-remote"); - command.stdin(Stdio::null()); - command.arg("--api-socket"); - command.arg(api_socket_path); - command.arg(s); - command -} - -pub fn add_net(vm_name: &OsStr, tap: RawFd, mac: &str) -> Result<OsString, NonZeroI32> { - let mut ch_remote = command(vm_name, "add-net") - .arg(format!("fd={},mac={}", tap, mac)) - .stdout(Stdio::piped()) - .spawn() - .or(Err(EPERM))?; - - let jq_out = match Command::new("jq") - .args(["-j", ".id"]) - .stdin(ch_remote.stdout.take().unwrap()) - .stderr(Stdio::inherit()) - .output() - { - Ok(o) => o, - Err(_) => { - // Try not to leave a zombie. - let _ = ch_remote.kill(); - let _ = ch_remote.wait(); - return Err(EPERM); - } - }; - - if let Ok(ch_remote_status) = ch_remote.wait() { - if ch_remote_status.success() && jq_out.status.success() { - return Ok(OsString::from_vec(jq_out.stdout)); - } - } - - Err(EPROTO) -} - -pub fn remove_device(vm_name: &OsStr, device_id: &OsStr) -> Result<(), NonZeroI32> { - let ch_remote = command(vm_name, "remove-device") - .arg(device_id) - .status() - .or(Err(EPERM))?; - - if ch_remote.success() { - Ok(()) - } else { - Err(EPROTO) - } -} - -/// # Safety -/// -/// - `vm_name` must point to a valid C string. -/// - `tap` must be a file descriptor describing an tap device. -/// - `mac` must be a valid pointer. -#[export_name = "ch_add_net"] -unsafe extern "C" fn add_net_c( - vm_name: *const c_char, - tap: RawFd, - mac: *const [u8; 6], - id: *mut *mut OsString, -) -> c_int { - let vm_name = CStr::from_ptr(vm_name); - let mac = format_mac(&*mac); - - match add_net(OsStr::from_bytes(vm_name.to_bytes()), tap, &mac) { - Err(e) => e.get(), - Ok(id_str) => { - if !id.is_null() { - let token = Box::into_raw(Box::new(id_str)); - *id = token; - } - 0 - } - } -} - -/// # Safety -/// -/// - `vm_name` must point to a valid C string. -/// - `id` must be a device ID obtained by calling `add_net_c`. After -/// calling `remove_device_c`, the pointer is no longer valid. -#[export_name = "ch_remove_device"] -unsafe extern "C" fn remove_device_c(vm_name: *const c_char, device_id: *mut OsString) -> c_int { - let vm_name = CStr::from_ptr(vm_name); - let device_id = Box::from_raw(device_id); - - if let Err(e) = remove_device(OsStr::from_bytes(vm_name.to_bytes()), device_id.as_ref()) { - e.get() - } else { - 0 - } -} - -/// # Safety -/// -/// `id` must be a device ID obtained by calling `add_net_c`. After -/// calling `device_free`, the pointer is no longer valid. -#[export_name = "ch_device_free"] -unsafe extern "C" fn device_free(id: *mut OsString) { - if !id.is_null() { - drop(Box::from_raw(id)) - } -} diff --git a/host/start-vm/default.nix b/host/start-vm/default.nix deleted file mode 100644 index 9a7440e..0000000 --- a/host/start-vm/default.nix +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-License-Identifier: MIT -# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> - -import ../../lib/call-package.nix ( -{ src, lib, stdenv, meson, ninja, rustc, clippy }: - -stdenv.mkDerivation (finalAttrs: { - name = "start-vm"; - - src = lib.fileset.toSource { - root = ../..; - fileset = lib.fileset.intersection src ./.; - }; - sourceRoot = "source/host/start-vm"; - - nativeBuildInputs = [ meson ninja rustc ]; - - mesonFlags = [ "-Dwerror=true" ]; - - doCheck = true; - - passthru.tests = { - clippy = finalAttrs.finalPackage.overrideAttrs ( - { nativeBuildInputs ? [], ... }: - { - nativeBuildInputs = nativeBuildInputs ++ [ clippy ]; - RUSTC = "clippy-driver"; - postBuild = ''touch $out && exit 0''; - } - ); - }; - - meta = { - mainProgram = "start-vm"; - }; -}) -) (_: {}) diff --git a/host/start-vm/lib.rs b/host/start-vm/lib.rs deleted file mode 100644 index f60a822..0000000 --- a/host/start-vm/lib.rs +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> -// SPDX-FileCopyrightText: 2022 Unikie - -mod ch; -mod net; -mod s6; -mod unix; - -use std::borrow::Cow; -use std::env::args_os; -use std::ffi::{CString, OsStr, OsString}; -use std::fs::remove_file; -use std::io::{self, ErrorKind}; -use std::os::unix::net::UnixListener; -use std::os::unix::prelude::*; -use std::path::Path; -use std::process::Command; - -use net::{format_mac, net_setup, NetConfig}; -use unix::clear_cloexec; - -pub use s6::notify_readiness; - -pub fn prog_name() -> String { - args_os() - .next() - .as_ref() - .map(Path::new) - .and_then(Path::file_name) - .map(OsStr::to_string_lossy) - .unwrap_or(Cow::Borrowed("start-vm")) - .into_owned() -} - -pub fn create_api_socket() -> Result<UnixListener, String> { - let _ = remove_file("env/cloud-hypervisor.sock"); - let api_socket = UnixListener::bind("env/cloud-hypervisor.sock") - .map_err(|e| format!("creating API socket: {e}"))?; - - // Safe because we own api_socket. - if unsafe { clear_cloexec(api_socket.as_fd()) } == -1 { - let errno = io::Error::last_os_error(); - return Err(format!("clearing CLOEXEC on API socket fd: {}", errno)); - } - - Ok(api_socket) -} - -pub fn vm_command( - service_dir: &Path, - vm_dir: &Path, - api_socket_fd: RawFd, -) -> Result<Command, String> { - let vm_name = service_dir - .file_name() - .ok_or_else(|| "directory has no name".to_string())? - .as_bytes(); - - if !vm_name.starts_with(b"vm-") { - return Err("not running from a VM service directory".to_string()); - } - - if vm_name.contains(&b',') { - return Err(format!("VM name may not contain a comma: {:?}", vm_name)); - } - - let vm_name = OsStr::from_bytes(&vm_name[3..]); - - let config_dir = vm_dir.join(vm_name); - - let mut command = Command::new("cloud-hypervisor"); - command.args(["--api-socket", &format!("fd={api_socket_fd}")]); - command.args(["--cmdline", "console=ttyS0 root=PARTLABEL=root"]); - command.args(["--memory", "size=256M,shared=on"]); - command.args(["--console", "pty"]); - command.arg("--kernel"); - command.arg(config_dir.join("vmlinux")); - - let net_providers_dir = config_dir.join("providers/net"); - match net_providers_dir.read_dir() { - Ok(entries) => { - // TODO: to support multiple net providers, we'll need - // a better naming scheme for tap and bridge devices. - #[allow(clippy::never_loop)] - for r in entries { - let entry = r - .map_err(|e| format!("examining directory entry: {}", e))? - .file_name(); - - // Safe because provider_name is the name of a directory entry, so - // can't contain a null byte. - let provider_name = unsafe { CString::from_vec_unchecked(entry.into_vec()) }; - - // Safe because we pass a valid pointer and check the result. - let NetConfig { fd, mac } = unsafe { net_setup(provider_name.as_ptr()) }; - if fd == -1 { - let e = io::Error::last_os_error(); - return Err(format!("setting up networking failed: {}", e)); - } - - command - .arg("--net") - .arg(format!("fd={},mac={}", fd, format_mac(&mac))); - - break; - } - } - Err(e) if e.kind() == ErrorKind::NotFound => {} - Err(e) => return Err(format!("reading directory {:?}: {}", net_providers_dir, e)), - } - - let blk_dir = config_dir.join("blk"); - match blk_dir.read_dir().map(Iterator::peekable) { - Ok(mut entries) => { - if entries.peek().is_some() { - command.arg("--disk"); - } - - for result in entries { - let entry = result - .map_err(|e| format!("examining directory entry: {}", e))? - .path(); - - if entry.extension() != Some(OsStr::new("img")) { - continue; - } - - if entry.as_os_str().as_bytes().contains(&b',') { - return Err(format!("illegal ',' character in path {:?}", entry)); - } - - let mut arg = OsString::from("path="); - arg.push(entry); - arg.push(",readonly=on"); - command.arg(arg); - } - } - Err(e) => return Err(format!("reading directory {:?}: {}", blk_dir, e)), - } - - if config_dir.join("wayland").exists() { - command.arg("--gpu").arg({ - let mut gpu = OsString::from("socket=../gpu-"); - gpu.push(vm_name); - gpu.push("/env/crosvm.sock"); - gpu - }); - } - - let shared_dirs_dir = config_dir.join("shared-dirs"); - match shared_dirs_dir.read_dir().map(Iterator::peekable) { - Ok(mut entries) => { - if entries.peek().is_some() { - command.arg("--fs"); - } - - for result in entries { - let entry = result - .map_err(|e| format!("examining directory entry: {}", e))? - .file_name(); - - let mut arg = OsString::from("tag="); - arg.push(&entry); - arg.push(",socket=../fs-"); - arg.push(vm_name); - arg.push("-"); - arg.push(&entry); - arg.push("/env/virtiofsd.sock"); - command.arg(arg); - } - } - Err(e) if e.kind() == ErrorKind::NotFound => {} - Err(e) => return Err(format!("reading directory {:?}: {}", shared_dirs_dir, e)), - } - - command.arg("--serial").arg({ - let mut serial = OsString::from("file=/run/"); - serial.push(vm_name); - serial.push(".log"); - serial - }); - - Ok(command) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_vm_name_comma() { - assert!(vm_command(Path::new("/vm-,"), Path::new(""), -1) - .unwrap_err() - .contains("comma")); - } -} diff --git a/host/start-vm/meson.build b/host/start-vm/meson.build deleted file mode 100644 index d059e3b..0000000 --- a/host/start-vm/meson.build +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-License-Identifier: EUPL-1.2+ -# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> - -project('start-vm', 'rust', 'c', - default_options : ['c_std=c2x', 'rust_std=2018', 'warning_level=3']) - -add_project_arguments('-D_GNU_SOURCE', '-Wno-error=attributes', language : 'c') -add_project_arguments('-C', 'panic=abort', language : 'rust') - -c_lib = static_library('start-vm', 'net.c', 'net-util.c', 'unix.c') -rust_lib = static_library('start_vm', 'lib.rs', link_with : c_lib) - -executable('start-vm', 'start-vm.rs', link_with : rust_lib, install : true) - -test_exe = executable('start-vm-test', 'lib.rs', - rust_args : ['--test', '-C', 'panic=unwind'], - link_with : c_lib) -test('Rust unit tests', test_exe, protocol : 'rust') - -subdir('tests') diff --git a/host/start-vm/net.rs b/host/start-vm/net.rs deleted file mode 100644 index 7c73fa0..0000000 --- a/host/start-vm/net.rs +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> - -use std::os::raw::c_char; - -#[repr(C)] -pub struct NetConfig { - pub fd: i32, - pub mac: [u8; 6], -} - -extern "C" { - pub fn net_setup(provider_vm_name: *const c_char) -> NetConfig; -} - -pub fn format_mac(mac: &[u8; 6]) -> String { - format!( - "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn format_mac_all_zero() { - assert_eq!(format_mac(&[0; 6]), "00:00:00:00:00:00"); - } - - #[test] - fn format_mac_hex() { - let mac = [0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54]; - assert_eq!(format_mac(&mac), "FE:DC:BA:98:76:54"); - } -} diff --git a/host/start-vm/tests/vm_command-basic.rs b/host/start-vm/tests/vm_command-basic.rs deleted file mode 100644 index b70b6a8..0000000 --- a/host/start-vm/tests/vm_command-basic.rs +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> - -use std::ffi::{OsStr, OsString}; -use std::fs::{create_dir_all, File}; - -use start_vm::vm_command; -use test_helper::TempDir; - -fn main() -> std::io::Result<()> { - let service_dir_parent = TempDir::new()?; - let service_dir = service_dir_parent.path().join("vm-testvm"); - - let vm_dir = TempDir::new()?; - - let kernel_path = vm_dir.path().join("testvm/vmlinux"); - let image_path = vm_dir.path().join("testvm/blk/root.img"); - - create_dir_all(image_path.parent().unwrap())?; - File::create(&kernel_path)?; - File::create(&image_path)?; - - let command = vm_command(&service_dir, vm_dir.path(), 4).unwrap(); - assert_eq!(command.get_program(), "cloud-hypervisor"); - - let mut expected_disk_arg = OsString::from("path="); - expected_disk_arg.push(image_path); - expected_disk_arg.push(",readonly=on"); - - let expected_args = vec![ - OsStr::new("--api-socket"), - OsStr::new("fd=4"), - OsStr::new("--cmdline"), - OsStr::new("console=ttyS0 root=PARTLABEL=root"), - OsStr::new("--memory"), - OsStr::new("size=256M,shared=on"), - OsStr::new("--console"), - OsStr::new("pty"), - OsStr::new("--kernel"), - kernel_path.as_os_str(), - OsStr::new("--disk"), - &expected_disk_arg, - OsStr::new("--serial"), - OsStr::new("file=/run/testvm.log"), - ]; - - assert!(command.get_args().eq(expected_args.into_iter())); - Ok(()) -} diff --git a/host/start-vm/tests/vm_command-multiple-disks.rs b/host/start-vm/tests/vm_command-multiple-disks.rs deleted file mode 100644 index cd1e997..0000000 --- a/host/start-vm/tests/vm_command-multiple-disks.rs +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> - -use std::collections::BTreeSet; -use std::ffi::{OsStr, OsString}; -use std::fs::{create_dir, create_dir_all, File}; -use std::os::unix::fs::symlink; - -use start_vm::vm_command; -use test_helper::TempDir; - -fn main() -> std::io::Result<()> { - let service_dir_parent = TempDir::new()?; - let service_dir = service_dir_parent.path().join("vm-testvm"); - - let vm_dir = TempDir::new()?; - let vm_config = vm_dir.path().join("testvm"); - - create_dir_all(&vm_config)?; - File::create(vm_config.join("vmlinux"))?; - create_dir(vm_config.join("blk"))?; - - let image_paths: Vec<_> = (1..=2) - .map(|n| vm_config.join(format!("blk/disk{n}.img"))) - .collect(); - - for image_path in &image_paths { - symlink("/dev/null", image_path)?; - } - - let command = vm_command(&service_dir, vm_dir.path(), -1).unwrap(); - let mut args = command.get_args(); - - assert!(args.any(|arg| arg == "--disk")); - - let expected_disk_args = image_paths - .iter() - .map(|image_path| { - let mut expected_disk_arg = OsString::from("path="); - expected_disk_arg.push(image_path); - expected_disk_arg.push(",readonly=on"); - expected_disk_arg - }) - .collect::<BTreeSet<_>>(); - - let disk_args = args - .map(OsStr::to_os_string) - .take(expected_disk_args.len()) - .collect::<BTreeSet<_>>(); - - assert_eq!(disk_args, expected_disk_args); - - Ok(()) -} diff --git a/host/start-vm/tests/vm_command-shared-dir.rs b/host/start-vm/tests/vm_command-shared-dir.rs deleted file mode 100644 index 6183f6d..0000000 --- a/host/start-vm/tests/vm_command-shared-dir.rs +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> - -use std::collections::BTreeSet; -use std::ffi::{OsStr, OsString}; -use std::fs::{create_dir, create_dir_all, File}; -use std::os::unix::fs::symlink; - -use start_vm::vm_command; -use test_helper::TempDir; - -fn main() -> std::io::Result<()> { - let service_dir_parent = TempDir::new()?; - let service_dir = service_dir_parent.path().join("vm-testvm"); - - let vm_dir = TempDir::new()?; - let vm_config = vm_dir.path().join("testvm"); - - create_dir_all(&vm_config)?; - File::create(vm_config.join("vmlinux"))?; - create_dir(vm_config.join("blk"))?; - symlink("/dev/null", vm_config.join("blk/root.img"))?; - - create_dir(vm_config.join("shared-dirs"))?; - - create_dir(vm_config.join("shared-dirs/dir1"))?; - symlink("/", vm_config.join("shared-dirs/dir1/dir"))?; - - create_dir(vm_config.join("shared-dirs/dir2"))?; - symlink("/", vm_config.join("shared-dirs/dir2/dir"))?; - - let command = vm_command(&service_dir, vm_dir.path(), -1).unwrap(); - let mut args = command.get_args(); - - assert!(args.any(|arg| arg == "--fs")); - - let expected_fs_args = (1..=2) - .map(|i| format!("tag=dir{i},socket=../fs-testvm-dir{i}/env/virtiofsd.sock")) - .map(OsString::from) - .collect::<BTreeSet<_>>(); - - let fs_args = args - .map(OsStr::to_os_string) - .take(expected_fs_args.len()) - .collect::<BTreeSet<_>>(); - - assert_eq!(fs_args, expected_fs_args); - - Ok(()) -} diff --git a/host/start-vm/ch.h b/host/start-vmm/ch.h index 9007153..5143723 100644 --- a/host/start-vm/ch.h +++ b/host/start-vmm/ch.h @@ -1,11 +1,16 @@ // SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> #include <stdint.h> struct ch_device; -int ch_add_net(const char *vm_name, int tap, const uint8_t mac[6], +struct net_config { + int fd; + uint8_t mac[6]; +}; + +int ch_add_net(const char *vm_name, const struct net_config *, struct ch_device **out); int ch_remove_device(const char *vm_name, struct ch_device *); diff --git a/host/start-vmm/ch.rs b/host/start-vmm/ch.rs new file mode 100644 index 0000000..0ef1354 --- /dev/null +++ b/host/start-vmm/ch.rs @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +use std::ffi::{CStr, OsStr, OsString}; +use std::io::Write; +use std::mem::take; +use std::num::NonZeroI32; +use std::os::raw::{c_char, c_int}; +use std::os::unix::prelude::*; +use std::process::{Command, Stdio}; + +use miniserde::{json, Serialize}; + +use crate::net::MacAddress; + +// Trivially safe. +const EINVAL: NonZeroI32 = unsafe { NonZeroI32::new_unchecked(22) }; +const EPERM: NonZeroI32 = unsafe { NonZeroI32::new_unchecked(1) }; +const EPROTO: NonZeroI32 = unsafe { NonZeroI32::new_unchecked(71) }; + +#[derive(Serialize)] +pub struct ConsoleConfig { + pub mode: &'static str, + pub file: Option<String>, +} + +#[derive(Serialize)] +pub struct DiskConfig { + pub path: String, + pub readonly: bool, +} + +#[derive(Serialize)] +pub struct FsConfig { + pub socket: String, + pub tag: String, +} + +#[derive(Serialize)] +pub struct GpuConfig { + pub socket: String, +} + +#[derive(Serialize)] +#[repr(C)] +pub struct NetConfig { + pub fd: RawFd, + pub mac: MacAddress, +} + +#[derive(Serialize)] +pub struct MemoryConfig { + pub size: i64, + pub shared: bool, +} + +#[derive(Serialize)] +pub struct PayloadConfig { + pub kernel: String, + pub cmdline: &'static str, +} + +#[derive(Serialize)] +pub struct VmConfig { + pub console: ConsoleConfig, + pub disks: Vec<DiskConfig>, + pub fs: Vec<FsConfig>, + pub gpu: Vec<GpuConfig>, + pub memory: MemoryConfig, + pub net: Vec<NetConfig>, + pub payload: PayloadConfig, + pub serial: ConsoleConfig, +} + +fn command(vm_name: &str, s: impl AsRef<OsStr>) -> Command { + let mut api_socket_path = OsString::from("/run/service/ext-vm-"); + api_socket_path.push(vm_name); + api_socket_path.push("/env/cloud-hypervisor.sock"); + + let mut command = Command::new("ch-remote"); + command.stdin(Stdio::null()); + command.arg("--api-socket"); + command.arg(api_socket_path); + command.arg(s); + command +} + +pub fn create_vm(vm_name: &str, mut config: VmConfig) -> Result<(), String> { + // Net devices can't be created from file descriptors in vm.create. + // https://github.com/cloud-hypervisor/cloud-hypervisor/issues/5523 + let nets = take(&mut config.net); + + let mut ch_remote = command(vm_name, "create") + .args(["--", "-"]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| format!("failed to start ch-remote: {e}"))?; + + let json = json::to_string(&config); + write!(ch_remote.stdin.as_ref().unwrap(), "{}", json) + .map_err(|e| format!("writing to ch-remote's stdin: {e}"))?; + + let status = ch_remote + .wait() + .map_err(|e| format!("waiting for ch-remote: {e}"))?; + if status.success() { + } else if let Some(code) = status.code() { + return Err(format!("ch-remote exited {code}")); + } else { + let signal = status.signal().unwrap(); + return Err(format!("ch-remote killed by signal {signal}")); + } + + for net in nets { + add_net(vm_name, &net).map_err(|e| format!("failed to add net: {e}"))?; + } + + Ok(()) +} + +pub fn add_net(vm_name: &str, net: &NetConfig) -> Result<OsString, NonZeroI32> { + let mut ch_remote = command(vm_name, "add-net") + .arg(format!("fd={},mac={}", net.fd, net.mac)) + .stdout(Stdio::piped()) + .spawn() + .or(Err(EPERM))?; + + let jq_out = match Command::new("jq") + .args(["-j", ".id"]) + .stdin(ch_remote.stdout.take().unwrap()) + .stderr(Stdio::inherit()) + .output() + { + Ok(o) => o, + Err(_) => { + // Try not to leave a zombie. + let _ = ch_remote.kill(); + let _ = ch_remote.wait(); + return Err(EPERM); + } + }; + + if let Ok(ch_remote_status) = ch_remote.wait() { + if ch_remote_status.success() && jq_out.status.success() { + return Ok(OsString::from_vec(jq_out.stdout)); + } + } + + Err(EPROTO) +} + +pub fn remove_device(vm_name: &str, device_id: &OsStr) -> Result<(), NonZeroI32> { + let ch_remote = command(vm_name, "remove-device") + .arg(device_id) + .status() + .or(Err(EPERM))?; + + if ch_remote.success() { + Ok(()) + } else { + Err(EPROTO) + } +} + +/// # Safety +/// +/// - `vm_name` must point to a valid C string. +/// - `tap` must be a file descriptor describing an tap device. +/// - `mac` must be a valid pointer. +#[export_name = "ch_add_net"] +unsafe extern "C" fn add_net_c( + vm_name: *const c_char, + net: &NetConfig, + id: *mut *mut OsString, +) -> c_int { + let Ok(vm_name) = CStr::from_ptr(vm_name).to_str() else { + return EINVAL.into(); + }; + + match add_net(vm_name, net) { + Err(e) => e.get(), + Ok(id_str) => { + if !id.is_null() { + let token = Box::into_raw(Box::new(id_str)); + *id = token; + } + 0 + } + } +} + +/// # Safety +/// +/// - `vm_name` must point to a valid C string. +/// - `id` must be a device ID obtained by calling `add_net_c`. After +/// calling `remove_device_c`, the pointer is no longer valid. +#[export_name = "ch_remove_device"] +unsafe extern "C" fn remove_device_c(vm_name: *const c_char, device_id: *mut OsString) -> c_int { + let Ok(vm_name) = CStr::from_ptr(vm_name).to_str() else { + return EINVAL.into(); + }; + let device_id = Box::from_raw(device_id); + + if let Err(e) = remove_device(vm_name, device_id.as_ref()) { + e.get() + } else { + 0 + } +} + +/// # Safety +/// +/// `id` must be a device ID obtained by calling `add_net_c`. After +/// calling `device_free`, the pointer is no longer valid. +#[export_name = "ch_device_free"] +unsafe extern "C" fn device_free(id: *mut OsString) { + if !id.is_null() { + drop(Box::from_raw(id)) + } +} diff --git a/host/start-vmm/default.nix b/host/start-vmm/default.nix new file mode 100644 index 0000000..3f5539c --- /dev/null +++ b/host/start-vmm/default.nix @@ -0,0 +1,126 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +import ../../lib/call-package.nix ( +{ src, lib, stdenv, fetchCrate, fetchFromGitHub, fetchurl, buildPackages +, ninja, rustc, clippy +}: + +let + packageCache = [ + (fetchCrate { + pname = "itoa"; + version = "1.0.9"; + unpack = false; + hash = "sha256-rxUKtoj/ISL87yKb6Jy1DdZq+eAaT/MgzBN+7Mm6zDg="; + }) + (fetchurl { + name = "miniserde-0.1.34.tar.gz"; + url = "https://github.com/dtolnay/miniserde/archive/0.1.34.tar.gz"; + hash = "sha256-LqDZUV6rPBH+PoY//ej/1EKsAVLLMb9RVNpFQmPwWsk="; + }) + (fetchCrate { + pname = "proc-macro2"; + version = "1.0.69"; + unpack = false; + hash = "sha256-E0wYn+tJVrIPb1R9LPcn1MD+BnIrIKDuyH7URal/kto="; + }) + (fetchCrate { + pname = "quote"; + version = "1.0.33"; + unpack = false; + hash = "sha256-Umf8pElgKGKKlRYPxCOjPosuavilMCV54yLktSApPK4="; + }) + (fetchCrate { + pname = "ryu"; + version = "1.0.15"; + unpack = false; + hash = "sha256-GtTMjaTvcj7WC87SARgdg3ka1DMhPYwk7//aHuyF10E="; + }) + (fetchCrate { + pname = "syn"; + version = "2.0.38"; + unpack = false; + hash = "sha256-6Wt5qqE3249h4mNjoMm0fYtOx12ii30dYUwjA+IyQIs="; + }) + (fetchCrate { + pname = "unicode-ident"; + version = "1.0.12"; + unpack = false; + hash = "sha256-M1S5rD+uH/Z1XLbbU2g622YWNPZ1V5Qt6k+s6+wP7ks="; + }) + ]; +in + +stdenv.mkDerivation (finalAttrs: { + name = "start-vmm"; + + src = lib.fileset.toSource { + root = ../..; + fileset = lib.fileset.intersection src ./.; + }; + sourceRoot = "source/host/start-vmm"; + + depsBuildBuild = [ buildPackages.stdenv.cc ]; + nativeBuildInputs = [ + (buildPackages.meson.overrideAttrs ({ version, patches ? [], ... }: { + version = "1.3.0"; + + src = assert lib.versionOlder version "1.3.0"; fetchFromGitHub { + owner = "mesonbuild"; + repo = "meson"; + rev = "1.3.0"; + hash = "sha256-Jt3PWnbv/8P6Rvf3E/Yli2vdtfgx3CmsW+jlc9CK5KA="; + }; + + patches = [ + ../../pkgs/meson/rust-static.patch + ] ++ builtins.filter (p: !lib.elem p.name or (baseNameOf p) [ + "d5252c5d4cf1c1931fef0c1c98dd66c000891d21.patch" + ]) patches; + })) + ninja rustc + ]; + + postPatch = lib.concatMapStringsSep "\n" (crate: '' + mkdir -p subprojects/packagecache + ln -s ${crate} subprojects/packagecache/${crate.name} + '') packageCache; + + preConfigure = '' + mesonFlagsArray+=(-Drust_args="-C panic=abort" -Dtests=false -Dwerror=true) + ''; + + passthru.tests = { + clippy = finalAttrs.finalPackage.overrideAttrs ( + { name, nativeBuildInputs ? [], ... }: + { + name = "${name}-clippy"; + nativeBuildInputs = nativeBuildInputs ++ [ clippy ]; + RUSTC = "clippy-driver"; + preConfigure = '' + # It's not currently possible to enable warnings only for + # non-subprojects without enumerating the subprojects. + # https://github.com/mesonbuild/meson/issues/9398#issuecomment-954094750 + mesonFlagsArray+=( + -Dwerror=true + -Dproc-macro2:werror=false + -Dproc-macro2:warning_level=0 + ) + ''; + postBuild = ''touch $out && exit 0''; + } + ); + + tests = finalAttrs.finalPackage.overrideAttrs ({ name, ... }: { + name = "${name}-tests"; + preConfigure = ""; + doCheck = true; + }); + + meta = { + mainProgram = "start-vmm"; + }; + }; +}) +) (_: {}) diff --git a/host/start-vmm/fork.c b/host/start-vmm/fork.c new file mode 100644 index 0000000..2ec17c2 --- /dev/null +++ b/host/start-vmm/fork.c @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +#include <errno.h> +#include <stdlib.h> +#include <unistd.h> + +#include <sys/wait.h> + +// Positive return value: in grandparent, pid of grandchild. +// 0: in grandchild. +// Negative return value: errno. +int double_fork(void) +{ + int fd[2], v; + size_t acc = 0; + ssize_t r; + pid_t child; + + if (pipe(fd) == -1) + return -1; + + switch (child = fork()) { + case -1: + close(fd[0]); + close(fd[1]); + return -1; + case 0: + close(fd[0]); + switch ((v = fork())) { + case 0: + close(fd[1]); + return 0; + case -1: + v = -errno; + [[fallthrough]]; + default: + do { + r = write(fd[1], (char *)&v + acc, sizeof v - acc); + } while ((r != -1 || errno == EINTR) && (acc += r) < sizeof v); + exit(v < 0 || r == -1); + } + default: + close(fd[1]); + do { + r = read(fd[0], (char *)&v + acc, sizeof v - acc); + } while ((r != -1 || errno == EINTR) && (acc += r) < sizeof v); + close(fd[0]); + if (r == -1) { + kill(child, SIGKILL); + waitpid(child, NULL, 0); + } + return v; + } +} diff --git a/host/start-vmm/fork.rs b/host/start-vmm/fork.rs new file mode 100644 index 0000000..3c0e11a --- /dev/null +++ b/host/start-vmm/fork.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +use std::ffi::c_int; + +extern "C" { + pub fn double_fork() -> c_int; +} diff --git a/host/start-vmm/lib.rs b/host/start-vmm/lib.rs new file mode 100644 index 0000000..f927bf2 --- /dev/null +++ b/host/start-vmm/lib.rs @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +mod ch; +mod fork; +mod net; +mod s6; +mod unix; + +use std::borrow::Cow; +use std::env::args_os; +use std::ffi::{CString, OsStr}; +use std::fs::remove_file; +use std::io::{self, ErrorKind}; +use std::os::unix::net::UnixListener; +use std::os::unix::prelude::*; +use std::os::unix::process::parent_id; +use std::path::Path; +use std::process::{exit, Command}; + +use ch::{ConsoleConfig, DiskConfig, FsConfig, GpuConfig, MemoryConfig, PayloadConfig, VmConfig}; +use fork::double_fork; +use net::net_setup; +use s6::notify_readiness; +use unix::clear_cloexec; + +const SIGTERM: i32 = 15; + +extern "C" { + fn kill(pid: i32, sig: i32) -> i32; +} + +pub fn prog_name() -> String { + args_os() + .next() + .as_ref() + .map(Path::new) + .and_then(Path::file_name) + .map(OsStr::to_string_lossy) + .unwrap_or(Cow::Borrowed("start-vmm")) + .into_owned() +} + +pub fn create_api_socket() -> Result<UnixListener, String> { + let _ = remove_file("env/cloud-hypervisor.sock"); + let api_socket = UnixListener::bind("env/cloud-hypervisor.sock") + .map_err(|e| format!("creating API socket: {e}"))?; + + // Safe because we own api_socket. + if unsafe { clear_cloexec(api_socket.as_fd()) } == -1 { + let errno = io::Error::last_os_error(); + return Err(format!("clearing CLOEXEC on API socket fd: {}", errno)); + } + + Ok(api_socket) +} + +pub fn vm_config(vm_name: &str, config_root: &Path) -> Result<VmConfig, String> { + if config_root.to_str().is_none() { + return Err(format!("config root {:?} is not valid UTF-8", config_root)); + } + + let config_dir = config_root.join(vm_name); + + let blk_dir = config_dir.join("blk"); + let kernel_path = config_dir.join("vmlinux"); + let net_providers_dir = config_dir.join("providers/net"); + let shared_dirs_dir = config_dir.join("shared-dirs"); + let wayland_path = config_dir.join("wayland"); + + Ok(VmConfig { + console: ConsoleConfig { + mode: "Pty", + file: None, + }, + disks: match blk_dir.read_dir() { + Ok(entries) => entries + .into_iter() + .map(|result| { + Ok(result + .map_err(|e| format!("examining directory entry: {e}"))? + .path()) + }) + .filter(|result| { + result + .as_ref() + .map(|entry| entry.extension() == Some(OsStr::new("img"))) + .unwrap_or(true) + }) + .map(|result: Result<_, String>| { + let entry = result?.to_str().unwrap().to_string(); + + if entry.contains(',') { + return Err(format!("illegal ',' character in path {:?}", entry)); + } + + Ok(DiskConfig { + path: entry, + readonly: true, + }) + }) + .collect::<Result<_, _>>()?, + Err(e) => return Err(format!("reading directory {:?}: {}", blk_dir, e)), + }, + fs: match shared_dirs_dir.read_dir() { + Ok(entries) => entries + .into_iter() + .map(|result| { + let entry = result + .map_err(|e| format!("examining directory entry: {}", e))? + .file_name(); + + let entry = entry.to_str().ok_or_else(|| { + format!("shared directory name {:?} is not valid UTF-8", entry) + })?; + + Ok(FsConfig { + tag: entry.to_string(), + socket: format!("../fs-{vm_name}-{entry}/env/virtiofsd.sock"), + }) + }) + .collect::<Result<_, String>>()?, + Err(e) if e.kind() == ErrorKind::NotFound => Default::default(), + Err(e) => return Err(format!("reading directory {:?}: {e}", shared_dirs_dir)), + }, + gpu: match wayland_path.try_exists() { + Ok(true) => vec![GpuConfig { + socket: format!("../gpu-{vm_name}/env/crosvm.sock"), + }], + Ok(false) => vec![], + Err(e) => return Err(format!("checking for existence of {:?}: {e}", wayland_path)), + }, + memory: MemoryConfig { + size: 256 << 20, + shared: true, + }, + net: match net_providers_dir.read_dir() { + Ok(entries) => entries + .into_iter() + .map(|result| { + let entry = result + .map_err(|e| format!("examining directory entry: {}", e))? + .file_name(); + + // Safe because provider_name is the name of a directory entry, so + // can't contain a null byte. + let provider_name = unsafe { CString::from_vec_unchecked(entry.into_vec()) }; + + // Safe because we pass a valid pointer and check the result. + let net = unsafe { net_setup(provider_name.as_ptr()) }; + if net.fd == -1 { + let e = io::Error::last_os_error(); + return Err(format!("setting up networking failed: {e}")); + } + + Ok(net) + }) + // TODO: to support multiple net providers, we'll need + // a better naming scheme for tap and bridge devices. + .take(1) + .collect::<Result<_, _>>()?, + Err(e) if e.kind() == ErrorKind::NotFound => Default::default(), + Err(e) => return Err(format!("reading directory {:?}: {e}", net_providers_dir)), + }, + payload: PayloadConfig { + kernel: kernel_path.to_str().unwrap().to_string(), + cmdline: "console=ttyS0 root=PARTLABEL=root", + }, + serial: ConsoleConfig { + mode: "File", + file: Some(format!("/run/{vm_name}.log")), + }, + }) +} + +/// # Safety +/// +/// Calls [notify_readiness], so can only be called once per process. +unsafe fn create_vm_child_main(vm_name: &str, config: VmConfig) -> ! { + if let Err(e) = ch::create_vm(vm_name, config) { + eprintln!("{}: creating VM: {e}", prog_name()); + if kill(parent_id() as _, SIGTERM) == -1 { + let e = io::Error::last_os_error(); + eprintln!("{}: killing cloud-hypervisor: {e}", prog_name()); + }; + exit(1); + } + + if let Err(e) = notify_readiness() { + eprintln!("{}: failed to notify readiness: {e}", prog_name()); + exit(1); + } + + exit(0) +} + +pub fn create_vm(dir: &Path, config_root: &Path) -> Result<(), String> { + let vm_name = dir + .file_name() + .ok_or_else(|| "directory has no name".to_string())?; + + let vm_name = &vm_name + .to_str() + .ok_or_else(|| format!("VM name {:?} is not valid UTF-8", vm_name))?; + + if !vm_name.starts_with("vm-") { + return Err("not running from a VM service directory".to_string()); + } + + if vm_name.contains(',') { + return Err(format!("VM name may not contain a comma: {:?}", vm_name)); + } + + let vm_name = &vm_name[3..]; + let config = vm_config(vm_name, config_root)?; + + // SAFETY: safe because we ensure we don't violate any invariants + // concerning OS resources shared between processes, by only + // passing data structs to the child main function. + match unsafe { double_fork() } { + e if e < 0 => Err(format!("double fork: {}", io::Error::from_raw_os_error(-e))), + // SAFETY: create_vm_child_main can only be called once per process, + // but this is a new process, so we know it hasn't been called before. + 0 => unsafe { create_vm_child_main(vm_name, config) }, + _ => Ok(()), + } +} + +pub fn vm_command(api_socket_fd: RawFd) -> Result<Command, String> { + let mut command = Command::new("cloud-hypervisor"); + command.args(["--api-socket", &format!("fd={api_socket_fd}")]); + + Ok(command) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_vm_name_comma() { + let e = create_vm(Path::new("/vm-,"), Path::new("/")).unwrap_err(); + assert!(e.contains("comma"), "unexpected error: {:?}", e); + } +} diff --git a/host/start-vmm/meson.build b/host/start-vmm/meson.build new file mode 100644 index 0000000..564be1b --- /dev/null +++ b/host/start-vmm/meson.build @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +project('start-vmm', 'rust', 'c', + default_options : ['c_std=c2x', 'rust_std=2018']) + +add_project_arguments('-D_GNU_SOURCE', '-Wno-error=attributes', language : 'c') + +miniserde_dep = dependency('miniserde') + +c_lib = static_library('start-vmm', 'fork.c', 'net.c', 'net-util.c', 'unix.c') +rust_lib = static_library('start_vmm', 'lib.rs', + dependencies : miniserde_dep, + link_with : c_lib) + +rust_lib_dep = declare_dependency( + dependencies : miniserde_dep, + link_with : rust_lib) + +executable('start-vmm', 'start-vmm.rs', + dependencies : miniserde_dep, + link_with : rust_lib, + install : true) + +if get_option('tests') + test_exe = executable('start-vmm-test', 'lib.rs', + dependencies : miniserde_dep, + rust_args : ['--test'], + link_with : c_lib) + test('Rust unit tests', test_exe, protocol : 'rust') + + subdir('tests') +endif diff --git a/host/start-vmm/meson_options.txt b/host/start-vmm/meson_options.txt new file mode 100644 index 0000000..21844bd --- /dev/null +++ b/host/start-vmm/meson_options.txt @@ -0,0 +1,6 @@ +# SPDX-License-Identifier: EUPL-1.2+ +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +option('tests', + type : 'boolean', + description : 'Build the tests') diff --git a/host/start-vm/net-util.c b/host/start-vmm/net-util.c index 1d2fb33..1d2fb33 100644 --- a/host/start-vm/net-util.c +++ b/host/start-vmm/net-util.c diff --git a/host/start-vm/net-util.h b/host/start-vmm/net-util.h index 5ec09c2..5ec09c2 100644 --- a/host/start-vm/net-util.h +++ b/host/start-vmm/net-util.h diff --git a/host/start-vm/net.c b/host/start-vmm/net.c index c8409f4..4fc5486 100644 --- a/host/start-vm/net.c +++ b/host/start-vmm/net.c @@ -1,5 +1,5 @@ // SPDX-License-Identifier: EUPL-1.2+ -// SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> #include "ch.h" #include "net-util.h" @@ -51,12 +51,15 @@ static int client_net_setup(const char *bridge_name) static int router_net_setup(const char *bridge_name, const char *router_vm_name, const uint8_t mac[6], struct ch_device **out) { - int e, fd = setup_tap(bridge_name, "router"); - if (fd == -1) + struct net_config net; + int e; + + memcpy(&net.mac, mac, sizeof net.mac); + if ((net.fd = setup_tap(bridge_name, "router")) == -1) return -1; - e = ch_add_net(router_vm_name, fd, mac, out); - close(fd); + e = ch_add_net(router_vm_name, &net, out); + close(net.fd); if (!e) return 0; errno = e; @@ -149,11 +152,6 @@ static int exit_listener_setup(const char *router_vm_name, } } -struct net_config { - int fd; - char mac[6]; -}; - struct net_config net_setup(const char *router_vm_name) { struct ch_device *router_vm_net_device = NULL; diff --git a/host/start-vmm/net.rs b/host/start-vmm/net.rs new file mode 100644 index 0000000..8ca19c5 --- /dev/null +++ b/host/start-vmm/net.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +use std::borrow::Cow; +use std::fmt::{self, Display, Formatter}; +use std::os::raw::c_char; + +use miniserde::ser::Fragment; +use miniserde::Serialize; + +use crate::ch::NetConfig; + +#[repr(transparent)] +pub struct MacAddress([u8; 6]); + +impl MacAddress { + pub fn new(octets: [u8; 6]) -> Self { + Self(octets) + } +} + +impl Display for MacAddress { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!( + f, + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5] + ) + } +} + +impl Serialize for MacAddress { + fn begin(&self) -> Fragment { + Fragment::Str(Cow::Owned(self.to_string())) + } +} + +extern "C" { + pub fn net_setup(provider_vm_name: *const c_char) -> NetConfig; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mac_to_string_all_zero() { + assert_eq!(MacAddress([0; 6]).to_string(), "00:00:00:00:00:00"); + } + + #[test] + fn mac_to_string_hex() { + let mac = MacAddress([0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54]); + assert_eq!(mac.to_string(), "FE:DC:BA:98:76:54"); + } +} diff --git a/host/start-vm/s6.rs b/host/start-vmm/s6.rs index eda587f..eda587f 100644 --- a/host/start-vm/s6.rs +++ b/host/start-vmm/s6.rs diff --git a/host/start-vm/shell.nix b/host/start-vmm/shell.nix index ed1a190..ed1a190 100644 --- a/host/start-vm/shell.nix +++ b/host/start-vmm/shell.nix diff --git a/host/start-vm/start-vm.rs b/host/start-vmm/start-vmm.rs index 7dfca09..5479b5f 100644 --- a/host/start-vm/start-vm.rs +++ b/host/start-vmm/start-vmm.rs @@ -6,7 +6,7 @@ use std::os::unix::prelude::*; use std::path::Path; use std::process::exit; -use start_vm::{create_api_socket, notify_readiness, prog_name, vm_command}; +use start_vmm::{create_api_socket, create_vm, prog_name, vm_command}; /// # Safety /// @@ -22,11 +22,11 @@ unsafe fn run() -> String { Err(e) => return e, }; - if let Err(e) = notify_readiness() { + if let Err(e) = create_vm(&dir, Path::new("/run/vm")) { return e; } - match vm_command(&dir, Path::new("/run/vm"), api_socket.into_raw_fd()) { + match vm_command(api_socket.into_raw_fd()) { Ok(mut command) => format!("failed to exec: {}", command.exec()), Err(e) => e, } diff --git a/host/start-vmm/subprojects/itoa.wrap b/host/start-vmm/subprojects/itoa.wrap new file mode 100644 index 0000000..d58e58e --- /dev/null +++ b/host/start-vmm/subprojects/itoa.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = itoa-1.0.9 +source_url = https://crates.io/api/v1/crates/itoa/1.0.9/download +source_filename = itoa-1.0.9.tar.gz +source_hash = af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38 +patch_directory = itoa diff --git a/host/start-vmm/subprojects/miniserde.wrap b/host/start-vmm/subprojects/miniserde.wrap new file mode 100644 index 0000000..47c33be --- /dev/null +++ b/host/start-vmm/subprojects/miniserde.wrap @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +# The GitHub archive is used so that mini_internal is included, +# and can be built with the same meson.build, +# to avoid exposing it as a dependency. +[wrap-file] +directory = miniserde-0.1.34 +source_url = https://github.com/dtolnay/miniserde/archive/0.1.34.tar.gz +source_filename = miniserde-0.1.34.tar.gz +source_hash = 2ea0d9515eab3c11fe3e863ffde8ffd442ac0152cb31bf5154da454263f05ac9 +depth = 1 +patch_directory = miniserde diff --git a/host/start-vmm/subprojects/packagefiles/itoa/meson.build b/host/start-vmm/subprojects/packagefiles/itoa/meson.build new file mode 100644 index 0000000..b572a1f --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/itoa/meson.build @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('itoa', 'rust', version : '1.0.9', default_options : ['rust_std=2018']) + +itoa = static_library('itoa', 'src/lib.rs', rust_crate_type : 'rlib') + +itoa_dep = declare_dependency(link_with : itoa) + +meson.override_dependency('itoa', itoa_dep) diff --git a/host/start-vmm/subprojects/packagefiles/miniserde/meson.build b/host/start-vmm/subprojects/packagefiles/miniserde/meson.build new file mode 100644 index 0000000..8765a8e --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/miniserde/meson.build @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('miniserde', 'rust', version : '0.1.34', + default_options : ['build.rust_std=2021', 'rust_std=2021']) + +quote_dep = dependency('quote', native : true) +syn_dep = dependency('syn', native : true) + +mini_internal = shared_library('mini_internal', 'derive/src/lib.rs', + dependencies : [quote_dep, syn_dep], + native : true, + rust_args : ['-C', 'panic=unwind'], + rust_crate_type : 'proc-macro') + +itoa_dep = dependency('itoa') +ryu_dep = dependency('ryu') + +miniserde = static_library('miniserde', 'src/lib.rs', + dependencies : [itoa_dep, ryu_dep], + link_with : mini_internal, + rust_crate_type : 'rlib') + +miniserde_dep = declare_dependency( + dependencies : [itoa_dep, ryu_dep], + link_with : miniserde) + +meson.override_dependency('miniserde', miniserde_dep) diff --git a/host/start-vmm/subprojects/packagefiles/proc-macro2/meson.build b/host/start-vmm/subprojects/packagefiles/proc-macro2/meson.build new file mode 100644 index 0000000..4366da5 --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/proc-macro2/meson.build @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('proc-macro2', 'rust', version : '1.0.69', + default_options : ['build.rust_std=2021', 'rust_std=2021']) + +unicode_ident_dep = dependency('unicode-ident', native : true) + +proc_macro2 = static_library('proc_macro2', 'src/lib.rs', + dependencies : unicode_ident_dep, + native : true, + rust_args : ['-C', 'panic=unwind', '--cfg', 'feature="proc-macro"'], + rust_crate_type : 'rlib') + +proc_macro2_dep = declare_dependency( + dependencies : unicode_ident_dep, + link_with : proc_macro2) + +meson.override_dependency('proc-macro2', proc_macro2_dep, native : true) diff --git a/host/start-vmm/subprojects/packagefiles/quote/meson.build b/host/start-vmm/subprojects/packagefiles/quote/meson.build new file mode 100644 index 0000000..98cc7ef --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/quote/meson.build @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('quote', 'rust', version : '1.0.33', + default_options : ['build.rust_std=2018', 'rust_std=2018']) + +proc_macro2_dep = dependency('proc-macro2', native : true) + +quote = static_library('quote', 'src/lib.rs', + dependencies : proc_macro2_dep, + native : true, + rust_args : ['-C', 'panic=unwind'], + rust_crate_type : 'rlib') + +quote_dep = declare_dependency( + dependencies : proc_macro2_dep, + link_with : quote) + +meson.override_dependency('quote', quote_dep, native : true) diff --git a/host/start-vmm/subprojects/packagefiles/ryu/meson.build b/host/start-vmm/subprojects/packagefiles/ryu/meson.build new file mode 100644 index 0000000..5636bd2 --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/ryu/meson.build @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('ryu', 'rust', version : '1.0.15', default_options : ['rust_std=2018']) + +ryu = static_library('ryu', 'src/lib.rs', rust_crate_type : 'rlib') + +ryu_dep = declare_dependency(link_with : ryu) + +meson.override_dependency('ryu', ryu_dep) diff --git a/host/start-vmm/subprojects/packagefiles/syn/meson.build b/host/start-vmm/subprojects/packagefiles/syn/meson.build new file mode 100644 index 0000000..b8a73e0 --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/syn/meson.build @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('syn', 'rust', version : '2.0.38', + default_options : ['build.rust_std=2021', 'rust_std=2021']) + +proc_macro2_dep = dependency('proc-macro2', native : true) +quote_dep = dependency('quote', native : true) + +syn = static_library('syn', 'src/lib.rs', + dependencies : [proc_macro2_dep, quote_dep], + native : true, + rust_args : [ + '-C', 'panic=unwind', + '--cfg', 'feature="clone-impls"', + '--cfg', 'feature="derive"', + '--cfg', 'feature="parsing"', + '--cfg', 'feature="printing"', + '--cfg', 'feature="proc-macro"', + ], + rust_crate_type : 'rlib') + +syn_dep = declare_dependency( + dependencies : [proc_macro2_dep, quote_dep], + link_with : syn) + +meson.override_dependency('syn', syn_dep, native : true) diff --git a/host/start-vmm/subprojects/packagefiles/unicode-ident/meson.build b/host/start-vmm/subprojects/packagefiles/unicode-ident/meson.build new file mode 100644 index 0000000..7146ec4 --- /dev/null +++ b/host/start-vmm/subprojects/packagefiles/unicode-ident/meson.build @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-License-Identifier: MIT + +project('unicode-ident', 'rust', version : '1.0.9', + default_options : ['build.rust_std=2018', 'rust_std=2018']) + +unicode_ident = static_library('unicode_ident', 'src/lib.rs', + native : true, + rust_args : ['-C', 'panic=unwind'], + rust_crate_type : 'rlib') + +unicode_ident_dep = declare_dependency(link_with : unicode_ident) + +meson.override_dependency('unicode-ident', unicode_ident_dep, native : true) diff --git a/host/start-vmm/subprojects/proc-macro2.wrap b/host/start-vmm/subprojects/proc-macro2.wrap new file mode 100644 index 0000000..53a372c --- /dev/null +++ b/host/start-vmm/subprojects/proc-macro2.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = proc-macro2-1.0.69 +source_url = https://crates.io/api/v1/crates/proc-macro2/1.0.69/download +source_filename = proc-macro2-1.0.69.tar.gz +source_hash = 134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da +patch_directory = proc-macro2 diff --git a/host/start-vmm/subprojects/quote.wrap b/host/start-vmm/subprojects/quote.wrap new file mode 100644 index 0000000..7995979 --- /dev/null +++ b/host/start-vmm/subprojects/quote.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = quote-1.0.33 +source_url = https://crates.io/api/v1/crates/quote/1.0.33/download +source_filename = quote-1.0.33.tar.gz +source_hash = 5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae +patch_directory = quote diff --git a/host/start-vmm/subprojects/ryu.wrap b/host/start-vmm/subprojects/ryu.wrap new file mode 100644 index 0000000..bbc4e75 --- /dev/null +++ b/host/start-vmm/subprojects/ryu.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = ryu-1.0.15 +source_url = https://crates.io/api/v1/crates/ryu/1.0.15/download +source_filename = ryu-1.0.15.tar.gz +source_hash = 1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741 +patch_directory = ryu diff --git a/host/start-vmm/subprojects/syn.wrap b/host/start-vmm/subprojects/syn.wrap new file mode 100644 index 0000000..ce08410 --- /dev/null +++ b/host/start-vmm/subprojects/syn.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = syn-2.0.38 +source_url = https://crates.io/api/v1/crates/syn/2.0.38/download +source_filename = syn-2.0.38.tar.gz +source_hash = e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b +patch_directory = syn diff --git a/host/start-vmm/subprojects/unicode-ident.wrap b/host/start-vmm/subprojects/unicode-ident.wrap new file mode 100644 index 0000000..e6ed206 --- /dev/null +++ b/host/start-vmm/subprojects/unicode-ident.wrap @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: CC0-1.0 +# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +[wrap-file] +directory = unicode-ident-1.0.12 +source_url = https://crates.io/api/v1/crates/unicode-ident/1.0.12/download +source_filename = unicode-ident-1.0.12.tar.gz +source_hash = 3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b +patch_directory = unicode-ident diff --git a/host/start-vm/tests/bridge_add-%d.c b/host/start-vmm/tests/bridge_add-%d.c index 17e6013..17e6013 100644 --- a/host/start-vm/tests/bridge_add-%d.c +++ b/host/start-vmm/tests/bridge_add-%d.c diff --git a/host/start-vm/tests/bridge_add-name-too-long.c b/host/start-vmm/tests/bridge_add-name-too-long.c index ec81373..ec81373 100644 --- a/host/start-vm/tests/bridge_add-name-too-long.c +++ b/host/start-vmm/tests/bridge_add-name-too-long.c diff --git a/host/start-vm/tests/bridge_add.c b/host/start-vmm/tests/bridge_add.c index 693a11f..693a11f 100644 --- a/host/start-vm/tests/bridge_add.c +++ b/host/start-vmm/tests/bridge_add.c diff --git a/host/start-vm/tests/bridge_add_if.c b/host/start-vmm/tests/bridge_add_if.c index f65151c..f65151c 100644 --- a/host/start-vm/tests/bridge_add_if.c +++ b/host/start-vmm/tests/bridge_add_if.c diff --git a/host/start-vm/tests/bridge_remove.c b/host/start-vmm/tests/bridge_remove.c index 9de41fe..9de41fe 100644 --- a/host/start-vm/tests/bridge_remove.c +++ b/host/start-vmm/tests/bridge_remove.c diff --git a/host/start-vm/tests/bridge_remove_if.c b/host/start-vmm/tests/bridge_remove_if.c index ebc7ce2..ebc7ce2 100644 --- a/host/start-vm/tests/bridge_remove_if.c +++ b/host/start-vmm/tests/bridge_remove_if.c diff --git a/host/start-vm/tests/helper.rs b/host/start-vmm/tests/helper.rs index abaf973..9ce55f3 100644 --- a/host/start-vm/tests/helper.rs +++ b/host/start-vmm/tests/helper.rs @@ -9,7 +9,7 @@ use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; use std::sync::OnceLock; -use start_vm::prog_name; +use start_vmm::prog_name; extern "C" { fn mkdtemp(template: *mut c_char) -> *mut c_char; @@ -31,7 +31,7 @@ impl TempDir { }); let mut dirname = tmpdir.clone().into_os_string().into_vec(); - dirname.extend_from_slice(b"/spectrum-start-vm-test-"); + dirname.extend_from_slice(b"/spectrum-start-vmm-test-"); dirname.extend_from_slice(&prog_name().into_bytes()); dirname.extend_from_slice(b".XXXXXX\0"); diff --git a/host/start-vm/tests/if_down.c b/host/start-vmm/tests/if_down.c index c912f4e..c912f4e 100644 --- a/host/start-vm/tests/if_down.c +++ b/host/start-vmm/tests/if_down.c diff --git a/host/start-vm/tests/if_rename-%d.c b/host/start-vmm/tests/if_rename-%d.c index 68dbea2..68dbea2 100644 --- a/host/start-vm/tests/if_rename-%d.c +++ b/host/start-vmm/tests/if_rename-%d.c diff --git a/host/start-vm/tests/if_rename-name-too-long.c b/host/start-vmm/tests/if_rename-name-too-long.c index 668824c..668824c 100644 --- a/host/start-vm/tests/if_rename-name-too-long.c +++ b/host/start-vmm/tests/if_rename-name-too-long.c diff --git a/host/start-vm/tests/if_rename.c b/host/start-vmm/tests/if_rename.c index c73ae92..c73ae92 100644 --- a/host/start-vm/tests/if_rename.c +++ b/host/start-vmm/tests/if_rename.c diff --git a/host/start-vm/tests/if_up.c b/host/start-vmm/tests/if_up.c index 33acb36..33acb36 100644 --- a/host/start-vm/tests/if_up.c +++ b/host/start-vmm/tests/if_up.c diff --git a/host/start-vm/tests/meson.build b/host/start-vmm/tests/meson.build index 9d652ff..7f6bd08 100644 --- a/host/start-vm/tests/meson.build +++ b/host/start-vmm/tests/meson.build @@ -1,8 +1,8 @@ # SPDX-License-Identifier: EUPL-1.2+ -# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> rust_helper = static_library('test_helper', 'helper.rs', - link_with : [c_lib, rust_lib]) + dependencies : rust_lib_dep) test('if_up', executable('if_up', 'if_up.c', '../net-util.c')) test('if_rename', executable('if_rename', 'if_rename.c', '../net-util.c')) @@ -30,8 +30,14 @@ test('tap_open (name too long)', executable('tap_open-name-too-long', 'tap_open-name-too-long.c', '../net-util.c')) test('vm_command-basic', executable('vm_command-basic', - 'vm_command-basic.rs', link_with : [rust_lib, rust_helper])) + 'vm_command-basic.rs', + dependencies : rust_lib_dep, + link_with : rust_helper)) test('vm_command-multiple-disks', executable('vm_command-multiple-disks', - 'vm_command-multiple-disks.rs', link_with : [rust_lib, rust_helper])) + 'vm_command-multiple-disks.rs', + dependencies : rust_lib_dep, + link_with : rust_helper)) test('vm_command-shared-dir', executable('vm_command-shared-dir', - 'vm_command-shared-dir.rs', link_with : [rust_lib, rust_helper])) + 'vm_command-shared-dir.rs', + dependencies : rust_lib_dep, + link_with : rust_helper)) diff --git a/host/start-vm/tests/tap_open-name-too-long.c b/host/start-vmm/tests/tap_open-name-too-long.c index ba4ebd6..ba4ebd6 100644 --- a/host/start-vm/tests/tap_open-name-too-long.c +++ b/host/start-vmm/tests/tap_open-name-too-long.c diff --git a/host/start-vm/tests/tap_open.c b/host/start-vmm/tests/tap_open.c index bf5d00c..bf5d00c 100644 --- a/host/start-vm/tests/tap_open.c +++ b/host/start-vmm/tests/tap_open.c diff --git a/host/start-vmm/tests/vm_command-basic.rs b/host/start-vmm/tests/vm_command-basic.rs new file mode 100644 index 0000000..bc911ae --- /dev/null +++ b/host/start-vmm/tests/vm_command-basic.rs @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +use std::fs::{create_dir_all, File}; +use std::path::PathBuf; + +use start_vmm::vm_config; +use test_helper::TempDir; + +fn main() -> std::io::Result<()> { + let tmp_dir = TempDir::new()?; + + let kernel_path = tmp_dir.path().join("testvm/vmlinux"); + let image_path = tmp_dir.path().join("testvm/blk/root.img"); + + create_dir_all(kernel_path.parent().unwrap())?; + create_dir_all(image_path.parent().unwrap())?; + File::create(&kernel_path)?; + File::create(&image_path)?; + + let mut config = vm_config("testvm", tmp_dir.path()).unwrap(); + + assert_eq!(config.console.mode, "Pty"); + assert_eq!(config.disks.len(), 1); + let disk1 = config.disks.pop().unwrap(); + assert_eq!(PathBuf::from(disk1.path), image_path); + assert!(disk1.readonly); + assert_eq!(PathBuf::from(config.payload.kernel), kernel_path); + assert_eq!(config.payload.cmdline, "console=ttyS0 root=PARTLABEL=root"); + assert_eq!(config.memory.size, 0x10000000); + assert!(config.memory.shared); + assert_eq!(config.serial.mode, "File"); + assert_eq!(config.serial.file.unwrap(), "/run/testvm.log"); + + Ok(()) +} diff --git a/host/start-vmm/tests/vm_command-multiple-disks.rs b/host/start-vmm/tests/vm_command-multiple-disks.rs new file mode 100644 index 0000000..f7f7a74 --- /dev/null +++ b/host/start-vmm/tests/vm_command-multiple-disks.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +use std::collections::BTreeSet; +use std::fs::{create_dir, create_dir_all, File}; +use std::os::unix::fs::symlink; +use std::path::PathBuf; + +use start_vmm::vm_config; +use test_helper::TempDir; + +fn main() -> std::io::Result<()> { + let tmp_dir = TempDir::new()?; + + let vm_config_dir = tmp_dir.path().join("testvm"); + + create_dir_all(&vm_config_dir)?; + File::create(vm_config_dir.join("vmlinux"))?; + create_dir(vm_config_dir.join("blk"))?; + + let image_paths: BTreeSet<_> = (1..=2) + .map(|n| vm_config_dir.join(format!("blk/disk{n}.img"))) + .collect(); + + for image_path in &image_paths { + symlink("/dev/null", image_path)?; + } + + let config = vm_config("testvm", tmp_dir.path()).unwrap(); + assert_eq!(config.disks.len(), 2); + assert!(config.disks.iter().all(|disk| disk.readonly)); + + let actual_paths: BTreeSet<_> = config + .disks + .into_iter() + .map(|disk| PathBuf::from(disk.path)) + .collect(); + + assert_eq!(actual_paths, image_paths); + + Ok(()) +} diff --git a/host/start-vmm/tests/vm_command-shared-dir.rs b/host/start-vmm/tests/vm_command-shared-dir.rs new file mode 100644 index 0000000..89d5a76 --- /dev/null +++ b/host/start-vmm/tests/vm_command-shared-dir.rs @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: EUPL-1.2+ +// SPDX-FileCopyrightText: 2022-2023 Alyssa Ross <hi@alyssa.is> + +use std::collections::BTreeSet; +use std::fs::{create_dir, create_dir_all, File}; +use std::os::unix::fs::symlink; + +use start_vmm::vm_config; +use test_helper::TempDir; + +fn main() -> std::io::Result<()> { + let tmp_dir = TempDir::new()?; + + let vm_config_dir = tmp_dir.path().join("testvm"); + + create_dir_all(&vm_config_dir)?; + File::create(vm_config_dir.join("vmlinux"))?; + create_dir(vm_config_dir.join("blk"))?; + symlink("/dev/null", vm_config_dir.join("blk/root.img"))?; + + create_dir(vm_config_dir.join("shared-dirs"))?; + + create_dir(vm_config_dir.join("shared-dirs/dir1"))?; + symlink("/", vm_config_dir.join("shared-dirs/dir1/dir"))?; + + create_dir(vm_config_dir.join("shared-dirs/dir2"))?; + symlink("/", vm_config_dir.join("shared-dirs/dir2/dir"))?; + + let config = vm_config("testvm", tmp_dir.path()).unwrap(); + assert_eq!(config.fs.len(), 2); + + let mut actual_tags = BTreeSet::new(); + let mut actual_sockets = BTreeSet::new(); + + for fs in config.fs { + actual_tags.insert(fs.tag); + actual_sockets.insert(fs.socket); + } + + let expected_tags = (1..=2).map(|i| format!("dir{i}")).collect(); + assert_eq!(actual_tags, expected_tags); + + let expected_sockets = (1..=2) + .map(|i| format!("../fs-testvm-dir{i}/env/virtiofsd.sock")) + .collect(); + assert_eq!(actual_sockets, expected_sockets); + + Ok(()) +} diff --git a/host/start-vm/unix.c b/host/start-vmm/unix.c index 43143fd..43143fd 100644 --- a/host/start-vm/unix.c +++ b/host/start-vmm/unix.c diff --git a/host/start-vm/unix.rs b/host/start-vmm/unix.rs index 8213497..8213497 100644 --- a/host/start-vm/unix.rs +++ b/host/start-vmm/unix.rs diff --git a/pkgs/default.nix b/pkgs/default.nix index 633c7e5..b173c0b 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> +# SPDX-FileCopyrightText: 2023-2024 Alyssa Ross <hi@alyssa.is> # SPDX-License-Identifier: MIT { ... } @ args: @@ -7,7 +7,15 @@ let config = import ../lib/config.nix args; pkgs = import ./overlaid.nix ({ elaboratedConfig = config; } // args); - inherit (pkgs.lib) cleanSource fileset makeScope optionalAttrs; + inherit (pkgs.lib) cleanSource fileset makeScope optionalAttrs sourceByRegex; + + subprojects = + project: + let dir = project + "/subprojects"; in + fileset.difference dir (fileset.fromSource (sourceByRegex dir [ + ".*\.wrap" + "packagefiles(/.*)?" + ])); scope = self: let pkgs = self.callPackage ({ pkgs }: pkgs) {}; in { inherit config; @@ -17,7 +25,7 @@ let lseek = self.callSpectrumPackage ../tools/lseek {}; rootfs = self.callSpectrumPackage ../host/rootfs {}; - start-vm = self.callSpectrumPackage ../host/start-vm {}; + start-vmm = self.callSpectrumPackage ../host/start-vmm {}; # Packages from the overlay, so it's possible to build them from # the CLI easily. @@ -27,7 +35,9 @@ let srcWithNix = fileset.difference (fileset.fromSource (cleanSource ../.)) - (fileset.unions (map fileset.maybeMissing [ + (fileset.unions ([ + (subprojects ../host/start-vmm) + ] ++ map fileset.maybeMissing [ ../Documentation/.jekyll-cache ../Documentation/_site ../Documentation/diagrams/stack.svg diff --git a/pkgs/meson/rust-static.patch b/pkgs/meson/rust-static.patch new file mode 100644 index 0000000..7a34b40 --- /dev/null +++ b/pkgs/meson/rust-static.patch @@ -0,0 +1,46 @@ +From 1e48f7b91fc5dd16c0eda17acbeaef82446d018a Mon Sep 17 00:00:00 2001 +From: Alyssa Ross <hi@alyssa.is> +Date: Fri, 15 Dec 2023 19:06:13 +0100 +Subject: [PATCH] rust: fix linking static executables +SPDX-License-Identifier: Apache-2.0 +SPDX-FileCopyrightText: 2012-2022 The Meson development team +SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> + +For the same reason as for static libraries, we have to use -l when +generating static executables. + +Fixes: https://github.com/mesonbuild/meson/issues/12585 +--- + mesonbuild/backend/ninjabackend.py | 2 +- + mesonbuild/compilers/rust.py | 5 +++++ + 2 files changed, 6 insertions(+), 1 deletion(-) + +diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py +index b4a312bb2e07..6434dd8789cf 100644 +--- a/mesonbuild/backend/ninjabackend.py ++++ b/mesonbuild/backend/ninjabackend.py +@@ -1994,7 +1994,7 @@ def _link_library(libname: str, static: bool, bundle: bool = False): + # "-l" argument and does not rely on platform specific dynamic linker. + lib = self.get_target_filename_for_linking(d) + link_whole = d in target.link_whole_targets +- if isinstance(target, build.StaticLibrary): ++ if isinstance(target, build.StaticLibrary) or (isinstance(target, build.Executable) and rustc.get_crt_static()): + static = isinstance(d, build.StaticLibrary) + libname = os.path.basename(lib) if verbatim else d.name + _link_library(libname, static, bundle=link_whole) +diff --git a/mesonbuild/compilers/rust.py b/mesonbuild/compilers/rust.py +index f9929088f8cb..d04b8fe3a701 100644 +--- a/mesonbuild/compilers/rust.py ++++ b/mesonbuild/compilers/rust.py +@@ -121,6 +121,11 @@ def get_sysroot(self) -> str: + p, stdo, stde = Popen_safe_logged(cmd) + return stdo.split('\n', maxsplit=1)[0] + ++ def get_crt_static(self) -> bool: ++ cmd = self.get_exelist(ccache=False) + ['--print', 'cfg'] ++ p, stdo, stde = Popen_safe_logged(cmd) ++ return bool(re.search('^target_feature="crt-static"$', stdo, re.MULTILINE)) ++ + def get_debug_args(self, is_debug: bool) -> T.List[str]: + return clike_debug_args[is_debug] + diff --git a/release/checks/pkg-tests.nix b/release/checks/pkg-tests.nix index f51ba42..7a41e8d 100644 --- a/release/checks/pkg-tests.nix +++ b/release/checks/pkg-tests.nix @@ -2,14 +2,14 @@ # SPDX-FileCopyrightText: 2023 Alyssa Ross <hi@alyssa.is> import ../../lib/call-package.nix ( -{ callSpectrumPackage, lseek, start-vm, lib }: +{ callSpectrumPackage, lseek, start-vmm, lib }: { recurseForDerivations = true; lseek = lib.recurseIntoAttrs lseek.tests; - start-vm = lib.recurseIntoAttrs start-vm.tests; + start-vmm = lib.recurseIntoAttrs start-vmm.tests; run-spectrum-vm = lib.recurseIntoAttrs (callSpectrumPackage ../../scripts/run-spectrum-vm.nix {}).tests; |