1use crate::compat::private::OptionCompatLevelMut;
4use crate::{
5 uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState,
6 Compatible, HandleAccessError, HandleAccessesError, HandledAccess, PathBeneathError,
7 PathFdError, PrivateHandledAccess, PrivateRule, Rule, Ruleset, RulesetCreated, RulesetError,
8 TailoredCompatLevel, TryCompat, ABI,
9};
10use enumflags2::{bitflags, make_bitflags, BitFlags};
11use std::fs::OpenOptions;
12use std::io::Error;
13use std::mem::zeroed;
14use std::os::unix::fs::OpenOptionsExt;
15use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
16use std::path::Path;
17
18#[cfg(test)]
19use crate::{RulesetAttr, RulesetCreatedAttr};
20#[cfg(test)]
21use strum::IntoEnumIterator;
22
23#[bitflags]
55#[repr(u64)]
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57#[non_exhaustive]
58pub enum AccessFs {
59 Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64,
61 WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64,
67 ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64,
69 ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64,
71 RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64,
73 RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64,
75 MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64,
77 MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64,
79 MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64,
81 MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64,
83 MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64,
85 MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64,
87 MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64,
89 Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64,
91 Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64,
93 IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64,
95}
96
97impl Access for AccessFs {
98 fn from_all(abi: ABI) -> BitFlags<Self> {
100 Self::from_read(abi) | Self::from_write(abi)
105 }
106}
107
108impl AccessFs {
109 pub fn from_read(abi: ABI) -> BitFlags<Self> {
113 match abi {
114 ABI::Unsupported => BitFlags::EMPTY,
115 ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 | ABI::V6 => make_bitflags!(AccessFs::{
116 Execute
117 | ReadFile
118 | ReadDir
119 }),
120 }
121 }
122
123 pub fn from_write(abi: ABI) -> BitFlags<Self> {
127 match abi {
128 ABI::Unsupported => BitFlags::EMPTY,
129 ABI::V1 => make_bitflags!(AccessFs::{
130 WriteFile
131 | RemoveDir
132 | RemoveFile
133 | MakeChar
134 | MakeDir
135 | MakeReg
136 | MakeSock
137 | MakeFifo
138 | MakeBlock
139 | MakeSym
140 }),
141 ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
142 ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate,
143 ABI::V5 | ABI::V6 => Self::from_write(ABI::V4) | AccessFs::IoctlDev,
144 }
145 }
146
147 pub fn from_file(abi: ABI) -> BitFlags<Self> {
149 Self::from_all(abi) & ACCESS_FILE
150 }
151}
152
153#[test]
154fn consistent_access_fs_rw() {
155 for abi in ABI::iter() {
156 let access_all = AccessFs::from_all(abi);
157 let access_read = AccessFs::from_read(abi);
158 let access_write = AccessFs::from_write(abi);
159 let access_file = AccessFs::from_file(abi);
160 assert_eq!(access_read, !access_write & access_all);
161 assert_eq!(access_read | access_write, access_all);
162 assert_eq!(access_file, access_all & ACCESS_FILE);
163 }
164}
165
166impl HandledAccess for AccessFs {}
167
168impl PrivateHandledAccess for AccessFs {
169 fn ruleset_handle_access(
170 ruleset: &mut Ruleset,
171 access: BitFlags<Self>,
172 ) -> Result<(), HandleAccessesError> {
173 ruleset.requested_handled_fs |= access;
175 ruleset.actual_handled_fs |= match access
176 .try_compat(
177 ruleset.compat.abi(),
178 ruleset.compat.level,
179 &mut ruleset.compat.state,
180 )
181 .map_err(HandleAccessError::Compat)?
182 {
183 Some(a) => a,
184 None => return Ok(()),
185 };
186 Ok(())
187 }
188
189 fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
190 AddRulesError::Fs(error)
191 }
192
193 fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
194 HandleAccessesError::Fs(error)
195 }
196}
197
198const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
201 ReadFile | WriteFile | Execute | Truncate | IoctlDev
202});
203
204fn is_file<F>(fd: F) -> Result<bool, Error>
206where
207 F: AsFd,
208{
209 unsafe {
210 let mut stat = zeroed();
211 match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
212 0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
213 _ => Err(Error::last_os_error()),
214 }
215 }
216}
217
218#[cfg_attr(test, derive(Debug))]
230pub struct PathBeneath<F> {
231 attr: uapi::landlock_path_beneath_attr,
232 parent_fd: F,
234 allowed_access: BitFlags<AccessFs>,
235 compat_level: Option<CompatLevel>,
236}
237
238impl<F> PathBeneath<F>
239where
240 F: AsFd,
241{
242 pub fn new<A>(parent: F, access: A) -> Self
246 where
247 A: Into<BitFlags<AccessFs>>,
248 {
249 PathBeneath {
250 attr: unsafe { zeroed() },
252 parent_fd: parent,
253 allowed_access: access.into(),
254 compat_level: None,
255 }
256 }
257}
258
259impl<F> TryCompat<AccessFs> for PathBeneath<F>
260where
261 F: AsFd,
262{
263 fn try_compat_children<L>(
264 mut self,
265 abi: ABI,
266 parent_level: L,
267 compat_state: &mut CompatState,
268 ) -> Result<Option<Self>, CompatError<AccessFs>>
269 where
270 L: Into<CompatLevel>,
271 {
272 self.allowed_access = match self.allowed_access.try_compat(
274 abi,
275 self.tailored_compat_level(parent_level),
276 compat_state,
277 )? {
278 Some(a) => a,
279 None => return Ok(None),
280 };
281 Ok(Some(self))
282 }
283
284 fn try_compat_inner(
285 &mut self,
286 _abi: ABI,
287 ) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
288 let valid_access =
290 if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
291 self.allowed_access & ACCESS_FILE
292 } else {
293 self.allowed_access
294 };
295
296 if self.allowed_access != valid_access {
297 let error = PathBeneathError::DirectoryAccess {
298 access: self.allowed_access,
299 incompatible: self.allowed_access ^ valid_access,
300 }
301 .into();
302 self.allowed_access = valid_access;
303 Ok(CompatResult::Partial(error))
305 } else {
306 Ok(CompatResult::Full)
307 }
308 }
309}
310
311#[test]
312fn path_beneath_try_compat_children() {
313 use crate::*;
314
315 let access_file = AccessFs::ReadFile | AccessFs::Refer;
317
318 let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
320 ruleset.compat.state = CompatState::Dummy;
322 assert!(matches!(
323 RulesetCreated::new(ruleset, None)
324 .set_compatibility(CompatLevel::HardRequirement)
325 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
326 .unwrap_err(),
327 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
328 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
329 ))) if access == access_file && incompatible == AccessFs::Refer
330 ));
331
332 let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
334 ruleset.compat.state = CompatState::Dummy;
336 assert!(matches!(
337 RulesetCreated::new(ruleset, None)
338 .set_compatibility(CompatLevel::HardRequirement)
339 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
340 .unwrap_err(),
341 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
342 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
343 ))) if access == access_file && incompatible == AccessFs::Refer
344 ));
345}
346
347#[test]
348fn path_beneath_try_compat() {
349 use crate::*;
350
351 let abi = ABI::V1;
352
353 for file in &["/etc/passwd", "/dev/null"] {
354 let mut compat_state = CompatState::Init;
355 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
356 assert!(matches!(
357 PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
358 .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
359 .unwrap_err(),
360 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
361 if access == ro_access && incompatible == AccessFs::ReadDir
362 ));
363
364 let mut compat_state = CompatState::Init;
365 assert!(matches!(
366 PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
367 .try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
368 .unwrap_err(),
369 CompatError::Access(AccessError::Empty)
370 ));
371 }
372
373 let full_access = AccessFs::from_all(ABI::V1);
374 for compat_level in &[
375 CompatLevel::BestEffort,
376 CompatLevel::SoftRequirement,
377 CompatLevel::HardRequirement,
378 ] {
379 let mut compat_state = CompatState::Init;
380 let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
381 .try_compat(abi, *compat_level, &mut compat_state)
382 .unwrap()
383 .unwrap();
384 assert_eq!(compat_state, CompatState::Full);
385
386 let raw_access = path_beneath.attr.allowed_access;
388 assert_eq!(raw_access, 0);
389
390 let _ = path_beneath.as_ptr();
392 let raw_access = path_beneath.attr.allowed_access;
393 assert_eq!(raw_access, full_access.bits());
394 }
395}
396
397impl<F> OptionCompatLevelMut for PathBeneath<F> {
398 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
399 &mut self.compat_level
400 }
401}
402
403impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
404 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
405 &mut self.compat_level
406 }
407}
408
409impl<F> Compatible for PathBeneath<F> {}
410
411impl<F> Compatible for &mut PathBeneath<F> {}
412
413#[test]
414fn path_beneath_compatibility() {
415 let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
416 let path_ref = &mut path;
417
418 let level = path_ref.as_option_compat_level_mut();
419 assert_eq!(level, &None);
420 assert_eq!(
421 <Option<CompatLevel> as Into<CompatLevel>>::into(*level),
422 CompatLevel::BestEffort
423 );
424
425 path_ref.set_compatibility(CompatLevel::SoftRequirement);
426 assert_eq!(
427 path_ref.as_option_compat_level_mut(),
428 &Some(CompatLevel::SoftRequirement)
429 );
430
431 path.set_compatibility(CompatLevel::HardRequirement);
432}
433
434impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
437
438impl<F> PrivateRule<AccessFs> for PathBeneath<F>
439where
440 F: AsFd,
441{
442 const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
443
444 fn as_ptr(&mut self) -> *const libc::c_void {
445 self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
446 self.attr.allowed_access = self.allowed_access.bits();
447 &self.attr as *const _ as _
448 }
449
450 fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
451 if ruleset.requested_handled_fs.contains(self.allowed_access) {
456 Ok(())
457 } else {
458 Err(AddRuleError::UnhandledAccess {
459 access: self.allowed_access,
460 incompatible: self.allowed_access & !ruleset.requested_handled_fs,
461 }
462 .into())
463 }
464 }
465}
466
467#[test]
468fn path_beneath_check_consistency() {
469 use crate::*;
470
471 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
472 let rx_access = AccessFs::Execute | AccessFs::ReadFile;
473 assert!(matches!(
474 Ruleset::from(ABI::Unsupported)
475 .handle_access(ro_access)
476 .unwrap()
477 .create()
478 .unwrap()
479 .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
480 .unwrap_err(),
481 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
482 if access == rx_access && incompatible == AccessFs::Execute
483 ));
484}
485
486#[cfg_attr(test, derive(Debug))]
506pub struct PathFd {
507 fd: OwnedFd,
508}
509
510impl PathFd {
511 pub fn new<T>(path: T) -> Result<Self, PathFdError>
512 where
513 T: AsRef<Path>,
514 {
515 Ok(PathFd {
516 fd: OpenOptions::new()
517 .read(true)
518 .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
520 .open(path.as_ref())
521 .map_err(|e| PathFdError::OpenCall {
522 source: e,
523 path: path.as_ref().into(),
524 })?
525 .into(),
526 })
527 }
528}
529
530impl AsFd for PathFd {
531 fn as_fd(&self) -> BorrowedFd<'_> {
532 self.fd.as_fd()
533 }
534}
535
536#[test]
537fn path_fd() {
538 use std::fs::File;
539 use std::io::Read;
540
541 PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
542 PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
543
544 let mut buffer = [0; 1];
545 File::from(PathFd::new("/etc/passwd").unwrap().fd)
547 .read(&mut buffer)
548 .unwrap_err();
549}
550
551pub fn path_beneath_rules<I, P, A>(
595 paths: I,
596 access: A,
597) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
598where
599 I: IntoIterator<Item = P>,
600 P: AsRef<Path>,
601 A: Into<BitFlags<AccessFs>>,
602{
603 let access = access.into();
604 paths.into_iter().filter_map(move |p| match PathFd::new(p) {
605 Ok(f) => {
606 let valid_access = match is_file(&f) {
607 Ok(true) => access & ACCESS_FILE,
608 Err(_) | Ok(false) => access,
610 };
611 Some(Ok(PathBeneath::new(f, valid_access)))
612 }
613 Err(_) => None,
614 })
615}
616
617#[test]
618fn path_beneath_rules_iter() {
619 let _ = Ruleset::default()
620 .handle_access(AccessFs::from_all(ABI::V1))
621 .unwrap()
622 .create()
623 .unwrap()
624 .add_rules(path_beneath_rules(
625 &["/usr", "/opt", "/does-not-exist", "/root"],
626 AccessFs::Execute,
627 ))
628 .unwrap();
629}