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