Skip to content

Commit 1dc27b4

Browse files
SashaMITcursoragent
andcommitted
feat(crosvm): make workspace build on non-Linux hosts (macOS, Windows)
Per state.md's "default Home path must remain a KVM-independent browser-hosted adapter so macOS and Windows stay in scope without pretending to offer Linux parity" and TASKS.md item "Keep the default Home path compatible with macOS and Windows by avoiding KVM-only assumptions / Remove remaining donor/KVM-only assumptions". elastos-crosvm was an unconditional workspace member whose network.rs uses Linux-only libc primitives (SOCK_CLOEXEC, Linux ioctl signature, sockaddr_in without sin_len) and broke the whole workspace on macOS. This change keeps Linux behavior bit-identical and lets the workspace compile on non-Linux: - elastos-crosvm/src/network_stub.rs: new non-Linux module mirroring the public surface of network.rs (NetworkConfig, ::new, ::setup, ::teardown, generate_mac, subnet_octet_for_vm). setup() fails closed with an explicit "requires Linux" error matching the existing /dev/kvm fail-closed pattern in vm.rs::start(). teardown() is a no-op on platforms where nothing was set up. - elastos-crosvm/src/lib.rs: cfg-gate mod network so the Linux module is used on Linux and the stub is used elsewhere. Doc comment updated to reference the KVM-independent paths that stay in scope on non-Linux. - elastos-crosvm/src/rootfs.rs: cfg-gate test_data_disk_creation to Linux only — it shells out to mkfs.ext4 which is e2fsprogs and not present on macOS/Windows. The production code that calls mkfs.ext4 is itself only reachable on Linux via the /dev/kvm gate. - elastos-guest/src/runtime.rs: use std::ptr::null_mut() instead of std::ptr::null() for the optional termios/winsize args to libc::openpty in a test. macOS' openpty signature requires *mut; Linux libc accepts either. Verified on Apple Silicon M5 Pro / macOS 26.4.1: - cargo build -p elastos-server --release succeeds; ./target/release/elastos --version and --help run cleanly - cargo check --workspace --all-targets succeeds - cargo test -p elastos-crosvm: 18/18 pass - cargo fmt --check and cargo clippy -p elastos-crosvm --all-targets -- -D warnings are clean Layer 1 only: this gets the workspace and the elastos binary running on macOS, with browser-hosted Home + WASM + data capsule paths available. MicroVM capsule launch still fails closed at runtime on non-Linux via the existing /dev/kvm check. Layer 2 (making critical microvm-configured capsules like shell and localhost-provider available on non-Linux via an alternate substrate, in-process provider, or WASM substrate) is separate planned work and not in scope here. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5b90ea8 commit 1dc27b4

4 files changed

Lines changed: 170 additions & 5 deletions

File tree

