use std::fs; use std::io; use std::os::unix::prelude::*; use obnth::{open_beneath, LookupFlags}; fn same_file_meta(f1: &fs::File, m2: &fs::Metadata) -> io::Result { let m1 = f1.metadata()?; Ok(m1.ino() == m2.ino() && m1.dev() == m2.dev()) } #[test] fn test_open_beneath_success() { let tmpdir = tempfile::tempdir().unwrap(); let tmpdir = tmpdir.as_ref(); let tmpdir_file = fs::File::open(tmpdir).unwrap(); let tmpdir_fd = tmpdir_file.as_raw_fd(); fs::create_dir(tmpdir.join("a")).unwrap(); fs::File::create(tmpdir.join("a/b")).unwrap(); fs::create_dir(tmpdir.join("a/sub")).unwrap(); macro_rules! check_ok { ($path:expr, $flags:expr, $lookup_flags:expr, $same_path:expr $(,)?) => { let f = open_beneath(tmpdir_fd, $path, $flags, 0o666, $lookup_flags).unwrap(); assert!( same_file_meta(&f, &tmpdir.join($same_path).symlink_metadata().unwrap()).unwrap() ); }; ($path:expr, $flags:expr, $same_path:expr $(,)?) => { check_ok!($path, $flags, LookupFlags::empty(), $same_path) }; } std::os::unix::fs::symlink("a/b", tmpdir.join("c")).unwrap(); std::os::unix::fs::symlink("/a/b", tmpdir.join("d")).unwrap(); std::os::unix::fs::symlink("a/", tmpdir.join("e")).unwrap(); std::os::unix::fs::symlink("/", tmpdir.join("f")).unwrap(); std::os::unix::fs::symlink("./b", tmpdir.join("a/g")).unwrap(); std::os::unix::fs::symlink(".", tmpdir.join("a/h")).unwrap(); std::os::unix::fs::symlink("/escape", tmpdir.join("a/i")).unwrap(); std::os::unix::fs::symlink("/a/", tmpdir.join("j")).unwrap(); std::os::unix::fs::symlink("a", tmpdir.join("k")).unwrap(); check_ok!("a", libc::O_RDONLY | libc::O_DIRECTORY, "a"); check_ok!("a/b", libc::O_RDONLY, "a/b"); check_ok!("a/../a", libc::O_RDONLY, "a"); check_ok!("a/../a/b", libc::O_RDONLY, "a/b"); check_ok!("a/sub/..", libc::O_RDONLY, "a"); check_ok!("a/sub/../..", libc::O_RDONLY, "."); check_ok!("e", libc::O_RDONLY, LookupFlags::IN_ROOT, "a"); check_ok!("a/b", libc::O_WRONLY, "a/b"); check_ok!("c", libc::O_WRONLY, "a/b"); check_ok!("e/", libc::O_RDONLY, "a/"); check_ok!("e/.", libc::O_RDONLY, "a/"); check_ok!("k/", libc::O_RDONLY, "a/"); check_ok!("k/.", libc::O_RDONLY, "a/"); check_ok!(".", libc::O_RDONLY, "."); check_ok!("./", libc::O_RDONLY, "."); check_ok!("/", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("c", libc::O_WRONLY, LookupFlags::IN_ROOT, "a/b"); check_ok!("d", libc::O_WRONLY, LookupFlags::IN_ROOT, "a/b"); check_ok!("j/", libc::O_RDONLY, LookupFlags::IN_ROOT, "a"); check_ok!("f", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("a/g", libc::O_RDONLY, LookupFlags::IN_ROOT, "a/b"); check_ok!("a/h", libc::O_RDONLY, LookupFlags::IN_ROOT, "a"); check_ok!("a/..", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("a/../..", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("/a/../../", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("../..", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("../../", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); check_ok!("/../../", libc::O_RDONLY, LookupFlags::IN_ROOT, "."); #[cfg(any(target_os = "linux", target_os = "android"))] { check_ok!("a", libc::O_PATH, "a"); check_ok!("a", libc::O_PATH | libc::O_NOFOLLOW, "a"); check_ok!("c", libc::O_PATH, "a/b"); check_ok!("c", libc::O_PATH | libc::O_NOFOLLOW, "c"); check_ok!("e/", libc::O_PATH, "a/"); check_ok!("e/.", libc::O_PATH, "a/"); check_ok!("k/", libc::O_PATH, "a/"); check_ok!("k/.", libc::O_PATH, "a/"); } // Trying to open(O_CREAT) a symlink will *not* let the OS follow the symlink and escape. So it // fails with EXDEV (escape detected) rather than succeeding (if running as root and able to // create `/escape`) or failing with EACCES (if not running as root). assert_eq!( open_beneath( tmpdir_fd, "a/i", libc::O_WRONLY | libc::O_CREAT, 0o666, LookupFlags::empty() ) .unwrap_err() .raw_os_error(), Some(libc::EXDEV), ); open_beneath( tmpdir_fd, "a/sub/file", libc::O_WRONLY | libc::O_CREAT, 0o600, LookupFlags::empty(), ) .unwrap(); assert_eq!( tmpdir.join("a/sub/file").metadata().unwrap().mode(), 0o100600 ); } #[test] fn test_open_beneath_error() { let tmpdir = tempfile::tempdir().unwrap(); let tmpdir = tmpdir.as_ref(); let tmpdir_file = fs::File::open(tmpdir).unwrap(); let tmpdir_fd = tmpdir_file.as_raw_fd(); fs::create_dir(tmpdir.join("a")).unwrap(); fs::File::create(tmpdir.join("a/b")).unwrap(); fs::create_dir(tmpdir.join("a/sub")).unwrap(); std::os::unix::fs::symlink("a/b", tmpdir.join("c")).unwrap(); std::os::unix::fs::symlink("/a/b", tmpdir.join("d")).unwrap(); std::os::unix::fs::symlink("a/", tmpdir.join("e")).unwrap(); std::os::unix::fs::symlink("/", tmpdir.join("f")).unwrap(); std::os::unix::fs::symlink("./b", tmpdir.join("a/g")).unwrap(); std::os::unix::fs::symlink(".", tmpdir.join("a/h")).unwrap(); std::os::unix::fs::symlink("loop", tmpdir.join("loop")).unwrap(); assert_eq!( open_beneath( libc::AT_FDCWD, ".", libc::O_RDONLY, 0o666, LookupFlags::empty() ) .unwrap_err() .raw_os_error(), Some(libc::EBADF) ); assert_eq!( open_beneath( fs::File::open(tmpdir.join("a/b")).unwrap().as_raw_fd(), ".", libc::O_RDONLY, 0o666, LookupFlags::empty() ) .unwrap_err() .raw_os_error(), Some(libc::ENOTDIR) ); macro_rules! check_err { ($path:expr, $flags:expr, $lookup_flags:expr, $eno:expr $(,)?) => { assert_eq!( open_beneath(tmpdir_fd, $path, $flags, 0o666, $lookup_flags) .unwrap_err() .raw_os_error(), Some($eno) ); }; ($path:expr, $flags:expr, $eno:expr $(,)?) => { check_err!($path, $flags, LookupFlags::empty(), $eno) }; } check_err!("", libc::O_RDONLY, libc::ENOENT); check_err!("NOEXIST", libc::O_RDONLY | libc::O_DIRECTORY, libc::ENOENT); check_err!("a/b", libc::O_RDONLY | libc::O_DIRECTORY, libc::ENOTDIR); check_err!("a/b/", libc::O_RDONLY, libc::ENOTDIR); check_err!("a/b/.", libc::O_RDONLY, libc::ENOTDIR); check_err!("a/b/./", libc::O_RDONLY, libc::ENOTDIR); check_err!("c", libc::O_RDONLY, LookupFlags::NO_SYMLINKS, libc::ELOOP); check_err!("d", libc::O_RDONLY, LookupFlags::NO_SYMLINKS, libc::ELOOP); check_err!("c", libc::O_RDONLY | libc::O_NOFOLLOW, libc::ELOOP); check_err!("d", libc::O_RDONLY | libc::O_NOFOLLOW, libc::ELOOP); check_err!("e/b", libc::O_RDONLY, LookupFlags::NO_SYMLINKS, libc::ELOOP); check_err!( "loop", libc::O_RDONLY, LookupFlags::NO_SYMLINKS, libc::ELOOP ); check_err!("d", libc::O_RDONLY, libc::EXDEV); check_err!("f", libc::O_RDONLY, libc::EXDEV); check_err!("..", libc::O_RDONLY, libc::EXDEV); check_err!("/", libc::O_RDONLY, libc::EXDEV); check_err!("a/../../a/b", libc::O_RDONLY, libc::EXDEV); check_err!("a/../..", libc::O_RDONLY, libc::EXDEV); check_err!("a/sub/../../..", libc::O_RDONLY, libc::EXDEV); check_err!("a/h", libc::O_WRONLY, libc::EISDIR); check_err!("e", libc::O_WRONLY, libc::EISDIR); } #[test] fn test_open_beneath_execute() { let tmpdir = tempfile::tempdir().unwrap(); let tmpdir = tmpdir.as_ref(); let tmpdir_file = fs::File::open(tmpdir).unwrap(); let tmpdir_fd = tmpdir_file.as_raw_fd(); fs::create_dir(tmpdir.join("a")).unwrap(); fs::File::create(tmpdir.join("a/b")).unwrap(); // 0o100 is "--x------"; i.e. execute permission but not read permission. // That allows us to look at files within the directory, but not list the directory (or open it // without O_PATH or O_SEARCH). std::fs::set_permissions(tmpdir.join("a"), fs::Permissions::from_mode(0o100)).unwrap(); let res = std::panic::catch_unwind(|| { if obnth::has_o_search() { let file = obnth::open_beneath(tmpdir_fd, "a/b", libc::O_RDONLY, 0, LookupFlags::empty()) .unwrap(); assert!(same_file_meta(&file, &fs::metadata(tmpdir.join("a/b")).unwrap()).unwrap()); } if unsafe { libc::geteuid() } != 0 { if !obnth::has_o_search() { assert_eq!( obnth::open_beneath(tmpdir_fd, "a/b", libc::O_RDONLY, 0, LookupFlags::empty()) .unwrap_err() .raw_os_error(), Some(libc::EACCES) ); } assert_eq!( obnth::open_beneath(tmpdir_fd, "a", libc::O_RDONLY, 0, LookupFlags::empty()) .unwrap_err() .raw_os_error(), Some(libc::EACCES) ); } }); // So it can be deleted std::fs::set_permissions(tmpdir.join("a"), fs::Permissions::from_mode(0o755)).unwrap(); res.unwrap(); }