/rust/registry/src/index.crates.io-1949cf8c6b5b557f/landlock-0.4.5/src/compat.rs
Line | Count | Source |
1 | | // SPDX-License-Identifier: Apache-2.0 OR MIT |
2 | | |
3 | | use crate::{uapi, Access, CompatError}; |
4 | | use std::fmt::{self, Display, Formatter}; |
5 | | use std::io::Error; |
6 | | |
7 | | #[cfg(test)] |
8 | | use std::convert::TryInto; |
9 | | #[cfg(test)] |
10 | | use strum::{EnumCount, IntoEnumIterator}; |
11 | | #[cfg(test)] |
12 | | use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; |
13 | | |
14 | | /// Version of the Landlock [ABI](https://en.wikipedia.org/wiki/Application_binary_interface). |
15 | | /// |
16 | | /// `ABI` enables getting the features supported by a specific Landlock ABI |
17 | | /// (without relying on the kernel version which may not be accessible or patched). |
18 | | /// For example, [`AccessFs::from_all(ABI::V1)`](Access::from_all) |
19 | | /// gets all the file system access rights defined by the first version. |
20 | | /// |
21 | | /// Without `ABI`, it would be hazardous to rely on the the full set of access flags |
22 | | /// (e.g., `BitFlags::<AccessFs>::all()` or `BitFlags::ALL`), |
23 | | /// a moving target that would change the semantics of your Landlock rule |
24 | | /// when migrating to a newer version of this crate. |
25 | | /// Indeed, a simple `cargo update` or `cargo install` run by any developer |
26 | | /// can result in a new version of this crate (fixing bugs or bringing non-breaking changes). |
27 | | /// This crate cannot give any guarantee concerning the new restrictions resulting from |
28 | | /// these unknown bits (i.e. access rights) that would not be controlled by your application but by |
29 | | /// a future version of this crate instead. |
30 | | /// Because we cannot know what the effect on your application of an unknown restriction would be |
31 | | /// when handling an untested Landlock access right (i.e. denied-by-default access), |
32 | | /// it could trigger bugs in your application. |
33 | | /// |
34 | | /// This crate provides a set of tools to sandbox as much as possible |
35 | | /// while guaranteeing a consistent behavior thanks to the [`Compatible`] methods. |
36 | | /// You should also test with different relevant kernel versions, |
37 | | /// see [landlock-test-tools](https://github.com/landlock-lsm/landlock-test-tools) and |
38 | | /// [CI integration](https://github.com/landlock-lsm/rust-landlock/pull/41). |
39 | | /// |
40 | | /// This way, we can have the guarantee that the use of a set of tested Landlock ABI works as |
41 | | /// expected because features brought by newer Landlock ABI will never be enabled by default |
42 | | /// (cf. [Linux kernel compatibility contract](https://docs.kernel.org/userspace-api/landlock.html#compatibility)). |
43 | | /// |
44 | | /// In a nutshell, test the access rights you request on a kernel that support them and |
45 | | /// on a kernel that doesn't support them. |
46 | | /// |
47 | | /// Derived `Debug` formats are [not stable](https://doc.rust-lang.org/stable/std/fmt/trait.Debug.html#stability). |
48 | | #[cfg_attr(test, derive(EnumIter, EnumCountMacro))] |
49 | | #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] |
50 | | #[non_exhaustive] |
51 | | pub enum ABI { |
52 | | /// Kernel not supporting Landlock, either because it is not built with Landlock |
53 | | /// or Landlock is not enabled at boot. |
54 | | Unsupported = 0, |
55 | | /// First Landlock ABI, introduced with |
56 | | /// [Linux 5.13](https://git.kernel.org/stable/c/17ae69aba89dbfa2139b7f8024b757ab3cc42f59). |
57 | | V1 = 1, |
58 | | /// Second Landlock ABI, introduced with |
59 | | /// [Linux 5.19](https://git.kernel.org/stable/c/cb44e4f061e16be65b8a16505e121490c66d30d0). |
60 | | V2 = 2, |
61 | | /// Third Landlock ABI, introduced with |
62 | | /// [Linux 6.2](https://git.kernel.org/stable/c/299e2b1967578b1442128ba8b3e86ed3427d3651). |
63 | | V3 = 3, |
64 | | /// Fourth Landlock ABI, introduced with |
65 | | /// [Linux 6.7](https://git.kernel.org/stable/c/136cc1e1f5be75f57f1e0404b94ee1c8792cb07d). |
66 | | V4 = 4, |
67 | | /// Fifth Landlock ABI, introduced with |
68 | | /// [Linux 6.10](https://git.kernel.org/stable/c/2fc0e7892c10734c1b7c613ef04836d57d4676d5). |
69 | | V5 = 5, |
70 | | /// Sixth Landlock ABI, introduced with |
71 | | /// [Linux 6.12](https://git.kernel.org/stable/c/e1b061b444fb01c237838f0d8238653afe6a8094). |
72 | | V6 = 6, |
73 | | /// Seventh Landlock ABI, introduced with |
74 | | /// [Linux 6.15](https://git.kernel.org/stable/c/72885116069abdd05c245707c3989fc605632970). |
75 | | V7 = 7, |
76 | | } |
77 | | |
78 | | // ABI should not be dynamically created (in other crates) according to the running kernel |
79 | | // to avoid inconsistent behaviors and non-determinism. Creating ABIs based on runtime detection |
80 | | // can lead to unreliable sandboxing where rules might differ between executions. |
81 | | impl ABI { |
82 | | #[cfg(test)] |
83 | | fn is_known(value: i32) -> bool { |
84 | | value > 0 && value < ABI::COUNT as i32 |
85 | | } |
86 | | } |
87 | | |
88 | | /// Converting from an integer to an ABI should only be used for testing. |
89 | | /// Indeed, manually setting the ABI can lead to inconsistent and unexpected behaviors. |
90 | | /// Instead, just use the appropriate access rights, this library will handle the rest. |
91 | | impl From<i32> for ABI { |
92 | 0 | fn from(value: i32) -> ABI { |
93 | 0 | match value { |
94 | 0 | n if n <= 0 => ABI::Unsupported, |
95 | 0 | 1 => ABI::V1, |
96 | 0 | 2 => ABI::V2, |
97 | 0 | 3 => ABI::V3, |
98 | 0 | 4 => ABI::V4, |
99 | 0 | 5 => ABI::V5, |
100 | 0 | 6 => ABI::V6, |
101 | | // Returns the greatest known ABI. |
102 | 0 | _ => ABI::V7, |
103 | | } |
104 | 0 | } |
105 | | } |
106 | | |
107 | | #[test] |
108 | | fn abi_from() { |
109 | | // EOPNOTSUPP (-95), ENOSYS (-38) |
110 | | for n in [-95, -38, -1, 0] { |
111 | | assert_eq!(ABI::from(n), ABI::Unsupported); |
112 | | } |
113 | | |
114 | | let mut last_i = 1; |
115 | | let mut last_abi = ABI::Unsupported; |
116 | | for (i, abi) in ABI::iter().enumerate() { |
117 | | last_i = i.try_into().unwrap(); |
118 | | last_abi = abi; |
119 | | assert_eq!(ABI::from(last_i), last_abi); |
120 | | } |
121 | | |
122 | | assert_eq!(ABI::from(last_i + 1), last_abi); |
123 | | assert_eq!(ABI::from(999), last_abi); |
124 | | } |
125 | | |
126 | | #[test] |
127 | | fn known_abi() { |
128 | | assert!(!ABI::is_known(-1)); |
129 | | assert!(!ABI::is_known(0)); |
130 | | assert!(!ABI::is_known(999)); |
131 | | |
132 | | let mut last_i = -1; |
133 | | for (i, _) in ABI::iter().enumerate().skip(1) { |
134 | | last_i = i as i32; |
135 | | assert!(ABI::is_known(last_i)); |
136 | | } |
137 | | assert!(!ABI::is_known(last_i + 1)); |
138 | | } |
139 | | |
140 | | impl Display for ABI { |
141 | 0 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { |
142 | 0 | match self { |
143 | 0 | ABI::Unsupported => write!(f, "unsupported"), |
144 | 0 | v => (*v as u32).fmt(f), |
145 | | } |
146 | 0 | } |
147 | | } |
148 | | |
149 | | /// Status of Landlock support for the running system. |
150 | | /// |
151 | | /// This enum is used to represent the status of the Landlock support for the system where the code |
152 | | /// is executed. It can indicate whether Landlock is available or not. |
153 | | /// |
154 | | /// # Warning |
155 | | /// |
156 | | /// Sandboxed programs should only use this data to log or provide information to users, |
157 | | /// not to change their behavior according to this status. Indeed, the `Ruleset` and the other |
158 | | /// types are designed to handle the compatibility in a simple and safe way. |
159 | | #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
160 | | pub enum LandlockStatus { |
161 | | /// Landlock is supported but not enabled (`EOPNOTSUPP`). |
162 | | NotEnabled, |
163 | | /// Landlock is not implemented (i.e. not built into the running kernel: `ENOSYS`). |
164 | | NotImplemented, |
165 | | /// Landlock is available and working on the running system. |
166 | | /// |
167 | | /// This indicates that the kernel supports Landlock and it's properly enabled. |
168 | | /// The crate uses the `effective_abi` for all operations, which represents |
169 | | /// the highest ABI version that both the kernel and this crate understand. |
170 | | Available { |
171 | | /// The effective ABI version that this crate will use for Landlock operations. |
172 | | /// This is the intersection of what the kernel supports and what this crate knows about. |
173 | | effective_abi: ABI, |
174 | | /// The actual kernel ABI version when it's newer than any ABI supported by this crate. |
175 | | /// |
176 | | /// If `Some(version)`, it means the running kernel supports Landlock ABI `version` |
177 | | /// which is higher than the latest ABI known by this crate. |
178 | | /// |
179 | | /// This field is purely informational and is never used for Landlock operations. |
180 | | /// The crate always and only uses `effective_abi` for all functionality. |
181 | | kernel_abi: Option<i32>, |
182 | | }, |
183 | | } |
184 | | |
185 | | impl LandlockStatus { |
186 | | // Must remain private to avoid inconsistent behavior using such unknown-at-build-time ABI |
187 | | // e.g., AccessFs::from_all(ABI::new_current()) |
188 | | // |
189 | | // This should not be Default::default() because the returned value would may not be the same |
190 | | // for all users. |
191 | 0 | fn current() -> Self { |
192 | | // Landlock ABI version starts at 1 but errno is only set for negative values. |
193 | 0 | let v = unsafe { |
194 | 0 | uapi::landlock_create_ruleset( |
195 | 0 | std::ptr::null(), |
196 | | 0, |
197 | | uapi::LANDLOCK_CREATE_RULESET_VERSION, |
198 | | ) |
199 | | }; |
200 | 0 | if v < 0 { |
201 | | // The only possible error values should be EOPNOTSUPP and ENOSYS. |
202 | 0 | match Error::last_os_error().raw_os_error() { |
203 | 0 | Some(libc::EOPNOTSUPP) => Self::NotEnabled, |
204 | 0 | _ => Self::NotImplemented, |
205 | | } |
206 | | } else { |
207 | 0 | let abi = ABI::from(v); |
208 | 0 | Self::Available { |
209 | 0 | effective_abi: abi, |
210 | 0 | kernel_abi: (v != abi as i32).then_some(v), |
211 | 0 | } |
212 | | } |
213 | 0 | } |
214 | | } |
215 | | |
216 | | // Test against the running kernel. |
217 | | #[test] |
218 | | fn test_current_landlock_status() { |
219 | | let status = LandlockStatus::current(); |
220 | | if *TEST_ABI == ABI::Unsupported { |
221 | | assert_eq!(status, LandlockStatus::NotImplemented); |
222 | | } else { |
223 | | assert!( |
224 | | matches!(status, LandlockStatus::Available { effective_abi, .. } if effective_abi == *TEST_ABI) |
225 | | ); |
226 | | if std::env::var(TEST_ABI_ENV_NAME).is_ok() { |
227 | | // We cannot reliably check for unknown kernel. |
228 | | assert!(matches!( |
229 | | status, |
230 | | LandlockStatus::Available { |
231 | | kernel_abi: None, |
232 | | .. |
233 | | } |
234 | | )); |
235 | | } |
236 | | } |
237 | | } |
238 | | |
239 | | impl From<LandlockStatus> for ABI { |
240 | 0 | fn from(status: LandlockStatus) -> Self { |
241 | 0 | match status { |
242 | | // The only possible error values should be EOPNOTSUPP and ENOSYS, |
243 | | // but let's convert all kind of errors as unsupported. |
244 | 0 | LandlockStatus::NotEnabled | LandlockStatus::NotImplemented => ABI::Unsupported, |
245 | 0 | LandlockStatus::Available { effective_abi, .. } => effective_abi, |
246 | | } |
247 | 0 | } |
248 | | } |
249 | | |
250 | | // This is only useful to tests and should not be exposed publicly because |
251 | | // the mapping can only be partial. |
252 | | #[cfg(test)] |
253 | | impl From<ABI> for LandlockStatus { |
254 | | fn from(abi: ABI) -> Self { |
255 | | match abi { |
256 | | // Convert to ENOSYS because of check_ruleset_support() and ruleset_unsupported() tests. |
257 | | ABI::Unsupported => Self::NotImplemented, |
258 | | _ => Self::Available { |
259 | | effective_abi: abi, |
260 | | kernel_abi: None, |
261 | | }, |
262 | | } |
263 | | } |
264 | | } |
265 | | |
266 | | #[cfg(test)] |
267 | | pub(crate) static TEST_ABI_ENV_NAME: &str = "LANDLOCK_CRATE_TEST_ABI"; |
268 | | |
269 | | #[cfg(test)] |
270 | | lazy_static! { |
271 | | pub(crate) static ref TEST_ABI: ABI = match std::env::var("LANDLOCK_CRATE_TEST_ABI") { |
272 | | Ok(s) => { |
273 | | let n = s.parse::<i32>().unwrap(); |
274 | | if ABI::is_known(n) || n == 0 { |
275 | | ABI::from(n) |
276 | | } else { |
277 | | panic!("Unknown ABI: {n}"); |
278 | | } |
279 | | } |
280 | | Err(std::env::VarError::NotPresent) => LandlockStatus::current().into(), |
281 | | Err(e) => panic!("Failed to read LANDLOCK_CRATE_TEST_ABI: {e}"), |
282 | | }; |
283 | | } |
284 | | |
285 | | #[cfg(test)] |
286 | | pub(crate) fn can_emulate(mock: ABI, partial_support: ABI, full_support: Option<ABI>) -> bool { |
287 | | mock < partial_support |
288 | | || mock <= *TEST_ABI |
289 | | || if let Some(full) = full_support { |
290 | | full <= *TEST_ABI |
291 | | } else { |
292 | | partial_support <= *TEST_ABI |
293 | | } |
294 | | } |
295 | | |
296 | | #[cfg(test)] |
297 | | pub(crate) fn get_errno_from_landlock_status() -> Option<i32> { |
298 | | match LandlockStatus::current() { |
299 | | LandlockStatus::NotImplemented | LandlockStatus::NotEnabled => { |
300 | | match Error::last_os_error().raw_os_error() { |
301 | | // Returns ENOSYS when the kernel is not built with Landlock support, |
302 | | // or EOPNOTSUPP when Landlock is supported but disabled at boot time. |
303 | | ret @ Some(libc::ENOSYS | libc::EOPNOTSUPP) => ret, |
304 | | // Other values can only come from bogus seccomp filters or debugging tampering. |
305 | | ret => { |
306 | | eprintln!("Current kernel should support this Landlock ABI according to $LANDLOCK_CRATE_TEST_ABI"); |
307 | | eprintln!("Unexpected result: {ret:?}"); |
308 | | unreachable!(); |
309 | | } |
310 | | } |
311 | | } |
312 | | LandlockStatus::Available { .. } => None, |
313 | | } |
314 | | } |
315 | | |
316 | | #[test] |
317 | | fn current_kernel_abi() { |
318 | | // Ensures that the tested Landlock ABI is the latest known version supported by the running |
319 | | // kernel. If this test failed, you need set the LANDLOCK_CRATE_TEST_ABI environment variable |
320 | | // to the Landlock ABI version supported by your kernel. With a missing variable, the latest |
321 | | // Landlock ABI version known by this crate is automatically set. |
322 | | // From Linux 5.13 to 5.18, you need to run: LANDLOCK_CRATE_TEST_ABI=1 cargo test |
323 | | let test_abi = *TEST_ABI; |
324 | | let current_abi = LandlockStatus::current().into(); |
325 | | println!( |
326 | | "Current kernel version: {}", |
327 | | std::fs::read_to_string("/proc/version") |
328 | | .unwrap_or_else(|_| "unknown".into()) |
329 | | .trim() |
330 | | ); |
331 | | println!("Expected Landlock ABI {test_abi:?} whereas the current ABI is {current_abi:#?}"); |
332 | | assert_eq!(test_abi, current_abi); |
333 | | } |
334 | | |
335 | | // CompatState is not public outside this crate. |
336 | | /// Returned by ruleset builder. |
337 | | #[derive(Copy, Clone, Debug, PartialEq, Eq)] |
338 | | pub enum CompatState { |
339 | | /// Initial undefined state. |
340 | | Init, |
341 | | /// All requested restrictions are enforced. |
342 | | Full, |
343 | | /// Some requested restrictions are enforced, following a best-effort approach. |
344 | | Partial, |
345 | | /// The running system doesn't support Landlock. |
346 | | No, |
347 | | /// Final unsupported state. |
348 | | Dummy, |
349 | | } |
350 | | |
351 | | impl CompatState { |
352 | 0 | fn update(&mut self, other: Self) { |
353 | 0 | *self = match (*self, other) { |
354 | 0 | (CompatState::Init, other) => other, |
355 | 0 | (CompatState::Dummy, _) => CompatState::Dummy, |
356 | 0 | (_, CompatState::Dummy) => CompatState::Dummy, |
357 | 0 | (CompatState::No, CompatState::No) => CompatState::No, |
358 | 0 | (CompatState::Full, CompatState::Full) => CompatState::Full, |
359 | 0 | (_, _) => CompatState::Partial, |
360 | | } |
361 | 0 | } |
362 | | } |
363 | | |
364 | | #[test] |
365 | | fn compat_state_update_1() { |
366 | | let mut state = CompatState::Full; |
367 | | |
368 | | state.update(CompatState::Full); |
369 | | assert_eq!(state, CompatState::Full); |
370 | | |
371 | | state.update(CompatState::No); |
372 | | assert_eq!(state, CompatState::Partial); |
373 | | |
374 | | state.update(CompatState::Full); |
375 | | assert_eq!(state, CompatState::Partial); |
376 | | |
377 | | state.update(CompatState::Full); |
378 | | assert_eq!(state, CompatState::Partial); |
379 | | |
380 | | state.update(CompatState::No); |
381 | | assert_eq!(state, CompatState::Partial); |
382 | | |
383 | | state.update(CompatState::Dummy); |
384 | | assert_eq!(state, CompatState::Dummy); |
385 | | |
386 | | state.update(CompatState::Full); |
387 | | assert_eq!(state, CompatState::Dummy); |
388 | | } |
389 | | |
390 | | #[test] |
391 | | fn compat_state_update_2() { |
392 | | let mut state = CompatState::Full; |
393 | | |
394 | | state.update(CompatState::Full); |
395 | | assert_eq!(state, CompatState::Full); |
396 | | |
397 | | state.update(CompatState::No); |
398 | | assert_eq!(state, CompatState::Partial); |
399 | | |
400 | | state.update(CompatState::Full); |
401 | | assert_eq!(state, CompatState::Partial); |
402 | | } |
403 | | |
404 | | #[test] |
405 | | fn try_compat_binary_states() { |
406 | | // Supported: state -> Full. |
407 | | let mut compat: Compatibility = ABI::Unsupported.into(); |
408 | | assert_eq!(compat.state, CompatState::Init); |
409 | | assert_eq!(compat.try_compat_binary(true, || "err"), Ok(true)); |
410 | | assert_eq!(compat.state, CompatState::Full); |
411 | | |
412 | | // Unsupported + BestEffort: state -> Partial (Full + No). |
413 | | assert_eq!(compat.try_compat_binary(false, || "err"), Ok(false)); |
414 | | assert_eq!(compat.state, CompatState::Partial); |
415 | | |
416 | | // Unsupported + SoftRequirement: state -> Dummy. |
417 | | let mut compat: Compatibility = ABI::Unsupported.into(); |
418 | | compat.level = Some(CompatLevel::SoftRequirement); |
419 | | assert_eq!(compat.try_compat_binary(false, || "err"), Ok(false)); |
420 | | assert_eq!(compat.state, CompatState::Dummy); |
421 | | |
422 | | // Unsupported + HardRequirement: returns error. |
423 | | let mut compat: Compatibility = ABI::Unsupported.into(); |
424 | | compat.level = Some(CompatLevel::HardRequirement); |
425 | | assert_eq!(compat.try_compat_binary(false, || "err"), Err("err")); |
426 | | } |
427 | | |
428 | | #[cfg_attr(test, derive(PartialEq))] |
429 | | #[derive(Copy, Clone, Debug)] |
430 | | pub(crate) struct Compatibility { |
431 | | status: LandlockStatus, |
432 | | pub(crate) level: Option<CompatLevel>, |
433 | | pub(crate) state: CompatState, |
434 | | } |
435 | | |
436 | | impl From<LandlockStatus> for Compatibility { |
437 | 0 | fn from(status: LandlockStatus) -> Self { |
438 | 0 | Compatibility { |
439 | 0 | status, |
440 | 0 | level: Default::default(), |
441 | 0 | state: CompatState::Init, |
442 | 0 | } |
443 | 0 | } |
444 | | } |
445 | | |
446 | | #[cfg(test)] |
447 | | impl From<ABI> for Compatibility { |
448 | | fn from(abi: ABI) -> Self { |
449 | | Self::from(LandlockStatus::from(abi)) |
450 | | } |
451 | | } |
452 | | |
453 | | impl Compatibility { |
454 | | // Compatibility is a semi-opaque struct. |
455 | | #[allow(clippy::new_without_default)] |
456 | 0 | pub(crate) fn new() -> Self { |
457 | 0 | LandlockStatus::current().into() |
458 | 0 | } |
459 | | |
460 | 0 | pub(crate) fn update(&mut self, state: CompatState) { |
461 | 0 | self.state.update(state); |
462 | 0 | } |
463 | | |
464 | 0 | pub(crate) fn abi(&self) -> ABI { |
465 | 0 | self.status.into() |
466 | 0 | } |
467 | | |
468 | 0 | pub(crate) fn status(&self) -> LandlockStatus { |
469 | 0 | self.status |
470 | 0 | } |
471 | | |
472 | | /// Handles the compat dispatch for a binary supported/not-supported check. |
473 | | /// |
474 | | /// This is factored out from the No branch of |
475 | | /// [`TryCompat::try_compat()`](crate::TryCompat::try_compat) for use by |
476 | | /// [`SyscallFlagExt::try_compat()`](crate::flags::SyscallFlagExt::try_compat), |
477 | | /// where a single flag is either fully supported or not (no Partial case). |
478 | | /// |
479 | | /// Returns `Ok(true)` if supported (caller should apply the flag), |
480 | | /// `Ok(false)` if unsupported but acceptable |
481 | | /// ([`BestEffort`](crate::CompatLevel::BestEffort) / |
482 | | /// [`SoftRequirement`](crate::CompatLevel::SoftRequirement)), or `Err` if |
483 | | /// unsupported with [`HardRequirement`](crate::CompatLevel::HardRequirement). |
484 | 0 | pub(crate) fn try_compat_binary<E, F>( |
485 | 0 | &mut self, |
486 | 0 | supported: bool, |
487 | 0 | make_error: F, |
488 | 0 | ) -> Result<bool, E> |
489 | 0 | where |
490 | 0 | F: FnOnce() -> E, |
491 | | { |
492 | 0 | if supported { |
493 | 0 | self.state.update(CompatState::Full); |
494 | 0 | Ok(true) |
495 | | } else { |
496 | 0 | match self.level.into() { |
497 | | CompatLevel::BestEffort => { |
498 | 0 | self.state.update(CompatState::No); |
499 | 0 | Ok(false) |
500 | | } |
501 | | CompatLevel::SoftRequirement => { |
502 | 0 | self.state.update(CompatState::Dummy); |
503 | 0 | Ok(false) |
504 | | } |
505 | 0 | CompatLevel::HardRequirement => Err(make_error()), |
506 | | } |
507 | | } |
508 | 0 | } |
509 | | } |
510 | | |
511 | | pub(crate) mod private { |
512 | | use crate::CompatLevel; |
513 | | |
514 | | pub trait OptionCompatLevelMut { |
515 | | fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel>; |
516 | | } |
517 | | } |
518 | | |
519 | | /// Properly handles runtime unsupported features. |
520 | | /// |
521 | | /// This guarantees consistent behaviors across crate users |
522 | | /// and runtime kernels even if this crate get new features. |
523 | | /// It eases backward compatibility and enables future-proofness. |
524 | | /// |
525 | | /// Landlock is a security feature designed to help improve security of a running system |
526 | | /// thanks to application developers. |
527 | | /// To protect users as much as possible, |
528 | | /// compatibility with the running system should then be handled in a best-effort way, |
529 | | /// contrary to common system features. |
530 | | /// In some circumstances |
531 | | /// (e.g. applications carefully designed to only be run with a specific set of kernel features), |
532 | | /// it may be required to error out if some of these features are not available |
533 | | /// and will then not be enforced. |
534 | | pub trait Compatible: Sized + private::OptionCompatLevelMut { |
535 | | /// To enable a best-effort security approach, |
536 | | /// Landlock features that are not supported by the running system |
537 | | /// are silently ignored by default, |
538 | | /// which is a sane choice for most use cases. |
539 | | /// However, on some rare circumstances, |
540 | | /// developers may want to have some guarantees that their applications |
541 | | /// will not run if a certain level of sandboxing is not possible. |
542 | | /// If we really want to error out when not all our requested requirements are met, |
543 | | /// then we can configure it with `set_compatibility()`. |
544 | | /// |
545 | | /// The `Compatible` trait is implemented for all object builders |
546 | | /// (e.g. [`Ruleset`](crate::Ruleset)). |
547 | | /// Such builders have a set of methods to incrementally build an object. |
548 | | /// These build methods rely on kernel features that may not be available at runtime. |
549 | | /// The `set_compatibility()` method enables to control the effect of |
550 | | /// the following build method calls starting after the `set_compatibility()` call. |
551 | | /// Such effect can be: |
552 | | /// * to silently ignore unsupported features |
553 | | /// and continue building ([`CompatLevel::BestEffort`]); |
554 | | /// * to silently ignore unsupported features |
555 | | /// and ignore the whole build ([`CompatLevel::SoftRequirement`]); |
556 | | /// * to return an error for any unsupported feature ([`CompatLevel::HardRequirement`]). |
557 | | /// |
558 | | /// Taking [`Ruleset`](crate::Ruleset) as an example, |
559 | | /// the [`handle_access()`](crate::RulesetAttr::handle_access()) build method |
560 | | /// returns a [`Result`] that can be [`Err(RulesetError)`](crate::RulesetError) |
561 | | /// with a nested [`CompatError`]. |
562 | | /// Such error can only occur with a running Linux kernel not supporting the requested |
563 | | /// Landlock accesses *and* if the current compatibility level is |
564 | | /// [`CompatLevel::HardRequirement`]. |
565 | | /// However, such error is not possible with [`CompatLevel::BestEffort`] |
566 | | /// nor [`CompatLevel::SoftRequirement`]. |
567 | | /// |
568 | | /// The order of this call is important because |
569 | | /// it defines the behavior of the following build method calls that return a [`Result`]. |
570 | | /// If `set_compatibility(CompatLevel::HardRequirement)` is called on an object, |
571 | | /// then a [`CompatError`] may be returned for the next method calls, |
572 | | /// until the next call to `set_compatibility()`. |
573 | | /// This enables to change the behavior of a set of build method calls, |
574 | | /// for instance to be sure that the sandbox will at least restrict some access rights. |
575 | | /// |
576 | | /// New objects inherit the compatibility configuration of their parents, if any. |
577 | | /// For instance, [`Ruleset::create()`](crate::Ruleset::create()) returns |
578 | | /// a [`RulesetCreated`](crate::RulesetCreated) object that inherits the |
579 | | /// `Ruleset`'s compatibility configuration. |
580 | | /// |
581 | | /// # Example with `SoftRequirement` |
582 | | /// |
583 | | /// Let's say an application legitimately needs to rename files between directories. |
584 | | /// Because of [previous Landlock limitations](https://docs.kernel.org/userspace-api/landlock.html#file-renaming-and-linking-abi-2), |
585 | | /// this was forbidden with the [first version of Landlock](ABI::V1), |
586 | | /// but it is now handled starting with the [second version](ABI::V2). |
587 | | /// For this use case, we only want the application to be sandboxed |
588 | | /// if we have the guarantee that it will not break a legitimate usage (i.e. rename files). |
589 | | /// We then create a ruleset which will either support file renaming |
590 | | /// (thanks to [`AccessFs::Refer`](crate::AccessFs::Refer)) or silently do nothing. |
591 | | /// |
592 | | /// ``` |
593 | | /// use landlock::*; |
594 | | /// |
595 | | /// fn ruleset_handling_renames() -> Result<RulesetCreated, RulesetError> { |
596 | | /// Ok(Ruleset::default() |
597 | | /// // This ruleset must either handle the AccessFs::Refer right, |
598 | | /// // or it must silently ignore the whole sandboxing. |
599 | | /// .set_compatibility(CompatLevel::SoftRequirement) |
600 | | /// .handle_access(AccessFs::Refer)? |
601 | | /// // However, this ruleset may also handle other (future) access rights |
602 | | /// // if they are supported by the running kernel. |
603 | | /// .set_compatibility(CompatLevel::BestEffort) |
604 | | /// .handle_access(AccessFs::from_all(ABI::V7))? |
605 | | /// .create()?) |
606 | | /// } |
607 | | /// ``` |
608 | | /// |
609 | | /// # Example with `HardRequirement` |
610 | | /// |
611 | | /// Security-dedicated applications may want to ensure that |
612 | | /// an untrusted software component is subject to a minimum of restrictions before launching it. |
613 | | /// In this case, we want to create a ruleset which will at least support |
614 | | /// all restrictions provided by the [first version of Landlock](ABI::V1), |
615 | | /// and opportunistically handle restrictions supported by newer kernels. |
616 | | /// |
617 | | /// ``` |
618 | | /// use landlock::*; |
619 | | /// |
620 | | /// fn ruleset_fragile() -> Result<RulesetCreated, RulesetError> { |
621 | | /// Ok(Ruleset::default() |
622 | | /// // This ruleset must either handle at least all accesses defined by |
623 | | /// // the first Landlock version (e.g. AccessFs::WriteFile), |
624 | | /// // or the following handle_access() call must return a wrapped |
625 | | /// // AccessError<AccessFs>::Incompatible error. |
626 | | /// .set_compatibility(CompatLevel::HardRequirement) |
627 | | /// .handle_access(AccessFs::from_all(ABI::V1))? |
628 | | /// // However, this ruleset may also handle new access rights |
629 | | /// // (e.g. AccessFs::Refer defined by the second version of Landlock) |
630 | | /// // if they are supported by the running kernel, |
631 | | /// // but without returning any error otherwise. |
632 | | /// .set_compatibility(CompatLevel::BestEffort) |
633 | | /// .handle_access(AccessFs::from_all(ABI::V7))? |
634 | | /// .create()?) |
635 | | /// } |
636 | | /// ``` |
637 | 0 | fn set_compatibility(mut self, level: CompatLevel) -> Self { |
638 | 0 | *self.as_option_compat_level_mut() = Some(level); |
639 | 0 | self |
640 | 0 | } Unexecuted instantiation: <landlock::ruleset::Ruleset as landlock::compat::Compatible>::set_compatibility Unexecuted instantiation: <_ as landlock::compat::Compatible>::set_compatibility |
641 | | |
642 | | /// Cf. [`set_compatibility()`](Compatible::set_compatibility()): |
643 | | /// |
644 | | /// - `set_best_effort(true)` translates to `set_compatibility(CompatLevel::BestEffort)`. |
645 | | /// |
646 | | /// - `set_best_effort(false)` translates to `set_compatibility(CompatLevel::HardRequirement)`. |
647 | | #[deprecated(note = "Use set_compatibility() instead")] |
648 | 0 | fn set_best_effort(self, best_effort: bool) -> Self |
649 | 0 | where |
650 | 0 | Self: Sized, |
651 | | { |
652 | 0 | self.set_compatibility(match best_effort { |
653 | 0 | true => CompatLevel::BestEffort, |
654 | 0 | false => CompatLevel::HardRequirement, |
655 | | }) |
656 | 0 | } |
657 | | } |
658 | | |
659 | | #[test] |
660 | | #[allow(deprecated)] |
661 | | fn deprecated_set_best_effort() { |
662 | | use crate::{CompatLevel, Compatible, Ruleset}; |
663 | | |
664 | | assert_eq!( |
665 | | Ruleset::default().set_best_effort(true).compat, |
666 | | Ruleset::default() |
667 | | .set_compatibility(CompatLevel::BestEffort) |
668 | | .compat |
669 | | ); |
670 | | assert_eq!( |
671 | | Ruleset::default().set_best_effort(false).compat, |
672 | | Ruleset::default() |
673 | | .set_compatibility(CompatLevel::HardRequirement) |
674 | | .compat |
675 | | ); |
676 | | } |
677 | | |
678 | | /// See the [`Compatible`] documentation. |
679 | | #[cfg_attr(test, derive(EnumIter))] |
680 | | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
681 | | pub enum CompatLevel { |
682 | | /// Takes into account the build requests if they are supported by the running system, |
683 | | /// or silently ignores them otherwise. |
684 | | /// Never returns a compatibility error. |
685 | | #[default] |
686 | | BestEffort, |
687 | | /// Takes into account the build requests if they are supported by the running system, |
688 | | /// or silently ignores the whole build object otherwise. |
689 | | /// Never returns a compatibility error. |
690 | | /// If not supported, |
691 | | /// the call to [`RulesetCreated::restrict_self()`](crate::RulesetCreated::restrict_self()) |
692 | | /// will return a |
693 | | /// [`RestrictionStatus { ruleset: RulesetStatus::NotEnforced, no_new_privs: false, }`](crate::RestrictionStatus). |
694 | | SoftRequirement, |
695 | | /// Takes into account the build requests if they are supported by the running system, |
696 | | /// or returns a compatibility error otherwise ([`CompatError`]). |
697 | | HardRequirement, |
698 | | } |
699 | | |
700 | | impl From<Option<CompatLevel>> for CompatLevel { |
701 | 0 | fn from(opt: Option<CompatLevel>) -> Self { |
702 | 0 | match opt { |
703 | 0 | None => CompatLevel::default(), |
704 | 0 | Some(ref level) => *level, |
705 | | } |
706 | 0 | } |
707 | | } |
708 | | |
709 | | // TailoredCompatLevel could be replaced with AsMut<Option<CompatLevel>>, but only traits defined |
710 | | // in the current crate can be implemented for types defined outside of the crate. Furthermore it |
711 | | // provides a default implementation which is handy for types such as BitFlags. |
712 | | pub trait TailoredCompatLevel { |
713 | 0 | fn tailored_compat_level<L>(&mut self, parent_level: L) -> CompatLevel |
714 | 0 | where |
715 | 0 | L: Into<CompatLevel>, |
716 | | { |
717 | 0 | parent_level.into() |
718 | 0 | } Unexecuted instantiation: <enumflags2::BitFlags<landlock::fs::AccessFs, u64> as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<landlock::compat::CompatLevel> Unexecuted instantiation: <enumflags2::BitFlags<landlock::fs::AccessFs, u64> as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<core::option::Option<landlock::compat::CompatLevel>> Unexecuted instantiation: <enumflags2::BitFlags<landlock::net::AccessNet, u64> as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<core::option::Option<landlock::compat::CompatLevel>> |
719 | | } |
720 | | |
721 | | impl<T> TailoredCompatLevel for T |
722 | | where |
723 | | Self: Compatible, |
724 | | { |
725 | | // Every Compatible trait implementation returns its own compatibility level, if set. |
726 | 0 | fn tailored_compat_level<L>(&mut self, parent_level: L) -> CompatLevel |
727 | 0 | where |
728 | 0 | L: Into<CompatLevel>, |
729 | | { |
730 | | // Using a mutable reference is not required but it makes the code simpler (no double AsRef |
731 | | // implementations for each Compatible types), and more importantly it guarantees |
732 | | // consistency with Compatible::set_compatibility(). |
733 | 0 | match self.as_option_compat_level_mut() { |
734 | 0 | None => parent_level.into(), |
735 | | // Returns the most constrained compatibility level. |
736 | 0 | Some(ref level) => parent_level.into().max(*level), |
737 | | } |
738 | 0 | } Unexecuted instantiation: <landlock::fs::PathBeneath<landlock::fs::PathFd> as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<core::option::Option<landlock::compat::CompatLevel>> Unexecuted instantiation: <landlock::fs::PathBeneath<landlock::fs::PathFd> as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<landlock::compat::CompatLevel> Unexecuted instantiation: <_ as landlock::compat::TailoredCompatLevel>::tailored_compat_level::<_> |
739 | | } |
740 | | |
741 | | #[test] |
742 | | fn tailored_compat_level() { |
743 | | use crate::{AccessFs, PathBeneath, PathFd}; |
744 | | |
745 | | fn new_path(level: CompatLevel) -> PathBeneath<PathFd> { |
746 | | PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute).set_compatibility(level) |
747 | | } |
748 | | |
749 | | for parent_level in CompatLevel::iter() { |
750 | | assert_eq!( |
751 | | new_path(CompatLevel::BestEffort).tailored_compat_level(parent_level), |
752 | | parent_level |
753 | | ); |
754 | | assert_eq!( |
755 | | new_path(CompatLevel::HardRequirement).tailored_compat_level(parent_level), |
756 | | CompatLevel::HardRequirement |
757 | | ); |
758 | | } |
759 | | |
760 | | assert_eq!( |
761 | | new_path(CompatLevel::SoftRequirement).tailored_compat_level(CompatLevel::SoftRequirement), |
762 | | CompatLevel::SoftRequirement |
763 | | ); |
764 | | |
765 | | for child_level in CompatLevel::iter() { |
766 | | assert_eq!( |
767 | | new_path(child_level).tailored_compat_level(CompatLevel::BestEffort), |
768 | | child_level |
769 | | ); |
770 | | assert_eq!( |
771 | | new_path(child_level).tailored_compat_level(CompatLevel::HardRequirement), |
772 | | CompatLevel::HardRequirement |
773 | | ); |
774 | | } |
775 | | } |
776 | | |
777 | | // CompatResult is not public outside this crate. |
778 | | pub enum CompatResult<A> |
779 | | where |
780 | | A: Access, |
781 | | { |
782 | | // Fully matches the request. |
783 | | Full, |
784 | | // Partially matches the request. |
785 | | Partial(CompatError<A>), |
786 | | // Doesn't matches the request. |
787 | | No(CompatError<A>), |
788 | | } |
789 | | |
790 | | // TryCompat is not public outside this crate. |
791 | | pub trait TryCompat<A> |
792 | | where |
793 | | Self: Sized + TailoredCompatLevel, |
794 | | A: Access, |
795 | | { |
796 | | fn try_compat_inner(&mut self, abi: ABI) -> Result<CompatResult<A>, CompatError<A>>; |
797 | | |
798 | | // Default implementation for objects without children. |
799 | | // |
800 | | // If returning something other than Ok(Some(self)), the implementation must use its own |
801 | | // compatibility level, if any, with self.tailored_compat_level(default_compat_level), and pass |
802 | | // it with the abi and compat_state to each child.try_compat(). See PathBeneath implementation |
803 | | // and the self.allowed_access.try_compat() call. |
804 | | // |
805 | | // # Warning |
806 | | // |
807 | | // Errors must be prioritized over incompatibility (i.e. return Err(e) over Ok(None)) for all |
808 | | // children. |
809 | 0 | fn try_compat_children<L>( |
810 | 0 | self, |
811 | 0 | _abi: ABI, |
812 | 0 | _parent_level: L, |
813 | 0 | _compat_state: &mut CompatState, |
814 | 0 | ) -> Result<Option<Self>, CompatError<A>> |
815 | 0 | where |
816 | 0 | L: Into<CompatLevel>, |
817 | | { |
818 | 0 | Ok(Some(self)) |
819 | 0 | } Unexecuted instantiation: <enumflags2::BitFlags<landlock::fs::AccessFs, u64> as landlock::compat::TryCompat<landlock::fs::AccessFs>>::try_compat_children::<landlock::compat::CompatLevel> Unexecuted instantiation: <enumflags2::BitFlags<landlock::net::AccessNet, u64> as landlock::compat::TryCompat<landlock::net::AccessNet>>::try_compat_children::<landlock::compat::CompatLevel> |
820 | | |
821 | | // Update compat_state and return an error according to try_compat_*() error, or to the |
822 | | // compatibility level, i.e. either route compatible object or error. |
823 | 0 | fn try_compat<L>( |
824 | 0 | mut self, |
825 | 0 | abi: ABI, |
826 | 0 | parent_level: L, |
827 | 0 | compat_state: &mut CompatState, |
828 | 0 | ) -> Result<Option<Self>, CompatError<A>> |
829 | 0 | where |
830 | 0 | L: Into<CompatLevel>, |
831 | | { |
832 | 0 | let compat_level = self.tailored_compat_level(parent_level); |
833 | 0 | let some_inner = match self.try_compat_inner(abi) { |
834 | | Ok(CompatResult::Full) => { |
835 | 0 | compat_state.update(CompatState::Full); |
836 | 0 | true |
837 | | } |
838 | 0 | Ok(CompatResult::Partial(error)) => match compat_level { |
839 | | CompatLevel::BestEffort => { |
840 | 0 | compat_state.update(CompatState::Partial); |
841 | 0 | true |
842 | | } |
843 | | CompatLevel::SoftRequirement => { |
844 | 0 | compat_state.update(CompatState::Dummy); |
845 | 0 | false |
846 | | } |
847 | | CompatLevel::HardRequirement => { |
848 | 0 | compat_state.update(CompatState::Dummy); |
849 | 0 | return Err(error); |
850 | | } |
851 | | }, |
852 | 0 | Ok(CompatResult::No(error)) => match compat_level { |
853 | | CompatLevel::BestEffort => { |
854 | 0 | compat_state.update(CompatState::No); |
855 | 0 | false |
856 | | } |
857 | | CompatLevel::SoftRequirement => { |
858 | 0 | compat_state.update(CompatState::Dummy); |
859 | 0 | false |
860 | | } |
861 | | CompatLevel::HardRequirement => { |
862 | 0 | compat_state.update(CompatState::Dummy); |
863 | 0 | return Err(error); |
864 | | } |
865 | | }, |
866 | 0 | Err(error) => { |
867 | | // Safeguard to help for test consistency. |
868 | 0 | compat_state.update(CompatState::Dummy); |
869 | 0 | return Err(error); |
870 | | } |
871 | | }; |
872 | | |
873 | | // At this point, any inner error have been returned, so we can proceed with |
874 | | // try_compat_children()?. |
875 | 0 | match self.try_compat_children(abi, compat_level, compat_state)? { |
876 | 0 | Some(n) if some_inner => Ok(Some(n)), |
877 | 0 | _ => Ok(None), |
878 | | } |
879 | 0 | } Unexecuted instantiation: <landlock::fs::PathBeneath<landlock::fs::PathFd> as landlock::compat::TryCompat<landlock::fs::AccessFs>>::try_compat::<core::option::Option<landlock::compat::CompatLevel>> Unexecuted instantiation: <enumflags2::BitFlags<landlock::fs::AccessFs, u64> as landlock::compat::TryCompat<landlock::fs::AccessFs>>::try_compat::<landlock::compat::CompatLevel> Unexecuted instantiation: <enumflags2::BitFlags<landlock::fs::AccessFs, u64> as landlock::compat::TryCompat<landlock::fs::AccessFs>>::try_compat::<core::option::Option<landlock::compat::CompatLevel>> Unexecuted instantiation: <enumflags2::BitFlags<landlock::net::AccessNet, u64> as landlock::compat::TryCompat<landlock::net::AccessNet>>::try_compat::<core::option::Option<landlock::compat::CompatLevel>> |
880 | | } |