1use std::path::{Component, Path, PathBuf};
29
30use murmur_artifact::ContainmentClass;
31use serde::Serialize;
32
33use crate::host_paths::{HostPath, HostPathAccess};
34use crate::types::CapabilityPolicy;
35
36pub const HOST_ROOT_GUEST_PATH: &str = "murmur-host";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "snake_case")]
43pub enum GuestRootSource {
44 Unconfined,
46 HostPaths,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct GuestRoot {
53 pub guest_path: String,
56 pub host_path: PathBuf,
58 pub access: HostPathAccess,
61 pub source: GuestRootSource,
62}
63
64#[must_use]
67pub fn extra_guest_roots(policy: &CapabilityPolicy) -> Vec<GuestRoot> {
68 extra_guest_roots_for(policy.containment_floor, &policy.host_paths)
69}
70
71#[must_use]
78pub fn extra_guest_roots_for(
79 containment_floor: ContainmentClass,
80 host_paths: &[HostPath],
81) -> Vec<GuestRoot> {
82 if containment_floor == ContainmentClass::Unconfined {
83 let root = Path::new("/");
84 return vec![GuestRoot {
85 guest_path: guest_path_for(root),
86 host_path: root.to_path_buf(),
87 access: HostPathAccess::ReadWrite,
88 source: GuestRootSource::Unconfined,
89 }];
90 }
91 host_paths
92 .iter()
93 .map(|host_path| GuestRoot {
94 guest_path: guest_path_for(&host_path.path),
95 host_path: host_path.path.clone(),
96 access: host_path.access,
97 source: GuestRootSource::HostPaths,
98 })
99 .collect()
100}
101
102#[must_use]
106pub(crate) fn tool_extra_roots(
107 runtime: &murmur_artifact::ArtifactRuntime,
108 declares_filesystem_scope: bool,
109 policy: &CapabilityPolicy,
110) -> Vec<GuestRoot> {
111 if crate::runtime_dir::gets_runtime_dir_preopen(runtime, declares_filesystem_scope) {
112 extra_guest_roots(policy)
113 } else {
114 Vec::new()
115 }
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120pub(crate) struct PlannedPreopen {
121 pub(crate) host_path: PathBuf,
122 pub(crate) guest_path: String,
124 pub(crate) read_only: bool,
125}
126
127pub(crate) fn planned_preopens(
134 preopen_root: &Path,
135 state_dir: Option<&Path>,
136 runtime_dir: Option<&Path>,
137 extra_roots: &[GuestRoot],
138) -> Vec<PlannedPreopen> {
139 let mut planned = vec![PlannedPreopen {
140 host_path: preopen_root.to_path_buf(),
141 guest_path: ".".to_string(),
142 read_only: false,
143 }];
144 if let Some(state_dir) = state_dir {
145 planned.push(PlannedPreopen {
146 host_path: state_dir.to_path_buf(),
147 guest_path: crate::state_store::STATE_PREOPEN_NAME.to_string(),
148 read_only: false,
149 });
150 }
151 if let Some(runtime_dir) = runtime_dir {
154 planned.push(PlannedPreopen {
155 host_path: runtime_dir.to_path_buf(),
156 guest_path: crate::runtime_dir::RUNTIME_DIR_GUEST_PATH.to_string(),
157 read_only: true,
158 });
159 }
160 planned.extend(extra_roots.iter().map(|root| PlannedPreopen {
161 host_path: root.host_path.clone(),
162 guest_path: root.guest_path.clone(),
163 read_only: root.access == HostPathAccess::Read,
164 }));
165 planned
166}
167
168#[must_use]
172pub fn guest_path_for(host_path: &Path) -> String {
173 let mut guest = HOST_ROOT_GUEST_PATH.to_string();
174 for component in host_path.components() {
175 if let Component::Normal(name) = component {
176 guest.push('/');
177 guest.push_str(&name.to_string_lossy());
178 }
179 }
180 guest
181}
182
183#[must_use]
190pub fn map_guest_path(roots: &[GuestRoot], guest: &str) -> Option<PathBuf> {
191 let components = fold_guest_path(guest)?;
192 if components.first().map(String::as_str) != Some(HOST_ROOT_GUEST_PATH) {
193 return None;
194 }
195 let (root, depth) = roots
196 .iter()
197 .filter_map(|root| {
198 let prefix = fold_guest_path(&root.guest_path)?;
199 components
200 .starts_with(&prefix)
201 .then_some((root, prefix.len()))
202 })
203 .max_by_key(|(_, depth)| *depth)?;
204 let mut host = root.host_path.clone();
205 for name in &components[depth..] {
206 host.push(name);
207 }
208 Some(host)
209}
210
211fn fold_guest_path(guest: &str) -> Option<Vec<String>> {
214 let mut names: Vec<String> = Vec::new();
215 for component in Path::new(guest).components() {
216 match component {
217 Component::RootDir | Component::CurDir | Component::Prefix(_) => {}
218 Component::ParentDir => {
219 names.pop()?;
220 }
221 Component::Normal(name) => names.push(name.to_string_lossy().into_owned()),
222 }
223 }
224 Some(names)
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 fn host_path(path: &str, access: HostPathAccess) -> HostPath {
232 HostPath {
233 declared: path.to_string(),
234 path: PathBuf::from(path),
235 access,
236 credential_locations: Vec::new(),
237 }
238 }
239
240 fn policy(floor: ContainmentClass, host_paths: Vec<HostPath>) -> CapabilityPolicy {
241 CapabilityPolicy {
242 containment_floor: floor,
243 host_paths,
244 ..CapabilityPolicy::default()
245 }
246 }
247
248 fn summary(roots: &[GuestRoot]) -> Vec<(String, String, &'static str, String)> {
249 roots
250 .iter()
251 .map(|root| {
252 (
253 root.guest_path.clone(),
254 root.host_path.display().to_string(),
255 root.access.wire_name(),
256 serde_json::to_value(root.source)
257 .unwrap()
258 .as_str()
259 .unwrap()
260 .to_string(),
261 )
262 })
263 .collect()
264 }
265
266 fn tool_plan(
268 runtime: &murmur_artifact::ArtifactRuntime,
269 scoped: bool,
270 state: bool,
271 policy: &CapabilityPolicy,
272 ) -> Vec<(String, bool)> {
273 let roots = tool_extra_roots(runtime, scoped, policy);
274 let runtime_dir = crate::runtime_dir::gets_runtime_dir_preopen(runtime, scoped)
275 .then_some(Path::new("/runtime"));
276 planned_preopens(
277 Path::new("/work"),
278 state.then_some(Path::new("/state")),
279 runtime_dir,
280 &roots,
281 )
282 .into_iter()
283 .map(|preopen| (preopen.guest_path, preopen.read_only))
284 .collect()
285 }
286
287 fn owned(list: &[(&str, bool)]) -> Vec<(String, bool)> {
288 list.iter()
289 .map(|(guest, read_only)| (guest.to_string(), *read_only))
290 .collect()
291 }
292
293 #[test]
294 fn guest_roots_contained_plan_is_todays_preopens() {
295 use murmur_artifact::ArtifactRuntime;
296 let contained = policy(ContainmentClass::Scoped, Vec::new());
297 assert_eq!(
298 tool_plan(&ArtifactRuntime::Tool, false, false, &contained),
299 owned(&[(".", false), ("murmur-runtime", true)])
300 );
301 assert_eq!(
302 tool_plan(&ArtifactRuntime::Tool, false, true, &contained),
303 owned(&[(".", false), ("state", false), ("murmur-runtime", true)])
304 );
305 }
306
307 #[test]
308 fn guest_roots_unconfined_plan_adds_the_host_root_last() {
309 use murmur_artifact::ArtifactRuntime;
310 for host_paths in [Vec::new(), vec![host_path("/a", HostPathAccess::Read)]] {
311 let unconfined = policy(ContainmentClass::Unconfined, host_paths);
312 assert_eq!(
313 tool_plan(&ArtifactRuntime::Tool, false, true, &unconfined),
314 owned(&[
315 (".", false),
316 ("state", false),
317 ("murmur-runtime", true),
318 ("murmur-host", false),
319 ])
320 );
321 }
322 }
323
324 #[test]
325 fn guest_roots_host_paths_plan_follows_the_existing_three() {
326 use murmur_artifact::ArtifactRuntime;
327 let contained = policy(
328 ContainmentClass::Advisory,
329 vec![
330 host_path("/a", HostPathAccess::Read),
331 host_path("/a/b", HostPathAccess::ReadWrite),
332 ],
333 );
334 assert_eq!(
335 tool_plan(&ArtifactRuntime::Tool, false, true, &contained),
336 owned(&[
337 (".", false),
338 ("state", false),
339 ("murmur-runtime", true),
340 ("murmur-host/a", true),
341 ("murmur-host/a/b", false),
342 ])
343 );
344 }
345
346 #[test]
347 fn guest_roots_never_reach_a_scoped_tool_a_driver_or_a_hook() {
348 use murmur_artifact::ArtifactRuntime;
349 let policies = [
350 policy(ContainmentClass::Unconfined, Vec::new()),
351 policy(
352 ContainmentClass::Advisory,
353 vec![host_path("/a", HostPathAccess::ReadWrite)],
354 ),
355 ];
356 for policy in &policies {
357 assert!(tool_extra_roots(&ArtifactRuntime::Tool, true, policy).is_empty());
358 assert_eq!(
359 tool_plan(&ArtifactRuntime::Tool, true, false, policy),
360 owned(&[(".", false)])
361 );
362 for runtime in [
363 ArtifactRuntime::Driver,
364 ArtifactRuntime::Hook,
365 ArtifactRuntime::Skill,
366 ] {
367 for scoped in [false, true] {
368 assert!(tool_extra_roots(&runtime, scoped, policy).is_empty());
369 }
370 }
371 assert_eq!(
373 planned_preopens(Path::new("/work"), None, None, &[]),
374 vec![PlannedPreopen {
375 host_path: PathBuf::from("/work"),
376 guest_path: ".".to_string(),
377 read_only: false,
378 }]
379 );
380 }
381 }
382
383 #[test]
384 fn guest_roots_guest_path_is_murmur_host_and_the_absolute_path() {
385 assert_eq!(guest_path_for(Path::new("/")), "murmur-host");
386 assert_eq!(
387 guest_path_for(Path::new("/home/op/.cargo")),
388 "murmur-host/home/op/.cargo"
389 );
390 assert_eq!(guest_path_for(Path::new("/data/")), "murmur-host/data");
391 }
392
393 #[test]
394 fn guest_roots_contained_without_host_paths_has_none() {
395 for floor in [
396 ContainmentClass::Advisory,
397 ContainmentClass::Scoped,
398 ContainmentClass::Sealed,
399 ] {
400 assert!(extra_guest_roots(&policy(floor, Vec::new())).is_empty());
401 }
402 }
403
404 #[test]
405 fn guest_roots_unconfined_is_the_whole_host_read_write_whatever_host_paths_say() {
406 let expected = vec![(
407 "murmur-host".to_string(),
408 "/".to_string(),
409 "read-write",
410 "unconfined".to_string(),
411 )];
412 let plain = extra_guest_roots(&policy(ContainmentClass::Unconfined, Vec::new()));
413 assert_eq!(summary(&plain), expected);
414 let with_host_paths = extra_guest_roots(&policy(
415 ContainmentClass::Unconfined,
416 vec![host_path("/a", HostPathAccess::Read)],
417 ));
418 assert_eq!(summary(&with_host_paths), expected);
419 }
420
421 #[test]
422 fn guest_roots_host_paths_follow_config_order_with_their_access() {
423 for floor in [
424 ContainmentClass::Advisory,
425 ContainmentClass::Scoped,
426 ContainmentClass::Sealed,
427 ] {
428 let roots = extra_guest_roots(&policy(
429 floor,
430 vec![
431 host_path("/a", HostPathAccess::Read),
432 host_path("/a/b", HostPathAccess::ReadWrite),
433 ],
434 ));
435 assert_eq!(
436 summary(&roots),
437 vec![
438 (
439 "murmur-host/a".to_string(),
440 "/a".to_string(),
441 "read",
442 "host_paths".to_string()
443 ),
444 (
445 "murmur-host/a/b".to_string(),
446 "/a/b".to_string(),
447 "read-write",
448 "host_paths".to_string()
449 ),
450 ]
451 );
452 }
453 }
454
455 #[test]
456 fn guest_roots_map_by_longest_prefix() {
457 let roots = extra_guest_roots(&policy(
458 ContainmentClass::Advisory,
459 vec![
460 host_path("/a/b", HostPathAccess::ReadWrite),
461 host_path("/a", HostPathAccess::Read),
462 ],
463 ));
464 assert_eq!(
465 map_guest_path(&roots, "murmur-host/a/b/c"),
466 Some(PathBuf::from("/a/b/c"))
467 );
468 assert_eq!(
469 map_guest_path(&roots, "murmur-host/a/bc"),
470 Some(PathBuf::from("/a/bc"))
471 );
472 assert_eq!(
473 map_guest_path(&roots, "/murmur-host/a/x"),
474 Some(PathBuf::from("/a/x"))
475 );
476 assert_eq!(
477 map_guest_path(&roots, "./murmur-host/a/./x/../y"),
478 Some(PathBuf::from("/a/y"))
479 );
480 assert_eq!(map_guest_path(&roots, "murmur-host/other/x"), None);
481 assert_eq!(map_guest_path(&roots, "murmur-host"), None);
482 assert_eq!(map_guest_path(&roots, "src/murmur-host/a"), None);
483 assert_eq!(map_guest_path(&roots, "murmur-hostile/a"), None);
484 assert_eq!(map_guest_path(&roots, "../murmur-host/a"), None);
485 assert_eq!(map_guest_path(&[], "murmur-host/a/x"), None);
486 }
487
488 #[test]
489 fn guest_roots_whole_host_maps_every_murmur_host_path() {
490 let roots = extra_guest_roots(&policy(ContainmentClass::Unconfined, Vec::new()));
491 assert_eq!(
492 map_guest_path(&roots, "murmur-host/home/op/notes"),
493 Some(PathBuf::from("/home/op/notes"))
494 );
495 assert_eq!(
496 map_guest_path(&roots, "murmur-host"),
497 Some(PathBuf::from("/"))
498 );
499 assert_eq!(map_guest_path(&roots, "notes"), None);
500 }
501}