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.71
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 6.15 (Landlock [ABI v7](ABI::V7))
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//! ## Audit logging
81//!
82//! Landlock ABI v7 adds control over audit logging through boolean setters, especially
83//! [`log_new_exec()`](RulesetCreatedAttr::log_new_exec)) which is useful (but noisy) for sandboxer
84//! tools.
85
86#[cfg(test)]
87#[macro_use]
88extern crate lazy_static;
89
90pub use access::{Access, HandledAccess};
91pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
92pub use enumflags2::{make_bitflags, BitFlags};
93pub use errata::Erratum;
94pub use errors::{
95 AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
96 HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError,
97 RulesetError, ScopeError, SyscallFlagError,
98};
99pub use flags::{RestrictSelfFlag, SyscallFlag};
100pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd};
101pub use net::{AccessNet, NetPort};
102pub use restrict_self::{RestrictSelf, RestrictSelfAttr, RestrictSelfStatus};
103pub use ruleset::{
104 RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr,
105 RulesetStatus,
106};
107pub use scope::Scope;
108
109use access::PrivateHandledAccess;
110use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat};
111use ruleset::PrivateRule;
112
113#[cfg(test)]
114use compat::{can_emulate, get_errno_from_landlock_status};
115#[cfg(test)]
116use errors::TestRulesetError;
117#[cfg(test)]
118use strum::IntoEnumIterator;
119
120mod access;
121mod compat;
122mod errata;
123mod errors;
124mod flags;
125mod fs;
126mod net;
127mod prctl;
128mod restrict_self;
129mod ruleset;
130mod scope;
131mod uapi;
132
133// Makes sure private traits cannot be implemented outside of this crate.
134mod private {
135 pub trait Sealed {}
136
137 impl Sealed for crate::AccessFs {}
138 impl Sealed for crate::AccessNet {}
139 impl Sealed for crate::Scope {}
140 impl Sealed for crate::RestrictSelfFlag {}
141}
142
143#[cfg(test)]
144mod tests {
145 use crate::*;
146
147 // These integration tests exercise the full builder-to-syscall path via
148 // check_ruleset_support(). Other tests in compat.rs and errata.rs make
149 // read-only kernel queries (LandlockStatus::current(), Erratum::current())
150 // but do not create rulesets or restrict threads. All remaining tests use
151 // Ruleset::from(ABI) to exercise the builder logic and compatibility
152 // engine without kernel interaction.
153
154 // Emulate old kernel supports. Iterates each ABI variant, mocks the
155 // builder via B::from(abi), runs the closure on a dedicated thread, and
156 // dispatches to the caller's assertion closures based on whether the
157 // mocked ABI is emulatable on the running kernel.
158 fn check_support<B, S, F, OkFn, ErrFn>(
159 partial: ABI,
160 full: Option<ABI>,
161 check: F,
162 assert_ok: OkFn,
163 assert_err: ErrFn,
164 ) where
165 B: From<ABI> + Send + 'static,
166 F: Fn(B) -> Result<S, TestRulesetError> + Send + Copy + 'static,
167 S: std::fmt::Debug + Send + 'static,
168 OkFn: Fn(ABI, Result<S, TestRulesetError>),
169 ErrFn: Fn(Result<S, TestRulesetError>),
170 {
171 // If there is no partial support, it means that `full == partial`.
172 assert!(partial <= full.unwrap_or(partial));
173 for abi in ABI::iter() {
174 // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests.
175 let ret = std::thread::spawn(move || check(B::from(abi)))
176 .join()
177 .unwrap();
178
179 // Useful for failed tests and with cargo test -- --show-output
180 println!("Checking ABI {abi:?}: received {ret:#?}");
181 if can_emulate(abi, partial, full) {
182 assert_ok(abi, ret);
183 } else {
184 assert_err(ret);
185 }
186 }
187 }
188
189 fn check_ruleset_support<F>(
190 partial: ABI,
191 full: Option<ABI>,
192 check: F,
193 error_if_abi_lt_partial: bool,
194 ) where
195 F: Fn(Ruleset) -> Result<RestrictionStatus, TestRulesetError> + Send + Copy + 'static,
196 {
197 check_support(
198 partial,
199 full,
200 check,
201 |abi, ret| {
202 if abi < partial && error_if_abi_lt_partial {
203 // TODO: Check exact error type; this may require better error types.
204 assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
205 } else {
206 let full_support = if let Some(full_inner) = full {
207 abi >= full_inner
208 } else {
209 false
210 };
211 let ruleset_status = if full_support {
212 RulesetStatus::FullyEnforced
213 } else if abi >= partial {
214 RulesetStatus::PartiallyEnforced
215 } else {
216 RulesetStatus::NotEnforced
217 };
218 let landlock_status = abi.into();
219 println!("Expecting ruleset status {ruleset_status:?}");
220 println!("Expecting Landlock status {landlock_status:?}");
221 assert!(matches!(
222 ret,
223 Ok(RestrictionStatus {
224 ruleset,
225 landlock,
226 no_new_privs: true,
227 ..
228 }) if ruleset == ruleset_status && landlock == landlock_status
229 ))
230 }
231 },
232 |ret| {
233 // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown
234 // access right is provided), or E2BIG (e.g. when there is an unknown field in a
235 // Landlock syscall attribute).
236 let errno = get_errno_from_landlock_status();
237 println!("Expecting error {errno:?}");
238 match ret {
239 Err(
240 ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset(
241 CreateRulesetError::CreateRulesetCall { ref source },
242 )),
243 ) => {
244 assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
245 match (source.raw_os_error(), errno) {
246 (Some(e1), Some(e2)) => assert_eq!(e1, e2),
247 (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
248 _ => unreachable!(),
249 }
250 }
251 // restrict_self flags may be rejected by the kernel with EINVAL
252 // when the mock ABI is higher than the running kernel's ABI.
253 Err(TestRulesetError::Ruleset(RulesetError::RestrictSelf(
254 RestrictSelfError::RestrictSelfCall { ref source },
255 ))) => {
256 assert_eq!(source.raw_os_error(), Some(libc::EINVAL));
257 }
258 _ => unreachable!(),
259 }
260 },
261 );
262 }
263
264 // Emulate old kernel supports for the domain-less RestrictSelf builder.
265 //
266 // Unlike check_ruleset_support, RestrictSelf does not create a Landlock domain, so there is no
267 // ruleset enforcement status to assert. We verify the kernel probe (status.landlock) and
268 // propagate any syscall error. RestrictSelf::apply() enforces PR_SET_NO_NEW_PRIVS by default.
269 fn check_restrict_self_support<F>(partial: ABI, full: Option<ABI>, check: F)
270 where
271 F: Fn(RestrictSelf) -> Result<RestrictSelfStatus, TestRulesetError> + Send + Copy + 'static,
272 {
273 check_support(
274 partial,
275 full,
276 check,
277 |abi, ret| {
278 let landlock_status: LandlockStatus = abi.into();
279 println!("Expecting Landlock status {landlock_status:?}");
280 assert!(matches!(
281 ret,
282 Ok(RestrictSelfStatus { landlock, .. }) if landlock == landlock_status
283 ));
284 },
285 |ret| {
286 // The errno value should be ENOSYS, EOPNOTSUPP, or EINVAL (e.g. when actual_flags
287 // carries bits unknown to the running kernel). Unlike check_ruleset_support,
288 // landlock_restrict_self() is the first syscall here, so ENOSYS is possible when
289 // Landlock is unavailable.
290 let errno = get_errno_from_landlock_status();
291 println!("Expecting error {errno:?}");
292 match ret {
293 Err(
294 ref error @ TestRulesetError::Ruleset(RulesetError::RestrictSelf(
295 RestrictSelfError::RestrictSelfCall { ref source },
296 )),
297 ) => {
298 assert_eq!(source.raw_os_error(), Some(*Errno::from(error)));
299 match (source.raw_os_error(), errno) {
300 (Some(e1), Some(e2)) => assert_eq!(e1, e2),
301 (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)),
302 _ => unreachable!(),
303 }
304 }
305 _ => unreachable!(),
306 }
307 },
308 );
309 }
310
311 #[test]
312 fn allow_root_compat() {
313 let abi = ABI::V1;
314
315 check_ruleset_support(
316 abi,
317 Some(abi),
318 move |ruleset: Ruleset| -> _ {
319 Ok(ruleset
320 .handle_access(AccessFs::from_all(abi))?
321 .create()?
322 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
323 .restrict_self()?)
324 },
325 false,
326 );
327 }
328
329 #[test]
330 fn too_much_access_rights_for_a_file() {
331 let abi = ABI::V1;
332
333 check_ruleset_support(
334 abi,
335 Some(abi),
336 move |ruleset: Ruleset| -> _ {
337 Ok(ruleset
338 .handle_access(AccessFs::from_all(abi))?
339 .create()?
340 // Same code as allow_root_compat() but with /etc/passwd instead of /
341 .add_rule(PathBeneath::new(
342 PathFd::new("/etc/passwd")?,
343 // Only allow legitimate access rights on a file.
344 AccessFs::from_file(abi),
345 ))?
346 .restrict_self()?)
347 },
348 false,
349 );
350
351 check_ruleset_support(
352 abi,
353 None,
354 move |ruleset: Ruleset| -> _ {
355 Ok(ruleset
356 .handle_access(AccessFs::from_all(abi))?
357 .create()?
358 // Same code as allow_root_compat() but with /etc/passwd instead of /
359 .add_rule(PathBeneath::new(
360 PathFd::new("/etc/passwd")?,
361 // Tries to allow all access rights on a file.
362 AccessFs::from_all(abi),
363 ))?
364 .restrict_self()?)
365 },
366 false,
367 );
368 }
369
370 #[test]
371 fn path_beneath_rules_with_too_much_access_rights_for_a_file() {
372 let abi = ABI::V1;
373
374 check_ruleset_support(
375 abi,
376 Some(abi),
377 move |ruleset: Ruleset| -> _ {
378 Ok(ruleset
379 .handle_access(AccessFs::from_all(ABI::V1))?
380 .create()?
381 // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules()
382 .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))?
383 .restrict_self()?)
384 },
385 false,
386 );
387 }
388
389 #[test]
390 fn allow_root_fragile() {
391 let abi = ABI::V1;
392
393 check_ruleset_support(
394 abi,
395 Some(abi),
396 move |ruleset: Ruleset| -> _ {
397 // Sets default support requirement: abort the whole sandboxing for any Landlock error.
398 Ok(ruleset
399 // Must have at least the execute check…
400 .set_compatibility(CompatLevel::HardRequirement)
401 .handle_access(AccessFs::Execute)?
402 // …and possibly others.
403 .set_compatibility(CompatLevel::BestEffort)
404 .handle_access(AccessFs::from_all(abi))?
405 .create()?
406 .no_new_privs(true)
407 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))?
408 .restrict_self()?)
409 },
410 true,
411 );
412 }
413
414 #[test]
415 fn ruleset_enforced() {
416 let abi = ABI::V1;
417
418 check_ruleset_support(
419 abi,
420 Some(abi),
421 move |ruleset: Ruleset| -> _ {
422 Ok(ruleset
423 // Restricting without rule exceptions is legitimate to forbid a set of actions.
424 .handle_access(AccessFs::Execute)?
425 .create()?
426 .restrict_self()?)
427 },
428 false,
429 );
430 }
431
432 #[test]
433 fn abi_v2_exec_refer() {
434 check_ruleset_support(
435 ABI::V1,
436 Some(ABI::V2),
437 move |ruleset: Ruleset| -> _ {
438 Ok(ruleset
439 .handle_access(AccessFs::Execute)?
440 // AccessFs::Refer is not supported by ABI::V1 (best-effort).
441 .handle_access(AccessFs::Refer)?
442 .create()?
443 .restrict_self()?)
444 },
445 false,
446 );
447 }
448
449 #[test]
450 fn abi_v2_refer_only() {
451 // When no access is handled, do not try to create a ruleset without access.
452 check_ruleset_support(
453 ABI::V2,
454 Some(ABI::V2),
455 move |ruleset: Ruleset| -> _ {
456 Ok(ruleset
457 .handle_access(AccessFs::Refer)?
458 .create()?
459 .restrict_self()?)
460 },
461 false,
462 );
463 }
464
465 #[test]
466 fn abi_v3_truncate() {
467 check_ruleset_support(
468 ABI::V2,
469 Some(ABI::V3),
470 move |ruleset: Ruleset| -> _ {
471 Ok(ruleset
472 .handle_access(AccessFs::Refer)?
473 .handle_access(AccessFs::Truncate)?
474 .create()?
475 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))?
476 .restrict_self()?)
477 },
478 false,
479 );
480 }
481
482 #[test]
483 fn ruleset_created_try_clone() {
484 check_ruleset_support(
485 ABI::V1,
486 Some(ABI::V1),
487 move |ruleset: Ruleset| -> _ {
488 Ok(ruleset
489 .handle_access(AccessFs::Execute)?
490 .create()?
491 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))?
492 .try_clone()?
493 .restrict_self()?)
494 },
495 false,
496 );
497 }
498
499 #[test]
500 fn abi_v4_tcp() {
501 check_ruleset_support(
502 ABI::V3,
503 Some(ABI::V4),
504 move |ruleset: Ruleset| -> _ {
505 Ok(ruleset
506 .handle_access(AccessFs::Truncate)?
507 .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)?
508 .create()?
509 .add_rule(NetPort::new(1, AccessNet::ConnectTcp))?
510 .restrict_self()?)
511 },
512 false,
513 );
514 }
515
516 #[test]
517 fn abi_v5_ioctl_dev() {
518 check_ruleset_support(
519 ABI::V4,
520 Some(ABI::V5),
521 move |ruleset: Ruleset| -> _ {
522 Ok(ruleset
523 .handle_access(AccessNet::BindTcp)?
524 .handle_access(AccessFs::IoctlDev)?
525 .create()?
526 .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))?
527 .restrict_self()?)
528 },
529 false,
530 );
531 }
532
533 #[test]
534 fn abi_v6_scope_mix() {
535 check_ruleset_support(
536 ABI::V5,
537 Some(ABI::V6),
538 move |ruleset: Ruleset| -> _ {
539 Ok(ruleset
540 .handle_access(AccessFs::IoctlDev)?
541 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
542 .create()?
543 .restrict_self()?)
544 },
545 false,
546 );
547 }
548
549 #[test]
550 fn abi_v6_scope_only() {
551 check_ruleset_support(
552 ABI::V6,
553 Some(ABI::V6),
554 move |ruleset: Ruleset| -> _ {
555 Ok(ruleset
556 .scope(Scope::AbstractUnixSocket | Scope::Signal)?
557 .create()?
558 .restrict_self()?)
559 },
560 false,
561 );
562 }
563
564 #[test]
565 fn abi_v7_log_flags() {
566 // Uses Scope::Signal to get partial enforcement at V6 (scopes supported
567 // but log flags not).
568 check_ruleset_support(
569 ABI::V6,
570 Some(ABI::V7),
571 move |ruleset: Ruleset| -> _ {
572 let status = ruleset
573 .scope(Scope::Signal)?
574 .create()?
575 .log_same_exec(false)?
576 .log_new_exec(true)?
577 .log_subdomains(false)?
578 .restrict_self()?;
579
580 if status.ruleset == RulesetStatus::FullyEnforced {
581 assert!(!status.log_same_exec);
582 assert!(status.log_new_exec);
583 assert!(!status.log_subdomains);
584 } else {
585 assert!(status.log_same_exec);
586 assert!(!status.log_new_exec);
587 assert!(status.log_subdomains);
588 }
589
590 Ok(status)
591 },
592 false,
593 );
594 }
595
596 #[test]
597 fn ruleset_created_try_clone_ownedfd() {
598 use std::os::unix::io::{AsRawFd, OwnedFd};
599
600 let abi = ABI::V1;
601 check_ruleset_support(
602 abi,
603 Some(abi),
604 move |ruleset: Ruleset| -> _ {
605 let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?;
606 let ruleset2 = ruleset1.try_clone().unwrap();
607 let ruleset3 = ruleset2.try_clone().unwrap();
608
609 let some1: Option<OwnedFd> = ruleset1.into();
610 if let Some(fd1) = some1 {
611 assert!(fd1.as_raw_fd() >= 0);
612
613 let some2: Option<OwnedFd> = ruleset2.into();
614 let fd2 = some2.unwrap();
615 assert!(fd2.as_raw_fd() >= 0);
616
617 assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd());
618 }
619 Ok(ruleset3.restrict_self()?)
620 },
621 false,
622 );
623 }
624
625 #[test]
626 fn restrict_self_log_subdomains() {
627 check_restrict_self_support(ABI::V7, Some(ABI::V7), move |rs: RestrictSelf| -> _ {
628 Ok(rs.log_subdomains(false)?.apply()?)
629 });
630 }
631}