Skip to main content

sandlock_core/
protection.rs

1//! Per-protection ABI floor for Landlock protections.
2//!
3//! Sandlock relies on a set of Landlock-provided protections, each
4//! introduced in a specific Landlock ABI version. This module names
5//! them as `Protection` variants and maps each to the minimum ABI the
6//! host kernel must support.
7//!
8//! The actual policy that decides whether a protection is enforced,
9//! degraded, or disabled lives in the higher-level
10//! `ProtectionPolicy` (also in this module). The decision-vs-availability
11//! resolution happens in `landlock::confine_inner`.
12
13use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16
17/// A single Landlock-provided protection, ABI-gated.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub enum Protection {
20    /// `LANDLOCK_ACCESS_FS_REFER` — ABI v2+.
21    FsRefer,
22    /// `LANDLOCK_ACCESS_FS_TRUNCATE` — ABI v3+.
23    FsTruncate,
24    /// `LANDLOCK_ACCESS_NET_BIND_TCP` / `_CONNECT_TCP` — ABI v4+.
25    NetTcp,
26    /// `LANDLOCK_ACCESS_FS_IOCTL_DEV` — ABI v5+.
27    FsIoctlDev,
28    /// `LANDLOCK_SCOPE_SIGNAL` — ABI v6+.
29    SignalScope,
30    /// `LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET` — ABI v6+.
31    AbstractUnixSocketScope,
32}
33
34impl Protection {
35    /// Minimum Landlock ABI version the host kernel must support for
36    /// this protection to be available.
37    pub const fn min_abi(self) -> u32 {
38        match self {
39            Protection::FsRefer => 2,
40            Protection::FsTruncate => 3,
41            Protection::NetTcp => 4,
42            Protection::FsIoctlDev => 5,
43            Protection::SignalScope => 6,
44            Protection::AbstractUnixSocketScope => 6,
45        }
46    }
47
48    /// Iterator over every known protection.
49    pub fn all() -> impl Iterator<Item = Protection> {
50        [
51            Protection::FsRefer,
52            Protection::FsTruncate,
53            Protection::NetTcp,
54            Protection::FsIoctlDev,
55            Protection::SignalScope,
56            Protection::AbstractUnixSocketScope,
57        ]
58        .into_iter()
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn min_abi_matches_kernel_documented_floors() {
68        // These numbers come from the kernel Landlock documentation
69        // (https://docs.kernel.org/userspace-api/landlock.html);
70        // they MUST NOT drift.
71        assert_eq!(Protection::FsRefer.min_abi(), 2);
72        assert_eq!(Protection::FsTruncate.min_abi(), 3);
73        assert_eq!(Protection::NetTcp.min_abi(), 4);
74        assert_eq!(Protection::FsIoctlDev.min_abi(), 5);
75        assert_eq!(Protection::SignalScope.min_abi(), 6);
76        assert_eq!(Protection::AbstractUnixSocketScope.min_abi(), 6);
77    }
78
79    #[test]
80    fn all_iterates_every_variant_exactly_once() {
81        let collected: Vec<Protection> = Protection::all().collect();
82        assert_eq!(collected.len(), 6);
83        // No duplicates.
84        for p in &collected {
85            assert_eq!(collected.iter().filter(|&q| q == p).count(), 1);
86        }
87    }
88}
89
90/// What a `ProtectionPolicy` instructs sandlock to do with a given
91/// `Protection`.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93pub enum ProtectionState {
94    /// Enforce; if the host kernel cannot provide this protection,
95    /// `confine_inner` returns an error naming the protection and the
96    /// kernel's actual ABI version. This is the default for every
97    /// protection.
98    Strict,
99    /// Enforce where the host kernel supports it; skip silently when
100    /// it does not. The skip is observable via `Sandbox::active_protections()`
101    /// and `sandlock check`.
102    Degradable,
103    /// Never enforce, even on a host kernel that supports the protection.
104    /// Intended for workloads that genuinely need the capability the
105    /// protection blocks.
106    Disabled,
107}
108
109/// Per-`Protection` state collection. The default for any protection
110/// not explicitly named is `ProtectionState::Strict` — meaning a
111/// freshly-constructed `ProtectionPolicy` produces the same behaviour
112/// as the current hard `MIN_ABI = 6` floor.
113#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
114pub struct ProtectionPolicy {
115    states: HashMap<Protection, ProtectionState>,
116}
117
118impl ProtectionPolicy {
119    /// A policy with no overrides — every protection defaults to
120    /// `Strict`. Equivalent to the pre-Protection behaviour.
121    pub fn strict_all() -> Self {
122        Self::default()
123    }
124
125    /// Look up the state for a given protection. Returns `Strict`
126    /// for protections not explicitly named.
127    pub fn state(&self, protection: Protection) -> ProtectionState {
128        self.states.get(&protection).copied().unwrap_or(ProtectionState::Strict)
129    }
130
131    /// Set the state for a single protection. Internal API — public
132    /// builder methods (in the polarity-dependent layer landing later)
133    /// call this. Marked `#[doc(hidden)] pub` so integration tests in
134    /// the `tests/` directory can drive the resolver directly; not part
135    /// of the stable public surface.
136    #[doc(hidden)]
137    pub fn set(&mut self, protection: Protection, state: ProtectionState) {
138        self.states.insert(protection, state);
139    }
140
141    /// Iterator over every protection paired with its resolved state
142    /// (including the implicit `Strict` for unnamed ones).
143    pub fn iter(&self) -> impl Iterator<Item = (Protection, ProtectionState)> + '_ {
144        Protection::all().map(|p| (p, self.state(p)))
145    }
146}
147
148/// Per-protection runtime status, resolved against the host's
149/// Landlock ABI and the `ProtectionPolicy`. Returned by
150/// `Sandbox::active_protections()`.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum ProtectionStatus {
153    /// Enforced (policy is Strict or Degradable, host supports it).
154    Active,
155    /// Policy named the protection as Degradable, host does not
156    /// support it — silently skipped.
157    Degraded,
158    /// Policy explicitly disabled the protection.
159    Disabled,
160    /// Policy was Strict and host does not support it — would have
161    /// caused `build()` to fail.
162    Unavailable,
163}
164
165impl ProtectionStatus {
166    /// Resolve a single `Protection` against the host ABI and a
167    /// `ProtectionPolicy`. Marked `#[doc(hidden)] pub` so integration
168    /// tests in the `tests/` directory can drive the resolver directly
169    /// with synthetic ABI values; not part of the stable public surface.
170    #[doc(hidden)]
171    pub fn resolve(p: Protection, host_abi: u32, pol: &ProtectionPolicy) -> Self {
172        let available = host_abi >= p.min_abi();
173        match (pol.state(p), available) {
174            (ProtectionState::Disabled, _) => ProtectionStatus::Disabled,
175            (ProtectionState::Strict, true) => ProtectionStatus::Active,
176            (ProtectionState::Strict, false) => ProtectionStatus::Unavailable,
177            (ProtectionState::Degradable, true) => ProtectionStatus::Active,
178            (ProtectionState::Degradable, false) => ProtectionStatus::Degraded,
179        }
180    }
181}
182
183#[cfg(test)]
184mod policy_tests {
185    use super::*;
186
187    #[test]
188    fn strict_all_returns_strict_for_every_protection() {
189        let pol = ProtectionPolicy::strict_all();
190        for p in Protection::all() {
191            assert_eq!(pol.state(p), ProtectionState::Strict);
192        }
193    }
194
195    #[test]
196    fn unnamed_protections_default_to_strict_even_after_other_overrides() {
197        let mut pol = ProtectionPolicy::strict_all();
198        pol.set(Protection::SignalScope, ProtectionState::Degradable);
199        assert_eq!(pol.state(Protection::SignalScope), ProtectionState::Degradable);
200        assert_eq!(pol.state(Protection::FsTruncate), ProtectionState::Strict);
201        assert_eq!(pol.state(Protection::AbstractUnixSocketScope), ProtectionState::Strict);
202    }
203
204    #[test]
205    fn iter_yields_every_protection_with_resolved_state() {
206        let mut pol = ProtectionPolicy::strict_all();
207        pol.set(Protection::FsIoctlDev, ProtectionState::Disabled);
208        let collected: Vec<_> = pol.iter().collect();
209        assert_eq!(collected.len(), 6);
210        assert!(collected.iter().any(|(p, s)| *p == Protection::FsIoctlDev && *s == ProtectionState::Disabled));
211        for (p, s) in &collected {
212            if *p != Protection::FsIoctlDev {
213                assert_eq!(*s, ProtectionState::Strict, "{:?} should default to Strict", p);
214            }
215        }
216    }
217}