Merge branch 'vm-ch'

# Conflicts:
#	scripts/prepare-rootfs.sh
This commit is contained in:
2026-09-01 16:30:19 -07:00
29 changed files with 2029 additions and 65 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
## Unreleased
- Linux x86_64 backend (`crates/vm-ch`): drives cloud-hypervisor over its API
socket, virtiofsd for mounts, an in-process vhost-user-net backend for
`--allow-net`, and hybrid vsock. Guest assets build for the host arch
(PVH vmlinux kernel, amd64 rootfs); OS tarballs are per-arch
(`hanzo-vm-os-<tag>-<arch>.tar.gz`).
## 0.1.0
First release of Hanzo VM.
Generated
+172 -7
View File
@@ -79,6 +79,15 @@ version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
[[package]]
name = "arc-swap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b"
dependencies = [
"rustversion",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@@ -160,7 +169,7 @@ dependencies = [
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -246,6 +255,17 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chacha20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"rand_core",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
@@ -356,6 +376,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -628,10 +657,22 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"r-efi 5.3.0",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core",
]
[[package]]
name = "glob"
version = "0.3.3"
@@ -946,7 +987,7 @@ version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2efe3f1a4437bffe000e6297a593b98184213cd27486776c335f95ab53d48e3a"
dependencies = [
"vmm-sys-util",
"vmm-sys-util 0.12.1",
]
[[package]]
@@ -958,7 +999,7 @@ dependencies = [
"bitflags 2.11.0",
"kvm-bindings",
"libc",
"vmm-sys-util",
"vmm-sys-util 0.12.1",
]
[[package]]
@@ -1000,7 +1041,7 @@ version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d379d0089d0fbf4161c35a4fdfd76125923f1a93632c49195f5372b4c0b1472"
dependencies = [
"vm-memory",
"vm-memory 0.15.0",
]
[[package]]
@@ -1309,6 +1350,29 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rcgen"
version = "0.13.2"
@@ -1430,6 +1494,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -1899,6 +1969,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
dependencies = [
"getrandom 0.4.3",
"rand",
]
[[package]]
name = "valuable"
version = "0.1.1"
@@ -1911,6 +1991,53 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "vhost"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c63c14485260662b4257bd929d5fd4aaf96bbecbe61d2224bad8807849f5f8f"
dependencies = [
"bitflags 2.11.0",
"libc",
"uuid",
"vm-memory 0.18.0",
"vmm-sys-util 0.15.0",
]
[[package]]
name = "vhost-user-backend"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "555753b65bc33837bd011f981e2c6d52d8ddc330d92c2deb9aa3f0b378a58bcc"
dependencies = [
"libc",
"log",
"vhost",
"virtio-bindings",
"virtio-queue",
"vm-memory 0.18.0",
"vmm-sys-util 0.15.0",
]
[[package]]
name = "virtio-bindings"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "091f1f09cfbf2a78563b562e7a949465cce1aef63b6065645188d995162f8868"
[[package]]
name = "virtio-queue"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f631bfd09362a9b17f0cfd5ca3b0a1b179d153fa276f9ce86919d560891e61d6"
dependencies = [
"libc",
"log",
"virtio-bindings",
"vm-memory 0.18.0",
"vmm-sys-util 0.15.0",
]
[[package]]
name = "vm"
version = "0.1.1"
@@ -1921,11 +2048,27 @@ dependencies = [
"serde_json",
"socket2 0.5.10",
"tracing",
"vm-ch",
"vm-darwin",
"vm-linux",
"vm-proto",
]
[[package]]
name = "vm-ch"
version = "0.1.0"
dependencies = [
"crossbeam-channel",
"libc",
"serde_json",
"vhost",
"vhost-user-backend",
"virtio-bindings",
"virtio-queue",
"vm-memory 0.18.0",
"vmm-sys-util 0.15.0",
]
[[package]]
name = "vm-darwin"
version = "0.1.1"
@@ -1969,8 +2112,8 @@ dependencies = [
"libc",
"linux-loader",
"vm-fdt",
"vm-memory",
"vmm-sys-util",
"vm-memory 0.15.0",
"vmm-sys-util 0.12.1",
]
[[package]]
@@ -1984,6 +2127,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "vm-memory"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b55e753c7725603745cb32b2287ef7ef3da05c03c7702cda3fa8abe25ae0465"
dependencies = [
"arc-swap",
"libc",
"thiserror 2.0.18",
"winapi",
]
[[package]]
name = "vm-proto"
version = "0.1.1"
@@ -2052,6 +2207,16 @@ dependencies = [
"libc",
]
[[package]]
name = "vmm-sys-util"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "506c62fdf617a5176827c2f9afbcf1be155b03a9b4bf9617a60dbc07e3a1642f"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "walkdir"
version = "2.5.0"
+1
View File
@@ -4,6 +4,7 @@ members = [
"crates/vm-cli",
"crates/vm-darwin",
"crates/vm-linux",
"crates/vm-ch",
"crates/vm-proto",
"crates/vm-proxy",
"crates/vm-sdk",
+30 -5
View File
@@ -16,9 +16,10 @@ files written inside a run never touch the host or the next run.
- **macOS** — Apple Virtualization.framework. Apple Silicon only, arm64 guests
only, no Rosetta. macOS 14 or later.
- **Linux** — an experimental KVM backend (`crates/vm-linux`), arm64 hosts with
`/dev/kvm`. cloud-hypervisor (amd64 and arm64, VFIO GPU passthrough) is the
planned replacement.
- **Linux arm64** — KVM backend (`crates/vm-linux`), hosts with `/dev/kvm`.
- **Linux x86_64** — cloud-hypervisor backend (`crates/vm-ch`), hosts with
`/dev/kvm` plus the `cloud-hypervisor` and `virtiofsd` binaries (see
[Linux hosts](#linux-hosts)).
## Install
@@ -36,6 +37,29 @@ Both put `hanzo-vm` in `~/.local/bin`. The first `hanzo-vm run` downloads the
guest image (kernel, initramfs, root filesystem) from the matching GitHub
release into `~/.hanzo/vm`; set `HANZO_VM_HOME` to put it elsewhere.
## Linux hosts
Both Linux backends need read/write access to `/dev/kvm` (membership in the
`kvm` group, or an ACL).
arm64 connects to the guest through the host's vhost-vsock device; grant
access once with:
```sh
sudo setfacl -m u:$USER:rw /dev/vhost-vsock
```
x86_64 drives release binaries of cloud-hypervisor (verified with v53.0) and
virtiofsd (verified with 1.10.0), looked up on `PATH` and in `~/.local/bin`:
```sh
curl -fsSLo ~/.local/bin/cloud-hypervisor \
https://github.com/cloud-hypervisor/cloud-hypervisor/releases/download/v53.0/cloud-hypervisor-static
chmod +x ~/.local/bin/cloud-hypervisor
sudo apt install virtiofsd # Debian/Ubuntu; installs /usr/libexec/virtiofsd
install -m 755 /usr/libexec/virtiofsd ~/.local/bin/virtiofsd
```
## Usage
```sh
@@ -181,8 +205,9 @@ just install # release CLI to ~/.local/bin/hanzo-vm
```
The guest image is built by `scripts/prepare-rootfs.sh` (kernel via
`scripts/build-kernel.sh`); it runs natively on an arm64 Linux host and inside
Docker on macOS. CI publishes the image with every release.
`scripts/build-kernel.sh`); it runs natively on arm64 and x86_64 Linux hosts
(building the guest needs `musl-tools` for the musl target) and inside Docker
on macOS. CI publishes the image with every release.
## Changelog
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "vm-ch"
version = "0.1.0"
edition = "2021"
description = "cloud-hypervisor backend for Hanzo VM"
license = "Apache-2.0"
repository = "https://github.com/hanzoai/vm"
[dependencies]
libc = "0.2"
crossbeam-channel = "0.5"
serde_json = "1"
vhost = { version = "0.17", features = ["vhost-user-backend"] }
vhost-user-backend = "0.23"
virtio-bindings = "0.2"
virtio-queue = "0.18"
vm-memory = { version = "0.18", features = ["backend-mmap", "backend-atomic"] }
vmm-sys-util = "0.15"
+87
View File
@@ -0,0 +1,87 @@
//! Minimal HTTP/1.1 client for the cloud-hypervisor API socket.
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
use crate::error::{Result, VzError};
/// PUT `/api/v1/<endpoint>` with an optional JSON body.
pub(crate) fn put(socket: &str, endpoint: &str, body: Option<&str>) -> Result<()> {
let mut stream = UnixStream::connect(socket)
.map_err(|e| VzError::new(format!("connect to {}: {}", socket, e)))?;
let _ = stream.set_read_timeout(Some(Duration::from_secs(10)));
let body = body.unwrap_or("");
let request = format!(
"PUT /api/v1/{} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
endpoint,
body.len(),
body
);
stream
.write_all(request.as_bytes())
.map_err(|e| VzError::new(format!("{}: send: {}", endpoint, e)))?;
// Read the status line and headers, then a Content-Length body if any.
let mut buf = Vec::new();
let mut chunk = [0u8; 4096];
let header_end = loop {
let n = stream
.read(&mut chunk)
.map_err(|e| VzError::new(format!("{}: read: {}", endpoint, e)))?;
if n == 0 {
break buf.len();
}
buf.extend_from_slice(&chunk[..n]);
if let Some(pos) = find_header_end(&buf) {
break pos;
}
if buf.len() > 64 * 1024 {
return Err(VzError::new(format!("{}: oversized response", endpoint)));
}
};
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
let status: u16 = head
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.ok_or_else(|| VzError::new(format!("{}: malformed response", endpoint)))?;
let content_length: usize = head
.lines()
.find_map(|l| {
let (k, v) = l.split_once(':')?;
k.eq_ignore_ascii_case("content-length")
.then(|| v.trim().parse().ok())?
})
.unwrap_or(0);
let mut rest = buf[header_end..].to_vec();
while rest.len() < content_length {
let n = stream
.read(&mut chunk)
.map_err(|e| VzError::new(format!("{}: read body: {}", endpoint, e)))?;
if n == 0 {
break;
}
rest.extend_from_slice(&chunk[..n]);
}
if (200..300).contains(&status) {
Ok(())
} else {
Err(VzError::new(format!(
"{}: HTTP {}: {}",
endpoint,
status,
String::from_utf8_lossy(&rest).trim()
)))
}
}
/// Offset just past the `\r\n\r\n` separator, if present.
fn find_header_end(buf: &[u8]) -> Option<usize> {
buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4)
}
+39
View File
@@ -0,0 +1,39 @@
use std::cell::RefCell;
/// Stores the kernel, initrd, and command-line paths for booting a Linux guest.
/// On macOS this wraps VZLinuxBootLoader; here it is a plain data holder
/// consumed by `VirtualMachine::new()`.
///
/// Uses `RefCell` for interior mutability to match Darwin`s `&self` setter API.
pub struct LinuxBootLoader {
pub(crate) kernel_path: String,
pub(crate) initrd_path: RefCell<Option<String>>,
pub(crate) command_line: RefCell<Option<String>>,
}
impl LinuxBootLoader {
pub fn new(kernel_path: &str, initrd_path: &str, command_line: &str) -> Self {
let bl = Self::new_with_kernel(kernel_path);
bl.set_initrd(initrd_path);
bl.set_command_line(command_line);
bl
}
pub fn new_with_kernel(kernel_path: &str) -> Self {
LinuxBootLoader {
kernel_path: kernel_path.to_string(),
initrd_path: RefCell::new(None),
command_line: RefCell::new(None),
}
}
pub fn set_initrd(&self, initrd_path: &str) {
*self.initrd_path.borrow_mut() = Some(initrd_path.to_string());
}
/// cloud-hypervisor attaches a virtio-console, so `console=hvc0` from the
/// caller is kept as-is.
pub fn set_command_line(&self, command_line: &str) {
*self.command_line.borrow_mut() = Some(command_line.to_string());
}
}
+140
View File
@@ -0,0 +1,140 @@
use std::cell::RefCell;
use std::os::fd::RawFd;
use crate::bootloader::LinuxBootLoader;
use crate::directory_sharing::VirtioFileSystemDevice;
use crate::entropy::VirtioEntropyDevice;
use crate::error::{Result, VzError};
use crate::memory_balloon::VirtioMemoryBalloonDevice;
use crate::network::VirtioNetworkDevice;
use crate::serial::VirtioConsoleSerialPort;
use crate::socket::VirtioSocketDevice;
use crate::storage::StorageDevice;
#[derive(Clone)]
pub(crate) struct ConfigData {
pub cpu_count: usize,
pub memory_size: u64,
pub kernel_path: String,
pub initrd_path: Option<String>,
pub command_line: String,
pub serial_read_fd: Option<RawFd>,
pub serial_write_fd: Option<RawFd>,
pub disk_path: Option<String>,
pub disk_read_only: bool,
pub network_fd: Option<RawFd>,
pub network_mac: Option<[u8; 6]>,
pub has_socket: bool,
pub mounts: Vec<(String, String, bool)>, // (tag, host_path, read_only)
}
pub struct VirtualMachineConfiguration {
pub(crate) inner: RefCell<ConfigData>,
}
impl VirtualMachineConfiguration {
pub fn new(boot_loader: &LinuxBootLoader, cpus: usize, memory: u64) -> Self {
VirtualMachineConfiguration {
inner: RefCell::new(ConfigData {
cpu_count: cpus,
memory_size: memory,
kernel_path: boot_loader.kernel_path.clone(),
initrd_path: boot_loader.initrd_path.borrow().clone(),
command_line: boot_loader
.command_line
.borrow()
.clone()
.unwrap_or_default(),
serial_read_fd: None,
serial_write_fd: None,
disk_path: None,
disk_read_only: false,
network_fd: None,
network_mac: None,
has_socket: false,
mounts: Vec::new(),
}),
}
}
pub fn set_cpu_count(&self, cpus: usize) {
self.inner.borrow_mut().cpu_count = cpus;
}
pub fn set_memory_size(&self, memory: u64) {
self.inner.borrow_mut().memory_size = memory;
}
pub fn set_boot_loader(&self, boot_loader: &LinuxBootLoader) {
let mut inner = self.inner.borrow_mut();
inner.kernel_path = boot_loader.kernel_path.clone();
inner.initrd_path = boot_loader.initrd_path.borrow().clone();
if let Some(ref cmdline) = *boot_loader.command_line.borrow() {
inner.command_line = cmdline.clone();
}
}
pub fn set_serial_ports(&self, ports: &[VirtioConsoleSerialPort]) {
if let Some(port) = ports.first() {
let mut inner = self.inner.borrow_mut();
inner.serial_read_fd = port.read_fd;
inner.serial_write_fd = port.write_fd;
}
}
pub fn set_storage_devices(&self, devices: &[&dyn StorageDevice]) {
if let Some(device) = devices.first() {
let mut inner = self.inner.borrow_mut();
inner.disk_path = device.get_disk_path().map(|s| s.to_string());
inner.disk_read_only = device.get_read_only();
}
}
pub fn set_network_devices(&self, devices: &[VirtioNetworkDevice]) {
if let Some(device) = devices.first() {
let mut inner = self.inner.borrow_mut();
inner.network_fd = device.fd;
let mac = device.mac_bytes();
if mac != [0; 6] {
inner.network_mac = Some(mac);
}
}
}
pub fn set_socket_devices(&self, devices: &[VirtioSocketDevice]) {
if !devices.is_empty() {
self.inner.borrow_mut().has_socket = true;
}
}
pub fn set_directory_sharing_devices(&self, devices: &[VirtioFileSystemDevice]) {
let mut inner = self.inner.borrow_mut();
for dev in devices {
inner
.mounts
.push((dev.tag.clone(), dev.host_path.clone(), dev.read_only));
}
}
pub fn set_entropy_devices(&self, _devices: &[VirtioEntropyDevice]) {
// Entropy device is a no-op on KVM — /dev/urandom is available.
}
pub fn set_memory_balloon_devices(&self, _devices: &[VirtioMemoryBalloonDevice]) {
// Memory balloon is not yet implemented for KVM.
}
pub fn validate(&self) -> Result<()> {
let inner = self.inner.borrow();
if inner.kernel_path.is_empty() {
return Err(VzError::new("kernel path is required"));
}
if inner.memory_size == 0 {
return Err(VzError::new("memory size must be > 0"));
}
if inner.cpu_count == 0 {
return Err(VzError::new("CPU count must be > 0"));
}
Ok(())
}
}
+32
View File
@@ -0,0 +1,32 @@
/// Stores host directory path and read-only flag for virtiofs sharing.
/// On Linux/KVM, virtiofs requires an external `virtiofsd` process;
/// this type only holds config and is consumed by VirtualMachine.
pub struct SharedDirectory {
pub(crate) path: String,
pub(crate) read_only: bool,
}
impl SharedDirectory {
pub fn new(path: &str, read_only: bool) -> Self {
SharedDirectory {
path: path.to_string(),
read_only,
}
}
}
pub struct VirtioFileSystemDevice {
pub(crate) tag: String,
pub(crate) host_path: String,
pub(crate) read_only: bool,
}
impl VirtioFileSystemDevice {
pub fn new(tag: &str, directory: &SharedDirectory) -> Self {
VirtioFileSystemDevice {
tag: tag.to_string(),
host_path: directory.path.clone(),
read_only: directory.read_only,
}
}
}
+15
View File
@@ -0,0 +1,15 @@
/// Placeholder — the guest gets entropy from the kernel's virtio-rng
/// if present in the FDT. Kept for API compatibility with vm-darwin.
pub struct VirtioEntropyDevice;
impl VirtioEntropyDevice {
pub fn new() -> Self {
VirtioEntropyDevice
}
}
impl Default for VirtioEntropyDevice {
fn default() -> Self {
Self::new()
}
}
+22
View File
@@ -0,0 +1,22 @@
#[derive(Debug, Clone)]
pub struct VzError {
message: String,
}
impl VzError {
pub fn new(message: impl Into<String>) -> Self {
VzError {
message: message.into(),
}
}
}
impl std::fmt::Display for VzError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for VzError {}
pub type Result<T> = std::result::Result<T, VzError>;
+30
View File
@@ -0,0 +1,30 @@
mod api;
mod net_backend;
mod bootloader;
mod configuration;
mod directory_sharing;
mod entropy;
mod error;
mod memory_balloon;
pub mod network;
mod serial;
mod socket;
mod storage;
pub mod terminal;
mod vm;
pub use bootloader::LinuxBootLoader;
pub use configuration::VirtualMachineConfiguration;
pub use directory_sharing::{SharedDirectory, VirtioFileSystemDevice};
pub use entropy::VirtioEntropyDevice;
pub use error::{Result, VzError};
pub use memory_balloon::VirtioMemoryBalloonDevice;
pub use network::{FileHandleNetworkAttachment, MACAddress, VirtioNetworkDevice};
pub use serial::{FileHandleSerialAttachment, VirtioConsoleSerialPort};
pub use socket::VirtioSocketDevice;
pub use storage::{
DiskImageAttachment, DiskImageCachingMode, DiskImageSynchronizationMode, NbdAttachment,
StorageAttachment, StorageDevice, VirtioBlockDevice,
};
pub use vm::{VirtualMachine, VmState};
+15
View File
@@ -0,0 +1,15 @@
/// Placeholder — memory balloon is not yet implemented for KVM.
/// Kept for API compatibility with vm-darwin.
pub struct VirtioMemoryBalloonDevice;
impl VirtioMemoryBalloonDevice {
pub fn new() -> Self {
VirtioMemoryBalloonDevice
}
}
impl Default for VirtioMemoryBalloonDevice {
fn default() -> Self {
Self::new()
}
}
+220
View File
@@ -0,0 +1,220 @@
//! vhost-user-net backend bridging cloud-hypervisor's virtio-net device to
//! the proxy's `SOCK_DGRAM` socketpair (one raw Ethernet frame per datagram,
//! same wire format as VZFileHandleNetworkDeviceAttachment on macOS).
//!
//! Queue 0 is RX (host -> guest), queue 1 is TX (guest -> host). Frames carry
//! a 12-byte virtio-net header on the queues and none on the socketpair. A
//! frame arriving while the guest has no RX buffer posted is dropped; the
//! proxy's TCP stack retransmits.
use std::io::Result as IoResult;
use std::os::fd::RawFd;
use std::sync::{Arc, RwLock};
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
use vhost::vhost_user::Listener;
use vhost_user_backend::{VhostUserBackendMut, VhostUserDaemon, VringRwLock, VringT};
use virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
use virtio_bindings::virtio_net::VIRTIO_NET_F_MAC;
use virtio_queue::QueueT;
use vm_memory::{Bytes, GuestAddressSpace, GuestMemoryAtomic, GuestMemoryMmap};
use vmm_sys_util::epoll::EventSet;
const VIRTIO_NET_HDR_SIZE: usize = 12;
const MAX_FRAME: usize = 65536;
const RX_QUEUE: u16 = 0;
const TX_QUEUE: u16 = 1;
/// epoll data for the socketpair fd (must be > num_queues).
const SOCK_EVENT: u16 = 3;
struct Net {
fd: RawFd,
mem: Option<GuestMemoryAtomic<GuestMemoryMmap>>,
event_idx: bool,
}
impl Net {
/// Guest -> host: drain the TX queue, stripping the virtio-net header.
fn process_tx(&self, vring: &VringRwLock) -> IoResult<()> {
let Some(atomic_mem) = self.mem.as_ref() else {
return Ok(());
};
let mem = atomic_mem.memory();
let mut vring = vring.get_mut();
let mut used = false;
let mut frame = vec![0u8; MAX_FRAME];
while let Some(chain) = vring.get_queue_mut().pop_descriptor_chain(mem.clone()) {
let head = chain.head_index();
let mut len = 0usize;
for desc in chain {
if desc.is_write_only() {
continue;
}
let n = (desc.len() as usize).min(MAX_FRAME - len);
if mem
.read_slice(&mut frame[len..len + n], desc.addr())
.is_err()
{
break;
}
len += n;
}
if len > VIRTIO_NET_HDR_SIZE {
unsafe {
libc::send(
self.fd,
frame[VIRTIO_NET_HDR_SIZE..].as_ptr() as *const libc::c_void,
len - VIRTIO_NET_HDR_SIZE,
libc::MSG_DONTWAIT,
);
}
}
vring.add_used(head, 0).ok();
used = true;
}
if used {
vring.signal_used_queue().ok();
}
Ok(())
}
/// Host -> guest: drain the socketpair into RX queue buffers, prepending
/// a zeroed virtio-net header.
fn process_rx(&self, vring: &VringRwLock) -> IoResult<()> {
let Some(atomic_mem) = self.mem.as_ref() else {
return Ok(());
};
let mem = atomic_mem.memory();
let mut vring = vring.get_mut();
let mut used = false;
let mut buf = vec![0u8; VIRTIO_NET_HDR_SIZE + MAX_FRAME];
loop {
let n = unsafe {
libc::recv(
self.fd,
buf[VIRTIO_NET_HDR_SIZE..].as_mut_ptr() as *mut libc::c_void,
MAX_FRAME,
libc::MSG_DONTWAIT,
)
};
if n <= 0 {
break; // EAGAIN or closed
}
let total = VIRTIO_NET_HDR_SIZE + n as usize;
let Some(chain) = vring.get_queue_mut().pop_descriptor_chain(mem.clone()) else {
continue; // no guest buffer posted: drop the frame
};
let head = chain.head_index();
let mut written = 0usize;
for desc in chain {
if !desc.is_write_only() || written == total {
continue;
}
let n = (desc.len() as usize).min(total - written);
if mem
.write_slice(&buf[written..written + n], desc.addr())
.is_err()
{
break;
}
written += n;
}
vring.add_used(head, written as u32).ok();
used = true;
}
if used {
vring.signal_used_queue().ok();
}
Ok(())
}
}
impl VhostUserBackendMut for Net {
type Bitmap = ();
type Vring = VringRwLock;
fn num_queues(&self) -> usize {
2
}
fn max_queue_size(&self) -> usize {
256
}
fn features(&self) -> u64 {
(1 << VIRTIO_F_VERSION_1)
| (1 << VIRTIO_NET_F_MAC)
| VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
}
fn protocol_features(&self) -> VhostUserProtocolFeatures {
VhostUserProtocolFeatures::MQ | VhostUserProtocolFeatures::REPLY_ACK
}
fn set_event_idx(&mut self, enabled: bool) {
self.event_idx = enabled;
}
fn update_memory(&mut self, mem: GuestMemoryAtomic<GuestMemoryMmap>) -> IoResult<()> {
self.mem = Some(mem);
Ok(())
}
fn handle_event(
&mut self,
device_event: u16,
_evset: EventSet,
vrings: &[VringRwLock],
_thread_id: usize,
) -> IoResult<()> {
match device_event {
RX_QUEUE | SOCK_EVENT => self.process_rx(&vrings[RX_QUEUE as usize]),
TX_QUEUE => self.process_tx(&vrings[TX_QUEUE as usize]),
_ => Ok(()),
}
}
}
/// Serve a vhost-user-net backend on `socket_path` for one connection.
/// Returns after the listener is ready; the daemon runs on its own thread
/// and exits when cloud-hypervisor disconnects.
pub(crate) fn spawn(socket_path: &str, fd: RawFd) -> crate::error::Result<()> {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFL);
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
}
let backend = Arc::new(RwLock::new(Net {
fd,
mem: None,
event_idx: false,
}));
let mut daemon = VhostUserDaemon::new(
"hanzo-vm-net".to_string(),
backend,
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
)
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net daemon: {}", e)))?;
let mut listener = Listener::new(socket_path, true)
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net listener: {}", e)))?;
std::thread::Builder::new()
.name("hanzo-vm-net".into())
.spawn(move || {
// start() blocks until cloud-hypervisor connects.
if daemon.start(&mut listener).is_ok() {
if let Some(handler) = daemon.get_epoll_handlers().first() {
let _ = handler.register_listener(fd, EventSet::IN, SOCK_EVENT as u64);
}
let _ = daemon.wait();
}
})
.map_err(|e| crate::error::VzError::new(format!("vhost-user-net thread: {}", e)))?;
Ok(())
}
+84
View File
@@ -0,0 +1,84 @@
use std::cell::Cell;
use std::os::unix::io::RawFd;
pub struct FileHandleNetworkAttachment {
pub(crate) fd: RawFd,
}
impl FileHandleNetworkAttachment {
/// Creates a network attachment from a connected datagram socket fd.
/// The fd should be one end of `socketpair(AF_UNIX, SOCK_DGRAM)`.
pub fn new(fd: RawFd) -> Self {
FileHandleNetworkAttachment { fd }
}
}
pub struct MACAddress {
pub(crate) bytes: [u8; 6],
}
impl MACAddress {
pub fn new() -> Self {
MACAddress { bytes: [0; 6] }
}
/// Generate a random locally-administered MAC address.
pub fn random_local() -> Self {
let mut bytes = [0u8; 6];
let fd = unsafe { libc::open(c"/dev/urandom".as_ptr(), libc::O_RDONLY) };
if fd >= 0 {
unsafe {
libc::read(fd, bytes.as_mut_ptr() as *mut libc::c_void, 6);
libc::close(fd);
}
}
// Set locally administered + unicast bits
bytes[0] = (bytes[0] & 0xFC) | 0x02;
MACAddress { bytes }
}
}
impl Default for MACAddress {
fn default() -> Self {
Self::new()
}
}
pub struct VirtioNetworkDevice {
pub(crate) fd: Option<RawFd>,
pub(crate) mac: Cell<[u8; 6]>,
}
impl VirtioNetworkDevice {
pub fn new() -> Self {
VirtioNetworkDevice {
fd: None,
mac: Cell::new([0; 6]),
}
}
pub fn new_with_attachment(attachment: &FileHandleNetworkAttachment) -> Self {
VirtioNetworkDevice {
fd: Some(attachment.fd),
mac: Cell::new([0; 6]),
}
}
pub fn set_attachment(&mut self, attachment: &FileHandleNetworkAttachment) {
self.fd = Some(attachment.fd);
}
pub fn set_mac_address(&self, address: &MACAddress) {
self.mac.set(address.bytes);
}
pub(crate) fn mac_bytes(&self) -> [u8; 6] {
self.mac.get()
}
}
impl Default for VirtioNetworkDevice {
fn default() -> Self {
Self::new()
}
}
+56
View File
@@ -0,0 +1,56 @@
use std::os::fd::RawFd;
pub struct FileHandleSerialAttachment {
pub(crate) read_fd: Option<RawFd>,
pub(crate) write_fd: Option<RawFd>,
}
impl FileHandleSerialAttachment {
pub fn new(read_fd: RawFd, write_fd: RawFd) -> Self {
FileHandleSerialAttachment {
read_fd: Some(read_fd),
write_fd: Some(write_fd),
}
}
/// Serial attachment with no read handle (stdin disconnected).
/// Output goes to the given write fd.
pub fn new_write_only(write_fd: RawFd) -> Self {
FileHandleSerialAttachment {
read_fd: None,
write_fd: Some(write_fd),
}
}
}
pub struct VirtioConsoleSerialPort {
pub(crate) read_fd: Option<RawFd>,
pub(crate) write_fd: Option<RawFd>,
}
impl VirtioConsoleSerialPort {
pub fn new() -> Self {
VirtioConsoleSerialPort {
read_fd: None,
write_fd: None,
}
}
pub fn new_with_attachment(attachment: &FileHandleSerialAttachment) -> Self {
VirtioConsoleSerialPort {
read_fd: attachment.read_fd,
write_fd: attachment.write_fd,
}
}
pub fn set_attachment(&mut self, attachment: &FileHandleSerialAttachment) {
self.read_fd = attachment.read_fd;
self.write_fd = attachment.write_fd;
}
}
impl Default for VirtioConsoleSerialPort {
fn default() -> Self {
Self::new()
}
}
+13
View File
@@ -0,0 +1,13 @@
pub struct VirtioSocketDevice;
impl VirtioSocketDevice {
pub fn new() -> Self {
VirtioSocketDevice
}
}
impl Default for VirtioSocketDevice {
fn default() -> Self {
Self::new()
}
}
+120
View File
@@ -0,0 +1,120 @@
use crate::error::{Result, VzError};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskImageCachingMode {
Automatic,
Cached,
Uncached,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskImageSynchronizationMode {
Full,
Fsync,
None,
}
// ---------------------------------------------------------------------------
// Attachment types – store config consumed later by the VM builder
// ---------------------------------------------------------------------------
pub trait StorageAttachment {
fn disk_path(&self) -> Option<&str>;
fn nbd_uri(&self) -> Option<&str>;
fn is_read_only(&self) -> bool;
}
pub struct DiskImageAttachment {
pub(crate) path: String,
pub(crate) read_only: bool,
}
impl DiskImageAttachment {
pub fn new(path: &str, read_only: bool) -> Result<Self> {
Ok(DiskImageAttachment {
path: path.to_string(),
read_only,
})
}
pub fn new_with_options(
path: &str,
read_only: bool,
_caching_mode: DiskImageCachingMode,
_sync_mode: DiskImageSynchronizationMode,
) -> Result<Self> {
Self::new(path, read_only)
}
}
impl StorageAttachment for DiskImageAttachment {
fn disk_path(&self) -> Option<&str> {
Some(&self.path)
}
fn nbd_uri(&self) -> Option<&str> {
None
}
fn is_read_only(&self) -> bool {
self.read_only
}
}
pub struct NbdAttachment {
pub(crate) uri: String,
pub(crate) read_only: bool,
}
impl NbdAttachment {
pub fn new(uri: &str, _timeout_secs: f64, _read_only: bool) -> Result<Self> {
// TODO: implement userspace NBD client for Linux.
// For now fall back to HANZO_VM_STORAGE=direct.
Err(VzError::new(format!(
"NBD storage ({}) not yet supported on Linux. Set HANZO_VM_STORAGE=direct",
uri
)))
}
}
impl StorageAttachment for NbdAttachment {
fn disk_path(&self) -> Option<&str> {
None
}
fn nbd_uri(&self) -> Option<&str> {
Some(&self.uri)
}
fn is_read_only(&self) -> bool {
self.read_only
}
}
// ---------------------------------------------------------------------------
// Block device config
// ---------------------------------------------------------------------------
pub trait StorageDevice {
fn get_disk_path(&self) -> Option<&str>;
fn get_read_only(&self) -> bool;
}
pub struct VirtioBlockDevice {
pub(crate) path: Option<String>,
pub(crate) read_only: bool,
}
impl VirtioBlockDevice {
pub fn new(attachment: &dyn StorageAttachment) -> Self {
VirtioBlockDevice {
path: attachment.disk_path().map(|s| s.to_string()),
read_only: attachment.is_read_only(),
}
}
}
impl StorageDevice for VirtioBlockDevice {
fn get_disk_path(&self) -> Option<&str> {
self.path.as_deref()
}
fn get_read_only(&self) -> bool {
self.read_only
}
}
+231
View File
@@ -0,0 +1,231 @@
use std::os::fd::RawFd;
/// Saved terminal state for later restoration.
pub struct TerminalState {
fd: RawFd,
termios: libc::termios,
}
impl TerminalState {
/// Save the current terminal attributes and switch to raw mode.
/// Returns `None` if the fd is not a terminal.
pub fn enter_raw_mode(fd: RawFd) -> Option<Self> {
unsafe {
let mut saved: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut saved) != 0 {
return None;
}
let mut raw = saved;
libc::cfmakeraw(&mut raw);
libc::tcsetattr(fd, libc::TCSANOW, &raw);
Some(TerminalState { fd, termios: saved })
}
}
pub fn restore(&self) {
unsafe {
libc::tcsetattr(self.fd, libc::TCSANOW, &self.termios);
}
}
}
impl Drop for TerminalState {
fn drop(&mut self) {
self.restore();
}
}
/// Get the terminal size (rows, cols) for the given fd.
/// Returns (24, 80) as fallback if the ioctl fails.
pub fn terminal_size(fd: RawFd) -> (u16, u16) {
unsafe {
let mut ws: libc::winsize = std::mem::zeroed();
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 {
(ws.ws_row, ws.ws_col)
} else {
(24, 80)
}
}
}
/// Read bytes from a raw file descriptor.
/// Returns the number of bytes read, or 0 on EOF/error.
pub fn read_raw(fd: RawFd, buf: &mut [u8]) -> usize {
unsafe {
let n = libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len());
if n > 0 {
n as usize
} else {
0
}
}
}
// --- epoll + signalfd-based stdin relay ---
/// Events returned by `StdinRelay::wait()`.
pub enum StdinEvent {
/// stdin has data available to read.
Ready,
/// Terminal was resized (SIGWINCH).
Resize,
/// Shutdown was signaled by the other thread.
Shutdown,
}
/// Write-end of the shutdown pipe.
pub struct ShutdownSignal {
pipe_write: RawFd,
}
unsafe impl Send for ShutdownSignal {}
impl ShutdownSignal {
pub fn signal(&self) {
unsafe {
libc::write(self.pipe_write, [1u8].as_ptr() as *const libc::c_void, 1);
}
}
}
impl Drop for ShutdownSignal {
fn drop(&mut self) {
unsafe {
libc::close(self.pipe_write);
}
}
}
// Event source identifiers stored in epoll_event.u64
const EV_STDIN: u64 = 0;
const EV_PIPE: u64 = 1;
const EV_SIGNAL: u64 = 2;
/// epoll-based event multiplexer for stdin, SIGWINCH, and shutdown.
/// Linux equivalent of macOS kqueue-based StdinRelay.
pub struct StdinRelay {
epoll_fd: RawFd,
signal_fd: RawFd,
pipe_read: RawFd,
}
impl StdinRelay {
/// Create a new relay watching the given stdin fd.
pub fn new(stdin_fd: RawFd) -> Option<(StdinRelay, ShutdownSignal)> {
unsafe {
// Create shutdown pipe
let mut fds = [0i32; 2];
if libc::pipe(fds.as_mut_ptr()) != 0 {
return None;
}
let pipe_read = fds[0];
let pipe_write = fds[1];
// Create epoll instance
let epoll_fd = libc::epoll_create1(0);
if epoll_fd < 0 {
libc::close(pipe_read);
libc::close(pipe_write);
return None;
}
// Register stdin
let mut ev = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: EV_STDIN,
};
if libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, stdin_fd, &mut ev) < 0 {
libc::close(epoll_fd);
libc::close(pipe_read);
libc::close(pipe_write);
return None;
}
// Register shutdown pipe
let mut ev = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: EV_PIPE,
};
libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, pipe_read, &mut ev);
// Create signalfd for SIGWINCH
let mut sigset: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut sigset);
libc::sigaddset(&mut sigset, libc::SIGWINCH);
// Block SIGWINCH so signalfd receives it
libc::sigprocmask(libc::SIG_BLOCK, &sigset, std::ptr::null_mut());
let signal_fd = libc::signalfd(-1, &sigset, libc::SFD_NONBLOCK);
if signal_fd < 0 {
// signalfd not available — proceed without resize support
libc::close(epoll_fd);
libc::close(pipe_read);
libc::close(pipe_write);
return None;
}
let mut ev = libc::epoll_event {
events: libc::EPOLLIN as u32,
u64: EV_SIGNAL,
};
libc::epoll_ctl(epoll_fd, libc::EPOLL_CTL_ADD, signal_fd, &mut ev);
Some((
StdinRelay {
epoll_fd,
signal_fd,
pipe_read,
},
ShutdownSignal { pipe_write },
))
}
}
/// Block until stdin is readable, a resize signal arrives, or shutdown
/// is signaled.
pub fn wait(&self) -> StdinEvent {
unsafe {
let mut events = [libc::epoll_event { events: 0, u64: 0 }; 4];
let n = libc::epoll_wait(self.epoll_fd, events.as_mut_ptr(), 4, -1);
if n < 1 {
return StdinEvent::Shutdown;
}
for event in events.iter().take(n as usize) {
match event.u64 {
EV_STDIN => return StdinEvent::Ready,
EV_PIPE => return StdinEvent::Shutdown,
EV_SIGNAL => {
// Drain the signalfd
let mut info: libc::signalfd_siginfo = std::mem::zeroed();
libc::read(
self.signal_fd,
&mut info as *mut _ as *mut libc::c_void,
std::mem::size_of::<libc::signalfd_siginfo>(),
);
return StdinEvent::Resize;
}
_ => {}
}
}
StdinEvent::Shutdown
}
}
}
impl Drop for StdinRelay {
fn drop(&mut self) {
unsafe {
libc::close(self.epoll_fd);
libc::close(self.signal_fd);
libc::close(self.pipe_read);
// Unblock SIGWINCH
let mut sigset: libc::sigset_t = std::mem::zeroed();
libc::sigemptyset(&mut sigset);
libc::sigaddset(&mut sigset, libc::SIGWINCH);
libc::sigprocmask(libc::SIG_UNBLOCK, &sigset, std::ptr::null_mut());
}
}
}
+422
View File
@@ -0,0 +1,422 @@
//! VirtualMachine driving a `cloud-hypervisor` child process over its API
//! socket: `vm.create` + `vm.boot` on start, `vm.shutdown` + `vmm.shutdown`
//! on stop. One `virtiofsd` child serves each shared directory, and the
//! in-process vhost-user-net backend serves the proxy socketpair, so guest
//! memory is always mapped `shared=on`.
use std::io::{Read, Write};
use std::net::TcpStream;
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crossbeam_channel::{bounded, Receiver, Sender};
use crate::api;
use crate::configuration::{ConfigData, VirtualMachineConfiguration};
use crate::error::{Result, VzError};
use crate::net_backend;
const GUEST_CID: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VmState {
Stopped = 0,
Running = 1,
Error = 3,
// Paused/Starting/etc. kept as discriminants for Darwin compatibility.
Unknown = -1,
}
pub struct VirtualMachine {
config: ConfigData,
// Duplicated at construction time: the caller's fds (e.g. an opened
// /dev/null) may not outlive the configuration.
serial_in: Option<OwnedFd>,
serial_out: Option<OwnedFd>,
run_dir: PathBuf,
api_socket: String,
vsock_socket: String,
ch_pid: Mutex<Option<i32>>,
virtiofsd: Mutex<Vec<Child>>,
state_tx: Sender<VmState>,
state_rx: Receiver<VmState>,
running: Arc<AtomicBool>,
}
/// Locate a helper binary on PATH or in `~/.local/bin`.
fn find_binary(name: &str) -> Option<PathBuf> {
if let Ok(path) = std::env::var("PATH") {
for dir in path.split(':') {
let candidate = Path::new(dir).join(name);
if candidate.is_file() {
return Some(candidate);
}
}
}
if let Ok(home) = std::env::var("HOME") {
let candidate = Path::new(&home).join(".local/bin").join(name);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
/// Duplicate a raw fd; None if the fd is invalid.
fn dup_owned(fd: i32) -> Option<OwnedFd> {
let duped = unsafe { libc::dup(fd) };
if duped < 0 {
return None;
}
Some(unsafe { OwnedFd::from_raw_fd(duped) })
}
fn wait_for_socket(path: &str, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if Path::new(path).exists() {
return true;
}
std::thread::sleep(Duration::from_millis(5));
}
false
}
impl VirtualMachine {
pub fn new(config: &VirtualMachineConfiguration) -> Self {
let inner = config.inner.borrow().clone();
let serial_in = inner.serial_read_fd.and_then(dup_owned);
let serial_out = inner.serial_write_fd.and_then(dup_owned);
let (state_tx, state_rx) = bounded(1);
// Sockets live next to the disk (the per-instance directory), or in
// a private temp directory when there is no disk.
let run_dir = inner
.disk_path
.as_deref()
.and_then(|p| Path::new(p).parent())
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| {
std::env::temp_dir().join(format!("hanzo-vm-{}", std::process::id()))
});
let _ = std::fs::create_dir_all(&run_dir);
let api_socket = run_dir.join("ch-api.sock").to_string_lossy().into_owned();
let vsock_socket = run_dir.join("vsock.sock").to_string_lossy().into_owned();
VirtualMachine {
config: inner,
serial_in,
serial_out,
run_dir,
api_socket,
vsock_socket,
ch_pid: Mutex::new(None),
virtiofsd: Mutex::new(Vec::new()),
state_tx,
state_rx,
running: Arc::new(AtomicBool::new(false)),
}
}
pub fn supported() -> bool {
Path::new("/dev/kvm").exists() && find_binary("cloud-hypervisor").is_some()
}
pub fn start(&self) -> Result<()> {
let ch_bin = find_binary("cloud-hypervisor")
.ok_or_else(|| VzError::new("cloud-hypervisor not found on PATH or in ~/.local/bin"))?;
// One virtiofsd child per shared directory; the socket must be
// listening before cloud-hypervisor creates the device.
let mut fs_sockets = Vec::new();
if !self.config.mounts.is_empty() {
let fsd_bin = find_binary("virtiofsd")
.ok_or_else(|| VzError::new("virtiofsd not found on PATH or in ~/.local/bin"))?;
for (tag, host_path, _read_only) in &self.config.mounts {
let socket = self.run_dir.join(format!("fs-{}.sock", tag));
let log = std::fs::File::create(self.run_dir.join(format!("fs-{}.log", tag)))
.map_err(|e| VzError::new(format!("virtiofsd log: {}", e)))?;
let child = Command::new(&fsd_bin)
.arg(format!("--socket-path={}", socket.display()))
.arg("--shared-dir")
.arg(host_path)
.arg("--cache")
.arg("auto")
.arg("--sandbox")
.arg("none")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::from(log))
.spawn()
.map_err(|e| VzError::new(format!("spawn virtiofsd: {}", e)))?;
self.virtiofsd.lock().unwrap().push(child);
fs_sockets.push((tag.clone(), socket));
}
for (tag, socket) in &fs_sockets {
if !wait_for_socket(&socket.to_string_lossy(), Duration::from_secs(5)) {
return Err(VzError::new(format!(
"virtiofsd socket for {} did not appear",
tag
)));
}
}
}
// In-process vhost-user-net backend over the proxy socketpair.
let net_socket = self.run_dir.join("net.sock");
if let Some(fd) = self.config.network_fd {
net_backend::spawn(&net_socket.to_string_lossy(), fd)?;
}
// Serial console: the virtio-console is wired to the child's stdio.
let stdio_of = |fd: &Option<OwnedFd>| -> Result<Stdio> {
match fd {
Some(fd) => {
Ok(Stdio::from(fd.try_clone().map_err(|e| {
VzError::new(format!("dup console fd: {}", e))
})?))
}
None => Ok(Stdio::null()),
}
};
let stdin = stdio_of(&self.serial_in)?;
let stdout = stdio_of(&self.serial_out)?;
let _ = std::fs::remove_file(&self.api_socket);
let _ = std::fs::remove_file(&self.vsock_socket);
let mut child = Command::new(&ch_bin)
.arg("--api-socket")
.arg(format!("path={}", self.api_socket))
.stdin(stdin)
.stdout(stdout)
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| VzError::new(format!("spawn cloud-hypervisor: {}", e)))?;
if !wait_for_socket(&self.api_socket, Duration::from_secs(5)) {
let _ = child.kill();
let _ = child.wait();
return Err(VzError::new("cloud-hypervisor API socket did not appear"));
}
let vm_config = self.vm_config_json(&fs_sockets, &net_socket);
if let Err(e) = api::put(&self.api_socket, "vm.create", Some(&vm_config.to_string()))
.and_then(|_| api::put(&self.api_socket, "vm.boot", None))
{
let _ = child.kill();
let _ = child.wait();
return Err(e);
}
*self.ch_pid.lock().unwrap() = Some(child.id() as i32);
self.running.store(true, Ordering::Release);
let _ = self.state_tx.try_send(VmState::Running);
// Monitor thread: the cloud-hypervisor process exits when the guest
// shuts down (or on vmm.shutdown / kill from stop()).
let running = self.running.clone();
let state_tx = self.state_tx.clone();
std::thread::Builder::new()
.name("hanzo-vm-monitor".into())
.spawn(move || {
let _ = child.wait();
running.store(false, Ordering::Release);
let _ = state_tx.try_send(VmState::Stopped);
})
.map_err(|e| VzError::new(format!("spawn monitor thread: {}", e)))?;
Ok(())
}
fn vm_config_json(
&self,
fs_sockets: &[(String, PathBuf)],
net_socket: &Path,
) -> serde_json::Value {
let c = &self.config;
let mut cfg = serde_json::json!({
"cpus": { "boot_vcpus": c.cpu_count, "max_vcpus": c.cpu_count },
// vhost-user devices (fs, net) require shared guest memory.
"memory": { "size": c.memory_size, "shared": true },
"payload": { "kernel": c.kernel_path, "cmdline": c.command_line },
"serial": { "mode": "Off" },
"rng": { "src": "/dev/urandom" },
});
// Interactive consoles need Tty for input; otherwise write console
// output through the child's own stdout (Tty mode EBADFs on a
// non-terminal stdout).
cfg["console"] = if c.serial_read_fd.is_some() {
serde_json::json!({ "mode": "Tty" })
} else {
serde_json::json!({ "mode": "File", "file": "/proc/self/fd/1" })
};
if let Some(ref initrd) = c.initrd_path {
cfg["payload"]["initramfs"] = serde_json::json!(initrd);
}
if let Some(ref disk) = c.disk_path {
cfg["disks"] = serde_json::json!([{
"path": disk,
"readonly": c.disk_read_only,
"image_type": "Raw",
}]);
}
if c.has_socket {
cfg["vsock"] = serde_json::json!({ "cid": GUEST_CID, "socket": self.vsock_socket });
}
if !fs_sockets.is_empty() {
let fs: Vec<_> = fs_sockets
.iter()
.map(|(tag, socket)| {
serde_json::json!({
"tag": tag,
"socket": socket.to_string_lossy(),
"num_queues": 1,
"queue_size": 1024,
})
})
.collect();
cfg["fs"] = serde_json::json!(fs);
}
if c.network_fd.is_some() {
let mut net = serde_json::json!({
"vhost_user": true,
"vhost_socket": net_socket.to_string_lossy(),
"num_queues": 2,
"queue_size": 256,
});
if let Some(mac) = c.network_mac {
net["mac"] = serde_json::json!(format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
));
}
cfg["net"] = serde_json::json!([net]);
}
cfg
}
pub fn stop(&self) -> Result<()> {
// Ask the VMM to shut down; fall back to SIGKILL.
let _ = api::put(&self.api_socket, "vm.shutdown", None);
let _ = api::put(&self.api_socket, "vmm.shutdown", None);
if let Some(pid) = *self.ch_pid.lock().unwrap() {
let deadline = Instant::now() + Duration::from_secs(2);
while self.running.load(Ordering::Acquire) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
if self.running.load(Ordering::Acquire) {
unsafe { libc::kill(pid, libc::SIGKILL) };
}
}
for mut child in self.virtiofsd.lock().unwrap().drain(..) {
let _ = child.kill();
let _ = child.wait();
}
self.running.store(false, Ordering::Release);
let _ = self.state_tx.try_send(VmState::Stopped);
Ok(())
}
pub fn state_channel(&self) -> Receiver<VmState> {
self.state_rx.clone()
}
pub fn can_start(&self) -> bool {
!self.running.load(Ordering::Acquire)
}
pub fn can_stop(&self) -> bool {
self.running.load(Ordering::Acquire)
}
pub fn can_pause(&self) -> bool {
false
}
pub fn can_resume(&self) -> bool {
false
}
pub fn can_request_stop(&self) -> bool {
self.can_stop()
}
/// Connect to a vsock port on the guest through cloud-hypervisor's
/// hybrid vsock socket: send `CONNECT <port>\n`, expect `OK <n>\n`,
/// then the stream is a raw pipe to the guest listener.
///
/// The connected `UnixStream` fd is rewrapped as a `TcpStream` because
/// the platform-neutral sandbox API is written against `TcpStream`;
/// both are plain stream sockets, so read/write/shutdown/try_clone all
/// behave (`set_nodelay` fails and is ignored by callers).
pub fn connect_to_vsock_port(&self, port: u32) -> Result<TcpStream> {
let mut stream = UnixStream::connect(&self.vsock_socket)
.map_err(|e| VzError::new(format!("vsock connect failed: {}", e)))?;
let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
stream
.write_all(format!("CONNECT {}\n", port).as_bytes())
.map_err(|e| VzError::new(format!("vsock handshake send: {}", e)))?;
// Read the response a byte at a time so no guest data is consumed.
let mut line = Vec::with_capacity(16);
let mut byte = [0u8; 1];
loop {
match stream.read(&mut byte) {
Ok(1) => {
if byte[0] == b'\n' {
break;
}
line.push(byte[0]);
if line.len() > 32 {
return Err(VzError::new("vsock handshake: oversized response"));
}
}
Ok(_) => return Err(VzError::new("vsock connect refused (EOF)")),
Err(e) => return Err(VzError::new(format!("vsock handshake read: {}", e))),
}
}
if !line.starts_with(b"OK ") {
return Err(VzError::new(format!(
"vsock connect refused: {}",
String::from_utf8_lossy(&line)
)));
}
let _ = stream.set_read_timeout(None);
Ok(unsafe { TcpStream::from_raw_fd(stream.into_raw_fd()) })
}
pub fn state(&self) -> VmState {
if self.running.load(Ordering::Acquire) {
VmState::Running
} else {
VmState::Stopped
}
}
}
impl Drop for VirtualMachine {
fn drop(&mut self) {
// Child processes outlive a dropped VirtualMachine unless killed.
if let Some(pid) = *self.ch_pid.lock().unwrap() {
if self.running.load(Ordering::Acquire) {
unsafe { libc::kill(pid, libc::SIGKILL) };
}
}
for mut child in self.virtiofsd.lock().unwrap().drain(..) {
let _ = child.kill();
let _ = child.wait();
}
}
}
+2 -1
View File
@@ -40,7 +40,7 @@ pub fn download_os_image(data_dir: &str) -> Result<()> {
fn download_os_image_version(data_dir: &str, version: &str) -> Result<()> {
let tag = format!("v{}", version);
let tarball_name = format!("hanzo-vm-os-{}-aarch64.tar.gz", tag);
let tarball_name = format!("hanzo-vm-os-{}-{}.tar.gz", tag, std::env::consts::ARCH);
let url = format!(
"https://github.com/{}/releases/download/{}/{}",
GITHUB_REPO, tag, tarball_name
@@ -89,6 +89,7 @@ fn cli_tarball_name(version: &str) -> Result<String> {
let platform = match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => "darwin-aarch64",
("linux", "aarch64") => "linux-aarch64",
("linux", "x86_64") => "linux-x86_64",
(os, arch) => bail!("automatic upgrade is not supported on {}-{}", os, arch),
};
+1
View File
@@ -1,4 +1,5 @@
use std::collections::HashMap;
#[cfg(target_os = "macos")]
use std::ffi::CString;
use std::io::IsTerminal;
+4 -17
View File
@@ -38,7 +38,7 @@ mod guest {
if libc::fcntl(2, libc::F_GETFD) >= 0 {
return;
}
let fd = libc::open(b"/dev/null\0".as_ptr() as *const libc::c_char, libc::O_RDWR);
let fd = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR);
if fd >= 0 {
for t in 0..3 {
libc::dup2(fd, t);
@@ -240,11 +240,7 @@ mod guest {
// Check if eth0 exists (network device present)
let has_eth0 = {
let mut ifr: libc::ifreq = std::mem::zeroed();
std::ptr::copy_nonoverlapping(
b"eth0\0".as_ptr(),
ifr.ifr_name.as_mut_ptr() as *mut u8,
5,
);
std::ptr::copy_nonoverlapping(c"eth0".as_ptr(), ifr.ifr_name.as_mut_ptr(), 5);
libc::ioctl(sock, libc::SIOCGIFFLAGS as _, &mut ifr) == 0
};
@@ -257,11 +253,7 @@ mod guest {
// Check if eth0 already has an IP (configured by initramfs)
let has_ip = {
let mut ifr: libc::ifreq = std::mem::zeroed();
std::ptr::copy_nonoverlapping(
b"eth0\0".as_ptr(),
ifr.ifr_name.as_mut_ptr() as *mut u8,
5,
);
std::ptr::copy_nonoverlapping(c"eth0".as_ptr(), ifr.ifr_name.as_mut_ptr(), 5);
libc::ioctl(sock, libc::SIOCGIFADDR as _, &mut ifr) == 0
};
@@ -438,12 +430,7 @@ mod guest {
let mut reader = stream.try_clone().expect("failed to clone stream");
let mut writer = stream;
loop {
let (msg_type, payload) = match frame::read_frame(&mut reader) {
Ok(Some(f)) => f,
_ => break, // EOF or error
};
while let Ok(Some((msg_type, payload))) = frame::read_frame(&mut reader) {
match msg_type {
frame::MOUNT_REQ => {
let mount_req: MountRequest = match serde_json::from_slice(&payload) {
+3 -1
View File
@@ -17,5 +17,7 @@ socket2 = "0.5"
[target.'cfg(target_os = "macos")'.dependencies]
vm-darwin = { version = "0.1.0", path = "../vm-darwin" }
[target.'cfg(target_os = "linux")'.dependencies]
[target.'cfg(all(target_os = "linux", target_arch = "aarch64"))'.dependencies]
vm-linux = { version = "0.1.0", path = "../vm-linux" }
[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
vm-ch = { version = "0.1.0", path = "../vm-ch" }
+9 -12
View File
@@ -8,20 +8,17 @@ pub use vm_proto::{
ReadFileRequest, WriteFileRequest, WriteFileResponse, VSOCK_PORT, VSOCK_PORT_FORWARD,
};
// Re-exports from platform-specific backend for advanced/escape-hatch use
// The platform backend: Virtualization.framework on macOS, KVM on arm64
// Linux, cloud-hypervisor on x86_64 Linux. All expose the same vocabulary.
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
pub(crate) use vm_ch as backend;
#[cfg(target_os = "macos")]
pub use vm_darwin::VirtualMachine;
#[cfg(target_os = "macos")]
pub use vm_darwin::VmState;
#[cfg(target_os = "macos")]
pub use vm_darwin::VzError;
pub(crate) use vm_darwin as backend;
#[cfg(all(target_os = "linux", target_arch = "aarch64"))]
pub(crate) use vm_linux as backend;
#[cfg(target_os = "linux")]
pub use vm_linux::VirtualMachine;
#[cfg(target_os = "linux")]
pub use vm_linux::VmState;
#[cfg(target_os = "linux")]
pub use vm_linux::VzError;
// Re-exports from the backend for advanced/escape-hatch use
pub use backend::{VirtualMachine, VmState, VzError};
/// Reject checkpoint names that could escape the checkpoints directory.
pub fn validate_checkpoint_name(name: &str) -> Result<(), String> {
+3 -13
View File
@@ -9,19 +9,9 @@ use std::time::Duration;
use anyhow::{bail, Context, Result};
use crossbeam_channel::Receiver;
#[cfg(target_os = "macos")]
use vm_darwin::network::FileHandleNetworkAttachment;
#[cfg(target_os = "macos")]
use vm_darwin::terminal;
#[cfg(target_os = "macos")]
use vm_darwin::*;
#[cfg(target_os = "linux")]
use vm_linux::network::FileHandleNetworkAttachment;
#[cfg(target_os = "linux")]
use vm_linux::terminal;
#[cfg(target_os = "linux")]
use vm_linux::*;
use crate::backend::network::FileHandleNetworkAttachment;
use crate::backend::terminal;
use crate::backend::*;
use vm_proto::{
frame, ChmodRequest, CopyRequest, DiscardRequest, ExecRequest, ForwardRequest, ForwardResponse,
+227
View File
@@ -0,0 +1,227 @@
# Hanzo VM microVM kernel config — x86_64 / cloud-hypervisor
# Same feature set as vm_defconfig (arm64); arch section swapped for
# PVH direct boot, kvmclock, and ACPI CPU/device enumeration.
# All drivers built-in (no loadable modules)
# Architecture
CONFIG_64BIT=y
CONFIG_SMP=y
CONFIG_NR_CPUS=16
CONFIG_HYPERVISOR_GUEST=y
CONFIG_PARAVIRT=y
CONFIG_KVM_GUEST=y
CONFIG_PVH=y
CONFIG_ACPI=y
# No loadable modules
# CONFIG_MODULES is not set
# General
CONFIG_SYSVIPC=y
CONFIG_POSIX_MQUEUE=y
CONFIG_NO_HZ_IDLE=y
CONFIG_HIGH_RES_TIMERS=y
CONFIG_PREEMPT_VOLUNTARY=y
CONFIG_HZ_100=y
CONFIG_SYSCTL=y
CONFIG_MULTIUSER=y
CONFIG_FHANDLE=y
CONFIG_EPOLL=y
CONFIG_SIGNALFD=y
CONFIG_TIMERFD=y
CONFIG_EVENTFD=y
CONFIG_AIO=y
CONFIG_IO_URING=y
# Size optimization
CONFIG_CC_OPTIMIZE_FOR_SIZE=y
# CONFIG_DEBUG_KERNEL is not set
# CONFIG_DEBUG_INFO is not set
# CONFIG_FTRACE is not set
# CONFIG_KPROBES is not set
# CONFIG_PROFILING is not set
# PCI (cloud-hypervisor uses PCI transport)
CONFIG_PCI=y
CONFIG_PCI_MSI=y
# VirtIO core
CONFIG_VIRTIO=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_MENU=y
# VirtIO devices (all built-in)
CONFIG_VIRTIO_BLK=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_VIRTIO_NET=y
CONFIG_VIRTIO_BALLOON=y
CONFIG_VIRTIO_MMIO=y
CONFIG_HW_RANDOM=y
CONFIG_HW_RANDOM_VIRTIO=y
CONFIG_VIRTIO_FS=y
CONFIG_VIRTIO_INPUT=y
# vsock (all built-in)
CONFIG_VSOCKETS=y
CONFIG_VIRTIO_VSOCKETS=y
CONFIG_VIRTIO_VSOCKETS_COMMON=y
# Block layer
CONFIG_BLOCK=y
CONFIG_BLK_DEV=y
CONFIG_BLK_DEV_INITRD=y
# Filesystems
CONFIG_EXT4_FS=y
CONFIG_EXT4_FS_POSIX_ACL=y
CONFIG_EXT4_USE_FOR_EXT2=y
CONFIG_OVERLAY_FS=y
CONFIG_FUSE_FS=y
CONFIG_TMPFS=y
CONFIG_TMPFS_POSIX_ACL=y
CONFIG_PROC_FS=y
CONFIG_PROC_SYSCTL=y
CONFIG_SYSFS=y
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_DEVPTS_FS=y
CONFIG_CONFIGFS_FS=y
# Networking
CONFIG_NET=y
CONFIG_NETDEVICES=y
CONFIG_LOOPBACK=y
CONFIG_PACKET=y
CONFIG_UNIX=y
CONFIG_INET=y
CONFIG_IP_MULTICAST=y
CONFIG_IP_ADVANCED_ROUTER=y
CONFIG_IP_MULTIPLE_TABLES=y
CONFIG_IP_ROUTE_MULTIPATH=y
CONFIG_IPV6=y
CONFIG_BRIDGE=y
# Netfilter (for iptables REDIRECT in proxy mode)
CONFIG_NETFILTER=y
CONFIG_NETFILTER_ADVANCED=y
CONFIG_NETFILTER_XTABLES=y
CONFIG_NETFILTER_XT_MATCH_CONNTRACK=y
CONFIG_NETFILTER_XT_MATCH_STATE=y
CONFIG_NETFILTER_XT_TARGET_REDIRECT=y
CONFIG_NF_CONNTRACK=y
CONFIG_NF_NAT=y
CONFIG_NF_TABLES=y
CONFIG_NF_TABLES_INET=y
CONFIG_NFT_NAT=y
CONFIG_NFT_REDIR=y
CONFIG_NFT_CT=y
CONFIG_IP_NF_IPTABLES=y
CONFIG_IP_NF_FILTER=y
CONFIG_IP_NF_NAT=y
CONFIG_IP_NF_TARGET_REDIRECT=y
CONFIG_IP_NF_TARGET_MASQUERADE=y
# CRC32 (required by ext4)
CONFIG_CRC32=y
CONFIG_LIBCRC32C=y
CONFIG_CRYPTO_CRC32C=y
# TTY / Console
CONFIG_TTY=y
CONFIG_VT=y
CONFIG_HVC_DRIVER=y
CONFIG_PRINTK=y
# PTY support (for shell/TTY exec)
CONFIG_UNIX98_PTYS=y
# Cgroups (future container features)
CONFIG_CGROUPS=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_PIDS=y
CONFIG_CGROUP_SCHED=y
CONFIG_MEMCG=y
# Namespaces
CONFIG_NAMESPACES=y
CONFIG_UTS_NS=y
CONFIG_IPC_NS=y
CONFIG_PID_NS=y
CONFIG_NET_NS=y
CONFIG_USER_NS=y
# inotify (used by many tools)
CONFIG_INOTIFY_USER=y
# Disable unnecessary subsystems
# CONFIG_SOUND is not set
# CONFIG_USB_SUPPORT is not set
# CONFIG_WLAN is not set
# CONFIG_WIRELESS is not set
# CONFIG_INPUT_KEYBOARD is not set
# CONFIG_INPUT_MOUSE is not set
# CONFIG_INPUT_TOUCHSCREEN is not set
# CONFIG_SCSI is not set
# CONFIG_ATA is not set
# CONFIG_DRM is not set
# CONFIG_FB is not set
# CONFIG_VGA_CONSOLE is not set
# CONFIG_SELINUX is not set
# CONFIG_SECURITY is not set
# CONFIG_AUDIT is not set
# CONFIG_SWAP is not set
# CONFIG_NFS_FS is not set
# CONFIG_CIFS is not set
# CONFIG_BTRFS_FS is not set
# CONFIG_XFS_FS is not set
# CONFIG_BLK_DEV_LOOP is not set
# CONFIG_BLK_DEV_RAM is not set
# CONFIG_PCMCIA is not set
# CONFIG_HWMON is not set
# CONFIG_THERMAL is not set
# CONFIG_WATCHDOG is not set
# CONFIG_MEDIA_SUPPORT is not set
# CONFIG_POWER_SUPPLY is not set
# CONFIG_MD is not set
# Cgroup v2 controllers k3s requires (cpuset is fatal when missing)
CONFIG_CPUSETS=y
CONFIG_PROC_PID_CPUSET=y
CONFIG_FAIR_GROUP_SCHED=y
CONFIG_CFS_BANDWIDTH=y
CONFIG_BLK_CGROUP=y
CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
# Container runtime (runc: session keyring, seccomp, cgroup2 device bpf)
CONFIG_KEYS=y
CONFIG_SECCOMP=y
CONFIG_SECCOMP_FILTER=y
# Pod networking: flannel vxlan, CNI bridge/veth, kube-proxy iptables
CONFIG_VETH=y
CONFIG_VXLAN=y
CONFIG_BRIDGE_NETFILTER=y
CONFIG_NETFILTER_XT_MARK=y
CONFIG_NETFILTER_XT_NAT=y
CONFIG_NETFILTER_XT_MATCH_ADDRTYPE=y
CONFIG_NETFILTER_XT_MATCH_COMMENT=y
CONFIG_NETFILTER_XT_MATCH_MULTIPORT=y
CONFIG_NETFILTER_XT_MATCH_STATISTIC=y
CONFIG_NETFILTER_XT_MATCH_PHYSDEV=y
CONFIG_NETFILTER_XT_TARGET_MASQUERADE=y
CONFIG_IP_NF_MANGLE=y
CONFIG_IP_NF_RAW=y
CONFIG_IP_NF_TARGET_REJECT=y
CONFIG_IP6_NF_IPTABLES=y
CONFIG_IP6_NF_FILTER=y
CONFIG_IP6_NF_MANGLE=y
CONFIG_IP6_NF_NAT=y
CONFIG_IP6_NF_TARGET_MASQUERADE=y
CONFIG_IP6_NF_TARGET_REJECT=y
CONFIG_NFT_COMPAT=y
CONFIG_NFT_MASQ=y
CONFIG_NFT_REJECT=y
+15 -1
View File
@@ -28,7 +28,21 @@ if [ ! -d "${BUILD_DIR}/linux-${KERNEL_VERSION}" ]; then
rm -f "${BUILD_DIR}/linux-${KERNEL_VERSION}.tar.xz"
fi
if [ "$(uname -m)" = "aarch64" ] && [ "$(uname -s)" = "Linux" ]; then
if [ "$(uname -m)" = "x86_64" ] && [ "$(uname -s)" = "Linux" ]; then
echo " Native x86_64 Linux detected, building without Docker"
cd "${BUILD_DIR}/linux-${KERNEL_VERSION}"
cp "${REPO_DIR}/kernel/vm_defconfig_x86" arch/x86/configs/vm_defconfig
make ARCH=x86_64 vm_defconfig
echo " Compiling kernel (this takes a few minutes)..."
make ARCH=x86_64 -j"$(nproc)" vmlinux 2>&1 | tail -5
# cloud-hypervisor direct-boots the uncompressed ELF via its PVH entry.
strip -o "${DATA_DIR}/Image" vmlinux
echo " Kernel built: $(du -h "${DATA_DIR}/Image" | cut -f1)"
elif [ "$(uname -m)" = "aarch64" ] && [ "$(uname -s)" = "Linux" ]; then
echo " Native aarch64 Linux detected, building without Docker"
cd "${BUILD_DIR}/linux-${KERNEL_VERSION}"
+10 -8
View File
@@ -13,15 +13,22 @@ DEBIAN_RELEASE="trixie"
DATA_DIR="${HANZO_VM_HOME:-$HOME/.hanzo/vm}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
GUEST_BINARY="${REPO_DIR}/target/aarch64-unknown-linux-musl/release/vm-guest"
# Matches the CLI's default --disk-size so the per-run clone is never
# extended (extending an APFS clone costs ~0.5s at truncate and ~0.7s at
# delete). The image is sparse; unwritten space costs nothing on disk.
ROOTFS_SIZE_MB=4096
# The guest image is built for the host architecture (macOS builds arm64
# in a container below).
case "$(uname -m)" in
x86_64) GUEST_TARGET="x86_64-unknown-linux-musl"; DEB_ARCH="amd64" ;;
*) GUEST_TARGET="aarch64-unknown-linux-musl"; DEB_ARCH="arm64" ;;
esac
GUEST_BINARY="${REPO_DIR}/target/${GUEST_TARGET}/release/vm-guest"
if [ ! -f "$GUEST_BINARY" ]; then
echo "ERROR: guest binary not found at ${GUEST_BINARY}"
echo " Run: cargo build -p vm-guest --target aarch64-unknown-linux-musl --release"
echo " Run: cargo build -p vm-guest --target ${GUEST_TARGET} --release"
exit 1
fi
@@ -34,11 +41,6 @@ if [ "$(uname)" = "Darwin" ]; then
"debian:${DEBIAN_RELEASE}-slim" /src/scripts/prepare-rootfs.sh
fi
if [ "$(uname -m)" != "aarch64" ]; then
echo "ERROR: the guest is aarch64; build on an arm64 Linux host"
exit 1
fi
SUDO=""
[ "$(id -u)" = 0 ] || SUDO=sudo
@@ -99,7 +101,7 @@ if [ -f "${DATA_DIR}/rootfs.ext4" ]; then
else
echo "==> Building Debian ${DEBIAN_RELEASE} rootfs (${ROOTFS_SIZE_MB}MB)..."
ROOT="${WORK}/rootfs"
$SUDO debootstrap --arch=arm64 --variant=minbase "$DEBIAN_RELEASE" "$ROOT" http://deb.debian.org/debian
$SUDO debootstrap --arch="$DEB_ARCH" --variant=minbase "$DEBIAN_RELEASE" "$ROOT" http://deb.debian.org/debian
$SUDO mkdir -p "${ROOT}/etc/dpkg/dpkg.cfg.d"
cat <<'DPKG' | $SUDO tee "${ROOT}/etc/dpkg/dpkg.cfg.d/01-nodoc" > /dev/null