summary refs log tree commit diff
path: root/host/start-vmm/lib.rs
blob: 9c2b4b6347e0d80697d18b1ff555799fd7d5a068 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// 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());
    }

    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)
}