consortium_cli/sandbox/
vfkit.rs1use std::path::{Path, PathBuf};
16use std::process::Command;
17
18use super::{NetworkPolicy, Sandbox, SandboxCommand, SandboxError, SandboxOutput, SandboxSpec};
19
20#[derive(Debug, Clone)]
22pub struct VfkitSandbox {
23 binary: PathBuf,
24 kernel: PathBuf,
25 initrd: PathBuf,
26 cmdline: String,
27 memory_mib: u32,
28 cpus: u32,
29}
30
31impl VfkitSandbox {
32 pub fn new(
34 binary: impl Into<PathBuf>,
35 kernel: impl Into<PathBuf>,
36 initrd: impl Into<PathBuf>,
37 cmdline: impl Into<String>,
38 ) -> Self {
39 Self {
40 binary: binary.into(),
41 kernel: kernel.into(),
42 initrd: initrd.into(),
43 cmdline: cmdline.into(),
44 memory_mib: 512,
45 cpus: 1,
46 }
47 }
48
49 #[must_use]
51 pub fn with_memory_mib(mut self, mib: u32) -> Self {
52 self.memory_mib = mib;
53 self
54 }
55
56 #[must_use]
58 pub fn with_cpus(mut self, cpus: u32) -> Self {
59 self.cpus = cpus;
60 self
61 }
62
63 fn device_arg(host_path: &Path, mount_tag: &str) -> String {
65 format!(
66 "virtio-fs,sharedDir={},mountTag={mount_tag}",
67 host_path.display()
68 )
69 }
70
71 fn build_argv(
73 &self,
74 spec: &SandboxSpec,
75 command: &SandboxCommand,
76 ) -> Result<Vec<String>, SandboxError> {
77 if let Some(path) = spec.read_only_paths.first() {
78 return Err(SandboxError::UnsupportedReadOnlyShare { path: path.clone() });
79 }
80
81 let payload = std::iter::once(command.program.as_str())
84 .chain(command.args.iter().map(String::as_str))
85 .collect::<Vec<_>>()
86 .join(" ");
87 let cmdline = format!("{} -- {payload}", self.cmdline);
88
89 let mut argv = vec![
90 "--bootloader".to_string(),
91 format!(
92 "linux,kernel={},initrd={},cmdline=\"{}\"",
93 self.kernel.display(),
94 self.initrd.display(),
95 cmdline
96 ),
97 "--memory".to_string(),
98 self.memory_mib.to_string(),
99 "--cpus".to_string(),
100 self.cpus.to_string(),
101 ];
102
103 for (index, path) in spec.writable_paths.iter().enumerate() {
104 argv.push("--device".to_string());
105 argv.push(Self::device_arg(path, &format!("share{index}")));
106 }
107
108 if spec.network == NetworkPolicy::Deny && !spec.writable_paths.is_empty() {
109 return Err(SandboxError::Unsupported("a network-denying policy"));
110 }
111 argv.push("--device".to_string());
115 argv.push("virtio-net,nat".to_string());
116
117 Ok(argv)
118 }
119}
120
121impl Sandbox for VfkitSandbox {
122 fn name(&self) -> &'static str {
123 "vfkit"
124 }
125
126 fn is_isolated(&self) -> bool {
127 true
130 }
131
132 fn exec(
133 &self,
134 spec: &SandboxSpec,
135 command: &SandboxCommand,
136 ) -> Result<SandboxOutput, SandboxError> {
137 spec.validate()?;
138 let argv = self.build_argv(spec, command)?;
139 let output = Command::new(&self.binary)
140 .args(&argv)
141 .stdin(std::process::Stdio::null())
142 .output()
143 .map_err(SandboxError::Spawn)?;
144 Ok(SandboxOutput {
145 status: output.status.code().unwrap_or(-1),
146 stdout: output.stdout,
147 stderr: output.stderr,
148 })
149 }
150}
151
152pub fn which() -> Option<PathBuf> {
154 let path = std::env::var_os("PATH")?;
155 std::env::split_paths(&path)
156 .map(|dir| dir.join("vfkit"))
157 .find(|candidate| candidate.is_file())
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn backend() -> VfkitSandbox {
165 VfkitSandbox::new("vfkit", "bzImage", "initrd", "console=ttyS0")
166 }
167
168 #[test]
169 fn device_string_matches_vfkit_grammar() {
170 assert_eq!(
171 VfkitSandbox::device_arg(Path::new("/srv/data"), "share0"),
172 "virtio-fs,sharedDir=/srv/data,mountTag=share0"
173 );
174 }
175
176 #[test]
177 fn reports_real_isolation() {
178 assert!(backend().is_isolated());
179 assert_eq!(backend().name(), "vfkit");
180 }
181
182 #[test]
183 fn read_only_grant_is_refused_not_widened() {
184 let spec = SandboxSpec::deny_all().with_read_only("/usr");
185 let err = backend()
186 .build_argv(&spec, &SandboxCommand::new("/bin/true"))
187 .expect_err("must refuse a read-only grant");
188 assert!(matches!(err, SandboxError::UnsupportedReadOnlyShare { .. }));
189 assert!(err.to_string().contains("read-only"));
190 }
191
192 #[test]
193 fn writable_share_becomes_a_virtio_fs_device() {
194 let spec = SandboxSpec::deny_all()
195 .with_writable("/srv/data")
196 .with_network(NetworkPolicy::Allow);
197 let argv = backend()
198 .build_argv(&spec, &SandboxCommand::new("/bin/true"))
199 .expect("should build");
200 assert!(argv.iter().any(|a| a == "--device"));
201 assert!(argv
202 .iter()
203 .any(|a| a == "virtio-fs,sharedDir=/srv/data,mountTag=share0"));
204 }
205
206 #[test]
207 fn command_payload_lands_in_the_bootloader_cmdline() {
208 let spec = SandboxSpec::deny_all().with_network(NetworkPolicy::Allow);
213 let cmd = SandboxCommand::new("/bin/echo").arg("hello");
214 let argv = backend().build_argv(&spec, &cmd).expect("should build");
215 let bootloader = argv.get(1).expect("bootloader arg");
216 assert!(bootloader.starts_with("linux,"), "{bootloader}");
217 assert!(
218 bootloader.contains("cmdline=\"console=ttyS0 -- /bin/echo hello\""),
219 "{bootloader}"
220 );
221 for arg in &argv {
223 if arg.starts_with("--") {
224 assert!(
225 matches!(
226 arg.as_str(),
227 "--bootloader" | "--memory" | "--cpus" | "--device"
228 ),
229 "unexpected flag {arg}"
230 );
231 }
232 }
233 }
234
235 #[test]
236 fn network_denial_is_refused_rather_than_approximated() {
237 let spec = SandboxSpec::deny_all().with_writable("/srv/data");
238 let err = backend()
239 .build_argv(&spec, &SandboxCommand::new("/bin/true"))
240 .expect_err("must refuse");
241 assert!(matches!(err, SandboxError::Unsupported(_)));
242 }
243}