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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
//! Landlock, through its system calls: what the kernel's `linux/landlock.h`
//! declares, and no more.
//!
//! A rule set allows rights beneath paths, and refuses every right it handles
//! elsewhere. The calls here make system calls only and allocate nothing, so
//! a command may make them between fork and exec. [`Rights`] is every
//! platform's, so what a task is granted can be worked out anywhere.
/// Rights over files beneath a path, as Landlock's access bits.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Rights(u64);
impl Rights {
pub const NONE: Self = Self(0);
pub const EXECUTE: Self = Self(1 << 0);
pub const WRITE_FILE: Self = Self(1 << 1);
pub const READ_FILE: Self = Self(1 << 2);
pub const READ_DIR: Self = Self(1 << 3);
/// Every right of ABI 1, from executing to making symlinks.
pub const ABI1: Self = Self((1 << 13) - 1);
pub const REFER: Self = Self(1 << 13);
pub const TRUNCATE: Self = Self(1 << 14);
pub const IOCTL_DEV: Self = Self(1 << 15);
/// Every right, those of later ABIs too.
pub const ALL: Self = Self(u64::MAX);
/// Reading files and running them.
pub const READ: Self = Self(Self::READ_FILE.0 | Self::EXECUTE.0);
/// Reading and running what lies beneath a directory, and listing it.
pub const READ_TREE: Self = Self(Self::READ.0 | Self::READ_DIR.0);
/// The rights that apply to a file rather than a directory.
pub const FILE: Self = Self(
Self::EXECUTE.0
| Self::WRITE_FILE.0
| Self::READ_FILE.0
| Self::TRUNCATE.0
| Self::IOCTL_DEV.0,
);
pub const fn bits(self) -> u64 {
self.0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
/// Every right the kernel's ABI `abi` handles.
pub fn handled(abi: u32) -> Self {
let mut rights = Self::ABI1;
if abi >= 2 {
rights |= Self::REFER;
}
if abi >= 3 {
rights |= Self::TRUNCATE;
}
if abi >= 5 {
rights |= Self::IOCTL_DEV;
}
rights
}
}
impl std::ops::BitOr for Rights {
type Output = Self;
fn bitor(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
impl std::ops::BitOrAssign for Rights {
fn bitor_assign(&mut self, other: Self) {
self.0 |= other.0;
}
}
impl std::ops::BitAnd for Rights {
type Output = Self;
fn bitand(self, other: Self) -> Self {
Self(self.0 & other.0)
}
}
impl std::ops::BitAndAssign for Rights {
fn bitand_assign(&mut self, other: Self) {
self.0 &= other.0;
}
}
impl std::ops::Not for Rights {
type Output = Self;
fn not(self) -> Self {
Self(!self.0)
}
}
/// How the path a rule allows rights beneath is opened.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Open {
/// Following links.
Following,
/// From the workspace root through no link: one where a link stands, in
/// any part of the way down, is left out as one that does not exist.
Unlinked,
/// As `Unlinked`, and only a directory: a file there, which may be a
/// hard link to any file in the workspace, is left out too.
Directory,
}
impl Open {
/// The name `relative` is opened by, beneath the workspace root: for a
/// directory, with `/.` after it, which only a directory resolves.
#[cfg(unix)]
pub fn name(self, relative: &std::path::Path) -> Result<std::ffi::CString, std::ffi::NulError> {
use std::os::unix::ffi::OsStrExt;
let mut name = relative.as_os_str().as_bytes().to_vec();
if self == Self::Directory {
name.extend_from_slice(b"/.");
}
std::ffi::CString::new(name)
}
}
#[cfg(target_os = "linux")]
pub use system::*;
#[cfg(target_os = "linux")]
mod system {
use std::ffi::CStr;
use std::io;
use std::os::fd::{FromRawFd, OwnedFd, RawFd};
use super::Rights;
const CREATE_RULESET_VERSION: u32 = 1 << 0;
const RULE_PATH_BENEATH: i32 = 1;
#[repr(C)]
struct RulesetAttr {
handled_access_fs: u64,
}
#[repr(C, packed)]
struct PathBeneathAttr {
allowed_access: u64,
parent_fd: i32,
}
/// The Landlock ABI the kernel offers.
pub fn abi() -> io::Result<u32> {
let version = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<RulesetAttr>(),
0usize,
CREATE_RULESET_VERSION,
)
};
if version < 1 {
return Err(io::Error::last_os_error());
}
Ok(version as u32)
}
/// A new rule set handling `handled`.
pub fn create_ruleset(handled: Rights) -> io::Result<OwnedFd> {
let attr = RulesetAttr {
handled_access_fs: handled.bits(),
};
let fd = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
&attr as *const RulesetAttr,
std::mem::size_of::<RulesetAttr>(),
0u32,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: the call returned a new descriptor, owned here.
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
/// Allows `rights` beneath the path opened as `fd`, or on it for a file,
/// in `ruleset`, which handles `handled`. What the rule set does not
/// handle, or a file cannot have, is left out; a rule left with no right
/// is not added.
pub fn add_rule(ruleset: RawFd, fd: RawFd, rights: Rights, handled: Rights) -> io::Result<()> {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
let directory = unsafe { libc::fstat(fd, &mut stat) } == 0
&& stat.st_mode & libc::S_IFMT == libc::S_IFDIR;
let mut rights = rights & handled;
if !directory {
rights &= Rights::FILE;
}
if rights.is_empty() {
return Ok(());
}
let attr = PathBeneathAttr {
allowed_access: rights.bits(),
parent_fd: fd,
};
let added = unsafe {
libc::syscall(
libc::SYS_landlock_add_rule,
ruleset,
RULE_PATH_BENEATH,
&attr as *const PathBeneathAttr,
0u32,
)
};
if added != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
/// Opens `name`, relative to the directory `directory`, as a path only,
/// through no link and without leaving it. Where a link stands in the
/// way, the error is `ELOOP` or `EXDEV`.
pub fn open_beneath(directory: RawFd, name: &CStr) -> io::Result<OwnedFd> {
// SAFETY: zeroed is a valid open_how, whose fields are integers.
let mut how: libc::open_how = unsafe { std::mem::zeroed() };
how.flags = (libc::O_PATH | libc::O_CLOEXEC) as u64;
how.resolve = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_SYMLINKS;
let fd = unsafe {
libc::syscall(
libc::SYS_openat2,
directory,
name.as_ptr(),
&how as *const libc::open_how,
std::mem::size_of::<libc::open_how>(),
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
// SAFETY: the call returned a new descriptor, owned here.
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
/// Restricts the calling thread, and what it runs, to `ruleset`, once it
/// can gain no privileges.
pub fn restrict_self(ruleset: RawFd) -> io::Result<()> {
unsafe {
if libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0
|| libc::syscall(libc::SYS_landlock_restrict_self, ruleset, 0u32) != 0
{
return Err(io::Error::last_os_error());
}
}
Ok(())
}
}