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}