/rust/registry/src/index.crates.io-6f17d22bba15001f/landlock-0.4.1/src/ruleset.rs
Line | Count | Source (jump to first uncovered line) |
1 | | use crate::compat::private::OptionCompatLevelMut; |
2 | | use crate::{ |
3 | | uapi, Access, AccessFs, AccessNet, AddRuleError, AddRulesError, BitFlags, CompatLevel, |
4 | | CompatState, Compatibility, Compatible, CreateRulesetError, RestrictSelfError, RulesetError, |
5 | | TryCompat, |
6 | | }; |
7 | | use libc::close; |
8 | | use std::io::Error; |
9 | | use std::mem::size_of_val; |
10 | | use std::os::unix::io::RawFd; |
11 | | |
12 | | #[cfg(test)] |
13 | | use crate::*; |
14 | | |
15 | | // Public interface without methods and which is impossible to implement outside this crate. |
16 | | pub trait Rule<T>: PrivateRule<T> |
17 | | where |
18 | | T: Access, |
19 | | { |
20 | | } |
21 | | |
22 | | // PrivateRule is not public outside this crate. |
23 | | pub trait PrivateRule<T> |
24 | | where |
25 | | Self: TryCompat<T> + Compatible, |
26 | | T: Access, |
27 | | { |
28 | | const TYPE_ID: uapi::landlock_rule_type; |
29 | | |
30 | | /// Returns a raw pointer to the rule's inner attribute. |
31 | | /// |
32 | | /// The caller must ensure that the rule outlives the pointer this function returns, or else it |
33 | | /// will end up pointing to garbage. |
34 | | fn as_ptr(&mut self) -> *const libc::c_void; |
35 | | |
36 | | fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError>; |
37 | | } |
38 | | |
39 | | /// Enforcement status of a ruleset. |
40 | | #[derive(Debug, PartialEq, Eq)] |
41 | | pub enum RulesetStatus { |
42 | | /// All requested restrictions are enforced. |
43 | | FullyEnforced, |
44 | | /// Some requested restrictions are enforced, |
45 | | /// following a best-effort approach. |
46 | | PartiallyEnforced, |
47 | | /// The running system doesn't support Landlock |
48 | | /// or a subset of the requested Landlock features. |
49 | | NotEnforced, |
50 | | } |
51 | | |
52 | | impl From<CompatState> for RulesetStatus { |
53 | 0 | fn from(state: CompatState) -> Self { |
54 | 0 | match state { |
55 | 0 | CompatState::Init | CompatState::No | CompatState::Dummy => RulesetStatus::NotEnforced, |
56 | 0 | CompatState::Full => RulesetStatus::FullyEnforced, |
57 | 0 | CompatState::Partial => RulesetStatus::PartiallyEnforced, |
58 | | } |
59 | 0 | } |
60 | | } |
61 | | |
62 | | // The Debug, PartialEq and Eq implementations are useful for crate users to debug and check the |
63 | | // result of a Landlock ruleset enforcement. |
64 | | /// Status of a [`RulesetCreated`] |
65 | | /// after calling [`restrict_self()`](RulesetCreated::restrict_self). |
66 | | #[derive(Debug, PartialEq, Eq)] |
67 | | #[non_exhaustive] |
68 | | pub struct RestrictionStatus { |
69 | | /// Status of the Landlock ruleset enforcement. |
70 | | pub ruleset: RulesetStatus, |
71 | | /// Status of `prctl(2)`'s `PR_SET_NO_NEW_PRIVS` enforcement. |
72 | | pub no_new_privs: bool, |
73 | | } |
74 | | |
75 | 0 | fn prctl_set_no_new_privs() -> Result<(), Error> { |
76 | 0 | match unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } { |
77 | 0 | 0 => Ok(()), |
78 | 0 | _ => Err(Error::last_os_error()), |
79 | | } |
80 | 0 | } |
81 | | |
82 | 0 | fn support_no_new_privs() -> bool { |
83 | | // Only Linux < 3.5 or kernel with seccomp filters should return an error. |
84 | 0 | matches!( |
85 | 0 | unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) }, |
86 | | 0 | 1 |
87 | | ) |
88 | 0 | } |
89 | | |
90 | | /// Landlock ruleset builder. |
91 | | /// |
92 | | /// `Ruleset` enables to create a Landlock ruleset in a flexible way |
93 | | /// following the builder pattern. |
94 | | /// Most build steps return a [`Result`] with [`RulesetError`]. |
95 | | /// |
96 | | /// You should probably not create more than one ruleset per application. |
97 | | /// Creating multiple rulesets is only useful when gradually restricting an application |
98 | | /// (e.g., a first set of generic restrictions before reading any file, |
99 | | /// then a second set of tailored restrictions after reading the configuration). |
100 | | /// |
101 | | /// # Simple example |
102 | | /// |
103 | | /// Simple helper handling only Landlock-related errors. |
104 | | /// |
105 | | /// ``` |
106 | | /// use landlock::{ |
107 | | /// Access, AccessFs, PathBeneath, PathFd, RestrictionStatus, Ruleset, RulesetAttr, |
108 | | /// RulesetCreatedAttr, RulesetError, ABI, |
109 | | /// }; |
110 | | /// use std::os::unix::io::AsFd; |
111 | | /// |
112 | | /// fn restrict_fd<T>(hierarchy: T) -> Result<RestrictionStatus, RulesetError> |
113 | | /// where |
114 | | /// T: AsFd, |
115 | | /// { |
116 | | /// // The Landlock ABI should be incremented (and tested) regularly. |
117 | | /// let abi = ABI::V1; |
118 | | /// let access_all = AccessFs::from_all(abi); |
119 | | /// let access_read = AccessFs::from_read(abi); |
120 | | /// Ok(Ruleset::default() |
121 | | /// .handle_access(access_all)? |
122 | | /// .create()? |
123 | | /// .add_rule(PathBeneath::new(hierarchy, access_read))? |
124 | | /// .restrict_self()?) |
125 | | /// } |
126 | | /// |
127 | | /// let fd = PathFd::new("/home").expect("failed to open /home"); |
128 | | /// let status = restrict_fd(fd).expect("failed to build the ruleset"); |
129 | | /// ``` |
130 | | /// |
131 | | /// # Generic example |
132 | | /// |
133 | | /// More generic helper handling a set of file hierarchies |
134 | | /// and multiple types of error (i.e. [`RulesetError`](crate::RulesetError) |
135 | | /// and [`PathFdError`](crate::PathFdError). |
136 | | /// |
137 | | /// ``` |
138 | | /// use landlock::{ |
139 | | /// Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, |
140 | | /// RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, |
141 | | /// }; |
142 | | /// use thiserror::Error; |
143 | | /// |
144 | | /// #[derive(Debug, Error)] |
145 | | /// enum MyRestrictError { |
146 | | /// #[error(transparent)] |
147 | | /// Ruleset(#[from] RulesetError), |
148 | | /// #[error(transparent)] |
149 | | /// AddRule(#[from] PathFdError), |
150 | | /// } |
151 | | /// |
152 | | /// fn restrict_paths(hierarchies: &[&str]) -> Result<RestrictionStatus, MyRestrictError> { |
153 | | /// // The Landlock ABI should be incremented (and tested) regularly. |
154 | | /// let abi = ABI::V1; |
155 | | /// let access_all = AccessFs::from_all(abi); |
156 | | /// let access_read = AccessFs::from_read(abi); |
157 | | /// Ok(Ruleset::default() |
158 | | /// .handle_access(access_all)? |
159 | | /// .create()? |
160 | | /// .add_rules( |
161 | | /// hierarchies |
162 | | /// .iter() |
163 | | /// .map::<Result<_, MyRestrictError>, _>(|p| { |
164 | | /// Ok(PathBeneath::new(PathFd::new(p)?, access_read)) |
165 | | /// }), |
166 | | /// )? |
167 | | /// .restrict_self()?) |
168 | | /// } |
169 | | /// |
170 | | /// let status = restrict_paths(&["/usr", "/home"]).expect("failed to build the ruleset"); |
171 | | /// ``` |
172 | | #[cfg_attr(test, derive(Debug))] |
173 | | pub struct Ruleset { |
174 | | pub(crate) requested_handled_fs: BitFlags<AccessFs>, |
175 | | pub(crate) requested_handled_net: BitFlags<AccessNet>, |
176 | | pub(crate) actual_handled_fs: BitFlags<AccessFs>, |
177 | | pub(crate) actual_handled_net: BitFlags<AccessNet>, |
178 | | pub(crate) compat: Compatibility, |
179 | | } |
180 | | |
181 | | impl From<Compatibility> for Ruleset { |
182 | 0 | fn from(compat: Compatibility) -> Self { |
183 | 0 | Ruleset { |
184 | 0 | // Non-working default handled FS accesses to force users to set them explicitely. |
185 | 0 | requested_handled_fs: Default::default(), |
186 | 0 | requested_handled_net: Default::default(), |
187 | 0 | actual_handled_fs: Default::default(), |
188 | 0 | actual_handled_net: Default::default(), |
189 | 0 | compat, |
190 | 0 | } |
191 | 0 | } |
192 | | } |
193 | | |
194 | | #[cfg(test)] |
195 | | impl From<ABI> for Ruleset { |
196 | | fn from(abi: ABI) -> Self { |
197 | | Ruleset::from(Compatibility::from(abi)) |
198 | | } |
199 | | } |
200 | | |
201 | | #[test] |
202 | | fn ruleset_add_rule_iter() { |
203 | | assert!(matches!( |
204 | | Ruleset::from(ABI::Unsupported) |
205 | | .handle_access(AccessFs::Execute) |
206 | | .unwrap() |
207 | | .create() |
208 | | .unwrap() |
209 | | .add_rule(PathBeneath::new( |
210 | | PathFd::new("/").unwrap(), |
211 | | AccessFs::ReadFile |
212 | | )) |
213 | | .unwrap_err(), |
214 | | RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. })) |
215 | | )); |
216 | | } |
217 | | |
218 | | impl Default for Ruleset { |
219 | | /// Returns a new `Ruleset`. |
220 | | /// This call automatically probes the running kernel to know if it supports Landlock. |
221 | | /// |
222 | | /// To be able to successfully call [`create()`](Ruleset::create), |
223 | | /// it is required to set the handled accesses with |
224 | | /// [`handle_access()`](Ruleset::handle_access). |
225 | 0 | fn default() -> Self { |
226 | 0 | // The API should be future-proof: one Rust program or library should have the same |
227 | 0 | // behavior if built with an old or a newer crate (e.g. with an extended ruleset_attr |
228 | 0 | // enum). It should then not be possible to give an "all-possible-handled-accesses" to the |
229 | 0 | // Ruleset builder because this value would be relative to the running kernel. |
230 | 0 | Compatibility::new().into() |
231 | 0 | } |
232 | | } |
233 | | |
234 | | impl Ruleset { |
235 | | #[allow(clippy::new_without_default)] |
236 | | #[deprecated(note = "Use Ruleset::default() instead")] |
237 | 0 | pub fn new() -> Self { |
238 | 0 | Ruleset::default() |
239 | 0 | } |
240 | | |
241 | | /// Attempts to create a real Landlock ruleset (if supported by the running kernel). |
242 | | /// The returned [`RulesetCreated`] is also a builder. |
243 | | /// |
244 | | /// On error, returns a wrapped [`CreateRulesetError`]. |
245 | 0 | pub fn create(mut self) -> Result<RulesetCreated, RulesetError> { |
246 | 0 | let body = || -> Result<RulesetCreated, CreateRulesetError> { |
247 | 0 | match self.compat.state { |
248 | | CompatState::Init => { |
249 | | // Checks that there is at least one requested access (e.g. |
250 | | // requested_handled_fs): one call to handle_access(). |
251 | 0 | Err(CreateRulesetError::MissingHandledAccess) |
252 | | } |
253 | | CompatState::No | CompatState::Dummy => { |
254 | | // There is at least one requested access. |
255 | | #[cfg(test)] |
256 | | assert!( |
257 | | !self.requested_handled_fs.is_empty() |
258 | | || !self.requested_handled_net.is_empty() |
259 | | ); |
260 | | |
261 | | // CompatState::No should be handled as CompatState::Dummy because it is not |
262 | | // possible to create an actual ruleset. |
263 | 0 | self.compat.update(CompatState::Dummy); |
264 | 0 | match self.compat.level.into() { |
265 | | CompatLevel::HardRequirement => { |
266 | 0 | Err(CreateRulesetError::MissingHandledAccess) |
267 | | } |
268 | 0 | _ => Ok(RulesetCreated::new(self, -1)), |
269 | | } |
270 | | } |
271 | | CompatState::Full | CompatState::Partial => { |
272 | | // There is at least one actual handled access. |
273 | | #[cfg(test)] |
274 | | assert!( |
275 | | !self.actual_handled_fs.is_empty() || !self.actual_handled_net.is_empty() |
276 | | ); |
277 | | |
278 | 0 | let attr = uapi::landlock_ruleset_attr { |
279 | 0 | handled_access_fs: self.actual_handled_fs.bits(), |
280 | 0 | handled_access_net: self.actual_handled_net.bits(), |
281 | 0 | }; |
282 | 0 | match unsafe { uapi::landlock_create_ruleset(&attr, size_of_val(&attr), 0) } { |
283 | 0 | fd if fd >= 0 => Ok(RulesetCreated::new(self, fd)), |
284 | 0 | _ => Err(CreateRulesetError::CreateRulesetCall { |
285 | 0 | source: Error::last_os_error(), |
286 | 0 | }), |
287 | | } |
288 | | } |
289 | | } |
290 | 0 | }; |
291 | 0 | Ok(body()?) |
292 | 0 | } |
293 | | } |
294 | | |
295 | | impl OptionCompatLevelMut for Ruleset { |
296 | 0 | fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> { |
297 | 0 | &mut self.compat.level |
298 | 0 | } |
299 | | } |
300 | | |
301 | | impl OptionCompatLevelMut for &mut Ruleset { |
302 | 0 | fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> { |
303 | 0 | &mut self.compat.level |
304 | 0 | } |
305 | | } |
306 | | |
307 | | impl Compatible for Ruleset {} |
308 | | |
309 | | impl Compatible for &mut Ruleset {} |
310 | | |
311 | | impl AsMut<Ruleset> for Ruleset { |
312 | 0 | fn as_mut(&mut self) -> &mut Ruleset { |
313 | 0 | self |
314 | 0 | } |
315 | | } |
316 | | |
317 | | // Tests unambiguous type. |
318 | | #[test] |
319 | | fn ruleset_as_mut() { |
320 | | let mut ruleset = Ruleset::from(ABI::Unsupported); |
321 | | let _ = ruleset.as_mut(); |
322 | | |
323 | | let mut ruleset_created = Ruleset::from(ABI::Unsupported) |
324 | | .handle_access(AccessFs::Execute) |
325 | | .unwrap() |
326 | | .create() |
327 | | .unwrap(); |
328 | | let _ = ruleset_created.as_mut(); |
329 | | } |
330 | | |
331 | | pub trait RulesetAttr: Sized + AsMut<Ruleset> + Compatible { |
332 | | /// Attempts to add a set of access rights that will be supported by this ruleset. |
333 | | /// By default, all actions requiring these access rights will be denied. |
334 | | /// Consecutive calls to `handle_access()` will be interpreted as logical ORs |
335 | | /// with the previous handled accesses. |
336 | | /// |
337 | | /// On error, returns a wrapped [`HandleAccessesError`](crate::HandleAccessesError). |
338 | | /// E.g., `RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError<AccessFs>))` |
339 | 0 | fn handle_access<T, U>(mut self, access: T) -> Result<Self, RulesetError> |
340 | 0 | where |
341 | 0 | T: Into<BitFlags<U>>, |
342 | 0 | U: Access, |
343 | 0 | { |
344 | 0 | U::ruleset_handle_access(self.as_mut(), access.into())?; |
345 | 0 | Ok(self) |
346 | 0 | } Unexecuted instantiation: <landlock::ruleset::Ruleset as landlock::ruleset::RulesetAttr>::handle_access::<enumflags2::BitFlags<landlock::fs::AccessFs, u64>, landlock::fs::AccessFs> Unexecuted instantiation: <_ as landlock::ruleset::RulesetAttr>::handle_access::<_, _> |
347 | | } |
348 | | |
349 | | impl RulesetAttr for Ruleset {} |
350 | | |
351 | | impl RulesetAttr for &mut Ruleset {} |
352 | | |
353 | | #[test] |
354 | | fn ruleset_attr() { |
355 | | let mut ruleset = Ruleset::from(ABI::Unsupported); |
356 | | let ruleset_ref = &mut ruleset; |
357 | | |
358 | | // Can pass this reference to prepare the ruleset... |
359 | | ruleset_ref |
360 | | .set_compatibility(CompatLevel::BestEffort) |
361 | | .handle_access(AccessFs::Execute) |
362 | | .unwrap() |
363 | | .handle_access(AccessFs::ReadFile) |
364 | | .unwrap(); |
365 | | |
366 | | // ...and finally create the ruleset (thanks to non-lexical lifetimes). |
367 | | ruleset |
368 | | .set_compatibility(CompatLevel::BestEffort) |
369 | | .handle_access(AccessFs::Execute) |
370 | | .unwrap() |
371 | | .handle_access(AccessFs::WriteFile) |
372 | | .unwrap() |
373 | | .create() |
374 | | .unwrap(); |
375 | | } |
376 | | |
377 | | #[test] |
378 | | fn ruleset_created_handle_access_fs() { |
379 | | // Tests AccessFs::ruleset_handle_access() |
380 | | let ruleset = Ruleset::from(ABI::V1) |
381 | | .handle_access(AccessFs::Execute) |
382 | | .unwrap() |
383 | | .handle_access(AccessFs::ReadDir) |
384 | | .unwrap(); |
385 | | let access = make_bitflags!(AccessFs::{Execute | ReadDir}); |
386 | | assert_eq!(ruleset.requested_handled_fs, access); |
387 | | assert_eq!(ruleset.actual_handled_fs, access); |
388 | | |
389 | | // Tests that only the required handled accesses are reported as incompatible: |
390 | | // access should not contains AccessFs::Execute. |
391 | | assert!(matches!(Ruleset::from(ABI::Unsupported) |
392 | | .handle_access(AccessFs::Execute) |
393 | | .unwrap() |
394 | | .set_compatibility(CompatLevel::HardRequirement) |
395 | | .handle_access(AccessFs::ReadDir) |
396 | | .unwrap_err(), |
397 | | RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( |
398 | | CompatError::Access(AccessError::Incompatible { access }) |
399 | | ))) if access == AccessFs::ReadDir |
400 | | )); |
401 | | } |
402 | | |
403 | | #[test] |
404 | | fn ruleset_created_handle_access_net_tcp() { |
405 | | let access = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); |
406 | | |
407 | | // Tests AccessNet::ruleset_handle_access() with ABI that doesn't support TCP rights. |
408 | | let ruleset = Ruleset::from(ABI::V3).handle_access(access).unwrap(); |
409 | | assert_eq!(ruleset.requested_handled_net, access); |
410 | | assert_eq!(ruleset.actual_handled_net, BitFlags::<AccessNet>::EMPTY); |
411 | | |
412 | | // Tests AccessNet::ruleset_handle_access() with ABI that supports TCP rights. |
413 | | let ruleset = Ruleset::from(ABI::V4).handle_access(access).unwrap(); |
414 | | assert_eq!(ruleset.requested_handled_net, access); |
415 | | assert_eq!(ruleset.actual_handled_net, access); |
416 | | |
417 | | // Tests that only the required handled accesses are reported as incompatible: |
418 | | // access should not contains AccessNet::BindTcp. |
419 | | assert!(matches!(Ruleset::from(ABI::Unsupported) |
420 | | .handle_access(AccessNet::BindTcp) |
421 | | .unwrap() |
422 | | .set_compatibility(CompatLevel::HardRequirement) |
423 | | .handle_access(AccessNet::ConnectTcp) |
424 | | .unwrap_err(), |
425 | | RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat( |
426 | | CompatError::Access(AccessError::Incompatible { access }) |
427 | | ))) if access == AccessNet::ConnectTcp |
428 | | )); |
429 | | } |
430 | | |
431 | | impl OptionCompatLevelMut for RulesetCreated { |
432 | 0 | fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> { |
433 | 0 | &mut self.compat.level |
434 | 0 | } |
435 | | } |
436 | | |
437 | | impl OptionCompatLevelMut for &mut RulesetCreated { |
438 | 0 | fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> { |
439 | 0 | &mut self.compat.level |
440 | 0 | } |
441 | | } |
442 | | |
443 | | impl Compatible for RulesetCreated {} |
444 | | |
445 | | impl Compatible for &mut RulesetCreated {} |
446 | | |
447 | | pub trait RulesetCreatedAttr: Sized + AsMut<RulesetCreated> + Compatible { |
448 | | /// Attempts to add a new rule to the ruleset. |
449 | | /// |
450 | | /// On error, returns a wrapped [`AddRulesError`]. |
451 | 0 | fn add_rule<T, U>(mut self, rule: T) -> Result<Self, RulesetError> |
452 | 0 | where |
453 | 0 | T: Rule<U>, |
454 | 0 | U: Access, |
455 | 0 | { |
456 | 0 | let body = || -> Result<Self, AddRulesError> { |
457 | 0 | let self_ref = self.as_mut(); |
458 | 0 | rule.check_consistency(self_ref)?; |
459 | 0 | let mut compat_rule = match rule |
460 | 0 | .try_compat( |
461 | 0 | self_ref.compat.abi(), |
462 | 0 | self_ref.compat.level, |
463 | 0 | &mut self_ref.compat.state, |
464 | 0 | ) |
465 | 0 | .map_err(AddRuleError::Compat)? |
466 | | { |
467 | 0 | Some(r) => r, |
468 | 0 | None => return Ok(self), |
469 | | }; |
470 | 0 | match self_ref.compat.state { |
471 | 0 | CompatState::Init | CompatState::No | CompatState::Dummy => Ok(self), |
472 | 0 | CompatState::Full | CompatState::Partial => match unsafe { |
473 | 0 | uapi::landlock_add_rule(self_ref.fd, T::TYPE_ID, compat_rule.as_ptr(), 0) |
474 | 0 | } { |
475 | 0 | 0 => Ok(self), |
476 | 0 | _ => Err(AddRuleError::<U>::AddRuleCall { |
477 | 0 | source: Error::last_os_error(), |
478 | 0 | } |
479 | 0 | .into()), |
480 | | }, |
481 | | } |
482 | 0 | }; Unexecuted instantiation: <&mut landlock::ruleset::RulesetCreated as landlock::ruleset::RulesetCreatedAttr>::add_rule::<landlock::fs::PathBeneath<landlock::fs::PathFd>, landlock::fs::AccessFs>::{closure#0} Unexecuted instantiation: <_ as landlock::ruleset::RulesetCreatedAttr>::add_rule::<_, _>::{closure#0} |
483 | 0 | Ok(body()?) |
484 | 0 | } Unexecuted instantiation: <&mut landlock::ruleset::RulesetCreated as landlock::ruleset::RulesetCreatedAttr>::add_rule::<landlock::fs::PathBeneath<landlock::fs::PathFd>, landlock::fs::AccessFs> Unexecuted instantiation: <_ as landlock::ruleset::RulesetCreatedAttr>::add_rule::<_, _> |
485 | | |
486 | | /// Attempts to add a set of new rules to the ruleset. |
487 | | /// |
488 | | /// On error, returns a (double) wrapped [`AddRulesError`]. |
489 | | /// |
490 | | /// # Example |
491 | | /// |
492 | | /// Create a custom iterator to read paths from environment variable. |
493 | | /// |
494 | | /// ``` |
495 | | /// use landlock::{ |
496 | | /// Access, AccessFs, BitFlags, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, |
497 | | /// RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, |
498 | | /// }; |
499 | | /// use std::env; |
500 | | /// use std::ffi::OsStr; |
501 | | /// use std::os::unix::ffi::{OsStrExt, OsStringExt}; |
502 | | /// use thiserror::Error; |
503 | | /// |
504 | | /// #[derive(Debug, Error)] |
505 | | /// enum PathEnvError<'a> { |
506 | | /// #[error(transparent)] |
507 | | /// Ruleset(#[from] RulesetError), |
508 | | /// #[error(transparent)] |
509 | | /// AddRuleIter(#[from] PathFdError), |
510 | | /// #[error("missing environment variable {0}")] |
511 | | /// MissingVar(&'a str), |
512 | | /// } |
513 | | /// |
514 | | /// struct PathEnv { |
515 | | /// paths: Vec<u8>, |
516 | | /// access: BitFlags<AccessFs>, |
517 | | /// } |
518 | | /// |
519 | | /// impl PathEnv { |
520 | | /// // env_var is the name of an environment variable |
521 | | /// // containing paths requested to be allowed. |
522 | | /// // Paths are separated with ":", e.g. "/bin:/lib:/usr:/proc". |
523 | | /// // In case an empty string is provided, |
524 | | /// // no restrictions are applied. |
525 | | /// // `access` is the set of access rights allowed for each of the parsed paths. |
526 | | /// fn new<'a>( |
527 | | /// env_var: &'a str, access: BitFlags<AccessFs> |
528 | | /// ) -> Result<Self, PathEnvError<'a>> { |
529 | | /// Ok(Self { |
530 | | /// paths: env::var_os(env_var) |
531 | | /// .ok_or(PathEnvError::MissingVar(env_var))? |
532 | | /// .into_vec(), |
533 | | /// access, |
534 | | /// }) |
535 | | /// } |
536 | | /// |
537 | | /// fn iter( |
538 | | /// &self, |
539 | | /// ) -> impl Iterator<Item = Result<PathBeneath<PathFd>, PathEnvError<'static>>> + '_ { |
540 | | /// let is_empty = self.paths.is_empty(); |
541 | | /// self.paths |
542 | | /// .split(|b| *b == b':') |
543 | | /// // Skips the first empty element from of an empty string. |
544 | | /// .skip_while(move |_| is_empty) |
545 | | /// .map(OsStr::from_bytes) |
546 | | /// .map(move |path| |
547 | | /// Ok(PathBeneath::new(PathFd::new(path)?, self.access))) |
548 | | /// } |
549 | | /// } |
550 | | /// |
551 | | /// fn restrict_env() -> Result<RestrictionStatus, PathEnvError<'static>> { |
552 | | /// Ok(Ruleset::default() |
553 | | /// .handle_access(AccessFs::from_all(ABI::V1))? |
554 | | /// .create()? |
555 | | /// // In the shell: export EXECUTABLE_PATH="/usr:/bin:/sbin" |
556 | | /// .add_rules(PathEnv::new("EXECUTABLE_PATH", AccessFs::Execute.into())?.iter())? |
557 | | /// .restrict_self()?) |
558 | | /// } |
559 | | /// ``` |
560 | 0 | fn add_rules<I, T, U, E>(mut self, rules: I) -> Result<Self, E> |
561 | 0 | where |
562 | 0 | I: IntoIterator<Item = Result<T, E>>, |
563 | 0 | T: Rule<U>, |
564 | 0 | U: Access, |
565 | 0 | E: From<RulesetError>, |
566 | 0 | { |
567 | 0 | for rule in rules { |
568 | 0 | self = self.add_rule(rule?)?; |
569 | | } |
570 | 0 | Ok(self) |
571 | 0 | } Unexecuted instantiation: <&mut landlock::ruleset::RulesetCreated as landlock::ruleset::RulesetCreatedAttr>::add_rules::<core::iter::adapters::filter_map::FilterMap<alloc::vec::into_iter::IntoIter<std::path::PathBuf>, landlock::fs::path_beneath_rules<alloc::vec::Vec<std::path::PathBuf>, std::path::PathBuf, enumflags2::BitFlags<landlock::fs::AccessFs, u64>>::{closure#0}>, landlock::fs::PathBeneath<landlock::fs::PathFd>, landlock::fs::AccessFs, landlock::errors::RulesetError> Unexecuted instantiation: <_ as landlock::ruleset::RulesetCreatedAttr>::add_rules::<_, _, _, _> |
572 | | |
573 | | /// Configures the ruleset to call `prctl(2)` with the `PR_SET_NO_NEW_PRIVS` command |
574 | | /// in [`restrict_self()`](RulesetCreated::restrict_self). |
575 | | /// |
576 | | /// This `prctl(2)` call is never ignored, even if an error was encountered on a [`Ruleset`] or |
577 | | /// [`RulesetCreated`] method call while [`CompatLevel::SoftRequirement`] was set. |
578 | 0 | fn set_no_new_privs(mut self, no_new_privs: bool) -> Self { |
579 | 0 | <Self as AsMut<RulesetCreated>>::as_mut(&mut self).no_new_privs = no_new_privs; |
580 | 0 | self |
581 | 0 | } |
582 | | } |
583 | | |
584 | | /// Ruleset created with [`Ruleset::create()`]. |
585 | | #[cfg_attr(test, derive(Debug))] |
586 | | pub struct RulesetCreated { |
587 | | fd: RawFd, |
588 | | no_new_privs: bool, |
589 | | pub(crate) requested_handled_fs: BitFlags<AccessFs>, |
590 | | pub(crate) requested_handled_net: BitFlags<AccessNet>, |
591 | | compat: Compatibility, |
592 | | } |
593 | | |
594 | | impl RulesetCreated { |
595 | 0 | pub(crate) fn new(ruleset: Ruleset, fd: RawFd) -> Self { |
596 | 0 | // The compatibility state is initialized by Ruleset::create(). |
597 | 0 | #[cfg(test)] |
598 | 0 | assert!(!matches!(ruleset.compat.state, CompatState::Init)); |
599 | 0 |
|
600 | 0 | RulesetCreated { |
601 | 0 | fd, |
602 | 0 | no_new_privs: true, |
603 | 0 | requested_handled_fs: ruleset.requested_handled_fs, |
604 | 0 | requested_handled_net: ruleset.requested_handled_net, |
605 | 0 | compat: ruleset.compat, |
606 | 0 | } |
607 | 0 | } |
608 | | |
609 | | /// Attempts to restrict the calling thread with the ruleset |
610 | | /// according to the best-effort configuration |
611 | | /// (see [`RulesetCreated::set_compatibility()`] and [`CompatLevel::BestEffort`]). |
612 | | /// Call `prctl(2)` with the `PR_SET_NO_NEW_PRIVS` |
613 | | /// according to the ruleset configuration. |
614 | | /// |
615 | | /// On error, returns a wrapped [`RestrictSelfError`]. |
616 | 0 | pub fn restrict_self(mut self) -> Result<RestrictionStatus, RulesetError> { |
617 | 0 | let mut body = || -> Result<RestrictionStatus, RestrictSelfError> { |
618 | | // Enforce no_new_privs even if something failed with SoftRequirement. The rationale is |
619 | | // that no_new_privs should not be an issue on its own if it is not explicitly |
620 | | // deactivated. |
621 | 0 | let enforced_nnp = if self.no_new_privs { |
622 | 0 | if let Err(e) = prctl_set_no_new_privs() { |
623 | 0 | match self.compat.level.into() { |
624 | 0 | CompatLevel::BestEffort => {} |
625 | 0 | CompatLevel::SoftRequirement => { |
626 | 0 | self.compat.update(CompatState::Dummy); |
627 | 0 | } |
628 | | CompatLevel::HardRequirement => { |
629 | 0 | return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }); |
630 | | } |
631 | | } |
632 | | // To get a consistent behavior, calls this prctl whether or not |
633 | | // Landlock is supported by the running kernel. |
634 | 0 | let support_nnp = support_no_new_privs(); |
635 | 0 | match self.compat.state { |
636 | | // It should not be an error for kernel (older than 3.5) not supporting |
637 | | // no_new_privs. |
638 | | CompatState::Init | CompatState::No | CompatState::Dummy => { |
639 | 0 | if support_nnp { |
640 | | // The kernel seems to be between 3.5 (included) and 5.13 (excluded), |
641 | | // or Landlock is not enabled; no_new_privs should be supported anyway. |
642 | 0 | return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }); |
643 | 0 | } |
644 | | } |
645 | | // A kernel supporting Landlock should also support no_new_privs (unless |
646 | | // filtered by seccomp). |
647 | | CompatState::Full | CompatState::Partial => { |
648 | 0 | return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }) |
649 | | } |
650 | | } |
651 | 0 | false |
652 | | } else { |
653 | 0 | true |
654 | | } |
655 | | } else { |
656 | 0 | false |
657 | | }; |
658 | | |
659 | 0 | match self.compat.state { |
660 | 0 | CompatState::Init | CompatState::No | CompatState::Dummy => Ok(RestrictionStatus { |
661 | 0 | ruleset: self.compat.state.into(), |
662 | 0 | no_new_privs: enforced_nnp, |
663 | 0 | }), |
664 | | CompatState::Full | CompatState::Partial => { |
665 | 0 | match unsafe { uapi::landlock_restrict_self(self.fd, 0) } { |
666 | | 0 => { |
667 | 0 | self.compat.update(CompatState::Full); |
668 | 0 | Ok(RestrictionStatus { |
669 | 0 | ruleset: self.compat.state.into(), |
670 | 0 | no_new_privs: enforced_nnp, |
671 | 0 | }) |
672 | | } |
673 | | // TODO: match specific Landlock restrict self errors |
674 | 0 | _ => Err(RestrictSelfError::RestrictSelfCall { |
675 | 0 | source: Error::last_os_error(), |
676 | 0 | }), |
677 | | } |
678 | | } |
679 | | } |
680 | 0 | }; |
681 | 0 | Ok(body()?) |
682 | 0 | } |
683 | | |
684 | | /// Creates a new `RulesetCreated` instance by duplicating the underlying file descriptor. |
685 | | /// Rule modification will affect both `RulesetCreated` instances simultaneously. |
686 | | /// |
687 | | /// On error, returns [`std::io::Error`]. |
688 | 0 | pub fn try_clone(&self) -> std::io::Result<Self> { |
689 | 0 | Ok(RulesetCreated { |
690 | 0 | fd: match self.fd { |
691 | 0 | -1 => -1, |
692 | 0 | self_fd => match unsafe { libc::fcntl(self_fd, libc::F_DUPFD_CLOEXEC, 0) } { |
693 | 0 | dup_fd if dup_fd >= 0 => dup_fd, |
694 | 0 | _ => return Err(Error::last_os_error()), |
695 | | }, |
696 | | }, |
697 | 0 | no_new_privs: self.no_new_privs, |
698 | 0 | requested_handled_fs: self.requested_handled_fs, |
699 | 0 | requested_handled_net: self.requested_handled_net, |
700 | 0 | compat: self.compat, |
701 | | }) |
702 | 0 | } |
703 | | } |
704 | | |
705 | | impl Drop for RulesetCreated { |
706 | 0 | fn drop(&mut self) { |
707 | 0 | if self.fd >= 0 { |
708 | 0 | unsafe { close(self.fd) }; |
709 | 0 | } |
710 | 0 | } |
711 | | } |
712 | | |
713 | | impl AsMut<RulesetCreated> for RulesetCreated { |
714 | 0 | fn as_mut(&mut self) -> &mut RulesetCreated { |
715 | 0 | self |
716 | 0 | } |
717 | | } |
718 | | |
719 | | impl RulesetCreatedAttr for RulesetCreated {} |
720 | | |
721 | | impl RulesetCreatedAttr for &mut RulesetCreated {} |
722 | | |
723 | | #[test] |
724 | | fn ruleset_created_attr() { |
725 | | let mut ruleset_created = Ruleset::from(ABI::Unsupported) |
726 | | .handle_access(AccessFs::Execute) |
727 | | .unwrap() |
728 | | .create() |
729 | | .unwrap(); |
730 | | let ruleset_created_ref = &mut ruleset_created; |
731 | | |
732 | | // Can pass this reference to populate the ruleset... |
733 | | ruleset_created_ref |
734 | | .set_compatibility(CompatLevel::BestEffort) |
735 | | .add_rule(PathBeneath::new( |
736 | | PathFd::new("/usr").unwrap(), |
737 | | AccessFs::Execute, |
738 | | )) |
739 | | .unwrap() |
740 | | .add_rule(PathBeneath::new( |
741 | | PathFd::new("/etc").unwrap(), |
742 | | AccessFs::Execute, |
743 | | )) |
744 | | .unwrap(); |
745 | | |
746 | | // ...and finally restrict with the last rules (thanks to non-lexical lifetimes). |
747 | | assert_eq!( |
748 | | ruleset_created |
749 | | .set_compatibility(CompatLevel::BestEffort) |
750 | | .add_rule(PathBeneath::new( |
751 | | PathFd::new("/tmp").unwrap(), |
752 | | AccessFs::Execute, |
753 | | )) |
754 | | .unwrap() |
755 | | .add_rule(PathBeneath::new( |
756 | | PathFd::new("/var").unwrap(), |
757 | | AccessFs::Execute, |
758 | | )) |
759 | | .unwrap() |
760 | | .restrict_self() |
761 | | .unwrap(), |
762 | | RestrictionStatus { |
763 | | ruleset: RulesetStatus::NotEnforced, |
764 | | no_new_privs: true, |
765 | | } |
766 | | ); |
767 | | } |
768 | | |
769 | | #[test] |
770 | | fn ruleset_compat_dummy() { |
771 | | for level in [CompatLevel::BestEffort, CompatLevel::SoftRequirement] { |
772 | | println!("level: {:?}", level); |
773 | | |
774 | | // ABI:Unsupported does not support AccessFs::Execute. |
775 | | let ruleset = Ruleset::from(ABI::Unsupported); |
776 | | assert_eq!(ruleset.compat.state, CompatState::Init); |
777 | | |
778 | | let ruleset = ruleset.set_compatibility(level); |
779 | | assert_eq!(ruleset.compat.state, CompatState::Init); |
780 | | |
781 | | let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap(); |
782 | | assert_eq!( |
783 | | ruleset.compat.state, |
784 | | match level { |
785 | | CompatLevel::BestEffort => CompatState::No, |
786 | | CompatLevel::SoftRequirement => CompatState::Dummy, |
787 | | _ => unreachable!(), |
788 | | } |
789 | | ); |
790 | | |
791 | | let ruleset_created = ruleset.create().unwrap(); |
792 | | // Because the compatibility state was either No or Dummy, calling create() updates it to |
793 | | // Dummy. |
794 | | assert_eq!(ruleset_created.compat.state, CompatState::Dummy); |
795 | | |
796 | | let ruleset_created = ruleset_created |
797 | | .add_rule(PathBeneath::new( |
798 | | PathFd::new("/usr").unwrap(), |
799 | | AccessFs::Execute, |
800 | | )) |
801 | | .unwrap(); |
802 | | assert_eq!(ruleset_created.compat.state, CompatState::Dummy); |
803 | | } |
804 | | } |
805 | | |
806 | | #[test] |
807 | | fn ruleset_compat_partial() { |
808 | | // CompatLevel::BestEffort |
809 | | let ruleset = Ruleset::from(ABI::V1); |
810 | | assert_eq!(ruleset.compat.state, CompatState::Init); |
811 | | |
812 | | // ABI::V1 does not support AccessFs::Refer. |
813 | | let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap(); |
814 | | assert_eq!(ruleset.compat.state, CompatState::No); |
815 | | |
816 | | let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap(); |
817 | | assert_eq!(ruleset.compat.state, CompatState::Partial); |
818 | | |
819 | | // Requesting to handle another unsupported handled access does not change anything. |
820 | | let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap(); |
821 | | assert_eq!(ruleset.compat.state, CompatState::Partial); |
822 | | } |
823 | | |
824 | | #[test] |
825 | | fn ruleset_unsupported() { |
826 | | assert_eq!( |
827 | | Ruleset::from(ABI::Unsupported) |
828 | | // BestEffort for Ruleset. |
829 | | .handle_access(AccessFs::Execute) |
830 | | .unwrap() |
831 | | .create() |
832 | | .unwrap() |
833 | | .restrict_self() |
834 | | .unwrap(), |
835 | | RestrictionStatus { |
836 | | ruleset: RulesetStatus::NotEnforced, |
837 | | // With BestEffort, no_new_privs is still enabled. |
838 | | no_new_privs: true, |
839 | | } |
840 | | ); |
841 | | |
842 | | assert_eq!( |
843 | | Ruleset::from(ABI::Unsupported) |
844 | | // SoftRequirement for Ruleset. |
845 | | .set_compatibility(CompatLevel::SoftRequirement) |
846 | | .handle_access(AccessFs::Execute) |
847 | | .unwrap() |
848 | | .create() |
849 | | .unwrap() |
850 | | .restrict_self() |
851 | | .unwrap(), |
852 | | RestrictionStatus { |
853 | | ruleset: RulesetStatus::NotEnforced, |
854 | | // With SoftRequirement, no_new_privs is still enabled. |
855 | | no_new_privs: true, |
856 | | } |
857 | | ); |
858 | | |
859 | | matches!( |
860 | | Ruleset::from(ABI::Unsupported) |
861 | | // HardRequirement for Ruleset. |
862 | | .set_compatibility(CompatLevel::HardRequirement) |
863 | | .handle_access(AccessFs::Execute) |
864 | | .unwrap_err(), |
865 | | RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess) |
866 | | ); |
867 | | |
868 | | assert_eq!( |
869 | | Ruleset::from(ABI::Unsupported) |
870 | | .handle_access(AccessFs::Execute) |
871 | | .unwrap() |
872 | | .create() |
873 | | .unwrap() |
874 | | // SoftRequirement for RulesetCreated without any rule. |
875 | | .set_compatibility(CompatLevel::SoftRequirement) |
876 | | .restrict_self() |
877 | | .unwrap(), |
878 | | RestrictionStatus { |
879 | | ruleset: RulesetStatus::NotEnforced, |
880 | | // With SoftRequirement, no_new_privs is untouched if there is no error (e.g. no rule). |
881 | | no_new_privs: true, |
882 | | } |
883 | | ); |
884 | | |
885 | | // Don't explicitly call create() on a CI that doesn't support Landlock. |
886 | | if compat::can_emulate(ABI::V1, ABI::V1, Some(ABI::V2)) { |
887 | | assert_eq!( |
888 | | Ruleset::from(ABI::V1) |
889 | | .handle_access(make_bitflags!(AccessFs::{Execute | Refer})) |
890 | | .unwrap() |
891 | | .create() |
892 | | .unwrap() |
893 | | // SoftRequirement for RulesetCreated with a rule. |
894 | | .set_compatibility(CompatLevel::SoftRequirement) |
895 | | .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Refer)) |
896 | | .unwrap() |
897 | | .restrict_self() |
898 | | .unwrap(), |
899 | | RestrictionStatus { |
900 | | ruleset: RulesetStatus::NotEnforced, |
901 | | // With SoftRequirement, no_new_privs is still enabled, even if there is an error |
902 | | // (e.g. unsupported access right). |
903 | | no_new_privs: true, |
904 | | } |
905 | | ); |
906 | | } |
907 | | |
908 | | assert_eq!( |
909 | | Ruleset::from(ABI::Unsupported) |
910 | | .handle_access(AccessFs::Execute) |
911 | | .unwrap() |
912 | | .create() |
913 | | .unwrap() |
914 | | .set_no_new_privs(false) |
915 | | .restrict_self() |
916 | | .unwrap(), |
917 | | RestrictionStatus { |
918 | | ruleset: RulesetStatus::NotEnforced, |
919 | | no_new_privs: false, |
920 | | } |
921 | | ); |
922 | | |
923 | | assert!(matches!( |
924 | | Ruleset::from(ABI::Unsupported) |
925 | | // Empty access-rights |
926 | | .handle_access(AccessFs::from_all(ABI::Unsupported)) |
927 | | .unwrap_err(), |
928 | | RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( |
929 | | CompatError::Access(AccessError::Empty) |
930 | | ))) |
931 | | )); |
932 | | |
933 | | assert!(matches!( |
934 | | Ruleset::from(ABI::Unsupported) |
935 | | // No handle_access() call. |
936 | | .create() |
937 | | .unwrap_err(), |
938 | | RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess) |
939 | | )); |
940 | | |
941 | | assert!(matches!( |
942 | | Ruleset::from(ABI::V1) |
943 | | // Empty access-rights |
944 | | .handle_access(AccessFs::from_all(ABI::Unsupported)) |
945 | | .unwrap_err(), |
946 | | RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( |
947 | | CompatError::Access(AccessError::Empty) |
948 | | ))) |
949 | | )); |
950 | | |
951 | | // Tests inconsistency between the ruleset handled access-rights and the rule access-rights. |
952 | | for handled_access in &[ |
953 | | make_bitflags!(AccessFs::{Execute | WriteFile}), |
954 | | AccessFs::Execute.into(), |
955 | | ] { |
956 | | let ruleset = Ruleset::from(ABI::V1) |
957 | | .handle_access(*handled_access) |
958 | | .unwrap(); |
959 | | // Fakes a call to create() to test without involving the kernel (i.e. no |
960 | | // landlock_ruleset_create() call). |
961 | | let ruleset_created = RulesetCreated::new(ruleset, -1); |
962 | | assert!(matches!( |
963 | | ruleset_created |
964 | | .add_rule(PathBeneath::new( |
965 | | PathFd::new("/").unwrap(), |
966 | | AccessFs::ReadFile |
967 | | )) |
968 | | .unwrap_err(), |
969 | | RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. })) |
970 | | )); |
971 | | } |
972 | | } |
973 | | |
974 | | #[test] |
975 | | fn ignore_abi_v2_with_abi_v1() { |
976 | | // We don't need kernel/CI support for Landlock because no related syscalls should actually be |
977 | | // performed. |
978 | | assert_eq!( |
979 | | Ruleset::from(ABI::V1) |
980 | | .set_compatibility(CompatLevel::HardRequirement) |
981 | | .handle_access(AccessFs::from_all(ABI::V1)) |
982 | | .unwrap() |
983 | | .set_compatibility(CompatLevel::SoftRequirement) |
984 | | // Because Ruleset only supports V1, Refer will be ignored. |
985 | | .handle_access(AccessFs::Refer) |
986 | | .unwrap() |
987 | | .create() |
988 | | .unwrap() |
989 | | .add_rule(PathBeneath::new( |
990 | | PathFd::new("/tmp").unwrap(), |
991 | | AccessFs::from_all(ABI::V2) |
992 | | )) |
993 | | .unwrap() |
994 | | .add_rule(PathBeneath::new( |
995 | | PathFd::new("/usr").unwrap(), |
996 | | make_bitflags!(AccessFs::{ReadFile | ReadDir}) |
997 | | )) |
998 | | .unwrap() |
999 | | .restrict_self() |
1000 | | .unwrap(), |
1001 | | RestrictionStatus { |
1002 | | ruleset: RulesetStatus::NotEnforced, |
1003 | | no_new_privs: true, |
1004 | | } |
1005 | | ); |
1006 | | } |
1007 | | |
1008 | | #[test] |
1009 | | fn unsupported_handled_access() { |
1010 | | matches!( |
1011 | | Ruleset::from(ABI::V3) |
1012 | | .handle_access(AccessNet::from_all(ABI::V3)) |
1013 | | .unwrap_err(), |
1014 | | RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat( |
1015 | | CompatError::Access(AccessError::Empty) |
1016 | | ))) |
1017 | | ); |
1018 | | } |