landlock/lib.rs
1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3//! Landlock is a security feature available since Linux 5.13.
4//! The goal is to enable to restrict ambient rights
5//! (e.g., global filesystem access)
6//! for a set of processes by creating safe security sandboxes as new security layers
7//! in addition to the existing system-wide access-controls.
8//! This kind of sandbox is expected to help mitigate the security impact of bugs,
9//! unexpected or malicious behaviors in applications.
10//! Landlock empowers any process, including unprivileged ones, to securely restrict themselves.
11//! More information about Landlock can be found in the [official website](https://landlock.io).
12//!
13//! This crate provides a safe abstraction for the Landlock system calls, along with some helpers.
14//!
15//! Minimum Supported Rust Version (MSRV): 1.63
16//!
17//! # Use cases
18//!
19//! This crate is especially useful to protect users' data by sandboxing:
20//! * trusted applications dealing with potentially malicious data
21//! (e.g., complex file format, network request) that could exploit security vulnerabilities;
22//! * sandbox managers, container runtimes or shells launching untrusted applications.
23//!
24//! # Examples
25//!
26//! A simple example can be found with the [`path_beneath_rules()`] helper.
27//! More complex examples can be found with the [`Ruleset` documentation](Ruleset)
28//! and the [sandboxer example](https://github.com/landlock-lsm/rust-landlock/blob/master/examples/sandboxer.rs).
29//!
30//! # Current limitations
31//!
32//! This crate exposes the Landlock features available as of Linux 5.19
33//! and then inherits some [kernel limitations](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#current-limitations)
34//! that will be addressed with future kernel releases
35//! (e.g., arbitrary mounts are always denied).
36//!
37//! # Compatibility
38//!
39//! Types defined in this crate are designed to enable the strictest Landlock configuration
40//! for the given kernel on which the program runs.
41//! In the default [best-effort](CompatLevel::BestEffort) mode,
42//! [`Ruleset`] will determine compatibility
43//! with the intersection of the currently running kernel's features
44//! and those required by the caller.
45//! This way, callers can distinguish between
46//! Landlock compatibility issues inherent to the current system
47//! (e.g., file names that don't exist)
48//! and misconfiguration that should be fixed in the program
49//! (e.g., empty or inconsistent access rights).
50//! [`RulesetError`] identifies such kind of errors.
51//!
52//! With [`set_compatibility(CompatLevel::BestEffort)`](Compatible::set_compatibility),
53//! users of the crate may mark Landlock features that are deemed required
54//! and other features that may be downgraded to use lower security on systems
55//! where they can't be enforced.
56//! It is discouraged to compare the system's provided [Landlock ABI](ABI) version directly,
57//! as it is difficult to track detailed ABI differences
58//! which are handled thanks to the [`Compatible`] trait.
59//!
60//! To make it easier to migrate to a new version of this library,
61//! we use the builder pattern
62//! and designed objects to require the minimal set of method arguments.
63//! Most `enum` are marked as `non_exhaustive` to enable backward-compatible evolutions.
64//!
65//! ## Test strategy
66//!
67//! Developers should test their sandboxed applications
68//! with a kernel that supports all requested Landlock features
69//! and check that [`RulesetCreated::restrict_self()`] returns a status matching
70//! [`Ok(RestrictionStatus { ruleset: RulesetStatus::FullyEnforced, no_new_privs: true, })`](RestrictionStatus)
71//! to make sure everything works as expected in an enforced sandbox.
72//! Alternatively, using [`set_compatibility(CompatLevel::HardRequirement)`](Compatible::set_compatibility)
73//! will immediately inform about unsupported Landlock features.
74//! These configurations should only depend on the test environment
75//! (e.g. [by checking an environment variable](https://github.com/landlock-lsm/rust-landlock/search?q=LANDLOCK_CRATE_TEST_ABI)).
76//! However, applications should only check that no error is returned (i.e. `Ok(_)`)
77//! and optionally log and inform users that the application is not fully sandboxed
78//! because of missing features from the running kernel.
79
80#[cfg(test)]
81#[macro_use]
82extern crate lazy_static;
83
84pub use access::{Access, HandledAccess};
85pub use compat::{CompatLevel, Compatible, ABI};
86pub use enumflags2::{make_bitflags, BitFlags};
87pub use errors::{
88 AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
89 HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
90 RulesetError, ScopeError,
91};
92pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
93pub use net::{AccessNet, NetPort};
94pub use ruleset::{
95 RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
96 RulesetStatus,
97};
98pub use scope::Scope;
99
100use access::PrivateHandledAccess;
101use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
102use ruleset::PrivateRule;
103
104#[cfg(test)]
105use compat::{can_emulate, get_errno_from_landlock_status};
106#[cfg(test)]
107use errors::TestRulesetError;
108#[cfg(test)]
109use strum::IntoEnumIterator;
110
111mod access;
112mod compat;
113mod errors;
114mod fs;
115mod net;
116mod ruleset;
117mod scope;
118mod uapi;
119
120// Makes sure private traits cannot be implemented outside of this crate.
121mod private {
122 pub trait Sealed {}
123
124 impl Sealed for crate::AccessFs {}
125 impl Sealed for crate::AccessNet {}
126 impl Sealed for crate::Scope {}
127}
128
129#[cfg(test)]
130mod tests {
131 use crate::*;
132
133 // Emulate old kernel supports.
134 fn check_ruleset_support<F>(
135 partial: ABI,
136 full: Option<ABI>,
137 check: F,
138 error_if_abi_lt_partial: bool,
139 ) where
140 F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
141 {
142 // If there is no partial support, it means that `full == partial`.
143 assert!(partial <= full.unwrap_or(partial));
144 for abi in ABI::iter() {
145 // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
146 let ret = std::thread::spawn(move || check(Ruleset::from(abi)))
147 .join()
148 .unwrap();
149
150 // Useful for failed tests and with cargo test -- --show-output
151 println!("Checking ABI {abi:?}: received {ret:#?}");
152 if can_emulate(abi, partial, full) {
153 if abi < partial && error_if_abi_lt_partial {
154 // TODO: Check exact error type; this may require better error types.
155 assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
156 } else {
157 let full_support = if let Some(full_inner) = full {
158 abi >= full_inner
159 } else {
160 false
161 };
162 let ruleset_status = if full_support {
163 RulesetStatus::FullyEnforced
164 } else if abi >= partial {
165 RulesetStatus::PartiallyEnforced
166 } else {
167 RulesetStatus::NotEnforced
168 };
169 println!("Expecting ruleset status {ruleset_status:?}");
170 assert!(matches!(
171 ret,
172 Ok(RestrictionStatus {
173 ruleset,
174 no_new_privs: true,
175 }) if ruleset == ruleset_status
176 ))
177 }
178 } else {
179 // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
180 // access right is provided), or E2BIG (e.g. when there is an unknown field in a
181 // Landlock syscall attribute).
182 let errno = get_errno_from_landlock_status();
183 println!("Expecting error {errno:?}");
184 match ret {
185 Err(
186 ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
187 CreateRulesetError::CreateRulesetCall { ref source },
188 )),
189 ) => {
190 assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
191 match (source.raw_os_error(), errno) {
192 (Some(e1), Some(e2)) => assert_eq!(e1, e2),
193 (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
194 _ => unreachable!(),
195 }
196 }
197 _ => unreachable!(),
198 }
199 }
200 }
201 }
202
203 #[test]
204 fn allow_root_compat() {
205 let abi = ABI::V1;
206
207 check_ruleset_support(
208 abi,
209 Some(abi),
210 move |ruleset: Ruleset| -> _ {
211 Ok(ruleset
212 .handle_access(AccessFs::from_all(abi))?
213 .create()?
214 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
215 .restrict_self()?)
216 },
217 false,
218 );
219 }
220
221 #[test]
222 fn too_much_access_rights_for_a_file() {
223 let abi = ABI::V1;
224
225 check_ruleset_support(
226 abi,
227 Some(abi),
228 move |ruleset: Ruleset| -> _ {
229 Ok(ruleset
230 .handle_access(AccessFs::from_all(abi))?
231 .create()?
232 // Same code as allow_root_compat() but with /etc/passwd instead of /
233 .add_rule(PathBeneath::new(
234 PathFd::new("/etc/passwd")?,
235 // Only allow legitimate access rights on a file.
236 AccessFs::from_file(abi),
237 ))?
238 .restrict_self()?)
239 },
240 false,
241 );
242
243 check_ruleset_support(
244 abi,
245 None,
246 move |ruleset: Ruleset| -> _ {
247 Ok(ruleset
248 .handle_access(AccessFs::from_all(abi))?
249 .create()?
250 // Same code as allow_root_compat() but with /etc/passwd instead of /
251 .add_rule(PathBeneath::new(
252 PathFd::new("/etc/passwd")?,
253 // Tries to allow all access rights on a file.
254 AccessFs::from_all(abi),
255 ))?
256 .restrict_self()?)
257 },
258 false,
259 );
260 }
261
262 #[test]
263 fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
264 let abi = ABI::V1;
265
266 check_ruleset_support(
267 abi,
268 Some(abi),
269 move |ruleset: Ruleset| -> _ {
270 Ok(ruleset
271 .handle_access(AccessFs::from_all(ABI::V1))?
272 .create()?
273 // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
274 .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
275 .restrict_self()?)
276 },
277 false,
278 );
279 }
280
281 #[test]
282 fn allow_root_fragile() {
283 let abi = ABI::V1;
284
285 check_ruleset_support(
286 abi,
287 Some(abi),
288 move |ruleset: Ruleset| -> _ {
289 // Sets default support requirement: abort the whole sandboxing for any Landlock error.
290 Ok(ruleset
291 // Must have at least the execute check…
292 .set_compatibility(CompatLevel::HardRequirement)
293 .handle_access(AccessFs::Execute)?
294 // …and possibly others.
295 .set_compatibility(CompatLevel::BestEffort)
296 .handle_access(AccessFs::from_all(abi))?
297 .create()?
298 .set_no_new_privs(true)
299 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
300 .restrict_self()?)
301 },
302 true,
303 );
304 }
305
306 #[test]
307 fn ruleset_enforced() {
308 let abi = ABI::V1;
309
310 check_ruleset_support(
311 abi,
312 Some(abi),
313 move |ruleset: Ruleset| -> _ {
314 Ok(ruleset
315 // Restricting without rule exceptions is legitimate to forbid a set of actions.
316 .handle_access(AccessFs::Execute)?
317 .create()?
318 .restrict_self()?)
319 },
320 false,
321 );
322 }
323
324 #[test]
325 fn abi_v2_exec_refer() {
326 check_ruleset_support(
327 ABI::V1,
328 Some(ABI::V2),
329 move |ruleset: Ruleset| -> _ {
330 Ok(ruleset
331 .handle_access(AccessFs::Execute)?
332 // AccessFs::Refer is not supported by ABI::V1 (best-effort).
333 .handle_access(AccessFs::Refer)?
334 .create()?
335 .restrict_self()?)
336 },
337 false,
338 );
339 }
340
341 #[test]
342 fn abi_v2_refer_only() {
343 // When no access is handled, do not try to create a ruleset without access.
344 check_ruleset_support(
345 ABI::V2,
346 Some(ABI::V2),
347 move |ruleset: Ruleset| -> _ {
348 Ok(ruleset
349 .handle_access(AccessFs::Refer)?
350 .create()?
351 .restrict_self()?)
352 },
353 false,
354 );
355 }
356
357 #[test]
358 fn abi_v3_truncate() {
359 check_ruleset_support(
360 ABI::V2,
361 Some(ABI::V3),
362 move |ruleset: Ruleset| -> _ {
363 Ok(ruleset
364 .handle_access(AccessFs::Refer)?
365 .handle_access(AccessFs::Truncate)?
366 .create()?
367 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
368 .restrict_self()?)
369 },
370 false,
371 );
372 }
373
374 #[test]
375 fn ruleset_created_try_clone() {
376 check_ruleset_support(
377 ABI::V1,
378 Some(ABI::V1),
379 move |ruleset: Ruleset| -> _ {
380 Ok(ruleset
381 .handle_access(AccessFs::Execute)?
382 .create()?
383 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
384 .try_clone()?
385 .restrict_self()?)
386 },
387 false,
388 );
389 }
390
391 #[test]
392 fn abi_v4_tcp() {
393 check_ruleset_support(
394 ABI::V3,
395 Some(ABI::V4),
396 move |ruleset: Ruleset| -> _ {
397 Ok(ruleset
398 .handle_access(AccessFs::Truncate)?
399 .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
400 .create()?
401 .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
402 .restrict_self()?)
403 },
404 false,
405 );
406 }
407
408 #[test]
409 fn abi_v5_ioctl_dev() {
410 check_ruleset_support(
411 ABI::V4,
412 Some(ABI::V5),
413 move |ruleset: Ruleset| -> _ {
414 Ok(ruleset
415 .handle_access(AccessNet::BindTcp)?
416 .handle_access(AccessFs::IoctlDev)?
417 .create()?
418 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
419 .restrict_self()?)
420 },
421 false,
422 );
423 }
424
425 #[test]
426 fn abi_v6_scope_mix() {
427 check_ruleset_support(
428 ABI::V5,
429 Some(ABI::V6),
430 move |ruleset: Ruleset| -> _ {
431 Ok(ruleset
432 .handle_access(AccessFs::IoctlDev)?
433 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
434 .create()?
435 .restrict_self()?)
436 },
437 false,
438 );
439 }
440
441 #[test]
442 fn abi_v6_scope_only() {
443 check_ruleset_support(
444 ABI::V6,
445 Some(ABI::V6),
446 move |ruleset: Ruleset| -> _ {
447 Ok(ruleset
448 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
449 .create()?
450 .restrict_self()?)
451 },
452 false,
453 );
454 }
455
456 #[test]
457 fn ruleset_created_try_clone_ownedfd() {
458 use std::os::unix::io::{AsRawFd, OwnedFd};
459
460 let abi = ABI::V1;
461 check_ruleset_support(
462 abi,
463 Some(abi),
464 move |ruleset: Ruleset| -> _ {
465 let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
466 let ruleset2 = ruleset1.try_clone().unwrap();
467 let ruleset3 = ruleset2.try_clone().unwrap();
468
469 let some1: Option<OwnedFd> = ruleset1.into();
470 if let Some(fd1) = some1 {
471 assert!(fd1.as_raw_fd() >= 0);
472
473 let some2: Option<OwnedFd> = ruleset2.into();
474 let fd2 = some2.unwrap();
475 assert!(fd2.as_raw_fd() >= 0);
476
477 assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
478 }
479 Ok(ruleset3.restrict_self()?)
480 },
481 false,
482 );
483 }
484}