1use super::Fs;
19use crate::sys::{Errno, PATH_MAX, Result, errno};
20use std::collections::VecDeque;
21use std::ffi::CString;
22use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
23use std::sync::Arc;
24
25const MAX_SYMLINKS: u32 = 40;
27
28const MAX_RESTARTS: u32 = 3;
31
32pub const RETRY: Errno = -2;
37
38#[derive(Debug)]
40pub enum At {
41 Child { parent: Arc<OwnedFd>, name: Vec<u8> },
45 Object(Arc<OwnedFd>),
48 Path,
51}
52
53#[derive(Debug)]
55pub struct Resolved {
56 pub guest: Vec<u8>,
58 pub host: Vec<u8>,
61 pub at: At,
63}
64
65impl Resolved {
66 pub fn at(&self, nofollow: bool) -> Result<(RawFd, CString, bool)> {
76 match &self.at {
77 At::Child { parent, name } => Ok((parent.as_raw_fd(), cstr(name)?, true)),
78 At::Object(fd) => Ok((libc::AT_FDCWD, fd_path(fd), false)),
79 At::Path => Ok((libc::AT_FDCWD, cstr(&self.host)?, nofollow)),
80 }
81 }
82
83 pub fn pinned(&self) -> bool {
85 !matches!(self.at, At::Path)
86 }
87
88 pub fn is_mount_point(&self) -> bool {
90 matches!(self.at, At::Object(_))
91 }
92
93 pub fn is_proc_fd_link(&self) -> bool {
95 matches!(self.at, At::Path) && is_proc_fd_link(&self.guest)
96 }
97
98 pub fn mode_nofollow(&self) -> Result<u32> {
100 let mut st: libc::stat = unsafe { std::mem::zeroed() };
101 let (dfd, c, nofollow) = self.at(true)?;
102 let flags = if nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 };
103 if unsafe { libc::fstatat(dfd, c.as_ptr(), &mut st, flags) } < 0 {
104 return Err(errno());
105 }
106 Ok(st.st_mode)
107 }
108
109 pub fn object(&self, deref: bool) -> Result<(Arc<OwnedFd>, u32)> {
122 let fd = match &self.at {
123 At::Object(fd) => fd.clone(),
124 At::Child { parent, name } => Arc::new(open_at(parent, name, libc::O_NOFOLLOW)?),
125 At::Path => {
126 let c = cstr(&self.host)?;
127 let flags = libc::O_PATH | libc::O_CLOEXEC | if deref { 0 } else { libc::O_NOFOLLOW };
128 let fd = unsafe { libc::open(c.as_ptr(), flags) };
129 if fd < 0 {
130 return Err(errno());
131 }
132 Arc::new(unsafe { OwnedFd::from_raw_fd(fd) })
133 }
134 };
135 let mode = fstat_mode(&fd)?;
136 if deref && self.pinned() && mode & libc::S_IFMT == libc::S_IFLNK {
137 return Err(RETRY);
138 }
139 Ok((fd, mode))
140 }
141
142 pub fn parent_path(&self) -> Result<CString> {
146 match &self.at {
147 At::Child { parent, name } => {
148 let mut p = format!("/proc/self/fd/{}/", parent.as_raw_fd()).into_bytes();
149 p.extend_from_slice(name);
150 cstr(&p)
151 }
152 At::Object(fd) => Ok(fd_path(fd)),
153 At::Path => cstr(&self.host),
154 }
155 }
156}
157
158pub fn fd_path(fd: &OwnedFd) -> CString {
160 CString::new(format!("/proc/self/fd/{}", fd.as_raw_fd())).unwrap()
161}
162
163#[derive(Clone, Copy, PartialEq, Eq)]
165enum Missing {
166 Lexical,
169 Errno,
171}
172
173pub fn canonicalize(fs: &Fs, pid: u32, guest: &[u8], deref_final: bool) -> Result<Vec<u8>> {
190 resolve_with(fs, pid, guest, deref_final, Missing::Lexical).map(|r| r.guest)
191}
192
193pub fn resolve(fs: &Fs, pid: u32, guest: &[u8], deref_final: bool) -> Result<Resolved> {
223 resolve_with(fs, pid, guest, deref_final, Missing::Errno)
224}
225
226fn resolve_with(fs: &Fs, pid: u32, guest: &[u8], deref_final: bool, missing: Missing) -> Result<Resolved> {
227 if guest.first() != Some(&b'/') {
228 return Err(libc::EINVAL);
229 }
230 if guest.len() >= PATH_MAX {
231 return Err(libc::ENAMETOOLONG);
232 }
233 for _ in 0..MAX_RESTARTS {
234 match walk(fs, pid, guest, deref_final, missing) {
235 Err(RETRY) => continue,
236 Ok(r) if r.guest.len() >= PATH_MAX => return Err(libc::ENAMETOOLONG),
237 r => return r,
238 }
239 }
240 Err(libc::ELOOP)
241}
242
243fn walk(fs: &Fs, pid: u32, path: &[u8], deref_final: bool, missing: Missing) -> Result<Resolved> {
245 let mut result: Vec<u8> = b"/".to_vec();
250 let mut cur: Option<Arc<OwnedFd>> = fs.mount_pin(b"/").flatten();
251 let mut seated = true;
252 let mut queue: VecDeque<Vec<u8>> = components(path);
253 let mut hops = 0u32;
254 while let Some(comp) = queue.pop_front() {
255 let is_final = queue.is_empty();
256 match comp.as_slice() {
257 b"." => continue,
258 b".." => {
259 pop_component(&mut result);
260 seated = false;
261 continue;
262 }
263 _ => {}
264 }
265 let candidate = join(&result, &comp);
266 if candidate == b"/proc/self" {
268 result = format!("/proc/{pid}").into_bytes();
269 seated = false;
270 continue;
271 }
272 if candidate == b"/proc/thread-self" {
273 result = format!("/proc/{pid}/task/{pid}").into_bytes();
274 seated = false;
275 continue;
276 }
277 if (!is_final || deref_final) && is_proc_magic_link(&candidate) {
278 if candidate.ends_with(b"/exe")
282 && let Some(exe) = proc_pid(&candidate).and_then(|n| fs.exe_of(n))
283 {
284 result = exe;
285 seated = false;
286 continue;
287 }
288 if candidate.ends_with(b"/cwd")
289 && let Some(cwd) = proc_pid(&candidate).and_then(|n| fs.virtual_cwd(n))
290 {
291 result = cwd;
292 seated = false;
293 continue;
294 }
295 if fs.proc_is_host {
296 if is_final && is_proc_fd_link(&candidate) {
303 return Ok(textual(fs, candidate));
304 }
305 let host = fs.substitute_guest_to_host(&candidate);
307 match read_link(&host) {
308 Ok(target) if target.first() == Some(&b'/') => {
309 let target = super::translate::strip_deleted(target);
310 result = fs.substitute_host_to_guest(&target).unwrap_or(target);
311 if candidate.ends_with(b"/root") {
312 result = b"/".to_vec();
313 }
314 seated = false;
315 continue;
316 }
317 Ok(_) if is_proc_fd_link(&candidate) => return Err(libc::ENOTDIR),
318 Ok(_) => return Ok(textual(fs, append_rest(candidate, queue.make_contiguous()))),
319 Err(e) => return Err(e),
320 }
321 }
322 }
323 if let Some(pin) = fs.mount_pin(&candidate) {
326 if is_final {
327 return Ok(match pin {
328 Some(fd) => object(fs, candidate, fd),
329 None => textual(fs, candidate),
330 });
331 }
332 result = candidate;
333 cur = pin;
334 seated = true;
335 continue;
336 }
337 if !is_final && let Some(fd) = fs.cache_get(&candidate) {
338 result = candidate;
339 cur = Some(fd);
340 seated = true;
341 continue;
342 }
343 if !seated {
344 cur = match open_dir(fs, &result) {
345 Err(libc::ENOENT) if fs.binding_below(&result) => None,
346 Err(libc::ENOENT) if missing == Missing::Lexical => {
347 return Ok(textual(fs, append_rest(candidate, queue.make_contiguous())));
348 }
349 r => r?,
350 };
351 seated = true;
352 }
353 let Some(dir) = cur.clone() else {
354 let host = fs.substitute_guest_to_host(&candidate);
356 let mode = match lstat_mode(&host) {
357 Ok(m) => m,
358 Err(_) if !is_final && fs.binding_below(&candidate) => {
359 result = candidate;
360 continue;
361 }
362 Err(_) if is_final => return Ok(textual(fs, candidate)),
363 Err(_) => return Ok(textual(fs, append_rest(candidate, queue.make_contiguous()))),
364 };
365 let is_link = mode & libc::S_IFMT == libc::S_IFLNK;
366 if is_link && (!is_final || deref_final) {
367 let target = read_link(&host)?;
368 follow(&mut hops, &mut result, &mut seated, &mut queue, target)?;
369 continue;
370 }
371 if !is_final && mode & libc::S_IFMT != libc::S_IFDIR {
372 return Err(libc::ENOTDIR);
373 }
374 result = candidate;
375 continue;
376 };
377 if is_final {
378 if !deref_final {
379 return Ok(child(fs, candidate, dir, comp));
380 }
381 match fstatat_mode(&dir, &comp) {
382 Ok(mode) if mode & libc::S_IFMT == libc::S_IFLNK => {
383 let target = readlinkat(&dir, &comp).map_err(|_| RETRY)?;
386 follow(&mut hops, &mut result, &mut seated, &mut queue, target)?;
387 continue;
388 }
389 _ => return Ok(child(fs, candidate, dir, comp)),
391 }
392 }
393 match open_dir_component(&dir, &comp) {
394 Ok(fd) => {
395 let fd = Arc::new(fd);
396 fs.cache_put(&candidate, fd.clone());
397 result = candidate;
398 cur = Some(fd);
399 }
400 Err(libc::ELOOP) => {
401 let target = readlinkat(&dir, &comp).map_err(|_| RETRY)?;
402 follow(&mut hops, &mut result, &mut seated, &mut queue, target)?;
403 }
404 Err(libc::ENOENT) if fs.binding_below(&candidate) => {
407 result = candidate;
408 cur = None;
409 }
410 Err(libc::ENOENT) if missing == Missing::Lexical => {
411 return Ok(textual(fs, append_rest(candidate, queue.make_contiguous())));
412 }
413 Err(e) => return Err(e),
415 }
416 }
417 if let Some(pin) = fs.mount_pin(&result) {
420 return Ok(match pin {
421 Some(fd) => object(fs, result, fd),
422 None => textual(fs, result),
423 });
424 }
425 let mut parent = result.clone();
426 pop_component(&mut parent);
427 let name = result.rsplit(|&c| c == b'/').next().unwrap_or_default().to_vec();
428 match open_dir(fs, &parent)? {
429 Some(dir) => Ok(child(fs, result, dir, name)),
430 None => Ok(textual(fs, result)),
431 }
432}
433
434fn follow(
437 hops: &mut u32,
438 result: &mut Vec<u8>,
439 seated: &mut bool,
440 queue: &mut VecDeque<Vec<u8>>,
441 target: Vec<u8>,
442) -> Result<()> {
443 *hops += 1;
444 if *hops > MAX_SYMLINKS {
445 return Err(libc::ELOOP);
446 }
447 if target.first() == Some(&b'/') {
448 *result = b"/".to_vec();
449 *seated = false;
450 }
451 for comp in components(&target).into_iter().rev() {
452 queue.push_front(comp);
453 }
454 Ok(())
455}
456
457fn open_dir(fs: &Fs, guest: &[u8]) -> Result<Option<Arc<OwnedFd>>> {
466 if let Some(fd) = fs.cache_get(guest) {
467 return Ok(Some(fd));
468 }
469 let (pin, rest) = fs.pin_for_guest(guest);
470 let Some(mut dir) = pin else {
471 return Ok(None);
472 };
473 let mut prefix = guest[..guest.len() - rest.len()].to_vec();
474 for comp in rest.split(|&c| c == b'/').filter(|c| !c.is_empty()) {
475 prefix = join(&prefix, comp);
476 if let Some(fd) = fs.cache_get(&prefix) {
477 dir = fd;
478 continue;
479 }
480 dir = Arc::new(open_dir_component(&dir, comp).map_err(|e| if e == libc::ELOOP { RETRY } else { e })?);
481 fs.cache_put(&prefix, dir.clone());
482 }
483 Ok(Some(dir))
484}
485
486fn open_dir_component(dir: &OwnedFd, comp: &[u8]) -> Result<OwnedFd> {
492 const RESOLVE_NO_SYMLINKS: u64 = 0x04;
493 #[repr(C)]
494 struct OpenHow {
495 flags: u64,
496 mode: u64,
497 resolve: u64,
498 }
499 let how = OpenHow {
500 flags: (libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC) as u64,
501 mode: 0,
502 resolve: RESOLVE_NO_SYMLINKS,
503 };
504 let c = cstr(comp)?;
505 let fd = unsafe {
506 libc::syscall(
507 libc::SYS_openat2,
508 dir.as_raw_fd(),
509 c.as_ptr(),
510 &how as *const OpenHow,
511 std::mem::size_of::<OpenHow>(),
512 )
513 };
514 if fd < 0 {
515 return Err(errno());
516 }
517 Ok(unsafe { OwnedFd::from_raw_fd(fd as RawFd) })
518}
519
520fn open_at(dir: &OwnedFd, name: &[u8], flags: i32) -> Result<OwnedFd> {
522 let c = cstr(name)?;
523 let fd = unsafe {
524 libc::openat(
525 dir.as_raw_fd(),
526 c.as_ptr(),
527 libc::O_PATH | libc::O_CLOEXEC | flags,
528 )
529 };
530 if fd < 0 {
531 return Err(errno());
532 }
533 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
534}
535
536fn child(fs: &Fs, guest: Vec<u8>, parent: Arc<OwnedFd>, name: Vec<u8>) -> Resolved {
538 Resolved {
539 host: fs.substitute_guest_to_host(&guest),
540 guest,
541 at: At::Child { parent, name },
542 }
543}
544
545fn object(fs: &Fs, guest: Vec<u8>, fd: Arc<OwnedFd>) -> Resolved {
547 Resolved {
548 host: fs.substitute_guest_to_host(&guest),
549 guest,
550 at: At::Object(fd),
551 }
552}
553
554fn textual(fs: &Fs, guest: Vec<u8>) -> Resolved {
556 Resolved {
557 host: fs.substitute_guest_to_host(&guest),
558 guest,
559 at: At::Path,
560 }
561}
562
563fn is_proc_magic_link(path: &[u8]) -> bool {
568 let Some(rest) = path.strip_prefix(b"/proc/") else {
569 return false;
570 };
571 let mut parts = rest.split(|&c| c == b'/');
572 let Some(pid) = parts.next() else { return false };
573 if pid.is_empty() || !pid.iter().all(|c| c.is_ascii_digit()) {
574 return false;
575 }
576 let mut parts: Vec<&[u8]> = parts.collect();
577 if parts.len() >= 2 && parts[0] == b"task" && parts[1].iter().all(|c| c.is_ascii_digit()) {
578 parts.drain(0..2);
579 }
580 match parts.as_slice() {
581 [b"exe"] | [b"cwd"] | [b"root"] => true,
582 [b"fd", n] => !n.is_empty() && n.iter().all(|c| c.is_ascii_digit()),
583 [b"map_files", _] | [b"ns", _] => true,
584 _ => false,
585 }
586}
587
588fn is_proc_fd_link(path: &[u8]) -> bool {
590 let Some(rest) = path.strip_prefix(b"/proc/") else {
591 return false;
592 };
593 let mut parts: Vec<&[u8]> = rest.split(|&c| c == b'/').collect();
594 if parts.len() >= 3 && parts[1] == b"task" {
595 parts.drain(0..2);
596 } else if !parts.is_empty() {
597 parts.remove(0);
598 }
599 matches!(parts.as_slice(), [b"fd", _] | [b"map_files", _])
600}
601
602fn proc_pid(path: &[u8]) -> Option<i32> {
604 let rest = path.strip_prefix(b"/proc/")?;
605 let parts: Vec<&[u8]> = rest.split(|&c| c == b'/').collect();
606 let pick = if parts.len() >= 3 && parts[1] == b"task" {
607 parts[2]
608 } else {
609 parts[0]
610 };
611 std::str::from_utf8(pick).ok()?.parse().ok()
612}
613
614fn components(path: &[u8]) -> VecDeque<Vec<u8>> {
616 path.split(|&c| c == b'/')
617 .filter(|c| !c.is_empty())
618 .map(<[u8]>::to_vec)
619 .collect()
620}
621
622fn append_rest(mut path: Vec<u8>, rest: &[Vec<u8>]) -> Vec<u8> {
624 for comp in rest {
625 match comp.as_slice() {
626 b"." => {}
627 b".." => pop_component(&mut path),
628 c => path = join(&path, c),
629 }
630 }
631 path
632}
633
634fn join(dir: &[u8], comp: &[u8]) -> Vec<u8> {
636 let mut out = Vec::with_capacity(dir.len() + comp.len() + 1);
637 out.extend_from_slice(dir);
638 if dir != b"/" {
639 out.push(b'/');
640 }
641 out.extend_from_slice(comp);
642 out
643}
644
645fn pop_component(path: &mut Vec<u8>) {
647 if let Some(i) = path.iter().rposition(|&c| c == b'/') {
648 path.truncate(i.max(1));
649 }
650}
651
652fn cstr(path: &[u8]) -> Result<CString> {
654 CString::new(path.to_vec()).map_err(|_| libc::EINVAL)
655}
656
657fn lstat_mode(host: &[u8]) -> Result<u32> {
659 let c = cstr(host)?;
660 let mut st: libc::stat = unsafe { std::mem::zeroed() };
661 if unsafe { libc::lstat(c.as_ptr(), &mut st) } < 0 {
662 return Err(errno());
663 }
664 Ok(st.st_mode)
665}
666
667fn fstatat_mode(dir: &OwnedFd, name: &[u8]) -> Result<u32> {
669 let c = cstr(name)?;
670 let mut st: libc::stat = unsafe { std::mem::zeroed() };
671 if unsafe { libc::fstatat(dir.as_raw_fd(), c.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) } < 0 {
672 return Err(errno());
673 }
674 Ok(st.st_mode)
675}
676
677fn fstat_mode(fd: &OwnedFd) -> Result<u32> {
679 let mut st: libc::stat = unsafe { std::mem::zeroed() };
680 if unsafe { libc::fstat(fd.as_raw_fd(), &mut st) } < 0 {
681 return Err(errno());
682 }
683 Ok(st.st_mode)
684}
685
686pub fn readlinkat(dir: &OwnedFd, name: &[u8]) -> Result<Vec<u8>> {
688 let c = cstr(name)?;
689 let mut buf = vec![0u8; PATH_MAX];
690 let n = unsafe {
691 libc::readlinkat(
692 dir.as_raw_fd(),
693 c.as_ptr(),
694 buf.as_mut_ptr() as *mut libc::c_char,
695 buf.len(),
696 )
697 };
698 if n < 0 {
699 return Err(errno());
700 }
701 buf.truncate(n as usize);
702 Ok(buf)
703}
704
705fn read_link(host: &[u8]) -> Result<Vec<u8>> {
707 super::translate::read_link(host)
708}
709
710#[allow(dead_code)]
718pub fn lexical_clean(path: &[u8]) -> Vec<u8> {
719 let mut parts: Vec<&[u8]> = Vec::new();
720 for comp in path.split(|&c| c == b'/') {
721 match comp {
722 b"" | b"." => {}
723 b".." => {
724 parts.pop();
725 }
726 c => parts.push(c),
727 }
728 }
729 let mut out = Vec::with_capacity(path.len() + 1);
730 for p in &parts {
731 out.push(b'/');
732 out.extend_from_slice(p);
733 }
734 if out.is_empty() {
735 out.push(b'/');
736 }
737 out
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use crate::config::{Binding, Config};
744 use std::os::unix::fs::symlink;
745 use std::path::PathBuf;
746 use std::sync::atomic::{AtomicUsize, Ordering};
747
748 static COUNTER: AtomicUsize = AtomicUsize::new(0);
749
750 fn me() -> u32 {
751 std::process::id()
752 }
753
754 fn fixture() -> (Fs, PathBuf, PathBuf) {
756 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
757 let base = std::env::temp_dir().join(format!("proot-next-canon-{}-{}", std::process::id(), n));
758 let root = base.join("root");
759 let bind = base.join("bind");
760 std::fs::create_dir_all(root.join("usr/bin")).unwrap();
761 std::fs::create_dir_all(root.join("etc")).unwrap();
762 std::fs::create_dir_all(&bind).unwrap();
763 std::fs::write(root.join("usr/bin/sh"), b"#!").unwrap();
764 std::fs::write(root.join("etc/passwd"), b"root").unwrap();
765 std::fs::write(bind.join("file"), b"bound").unwrap();
766 symlink("usr/bin", root.join("bin")).unwrap(); symlink("/usr/bin", root.join("abs")).unwrap(); symlink("loop", root.join("loop")).unwrap(); symlink("../../..", root.join("escape")).unwrap(); symlink("/mnt/file", root.join("tobind")).unwrap(); symlink("/etc/passwd", root.join("usr/bin/pw")).unwrap();
772 let mut c = Config {
773 root: root.as_os_str().as_encoded_bytes().to_vec(),
774 ..Config::default()
775 };
776 c.bindings.push(Binding {
777 host: bind.as_os_str().as_encoded_bytes().to_vec(),
778 guest: b"/mnt".to_vec(),
779 read_only: false,
780 });
781 (Fs::new(&c), root, bind)
782 }
783
784 fn pinned_inode(r: &Resolved) -> u64 {
786 let mut st: libc::stat = unsafe { std::mem::zeroed() };
787 let (dfd, c, nofollow) = r.at(false).unwrap();
788 let flags = if nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 };
789 assert_eq!(unsafe { libc::fstatat(dfd, c.as_ptr(), &mut st, flags) }, 0);
790 st.st_ino
791 }
792
793 fn inode(p: &std::path::Path) -> u64 {
794 use std::os::unix::fs::MetadataExt;
795 std::fs::symlink_metadata(p).unwrap().ino()
796 }
797
798 #[test]
799 fn follows_links_inside_the_guest() {
800 let (fs, _root, _bind) = fixture();
801 assert_eq!(
802 canonicalize(&fs, me(), b"/bin/sh", true).unwrap(),
803 b"/usr/bin/sh".to_vec()
804 );
805 assert_eq!(
806 canonicalize(&fs, me(), b"/abs/sh", true).unwrap(),
807 b"/usr/bin/sh".to_vec()
808 );
809 assert_eq!(canonicalize(&fs, me(), b"/abs", false).unwrap(), b"/abs".to_vec());
810 assert_eq!(
811 canonicalize(&fs, me(), b"/abs", true).unwrap(),
812 b"/usr/bin".to_vec()
813 );
814 assert_eq!(
815 canonicalize(&fs, me(), b"//usr/./bin/../bin/pw", true).unwrap(),
816 b"/etc/passwd".to_vec()
817 );
818 assert_eq!(
819 canonicalize(&fs, me(), b"/usr/bin/pw", false).unwrap(),
820 b"/usr/bin/pw".to_vec()
821 );
822 }
823
824 #[test]
825 fn cannot_escape_and_detects_loops() {
826 let (fs, _root, _bind) = fixture();
827 assert_eq!(
828 canonicalize(&fs, me(), b"/escape/etc/passwd", true).unwrap(),
829 b"/etc/passwd".to_vec()
830 );
831 assert_eq!(
832 canonicalize(&fs, me(), b"/../../etc", true).unwrap(),
833 b"/etc".to_vec()
834 );
835 assert_eq!(canonicalize(&fs, me(), b"/loop", true), Err(libc::ELOOP));
836 assert_eq!(canonicalize(&fs, me(), b"/loop/x", true), Err(libc::ELOOP));
837 assert_eq!(
838 canonicalize(&fs, me(), b"/etc/passwd/x", true),
839 Err(libc::ENOTDIR)
840 );
841 assert_eq!(canonicalize(&fs, me(), b"relative", true), Err(libc::EINVAL));
842 }
843
844 #[test]
845 fn missing_components_get_the_kernels_errors() {
846 let (fs, _root, _bind) = fixture();
847 assert_eq!(
849 canonicalize(&fs, me(), b"/etc/new", true).unwrap(),
850 b"/etc/new".to_vec()
851 );
852 let r = resolve(&fs, me(), b"/etc/new", false).unwrap();
853 assert!(matches!(&r.at, At::Child { name, .. } if name == b"new"));
854 assert_eq!(
857 resolve(&fs, me(), b"/nope/a/../b/./c", true).err(),
858 Some(libc::ENOENT)
859 );
860 assert_eq!(resolve(&fs, me(), b"/nope/..", true).err(), Some(libc::ENOENT));
861 assert_eq!(
862 canonicalize(&fs, me(), b"/nope/a/../b/./c", true).unwrap(),
863 b"/nope/b/c".to_vec()
864 );
865 }
866
867 #[test]
868 fn links_into_bindings_resolve_on_the_binding_host() {
869 let (fs, _root, bind) = fixture();
870 assert_eq!(
871 canonicalize(&fs, me(), b"/tobind", true).unwrap(),
872 b"/mnt/file".to_vec()
873 );
874 let r = resolve(&fs, me(), b"/tobind", true).unwrap();
875 assert_eq!(r.host, bind.join("file").as_os_str().as_encoded_bytes().to_vec());
876 assert_eq!(pinned_inode(&r), inode(&bind.join("file")));
877 assert_eq!(
878 canonicalize(&fs, me(), b"/mnt/../etc", true).unwrap(),
879 b"/etc".to_vec()
880 );
881 }
882
883 #[test]
884 fn roots_and_mount_points_are_the_pinned_objects() {
885 let (fs, root, bind) = fixture();
886 let r = resolve(&fs, me(), b"/", true).unwrap();
887 assert!(r.is_mount_point());
888 assert_eq!(pinned_inode(&r), inode(&root));
889 let r = resolve(&fs, me(), b"/mnt", false).unwrap();
890 assert!(r.is_mount_point());
891 assert_eq!(pinned_inode(&r), inode(&bind));
892 let r = resolve(&fs, me(), b"/usr/bin/..", true).unwrap();
893 assert!(matches!(&r.at, At::Child { name, .. } if name == b"usr"));
894 assert_eq!(pinned_inode(&r), inode(&root.join("usr")));
895 }
896
897 #[test]
898 fn directory_cache_is_used_and_flushed() {
899 let (fs, root, _bind) = fixture();
900 canonicalize(&fs, me(), b"/usr/bin/sh", true).unwrap();
901 assert!(fs.cache_has_dir(b"/usr/bin") || std::env::var_os("PROOT_NO_PATH_CACHE").is_some());
902 std::fs::rename(root.join("usr/bin"), root.join("usr/realbin")).unwrap();
904 symlink("realbin", root.join("usr/bin")).unwrap();
905 fs.flush_cache();
906 assert!(!fs.cache_has_dir(b"/usr/bin"));
907 assert_eq!(
908 canonicalize(&fs, me(), b"/usr/bin/sh", true).unwrap(),
909 b"/usr/realbin/sh".to_vec()
910 );
911 }
912
913 #[test]
914 fn a_swapped_in_symlink_is_seen_not_followed() {
915 let (fs, root, _bind) = fixture();
916 let outside = root.parent().unwrap().join("outside");
917 std::fs::create_dir_all(&outside).unwrap();
918 std::fs::write(outside.join("sh"), b"host").unwrap();
919 let r = resolve(&fs, me(), b"/usr/bin/sh", true).unwrap();
922 std::fs::rename(root.join("usr/bin"), root.join("usr/realbin")).unwrap();
923 symlink(&outside, root.join("usr/bin")).unwrap();
924 assert_eq!(pinned_inode(&r), inode(&root.join("usr/realbin/sh")));
925 let (dfd, c, nofollow) = r.at(false).unwrap();
926 assert!(nofollow);
927 let fd = unsafe { libc::openat(dfd, c.as_ptr(), libc::O_RDONLY | libc::O_NOFOLLOW) };
928 assert!(fd >= 0);
929 let mut buf = [0u8; 2];
930 assert_eq!(unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, 2) }, 2);
931 assert_eq!(&buf, b"#!");
932 unsafe { libc::close(fd) };
933 fs.flush_cache();
935 assert_eq!(resolve(&fs, me(), b"/usr/bin/sh", true).err(), Some(libc::ENOENT));
936 }
937
938 #[test]
939 fn a_binding_below_a_missing_directory_is_reached() {
940 let n = COUNTER.fetch_add(1, Ordering::Relaxed);
942 let base = std::env::temp_dir().join(format!("proot-next-canon-below-{}-{}", std::process::id(), n));
943 std::fs::create_dir_all(base.join("root")).unwrap();
944 std::fs::write(base.join("file"), b"bound").unwrap();
945 let mut c = Config {
946 root: base.join("root").as_os_str().as_encoded_bytes().to_vec(),
947 ..Config::default()
948 };
949 c.bindings.push(Binding {
950 host: base.join("file").as_os_str().as_encoded_bytes().to_vec(),
951 guest: b"/opt/x/y/file".to_vec(),
952 read_only: false,
953 });
954 let fs = Fs::new(&c);
955 for path in [&b"/opt/x/y/file"[..], b"/opt/x/../x/y/./file"] {
956 let r = resolve(&fs, me(), path, true).unwrap();
957 assert_eq!(r.guest, b"/opt/x/y/file".to_vec());
958 assert_eq!(pinned_inode(&r), inode(&base.join("file")));
959 }
960 assert_eq!(resolve(&fs, me(), b"/nope/x", true).err(), Some(libc::ENOENT));
961 }
962
963 #[test]
964 fn classifies_proc_magic_links() {
965 for path in [
966 &b"/proc/12/exe"[..],
967 b"/proc/12/fd/3",
968 b"/proc/12/ns/pid",
969 b"/proc/12/task/13/ns/net",
970 ] {
971 assert!(is_proc_magic_link(path), "{}", String::from_utf8_lossy(path));
972 }
973 for path in [&b"/proc/12/ns"[..], b"/proc/12/status", b"/proc/self/ns/pid"] {
974 assert!(!is_proc_magic_link(path), "{}", String::from_utf8_lossy(path));
975 }
976 }
977}