Skip to main content

vm_ch/
vm.rs

1//! VirtualMachine driving a `cloud-hypervisor` child process over its API
2//! socket: `vm.create` + `vm.boot` on start, `vm.shutdown` + `vmm.shutdown`
3//! on stop. One `virtiofsd` child serves each shared directory, and the
4//! in-process vhost-user-net backend serves the proxy socketpair, so guest
5//! memory is always mapped `shared=on`.
6
7use std::io::{Read, Write};
8use std::net::TcpStream;
9use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
10use std::os::unix::net::UnixStream;
11use std::path::{Path, PathBuf};
12use std::process::{Child, Command, Stdio};
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16
17use crossbeam_channel::{bounded, Receiver, Sender};
18
19use crate::api;
20use crate::configuration::{ConfigData, VirtualMachineConfiguration};
21use crate::error::{Result, VzError};
22use crate::net_backend;
23
24const GUEST_CID: u32 = 3;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VmState {
28    Stopped = 0,
29    Running = 1,
30    Error = 3,
31    // Paused/Starting/etc. kept as discriminants for Darwin compatibility.
32    Unknown = -1,
33}
34
35pub struct VirtualMachine {
36    config: ConfigData,
37    // Duplicated at construction time: the caller's fds (e.g. an opened
38    // /dev/null) may not outlive the configuration.
39    serial_in: Option<OwnedFd>,
40    serial_out: Option<OwnedFd>,
41    run_dir: PathBuf,
42    api_socket: String,
43    vsock_socket: String,
44    ch_pid: Mutex<Option<i32>>,
45    virtiofsd: Mutex<Vec<Child>>,
46    state_tx: Sender<VmState>,
47    state_rx: Receiver<VmState>,
48    running: Arc<AtomicBool>,
49}
50
51/// Locate a helper binary on PATH or in `~/.local/bin`.
52fn find_binary(name: &str) -> Option<PathBuf> {
53    if let Ok(path) = std::env::var("PATH") {
54        for dir in path.split(':') {
55            let candidate = Path::new(dir).join(name);
56            if candidate.is_file() {
57                return Some(candidate);
58            }
59        }
60    }
61    if let Ok(home) = std::env::var("HOME") {
62        let candidate = Path::new(&home).join(".local/bin").join(name);
63        if candidate.is_file() {
64            return Some(candidate);
65        }
66    }
67    None
68}
69
70/// Duplicate a raw fd; None if the fd is invalid.
71fn dup_owned(fd: i32) -> Option<OwnedFd> {
72    let duped = unsafe { libc::dup(fd) };
73    if duped < 0 {
74        return None;
75    }
76    Some(unsafe { OwnedFd::from_raw_fd(duped) })
77}
78
79fn wait_for_socket(path: &str, timeout: Duration) -> bool {
80    let deadline = Instant::now() + timeout;
81    while Instant::now() < deadline {
82        if Path::new(path).exists() {
83            return true;
84        }
85        std::thread::sleep(Duration::from_millis(5));
86    }
87    false
88}
89
90impl VirtualMachine {
91    pub fn new(config: &VirtualMachineConfiguration) -> Self {
92        let inner = config.inner.borrow().clone();
93        let serial_in = inner.serial_read_fd.and_then(dup_owned);
94        let serial_out = inner.serial_write_fd.and_then(dup_owned);
95        let (state_tx, state_rx) = bounded(1);
96
97        // Sockets live in a private per-pid directory, removed on drop.
98        // Never next to the disk: a throwaway disk may sit in a directory
99        // shared between instances (tmpfs), and colliding API sockets stop
100        // cloud-hypervisor from starting at all.
101        let run_dir = std::env::temp_dir().join(format!("hanzo-vm-{}", std::process::id()));
102        let _ = std::fs::create_dir_all(&run_dir);
103
104        let api_socket = run_dir.join("ch-api.sock").to_string_lossy().into_owned();
105        let vsock_socket = run_dir.join("vsock.sock").to_string_lossy().into_owned();
106
107        VirtualMachine {
108            config: inner,
109            serial_in,
110            serial_out,
111            run_dir,
112            api_socket,
113            vsock_socket,
114            ch_pid: Mutex::new(None),
115            virtiofsd: Mutex::new(Vec::new()),
116            state_tx,
117            state_rx,
118            running: Arc::new(AtomicBool::new(false)),
119        }
120    }
121
122    pub fn supported() -> bool {
123        Path::new("/dev/kvm").exists() && find_binary("cloud-hypervisor").is_some()
124    }
125
126    pub fn start(&self) -> Result<()> {
127        let ch_bin = find_binary("cloud-hypervisor")
128            .ok_or_else(|| VzError::new("cloud-hypervisor not found on PATH or in ~/.local/bin"))?;
129
130        // One virtiofsd child per shared directory; the socket must be
131        // listening before cloud-hypervisor creates the device.
132        let mut fs_sockets = Vec::new();
133        if !self.config.mounts.is_empty() {
134            let fsd_bin = find_binary("virtiofsd")
135                .ok_or_else(|| VzError::new("virtiofsd not found on PATH or in ~/.local/bin"))?;
136            for (tag, host_path, _read_only) in &self.config.mounts {
137                let socket = self.run_dir.join(format!("fs-{}.sock", tag));
138                let log = std::fs::File::create(self.run_dir.join(format!("fs-{}.log", tag)))
139                    .map_err(|e| VzError::new(format!("virtiofsd log: {}", e)))?;
140                let child = Command::new(&fsd_bin)
141                    .arg(format!("--socket-path={}", socket.display()))
142                    .arg("--shared-dir")
143                    .arg(host_path)
144                    .arg("--cache")
145                    .arg("auto")
146                    .arg("--sandbox")
147                    .arg("none")
148                    .stdin(Stdio::null())
149                    .stdout(Stdio::null())
150                    .stderr(Stdio::from(log))
151                    .spawn()
152                    .map_err(|e| VzError::new(format!("spawn virtiofsd: {}", e)))?;
153                self.virtiofsd.lock().unwrap().push(child);
154                fs_sockets.push((tag.clone(), socket));
155            }
156            for (tag, socket) in &fs_sockets {
157                if !wait_for_socket(&socket.to_string_lossy(), Duration::from_secs(5)) {
158                    return Err(VzError::new(format!(
159                        "virtiofsd socket for {} did not appear",
160                        tag
161                    )));
162                }
163            }
164        }
165
166        // In-process vhost-user-net backend over the proxy socketpair.
167        let net_socket = self.run_dir.join("net.sock");
168        if let Some(fd) = self.config.network_fd {
169            net_backend::spawn(&net_socket.to_string_lossy(), fd)?;
170        }
171
172        // Serial console: the virtio-console is wired to the child's stdio.
173        let stdio_of = |fd: &Option<OwnedFd>| -> Result<Stdio> {
174            match fd {
175                Some(fd) => {
176                    Ok(Stdio::from(fd.try_clone().map_err(|e| {
177                        VzError::new(format!("dup console fd: {}", e))
178                    })?))
179                }
180                None => Ok(Stdio::null()),
181            }
182        };
183        let stdin = stdio_of(&self.serial_in)?;
184        let stdout = stdio_of(&self.serial_out)?;
185
186        let _ = std::fs::remove_file(&self.api_socket);
187        let _ = std::fs::remove_file(&self.vsock_socket);
188
189        let mut child = Command::new(&ch_bin)
190            .arg("--api-socket")
191            .arg(format!("path={}", self.api_socket))
192            .stdin(stdin)
193            .stdout(stdout)
194            .stderr(Stdio::inherit())
195            .spawn()
196            .map_err(|e| VzError::new(format!("spawn cloud-hypervisor: {}", e)))?;
197
198        if !wait_for_socket(&self.api_socket, Duration::from_secs(5)) {
199            let _ = child.kill();
200            let _ = child.wait();
201            return Err(VzError::new("cloud-hypervisor API socket did not appear"));
202        }
203
204        let vm_config = self.vm_config_json(&fs_sockets, &net_socket);
205        if let Err(e) = api::put(&self.api_socket, "vm.create", Some(&vm_config.to_string()))
206            .and_then(|_| api::put(&self.api_socket, "vm.boot", None))
207        {
208            let _ = child.kill();
209            let _ = child.wait();
210            return Err(e);
211        }
212
213        *self.ch_pid.lock().unwrap() = Some(child.id() as i32);
214        self.running.store(true, Ordering::Release);
215        let _ = self.state_tx.try_send(VmState::Running);
216
217        // Monitor thread: the cloud-hypervisor process exits when the guest
218        // shuts down (or on vmm.shutdown / kill from stop()).
219        let running = self.running.clone();
220        let state_tx = self.state_tx.clone();
221        std::thread::Builder::new()
222            .name("hanzo-vm-monitor".into())
223            .spawn(move || {
224                let _ = child.wait();
225                running.store(false, Ordering::Release);
226                let _ = state_tx.try_send(VmState::Stopped);
227            })
228            .map_err(|e| VzError::new(format!("spawn monitor thread: {}", e)))?;
229
230        Ok(())
231    }
232
233    fn vm_config_json(
234        &self,
235        fs_sockets: &[(String, PathBuf)],
236        net_socket: &Path,
237    ) -> serde_json::Value {
238        let c = &self.config;
239        let mut cfg = serde_json::json!({
240            "cpus": { "boot_vcpus": c.cpu_count, "max_vcpus": c.cpu_count },
241            // vhost-user devices (fs, net) require shared guest memory.
242            "memory": { "size": c.memory_size, "shared": true },
243            "payload": { "kernel": c.kernel_path, "cmdline": c.command_line },
244            "serial": { "mode": "Off" },
245            "rng": { "src": "/dev/urandom" },
246        });
247        // Interactive consoles need Tty for input; verbose writes console
248        // output through the child's own stdout (Tty mode EBADFs on a
249        // non-terminal stdout). With no serial port configured the console
250        // device is off entirely.
251        cfg["console"] = if c.serial_read_fd.is_some() {
252            serde_json::json!({ "mode": "Tty" })
253        } else if c.serial_write_fd.is_some() {
254            serde_json::json!({ "mode": "File", "file": "/proc/self/fd/1" })
255        } else {
256            serde_json::json!({ "mode": "Off" })
257        };
258        if let Some(ref initrd) = c.initrd_path {
259            cfg["payload"]["initramfs"] = serde_json::json!(initrd);
260        }
261        if let Some(ref disk) = c.disk_path {
262            cfg["disks"] = serde_json::json!([{
263                "path": disk,
264                "readonly": c.disk_read_only,
265                "image_type": "Raw",
266            }]);
267        }
268        if c.has_socket {
269            cfg["vsock"] = serde_json::json!({ "cid": GUEST_CID, "socket": self.vsock_socket });
270        }
271        if !fs_sockets.is_empty() {
272            let fs: Vec<_> = fs_sockets
273                .iter()
274                .map(|(tag, socket)| {
275                    serde_json::json!({
276                        "tag": tag,
277                        "socket": socket.to_string_lossy(),
278                        "num_queues": 1,
279                        "queue_size": 1024,
280                    })
281                })
282                .collect();
283            cfg["fs"] = serde_json::json!(fs);
284        }
285        if c.network_fd.is_some() {
286            let mut net = serde_json::json!({
287                "vhost_user": true,
288                "vhost_socket": net_socket.to_string_lossy(),
289                "num_queues": 2,
290                "queue_size": 256,
291            });
292            if let Some(mac) = c.network_mac {
293                net["mac"] = serde_json::json!(format!(
294                    "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
295                    mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
296                ));
297            }
298            cfg["net"] = serde_json::json!([net]);
299        }
300        cfg
301    }
302
303    pub fn stop(&self) -> Result<()> {
304        // Ask the VMM to shut down; fall back to SIGKILL.
305        let _ = api::put(&self.api_socket, "vm.shutdown", None);
306        let _ = api::put(&self.api_socket, "vmm.shutdown", None);
307
308        if let Some(pid) = *self.ch_pid.lock().unwrap() {
309            let deadline = Instant::now() + Duration::from_secs(2);
310            while self.running.load(Ordering::Acquire) && Instant::now() < deadline {
311                std::thread::sleep(Duration::from_millis(10));
312            }
313            if self.running.load(Ordering::Acquire) {
314                unsafe { libc::kill(pid, libc::SIGKILL) };
315            }
316        }
317
318        for mut child in self.virtiofsd.lock().unwrap().drain(..) {
319            let _ = child.kill();
320            let _ = child.wait();
321        }
322
323        self.running.store(false, Ordering::Release);
324        let _ = self.state_tx.try_send(VmState::Stopped);
325        Ok(())
326    }
327
328    pub fn state_channel(&self) -> Receiver<VmState> {
329        self.state_rx.clone()
330    }
331
332    pub fn can_start(&self) -> bool {
333        !self.running.load(Ordering::Acquire)
334    }
335
336    pub fn can_stop(&self) -> bool {
337        self.running.load(Ordering::Acquire)
338    }
339
340    pub fn can_pause(&self) -> bool {
341        false
342    }
343    pub fn can_resume(&self) -> bool {
344        false
345    }
346
347    pub fn can_request_stop(&self) -> bool {
348        self.can_stop()
349    }
350
351    /// Connect to a vsock port on the guest through cloud-hypervisor's
352    /// hybrid vsock socket: send `CONNECT <port>\n`, expect `OK <n>\n`,
353    /// then the stream is a raw pipe to the guest listener.
354    ///
355    /// The connected `UnixStream` fd is rewrapped as a `TcpStream` because
356    /// the platform-neutral sandbox API is written against `TcpStream`;
357    /// both are plain stream sockets, so read/write/shutdown/try_clone all
358    /// behave (`set_nodelay` fails and is ignored by callers).
359    pub fn connect_to_vsock_port(&self, port: u32) -> Result<TcpStream> {
360        let mut stream = UnixStream::connect(&self.vsock_socket)
361            .map_err(|e| VzError::new(format!("vsock connect failed: {}", e)))?;
362        let _ = stream.set_read_timeout(Some(Duration::from_secs(2)));
363
364        stream
365            .write_all(format!("CONNECT {}\n", port).as_bytes())
366            .map_err(|e| VzError::new(format!("vsock handshake send: {}", e)))?;
367
368        // Read the response a byte at a time so no guest data is consumed.
369        let mut line = Vec::with_capacity(16);
370        let mut byte = [0u8; 1];
371        loop {
372            match stream.read(&mut byte) {
373                Ok(1) => {
374                    if byte[0] == b'\n' {
375                        break;
376                    }
377                    line.push(byte[0]);
378                    if line.len() > 32 {
379                        return Err(VzError::new("vsock handshake: oversized response"));
380                    }
381                }
382                Ok(_) => return Err(VzError::new("vsock connect refused (EOF)")),
383                Err(e) => return Err(VzError::new(format!("vsock handshake read: {}", e))),
384            }
385        }
386        if !line.starts_with(b"OK ") {
387            return Err(VzError::new(format!(
388                "vsock connect refused: {}",
389                String::from_utf8_lossy(&line)
390            )));
391        }
392
393        let _ = stream.set_read_timeout(None);
394        Ok(unsafe { TcpStream::from_raw_fd(stream.into_raw_fd()) })
395    }
396
397    pub fn state(&self) -> VmState {
398        if self.running.load(Ordering::Acquire) {
399            VmState::Running
400        } else {
401            VmState::Stopped
402        }
403    }
404}
405
406impl Drop for VirtualMachine {
407    fn drop(&mut self) {
408        // Child processes outlive a dropped VirtualMachine unless killed.
409        if let Some(pid) = *self.ch_pid.lock().unwrap() {
410            if self.running.load(Ordering::Acquire) {
411                unsafe { libc::kill(pid, libc::SIGKILL) };
412            }
413        }
414        for mut child in self.virtiofsd.lock().unwrap().drain(..) {
415            let _ = child.kill();
416            let _ = child.wait();
417        }
418        let _ = std::fs::remove_dir_all(&self.run_dir);
419    }
420}