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