Skip to main content

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}