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 assert_eq!(access_read, !access_write & access_all);
160 assert_eq!(access_read | access_write, access_all);
161 }
162}
163
164impl HandledAccess for AccessFs {}
165
166impl PrivateHandledAccess for AccessFs {
167 fn ruleset_handle_access(
168 ruleset: &mut Ruleset,
169 access: BitFlags<Self>,
170 ) -> Result<(), HandleAccessesError> {
171 ruleset.requested_handled_fs |= access;
173 ruleset.actual_handled_fs |= match access
174 .try_compat(
175 ruleset.compat.abi(),
176 ruleset.compat.level,
177 &mut ruleset.compat.state,
178 )
179 .map_err(HandleAccessError::Compat)?
180 {
181 Some(a) => a,
182 None => return Ok(()),
183 };
184 Ok(())
185 }
186
187 fn into_add_rules_error(error: AddRuleError<Self>) -> AddRulesError {
188 AddRulesError::Fs(error)
189 }
190
191 fn into_handle_accesses_error(error: HandleAccessError<Self>) -> HandleAccessesError {
192 HandleAccessesError::Fs(error)
193 }
194}
195
196const ACCESS_FILE: BitFlags<AccessFs> = make_bitflags!(AccessFs::{
199 ReadFile | WriteFile | Execute | Truncate | IoctlDev
200});
201
202fn is_file<F>(fd: F) -> Result<bool, Error>
204where
205 F: AsFd,
206{
207 unsafe {
208 let mut stat = zeroed();
209 match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) {
210 0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR),
211 _ => Err(Error::last_os_error()),
212 }
213 }
214}
215
216#[cfg_attr(test, derive(Debug))]
228pub struct PathBeneath<F> {
229 attr: uapi::landlock_path_beneath_attr,
230 parent_fd: F,
232 allowed_access: BitFlags<AccessFs>,
233 compat_level: Option<CompatLevel>,
234}
235
236impl<F> PathBeneath<F>
237where
238 F: AsFd,
239{
240 pub fn new<A>(parent: F, access: A) -> Self
244 where
245 A: Into<BitFlags<AccessFs>>,
246 {
247 PathBeneath {
248 attr: unsafe { zeroed() },
250 parent_fd: parent,
251 allowed_access: access.into(),
252 compat_level: None,
253 }
254 }
255}
256
257impl<F> TryCompat<AccessFs> for PathBeneath<F>
258where
259 F: AsFd,
260{
261 fn try_compat_children<L>(
262 mut self,
263 abi: ABI,
264 parent_level: L,
265 compat_state: &mut CompatState,
266 ) -> Result<Option<Self>, CompatError<AccessFs>>
267 where
268 L: Into<CompatLevel>,
269 {
270 self.allowed_access = match self.allowed_access.try_compat(
272 abi,
273 self.tailored_compat_level(parent_level),
274 compat_state,
275 )? {
276 Some(a) => a,
277 None => return Ok(None),
278 };
279 Ok(Some(self))
280 }
281
282 fn try_compat_inner(
283 &mut self,
284 _abi: ABI,
285 ) -> Result<CompatResult<AccessFs>, CompatError<AccessFs>> {
286 let valid_access =
288 if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? {
289 self.allowed_access & ACCESS_FILE
290 } else {
291 self.allowed_access
292 };
293
294 if self.allowed_access != valid_access {
295 let error = PathBeneathError::DirectoryAccess {
296 access: self.allowed_access,
297 incompatible: self.allowed_access ^ valid_access,
298 }
299 .into();
300 self.allowed_access = valid_access;
301 Ok(CompatResult::Partial(error))
303 } else {
304 Ok(CompatResult::Full)
305 }
306 }
307}
308
309#[test]
310fn path_beneath_try_compat_children() {
311 use crate::*;
312
313 let access_file = AccessFs::ReadFile | AccessFs::Refer;
315
316 let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap();
318 ruleset.compat.state = CompatState::Dummy;
320 assert!(matches!(
321 RulesetCreated::new(ruleset, None)
322 .set_compatibility(CompatLevel::HardRequirement)
323 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
324 .unwrap_err(),
325 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
326 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
327 ))) if access == access_file && incompatible == AccessFs::Refer
328 ));
329
330 let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap();
332 ruleset.compat.state = CompatState::Dummy;
334 assert!(matches!(
335 RulesetCreated::new(ruleset, None)
336 .set_compatibility(CompatLevel::HardRequirement)
337 .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file))
338 .unwrap_err(),
339 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat(
340 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
341 ))) if access == access_file && incompatible == AccessFs::Refer
342 ));
343}
344
345#[test]
346fn path_beneath_try_compat() {
347 use crate::*;
348
349 let abi = ABI::V1;
350
351 for file in &["/etc/passwd", "/dev/null"] {
352 let mut compat_state = CompatState::Init;
353 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
354 assert!(matches!(
355 PathBeneath::new(PathFd::new(file).unwrap(), ro_access)
356 .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state)
357 .unwrap_err(),
358 CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible })
359 if access == ro_access && incompatible == AccessFs::ReadDir
360 ));
361
362 let mut compat_state = CompatState::Init;
363 assert!(matches!(
364 PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY)
365 .try_compat(abi, CompatLevel::BestEffort, &mut compat_state)
366 .unwrap_err(),
367 CompatError::Access(AccessError::Empty)
368 ));
369 }
370
371 let full_access = AccessFs::from_all(ABI::V1);
372 for compat_level in &[
373 CompatLevel::BestEffort,
374 CompatLevel::SoftRequirement,
375 CompatLevel::HardRequirement,
376 ] {
377 let mut compat_state = CompatState::Init;
378 let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access)
379 .try_compat(abi, *compat_level, &mut compat_state)
380 .unwrap()
381 .unwrap();
382 assert_eq!(compat_state, CompatState::Full);
383
384 let raw_access = path_beneath.attr.allowed_access;
386 assert_eq!(raw_access, 0);
387
388 let _ = path_beneath.as_ptr();
390 let raw_access = path_beneath.attr.allowed_access;
391 assert_eq!(raw_access, full_access.bits());
392 }
393}
394
395impl<F> OptionCompatLevelMut for PathBeneath<F> {
396 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
397 &mut self.compat_level
398 }
399}
400
401impl<F> OptionCompatLevelMut for &mut PathBeneath<F> {
402 fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel> {
403 &mut self.compat_level
404 }
405}
406
407impl<F> Compatible for PathBeneath<F> {}
408
409impl<F> Compatible for &mut PathBeneath<F> {}
410
411#[test]
412fn path_beneath_compatibility() {
413 let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1));
414 let path_ref = &mut path;
415
416 let level = path_ref.as_option_compat_level_mut();
417 assert_eq!(level, &None);
418 assert_eq!(
419 <Option<CompatLevel> as Into<CompatLevel>>::into(*level),
420 CompatLevel::BestEffort
421 );
422
423 path_ref.set_compatibility(CompatLevel::SoftRequirement);
424 assert_eq!(
425 path_ref.as_option_compat_level_mut(),
426 &Some(CompatLevel::SoftRequirement)
427 );
428
429 path.set_compatibility(CompatLevel::HardRequirement);
430}
431
432impl<F> Rule<AccessFs> for PathBeneath<F> where F: AsFd {}
435
436impl<F> PrivateRule<AccessFs> for PathBeneath<F>
437where
438 F: AsFd,
439{
440 const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH;
441
442 fn as_ptr(&mut self) -> *const libc::c_void {
443 self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd();
444 self.attr.allowed_access = self.allowed_access.bits();
445 &self.attr as *const _ as _
446 }
447
448 fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> {
449 if ruleset.requested_handled_fs.contains(self.allowed_access) {
454 Ok(())
455 } else {
456 Err(AddRuleError::UnhandledAccess {
457 access: self.allowed_access,
458 incompatible: self.allowed_access & !ruleset.requested_handled_fs,
459 }
460 .into())
461 }
462 }
463}
464
465#[test]
466fn path_beneath_check_consistency() {
467 use crate::*;
468
469 let ro_access = AccessFs::ReadDir | AccessFs::ReadFile;
470 let rx_access = AccessFs::Execute | AccessFs::ReadFile;
471 assert!(matches!(
472 Ruleset::from(ABI::Unsupported)
473 .handle_access(ro_access)
474 .unwrap()
475 .create()
476 .unwrap()
477 .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access))
478 .unwrap_err(),
479 RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible }))
480 if access == rx_access && incompatible == AccessFs::Execute
481 ));
482}
483
484#[cfg_attr(test, derive(Debug))]
504pub struct PathFd {
505 fd: OwnedFd,
506}
507
508impl PathFd {
509 pub fn new<T>(path: T) -> Result<Self, PathFdError>
510 where
511 T: AsRef<Path>,
512 {
513 Ok(PathFd {
514 fd: OpenOptions::new()
515 .read(true)
516 .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
518 .open(path.as_ref())
519 .map_err(|e| PathFdError::OpenCall {
520 source: e,
521 path: path.as_ref().into(),
522 })?
523 .into(),
524 })
525 }
526}
527
528impl AsFd for PathFd {
529 fn as_fd(&self) -> BorrowedFd<'_> {
530 self.fd.as_fd()
531 }
532}
533
534#[test]
535fn path_fd() {
536 use std::fs::File;
537 use std::io::Read;
538
539 PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute);
540 PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute);
541
542 let mut buffer = [0; 1];
543 File::from(PathFd::new("/etc/passwd").unwrap().fd)
545 .read(&mut buffer)
546 .unwrap_err();
547}
548
549pub fn path_beneath_rules<I, P, A>(
586 paths: I,
587 access: A,
588) -> impl Iterator<Item = Result<PathBeneath<PathFd>, RulesetError>>
589where
590 I: IntoIterator<Item = P>,
591 P: AsRef<Path>,
592 A: Into<BitFlags<AccessFs>>,
593{
594 let access = access.into();
595 paths.into_iter().filter_map(move |p| match PathFd::new(p) {
596 Ok(f) => {
597 let valid_access = match is_file(&f) {
598 Ok(true) => access & ACCESS_FILE,
599 Err(_) | Ok(false) => access,
601 };
602 Some(Ok(PathBeneath::new(f, valid_access)))
603 }
604 Err(_) => None,
605 })
606}
607
608#[test]
609fn path_beneath_rules_iter() {
610 let _ = Ruleset::default()
611 .handle_access(AccessFs::from_all(ABI::V1))
612 .unwrap()
613 .create()
614 .unwrap()
615 .add_rules(path_beneath_rules(
616 &["/usr", "/opt", "/does-not-exist", "/root"],
617 AccessFs::Execute,
618 ))
619 .unwrap();
620}