1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
extern crate libc;

#[cfg(target_os = "openbsd")]
mod openbsd;

use std::ffi::NulError;
use std::{error, fmt};

#[derive(Debug, PartialEq)]
pub enum Error {
    NotSupported,
    Path(NulError),
    Permissions(NulError),
    Os(i32),
}

impl Error {
    pub fn ignore_platform(self) -> Result<(), Self> {
        match self {
            Error::NotSupported => Ok(()),
            x => Err(x),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::NotSupported => write!(f, "platform is not supported"),
            Error::Path(_) => write!(f, "unexpected NUL character in path argument"),
            Error::Permissions(_) => {
                write!(f, "unexpected NUL character in permissions argument")
            }
            Error::Os(errno) => write!(f, "unable to unveil ({})", errno),
        }
    }
}

impl error::Error for Error {}

#[cfg(target_os = "openbsd")]
pub fn unveil(path: impl AsRef<[u8]>, permissions: &str) -> Result<(), Error> {
    use std::ffi::CString;

    let path = path.as_ref();

    // iff path is empty, pass (NULL, NULL) to lock unveil(2). POSIX
    // doesn't allow empty pathnames, and unveil(2) assigns no other
    // meaning to empty path as of OpenBSD 6.8, so this is safe.
    if path.is_empty() {
        return openbsd::unveil(None, None).map_err(Error::Os);
    }

    // empty permissions means "deny all operations on path", which
    // is useful to override an ancestor's allowed operations. there
    // is no meaning for (non-NULL, NULL) as of OpenBSD 6.8.
    let path = CString::new(path).map_err(Error::Path)?;
    let permissions = CString::new(permissions).map_err(Error::Permissions)?;

    openbsd::unveil(Some(&path), Some(&permissions)).map_err(Error::Os)
}

#[cfg(not(target_os = "openbsd"))]
#[allow(unused_variables)]
pub fn unveil(path: impl AsRef<[u8]>, permissions: &str) -> Result<(), Error> {
    Err(Error::NotSupported)
}

#[cfg(test)]
mod tests {
    use *;

    // all tests for OpenBSD targets should live in one test function,
    // because the ones that affect process state need to run in a
    // specific order. with separate test functions, the tests would
    // run in a random order (by default) or in lexicographic order
    // of test names (cargo test -- --test-threads=1).
    #[test]
    #[cfg(target_os = "openbsd")]
    fn test_unveil() {
        assert_eq!(unveil(".", "r"), Ok(()), "simple unveil should succeed");

        // null(4) is a file, and FFh is a valid filename (despite
        // being invalid UTF-8), so this should only throw ENOTDIR
        assert_eq!(
            unveil(b"/dev/null/\xFF", "r").unwrap_err(),
            Error::Os(libc::ENOTDIR),
            "unveil binary path under regular file should throw ENOTDIR",
        );

        assert_eq!(
            unveil("/dev/null", ""),
            Ok(()),
            "unveil child with empty permissions should succeed",
        );

        assert_eq!(unveil("/dev", "r"), Ok(()), "unveil parent should succeed");

        assert_eq!(
            unveil("", ""),
            Ok(()),
            "unveil empty strings should lock successfully",
        );

        assert_eq!(
            unveil(".", "r").unwrap_err(),
            Error::Os(libc::EPERM),
            "simple unveil after locking should throw EPERM",
        );

        use std::fs::File;

        assert!(
            File::open("/dev/zero").is_ok(),
            "opening /dev/zero should succeed",
        );

        assert!(
            File::open("/dev/null").is_err(),
            "opening /dev/null should fail",
        );
    }

    #[test]
    #[cfg(not(target_os = "openbsd"))]
    fn test_unveil_not_supported() {
        assert_eq!(unveil(".", "r").unwrap_err(), Error::NotSupported);
    }
}