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
26pub const XATTR_SECURITY_SELINUX: &str = "security.selinux";
32
33fn process_subs_file(file: impl Read, aliases: &mut HashMap<OsString, OsString>) -> Result<()> {
53 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
55 let line = item?;
56 let mut parts = line.split_whitespace();
57 let alias = match parts.next() {
58 None => continue, Some(comment) if comment.starts_with("#") => continue,
60 Some(alias) => alias,
61 };
62 let Some(original) = parts.next() else {
63 bail!("{line_nr}: missing original path");
64 };
65 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
66
67 aliases.insert(OsString::from(alias), OsString::from(original));
68 }
69 Ok(())
70}
71
72fn process_spec_file(
73 file: impl Read,
74 regexps: &mut Vec<String>,
75 contexts: &mut Vec<String>,
76) -> Result<()> {
77 for (line_nr, item) in BufReader::new(file).lines().enumerate() {
79 let line = item?;
80
81 let mut parts = line.split_whitespace();
82 let regex = match parts.next() {
83 None => continue, Some(comment) if comment.starts_with("#") => continue,
85 Some(regex) => regex,
86 };
87
88 let Some(next) = parts.next() else {
93 bail!("{line_nr}: missing separator after regex");
94 };
95 if let Some(ifmt) = next.strip_prefix("-") {
96 ensure!(
97 ["b", "c", "d", "p", "l", "s", "-"].contains(&ifmt),
98 "{line_nr}: invalid type code -{ifmt}"
99 );
100 let Some(context) = parts.next() else {
101 bail!("{line_nr}: missing context field");
102 };
103 regexps.push(format!("^({regex}){ifmt}$"));
104 contexts.push(context.to_string());
105 } else {
106 let context = next;
107 regexps.push(format!("^({regex}).$"));
108 contexts.push(context.to_string());
109 }
110 ensure!(parts.next().is_none(), "{line_nr}: trailing data");
111 }
112
113 Ok(())
114}
115
116struct Policy {
117 aliases: HashMap<OsString, OsString>,
118 dfa: dfa::DFA,
119 cache: dfa::Cache,
120 contexts: Vec<String>,
121}
122
123pub fn openat<'a, H: FsVerityHashValue>(
125 dir: &'a Directory<H>,
126 filename: impl AsRef<OsStr>,
127 repo: &Repository<H>,
128) -> Result<Option<Box<dyn Read + 'a>>> {
129 match dir.get_file_opt(filename.as_ref())? {
130 Some(file) => match file {
131 RegularFile::Inline(data) => Ok(Some(Box::new(&**data))),
132 RegularFile::External(id, ..) => Ok(Some(Box::new(File::from(repo.open_object(id)?)))),
133 },
134 None => Ok(None),
135 }
136}
137
138impl Policy {
139 pub fn build<H: FsVerityHashValue>(dir: &Directory<H>, repo: &Repository<H>) -> Result<Self> {
140 let mut aliases = HashMap::new();
141 let mut regexps = vec![];
142 let mut contexts = vec![];
143
144 for suffix in ["", ".local", ".homedirs"] {
145 if let Some(file) = openat(dir, format!("file_contexts{suffix}"), repo)? {
146 process_spec_file(file, &mut regexps, &mut contexts)
147 .with_context(|| format!("SELinux spec file file_contexts{suffix}"))?;
148 } else if suffix.is_empty() {
149 bail!("SELinux policy is missing mandatory file_contexts file");
150 }
151 }
152
153 for suffix in [".subs", ".subs_dist"] {
154 if let Some(file) = openat(dir, format!("file_contexts{suffix}"), repo)? {
155 process_subs_file(file, &mut aliases)
156 .with_context(|| format!("SELinux subs file file_contexts{suffix}"))?;
157 }
158 }
159
160 regexps.reverse();
162 contexts.reverse();
163
164 let mut builder = dfa::Builder::new();
165 builder.syntax(
166 syntax::Config::new()
167 .unicode(false)
168 .utf8(false)
169 .line_terminator(0),
170 );
171 builder.configure(
172 dfa::Config::new()
173 .cache_capacity(10_000_000)
174 .skip_cache_capacity_check(true),
175 );
176 let dfa = builder.build_many(®exps)?;
177 let cache = dfa.create_cache();
178
179 Ok(Policy {
180 aliases,
181 dfa,
182 cache,
183 contexts,
184 })
185 }
186
187 pub fn check_aliased(&self, filename: &OsStr) -> Option<&OsStr> {
188 self.aliases.get(filename).map(|x| x.as_os_str())
189 }
190
191 pub fn lookup(&mut self, filename: &OsStr, ifmt: u8) -> Option<&str> {
193 let key = &[filename.as_bytes(), &[ifmt]].concat();
194 let input = Input::new(&key).anchored(Anchored::Yes);
195
196 match self
197 .dfa
198 .try_search_fwd(&mut self.cache, &input)
199 .expect("regex troubles")
200 {
201 Some(halfmatch) => match self.contexts[halfmatch.pattern()].as_str() {
202 "<<none>>" => None,
203 ctx => Some(ctx),
204 },
205 None => None,
206 }
207 }
208}
209
210fn relabel(stat: &Stat, path: &Path, ifmt: u8, policy: &mut Policy) {
211 let mut xattrs = stat.xattrs.borrow_mut();
212 let key = OsStr::new(XATTR_SECURITY_SELINUX);
213
214 if let Some(label) = policy.lookup(path.as_os_str(), ifmt) {
215 xattrs.insert(Box::from(key), Box::from(label.as_bytes()));
216 } else {
217 xattrs.remove(key);
218 }
219}
220
221fn relabel_leaf<H: FsVerityHashValue>(leaf: &Leaf<H>, path: &Path, policy: &mut Policy) {
222 let ifmt = match leaf.content {
223 LeafContent::Regular(..) => b'-',
224 LeafContent::Fifo => b'p', LeafContent::Socket => b's',
226 LeafContent::Symlink(..) => b'l',
227 LeafContent::BlockDevice(..) => b'b',
228 LeafContent::CharacterDevice(..) => b'c',
229 };
230 relabel(&leaf.stat, path, ifmt, policy);
231}
232
233fn relabel_inode<H: FsVerityHashValue>(inode: &Inode<H>, path: &mut PathBuf, policy: &mut Policy) {
234 match inode {
235 Inode::Directory(ref dir) => relabel_dir(dir, path, policy),
236 Inode::Leaf(ref leaf) => relabel_leaf(leaf, path, policy),
237 }
238}
239
240fn relabel_dir<H: FsVerityHashValue>(dir: &Directory<H>, path: &mut PathBuf, policy: &mut Policy) {
241 relabel(&dir.stat, path, b'd', policy);
242
243 for (name, inode) in dir.sorted_entries() {
244 path.push(name);
245 match policy.check_aliased(path.as_os_str()) {
246 Some(original) => relabel_inode(inode, &mut PathBuf::from(original), policy),
247 None => relabel_inode(inode, path, policy),
248 }
249 path.pop();
250 }
251}
252
253fn parse_config(file: impl Read) -> Result<Option<String>> {
254 for line in BufReader::new(file).lines() {
255 if let Some((key, value)) = line?.split_once('=') {
256 if key.trim().eq_ignore_ascii_case("SELINUXTYPE") {
258 return Ok(Some(value.trim().to_string()));
259 }
260 }
261 }
262 Ok(None)
263}
264
265fn strip_selinux_labels<H: FsVerityHashValue>(fs: &FileSystem<H>) {
266 fs.for_each_stat(|stat| {
267 stat.xattrs
268 .borrow_mut()
269 .remove(OsStr::new(XATTR_SECURITY_SELINUX));
270 });
271}
272
273pub fn selabel<H: FsVerityHashValue>(fs: &mut FileSystem<H>, repo: &Repository<H>) -> Result<bool> {
292 let Some(etc_selinux) = fs.root.get_directory_opt("etc/selinux".as_ref())? else {
294 strip_selinux_labels(fs);
295 return Ok(false);
296 };
297
298 let Some(etc_selinux_config) = openat(etc_selinux, "config", repo)? else {
299 strip_selinux_labels(fs);
300 return Ok(false);
301 };
302
303 let Some(policy) = parse_config(etc_selinux_config)? else {
304 strip_selinux_labels(fs);
305 return Ok(false);
306 };
307
308 let dir = etc_selinux
309 .get_directory(policy.as_ref())?
310 .get_directory("contexts/files".as_ref())?;
311
312 let mut policy = Policy::build(dir, repo)?;
313 let mut path = PathBuf::from("/");
314 relabel_dir(&fs.root, &mut path, &mut policy);
315 Ok(true)
316}