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.68
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, LandlockStatus, 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 let landlock_status = abi.into();
170 println!("Expecting ruleset status {ruleset_status:?}");
171 println!("Expecting Landlock status {landlock_status:?}");
172 assert!(matches!(
173 ret,
174 Ok(RestrictionStatus {
175 ruleset,
176 landlock,
177 no_new_privs: true,
178 }) if ruleset == ruleset_status && landlock == landlock_status
179 ))
180 }
181 } else {
182 // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
183 // access right is provided), or E2BIG (e.g. when there is an unknown field in a
184 // Landlock syscall attribute).
185 let errno = get_errno_from_landlock_status();
186 println!("Expecting error {errno:?}");
187 match ret {
188 Err(
189 ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
190 CreateRulesetError::CreateRulesetCall { ref source },
191 )),
192 ) => {
193 assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
194 match (source.raw_os_error(), errno) {
195 (Some(e1), Some(e2)) => assert_eq!(e1, e2),
196 (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
197 _ => unreachable!(),
198 }
199 }
200 _ => unreachable!(),
201 }
202 }
203 }
204 }
205
206 #[test]
207 fn allow_root_compat() {
208 let abi = ABI::V1;
209
210 check_ruleset_support(
211 abi,
212 Some(abi),
213 move |ruleset: Ruleset| -> _ {
214 Ok(ruleset
215 .handle_access(AccessFs::from_all(abi))?
216 .create()?
217 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
218 .restrict_self()?)
219 },
220 false,
221 );
222 }
223
224 #[test]
225 fn too_much_access_rights_for_a_file() {
226 let abi = ABI::V1;
227
228 check_ruleset_support(
229 abi,
230 Some(abi),
231 move |ruleset: Ruleset| -> _ {
232 Ok(ruleset
233 .handle_access(AccessFs::from_all(abi))?
234 .create()?
235 // Same code as allow_root_compat() but with /etc/passwd instead of /
236 .add_rule(PathBeneath::new(
237 PathFd::new("/etc/passwd")?,
238 // Only allow legitimate access rights on a file.
239 AccessFs::from_file(abi),
240 ))?
241 .restrict_self()?)
242 },
243 false,
244 );
245
246 check_ruleset_support(
247 abi,
248 None,
249 move |ruleset: Ruleset| -> _ {
250 Ok(ruleset
251 .handle_access(AccessFs::from_all(abi))?
252 .create()?
253 // Same code as allow_root_compat() but with /etc/passwd instead of /
254 .add_rule(PathBeneath::new(
255 PathFd::new("/etc/passwd")?,
256 // Tries to allow all access rights on a file.
257 AccessFs::from_all(abi),
258 ))?
259 .restrict_self()?)
260 },
261 false,
262 );
263 }
264
265 #[test]
266 fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
267 let abi = ABI::V1;
268
269 check_ruleset_support(
270 abi,
271 Some(abi),
272 move |ruleset: Ruleset| -> _ {
273 Ok(ruleset
274 .handle_access(AccessFs::from_all(ABI::V1))?
275 .create()?
276 // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
277 .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
278 .restrict_self()?)
279 },
280 false,
281 );
282 }
283
284 #[test]
285 fn allow_root_fragile() {
286 let abi = ABI::V1;
287
288 check_ruleset_support(
289 abi,
290 Some(abi),
291 move |ruleset: Ruleset| -> _ {
292 // Sets default support requirement: abort the whole sandboxing for any Landlock error.
293 Ok(ruleset
294 // Must have at least the execute check…
295 .set_compatibility(CompatLevel::HardRequirement)
296 .handle_access(AccessFs::Execute)?
297 // …and possibly others.
298 .set_compatibility(CompatLevel::BestEffort)
299 .handle_access(AccessFs::from_all(abi))?
300 .create()?
301 .set_no_new_privs(true)
302 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
303 .restrict_self()?)
304 },
305 true,
306 );
307 }
308
309 #[test]
310 fn ruleset_enforced() {
311 let abi = ABI::V1;
312
313 check_ruleset_support(
314 abi,
315 Some(abi),
316 move |ruleset: Ruleset| -> _ {
317 Ok(ruleset
318 // Restricting without rule exceptions is legitimate to forbid a set of actions.
319 .handle_access(AccessFs::Execute)?
320 .create()?
321 .restrict_self()?)
322 },
323 false,
324 );
325 }
326
327 #[test]
328 fn abi_v2_exec_refer() {
329 check_ruleset_support(
330 ABI::V1,
331 Some(ABI::V2),
332 move |ruleset: Ruleset| -> _ {
333 Ok(ruleset
334 .handle_access(AccessFs::Execute)?
335 // AccessFs::Refer is not supported by ABI::V1 (best-effort).
336 .handle_access(AccessFs::Refer)?
337 .create()?
338 .restrict_self()?)
339 },
340 false,
341 );
342 }
343
344 #[test]
345 fn abi_v2_refer_only() {
346 // When no access is handled, do not try to create a ruleset without access.
347 check_ruleset_support(
348 ABI::V2,
349 Some(ABI::V2),
350 move |ruleset: Ruleset| -> _ {
351 Ok(ruleset
352 .handle_access(AccessFs::Refer)?
353 .create()?
354 .restrict_self()?)
355 },
356 false,
357 );
358 }
359
360 #[test]
361 fn abi_v3_truncate() {
362 check_ruleset_support(
363 ABI::V2,
364 Some(ABI::V3),
365 move |ruleset: Ruleset| -> _ {
366 Ok(ruleset
367 .handle_access(AccessFs::Refer)?
368 .handle_access(AccessFs::Truncate)?
369 .create()?
370 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
371 .restrict_self()?)
372 },
373 false,
374 );
375 }
376
377 #[test]
378 fn ruleset_created_try_clone() {
379 check_ruleset_support(
380 ABI::V1,
381 Some(ABI::V1),
382 move |ruleset: Ruleset| -> _ {
383 Ok(ruleset
384 .handle_access(AccessFs::Execute)?
385 .create()?
386 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
387 .try_clone()?
388 .restrict_self()?)
389 },
390 false,
391 );
392 }
393
394 #[test]
395 fn abi_v4_tcp() {
396 check_ruleset_support(
397 ABI::V3,
398 Some(ABI::V4),
399 move |ruleset: Ruleset| -> _ {
400 Ok(ruleset
401 .handle_access(AccessFs::Truncate)?
402 .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
403 .create()?
404 .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
405 .restrict_self()?)
406 },
407 false,
408 );
409 }
410
411 #[test]
412 fn abi_v5_ioctl_dev() {
413 check_ruleset_support(
414 ABI::V4,
415 Some(ABI::V5),
416 move |ruleset: Ruleset| -> _ {
417 Ok(ruleset
418 .handle_access(AccessNet::BindTcp)?
419 .handle_access(AccessFs::IoctlDev)?
420 .create()?
421 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
422 .restrict_self()?)
423 },
424 false,
425 );
426 }
427
428 #[test]
429 fn abi_v6_scope_mix() {
430 check_ruleset_support(
431 ABI::V5,
432 Some(ABI::V6),
433 move |ruleset: Ruleset| -> _ {
434 Ok(ruleset
435 .handle_access(AccessFs::IoctlDev)?
436 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
437 .create()?
438 .restrict_self()?)
439 },
440 false,
441 );
442 }
443
444 #[test]
445 fn abi_v6_scope_only() {
446 check_ruleset_support(
447 ABI::V6,
448 Some(ABI::V6),
449 move |ruleset: Ruleset| -> _ {
450 Ok(ruleset
451 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
452 .create()?
453 .restrict_self()?)
454 },
455 false,
456 );
457 }
458
459 #[test]
460 fn ruleset_created_try_clone_ownedfd() {
461 use std::os::unix::io::{AsRawFd, OwnedFd};
462
463 let abi = ABI::V1;
464 check_ruleset_support(
465 abi,
466 Some(abi),
467 move |ruleset: Ruleset| -> _ {
468 let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
469 let ruleset2 = ruleset1.try_clone().unwrap();
470 let ruleset3 = ruleset2.try_clone().unwrap();
471
472 let some1: Option<OwnedFd> = ruleset1.into();
473 if let Some(fd1) = some1 {
474 assert!(fd1.as_raw_fd() >= 0);
475
476 let some2: Option<OwnedFd> = ruleset2.into();
477 let fd2 = some2.unwrap();
478 assert!(fd2.as_raw_fd() >= 0);
479
480 assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
481 }
482 Ok(ruleset3.restrict_self()?)
483 },
484 false,
485 );
486 }
487}