elastos/crates/elastos-crosvm/src/lib.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
//! ElastOS crosvm Compute Provider
22
//!
3-
//! Runs capsules in crosvm VMs with hardware-level isolation.
4-
//! crosvm is the sole VM backend — no fallback, no feature gating.
3+
//! Runs capsules in crosvm VMs with hardware-level isolation on Linux/KVM.
4+
//! crosvm is the sole microVM backend; capsule launch fails closed on hosts
5+
//! without `/dev/kvm` rather than silently downgrading.
56
//!
6-
//! # Requirements
7+
//! On non-Linux hosts (macOS, Windows) the crate still compiles so the rest
8+
//! of the runtime — browser-hosted Home, WASM capsules, data capsules — stays
9+
//! in scope. Per `state.md`: *"The default Home path must remain a
10+
//! KVM-independent browser-hosted adapter so macOS and Windows stay in scope
11+
//! without pretending to offer Linux parity."* The guest-network module is
12+
//! replaced by a stub that mirrors the public surface and returns explicit
13+
//! errors if any microVM path is invoked.
14+
//!
15+
//! # Linux requirements
716
//!
817
//! - Linux with KVM support (`/dev/kvm`)
918
//! - crosvm binary
@@ -22,6 +31,10 @@
2231
//! ```
2332
2433
mod config;
34+
#[cfg(target_os = "linux")]
35+
mod network;
36+
#[cfg(not(target_os = "linux"))]
37+
#[path = "network_stub.rs"]
2538
mod network;
2639
mod provider;
2740
mod proxy;
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//! Non-Linux stub for the guest-network module.
2+
//!
3+
//! crosvm guest networking is implemented via Linux TAP devices and ioctl.
4+
//! On non-Linux platforms (macOS, Windows) the runtime can still compile and
5+
//! run for KVM-independent paths (browser-hosted Home, WASM capsules, data
6+
//! capsules). MicroVM launch already fails closed via the `/dev/kvm` check in
7+
//! `vm.rs::start()`; this stub keeps the rest of the crate buildable.
8+
//!
9+
//! Per `state.md`:
10+
//!
11+
//! > The default Home path must remain a KVM-independent browser-hosted adapter
12+
//! > so macOS and Windows stay in scope without pretending to offer Linux parity.
13+
14+
use std::sync::atomic::AtomicI32;
15+
16+
use elastos_common::{ElastosError, Result};
17+
18+
/// Network configuration mirror for non-Linux platforms.
19+
///
20+
/// The field shape mirrors the Linux implementation so that callers compile
21+
/// unchanged. All operations that would touch a real TAP device fail closed.
22+
pub struct NetworkConfig {
23+
pub tap_name: String,
24+
pub host_ip: String,
25+
pub guest_ip: String,
26+
pub mask: String,
27+
pub prefix_len: u8,
28+
pub guest_mac: String,
29+
_tap_fd: AtomicI32,
30+
}
31+
32+
impl std::fmt::Debug for NetworkConfig {
33+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34+
f.debug_struct("NetworkConfig")
35+
.field("tap_name", &self.tap_name)
36+
.field("host_ip", &self.host_ip)
37+
.field("guest_ip", &self.guest_ip)
38+
.finish()
39+
}
40+
}
41+
42+
impl Clone for NetworkConfig {
43+
fn clone(&self) -> Self {
44+
Self {
45+
tap_name: self.tap_name.clone(),
46+
host_ip: self.host_ip.clone(),
47+
guest_ip: self.guest_ip.clone(),
48+
mask: self.mask.clone(),
49+
prefix_len: self.prefix_len,
50+
guest_mac: self.guest_mac.clone(),
51+
_tap_fd: AtomicI32::new(-1),
52+
}
53+
}
54+
}
55+
56+
impl NetworkConfig {
57+
pub fn new(vm_id: &str) -> Self {
58+
let tap_suffix: String = vm_id.chars().take(8).collect();
59+
let tap_name = format!("cv{}", tap_suffix);
60+
let subnet_octet = subnet_octet_for_vm(vm_id);
61+
62+
Self {
63+
tap_name,
64+
host_ip: format!("172.16.{}.1", subnet_octet),
65+
guest_ip: format!("172.16.{}.2", subnet_octet),
66+
mask: "255.255.255.252".to_string(),
67+
prefix_len: 30,
68+
guest_mac: generate_mac(vm_id),
69+
_tap_fd: AtomicI32::new(-1),
70+
}
71+
}
72+
73+
/// Fails closed on non-Linux: TAP networking requires the Linux tun/tap
74+
/// stack and CAP_NET_ADMIN. Callers should never reach this path because
75+
/// `crosvm::is_supported()` returns `false` here, but the error is explicit
76+
/// in case a code path slips through.
77+
pub fn setup(&self) -> Result<()> {
78+
Err(ElastosError::Compute(
79+
"crosvm guest networking requires Linux (TAP via /dev/net/tun); \
80+
this build target does not support microVM networking"
81+
.into(),
82+
))
83+
}
84+
85+
/// Idempotent on non-Linux: nothing was set up, so nothing to tear down.
86+
pub fn teardown(&self) -> Result<()> {
87+
Ok(())
88+
}
89+
}
90+
91+
/// Deterministic MAC derivation. Pure logic, identical to the Linux module.
92+
pub fn generate_mac(vm_id: &str) -> String {
93+
use std::collections::hash_map::DefaultHasher;
94+
use std::hash::{Hash, Hasher};
95+
let mut hasher = DefaultHasher::new();
96+
vm_id.hash(&mut hasher);
97+
let hash = hasher.finish();
98+
format!(
99+
"AA:FC:{:02X}:{:02X}:{:02X}:{:02X}",
100+
(hash >> 8) as u8,
101+
(hash >> 16) as u8,
102+
(hash >> 24) as u8,
103+
(hash >> 32) as u8,
104+
)
105+
}
106+
107+
/// Deterministic subnet allocator. Pure logic, identical to the Linux module.
108+
pub fn subnet_octet_for_vm(vm_id: &str) -> u8 {
109+
let hash: u64 = vm_id
110+
.bytes()
111+
.fold(0u64, |acc, b| acc.wrapping_mul(131).wrapping_add(b as u64));
112+
((hash % 250) as u8) + 1
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
119+
#[test]
120+
fn stub_network_config_new_matches_linux_shape() {
121+
let config = NetworkConfig::new("test-vm-123");
122+
assert!(config.tap_name.starts_with("cv"));
123+
assert!(config.host_ip.starts_with("172.16."));
124+
assert!(config.host_ip.ends_with(".1"));
125+
assert!(config.guest_ip.ends_with(".2"));
126+
assert_eq!(config.prefix_len, 30);
127+
}
128+
129+
#[test]
130+
fn stub_setup_fails_closed_with_explicit_reason() {
131+
let config = NetworkConfig::new("test-vm");
132+
let err = config.setup().unwrap_err();
133+
assert!(
134+
err.to_string().contains("Linux"),
135+
"expected error to mention Linux requirement, got: {err}"
136+
);
137+
}
138+
139+
#[test]
140+
fn stub_teardown_is_noop_ok() {
141+
let config = NetworkConfig::new("test-vm");
142+
assert!(config.teardown().is_ok());
143+
}
144+
}

