1use crate::buildservice::{BuildConstraints, BuildRequest};
3use oci_spec::runtime::{
4 Capability, LinuxIdMappingBuilder, LinuxNamespace, LinuxNamespaceBuilder, LinuxNamespaceType,
5};
6use std::{collections::HashSet, path::Path};
7
8use super::{
9 scratch_name,
10 subuid::{SubordinateError, SubordinateInfo},
11};
12use thiserror::Error;
13
14#[derive(Debug, Error)]
15pub enum SpecError {
16 #[error("oci error: {0}")]
17 OciError(oci_spec::OciSpecError),
18 #[error("subordinate error: {0}")]
19 SubordinateError(SubordinateError),
20}
21
22pub(crate) fn make_spec(
44 request: &BuildRequest,
45 rootless: bool,
46 sandbox_shell: &str,
47) -> Result<oci_spec::runtime::Spec, SpecError> {
48 let allow_network = request
49 .constraints
50 .contains(&BuildConstraints::NetworkAccess);
51
52 let mut ro_host_mounts: Vec<_> = request
54 .constraints
55 .iter()
56 .filter_map(|constraint| match constraint {
57 BuildConstraints::AvailableReadOnlyPath(path) => {
58 Some((Path::new(path), Path::new(path)))
59 }
60 _ => None,
61 })
62 .collect();
63
64 if request
66 .constraints
67 .contains(&BuildConstraints::ProvideBinSh)
68 {
69 ro_host_mounts.push((Path::new(sandbox_shell), Path::new("/bin/sh")))
70 }
71
72 oci_spec::runtime::SpecBuilder::default()
73 .process(
74 configure_process(
75 &request.command_args,
76 Path::new(&request.working_dir),
77 request
78 .environment_vars
79 .iter()
80 .map(|e| {
81 (
82 e.key.as_str(),
83 String::from_utf8(e.value.to_vec()).expect("invalid string in env"),
85 )
86 })
87 .collect::<Vec<_>>(),
88 rootless,
89 )
90 .map_err(SpecError::OciError)?,
91 )
92 .linux(configure_linux(allow_network, rootless)?)
93 .root(
94 oci_spec::runtime::RootBuilder::default()
95 .path("root")
96 .readonly(true)
97 .build()
98 .map_err(SpecError::OciError)?,
99 )
100 .hostname("localhost")
101 .mounts(
102 configure_mounts(
103 rootless,
104 allow_network,
105 &request.scratch_paths,
106 request.inputs.iter(),
107 Path::new(&request.inputs_dir),
108 ro_host_mounts,
109 )
110 .map_err(SpecError::OciError)?,
111 )
112 .build()
113 .map_err(SpecError::OciError)
114}
115
116fn configure_process<'a>(
121 command_args: &[String],
122 cwd: &Path,
123 env: impl IntoIterator<Item = (&'a str, String)>,
124 rootless: bool,
125) -> Result<oci_spec::runtime::Process, oci_spec::OciSpecError> {
126 let spec_builder = oci_spec::runtime::ProcessBuilder::default()
127 .args(command_args)
128 .env(
129 env.into_iter()
130 .map(|(k, v)| format!("{k}={v}"))
131 .collect::<Vec<_>>(),
132 )
133 .terminal(true)
134 .user(
135 oci_spec::runtime::UserBuilder::default()
136 .uid(1000u32)
137 .gid(100u32)
138 .build()?,
139 )
140 .cwd(Path::new("/").join(cwd)) .capabilities({
142 let caps: HashSet<Capability> = if !rootless {
143 HashSet::from([Capability::AuditWrite, Capability::Kill])
144 } else {
145 HashSet::from([
146 Capability::AuditWrite,
147 Capability::Chown,
148 Capability::DacOverride,
149 Capability::Fowner,
150 Capability::Fsetid,
151 Capability::Kill,
152 Capability::Mknod,
153 Capability::NetBindService,
154 Capability::NetRaw,
155 Capability::Setfcap,
156 Capability::Setgid,
157 Capability::Setpcap,
158 Capability::Setuid,
159 Capability::SysChroot,
160 ])
161 };
162
163 oci_spec::runtime::LinuxCapabilitiesBuilder::default()
164 .bounding(caps.clone())
165 .effective(caps.clone())
166 .inheritable(caps.clone())
167 .permitted(caps.clone())
168 .ambient(caps)
169 .build()?
170 })
171 .rlimits([oci_spec::runtime::PosixRlimitBuilder::default()
172 .typ(oci_spec::runtime::PosixRlimitType::RlimitNofile)
173 .hard(1024_u64)
174 .soft(1024_u64)
175 .build()?])
176 .no_new_privileges(true);
177
178 spec_builder.build()
179}
180
181fn configure_linux(
184 allow_network: bool,
185 rootless: bool,
186) -> Result<oci_spec::runtime::Linux, SpecError> {
187 let mut linux = oci_spec::runtime::Linux::default();
188
189 linux.set_namespaces(Some({
191 let mut namespace_types = vec![
192 LinuxNamespaceType::Pid,
193 LinuxNamespaceType::Ipc,
194 LinuxNamespaceType::Uts,
195 LinuxNamespaceType::Mount,
196 ];
201 if !allow_network {
202 namespace_types.push(LinuxNamespaceType::Network)
203 }
204 if rootless {
205 namespace_types.push(LinuxNamespaceType::User)
206 }
207
208 namespace_types
209 .into_iter()
210 .map(|e| LinuxNamespaceBuilder::default().typ(e).build())
211 .collect::<Result<Vec<LinuxNamespace>, _>>()
212 .map_err(SpecError::OciError)?
213 }));
214
215 linux.set_masked_paths(Some(
216 [
217 "/proc/kcore",
218 "/proc/latency_stats",
219 "/proc/timer_list",
220 "/proc/timer_stats",
221 "/proc/sched_debug",
222 "/sys/firmware",
223 ]
224 .into_iter()
225 .map(|e| e.to_string())
226 .collect::<Vec<_>>(),
227 ));
228
229 linux.set_readonly_paths(Some(
230 [
231 "/proc/asound",
232 "/proc/bus",
233 "/proc/fs",
234 "/proc/irq",
235 "/proc/sys",
236 "/proc/sysrq-trigger",
237 ]
238 .into_iter()
239 .map(|e| e.to_string())
240 .collect::<Vec<_>>(),
241 ));
242 let info = SubordinateInfo::for_effective_user().map_err(SpecError::SubordinateError)?;
243 linux.set_uid_mappings(Some(vec![
244 LinuxIdMappingBuilder::default()
245 .host_id(info.uid)
246 .container_id(0_u32)
247 .size(1_u32)
248 .build()
249 .unwrap(),
250 LinuxIdMappingBuilder::default()
251 .host_id(info.subuid)
252 .container_id(1000_u32)
253 .size(1_u32)
254 .build()
255 .unwrap(),
256 ]));
257 linux.set_gid_mappings(Some(vec![
258 LinuxIdMappingBuilder::default()
259 .host_id(info.gid)
260 .container_id(0_u32)
261 .size(1_u32)
262 .build()
263 .unwrap(),
264 LinuxIdMappingBuilder::default()
265 .host_id(info.subgid)
266 .container_id(100_u32)
267 .size(1_u32)
268 .build()
269 .unwrap(),
270 ]));
271
272 Ok(linux)
273}
274
275fn configure_mounts<'a, S>(
279 rootless: bool,
280 allow_network: bool,
281 scratch_paths: impl IntoIterator<Item = S>,
282 inputs: impl Iterator<Item = (&'a snix_castore::PathComponent, &'a snix_castore::Node)>,
283
284 inputs_dir: &Path,
285 ro_host_mounts: impl IntoIterator<Item = (&'a Path, &'a Path)>,
286) -> Result<Vec<oci_spec::runtime::Mount>, oci_spec::OciSpecError>
287where
288 S: AsRef<str> + 'a,
289{
290 let mut mounts: Vec<_> = if rootless {
291 oci_spec::runtime::get_rootless_mounts()
292 } else {
293 oci_spec::runtime::get_default_mounts()
294 };
295
296 mounts.push(configure_mount(
297 Path::new("tmpfs"),
298 Path::new("/tmp"),
299 "tmpfs",
300 &["nosuid", "noatime", "mode=700"],
301 )?);
302
303 let scratch_root = Path::new("scratch"); for scratch_path in scratch_paths {
306 let scratch_path = scratch_path.as_ref();
307 let src = scratch_root.join(scratch_name(scratch_path));
308 mounts.push(configure_mount(
309 &src,
310 &Path::new("/").join(scratch_path),
311 "none",
312 &["rbind", "rw"],
313 )?);
314 }
315
316 for (input_name, _input) in inputs {
318 let input_name = std::str::from_utf8(input_name.as_ref()).expect("invalid input name");
319 mounts.push(configure_mount(
320 &Path::new("inputs").join(input_name),
321 &Path::new("/").join(inputs_dir).join(input_name),
322 "none",
323 &[
324 "rbind", "ro",
325 "nosuid", "nodev",
329 ],
330 )?);
331 }
332
333 for (src, dst) in ro_host_mounts.into_iter() {
335 mounts.push(configure_mount(src, dst, "none", &["rbind", "ro"])?);
336 }
337
338 if allow_network {
340 for p in [
341 Path::new("/etc/resolv.conf"),
342 Path::new("/etc/services"),
343 Path::new("/etc/hosts"),
344 ] {
345 mounts.push(configure_mount(p, p, "none", &["rbind", "ro"])?);
346 }
347 }
348
349 Ok(mounts)
350}
351
352fn configure_mount(
354 source: &Path,
355 destination: &Path,
356 typ: &str,
357 options: &[&str],
358) -> Result<oci_spec::runtime::Mount, oci_spec::OciSpecError> {
359 oci_spec::runtime::MountBuilder::default()
360 .destination(destination)
361 .typ(typ.to_string())
362 .source(source)
363 .options(options.iter().map(|e| e.to_string()).collect::<Vec<_>>())
364 .build()
365}