1use std::{
2 ffi::OsString,
3 fs,
4 path::{Path, PathBuf},
5 process::{Output, Stdio},
6};
7
8use tokio::process::Command;
9
10use crate::sandbox::{InputsProvider, SandboxSpec};
11
12const COMMON_BWRAP_ARGS: &[&str] = &[
13 "--unshare-uts",
14 "--unshare-ipc",
15 "--unshare-pid",
16 "--die-with-parent",
17 "--as-pid-1",
18 "--unshare-user",
19 "--uid",
20 "1000",
21 "--gid",
22 "100",
23 "--clearenv",
24 "--tmpfs",
25 "/",
26 "--dev",
27 "/dev",
28 "--proc",
29 "/proc",
30 "--tmpfs",
31 "/tmp",
32];
33
34const ETC_PASSWD: &[u8] = b"
35root:x:0:0:Nix build user:/build:/noshell
36nixbld:x:1000:100:Nix build user:/build:/noshell
37nobody:x:65534:65534:Nobody:/:/noshell
38";
39
40const ETC_GROUP: &[u8] = b"
41root:x:0:
42nixbld:!:100:
43nogroup:x:65534:
44";
45
46const ETC_HOSTS: &[u8] = b"
47127.0.0.1 localhost
48::1 localhost
49";
50
51const ETC_NSSWITCH: &[u8] = b"
52hosts: files dns
53services: files
54";
55
56pub struct Bwrap {
90 host_workdir: PathBuf,
91 args: Vec<OsString>,
92 inputs_provider: InputsProvider,
93}
94
95pub struct SandboxOutcome {
97 output: Output,
98 scratch_dir: PathBuf,
99}
100
101impl SandboxOutcome {
102 pub fn output(&self) -> &Output {
104 &self.output
105 }
106
107 pub fn find_path(&self, path: impl AsRef<Path>) -> Option<PathBuf> {
111 let path = self.scratch_dir.join(path);
112 if path.is_symlink() || path.exists() {
119 Some(path)
120 } else {
121 None
122 }
123 }
124}
125
126impl Bwrap {
127 pub async fn run(mut self) -> std::io::Result<SandboxOutcome> {
130 let _guard = self
131 .inputs_provider
132 .provide_inputs(self.host_workdir.join("host_inputs_dir"))?;
133
134 Ok(SandboxOutcome {
135 output: Command::new("bwrap")
136 .args(self.args)
137 .stdin(Stdio::null())
139 .output()
140 .await?,
141 scratch_dir: self.host_workdir.join("scratches"),
142 })
143 }
144
145 pub fn initialize(spec: SandboxSpec) -> std::io::Result<Bwrap> {
147 let scratch_dir = spec.host_workdir().join("scratches");
148 fs::create_dir_all(&scratch_dir)?;
149 let mut args: Vec<OsString> = COMMON_BWRAP_ARGS.iter().map(|s| s.into()).collect();
150 if !spec.allow_network() {
151 args.push("--unshare-net".into());
152 }
153 for env in spec.env_vars() {
154 args.extend([
155 "--setenv".into(),
156 env.key.clone().into(),
157 str::from_utf8(&env.value)
158 .expect("invalid string in env")
159 .into(),
160 ]);
161 }
162
163 let host_inputs_dir = spec.host_workdir().join("host_inputs_dir");
164 fs::create_dir_all(&host_inputs_dir)?;
165 args.extend([
166 "--ro-bind".into(),
167 Path::new("/").join(&host_inputs_dir).into(),
168 Path::new("/")
169 .join(spec.inputs_provider().inputs_dir())
170 .into(),
171 ]);
172 for scratch in spec.scratches() {
173 let scratch_path = scratch_dir.join(scratch);
174 fs::create_dir_all(&scratch_path)?;
175 if scratch == spec.inputs_provider().inputs_dir() {
176 let overlay_workdir = spec.host_workdir().join("overlay_workdir");
177 fs::create_dir_all(&overlay_workdir)?;
178 args.extend([
179 "--overlay-src".into(),
180 OsString::from(&host_inputs_dir),
181 "--overlay".into(),
182 scratch_path.into(),
183 overlay_workdir.into(),
184 Path::new("/")
185 .join(spec.inputs_provider().inputs_dir())
186 .into(),
187 ]);
188 } else {
189 args.extend([
190 "--bind".into(),
191 scratch_path.into(),
192 Path::new("/").join(scratch).into(),
193 ]);
194 }
195 }
196 args.extend([
197 "--chdir".into(),
198 Path::new("/").join(spec.sandbox_workdir()).into(),
199 ]);
200
201 if let Some(shell) = spec.provide_shell() {
202 args.extend_from_slice(&["--ro-bind".into(), shell.into(), "/bin/sh".into()]);
203 }
204
205 for file in spec.additional_files() {
206 let mut found = false;
207 for scratch in spec.scratches() {
208 if file.path.starts_with(scratch) {
209 found = true;
210 }
211 }
212 if !found {
213 return Err(std::io::Error::other(format!(
214 "Additional file does not belong to any scratch: {:?}",
215 file.path
216 )));
217 }
218 let file_path = scratch_dir.join(&file.path);
221 fs::create_dir_all(file_path.parent().expect("parent"))?;
222 fs::write(&file_path, &file.contents)?;
223 }
224 let etc = &spec.host_workdir().join("etc");
225 fs::create_dir_all(etc)?;
226 fs::write(etc.join("passwd"), ETC_PASSWD)?;
227 fs::write(etc.join("group"), ETC_GROUP)?;
228 fs::write(etc.join("hosts"), ETC_HOSTS)?;
229 fs::write(etc.join("nsswitch.conf"), ETC_NSSWITCH)?;
230
231 args.extend([
232 "--ro-bind".into(),
233 etc.join("passwd").into(),
234 "/etc/passwd".into(),
235 "--ro-bind".into(),
236 etc.join("group").into(),
237 "/etc/group".into(),
238 ]);
239 if spec.allow_network() {
240 args.extend([
241 "--ro-bind".into(),
242 "/etc/hosts".into(),
243 "/etc/hosts".into(),
244 "--ro-bind".into(),
245 "/etc/resolv.conf".into(),
246 "/etc/resolv.conf".into(),
247 "--ro-bind".into(),
248 "/etc/services".into(),
249 "/etc/services".into(),
250 "--ro-bind".into(),
251 etc.join("nsswitch.conf").into(),
252 "/etc/nsswitch.conf".into(),
253 ]);
254 } else {
256 args.extend([
259 "--ro-bind".into(),
260 etc.join("hosts").into(),
261 "/etc/hosts".into(),
262 ]);
263 }
264 args.extend(spec.command().into_iter().map(|s| s.into()));
265
266 Ok(Self {
267 host_workdir: spec.host_workdir().into(),
268 args,
269 inputs_provider: spec.into(),
270 })
271 }
272}