elastos/crates/elastos-crosvm/src/rootfs.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,11 @@ mod tests {
177177
assert!(manager.overlay_dir().exists());
178178
}
179179

180+
// Shells out to `mkfs.ext4`, which is part of e2fsprogs on Linux. macOS and
181+
// Windows hosts do not ship it, so the test is scoped to Linux. The
182+
// production code that calls mkfs.ext4 is itself only reached on Linux via
183+
// the /dev/kvm fail-closed path in vm.rs::start().
184+
#[cfg(target_os = "linux")]
180185
#[tokio::test]
181186
async fn test_data_disk_creation() {
182187
let temp = tempdir().unwrap();

elastos/crates/elastos-guest/src/runtime.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,12 +1135,15 @@ mod tests {
11351135
let mut slave_fd = -1;
11361136
let mut name = [0i8; 128];
11371137

1138+
// null_mut() (not null()) matches macOS' `openpty` signature, which
1139+
// takes `*mut termios` / `*mut winsize` for the optional args. Linux
1140+
// libc bindings accept either; null_mut is valid on both.
11381141
let rc = libc::openpty(
11391142
&mut master_fd,
11401143
&mut slave_fd,
11411144
name.as_mut_ptr(),
1142-
std::ptr::null(),
1143-
std::ptr::null(),
1145+
std::ptr::null_mut(),
1146+
std::ptr::null_mut(),
11441147
);
11451148
assert_eq!(rc, 0, "openpty failed: {}", std::io::Error::last_os_error());
11461149
assert!(master_fd >= 0);

0 commit comments

Comments
 (0)