1use std::{
2 ffi::OsString,
3 fs,
4 path::{Path, PathBuf},
5 process::Stdio,
6};
7
8use futures::stream::BoxStream;
9use tokio::process::Command;
10
11use crate::sandbox::{
12 InputsProvider, SandboxOutputs, SandboxSpec,
13 event::{SandboxEvent, stream_process},
14};
15
16const COMMON_BWRAP_ARGS: &[&str] = &[
17 "--unshare-uts",
18 "--unshare-ipc",
19 "--unshare-pid",
20 "--die-with-parent",
21 "--as-pid-1",
22 "--unshare-user",
23 "--uid",
24 "1000",
25 "--gid",
26 "100",
27 "--clearenv",
28 "--tmpfs",
29 "/",
30 "--dev",
31 "/dev",
32 "--proc",
33 "/proc",
34 "--tmpfs",
35 "/tmp",
36];
37
38const ETC_PASSWD: &[u8] = b"
39root:x:0:0:Nix build user:/build:/noshell
40nixbld:x:1000:100:Nix build user:/build:/noshell
41nobody:x:65534:65534:Nobody:/:/noshell
42";
43
44const ETC_GROUP: &[u8] = b"
45root:x:0:
46nixbld:!:100:
47nogroup:x:65534:
48";
49
50const ETC_HOSTS: &[u8] = b"
51127.0.0.1 localhost
52::1 localhost
53";
54
55const ETC_NSSWITCH: &[u8] = b"
56hosts: files dns
57services: files
58";
59
60pub struct Bwrap {
94 host_workdir: PathBuf,
95 args: Vec<OsString>,
96 inputs_provider: InputsProvider,
97}
98
99#[derive(Debug, Clone)]
101pub struct BwrapOutputs {
102 scratch_dir: PathBuf,
103}
104
105impl SandboxOutputs for BwrapOutputs {
106 async fn find_path(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
110 let path = self.scratch_dir.join(path);
111 if let Ok(metadata) = tokio::fs::symlink_metadata(&path).await {
118 metadata.is_symlink() || metadata.is_dir() || metadata.is_file()
119 } else {
120 false
121 }
122 .then_some(path)
123 }
124}
125
126impl Bwrap {
127 pub fn run(self) -> std::io::Result<(BoxStream<'static, SandboxEvent>, BwrapOutputs)> {
129 let outputs = BwrapOutputs {
130 scratch_dir: self.host_workdir.join("scratches"),
131 };
132
133 let guard = self
134 .inputs_provider
135 .provide_inputs(self.host_workdir.join("host_inputs_dir"))?;
136
137 let child = Command::new("bwrap")
138 .args(self.args)
139 .stdin(Stdio::null())
141 .stdout(Stdio::piped())
142 .stderr(Stdio::piped())
143 .kill_on_drop(true)
144 .spawn()?;
145
146 let stream = stream_process(child, guard)?;
147
148 Ok((stream, outputs))
149 }
150
151 pub fn initialize(spec: SandboxSpec) -> std::io::Result<Bwrap> {
153 let scratch_dir = spec.host_workdir().join("scratches");
154 fs::create_dir_all(&scratch_dir)?;
155 let mut args: Vec<OsString> = COMMON_BWRAP_ARGS.iter().map(|s| s.into()).collect();
156 if !spec.allow_network() {
157 args.push("--unshare-net".into());
158 }
159 for env in spec.env_vars() {
160 args.extend([
161 "--setenv".into(),
162 env.key.clone().into(),
163 str::from_utf8(&env.value)
164 .expect("invalid string in env")
165 .into(),
166 ]);
167 }
168
169 let host_inputs_dir = spec.host_workdir().join("host_inputs_dir");
170 fs::create_dir_all(&host_inputs_dir)?;
171 args.extend([
172 "--ro-bind".into(),
173 Path::new("/").join(&host_inputs_dir).into(),
174 Path::new("/")
175 .join(spec.inputs_provider().inputs_dir())
176 .into(),
177 ]);
178 for scratch in spec.scratches() {
179 let scratch_path = scratch_dir.join(scratch);
180 fs::create_dir_all(&scratch_path)?;
181 if scratch == spec.inputs_provider().inputs_dir() {
182 let overlay_workdir = spec.host_workdir().join("overlay_workdir");
183 fs::create_dir_all(&overlay_workdir)?;
184 args.extend([
185 "--overlay-src".into(),
186 OsString::from(&host_inputs_dir),
187 "--overlay".into(),
188 scratch_path.into(),
189 overlay_workdir.into(),
190 Path::new("/")
191 .join(spec.inputs_provider().inputs_dir())
192 .into(),
193 ]);
194 } else {
195 args.extend([
196 "--bind".into(),
197 scratch_path.into(),
198 Path::new("/").join(scratch).into(),
199 ]);
200 }
201 }
202 args.extend([
203 "--chdir".into(),
204 Path::new("/").join(spec.sandbox_workdir()).into(),
205 ]);
206
207 if let Some(shell) = spec.provide_shell() {
208 args.extend_from_slice(&["--ro-bind".into(), shell.into(), "/bin/sh".into()]);
209 }
210
211 for file in spec.additional_files() {
212 let mut found = false;
213 for scratch in spec.scratches() {
214 if file.path.starts_with(scratch) {
215 found = true;
216 }
217 }
218 if !found {
219 return Err(std::io::Error::other(format!(
220 "Additional file does not belong to any scratch: {:?}",
221 file.path
222 )));
223 }
224 let file_path = scratch_dir.join(&file.path);
227 fs::create_dir_all(file_path.parent().expect("parent"))?;
228 fs::write(&file_path, &file.contents)?;
229 }
230 let etc = &spec.host_workdir().join("etc");
231 fs::create_dir_all(etc)?;
232 fs::write(etc.join("passwd"), ETC_PASSWD)?;
233 fs::write(etc.join("group"), ETC_GROUP)?;
234 fs::write(etc.join("hosts"), ETC_HOSTS)?;
235 fs::write(etc.join("nsswitch.conf"), ETC_NSSWITCH)?;
236
237 args.extend([
238 "--ro-bind".into(),
239 etc.join("passwd").into(),
240 "/etc/passwd".into(),
241 "--ro-bind".into(),
242 etc.join("group").into(),
243 "/etc/group".into(),
244 ]);
245 if spec.allow_network() {
246 args.extend([
247 "--ro-bind".into(),
248 "/etc/hosts".into(),
249 "/etc/hosts".into(),
250 "--ro-bind".into(),
251 "/etc/resolv.conf".into(),
252 "/etc/resolv.conf".into(),
253 "--ro-bind".into(),
254 "/etc/services".into(),
255 "/etc/services".into(),
256 "--ro-bind".into(),
257 etc.join("nsswitch.conf").into(),
258 "/etc/nsswitch.conf".into(),
259 ]);
260 } else {
262 args.extend([
265 "--ro-bind".into(),
266 etc.join("hosts").into(),
267 "/etc/hosts".into(),
268 ]);
269 }
270 args.extend(spec.command().into_iter().map(|s| s.into()));
271
272 Ok(Self {
273 host_workdir: spec.host_workdir().into(),
274 args,
275 inputs_provider: spec.into(),
276 })
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use tempfile::TempDir;
284
285 #[tokio::test]
286 async fn test_find_path() {
287 let temp_dir = TempDir::new().expect("failed to create temp directory");
288 let scratch_dir = temp_dir.path();
289
290 let file_path = scratch_dir.join("file.txt");
291 std::fs::write(&file_path, b"hello").expect("failed to write test file");
292
293 let symlink_path = scratch_dir.join("link");
294 std::os::unix::fs::symlink("nonexistent", &symlink_path)
295 .expect("failed to create test symlink");
296
297 let outputs = BwrapOutputs {
298 scratch_dir: scratch_dir.to_path_buf(),
299 };
300
301 assert_eq!(outputs.find_path("file.txt").await, Some(file_path));
302 assert_eq!(outputs.find_path("link").await, Some(symlink_path));
303 assert_eq!(outputs.find_path("missing").await, None);
304 }
305}