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 | ABI::V7 => {
116 make_bitflags!(AccessFs::{
117 Execute
118 | ReadFile
119 | ReadDir
120 })
121 }
122 }
123 }
124
125 pub fn from_write(abi: ABI) -> BitFlags<Self> {
129 match abi {
130 ABI::Unsupported => BitFlags::EMPTY,
131 ABI::V1 => make_bitflags!(AccessFs::{
132 WriteFile
133 | RemoveDir
134 | RemoveFile
135 | MakeChar
136 | MakeDir
137 | MakeReg
138 | MakeSock
139 | MakeFifo
140 | MakeBlock
141 | MakeSym
142 }),
143 ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer,
144 ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate,
145 ABI::V5 | ABI::V6 | ABI::V7 => Self::from_write(ABI::V4) | AccessFs::IoctlDev,
146 }
147 }
148
149 pub fn from_file(abi: ABI) -> BitFlags<Self> {
151 Self::from_all(abi) & ACCESS_FILE
152 }
153}
154
155#[test]
156fn consistent_access_fs_rw() {
157 for abi in ABI::iter() {
158 let access_all = AccessFs::from_all(abi);
159 let access_read = AccessFs::from_read(abi);
160 let access_write = AccessFs::from_write(abi);
161 let access_file = AccessFs::from_file(abi);
162 assert_eq!(access_read, !access_write & access_all);
163 assert_eq!(access_read | access_write, access_all);
164 assert_eq!(access_file, access_all & ACCESS_FILE);
165 }
166}
167
168impl HandledAccess for AccessFs {}
169
170impl PrivateHandledAccess for AccessFs {
171 fn ruleset_handle_access(
172 ruleset: &mut Ruleset,
173 access: BitFlags<Self>,
174 ) -> Result<(), HandleAccessesError> {
175 ruleset.requested_handled_fs |= access;
177 ruleset.actual_handled_fs |= match access
178 .try_compat(
179 ruleset.compat.abi(),
180 ruleset.compat.level,
181 &mut ruleset.compat.state,
182 )
183 .map_err(HandleAccessError::Compat)?
184 {
185 Some(a) => a,
186 None => return Ok(()),
187 };
188 Ok(())
189 }
190
191 fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
192 AddRulesError::Fs(error)
193 }
194
195 fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
196 HandleAccessesError::Fs(error)
197 }
198}
199
200const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
203 ReadFile | WriteFile | Execute | Truncate | IoctlDev
204});
205
206fn is_file<F>(fd: F) -> Result<bool, Error>
208where
209 F: AsFd,
210{
211 unsafe {
212 let mut stat = zeroed();
213 match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
214 0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
215 _ => Err(Error::last_os_error()),
216 }
217 }
218}
219
220#[derive(Debug)]
232pub struct PathBeneath<F> {
233 attr: uapi::landlock_path_beneath_attr,
234 parent_fd: F,
236 allowed_access: BitFlags<AccessFs>,
237 compat_level: Option<CompatLevel>,
238}
239
240impl<F> PathBeneath<F>
241where
242 F: AsFd,
243{
244 pub fn new<A>(parent: F, access: A) -> Self
248 where
249 A: Into<BitFlags<AccessFs>>,
250 {
251 PathBeneath {
252 attr: unsafe { zeroed() },
254 parent_fd: parent,
255 allowed_access: access.into(),
256 compat_level: None,
257 }
258 }
259}
260
261impl<F> TryCompat<AccessFs> for PathBeneath<F>
262where
263 F: AsFd,
264{
265 fn try_compat_children<L>(
266 mut self,
267 abi: ABI,
268 parent_level: L,
269 compat_state: &mut CompatState,
270 ) -> Result<Option<Self>, CompatError<AccessFs>>
271 where
272 L: Into<CompatLevel>,
273 {
274 self.allowed_access = match self.allowed_access.try_compat(
276 abi,
277 self.tailored_compat_level(parent_level),
278 compat_state,
279 )? {
280 Some(a) => a,
281 None => return Ok(None),
282 };
283 Ok(Some(self))
284 }
285
286 fn try_compat_inner(
287 &mut self,
288 _abi: ABI,
289 ) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
290 let valid_access =
292 if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
293 self.allowed_access & ACCESS_FILE
294 } else {
295 self.allowed_access
296 };
297
298 if self.allowed_access != valid_access {
299 let error = PathBeneathError::DirectoryAccess {
300 access: self.allowed_access,
301 incompatible: self.allowed_access ^ valid_access,
302 }
303 .into();
304 self.allowed_access = valid_access;
305 Ok(CompatResult::Partial(error))
307 } else {
308 Ok(CompatResult::Full)
309 }
310 }
311}
312
313#[test]
314fn path_beneath_try_compat_children() {
315 use crate::*;
316
317 let access_file = AccessFs::ReadFile | AccessFs::Refer;
319
320 let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
322 ruleset.compat.state = CompatState::Dummy;
324 assert!(matches!(
325 RulesetCreated::new(ruleset, None)
326 .set_compatibility(CompatLevel::HardRequirement)
327 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
328 .unwrap_err(),
329 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
330 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
331 ))) if access == access_file && incompatible == AccessFs::Refer
332 ));
333
334 let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
336 ruleset.compat.state = CompatState::Dummy;
338 assert!(matches!(
339 RulesetCreated::new(ruleset, None)
340 .set_compatibility(CompatLevel::HardRequirement)
341 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
342 .unwrap_err(),
343 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
344 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
345 ))) if access == access_file && incompatible == AccessFs::Refer
346 ));
347}
348
349#[test]
350fn path_beneath_try_compat() {
351 use crate::*;
352
353 let abi = ABI::V1;
354
355 for file in &["/etc/passwd", "/dev/null"] {
356 let mut compat_state = CompatState::Init;
357 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
358 assert!(matches!(
359 PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
360 .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
361 .unwrap_err(),
362 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
363 if access == ro_access && incompatible == AccessFs::ReadDir
364 ));
365
366 let mut compat_state = CompatState::Init;
367 assert!(matches!(
368 PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
369 .try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
370 .unwrap_err(),
371 CompatError::Access(AccessError::Empty)
372 ));
373 }
374
375 let full_access = AccessFs::from_all(ABI::V1);
376 for compat_level in &[
377 CompatLevel::BestEffort,
378 CompatLevel::SoftRequirement,
379 CompatLevel::HardRequirement,
380 ] {
381 let mut compat_state = CompatState::Init;
382 let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
383 .try_compat(abi, *compat_level, &mut compat_state)
384 .unwrap()
385 .unwrap();
386 assert_eq!(compat_state, CompatState::Full);
387
388 let raw_access = path_beneath.attr.allowed_access;
390 assert_eq!(raw_access, 0);
391
392 let _ = path_beneath.as_ptr();
394 let raw_access = path_beneath.attr.allowed_access;
395 assert_eq!(raw_access, full_access.bits());
396 }
397}
398
399impl<F> OptionCompatLevelMut for PathBeneath<F> {
400 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
401 &mut self.compat_level
402 }
403}
404
405impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
406 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
407 &mut self.compat_level
408 }
409}
410
411impl<F> Compatible for PathBeneath<F> {}
412
413impl<F> Compatible for &mut PathBeneath<F> {}
414
415#[test]
416fn path_beneath_compatibility() {
417 let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
418 let path_ref = &mut path;
419
420 let level = path_ref.as_option_compat_level_mut();
421 assert_eq!(level, &None);
422 assert_eq!(
423 <Option<CompatLevel> as Into<CompatLevel>>::into(*level),
424 CompatLevel::BestEffort
425 );
426
427 path_ref.set_compatibility(CompatLevel::SoftRequirement);
428 assert_eq!(
429 path_ref.as_option_compat_level_mut(),
430 &Some(CompatLevel::SoftRequirement)
431 );
432
433 path.set_compatibility(CompatLevel::HardRequirement);
434}
435
436impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
439
440impl<F> PrivateRule<AccessFs> for PathBeneath<F>
441where
442 F: AsFd,
443{
444 const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
445
446 fn as_ptr(&mut self) -> *const libc::c_void {
447 self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
448 self.attr.allowed_access = self.allowed_access.bits();
449 &self.attr as *const _ as _
450 }
451
452 fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
453 if ruleset.requested_handled_fs.contains(self.allowed_access) {
458 Ok(())
459 } else {
460 Err(AddRuleError::UnhandledAccess {
461 access: self.allowed_access,
462 incompatible: self.allowed_access & !ruleset.requested_handled_fs,
463 }
464 .into())
465 }
466 }
467}
468
469#[test]
470fn path_beneath_check_consistency() {
471 use crate::*;
472
473 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
474 let rx_access = AccessFs::Execute | AccessFs::ReadFile;
475 assert!(matches!(
476 Ruleset::from(ABI::Unsupported)
477 .handle_access(ro_access)
478 .unwrap()
479 .create()
480 .unwrap()
481 .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
482 .unwrap_err(),
483 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
484 if access == rx_access && incompatible == AccessFs::Execute
485 ));
486}
487
488#[derive(Debug)]
508pub struct PathFd {
509 fd: OwnedFd,
510}
511
512impl PathFd {
513 pub fn new<T>(path: T) -> Result<Self, PathFdError>
514 where
515 T: AsRef<Path>,
516 {
517 Ok(PathFd {
518 fd: OpenOptions::new()
519 .read(true)
520 .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
522 .open(path.as_ref())
523 .map_err(|e| PathFdError::OpenCall {
524 source: e,
525 path: path.as_ref().into(),
526 })?
527 .into(),
528 })
529 }
530}
531
532impl AsFd for PathFd {
533 fn as_fd(&self) -> BorrowedFd<'_> {
534 self.fd.as_fd()
535 }
536}
537
538#[test]
539fn path_fd() {
540 use std::fs::File;
541 use std::io::Read;
542
543 PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
544 PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
545
546 let mut buffer = [0; 1];
547 File::from(PathFd::new("/etc/passwd").unwrap().fd)
549 .read(&mut buffer)
550 .unwrap_err();
551}
552
553pub fn path_beneath_rules<I, P, A>(
597 paths: I,
598 access: A,
599) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
600where
601 I: IntoIterator<Item = P>,
602 P: AsRef<Path>,
603 A: Into<BitFlags<AccessFs>>,
604{
605 let access = access.into();
606 paths.into_iter().filter_map(move |p| match PathFd::new(p) {
607 Ok(f) => {
608 let valid_access = match is_file(&f) {
609 Ok(true) => access & ACCESS_FILE,
610 Err(_) | Ok(false) => access,
612 };
613 Some(Ok(PathBeneath::new(f, valid_access)))
614 }
615 Err(_) => None,
616 })
617}
618
619#[test]
620fn path_beneath_rules_iter() {
621 let _ = Ruleset::default()
622 .handle_access(AccessFs::from_all(ABI::V1))
623 .unwrap()
624 .create()
625 .unwrap()
626 .add_rules(path_beneath_rules(
627 &["/usr", "/opt", "/does-not-exist", "/root"],
628 AccessFs::Execute,
629 ))
630 .unwrap();
631}