priv_sep

Privilege separation library.
git clone https://git.philomathiclife.com/repos/priv_sep
Log | Files | Refs | README

tests.rs (10549B)


      1 #![cfg_attr(
      2     feature = "std",
      3     expect(
      4         clippy::std_instead_of_core,
      5         reason = "false positive until core::io::ErrorKind is stable"
      6     )
      7 )]
      8 #[cfg(feature = "std")]
      9 use super::PrivDropErr;
     10 use super::{Errno, Gid, SetresidErr, Uid, UserInfo};
     11 #[cfg(all(feature = "std", target_os = "openbsd"))]
     12 use super::{Permissions, Promise, Promises};
     13 #[cfg(feature = "std")]
     14 use core::net::{Ipv6Addr, SocketAddrV6};
     15 #[cfg(all(feature = "std", target_os = "openbsd"))]
     16 extern crate alloc;
     17 #[cfg(all(feature = "std", target_os = "openbsd"))]
     18 use alloc::format;
     19 #[cfg(all(feature = "std", target_os = "openbsd"))]
     20 use std::io::{self, Write as _};
     21 #[cfg(feature = "std")]
     22 use std::{
     23     fs,
     24     io::{Error, ErrorKind},
     25     net::TcpListener,
     26 };
     27 use tokio as _;
     28 #[cfg(feature = "std")]
     29 const README: &str = "README.md";
     30 #[test]
     31 fn chdir() {
     32     assert_eq!(crate::chdir(c"./"), Ok(()));
     33 }
     34 #[cfg(not(target_os = "macos"))]
     35 #[test]
     36 fn setgroups() {
     37     if Uid::geteuid().is_root() {
     38         assert_eq!(crate::setgroups(&[Gid::getegid()]), Ok(()));
     39     } else {
     40         assert_eq!(crate::setgroups(&[Gid::getegid()]), Err(Errno::EPERM));
     41     }
     42 }
     43 #[test]
     44 fn getuid() {
     45     _ = Uid::getuid();
     46 }
     47 #[test]
     48 fn geteuid() {
     49     _ = Uid::geteuid();
     50 }
     51 #[test]
     52 fn getgid() {
     53     _ = Gid::getgid();
     54 }
     55 #[test]
     56 fn getegid() {
     57     _ = Gid::getegid();
     58 }
     59 #[test]
     60 fn setresuid() -> Result<(), Errno> {
     61     Uid::geteuid().setresuid()
     62 }
     63 #[test]
     64 fn setresgid() -> Result<(), Errno> {
     65     Gid::getegid().setresgid()
     66 }
     67 #[test]
     68 fn user_info_new() {
     69     assert_eq!(
     70         UserInfo::new(c"root"),
     71         Ok(Some(UserInfo {
     72             uid: Uid::ROOT,
     73             gid: Gid(0),
     74         }))
     75     );
     76 }
     77 #[test]
     78 fn user_info_setresid() -> Result<(), Errno> {
     79     UserInfo {
     80         uid: Uid::geteuid(),
     81         gid: Gid::getegid(),
     82     }
     83     .setresid()
     84 }
     85 #[test]
     86 fn user_info_setresid_if_valid() -> Result<(), SetresidErr> {
     87     UserInfo {
     88         uid: Uid::geteuid(),
     89         gid: Gid::getegid(),
     90     }
     91     .setresid_if_valid()
     92 }
     93 #[test]
     94 fn user_info_setresid_if_valid_failure() {
     95     assert_eq!(
     96         UserInfo {
     97             uid: Uid::geteuid(),
     98             gid: Gid(u32::MAX),
     99         }
    100         .setresid_if_valid(),
    101         Err(SetresidErr::GidMismatch)
    102     );
    103 }
    104 #[cfg(feature = "std")]
    105 #[test]
    106 #[ignore = "primarily useful for root and interferes with chroot_drop_priv"]
    107 fn priv_drop() {
    108     if Uid::geteuid().is_root() {
    109         assert!(
    110             UserInfo::priv_drop(c"nobody", || {
    111                 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 443, 0, 0))
    112             })
    113             .is_ok_and(|_| true)
    114         );
    115         assert!(
    116             TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 80, 0, 0)).map_or_else(
    117                 |e| matches!(e.kind(), ErrorKind::PermissionDenied),
    118                 |_| false
    119             )
    120         );
    121     } else {
    122         assert!(
    123             UserInfo::priv_drop(c"root", || Ok::<_, Error>(()))
    124                 .map_or_else(|e| matches!(e, PrivDropErr::RootEntry), |()| false)
    125         );
    126     }
    127 }
    128 #[cfg(feature = "std")]
    129 #[test]
    130 #[ignore = "primarily useful for root and interferes with priv_drop"]
    131 fn chroot_drop_priv() {
    132     if Uid::geteuid().is_root() {
    133         assert!(
    134             UserInfo::chroot_then_priv_drop(c"nobody", c"./", false, || {
    135                 TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 987, 0, 0))
    136             })
    137             .is_ok_and(|_| {
    138                 fs::exists(README).is_ok_and(|exists| {
    139                     exists
    140                         && TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 82, 0, 0))
    141                             .map_or_else(
    142                                 |e| matches!(e.kind(), ErrorKind::PermissionDenied),
    143                                 |_| false,
    144                             )
    145                 })
    146             })
    147         );
    148     }
    149 }
    150 #[expect(
    151     clippy::panic,
    152     reason = "reasonable to expect files to not exist that need to in order to test pledge and unveil"
    153 )]
    154 #[expect(
    155     clippy::expect_used,
    156     reason = "kinda reasonable to expect files to exist that mustn't in order to test pledge and unveil"
    157 )]
    158 #[cfg(all(feature = "std", target_os = "openbsd"))]
    159 #[test]
    160 #[ignore = "interferes with other tests"]
    161 fn unveil_pledge() {
    162     const FILE_EXISTS: &str = "/home/zack/foo.txt";
    163     const FILE_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23";
    164     const DIR_NOT_EXISTS: &str = "/home/zack/aadkjfasj3s23/";
    165     _ = fs::metadata(FILE_EXISTS)
    166         .unwrap_or_else(|_e| panic!("{FILE_EXISTS} does not exist, so unit testing cannot occur"));
    167     drop(
    168         fs::metadata(FILE_NOT_EXISTS)
    169             .expect_err(format!("{FILE_NOT_EXISTS} exists, so unit testing cannot occur").as_str()),
    170     );
    171     drop(
    172         fs::metadata(DIR_NOT_EXISTS)
    173             .expect_err(format!("{DIR_NOT_EXISTS} exists, so unit testing cannot occur").as_str()),
    174     );
    175     // This tests that a NULL `promise` does nothing.
    176     assert_eq!(Promises::pledge_none(), Ok(()));
    177     assert!(writeln!(io::stdout()).is_ok_and(|()| true));
    178     assert_eq!(Promises::ALL.pledge(), Ok(()));
    179     // This tests that duplicates are ignored as well as the implementation of PartialEq.
    180     let mut initial_promises = Promises::new([
    181         Promise::Stdio,
    182         Promise::Unveil,
    183         Promise::Rpath,
    184         Promise::Stdio,
    185     ]);
    186     assert_eq!(initial_promises.len(), 3);
    187     assert_eq!(
    188         initial_promises,
    189         Promises::new([Promise::Rpath, Promise::Stdio, Promise::Unveil])
    190     );
    191     // Test retain.
    192     let mut vals = Promises::new([
    193         Promise::Audio,
    194         Promise::Bpf,
    195         Promise::Chown,
    196         Promise::Cpath,
    197         Promise::Error,
    198         Promise::Exec,
    199     ]);
    200     vals.retain([Promise::Error, Promise::Chown]);
    201     assert_eq!(vals.len(), 2);
    202     assert!(vals.contains(Promise::Chown));
    203     assert!(vals.contains(Promise::Error));
    204     assert_eq!(initial_promises.pledge(), Ok(()));
    205     // This tests unveil with no permissions.
    206     assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Ok(()));
    207     assert!(fs::metadata(FILE_EXISTS).map_or_else(
    208         |e| matches!(e.kind(), ErrorKind::PermissionDenied),
    209         |_| false
    210     ));
    211     // This tests unveil with read permissions,
    212     // and one can unveil more permissions (unlike pledge which can only remove promises).
    213     assert_eq!(Permissions::READ.unveil(FILE_EXISTS), Ok(()));
    214     assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true));
    215     // This tests that calls to unveil on missing files don't error.
    216     assert_eq!(Permissions::NONE.unveil(FILE_NOT_EXISTS), Ok(()));
    217     // This tests that calls to unveil on missing directories error.
    218     assert_eq!(Permissions::NONE.unveil(DIR_NOT_EXISTS), Err(Errno::ENOENT));
    219     // This tests that unveil can no longer be called.
    220     assert_eq!(Permissions::unveil_no_more(), Ok(()));
    221     assert_eq!(Permissions::NONE.unveil(FILE_EXISTS), Err(Errno::EPERM));
    222     assert!(fs::metadata(FILE_EXISTS).is_ok_and(|_| true));
    223     // The below tests that Promises can only be removed and not added.
    224     initial_promises.remove_promises([Promise::Unveil]);
    225     assert_eq!(initial_promises.len(), 2);
    226     initial_promises.remove(Promise::Rpath);
    227     assert_eq!(initial_promises.len(), 1);
    228     initial_promises.remove(Promise::Rpath);
    229     assert_eq!(initial_promises.len(), 1);
    230     assert_eq!(initial_promises.pledge(), Ok(()));
    231     assert!(writeln!(io::stdout()).is_ok_and(|()| true));
    232     assert_eq!(Promises::new([Promise::Rpath]).pledge(), Err(Errno::EPERM));
    233     // If the below is uncommented, the program should crash since the above
    234     // call to pledge no longer allows access to the file system.
    235     // drop(fs::metadata(FILE_EXISTS));
    236 }
    237 #[cfg(all(feature = "std", target_os = "openbsd"))]
    238 #[test]
    239 #[ignore = "interferes with other tests when root"]
    240 fn pledge_inet_drop_priv() {
    241     if Uid::geteuid().is_root() {
    242         assert!(
    243             Promises::new_priv_drop(
    244                 c"nobody",
    245                 [Promise::Inet, Promise::Rpath, Promise::Unveil],
    246                 false,
    247                 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 47, 0, 0)),
    248             )
    249             .is_ok_and(|(_, _)| {
    250                 Permissions::unveil_raw(README, c"r").is_ok_and(|()| {
    251                     fs::exists(README).is_ok_and(|exists| {
    252                         Permissions::NONE.unveil(README).is_ok_and(|()| {
    253                             Promises::pledge_raw(c"inet stdio").is_ok_and(|()| {
    254                                 exists
    255                                     && TcpListener::bind(SocketAddrV6::new(
    256                                         Ipv6Addr::LOCALHOST,
    257                                         792,
    258                                         0,
    259                                         0,
    260                                     ))
    261                                     .map_or_else(
    262                                         |e| matches!(e.kind(), ErrorKind::PermissionDenied),
    263                                         |_| false,
    264                                     )
    265                             })
    266                         })
    267                     })
    268                 })
    269             })
    270         );
    271     }
    272 }
    273 #[cfg(all(feature = "std", target_os = "openbsd"))]
    274 #[test]
    275 #[ignore = "interferes with other tests when root"]
    276 fn inet_chroot_priv_pledge() {
    277     if Uid::geteuid().is_root() {
    278         assert!(
    279             Promises::new_chroot_then_priv_drop(
    280                 c"nobody",
    281                 c"./",
    282                 [Promise::Inet, Promise::Rpath, Promise::Unveil],
    283                 false,
    284                 || TcpListener::bind(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 382, 0, 0))
    285             )
    286             .is_ok_and(
    287                 |(_, mut promises)| Permissions::READ.unveil(README).is_ok_and(|()| fs::exists(
    288                     README
    289                 )
    290                 .is_ok_and(
    291                     |exists| Permissions::NONE.unveil(README).is_ok_and(|()| promises
    292                         .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])
    293                         .is_ok_and(|()| exists
    294                             && TcpListener::bind(SocketAddrV6::new(
    295                                 Ipv6Addr::LOCALHOST,
    296                                 588,
    297                                 0,
    298                                 0
    299                             ))
    300                             .map_or_else(
    301                                 |e| matches!(e.kind(), ErrorKind::PermissionDenied),
    302                                 |_| false
    303                             )))
    304                 ))
    305             )
    306         );
    307     }
    308 }