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