1use std::{
10 collections::HashMap,
11 ffi::{OsStr, OsString},
12 fs::File,
13 io::{BufRead, BufReader, Cursor, Read},
14 os::unix::ffi::OsStrExt,
15 path::{Path, PathBuf},
16};
17
18use anyhow::{Context, Result, bail, ensure};
19use fn_error_context::context;
20use pcre2::bytes::Regex;
21use regex_automata::{Anchored, Input, hybrid::dfa, util::syntax};
22use rustix::{
23 fd::AsFd,
24 fs::{Mode, OFlags, openat},
25 io::Errno,
26};
27
28use composefs::{
29 fsverity::FsVerityHashValue,
30 repository::Repository,
31 tree::{Directory, DirectoryRef, FileSystem, Inode, Leaf, LeafContent, RegularFile, Stat},
32};
33
34pub const XATTR_SECURITY_SELINUX: &str = "security.selinux";
40
41#[context("Processing SELinux substitutions file")]
42fn process_subs_file(file: impl Read, aliases: &mut HashMap<OsString, OsString>) -> Result<()> {
43 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
45 let line = item?;
46 let mut parts = line.split_whitespace();
47 let alias = match parts.next() {
48 None => continue, Some(comment) if comment.starts_with("#") => continue,
50 Some(alias) => alias,
51 };
52 let Some(original) = parts.next() else {
53 bail!("{line_nr}: missing original path");
54 };
55 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
56
57 aliases.insert(OsString::from(alias), OsString::from(original));
58 }
59 Ok(())
60}
61
62fn process_spec_file(
63 file: impl Read,
64 regexps: &mut Vec<String>,
65 contexts: &mut Vec<String>,
66) -> Result<()> {
67 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
69 let line = item?;
70
71 let mut parts = line.split_whitespace();
72 let regex = match parts.next() {
73 None => continue, Some(comment) if comment.starts_with("#") => continue,
75 Some(regex) => regex,
76 };
77
78 let Some(next) = parts.next() else {
83 bail!("{line_nr}: missing separator after regex");
84 };
85 if let Some(ifmt) = next.strip_prefix("-") {
86 ensure!(
87 ["b", "c", "d", "p", "l", "s", "-"].contains(&ifmt),
88 "{line_nr}: invalid type code -{ifmt}"
89 );
90 let Some(context) = parts.next() else {
91 bail!("{line_nr}: missing context field");
92 };
93 regexps.push(format!("^({regex}){ifmt}$"));
94 contexts.push(context.to_string());
95 } else {
96 let context = next;
97 regexps.push(format!("^({regex}).$"));
98 contexts.push(context.to_string());
99 }
100 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
101 }
102
103 Ok(())
104}
105
106#[cfg(test)]
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126enum MatchStrategy {
127 Dfa,
130 Pcre,
132 Hybrid,
134}
135
136struct DfaState {
138 dfa: dfa::DFA,
139 cache: dfa::Cache,
140 context_map: Vec<usize>,
142}
143
144struct PcreFallback {
147 index: usize,
149 regex: Regex,
150}
151
152struct Matcher {
158 dfa: Option<DfaState>,
160 pcre_fallbacks: Vec<PcreFallback>,
162}
163
164struct Policy {
165 aliases: HashMap<OsString, OsString>,
166 matcher: Matcher,
167 contexts: Vec<String>,
169}
170
171fn dfa_syntax_config() -> syntax::Config {
175 syntax::Config::new()
176 .unicode(false)
177 .utf8(false)
178 .line_terminator(0)
179}
180
181fn make_dfa_builder() -> dfa::Builder {
182 let mut builder = dfa::Builder::new();
183 builder.syntax(dfa_syntax_config());
184 builder.configure(
185 dfa::Config::new()
186 .cache_capacity(10_000_000)
187 .skip_cache_capacity_check(true),
188 );
189 builder
190}
191
192fn is_dfa_compatible(syntax_config: &syntax::Config, pattern: &str) -> bool {
195 syntax::parse_with(pattern, syntax_config).is_ok()
196}
197
198impl Matcher {
199 fn build(regexps: &[String]) -> Result<Self> {
206 let builder = make_dfa_builder();
207 match builder.build_many(regexps) {
208 Ok(dfa) => {
209 let cache = dfa.create_cache();
210 Ok(Matcher {
211 dfa: Some(DfaState {
212 dfa,
213 cache,
214 context_map: (0..regexps.len()).collect(),
215 }),
216 pcre_fallbacks: vec![],
217 })
218 }
219 Err(_) => Self::build_partitioned(&builder, regexps),
220 }
221 }
222
223 #[cfg(test)]
225 fn build_with_strategy(strategy: MatchStrategy, regexps: &[String]) -> Result<Self> {
226 match strategy {
227 MatchStrategy::Hybrid => Self::build(regexps),
228 MatchStrategy::Dfa => {
229 let dfa = make_dfa_builder().build_many(regexps)?;
230 let cache = dfa.create_cache();
231 Ok(Matcher {
232 dfa: Some(DfaState {
233 dfa,
234 cache,
235 context_map: (0..regexps.len()).collect(),
236 }),
237 pcre_fallbacks: vec![],
238 })
239 }
240 MatchStrategy::Pcre => {
241 let mut fallbacks = Vec::with_capacity(regexps.len());
242 for (i, r) in regexps.iter().enumerate() {
243 fallbacks.push(PcreFallback {
244 index: i,
245 regex: Regex::new(r)
246 .with_context(|| format!("Compiling PCRE2 regex: {r}"))?,
247 });
248 }
249 Ok(Matcher {
250 dfa: None,
251 pcre_fallbacks: fallbacks,
252 })
253 }
254 }
255 }
256
257 fn build_partitioned(builder: &dfa::Builder, regexps: &[String]) -> Result<Self> {
263 let syntax_config = dfa_syntax_config();
264 let mut dfa_indices = Vec::new();
265 let mut dfa_patterns = Vec::new();
266 let mut pcre_fallbacks = Vec::new();
267
268 for (i, pattern) in regexps.iter().enumerate() {
269 if is_dfa_compatible(&syntax_config, pattern) {
270 dfa_indices.push(i);
271 dfa_patterns.push(pattern.as_str());
272 } else {
273 pcre_fallbacks.push(PcreFallback {
274 index: i,
275 regex: Regex::new(pattern)
276 .with_context(|| format!("Compiling PCRE2 regex: {pattern}"))?,
277 });
278 }
279 }
280
281 let dfa_state = if dfa_patterns.is_empty() {
282 None
283 } else {
284 let dfa = builder.build_many(&dfa_patterns)?;
285 let cache = dfa.create_cache();
286 Some(DfaState {
287 dfa,
288 cache,
289 context_map: dfa_indices,
290 })
291 };
292
293 Ok(Matcher {
294 dfa: dfa_state,
295 pcre_fallbacks,
296 })
297 }
298
299 fn lookup(&mut self, key: &[u8]) -> Option<usize> {
305 let dfa_idx = self.dfa.as_mut().and_then(|d| {
307 let input = Input::new(key).anchored(Anchored::Yes);
308 d.dfa
309 .try_search_fwd(&mut d.cache, &input)
310 .expect("DFA search error")
311 .map(|hm| d.context_map[hm.pattern().as_usize()])
312 });
313
314 for fb in &self.pcre_fallbacks {
318 if dfa_idx.is_some_and(|d| fb.index >= d) {
319 break;
320 }
321 if fb.regex.is_match(key).unwrap_or(false) {
322 return Some(fb.index);
323 }
324 }
325
326 dfa_idx
327 }
328}
329
330pub fn open_file<H: FsVerityHashValue>(
332 dir: DirectoryRef<'_, H>,
333 filename: impl AsRef<OsStr>,
334 repo: &Repository<H>,
335) -> Result<Option<Box<dyn Read>>> {
336 match dir.get_file_opt(filename.as_ref())? {
337 Some(file) => match file {
338 RegularFile::Inline(data) => Ok(Some(Box::new(Cursor::new(data.clone())))),
339 RegularFile::External(id, ..) | RegularFile::ExternalNoVerity(id, ..) => {
340 Ok(Some(Box::new(File::from(repo.open_object(id)?))))
341 }
342 RegularFile::Sparse(..) => Ok(None),
343 },
344 None => Ok(None),
345 }
346}
347
348fn open_file_from_dir(
350 dirfd: impl AsFd,
351 filename: impl AsRef<OsStr>,
352) -> Result<Option<Box<dyn Read>>> {
353 match openat(
354 dirfd,
355 filename.as_ref(),
356 OFlags::RDONLY | OFlags::CLOEXEC,
357 Mode::empty(),
358 ) {
359 Ok(fd) => Ok(Some(Box::new(File::from(fd)))),
360 Err(Errno::NOENT) => Ok(None),
361 Err(e) => Err(e.into()),
362 }
363}
364
365impl Policy {
366 #[context("Building SELinux policy")]
371 fn build_from(mut open: impl FnMut(&str) -> Result<Option<Box<dyn Read>>>) -> Result<Self> {
372 let mut aliases = HashMap::new();
373 let mut regexps = vec![];
374 let mut contexts = vec![];
375
376 for suffix in ["", ".local", ".homedirs"] {
377 let name = format!("file_contexts{suffix}");
378 if let Some(file) = open(&name)? {
379 process_spec_file(file, &mut regexps, &mut contexts)
380 .with_context(|| format!("SELinux spec file {name}"))?;
381 } else if suffix.is_empty() {
382 bail!("SELinux policy is missing mandatory file_contexts file");
383 }
384 }
385
386 for suffix in [".subs", ".subs_dist"] {
387 let name = format!("file_contexts{suffix}");
388 if let Some(file) = open(&name)? {
389 process_subs_file(file, &mut aliases)
390 .with_context(|| format!("SELinux subs file {name}"))?;
391 }
392 }
393
394 regexps.reverse();
396 contexts.reverse();
397
398 let matcher = Matcher::build(®exps)?;
399
400 Ok(Policy {
401 aliases,
402 matcher,
403 contexts,
404 })
405 }
406
407 pub fn check_aliased(&self, filename: &OsStr) -> Option<&OsStr> {
408 self.aliases.get(filename).map(|x| x.as_os_str())
409 }
410
411 pub fn lookup(&mut self, filename: &OsStr, ifmt: u8) -> Option<&str> {
413 let key = [filename.as_bytes(), &[ifmt]].concat();
414 self.matcher.lookup(&key).and_then(|idx| {
415 let ctx = self.contexts[idx].as_str();
416 (ctx != "<<none>>").then_some(ctx)
417 })
418 }
419}
420
421fn relabel(stat: &mut Stat, path: &Path, ifmt: u8, policy: &mut Policy) {
422 let key = OsStr::new(XATTR_SECURITY_SELINUX);
423
424 if let Some(label) = policy.lookup(path.as_os_str(), ifmt) {
425 stat.xattrs
426 .insert(Box::from(key), Box::from(label.as_bytes()));
427 } else {
428 stat.xattrs.remove(key);
429 }
430}
431
432fn relabel_dir<H: FsVerityHashValue>(
433 dir: &mut Directory<H>,
434 leaves: &mut Vec<Leaf<H>>,
435 path: &mut PathBuf,
436 policy: &mut Policy,
437 labeled: &mut HashMap<composefs::generic_tree::LeafId, Option<Box<[u8]>>>,
441) {
442 use composefs::generic_tree::LeafId;
443
444 relabel(&mut dir.stat, path, b'd', policy);
445
446 let children: Vec<(Box<OsStr>, Option<LeafId>)> = dir
448 .sorted_entries()
449 .map(|(name, inode)| {
450 let id = match inode {
451 Inode::Leaf(id, _) => Some(*id),
452 Inode::Directory(_) => None,
453 };
454 (Box::from(name), id)
455 })
456 .collect();
457
458 for (name, leaf_id) in children {
459 path.push(Path::new(&name));
460 let aliased_path = policy.check_aliased(path.as_os_str()).map(PathBuf::from);
461 let effective_path = aliased_path.as_deref().unwrap_or(path.as_path());
462
463 if let Some(id) = leaf_id {
464 let ifmt = match leaves[id.0].content {
466 LeafContent::Regular(..) => b'-',
467 LeafContent::Fifo => b'p',
468 LeafContent::Socket => b's',
469 LeafContent::Symlink(..) => b'l',
470 LeafContent::BlockDevice(..) => b'b',
471 LeafContent::CharacterDevice(..) => b'c',
472 };
473 let new_label: Option<&str> = policy.lookup(effective_path.as_os_str(), ifmt);
474
475 let effective_id = if let Some(prev_label) = labeled.get(&id) {
477 let labels_match = match (prev_label.as_deref(), new_label) {
479 (Some(p), Some(n)) => p == n.as_bytes(),
480 (None, None) => true,
481 _ => false,
482 };
483
484 if labels_match {
485 id
487 } else {
488 let clone = leaves[id.0].clone();
492 let new_id = LeafId(leaves.len());
493 leaves.push(clone);
494 dir.remap_leaf(name.as_ref(), new_id);
496 new_id
497 }
498 } else {
499 id
500 };
501
502 let key = OsStr::new(XATTR_SECURITY_SELINUX);
504 if let Some(label) = new_label {
505 leaves[effective_id.0]
506 .stat
507 .xattrs
508 .insert(Box::from(key), Box::from(label.as_bytes()));
509 } else {
510 leaves[effective_id.0].stat.xattrs.remove(key);
511 }
512
513 labeled
515 .entry(effective_id)
516 .or_insert_with(|| new_label.map(|l| Box::from(l.as_bytes())));
517 } else {
518 let mut sub_path = effective_path.to_path_buf();
519 let subdir = dir.get_directory_mut(name.as_ref()).unwrap();
520 relabel_dir(subdir, leaves, &mut sub_path, policy, labeled);
521 }
522
523 path.pop();
524 }
525}
526
527fn parse_config(file: impl Read) -> Result<Option<String>> {
528 for line in BufReader::new(file).lines() {
529 if let Some((key, value)) = line?.split_once('=') {
530 if key.trim().eq_ignore_ascii_case("SELINUXTYPE") {
532 return Ok(Some(value.trim().to_string()));
533 }
534 }
535 }
536 Ok(None)
537}
538
539fn strip_selinux_labels<H: FsVerityHashValue>(fs: &mut FileSystem<H>) {
540 fs.for_each_stat_mut(|stat| {
541 stat.xattrs.remove(OsStr::new(XATTR_SECURITY_SELINUX));
542 });
543}
544
545fn build_policy(
548 mut open_config: impl FnMut(&str) -> Result<Option<Box<dyn Read>>>,
549 mut open_policy_file: impl FnMut(&str, &str) -> Result<Option<Box<dyn Read>>>,
550) -> Result<Option<Policy>> {
551 let Some(etc_selinux_config) = open_config("config")? else {
552 return Ok(None);
553 };
554
555 let Some(policy_name) = parse_config(etc_selinux_config)? else {
556 return Ok(None);
557 };
558
559 let policy = Policy::build_from(|filename| open_policy_file(&policy_name, filename))?;
560 Ok(Some(policy))
561}
562
563fn apply_policy<H: FsVerityHashValue>(fs: &mut FileSystem<H>, policy: Option<Policy>) -> bool {
565 match policy {
566 Some(mut policy) => {
567 let mut path = PathBuf::from("/");
568 let mut labeled = HashMap::new();
569 let FileSystem { root, leaves } = fs;
570 relabel_dir(root, leaves, &mut path, &mut policy, &mut labeled);
571 true
572 }
573 None => {
574 strip_selinux_labels(fs);
575 false
576 }
577 }
578}
579
580#[context("Applying SELinux labels to filesystem")]
599pub fn selabel<H: FsVerityHashValue>(fs: &mut FileSystem<H>, repo: &Repository<H>) -> Result<bool> {
600 let policy = {
602 let root = fs.as_dir();
603 let Some(etc_selinux) = root.get_directory_ref_opt("etc/selinux".as_ref())? else {
604 strip_selinux_labels(fs);
605 return Ok(false);
606 };
607
608 build_policy(
609 |filename| open_file(etc_selinux, filename, repo),
610 |policy_name, filename| {
611 let dir = etc_selinux
612 .get_directory_ref(policy_name.as_ref())?
613 .get_directory_ref("contexts/files".as_ref())?;
614 open_file(dir, filename, repo)
615 },
616 )?
617 };
618
619 Ok(apply_policy(fs, policy))
621}
622
623#[context("Applying SELinux labels to filesystem from directory")]
643pub fn selabel_from_dir(
644 fs: &mut FileSystem<impl FsVerityHashValue>,
645 rootfs: impl AsFd,
646) -> Result<bool> {
647 let etc_selinux = match openat(
649 &rootfs,
650 "etc/selinux",
651 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
652 Mode::empty(),
653 ) {
654 Ok(fd) => fd,
655 Err(Errno::NOENT) => {
656 strip_selinux_labels(fs);
657 return Ok(false);
658 }
659 Err(e) => return Err(e.into()),
660 };
661
662 let policy = build_policy(
663 |filename| open_file_from_dir(&etc_selinux, filename),
664 |policy_name, filename| {
665 let path = format!("{policy_name}/contexts/files/{filename}");
666 open_file_from_dir(&etc_selinux, path)
667 },
668 )?;
669
670 Ok(apply_policy(fs, policy))
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676
677 use composefs::dumpfile::dumpfile_to_filesystem;
678 use composefs::fsverity::Sha256HashValue;
679 use composefs::generic_tree::LeafId;
680 use composefs::test::TestRepo;
681 use indoc::indoc;
682
683 fn collect_leaf_ids(dir: &Directory<Sha256HashValue>) -> Vec<LeafId> {
685 let mut ids = Vec::new();
686 for inode in dir.inodes() {
687 match inode {
688 Inode::Directory(sub) => ids.extend(collect_leaf_ids(sub)),
689 Inode::Leaf(id, _) => ids.push(*id),
690 }
691 }
692 ids
693 }
694
695 fn assert_no_hardlinks(fs: &FileSystem<Sha256HashValue>) {
699 let ids = collect_leaf_ids(&fs.root);
700 let mut seen = std::collections::HashSet::new();
701 for id in &ids {
702 assert!(
703 seen.insert(id.0),
704 "LeafId {} is shared between two paths after selabel (hardlink not broken)",
705 id.0,
706 );
707 }
708 }
709
710 fn selinux_label(stat: &Stat) -> Option<String> {
712 stat.xattrs
713 .get(OsStr::new(XATTR_SECURITY_SELINUX))
714 .map(|v| String::from_utf8_lossy(v).into())
715 }
716
717 fn get_label(fs: &FileSystem<Sha256HashValue>, path: &str) -> Option<String> {
722 if path == "/" {
723 return selinux_label(&fs.root.stat);
724 }
725 let p = Path::new(path);
726 let parent = p.parent().unwrap();
727 let name = p.file_name().unwrap();
728 let root = fs.as_dir();
729 let dir = if parent == Path::new("/") {
730 root
731 } else {
732 root.get_directory_ref(parent.as_os_str()).unwrap()
733 };
734 match dir
735 .lookup(name)
736 .unwrap_or_else(|| panic!("{path} not found"))
737 {
738 Inode::Directory(d) => selinux_label(&d.stat),
739 Inode::Leaf(leaf_id, _) => selinux_label(&fs.leaf(*leaf_id).stat),
740 }
741 }
742
743 fn build_fs_with_selinux(
753 file_contexts: &[u8],
754 extra_policy_files: &[(&str, &[u8])],
755 fs_entries: &str,
756 ) -> FileSystem<Sha256HashValue> {
757 use composefs::dumpfile::write_dumpfile;
758
759 let dir_stat = || Stat {
760 st_mode: 0o40755,
761 st_uid: 0,
762 st_gid: 0,
763 st_mtim_sec: 0,
764 st_mtim_nsec: 0,
765 xattrs: Default::default(),
766 };
767
768 let mut fs = FileSystem::<Sha256HashValue>::new(dir_stat());
769
770 let push_inline =
772 |fs: &mut FileSystem<Sha256HashValue>, data: &[u8]| -> Inode<Sha256HashValue> {
773 let id = fs.push_leaf(
774 Stat {
775 st_mode: 0o100644,
776 st_uid: 0,
777 st_gid: 0,
778 st_mtim_sec: 0,
779 st_mtim_nsec: 0,
780 xattrs: Default::default(),
781 },
782 LeafContent::Regular(RegularFile::Inline(data.to_vec().into_boxed_slice())),
783 );
784 Inode::leaf(id)
785 };
786
787 let selinux_config = b"SELINUX=enforcing\nSELINUXTYPE=targeted\n";
791
792 for path in [
794 "etc",
795 "etc/selinux",
796 "etc/selinux/targeted",
797 "etc/selinux/targeted/contexts",
798 "etc/selinux/targeted/contexts/files",
799 ] {
800 let (dir, name) = fs.root.split_mut(path.as_ref()).unwrap();
801 dir.insert(name, Inode::Directory(Box::new(Directory::new(dir_stat()))));
802 }
803 let config_inode = push_inline(&mut fs, selinux_config);
804 fs.root
805 .get_directory_mut("etc/selinux".as_ref())
806 .unwrap()
807 .insert(OsStr::new("config"), config_inode);
808
809 let fc_inode = push_inline(&mut fs, file_contexts);
811 let extra_inodes: Vec<_> = extra_policy_files
812 .iter()
813 .map(|(name, content)| (name.to_string(), push_inline(&mut fs, content)))
814 .collect();
815
816 let files_dir = fs
817 .root
818 .get_directory_mut("etc/selinux/targeted/contexts/files".as_ref())
819 .unwrap();
820 files_dir.insert(OsStr::new("file_contexts"), fc_inode);
821 for (name, inode) in extra_inodes {
822 files_dir.insert(OsStr::new(&name), inode);
823 }
824
825 let mut buf = Vec::new();
827 write_dumpfile(&mut buf, &fs).unwrap();
828 let mut dumpfile = String::from_utf8(buf).unwrap();
829 dumpfile.push_str(fs_entries);
830 dumpfile_to_filesystem(&dumpfile).unwrap()
831 }
832
833 #[test]
836 fn selabel_applies_correct_labels() {
837 let file_contexts = indoc! {b"
838 /\t\tsystem_u:object_r:root_t:s0
839 /usr\t\tsystem_u:object_r:usr_t:s0
840 /usr/bin(/.*)?\t\tsystem_u:object_r:bin_t:s0
841 /etc(/.*)?\t\tsystem_u:object_r:etc_t:s0
842 "};
843
844 let fs_entries = "\
845/boot 0 40755 2 0 0 0 0.0 - - -
846/etc/hostname 9 100644 1 0 0 0 0.0 - testhost\\n -
847/sysroot 0 40755 2 0 0 0 0.0 - - -
848/usr 0 40755 2 0 0 0 1000.0 - - -
849/usr/bin 0 40755 2 0 0 0 1000.0 - - -
850/usr/bin/hello 21 100755 1 0 0 0 0.0 - #!/bin/sh\\necho\\x20hello\\n -
851";
852 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
853 let test_repo = TestRepo::<Sha256HashValue>::new();
854
855 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
856
857 assert_eq!(get_label(&fs, "/").unwrap(), "system_u:object_r:root_t:s0");
858 assert_eq!(
859 get_label(&fs, "/usr").unwrap(),
860 "system_u:object_r:usr_t:s0"
861 );
862 assert_eq!(
863 get_label(&fs, "/usr/bin").unwrap(),
864 "system_u:object_r:bin_t:s0"
865 );
866 assert_eq!(
867 get_label(&fs, "/usr/bin/hello").unwrap(),
868 "system_u:object_r:bin_t:s0"
869 );
870 assert_eq!(
871 get_label(&fs, "/etc").unwrap(),
872 "system_u:object_r:etc_t:s0"
873 );
874 assert_eq!(
875 get_label(&fs, "/etc/hostname").unwrap(),
876 "system_u:object_r:etc_t:s0"
877 );
878 }
879
880 #[test]
882 fn selabel_strips_when_no_policy() {
883 let dumpfile = "\
884/ 0 40755 2 0 0 0 0.0 - - -
885/file 1 100644 1 0 0 0 0.0 - x - security.selinux=old_label
886";
887 let mut fs = dumpfile_to_filesystem::<Sha256HashValue>(dumpfile).unwrap();
888 let test_repo = TestRepo::<Sha256HashValue>::new();
889
890 assert!(!selabel(&mut fs, &test_repo.repo).unwrap());
891 assert!(get_label(&fs, "/").is_none());
892 assert!(get_label(&fs, "/file").is_none());
893 }
894
895 #[test]
898 fn selabel_type_specific_labels() {
899 let file_contexts = indoc! {b"
902 /var(/.*)? system_u:object_r:var_t:s0
903 /var/log(/.*)? -d system_u:object_r:var_log_dir_t:s0
904 /var/log(/.*)? -- system_u:object_r:var_log_t:s0
905 /var/log(/.*)? -l system_u:object_r:var_log_link_t:s0
906 "};
907
908 let fs_entries = "\
909/var 0 40755 2 0 0 0 0.0 - - -
910/var/log 0 40755 2 0 0 0 0.0 - - -
911/var/log/messages 10 100644 1 0 0 0 0.0 - 0123456789 -
912/var/log/current 4 120777 1 0 0 0 0.0 /var/log/messages - -
913";
914 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
915 let test_repo = TestRepo::<Sha256HashValue>::new();
916
917 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
918
919 assert_eq!(
920 get_label(&fs, "/var").unwrap(),
921 "system_u:object_r:var_t:s0"
922 );
923 assert_eq!(
924 get_label(&fs, "/var/log").unwrap(),
925 "system_u:object_r:var_log_dir_t:s0"
926 );
927 assert_eq!(
928 get_label(&fs, "/var/log/messages").unwrap(),
929 "system_u:object_r:var_log_t:s0"
930 );
931 assert_eq!(
932 get_label(&fs, "/var/log/current").unwrap(),
933 "system_u:object_r:var_log_link_t:s0"
934 );
935 }
936
937 #[test]
939 fn selabel_subs_aliases() {
940 let file_contexts = indoc! {b"
941 /home(/.*)? system_u:object_r:home_t:s0
942 "};
943 let subs_content = b"/srv/home /home\n";
944
945 let fs_entries = "\
946/home 0 40755 2 0 0 0 0.0 - - -
947/home/user.txt 5 100644 1 0 0 0 0.0 - hello -
948/srv 0 40755 2 0 0 0 0.0 - - -
949/srv/home 0 40755 2 0 0 0 0.0 - - -
950/srv/home/data.txt 5 100644 1 0 0 0 0.0 - world -
951";
952 let mut fs = build_fs_with_selinux(
953 file_contexts,
954 &[("file_contexts.subs", subs_content)],
955 fs_entries,
956 );
957 let test_repo = TestRepo::<Sha256HashValue>::new();
958
959 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
960
961 assert_eq!(
962 get_label(&fs, "/home").unwrap(),
963 "system_u:object_r:home_t:s0"
964 );
965 assert_eq!(
966 get_label(&fs, "/home/user.txt").unwrap(),
967 "system_u:object_r:home_t:s0"
968 );
969 assert_eq!(
970 get_label(&fs, "/srv/home").unwrap(),
971 "system_u:object_r:home_t:s0"
972 );
973 assert_eq!(
974 get_label(&fs, "/srv/home/data.txt").unwrap(),
975 "system_u:object_r:home_t:s0"
976 );
977 }
978
979 #[test]
981 fn selabel_none_context() {
982 let file_contexts = indoc! {b"
983 /tmp(/.*)? system_u:object_r:tmp_t:s0
984 /tmp/private(/.*)? <<none>>
985 "};
986
987 let fs_entries = "\
988/tmp 0 40755 2 0 0 0 0.0 - - -
989/tmp/scratch.txt 5 100644 1 0 0 0 0.0 - hello -
990/tmp/private 0 40755 2 0 0 0 0.0 - - -
991/tmp/private/secret.txt 6 100644 1 0 0 0 0.0 - secret -
992";
993 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
994 let test_repo = TestRepo::<Sha256HashValue>::new();
995
996 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
997
998 assert_eq!(
999 get_label(&fs, "/tmp").unwrap(),
1000 "system_u:object_r:tmp_t:s0"
1001 );
1002 assert_eq!(
1003 get_label(&fs, "/tmp/scratch.txt").unwrap(),
1004 "system_u:object_r:tmp_t:s0"
1005 );
1006 assert!(get_label(&fs, "/tmp/private").is_none());
1007 assert!(get_label(&fs, "/tmp/private/secret.txt").is_none());
1008 }
1009
1010 #[test]
1012 fn selabel_local_overrides() {
1013 let file_contexts = indoc! {b"
1014 /opt(/.*)? system_u:object_r:opt_t:s0
1015 "};
1016 let local_content = indoc! {b"
1017 /opt/custom(/.*)? system_u:object_r:custom_t:s0
1018 "};
1019
1020 let fs_entries = "\
1021/opt 0 40755 2 0 0 0 0.0 - - -
1022/opt/readme.txt 7 100644 1 0 0 0 0.0 - default -
1023/opt/custom 0 40755 2 0 0 0 0.0 - - -
1024/opt/custom/app 3 100755 1 0 0 0 0.0 - app -
1025";
1026 let mut fs = build_fs_with_selinux(
1027 file_contexts,
1028 &[("file_contexts.local", local_content)],
1029 fs_entries,
1030 );
1031 let test_repo = TestRepo::<Sha256HashValue>::new();
1032
1033 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1034
1035 assert_eq!(
1036 get_label(&fs, "/opt").unwrap(),
1037 "system_u:object_r:opt_t:s0"
1038 );
1039 assert_eq!(
1040 get_label(&fs, "/opt/readme.txt").unwrap(),
1041 "system_u:object_r:opt_t:s0"
1042 );
1043 assert_eq!(
1044 get_label(&fs, "/opt/custom").unwrap(),
1045 "system_u:object_r:custom_t:s0"
1046 );
1047 assert_eq!(
1048 get_label(&fs, "/opt/custom/app").unwrap(),
1049 "system_u:object_r:custom_t:s0"
1050 );
1051 }
1052
1053 #[test]
1055 fn selabel_device_and_fifo_labels() {
1056 let file_contexts = indoc! {b"
1057 /dev(/.*)? system_u:object_r:device_t:s0
1058 /dev(/.*)? -b system_u:object_r:fixed_disk_device_t:s0
1059 /dev(/.*)? -c system_u:object_r:tty_device_t:s0
1060 /dev(/.*)? -p system_u:object_r:fifo_t:s0
1061 "};
1062
1063 let fs_entries = "\
1064/dev 0 40755 2 0 0 0 0.0 - - -
1065/dev/sda 0 60660 1 0 0 2049 0.0 - - -
1066/dev/tty0 0 20666 1 0 0 1024 0.0 - - -
1067/dev/initctl 0 10644 1 0 0 0 0.0 - - -
1068";
1069 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1070 let test_repo = TestRepo::<Sha256HashValue>::new();
1071
1072 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1073
1074 assert_eq!(
1075 get_label(&fs, "/dev").unwrap(),
1076 "system_u:object_r:device_t:s0"
1077 );
1078 assert_eq!(
1079 get_label(&fs, "/dev/sda").unwrap(),
1080 "system_u:object_r:fixed_disk_device_t:s0"
1081 );
1082 assert_eq!(
1083 get_label(&fs, "/dev/tty0").unwrap(),
1084 "system_u:object_r:tty_device_t:s0"
1085 );
1086 assert_eq!(
1087 get_label(&fs, "/dev/initctl").unwrap(),
1088 "system_u:object_r:fifo_t:s0"
1089 );
1090 }
1091
1092 #[test]
1099 fn selabel_breaks_hardlinks_with_different_labels() {
1100 let file_contexts = indoc! {b"
1102 /(/.*)? system_u:object_r:default_t:s0
1103 /usr(/.*)? system_u:object_r:usr_t:s0
1104 /opt(/.*)? system_u:object_r:opt_t:s0
1105 "};
1106
1107 let fs_entries = "\
1112/opt 0 40755 2 0 0 0 0.0 - - -
1113/usr 0 40755 2 0 0 0 0.0 - - -
1114/usr/bin 0 40755 2 0 0 0 0.0 - - -
1115/usr/bin/foo 5 100644 2 0 0 0 0.0 - hello -
1116/opt/foo 0 @120000 - - - - 0.0 /usr/bin/foo - -
1117";
1118 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1119 let test_repo = TestRepo::<Sha256HashValue>::new();
1120
1121 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1122
1123 assert_eq!(
1125 get_label(&fs, "/usr/bin/foo"),
1126 Some("system_u:object_r:usr_t:s0".into()),
1127 "/usr/bin/foo should have usr_t"
1128 );
1129 assert_eq!(
1130 get_label(&fs, "/opt/foo"),
1131 Some("system_u:object_r:opt_t:s0".into()),
1132 "/opt/foo should have opt_t"
1133 );
1134
1135 let usr_bin = fs.as_dir().get_directory_ref("usr/bin".as_ref()).unwrap();
1137 let opt = fs.as_dir().get_directory_ref("opt".as_ref()).unwrap();
1138 let foo_usr_id = match usr_bin.lookup(OsStr::new("foo")).unwrap() {
1139 Inode::Leaf(id, _) => *id,
1140 _ => panic!("expected leaf"),
1141 };
1142 let foo_opt_id = match opt.lookup(OsStr::new("foo")).unwrap() {
1143 Inode::Leaf(id, _) => *id,
1144 _ => panic!("expected leaf"),
1145 };
1146 assert_ne!(
1147 foo_usr_id, foo_opt_id,
1148 "hardlink should have been broken into separate LeafIds"
1149 );
1150 }
1151
1152 #[test]
1163 fn selabel_no_hardlinks_after_labeling_bootable_layout() {
1164 let file_contexts = indoc! {b"
1168 /(/.*)? system_u:object_r:default_t:s0
1169 /usr(/.*)? system_u:object_r:usr_t:s0
1170 /usr/lib(/.*)? system_u:object_r:lib_t:s0
1171 /usr/share(/.*)? system_u:object_r:usr_t:s0
1172 "};
1173
1174 let fs_entries = "\
1181/usr 0 40755 2 0 0 0 0.0 - - -
1182/usr/lib 0 40755 2 0 0 0 0.0 - - -
1183/usr/lib/pkgA 0 40755 2 0 0 0 0.0 - - -
1184/usr/lib/pkgA/COPYING 674 100644 2 0 0 0 0.0 - GPL2 -
1185/usr/lib/pkgB 0 40755 2 0 0 0 0.0 - - -
1186/usr/lib/pkgB/COPYING 674 100644 2 0 0 0 0.0 - GPL2 -
1187/usr/lib/pkgC 0 40755 2 0 0 0 0.0 - - -
1188/usr/lib/pkgC/COPYING 1024 100644 2 0 0 0 0.0 - APACHE2 -
1189/usr/share 0 40755 2 0 0 0 0.0 - - -
1190/usr/share/licenses 0 40755 2 0 0 0 0.0 - - -
1191/usr/share/licenses/pkgA 0 40755 2 0 0 0 0.0 - - -
1192/usr/share/licenses/pkgA/COPYING 0 @120000 - - - - 0.0 /usr/lib/pkgA/COPYING - -
1193/usr/share/licenses/pkgB 0 40755 2 0 0 0 0.0 - - -
1194/usr/share/licenses/pkgB/COPYING 0 @120000 - - - - 0.0 /usr/lib/pkgB/COPYING - -
1195/usr/share/licenses/pkgC 0 40755 2 0 0 0 0.0 - - -
1196/usr/share/licenses/pkgC/COPYING 0 @120000 - - - - 0.0 /usr/lib/pkgC/COPYING - -
1197";
1198 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1199 let test_repo = TestRepo::<Sha256HashValue>::new();
1200
1201 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1202
1203 assert_eq!(
1205 get_label(&fs, "/usr/lib/pkgA/COPYING"),
1206 Some("system_u:object_r:lib_t:s0".into()),
1207 );
1208 assert_eq!(
1209 get_label(&fs, "/usr/share/licenses/pkgA/COPYING"),
1210 Some("system_u:object_r:usr_t:s0".into()),
1211 );
1212 assert_eq!(
1213 get_label(&fs, "/usr/lib/pkgB/COPYING"),
1214 Some("system_u:object_r:lib_t:s0".into()),
1215 );
1216 assert_eq!(
1217 get_label(&fs, "/usr/share/licenses/pkgB/COPYING"),
1218 Some("system_u:object_r:usr_t:s0".into()),
1219 );
1220 assert_eq!(
1221 get_label(&fs, "/usr/lib/pkgC/COPYING"),
1222 Some("system_u:object_r:lib_t:s0".into()),
1223 );
1224 assert_eq!(
1225 get_label(&fs, "/usr/share/licenses/pkgC/COPYING"),
1226 Some("system_u:object_r:usr_t:s0".into()),
1227 );
1228
1229 assert_no_hardlinks(&fs);
1232 }
1233
1234 #[test]
1237 fn selabel_pcre2_positive_lookahead() {
1238 let file_contexts = indoc! {b"
1239 /(/.*)? system_u:object_r:default_t:s0
1240 /opt(/.*)? system_u:object_r:opt_t:s0
1241 /opt/(?=protected).* system_u:object_r:protected_t:s0
1242 "};
1243
1244 let fs_entries = "\
1245/opt 0 40755 2 0 0 0 0.0 - - -
1246/opt/protected_data 5 100644 1 0 0 0 0.0 - hello -
1247/opt/other_file 5 100644 1 0 0 0 0.0 - world -
1248";
1249 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1250 let test_repo = TestRepo::<Sha256HashValue>::new();
1251
1252 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1253
1254 assert_eq!(
1256 get_label(&fs, "/opt/protected_data").unwrap(),
1257 "system_u:object_r:protected_t:s0"
1258 );
1259 assert_eq!(
1261 get_label(&fs, "/opt/other_file").unwrap(),
1262 "system_u:object_r:opt_t:s0"
1263 );
1264 }
1265
1266 #[test]
1269 fn selabel_pcre2_negative_lookahead() {
1270 let file_contexts = indoc! {b"
1272 /(/.*)? system_u:object_r:default_t:s0
1273 /srv(/.*)? system_u:object_r:srv_t:s0
1274 /srv/(?!backup).* system_u:object_r:srv_public_t:s0
1275 "};
1276
1277 let fs_entries = "\
1278/srv 0 40755 2 0 0 0 0.0 - - -
1279/srv/website 5 100644 1 0 0 0 0.0 - hello -
1280/srv/backup_2024 5 100644 1 0 0 0 0.0 - world -
1281";
1282 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1283 let test_repo = TestRepo::<Sha256HashValue>::new();
1284
1285 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1286
1287 assert_eq!(
1289 get_label(&fs, "/srv/website").unwrap(),
1290 "system_u:object_r:srv_public_t:s0"
1291 );
1292 assert_eq!(
1295 get_label(&fs, "/srv/backup_2024").unwrap(),
1296 "system_u:object_r:srv_t:s0"
1297 );
1298 }
1299
1300 #[test]
1303 fn selabel_replaces_stale_labels() {
1304 let file_contexts = indoc! {b"
1305 /(/.*)? system_u:object_r:default_t:s0
1306 /usr(/.*)? system_u:object_r:usr_t:s0
1307 "};
1308
1309 let fs_entries = "\
1310/usr 0 40755 2 0 0 0 0.0 - - - security.selinux=unconfined_u:object_r:container_file_t:s0:c0,c1
1311/usr/lib 0 40755 2 0 0 0 0.0 - - - security.selinux=unconfined_u:object_r:container_file_t:s0:c0,c1
1312/usr/lib/readme.txt 5 100644 1 0 0 0 0.0 - hello - security.selinux=unconfined_u:object_r:container_file_t:s0:c0,c1
1313";
1314 let mut fs = build_fs_with_selinux(file_contexts, &[], fs_entries);
1315 let test_repo = TestRepo::<Sha256HashValue>::new();
1316
1317 assert!(selabel(&mut fs, &test_repo.repo).unwrap());
1318
1319 assert_eq!(
1320 get_label(&fs, "/usr").unwrap(),
1321 "system_u:object_r:usr_t:s0"
1322 );
1323 assert_eq!(
1324 get_label(&fs, "/usr/lib").unwrap(),
1325 "system_u:object_r:usr_t:s0"
1326 );
1327 assert_eq!(
1328 get_label(&fs, "/usr/lib/readme.txt").unwrap(),
1329 "system_u:object_r:usr_t:s0"
1330 );
1331 }
1332
1333 #[test]
1336 fn matcher_strategies_agree() {
1337 let file_contexts = indoc! {b"
1338 /\t\tsystem_u:object_r:root_t:s0
1339 /usr\t\tsystem_u:object_r:usr_t:s0
1340 /usr/bin(/.*)?\t\tsystem_u:object_r:bin_t:s0
1341 /etc(/.*)?\t\tsystem_u:object_r:etc_t:s0
1342 /var(/.*)? -d system_u:object_r:var_dir_t:s0
1343 /var(/.*)? -- system_u:object_r:var_file_t:s0
1344 /tmp(/.*)? <<none>>
1345 "};
1346
1347 let mut regexps = vec![];
1348 let mut contexts = vec![];
1349 process_spec_file(file_contexts.as_slice(), &mut regexps, &mut contexts).unwrap();
1350 regexps.reverse();
1351 contexts.reverse();
1352
1353 let mut matchers: Vec<(MatchStrategy, Matcher)> = [
1354 MatchStrategy::Dfa,
1355 MatchStrategy::Pcre,
1356 MatchStrategy::Hybrid,
1357 ]
1358 .into_iter()
1359 .map(|s| (s, Matcher::build_with_strategy(s, ®exps).unwrap()))
1360 .collect();
1361
1362 let test_cases: &[(&[u8], u8)] = &[
1364 (b"/", b'd'),
1365 (b"/usr", b'd'),
1366 (b"/usr/bin", b'd'),
1367 (b"/usr/bin/hello", b'-'),
1368 (b"/etc", b'd'),
1369 (b"/etc/hostname", b'-'),
1370 (b"/var", b'd'),
1371 (b"/var/log", b'd'),
1372 (b"/var/spool/mail", b'-'),
1373 (b"/tmp", b'd'),
1374 (b"/tmp/scratch", b'-'),
1375 (b"/nonexistent", b'-'),
1376 ];
1377
1378 let expected: Vec<_> = {
1380 let (_, m) = &mut matchers[0];
1381 test_cases
1382 .iter()
1383 .map(|(path, ifmt)| {
1384 let key = [*path, &[*ifmt]].concat();
1385 m.lookup(&key)
1386 })
1387 .collect()
1388 };
1389
1390 for (strategy, m) in &mut matchers[1..] {
1391 for (i, (path, ifmt)) in test_cases.iter().enumerate() {
1392 let key = [*path, &[*ifmt]].concat();
1393 let result = m.lookup(&key);
1394 assert_eq!(
1395 result,
1396 expected[i],
1397 "{strategy:?} disagrees with {:?} on path {:?} (ifmt={ifmt:?}): \
1398 got {result:?}, expected {:?}",
1399 MatchStrategy::Dfa,
1400 String::from_utf8_lossy(path),
1401 expected[i],
1402 );
1403 }
1404 }
1405 }
1406
1407 #[test]
1412 fn hybrid_lookaround_priority() {
1413 let patterns: &[(&str, &str)] = &[
1420 (r"/(.*)?", "system_u:object_r:default_t:s0"),
1422 (r"/usr(.*)?", "system_u:object_r:usr_t:s0"),
1423 (r"/usr/bin/(?!bad).*", "system_u:object_r:bin_ok_t:s0"),
1425 (r"/usr/bin/good", "system_u:object_r:bin_good_t:s0"),
1427 (r"/etc/(?!shadow).*", "system_u:object_r:etc_public_t:s0"),
1429 (r"/etc/hostname", "system_u:object_r:hostname_t:s0"),
1431 ];
1432
1433 let mut regexps: Vec<String> = patterns
1434 .iter()
1435 .map(|(re, _)| format!("^({re}).$"))
1436 .collect();
1437 let mut contexts: Vec<String> = patterns.iter().map(|(_, ctx)| ctx.to_string()).collect();
1438 regexps.reverse();
1439 contexts.reverse();
1440
1441 let mut pcre = Matcher::build_with_strategy(MatchStrategy::Pcre, ®exps).unwrap();
1442 let mut hybrid = Matcher::build_with_strategy(MatchStrategy::Hybrid, ®exps).unwrap();
1443
1444 let test_cases: &[(&[u8], u8, &str)] = &[
1445 (b"/etc/hostname", b'-', "system_u:object_r:hostname_t:s0"),
1447 (b"/etc/passwd", b'-', "system_u:object_r:etc_public_t:s0"),
1449 (b"/etc/shadow", b'-', "system_u:object_r:default_t:s0"),
1452 (b"/usr/bin/good", b'-', "system_u:object_r:bin_good_t:s0"),
1454 (b"/usr/bin/hello", b'-', "system_u:object_r:bin_ok_t:s0"),
1456 (b"/usr/bin/bad", b'-', "system_u:object_r:usr_t:s0"),
1459 ];
1460
1461 for (path, ifmt, expected) in test_cases {
1462 let key = [*path, &[*ifmt]].concat();
1463 let path_str = String::from_utf8_lossy(path);
1464
1465 let pcre_idx = pcre.lookup(&key);
1466 let hybrid_idx = hybrid.lookup(&key);
1467
1468 assert_eq!(
1469 pcre_idx, hybrid_idx,
1470 "hybrid disagrees with pcre2 on {path_str}: \
1471 pcre2={pcre_idx:?} hybrid={hybrid_idx:?}"
1472 );
1473
1474 let label = pcre_idx.map(|i| contexts[i].as_str());
1475 assert_eq!(
1476 label,
1477 Some(*expected),
1478 "{path_str}: expected {expected}, got {label:?}"
1479 );
1480 }
1481 }
1482
1483 mod proptest_matcher {
1484 use super::*;
1485 use proptest::prelude::*;
1486 use proptest::strategy::ValueTree;
1487
1488 fn path_segment() -> impl Strategy<Value = String> {
1490 "[a-z][a-z0-9_]{0,7}"
1491 }
1492
1493 fn dir_path() -> impl Strategy<Value = String> {
1495 proptest::collection::vec(path_segment(), 1..=4)
1496 .prop_map(|segs| format!("/{}", segs.join("/")))
1497 }
1498
1499 fn filename() -> impl Strategy<Value = String> {
1501 "[a-z][a-z0-9_.]{0,11}"
1502 }
1503
1504 #[derive(Debug, Clone)]
1507 enum PatternKind {
1508 Exact(String),
1510 DirWild(String),
1512 Lookahead(String, String),
1514 }
1515
1516 fn pattern_kind() -> impl Strategy<Value = PatternKind> {
1517 prop_oneof![
1518 8 => (dir_path(), filename()).prop_map(|(d, f)| PatternKind::Exact(
1519 format!("{d}/{f}")
1520 )),
1521 4 => dir_path().prop_map(PatternKind::DirWild),
1522 1 => (dir_path(), filename()).prop_map(|(d, f)| PatternKind::Lookahead(d, f)),
1523 ]
1524 }
1525
1526 impl PatternKind {
1527 fn to_regexp(&self) -> String {
1528 match self {
1529 PatternKind::Exact(path) => format!("^({path}).$"),
1530 PatternKind::DirWild(dir) => format!("^({dir}(/.*)?).$"),
1531 PatternKind::Lookahead(dir, excluded) => {
1532 format!("^({dir}/(?!{excluded}).*).$")
1533 }
1534 }
1535 }
1536
1537 fn context(&self, idx: usize) -> String {
1538 format!("system_u:object_r:rule{idx}_t:s0")
1539 }
1540 }
1541
1542 fn test_path(dirs: &[String]) -> impl Strategy<Value = Vec<u8>> + '_ {
1544 let ifmt = prop_oneof![Just(b'-'), Just(b'd'), Just(b'l'),];
1545
1546 let path = prop_oneof![
1547 (proptest::sample::select(dirs.to_vec()), filename())
1549 .prop_map(|(d, f)| format!("{d}/{f}")),
1550 proptest::sample::select(dirs.to_vec()),
1552 dir_path(),
1554 ];
1555
1556 (path, ifmt).prop_map(|(p, i)| [p.as_bytes(), &[i]].concat())
1557 }
1558
1559 proptest! {
1560 #![proptest_config(ProptestConfig::with_cases(20))]
1561
1562 #[test]
1566 fn hybrid_matches_pcre2(
1567 patterns in proptest::collection::vec(pattern_kind(), 20..=200),
1568 path_count in 50..=200usize,
1569 ) {
1570 let mut regexps = Vec::new();
1572 let mut contexts = Vec::new();
1573
1574 regexps.push("^(/(.*)?).$$".to_string());
1576 contexts.push("system_u:object_r:default_t:s0".to_string());
1577
1578 for (i, pat) in patterns.iter().enumerate() {
1579 regexps.push(pat.to_regexp());
1580 contexts.push(pat.context(i));
1581 }
1582
1583 regexps.reverse();
1584 contexts.reverse();
1585
1586 let mut pcre = Matcher::build_with_strategy(
1587 MatchStrategy::Pcre, ®exps,
1588 ).unwrap();
1589 let mut hybrid = Matcher::build(®exps).unwrap();
1590
1591 let dirs: Vec<String> = patterns.iter().filter_map(|p| match p {
1593 PatternKind::Exact(path) => {
1594 path.rsplit_once('/').map(|(d, _)| d.to_string())
1595 }
1596 PatternKind::DirWild(d) | PatternKind::Lookahead(d, _) => {
1597 Some(d.clone())
1598 }
1599 }).collect();
1600
1601 let dirs = if dirs.is_empty() {
1603 vec!["/fallback".to_string()]
1604 } else {
1605 dirs
1606 };
1607
1608 let path_strategy = proptest::collection::vec(
1609 test_path(&dirs), path_count,
1610 );
1611 let mut runner = proptest::test_runner::TestRunner::new(
1612 ProptestConfig::default(),
1613 );
1614 let paths = path_strategy
1615 .new_tree(&mut runner)
1616 .unwrap()
1617 .current();
1618
1619 for key in &paths {
1620 let pcre_result = pcre.lookup(key);
1621 let hybrid_result = hybrid.lookup(key);
1622 prop_assert_eq!(
1623 pcre_result, hybrid_result,
1624 "disagreement on {:?}",
1625 String::from_utf8_lossy(key),
1626 );
1627 }
1628 }
1629 }
1630 }
1631}