Skip to main content

qk_executor/
landlock.rs

1//! Landlock, through its system calls: what the kernel's `linux/landlock.h`
2//! declares, and no more.
3//!
4//! A rule set allows rights beneath paths, and refuses every right it handles
5//! elsewhere. The calls here make system calls only and allocate nothing, so
6//! a command may make them between fork and exec. [`Rights`] is every
7//! platform's, so what a task is granted can be worked out anywhere.
8
9/// Rights over files beneath a path, as Landlock's access bits.
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct Rights(u64);
12
13impl Rights {
14    pub const NONE: Self = Self(0);
15    pub const EXECUTE: Self = Self(1 << 0);
16    pub const WRITE_FILE: Self = Self(1 << 1);
17    pub const READ_FILE: Self = Self(1 << 2);
18    pub const READ_DIR: Self = Self(1 << 3);
19    /// Every right of ABI 1, from executing to making symlinks.
20    pub const ABI1: Self = Self((1 << 13) - 1);
21    pub const REFER: Self = Self(1 << 13);
22    pub const TRUNCATE: Self = Self(1 << 14);
23    pub const IOCTL_DEV: Self = Self(1 << 15);
24    /// Every right, those of later ABIs too.
25    pub const ALL: Self = Self(u64::MAX);
26    /// Reading files and running them.
27    pub const READ: Self = Self(Self::READ_FILE.0 | Self::EXECUTE.0);
28    /// Reading and running what lies beneath a directory, and listing it.
29    pub const READ_TREE: Self = Self(Self::READ.0 | Self::READ_DIR.0);
30    /// The rights that apply to a file rather than a directory.
31    pub const FILE: Self = Self(
32        Self::EXECUTE.0
33            | Self::WRITE_FILE.0
34            | Self::READ_FILE.0
35            | Self::TRUNCATE.0
36            | Self::IOCTL_DEV.0,
37    );
38
39    pub const fn bits(self) -> u64 {
40        self.0
41    }
42
43    pub const fn is_empty(self) -> bool {
44        self.0 == 0
45    }
46
47    /// Every right the kernel's ABI `abi` handles.
48    pub fn handled(abi: u32) -> Self {
49        let mut rights = Self::ABI1;
50        if abi >= 2 {
51            rights |= Self::REFER;
52        }
53        if abi >= 3 {
54            rights |= Self::TRUNCATE;
55        }
56        if abi >= 5 {
57            rights |= Self::IOCTL_DEV;
58        }
59        rights
60    }
61}
62
63impl std::ops::BitOr for Rights {
64    type Output = Self;
65
66    fn bitor(self, other: Self) -> Self {
67        Self(self.0 | other.0)
68    }
69}
70
71impl std::ops::BitOrAssign for Rights {
72    fn bitor_assign(&mut self, other: Self) {
73        self.0 |= other.0;
74    }
75}
76
77impl std::ops::BitAnd for Rights {
78    type Output = Self;
79
80    fn bitand(self, other: Self) -> Self {
81        Self(self.0 & other.0)
82    }
83}
84
85impl std::ops::BitAndAssign for Rights {
86    fn bitand_assign(&mut self, other: Self) {
87        self.0 &= other.0;
88    }
89}
90
91impl std::ops::Not for Rights {
92    type Output = Self;
93
94    fn not(self) -> Self {
95        Self(!self.0)
96    }
97}
98
99/// How the path a rule allows rights beneath is opened.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
101pub enum Open {
102    /// Following links.
103    Following,
104    /// From the workspace root through no link: one where a link stands, in
105    /// any part of the way down, is left out as one that does not exist.
106    Unlinked,
107    /// As `Unlinked`, and only a directory: a file there, which may be a
108    /// hard link to any file in the workspace, is left out too.
109    Directory,
110}
111
112impl Open {
113    /// The name `relative` is opened by, beneath the workspace root: for a
114    /// directory, with `/.` after it, which only a directory resolves.
115    #[cfg(unix)]
116    pub fn name(self, relative: &std::path::Path) -> Result<std::ffi::CString, std::ffi::NulError> {
117        use std::os::unix::ffi::OsStrExt;
118        let mut name = relative.as_os_str().as_bytes().to_vec();
119        if self == Self::Directory {
120            name.extend_from_slice(b"/.");
121        }
122        std::ffi::CString::new(name)
123    }
124}
125
126#[cfg(target_os = "linux")]
127pub use system::*;
128
129#[cfg(target_os = "linux")]
130mod system {
131    use std::ffi::CStr;
132    use std::io;
133    use std::os::fd::{FromRawFd, OwnedFd, RawFd};
134
135    use super::Rights;
136
137    const CREATE_RULESET_VERSION: u32 = 1 << 0;
138    const RULE_PATH_BENEATH: i32 = 1;
139
140    #[repr(C)]
141    struct RulesetAttr {
142        handled_access_fs: u64,
143    }
144
145    #[repr(C, packed)]
146    struct PathBeneathAttr {
147        allowed_access: u64,
148        parent_fd: i32,
149    }
150
151    /// The Landlock ABI the kernel offers.
152    pub fn abi() -> io::Result<u32> {
153        let version = unsafe {
154            libc::syscall(
155                libc::SYS_landlock_create_ruleset,
156                std::ptr::null::<RulesetAttr>(),
157                0usize,
158                CREATE_RULESET_VERSION,
159            )
160        };
161        if version < 1 {
162            return Err(io::Error::last_os_error());
163        }
164        Ok(version as u32)
165    }
166
167    /// A new rule set handling `handled`.
168    pub fn create_ruleset(handled: Rights) -> io::Result<OwnedFd> {
169        let attr = RulesetAttr {
170            handled_access_fs: handled.bits(),
171        };
172        let fd = unsafe {
173            libc::syscall(
174                libc::SYS_landlock_create_ruleset,
175                &attr as *const RulesetAttr,
176                std::mem::size_of::<RulesetAttr>(),
177                0u32,
178            )
179        };
180        if fd < 0 {
181            return Err(io::Error::last_os_error());
182        }
183        // SAFETY: the call returned a new descriptor, owned here.
184        Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
185    }
186
187    /// Allows `rights` beneath the path opened as `fd`, or on it for a file,
188    /// in `ruleset`, which handles `handled`. What the rule set does not
189    /// handle, or a file cannot have, is left out; a rule left with no right
190    /// is not added.
191    pub fn add_rule(ruleset: RawFd, fd: RawFd, rights: Rights, handled: Rights) -> io::Result<()> {
192        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
193        let directory = unsafe { libc::fstat(fd, &mut stat) } == 0
194            && stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
195        let mut rights = rights & handled;
196        if !directory {
197            rights &= Rights::FILE;
198        }
199        if rights.is_empty() {
200            return Ok(());
201        }
202        let attr = PathBeneathAttr {
203            allowed_access: rights.bits(),
204            parent_fd: fd,
205        };
206        let added = unsafe {
207            libc::syscall(
208                libc::SYS_landlock_add_rule,
209                ruleset,
210                RULE_PATH_BENEATH,
211                &attr as *const PathBeneathAttr,
212                0u32,
213            )
214        };
215        if added != 0 {
216            return Err(io::Error::last_os_error());
217        }
218        Ok(())
219    }
220
221    /// Opens `name`, relative to the directory `directory`, as a path only,
222    /// through no link and without leaving it. Where a link stands in the
223    /// way, the error is `ELOOP` or `EXDEV`.
224    pub fn open_beneath(directory: RawFd, name: &CStr) -> io::Result<OwnedFd> {
225        // SAFETY: zeroed is a valid open_how, whose fields are integers.
226        let mut how: libc::open_how = unsafe { std::mem::zeroed() };
227        how.flags = (libc::O_PATH | libc::O_CLOEXEC) as u64;
228        how.resolve = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_SYMLINKS;
229        let fd = unsafe {
230            libc::syscall(
231                libc::SYS_openat2,
232                directory,
233                name.as_ptr(),
234                &how as *const libc::open_how,
235                std::mem::size_of::<libc::open_how>(),
236            )
237        };
238        if fd < 0 {
239            return Err(io::Error::last_os_error());
240        }
241        // SAFETY: the call returned a new descriptor, owned here.
242        Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
243    }
244
245    /// Restricts the calling thread, and what it runs, to `ruleset`, once it
246    /// can gain no privileges.
247    pub fn restrict_self(ruleset: RawFd) -> io::Result<()> {
248        unsafe {
249            if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
250                || libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
251            {
252                return Err(io::Error::last_os_error());
253            }
254        }
255        Ok(())
256    }
257}