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