1use std::path::{Path, PathBuf};
29
30use crate::sandbox::EnforcementTier;
31
32pub const SESSION_TEMP_DIR_NAME: &str = "tmp";
34
35pub const TMP_PATH: &str = "/tmp";
37
38pub const TEMP_DIR_ENV: [&str; 3] = ["TMPDIR", "TMP", "TEMP"];
41
42pub const TMP_MAP_FAILURE_PREFIX: &str =
44 "sandbox: could not map /tmp onto this session's temporary folder:";
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TempMapping {
49 ComposedRoot,
51 MountNamespace,
53 NotMapped(NotMappedReason),
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum NotMappedReason {
60 NoKernelContainment,
62 NoMountNamespaceNoLandlock,
64 NoMountNamespace,
66 WorkdirIsTmp,
68 Unconfined,
70}
71
72impl TempMapping {
73 #[must_use]
75 pub fn is_mapped(self) -> bool {
76 !matches!(self, Self::NotMapped(_))
77 }
78
79 #[must_use]
81 pub fn mapped_by(self) -> Option<&'static str> {
82 match self {
83 Self::ComposedRoot => Some("composed-root"),
84 Self::MountNamespace => Some("mount-namespace"),
85 Self::NotMapped(_) => None,
86 }
87 }
88
89 #[must_use]
93 pub fn tmpdir_value(self, folder: &Path) -> String {
94 match self {
95 Self::NotMapped(NotMappedReason::Unconfined) => std::env::var_os("TMPDIR")
96 .map(|value| value.to_string_lossy().into_owned())
97 .unwrap_or_else(|| TMP_PATH.to_string()),
98 _ if self.is_mapped() => TMP_PATH.to_string(),
99 _ => folder.to_string_lossy().into_owned(),
100 }
101 }
102}
103
104impl NotMappedReason {
105 #[must_use]
107 pub fn describe(self) -> &'static str {
108 match self {
109 Self::NoKernelContainment => {
110 "this host has no kernel containment for capsule commands, so a literal /tmp path \
111 is the host's own /tmp; TMPDIR names the session's temporary folder"
112 }
113 Self::NoMountNamespaceNoLandlock => {
114 "this host can give capsule commands neither a private mount namespace nor \
115 Landlock, so a literal /tmp path is the host's own /tmp; TMPDIR names the \
116 session's temporary folder"
117 }
118 Self::NoMountNamespace => {
119 "this host cannot give capsule commands a private mount namespace, so a literal \
120 /tmp path is refused; TMPDIR names the session's temporary folder"
121 }
122 Self::WorkdirIsTmp => {
123 "the workdir is /tmp itself, which mapping /tmp would hide; TMPDIR names the \
124 session's temporary folder"
125 }
126 Self::Unconfined => {
127 "the session runs unconfined, so commands see the TMPDIR mur run was started with \
128 and the host's own /tmp"
129 }
130 }
131 }
132}
133
134pub(crate) fn canonical_tmp() -> PathBuf {
136 std::fs::canonicalize(TMP_PATH).unwrap_or_else(|_| PathBuf::from(TMP_PATH))
137}
138
139pub(crate) fn tier_temp_mapping(applied: EnforcementTier) -> TempMapping {
143 match applied {
144 EnforcementTier::KernelSealed => TempMapping::ComposedRoot,
145 EnforcementTier::KernelFull => TempMapping::NotMapped(NotMappedReason::NoMountNamespace),
146 EnforcementTier::KernelSeccompOnly => {
147 TempMapping::NotMapped(NotMappedReason::NoMountNamespaceNoLandlock)
148 }
149 EnforcementTier::EnvironmentOnly => {
150 TempMapping::NotMapped(NotMappedReason::NoKernelContainment)
151 }
152 EnforcementTier::Unconfined => TempMapping::NotMapped(NotMappedReason::Unconfined),
153 }
154}
155
156pub(crate) fn temp_mapping(
163 applied: EnforcementTier,
164 mount_namespace_usable: bool,
165 accessible_workdir: &Path,
166) -> TempMapping {
167 let workdir_is_tmp = || {
168 let workdir = std::fs::canonicalize(accessible_workdir)
169 .unwrap_or_else(|_| accessible_workdir.to_path_buf());
170 workdir == canonical_tmp()
171 };
172 match applied {
173 EnforcementTier::KernelFull | EnforcementTier::KernelSeccompOnly if workdir_is_tmp() => {
174 TempMapping::NotMapped(NotMappedReason::WorkdirIsTmp)
175 }
176 EnforcementTier::KernelFull | EnforcementTier::KernelSeccompOnly
177 if mount_namespace_usable =>
178 {
179 TempMapping::MountNamespace
180 }
181 _ => tier_temp_mapping(applied),
182 }
183}
184
185#[cfg(target_os = "linux")]
202pub(crate) fn reexposed_paths<'a>(
203 canonical_tmp: &Path,
204 writable: impl IntoIterator<Item = &'a Path>,
205 read_only: impl IntoIterator<Item = &'a Path>,
206) -> Vec<(PathBuf, bool)> {
207 let beneath_tmp = |path: &Path| {
208 std::fs::canonicalize(path)
209 .ok()
210 .filter(|canonical| canonical != canonical_tmp && canonical.starts_with(canonical_tmp))
211 };
212 let mut wanted: Vec<(PathBuf, bool)> = Vec::new();
213 for (path, is_read_only) in writable
214 .into_iter()
215 .map(|path| (path, false))
216 .chain(read_only.into_iter().map(|path| (path, true)))
217 {
218 let Some(canonical) = beneath_tmp(path) else {
219 continue;
220 };
221 match wanted.iter_mut().find(|(seen, _)| *seen == canonical) {
222 Some((_, seen_read_only)) => *seen_read_only &= is_read_only,
223 None => wanted.push((canonical, is_read_only)),
224 }
225 }
226 wanted.sort();
227 let mut kept: Vec<(PathBuf, bool)> = Vec::new();
228 for (path, is_read_only) in wanted {
229 let under_writable = kept
230 .iter()
231 .any(|(outer, outer_read_only)| !outer_read_only && path.starts_with(outer));
232 if !under_writable {
233 kept.push((path, is_read_only));
234 }
235 }
236 kept
237}
238
239#[cfg(all(test, target_os = "linux"))]
240pub(crate) use linux::TmpMapFailure;
241#[cfg(target_os = "linux")]
242pub(crate) use linux::{map_private_tmp, TmpMapSpec};
243
244#[cfg(target_os = "linux")]
245#[allow(unsafe_code)]
246mod linux {
247 use std::ffi::CString;
248 use std::os::unix::ffi::OsStrExt;
249 use std::path::{Path, PathBuf};
250 use std::sync::atomic::{AtomicI32, Ordering};
251
252 use crate::sealed::{open_verified_dir, proc_fd_path, remount_read_only, PROC_FD_PATH_LEN};
253
254 struct Reexposure {
257 source: CString,
259 components: Vec<CString>,
261 directory: bool,
263 read_only: bool,
265 fd: AtomicI32,
268 }
269
270 pub(crate) struct TmpMapSpec {
273 folder: CString,
276 folder_display: PathBuf,
277 folder_identity: (u64, u64),
278 tmp: CString,
280 tmp_display: PathBuf,
281 reexposures: Vec<Reexposure>,
282 workdir: CString,
284 workdir_display: PathBuf,
285 }
286
287 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
289 pub(crate) struct TmpMapFailure {
290 step: Step,
291 errno: i32,
292 }
293
294 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
295 enum Step {
296 Unshare,
297 MakePrivate,
298 OpenSource(usize),
299 OpenFolder,
300 BindFolder,
301 OpenTmp,
302 CreateMountpoint(usize),
303 OpenMountpoint(usize),
304 BindReexposure(usize),
305 RemountReadOnly(usize),
306 Chdir,
307 }
308
309 impl TmpMapFailure {
310 fn at(step: Step) -> Self {
311 Self {
312 step,
313 errno: std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
314 }
315 }
316
317 #[cfg(test)]
319 pub(crate) fn forced(errno: i32) -> Self {
320 Self {
321 step: Step::Unshare,
322 errno,
323 }
324 }
325 }
326
327 impl TmpMapSpec {
328 pub(crate) fn new(
332 folder: &Path,
333 folder_identity: (u64, u64),
334 canonical_tmp: &Path,
335 reexposed: &[(PathBuf, bool)],
336 workdir: &Path,
337 ) -> Result<Self, String> {
338 let c_path = |path: &Path| {
339 CString::new(path.as_os_str().as_bytes()).map_err(|_| {
340 format!(
341 "{} {} holds a NUL byte",
342 super::TMP_MAP_FAILURE_PREFIX,
343 path.display()
344 )
345 })
346 };
347 let mut reexposures = Vec::with_capacity(reexposed.len());
348 for (source, read_only) in reexposed {
349 let Ok(beneath) = source.strip_prefix(canonical_tmp) else {
350 continue;
351 };
352 let components = beneath
353 .components()
354 .map(|component| c_path(Path::new(component.as_os_str())))
355 .collect::<Result<Vec<_>, _>>()?;
356 if components.is_empty() {
357 continue;
358 }
359 reexposures.push(Reexposure {
360 source: c_path(source)?,
361 components,
362 directory: std::fs::symlink_metadata(source)
363 .map(|meta| meta.is_dir())
364 .unwrap_or(true),
365 read_only: *read_only,
366 fd: AtomicI32::new(-1),
367 });
368 }
369 Ok(Self {
370 folder: c_path(folder)?,
371 folder_display: folder.to_path_buf(),
372 folder_identity,
373 tmp: c_path(canonical_tmp)?,
374 tmp_display: canonical_tmp.to_path_buf(),
375 reexposures,
376 workdir: c_path(workdir)?,
377 workdir_display: workdir.to_path_buf(),
378 })
379 }
380
381 #[cfg(test)]
384 pub(crate) fn reexposed_sources(&self) -> Vec<(String, bool)> {
385 self.reexposures
386 .iter()
387 .map(|entry| (entry.source.to_string_lossy().into_owned(), entry.read_only))
388 .collect()
389 }
390
391 pub(crate) fn describe(&self, failure: TmpMapFailure) -> String {
395 let tmp = self.tmp_display.display();
396 let entry = |index: usize| {
397 self.reexposures
398 .get(index)
399 .map(|entry| {
400 entry
401 .components
402 .iter()
403 .map(|component| component.to_string_lossy().into_owned())
404 .collect::<Vec<_>>()
405 .join("/")
406 })
407 .unwrap_or_default()
408 };
409 let source = |index: usize| {
410 self.reexposures
411 .get(index)
412 .map(|entry| entry.source.to_string_lossy().into_owned())
413 .unwrap_or_default()
414 };
415 let step = match failure.step {
416 Step::Unshare => "unshare(CLONE_NEWNS)".to_string(),
417 Step::MakePrivate => "making / private (MS_REC|MS_PRIVATE)".to_string(),
418 Step::OpenSource(index) => format!("opening {} to re-expose it", source(index)),
419 Step::OpenFolder => format!(
420 "opening the temporary folder {}",
421 self.folder_display.display()
422 ),
423 Step::BindFolder => format!(
424 "binding the temporary folder {} over {tmp}",
425 self.folder_display.display()
426 ),
427 Step::OpenTmp => format!("opening the new {tmp}"),
428 Step::CreateMountpoint(index) => {
429 format!("creating the mountpoint {tmp}/{}", entry(index))
430 }
431 Step::OpenMountpoint(index) => {
432 format!("opening the mountpoint {tmp}/{}", entry(index))
433 }
434 Step::BindReexposure(index) => {
435 format!("binding {} back at its own path", source(index))
436 }
437 Step::RemountReadOnly(index) => {
438 format!("remounting {} read-only", source(index))
439 }
440 Step::Chdir => format!(
441 "entering the working directory {}",
442 self.workdir_display.display()
443 ),
444 };
445 format!(
446 "{} {step} failed: {}",
447 super::TMP_MAP_FAILURE_PREFIX,
448 std::io::Error::from_raw_os_error(failure.errno)
449 )
450 }
451 }
452
453 unsafe fn close_keeping_errno(fd: libc::c_int) {
458 let errno = *libc::__errno_location();
459 libc::close(fd);
460 *libc::__errno_location() = errno;
461 }
462
463 unsafe fn bind_fd_onto_fd(source: libc::c_int, target: libc::c_int) -> Result<(), ()> {
469 let mut source_path = [0u8; PROC_FD_PATH_LEN];
470 let mut target_path = [0u8; PROC_FD_PATH_LEN];
471 proc_fd_path(source, &mut source_path)?;
472 proc_fd_path(target, &mut target_path)?;
473 if libc::mount(
474 source_path.as_ptr() as *const libc::c_char,
475 target_path.as_ptr() as *const libc::c_char,
476 std::ptr::null(),
477 libc::MS_BIND | libc::MS_REC,
478 std::ptr::null(),
479 ) != 0
480 {
481 return Err(());
482 }
483 Ok(())
484 }
485
486 unsafe fn fd_kind_is(fd: libc::c_int, directory: bool) -> bool {
491 let mut stat: libc::stat = std::mem::zeroed();
492 if libc::fstat(fd, &mut stat) != 0 {
493 return false;
494 }
495 let kind = stat.st_mode & libc::S_IFMT;
496 if directory {
497 kind == libc::S_IFDIR
498 } else {
499 kind == libc::S_IFREG
500 }
501 }
502
503 unsafe fn close_sources(spec: &TmpMapSpec) {
508 for entry in &spec.reexposures {
509 let fd = entry.fd.swap(-1, Ordering::Relaxed);
510 if fd >= 0 {
511 close_keeping_errno(fd);
512 }
513 }
514 }
515
516 pub(crate) unsafe fn map_private_tmp(spec: &TmpMapSpec) -> Result<(), TmpMapFailure> {
525 if libc::unshare(libc::CLONE_NEWNS) != 0 {
527 return Err(TmpMapFailure::at(Step::Unshare));
528 }
529 if libc::mount(
531 std::ptr::null(),
532 c"/".as_ptr(),
533 std::ptr::null(),
534 libc::MS_REC | libc::MS_PRIVATE,
535 std::ptr::null(),
536 ) != 0
537 {
538 return Err(TmpMapFailure::at(Step::MakePrivate));
539 }
540 for (index, entry) in spec.reexposures.iter().enumerate() {
543 let fd = libc::open(
544 entry.source.as_ptr(),
545 libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
546 );
547 if fd < 0 {
548 let failure = TmpMapFailure::at(Step::OpenSource(index));
549 close_sources(spec);
550 return Err(failure);
551 }
552 entry.fd.store(fd, Ordering::Relaxed);
553 if !fd_kind_is(fd, entry.directory) {
554 close_sources(spec);
555 *libc::__errno_location() = libc::ESTALE;
556 return Err(TmpMapFailure::at(Step::OpenSource(index)));
557 }
558 }
559 let folder = match open_verified_dir(&spec.folder, spec.folder_identity) {
562 Ok(fd) => fd,
563 Err(()) => {
564 let failure = TmpMapFailure::at(Step::OpenFolder);
565 close_sources(spec);
566 return Err(failure);
567 }
568 };
569 let tmp_target = libc::open(
570 spec.tmp.as_ptr(),
571 libc::O_PATH | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
572 );
573 let bound = tmp_target >= 0 && bind_fd_onto_fd(folder, tmp_target).is_ok();
574 let failure = (!bound).then(|| TmpMapFailure::at(Step::BindFolder));
575 close_keeping_errno(folder);
576 if tmp_target >= 0 {
577 close_keeping_errno(tmp_target);
578 }
579 if let Some(failure) = failure {
580 close_sources(spec);
581 return Err(failure);
582 }
583 let new_tmp = libc::open(
587 spec.tmp.as_ptr(),
588 libc::O_PATH | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
589 );
590 if new_tmp < 0 {
591 let failure = TmpMapFailure::at(Step::OpenTmp);
592 close_sources(spec);
593 return Err(failure);
594 }
595 let result = reexpose_all(spec, new_tmp);
596 close_keeping_errno(new_tmp);
597 close_sources(spec);
598 result?;
599 if libc::chdir(spec.workdir.as_ptr()) != 0 {
603 return Err(TmpMapFailure::at(Step::Chdir));
604 }
605 Ok(())
606 }
607
608 unsafe fn open_or_create_dir(parent: libc::c_int, name: &CString) -> libc::c_int {
614 let flags = libc::O_PATH | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC;
615 let fd = libc::openat(parent, name.as_ptr(), flags);
616 if fd >= 0 || *libc::__errno_location() != libc::ENOENT {
617 return fd;
618 }
619 if libc::mkdirat(parent, name.as_ptr(), 0o700) != 0
620 && *libc::__errno_location() != libc::EEXIST
621 {
622 return -1;
623 }
624 libc::openat(parent, name.as_ptr(), flags)
625 }
626
627 unsafe fn open_or_create_file(parent: libc::c_int, name: &CString) -> libc::c_int {
633 let flags = libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC;
634 let fd = libc::openat(parent, name.as_ptr(), flags);
635 if fd >= 0 || *libc::__errno_location() != libc::ENOENT {
636 return fd;
637 }
638 let created = libc::openat(
639 parent,
640 name.as_ptr(),
641 libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
642 0o600,
643 );
644 if created >= 0 {
645 libc::close(created);
646 } else if *libc::__errno_location() != libc::EEXIST {
647 return -1;
648 }
649 libc::openat(parent, name.as_ptr(), flags)
650 }
651
652 unsafe fn remount_fd_read_only(fd: libc::c_int) -> Result<(), ()> {
658 let mut path = [0u8; PROC_FD_PATH_LEN];
659 proc_fd_path(fd, &mut path)?;
660 let path = std::ffi::CStr::from_bytes_until_nul(&path).map_err(|_| ())?;
661 remount_read_only(path)
662 }
663
664 unsafe fn reexpose_all(spec: &TmpMapSpec, new_tmp: libc::c_int) -> Result<(), TmpMapFailure> {
669 for (index, entry) in spec.reexposures.iter().enumerate() {
670 let Some((last, leading)) = entry.components.split_last() else {
671 continue;
672 };
673 let mut parent = new_tmp;
675 for component in leading {
676 let next = open_or_create_dir(parent, component);
677 let failure = (next < 0).then(|| TmpMapFailure::at(Step::CreateMountpoint(index)));
678 if parent != new_tmp {
679 close_keeping_errno(parent);
680 }
681 if let Some(failure) = failure {
682 return Err(failure);
683 }
684 parent = next;
685 }
686 let close_parent = |parent: libc::c_int| {
687 if parent != new_tmp {
688 unsafe { close_keeping_errno(parent) };
690 }
691 };
692 let mountpoint = if entry.directory {
693 open_or_create_dir(parent, last)
694 } else {
695 open_or_create_file(parent, last)
696 };
697 if mountpoint < 0 {
698 let failure = TmpMapFailure::at(Step::CreateMountpoint(index));
699 close_parent(parent);
700 return Err(failure);
701 }
702 if !fd_kind_is(mountpoint, entry.directory) {
703 libc::close(mountpoint);
704 close_parent(parent);
705 *libc::__errno_location() = libc::ENOTDIR;
706 return Err(TmpMapFailure::at(Step::OpenMountpoint(index)));
707 }
708 let bound = bind_fd_onto_fd(entry.fd.load(Ordering::Relaxed), mountpoint);
709 let failure = bound
710 .is_err()
711 .then(|| TmpMapFailure::at(Step::BindReexposure(index)));
712 close_keeping_errno(mountpoint);
713 if let Some(failure) = failure {
714 close_parent(parent);
715 return Err(failure);
716 }
717 if entry.read_only {
718 let root = libc::openat(
720 parent,
721 last.as_ptr(),
722 libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
723 );
724 let remounted = root >= 0 && remount_fd_read_only(root).is_ok();
725 let failure = (!remounted).then(|| TmpMapFailure::at(Step::RemountReadOnly(index)));
726 if root >= 0 {
727 close_keeping_errno(root);
728 }
729 if let Some(failure) = failure {
730 close_parent(parent);
731 return Err(failure);
732 }
733 }
734 close_parent(parent);
735 }
736 Ok(())
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
746 fn temp_mapping_follows_the_per_tier_table() {
747 let elsewhere = Path::new("/nonexistent-murmur-temp-dir-workdir");
748 let tmp = canonical_tmp();
749 let rows = [
750 (
751 EnforcementTier::KernelSealed,
752 true,
753 elsewhere,
754 TempMapping::ComposedRoot,
755 ),
756 (
757 EnforcementTier::KernelSealed,
758 true,
759 tmp.as_path(),
760 TempMapping::ComposedRoot,
761 ),
762 (
763 EnforcementTier::KernelFull,
764 true,
765 elsewhere,
766 TempMapping::MountNamespace,
767 ),
768 (
769 EnforcementTier::KernelFull,
770 false,
771 elsewhere,
772 TempMapping::NotMapped(NotMappedReason::NoMountNamespace),
773 ),
774 (
775 EnforcementTier::KernelSeccompOnly,
776 true,
777 elsewhere,
778 TempMapping::MountNamespace,
779 ),
780 (
781 EnforcementTier::KernelSeccompOnly,
782 false,
783 elsewhere,
784 TempMapping::NotMapped(NotMappedReason::NoMountNamespaceNoLandlock),
785 ),
786 (
787 EnforcementTier::EnvironmentOnly,
788 true,
789 elsewhere,
790 TempMapping::NotMapped(NotMappedReason::NoKernelContainment),
791 ),
792 (
793 EnforcementTier::EnvironmentOnly,
794 false,
795 elsewhere,
796 TempMapping::NotMapped(NotMappedReason::NoKernelContainment),
797 ),
798 (
799 EnforcementTier::KernelFull,
800 true,
801 tmp.as_path(),
802 TempMapping::NotMapped(NotMappedReason::WorkdirIsTmp),
803 ),
804 (
805 EnforcementTier::KernelSeccompOnly,
806 true,
807 Path::new(TMP_PATH),
808 TempMapping::NotMapped(NotMappedReason::WorkdirIsTmp),
809 ),
810 ];
811 for (tier, usable, workdir, expected) in rows {
812 assert_eq!(
813 temp_mapping(tier, usable, workdir),
814 expected,
815 "{tier:?}, mount namespace usable: {usable}, workdir {}",
816 workdir.display()
817 );
818 }
819 }
820
821 #[test]
822 fn tmpdir_is_tmp_when_mapped_and_the_folder_otherwise() {
823 let folder = Path::new("/home/u/.murmur/records/w/ses_1/tmp");
824 assert_eq!(TempMapping::ComposedRoot.tmpdir_value(folder), "/tmp");
825 assert_eq!(TempMapping::MountNamespace.tmpdir_value(folder), "/tmp");
826 for reason in [
827 NotMappedReason::NoKernelContainment,
828 NotMappedReason::NoMountNamespaceNoLandlock,
829 NotMappedReason::NoMountNamespace,
830 NotMappedReason::WorkdirIsTmp,
831 ] {
832 let mapping = TempMapping::NotMapped(reason);
833 assert_eq!(mapping.tmpdir_value(folder), folder.to_str().unwrap());
834 assert_eq!(mapping.mapped_by(), None);
835 assert!(!reason.describe().is_empty());
836 }
837 assert_eq!(TempMapping::ComposedRoot.mapped_by(), Some("composed-root"));
838 assert_eq!(
839 TempMapping::MountNamespace.mapped_by(),
840 Some("mount-namespace")
841 );
842 }
843
844 #[cfg(target_os = "linux")]
849 #[test]
850 fn reexposed_paths_are_exact_and_read_only_unless_writable() {
851 let tmp = canonical_tmp();
852 let base = tempfile::tempdir_in(&tmp).unwrap();
853 let base = std::fs::canonicalize(base.path()).unwrap();
854 let workdir = base.join("work");
855 let session = workdir.join(".murmur").join("ses");
856 let records = base.join("records");
857 let inside_records = records.join("nested-work");
858 let grant = base.join("bin").join("tool");
859 for dir in [
860 &session,
861 &inside_records,
862 &grant.parent().unwrap().to_path_buf(),
863 ] {
864 std::fs::create_dir_all(dir).unwrap();
865 }
866 std::fs::write(&grant, b"x").unwrap();
867 let home = std::env::var_os("HOME").map(PathBuf::from).unwrap();
868 let absent = tmp.join("murmur-temp-dir-test-absent");
869
870 let paths = reexposed_paths(
871 &tmp,
872 [
873 workdir.as_path(),
874 session.as_path(),
875 inside_records.as_path(),
876 ],
877 [
878 records.as_path(),
879 grant.as_path(),
880 workdir.join(".murmur").as_path(),
881 tmp.as_path(),
882 home.as_path(),
883 absent.as_path(),
884 grant.as_path(),
885 ],
886 );
887 assert_eq!(
888 paths,
889 vec![
890 (grant.clone(), true),
891 (records.clone(), true),
892 (inside_records.clone(), false),
893 (workdir.clone(), false),
894 ]
895 );
896 }
897
898 #[cfg(target_os = "linux")]
899 #[test]
900 fn a_mapping_failure_names_its_step_and_errno() {
901 let spec = TmpMapSpec::new(
902 Path::new("/home/u/.murmur/records/w/ses_1/tmp"),
903 (1, 2),
904 Path::new("/tmp"),
905 &[],
906 Path::new("/work"),
907 )
908 .unwrap();
909 let message = spec.describe(TmpMapFailure::forced(libc::EPERM));
910 assert!(message.starts_with(TMP_MAP_FAILURE_PREFIX), "{message}");
911 assert!(
912 message.contains("unshare(CLONE_NEWNS) failed: "),
913 "{message}"
914 );
915 assert!(
916 message.contains(&std::io::Error::from_raw_os_error(libc::EPERM).to_string()),
917 "{message}"
918 );
919 }
920}