landlock/restrict_self.rs
1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3//! Restrict_self flag configuration.
4//!
5//! The [`RestrictSelfAttr`] trait provides the
6//! [`log_subdomains()`](RestrictSelfAttr::log_subdomains) and
7//! [`all_threads()`](RestrictSelfAttr::all_threads) setters shared
8//! between [`RulesetCreated`](crate::RulesetCreated) (with a domain) and
9//! [`RestrictSelf`] (without a domain).
10//!
11//! Domain-specific setters ([`log_same_exec()`](crate::RulesetCreatedAttr::log_same_exec),
12//! [`log_new_exec()`](crate::RulesetCreatedAttr::log_new_exec)) are on
13//! [`RulesetCreatedAttr`](crate::RulesetCreatedAttr) which requires
14//! `RestrictSelfAttr` as a supertrait.
15
16use crate::compat::private::OptionCompatLevelMut;
17use crate::compat::Compatibility;
18use crate::flags::{RestrictSelfFlag, SyscallFlagExt};
19use crate::prctl::try_set_no_new_privs;
20use crate::{
21 uapi, CompatLevel, CompatState, Compatible, LandlockStatus, RestrictSelfError, RulesetError,
22};
23use private::RestrictSelfFlagsState;
24
25#[cfg(test)]
26use crate::ABI;
27
28pub(crate) mod private {
29 use crate::RulesetError;
30
31 /// Private plumbing trait for types that store restrict_self flags.
32 ///
33 /// Follows the same pattern as
34 /// [`OptionCompatLevelMut`](crate::compat::private::OptionCompatLevelMut)
35 /// for [`Compatible`](crate::Compatible).
36 ///
37 /// The `try_set_flag()` method encapsulates all internal state access
38 /// (requested/actual flags and compat state) to avoid exposing
39 /// `pub(crate)` types in the trait interface.
40 pub trait RestrictSelfFlagsState {
41 fn try_set_flag(
42 &mut self,
43 flag: super::RestrictSelfFlag,
44 set: bool,
45 ) -> Result<(), RulesetError>;
46 }
47}
48
49/// Trait for types that accept restrict_self flag configuration.
50///
51/// Provides [`log_subdomains()`](Self::log_subdomains) and
52/// [`all_threads()`](Self::all_threads) which work both with and without a
53/// Landlock domain.
54///
55/// Implemented by [`RulesetCreated`](crate::RulesetCreated) (via
56/// [`RulesetCreatedAttr`](crate::RulesetCreatedAttr) supertrait) and
57/// [`RestrictSelf`].
58///
59/// Domain-specific setters (`log_same_exec`, `log_new_exec`) are on
60/// [`RulesetCreatedAttr`](crate::RulesetCreatedAttr).
61pub trait RestrictSelfAttr: Sized + private::RestrictSelfFlagsState {
62 /// Controls logging of denied accesses from nested Landlock domains.
63 /// Logging is **enabled** by default. See the
64 /// [kernel documentation](https://docs.kernel.org/userspace-api/landlock.html#enforcing-a-ruleset).
65 ///
66 /// Calling with `false` sets the `LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF` flag.
67 /// Setters are last-call-wins: calling again with a different boolean
68 /// re-configures the flag (e.g., `log_subdomains(false).log_subdomains(true)`
69 /// leaves logging enabled).
70 ///
71 /// Setting to the default value never triggers a compatibility check,
72 /// so it cannot error even under
73 /// [`CompatLevel::HardRequirement`](crate::CompatLevel::HardRequirement)
74 /// on an unsupported kernel.
75 ///
76 /// Available since Landlock [ABI v7](crate::ABI::V7).
77 ///
78 /// On error, returns a wrapped
79 /// [`SyscallFlagError<RestrictSelfFlag>`](crate::SyscallFlagError).
80 fn log_subdomains(mut self, set: bool) -> Result<Self, RulesetError> {
81 self.try_set_flag(RestrictSelfFlag::LogSubdomains, set)?;
82 Ok(self)
83 }
84
85 /// Applies the Landlock configuration to **all threads** of the calling
86 /// process, rather than only the calling thread. Disabled by default.
87 ///
88 /// Calling with `true` sets the `LANDLOCK_RESTRICT_SELF_TSYNC` flag, which
89 /// atomically enforces the domain and logging configuration on every
90 /// thread of the process. If the calling thread runs with `no_new_privs`,
91 /// the kernel also enables it on the sibling threads.
92 /// Calling with `false` is a no-op (the default behavior).
93 ///
94 /// When enforcing with a Landlock domain (via
95 /// [`RulesetCreated`](crate::RulesetCreated)), this works on its own.
96 /// On the domain-less [`RestrictSelf`] builder the kernel only accepts
97 /// this flag together with
98 /// [`log_subdomains(false)`](Self::log_subdomains); calling
99 /// [`apply()`](RestrictSelf::apply) with `all_threads(true)` alone returns
100 /// a wrapped [`RestrictSelfError`]. The crate does not pre-check this so
101 /// as not to restrict what the kernel allows.
102 ///
103 /// Setting to the default value never triggers a compatibility check,
104 /// so it cannot error even under
105 /// [`CompatLevel::HardRequirement`](crate::CompatLevel::HardRequirement)
106 /// on an unsupported kernel.
107 ///
108 /// Available since Landlock [ABI v8](crate::ABI::V8).
109 ///
110 /// # Warning
111 ///
112 /// On a kernel older than ABI v8, this flag is not supported. With the
113 /// default [`CompatLevel::BestEffort`](crate::CompatLevel::BestEffort) it
114 /// is silently dropped. When enforcing a domain (via
115 /// [`RulesetCreated`](crate::RulesetCreated)) this leaves **only the
116 /// calling thread and its future children restricted, not the sibling and
117 /// parent threads**, a weaker guarantee than requested for a multithreaded
118 /// process. On the domain-less [`RestrictSelf`] builder the remaining
119 /// configuration (e.g. [`log_subdomains()`](Self::log_subdomains)) then
120 /// applies only to the calling thread, and if this flag was the only
121 /// request the enforcement syscall is skipped entirely.
122 /// Applications that require process-wide enforcement should use
123 /// [`CompatLevel::HardRequirement`](crate::CompatLevel::HardRequirement)
124 /// (which errors on an unsupported kernel) or inspect the `all_threads`
125 /// field of the returned status.
126 ///
127 /// On error, returns a wrapped
128 /// [`SyscallFlagError<RestrictSelfFlag>`](crate::SyscallFlagError).
129 fn all_threads(mut self, set: bool) -> Result<Self, RulesetError> {
130 self.try_set_flag(RestrictSelfFlag::AllThreads, set)?;
131 Ok(self)
132 }
133}
134
135/// Builder for calling `landlock_restrict_self()` without creating a
136/// Landlock domain.
137///
138/// Use this when you want to configure `landlock_restrict_self()` flags
139/// without creating a ruleset or a Landlock domain (e.g., muting
140/// subdomain audit logs for nested domains).
141///
142/// [`log_subdomains()`](RestrictSelfAttr::log_subdomains) and
143/// [`all_threads()`](RestrictSelfAttr::all_threads) are available on this
144/// builder. On this domain-less path, the kernel only accepts
145/// [`all_threads()`](RestrictSelfAttr::all_threads) when paired with
146/// [`log_subdomains(false)`](RestrictSelfAttr::log_subdomains).
147/// Domain-specific
148/// setters ([`log_same_exec()`](crate::RulesetCreatedAttr::log_same_exec),
149/// [`log_new_exec()`](crate::RulesetCreatedAttr::log_new_exec)) require a
150/// Landlock domain via [`RulesetCreated`](crate::RulesetCreated).
151///
152/// Available since Landlock [ABI v7](crate::ABI::V7).
153///
154/// `no_new_privs` is enforced by default; call
155/// [`no_new_privs(false)`](Self::no_new_privs) to opt out.
156///
157/// # Example
158///
159/// ```no_run
160/// use landlock::*;
161///
162/// let status = RestrictSelf::default()
163/// .log_subdomains(false)?
164/// .apply()?;
165/// println!("Landlock status: {:?}", status.landlock);
166/// # Ok::<(), RulesetError>(())
167/// ```
168///
169/// Use [`set_compatibility()`](Compatible::set_compatibility) to control
170/// how unsupported flags are handled.
171///
172/// [`apply()`](Self::apply) returns a [`RestrictSelfStatus`] with the
173/// Landlock support status and the effective flag states. Its name
174/// differs from [`RulesetCreated::restrict_self()`](crate::RulesetCreated::restrict_self)
175/// to avoid the redundant `RestrictSelf::restrict_self()`.
176#[derive(Debug)]
177pub struct RestrictSelf {
178 requested_flags: u32,
179 actual_flags: u32,
180 no_new_privs: bool,
181 compat: Compatibility,
182}
183
184impl Default for RestrictSelf {
185 /// Returns a new `RestrictSelf`.
186 /// This call automatically probes the running kernel to know if it
187 /// supports Landlock.
188 fn default() -> Self {
189 Self {
190 requested_flags: 0,
191 actual_flags: 0,
192 no_new_privs: true,
193 compat: Compatibility::new(),
194 }
195 }
196}
197
198#[cfg(test)]
199impl From<ABI> for RestrictSelf {
200 fn from(abi: ABI) -> Self {
201 Self {
202 requested_flags: 0,
203 actual_flags: 0,
204 no_new_privs: true,
205 compat: Compatibility::from(abi),
206 }
207 }
208}
209
210impl RestrictSelfFlagsState for RestrictSelf {
211 fn try_set_flag(&mut self, flag: RestrictSelfFlag, set: bool) -> Result<(), RulesetError> {
212 let raw_bit = flag.raw_bit();
213 // Last-call-wins: requested tracks non-default user intent, actual
214 // tracks the bit that will be passed to the kernel.
215 //
216 // requested_flags is updated unconditionally; actual_flags is
217 // updated only if try_compat succeeds. On HardRequirement +
218 // unsupported, try_compat returns Err and requested_flags is
219 // left in a "user requested this" state; the builder is consumed
220 // by `?` on error so this inconsistency is not observable.
221 if set == flag.default_value() {
222 self.requested_flags &= !raw_bit;
223 } else {
224 self.requested_flags |= raw_bit;
225 }
226 if flag.try_compat(set, &mut self.compat)? {
227 self.actual_flags |= raw_bit;
228 } else {
229 self.actual_flags &= !raw_bit;
230 }
231 Ok(())
232 }
233}
234
235impl RestrictSelfAttr for RestrictSelf {}
236
237impl OptionCompatLevelMut for RestrictSelf {
238 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
239 &mut self.compat.level
240 }
241}
242
243impl Compatible for RestrictSelf {}
244
245/// Status returned by [`RestrictSelf::apply()`].
246///
247/// This is a proper subset of [`RestrictionStatus`](crate::RestrictionStatus):
248/// `log_same_exec` and `log_new_exec` are domain-specific and not configurable
249/// on [`RestrictSelf`], so they are not reported here; `ruleset` does not
250/// apply without a domain.
251#[derive(Debug, PartialEq, Eq)]
252#[non_exhaustive]
253pub struct RestrictSelfStatus {
254 /// Landlock support status of the running system.
255 pub landlock: LandlockStatus,
256 /// `no_new_privs` was successfully enforced via
257 /// `prctl(PR_SET_NO_NEW_PRIVS, 1)`.
258 pub no_new_privs: bool,
259 /// Subdomain logging is enabled (default: true).
260 pub log_subdomains: bool,
261 /// The configuration was applied to all threads of the process (default:
262 /// false).
263 pub all_threads: bool,
264}
265
266impl RestrictSelf {
267 /// Configures whether to call `prctl(PR_SET_NO_NEW_PRIVS)` during
268 /// [`apply()`](Self::apply). Defaults to `true`.
269 ///
270 /// This `prctl(2)` call is never ignored, even if an error was
271 /// encountered while [`CompatLevel::SoftRequirement`] was set.
272 ///
273 /// See [`RestrictSelfAttr::log_subdomains()`] for compat-state
274 /// behavior when toggling this setter on unsupported kernels.
275 pub fn no_new_privs(mut self, yes: bool) -> Self {
276 self.no_new_privs = yes;
277 self
278 }
279
280 /// Applies the configured restrict_self flags by calling
281 /// `landlock_restrict_self(-1, flags)`.
282 ///
283 /// If `no_new_privs` is configured (default), also calls
284 /// `prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)` first, since the kernel
285 /// requires `no_new_privs` (or `CAP_SYS_ADMIN`) for
286 /// `landlock_restrict_self()`. See
287 /// [`no_new_privs()`](Self::no_new_privs) to opt out.
288 ///
289 /// Returns a [`RestrictSelfStatus`] with the Landlock support status.
290 /// Skips the restrict_self syscall if no flags are enforceable.
291 pub fn apply(mut self) -> Result<RestrictSelfStatus, RulesetError> {
292 let enforced_nnp = if self.no_new_privs {
293 try_set_no_new_privs(&mut self.compat)?
294 } else {
295 false
296 };
297
298 let log_subdomains = RestrictSelfFlag::LogSubdomains.is_set(self.actual_flags);
299 let all_threads = RestrictSelfFlag::AllThreads.is_set(self.actual_flags);
300
301 let status = RestrictSelfStatus {
302 landlock: self.compat.status(),
303 no_new_privs: enforced_nnp,
304 log_subdomains,
305 all_threads,
306 };
307
308 // Skip the syscall when the compat state indicates no features are
309 // enforceable, mirroring RulesetCreated::restrict_self().
310 match self.compat.state {
311 CompatState::Init | CompatState::No | CompatState::Dummy => return Ok(status),
312 CompatState::Full | CompatState::Partial => {
313 if self.actual_flags == 0 {
314 return Ok(status);
315 }
316 }
317 }
318
319 match unsafe { uapi::landlock_restrict_self(-1, self.actual_flags) } {
320 0 => Ok(status),
321 _ => Err(RestrictSelfError::RestrictSelfCall {
322 source: std::io::Error::last_os_error(),
323 }
324 .into()),
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::uapi;
333 use crate::*;
334
335 #[test]
336 fn restrict_self_default() {
337 let rs = RestrictSelf::default();
338 assert_eq!(rs.requested_flags, 0);
339 assert_eq!(rs.actual_flags, 0);
340
341 // apply() on an unconfigured RestrictSelf returns the kernel's default
342 // flag states. The compat state is Init, so no real syscall is made.
343 let status = rs.apply().unwrap();
344 assert!(status.log_subdomains);
345 assert!(!status.all_threads);
346 }
347
348 #[test]
349 fn restrict_self_log_subdomains() {
350 // With mocked V7: flag should be set.
351 // TODO: Add real kernel test with audit parsing for end-to-end validation.
352 let rs = RestrictSelf {
353 requested_flags: 0,
354 actual_flags: 0,
355 no_new_privs: true,
356 compat: ABI::V7.into(),
357 };
358 let rs = rs.log_subdomains(false).unwrap();
359 assert_ne!(rs.requested_flags, 0);
360 assert_ne!(rs.actual_flags, 0);
361 assert_eq!(rs.requested_flags, rs.actual_flags);
362 }
363
364 #[test]
365 fn restrict_self_compatibility() {
366 // HardRequirement on unsupported ABI returns error.
367 let rs = RestrictSelf {
368 requested_flags: 0,
369 actual_flags: 0,
370 no_new_privs: true,
371 compat: ABI::Unsupported.into(),
372 };
373 assert!(matches!(
374 rs.set_compatibility(CompatLevel::HardRequirement)
375 .log_subdomains(false)
376 .unwrap_err(),
377 RulesetError::RestrictSelfFlags(SyscallFlagError::NotSupported {
378 flag: RestrictSelfFlag::LogSubdomains,
379 set: false,
380 })
381 ));
382 }
383
384 #[test]
385 fn restrict_self_no_flags() {
386 // apply() with no flags set should skip the syscall.
387 let rs = RestrictSelf {
388 requested_flags: 0,
389 actual_flags: 0,
390 no_new_privs: true,
391 compat: ABI::V7.into(),
392 };
393 let status = rs.apply().unwrap();
394 assert!(matches!(status.landlock, LandlockStatus::Available { .. }));
395 assert!(status.log_subdomains); // default: enabled
396 }
397
398 #[test]
399 fn restrict_self_partial_no_op() {
400 // When all flags are dropped by BestEffort (actual_flags == 0),
401 // apply() should still return the Landlock status from the kernel probe.
402 let mut compat: Compatibility = ABI::V7.into();
403 compat.update(CompatState::No);
404 assert_eq!(compat.state, CompatState::No);
405
406 let rs = RestrictSelf {
407 requested_flags: 0b01,
408 actual_flags: 0,
409 no_new_privs: true,
410 compat,
411 };
412 let status = rs.apply().unwrap();
413 assert!(matches!(status.landlock, LandlockStatus::Available { .. }));
414 assert!(status.log_subdomains); // default: enabled (flag was dropped)
415 }
416
417 #[test]
418 fn restrict_self_best_effort_drops_unsupported() {
419 // On an unsupported ABI, BestEffort silently drops all flags.
420 let rs = RestrictSelf {
421 requested_flags: 0,
422 actual_flags: 0,
423 no_new_privs: true,
424 compat: ABI::Unsupported.into(),
425 };
426 let rs = rs.log_subdomains(false).unwrap();
427 assert_ne!(rs.requested_flags, 0);
428 assert_eq!(rs.actual_flags, 0);
429 let status = rs.apply().unwrap();
430 assert_eq!(status.landlock, LandlockStatus::NotImplemented);
431 assert!(status.log_subdomains); // flag was dropped, logging still enabled
432 }
433
434 #[test]
435 fn restrict_self_soft_requirement_drops_unsupported() {
436 // On an unsupported ABI, SoftRequirement transitions to Dummy and
437 // silently drops the flag (without erroring like HardRequirement).
438 let rs = RestrictSelf {
439 requested_flags: 0,
440 actual_flags: 0,
441 no_new_privs: true,
442 compat: ABI::Unsupported.into(),
443 };
444 let rs = rs
445 .set_compatibility(CompatLevel::SoftRequirement)
446 .log_subdomains(false)
447 .unwrap();
448 assert_ne!(rs.requested_flags, 0);
449 assert_eq!(rs.actual_flags, 0);
450 let status = rs.apply().unwrap();
451 assert_eq!(status.landlock, LandlockStatus::NotImplemented);
452 assert!(status.log_subdomains); // flag was dropped, logging still enabled
453 }
454
455 #[test]
456 fn restrict_self_subdomains_applied() {
457 let rs = RestrictSelf {
458 requested_flags: 0,
459 actual_flags: 0,
460 no_new_privs: true,
461 compat: ABI::V7.into(),
462 };
463 let rs = rs.log_subdomains(false).unwrap();
464 assert_ne!(rs.actual_flags, 0);
465 assert_ne!(
466 rs.actual_flags & uapi::LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
467 0
468 );
469 }
470
471 #[test]
472 fn restrict_self_all_threads_applied() {
473 // all_threads(true) is supported since ABI v8; on a mocked V8 the
474 // TSYNC bit is set in the actual flags. This is a pure flag-composition
475 // check: apply() is not called, so the kernel is not involved.
476 // log_subdomains(false) is set alongside because that is the only flag
477 // combination the kernel would accept on the domain-less path
478 // (ruleset_fd == -1); here it only documents that pairing.
479 let rs = RestrictSelf {
480 requested_flags: 0,
481 actual_flags: 0,
482 no_new_privs: true,
483 compat: ABI::V8.into(),
484 };
485 let rs = rs.log_subdomains(false).unwrap().all_threads(true).unwrap();
486 assert_ne!(rs.actual_flags & uapi::LANDLOCK_RESTRICT_SELF_TSYNC, 0);
487 assert_ne!(
488 rs.actual_flags & uapi::LANDLOCK_RESTRICT_SELF_LOG_SUBDOMAINS_OFF,
489 0
490 );
491 }
492
493 #[test]
494 fn restrict_self_all_threads_best_effort_drops_v7() {
495 // On a kernel that predates ABI v8 (here mocked V7), BestEffort
496 // silently drops all_threads(true): the TSYNC bit is requested but not
497 // applied, so only the calling thread would be restricted.
498 let rs = RestrictSelf {
499 requested_flags: 0,
500 actual_flags: 0,
501 no_new_privs: true,
502 compat: ABI::V7.into(),
503 };
504 let rs = rs.all_threads(true).unwrap();
505 assert_ne!(rs.requested_flags & uapi::LANDLOCK_RESTRICT_SELF_TSYNC, 0);
506 assert_eq!(rs.actual_flags & uapi::LANDLOCK_RESTRICT_SELF_TSYNC, 0);
507 let status = rs.apply().unwrap();
508 assert!(!status.all_threads);
509 }
510
511 #[test]
512 fn restrict_self_all_threads_hard_requirement_v7() {
513 // HardRequirement on a pre-v8 ABI returns an error rather than
514 // silently restricting only the calling thread.
515 let rs = RestrictSelf {
516 requested_flags: 0,
517 actual_flags: 0,
518 no_new_privs: true,
519 compat: ABI::V7.into(),
520 };
521 assert!(matches!(
522 rs.set_compatibility(CompatLevel::HardRequirement)
523 .all_threads(true)
524 .unwrap_err(),
525 RulesetError::RestrictSelfFlags(SyscallFlagError::NotSupported {
526 flag: RestrictSelfFlag::AllThreads,
527 set: true,
528 })
529 ));
530 }
531
532 #[test]
533 fn restrict_self_hard_requirement_supported() {
534 let rs = RestrictSelf {
535 requested_flags: 0,
536 actual_flags: 0,
537 no_new_privs: true,
538 compat: ABI::V7.into(),
539 };
540 let rs = rs
541 .set_compatibility(CompatLevel::HardRequirement)
542 .log_subdomains(false)
543 .unwrap();
544 assert_ne!(rs.requested_flags, 0);
545 assert_ne!(rs.actual_flags, 0);
546 }
547
548 #[test]
549 fn restrict_self_last_call_wins() {
550 // Setting a flag to non-default then back to default should clear
551 // both requested and actual (last call wins).
552 let rs = RestrictSelf {
553 requested_flags: 0,
554 actual_flags: 0,
555 no_new_privs: true,
556 compat: ABI::V7.into(),
557 };
558 let rs = rs
559 .log_subdomains(false)
560 .unwrap()
561 .log_subdomains(true)
562 .unwrap();
563 assert_eq!(rs.requested_flags, 0);
564 assert_eq!(rs.actual_flags, 0);
565 }
566
567 #[test]
568 fn restrict_self_default_after_hard_requirement() {
569 // Setting a flag to its default value never requires a compat check,
570 // so HardRequirement on an unsupported ABI does not error.
571 let rs = RestrictSelf {
572 requested_flags: 0,
573 actual_flags: 0,
574 no_new_privs: true,
575 compat: ABI::Unsupported.into(),
576 };
577 let rs = rs
578 .set_compatibility(CompatLevel::HardRequirement)
579 .log_subdomains(true)
580 .unwrap();
581 assert_eq!(rs.requested_flags, 0);
582 assert_eq!(rs.actual_flags, 0);
583 }
584
585 #[test]
586 fn restrict_self_no_nnp() {
587 // With no_new_privs(false) and Unsupported (state Init, syscall
588 // skipped), apply() reports no_new_privs: false without calling
589 // prctl.
590 let rs = RestrictSelf {
591 requested_flags: 0,
592 actual_flags: 0,
593 no_new_privs: true,
594 compat: ABI::Unsupported.into(),
595 };
596 let status = rs.no_new_privs(false).apply().unwrap();
597 assert!(!status.no_new_privs);
598 assert_eq!(status.landlock, LandlockStatus::NotImplemented);
599 }
600}