1use std::{
9 collections::HashMap,
10 ffi::{OsStr, OsString},
11 fs::File,
12 io::{BufRead, BufReader, Read},
13 os::unix::ffi::OsStrExt,
14 path::{Path, PathBuf},
15};
16
17use anyhow::{bail, ensure, Context, Result};
18use regex_automata::{hybrid::dfa, util::syntax, Anchored, Input};
19
20use composefs::{
21 fsverity::FsVerityHashValue,
22 repository::Repository,
23 tree::{Directory, FileSystem, Inode, Leaf, LeafContent, RegularFile, Stat},
24};
25
26fn process_subs_file(file: impl Read, aliases: &mut HashMap<OsString, OsString>) -> Result<()> {
46 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
48 let line = item?;
49 let mut parts = line.split_whitespace();
50 let alias = match parts.next() {
51 None => continue, Some(comment) if comment.starts_with("#") => continue,
53 Some(alias) => alias,
54 };
55 let Some(original) = parts.next() else {
56 bail!("{line_nr}: missing original path");
57 };
58 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
59
60 aliases.insert(OsString::from(alias), OsString::from(original));
61 }
62 Ok(())
63}
64
65fn process_spec_file(
66 file: impl Read,
67 regexps: &mut Vec<String>,
68 contexts: &mut Vec<String>,
69) -> Result<()> {
70 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
72 let line = item?;
73
74 let mut parts = line.split_whitespace();
75 let regex = match parts.next() {
76 None => continue, Some(comment) if comment.starts_with("#") => continue,
78 Some(regex) => regex,
79 };
80
81 let Some(next) = parts.next() else {
86 bail!("{line_nr}: missing separator after regex");
87 };
88 if let Some(ifmt) = next.strip_prefix("-") {
89 ensure!(
90 ["b", "c", "d", "p", "l", "s", "-"].contains(&ifmt),
91 "{line_nr}: invalid type code -{ifmt}"
92 );
93 let Some(context) = parts.next() else {
94 bail!("{line_nr}: missing context field");
95 };
96 regexps.push(format!("^({regex}){ifmt}$"));
97 contexts.push(context.to_string());
98 } else {
99 let context = next;
100 regexps.push(format!("^({regex}).$"));
101 contexts.push(context.to_string());
102 }
103 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
104 }
105
106 Ok(())
107}
108
109struct Policy {
110 aliases: HashMap<OsString, OsString>,
111 dfa: dfa::DFA,
112 cache: dfa::Cache,
113 contexts: Vec<String>,
114}
115
116pub fn openat<'a, H: FsVerityHashValue>(
118 dir: &'a Directory<H>,
119 filename: impl AsRef<OsStr>,
120 repo: &Repository<H>,
121) -> Result<Option<Box<dyn Read + 'a>>> {
122 match dir.get_file_opt(filename.as_ref())? {
123 Some(file) => match file {
124 RegularFile::Inline(data) => Ok(Some(Box::new(&**data))),
125 RegularFile::External(id, ..) => Ok(Some(Box::new(File::from(repo.open_object(id)?)))),
126 },
127 None => Ok(None),
128 }
129}
130
131impl Policy {
132 pub fn build<H: FsVerityHashValue>(dir: &Directory<H>, repo: &Repository<H>) -> Result<Self> {
133 let mut aliases = HashMap::new();
134 let mut regexps = vec![];
135 let mut contexts = vec![];
136
137 for suffix in ["", ".local", ".homedirs"] {
138 if let Some(file) = openat(dir, format!("file_contexts{suffix}"), repo)? {
139 process_spec_file(file, &mut regexps, &mut contexts)
140 .with_context(|| format!("SELinux spec file file_contexts{suffix}"))?;
141 } else if suffix.is_empty() {
142 bail!("SELinux policy is missing mandatory file_contexts file");
143 }
144 }
145
146 for suffix in [".subs", ".subs_dist"] {
147 if let Some(file) = openat(dir, format!("file_contexts{suffix}"), repo)? {
148 process_subs_file(file, &mut aliases)
149 .with_context(|| format!("SELinux subs file file_contexts{suffix}"))?;
150 }
151 }
152
153 regexps.reverse();
155 contexts.reverse();
156
157 let mut builder = dfa::Builder::new();
158 builder.syntax(
159 syntax::Config::new()
160 .unicode(false)
161 .utf8(false)
162 .line_terminator(0),
163 );
164 builder.configure(
165 dfa::Config::new()
166 .cache_capacity(10_000_000)
167 .skip_cache_capacity_check(true),
168 );
169 let dfa = builder.build_many(®exps)?;
170 let cache = dfa.create_cache();
171
172 Ok(Policy {
173 aliases,
174 dfa,
175 cache,
176 contexts,
177 })
178 }
179
180 pub fn check_aliased(&self, filename: &OsStr) -> Option<&OsStr> {
181 self.aliases.get(filename).map(|x| x.as_os_str())
182 }
183
184 pub fn lookup(&mut self, filename: &OsStr, ifmt: u8) -> Option<&str> {
186 let key = &[filename.as_bytes(), &[ifmt]].concat();
187 let input = Input::new(&key).anchored(Anchored::Yes);
188
189 match self
190 .dfa
191 .try_search_fwd(&mut self.cache, &input)
192 .expect("regex troubles")
193 {
194 Some(halfmatch) => match self.contexts[halfmatch.pattern()].as_str() {
195 "<<none>>" => None,
196 ctx => Some(ctx),
197 },
198 None => None,
199 }
200 }
201}
202
203fn relabel(stat: &Stat, path: &Path, ifmt: u8, policy: &mut Policy) {
204 let security_selinux = OsStr::new("security.selinux"); let mut xattrs = stat.xattrs.borrow_mut();
206
207 if let Some(label) = policy.lookup(path.as_os_str(), ifmt) {
208 xattrs.insert(Box::from(security_selinux), Box::from(label.as_bytes()));
209 } else {
210 xattrs.remove(security_selinux);
211 }
212}
213
214fn relabel_leaf<H: FsVerityHashValue>(leaf: &Leaf<H>, path: &Path, policy: &mut Policy) {
215 let ifmt = match leaf.content {
216 LeafContent::Regular(..) => b'-',
217 LeafContent::Fifo => b'p', LeafContent::Socket => b's',
219 LeafContent::Symlink(..) => b'l',
220 LeafContent::BlockDevice(..) => b'b',
221 LeafContent::CharacterDevice(..) => b'c',
222 };
223 relabel(&leaf.stat, path, ifmt, policy);
224}
225
226fn relabel_inode<H: FsVerityHashValue>(inode: &Inode<H>, path: &mut PathBuf, policy: &mut Policy) {
227 match inode {
228 Inode::Directory(ref dir) => relabel_dir(dir, path, policy),
229 Inode::Leaf(ref leaf) => relabel_leaf(leaf, path, policy),
230 }
231}
232
233fn relabel_dir<H: FsVerityHashValue>(dir: &Directory<H>, path: &mut PathBuf, policy: &mut Policy) {
234 relabel(&dir.stat, path, b'd', policy);
235
236 for (name, inode) in dir.sorted_entries() {
237 path.push(name);
238 match policy.check_aliased(path.as_os_str()) {
239 Some(original) => relabel_inode(inode, &mut PathBuf::from(original), policy),
240 None => relabel_inode(inode, path, policy),
241 }
242 path.pop();
243 }
244}
245
246fn parse_config(file: impl Read) -> Result<Option<String>> {
247 for line in BufReader::new(file).lines() {
248 if let Some((key, value)) = line?.split_once('=') {
249 if key.trim().eq_ignore_ascii_case("SELINUXTYPE") {
251 return Ok(Some(value.trim().to_string()));
252 }
253 }
254 }
255 Ok(None)
256}
257
258pub fn selabel<H: FsVerityHashValue>(fs: &mut FileSystem<H>, repo: &Repository<H>) -> Result<()> {
272 let Some(etc_selinux) = fs.root.get_directory_opt("etc/selinux".as_ref())? else {
274 return Ok(());
275 };
276
277 let Some(etc_selinux_config) = openat(etc_selinux, "config", repo)? else {
278 return Ok(());
279 };
280
281 let Some(policy) = parse_config(etc_selinux_config)? else {
282 return Ok(());
283 };
284
285 let dir = etc_selinux
286 .get_directory(policy.as_ref())?
287 .get_directory("contexts/files".as_ref())?;
288
289 let mut policy = Policy::build(dir, repo)?;
290 let mut path = PathBuf::from("/");
291 relabel_dir(&fs.root, &mut path, &mut policy);
292 Ok(())
293}