extrasafe_multiarch/lib.rs
1#![deny(non_ascii_idents)]
2#![deny(unsafe_code)]
3#![deny(unused_results)]
4#![allow(clippy::unwrap_or_default)] // explicit is better than implicit
5#![allow(clippy::new_without_default)]
6// Denied in CI
7#![warn(missing_docs)]
8#![warn(trivial_casts, trivial_numeric_casts)]
9
10//! extrasafe is a library that makes it easy to improve your program's security by selectively
11//! allowing the syscalls it can perform via the Linux kernel's seccomp facilities.
12//!
13//! See the [`SafetyContext`] struct's documentation and the tests/ and examples/ directories for
14//! more information on how to use it.
15
16
17// Filter is the entire, top-level seccomp filter chain. All SeccompilerRules are or-ed together.
18// Vec<(i64, Vec<SeccompilerRule>)>, Vec is empty if Rule has no filters.
19// Rule is a syscall + multiple argument filters. All argument filters are and-ed together in a
20// single Rule.
21// ArgumentFilter is a single condition on a single argument
22// Comparator is used in an ArgumentFilter to choose the comparison operation
23pub use seccompiler::SeccompFilter as SeccompilerFilter;
24pub use seccompiler::SeccompRule as SeccompilerRule;
25pub use seccompiler::SeccompCondition as SeccompilerArgumentFilter;
26pub use seccompiler::Error as SeccompilerError;
27pub use seccompiler::SeccompCmpOp as SeccompilerComparator;
28
29use seccompiler::SeccompAction;
30
31pub mod syscalls;
32
33pub mod error;
34pub use error::*;
35
36#[macro_use]
37pub mod macros;
38pub use macros::*;
39
40pub mod builtins;
41
42#[cfg(feature = "landlock")]
43mod landlock;
44#[cfg(feature = "landlock")]
45pub use landlock::*;
46
47#[cfg(feature = "landlock")]
48use std::path::PathBuf;
49use std::collections::{BTreeMap, HashMap};
50
51#[derive(Debug, Clone, PartialEq)]
52/// A restriction on the arguments of a syscall. May be combined with other
53/// [`SeccompArgumentFilter`] as part of a single [`SeccompRule`], in which case they are and-ed
54/// together and must all return true for the syscall to be allowed.
55///
56/// Because some syscalls take 32 bit arguments which may or may not be sign-extended to 64 bits
57/// when passed to the linux kernel, there is an option to indicate whether the argument is 32 or
58/// 64 bits. It shouldn't need to be used frequently.
59/// See <https://github.com/rust-vmm/seccompiler/issues/59> for more details
60/// # Examples
61///
62/// ```
63/// # use extrasafe::*;
64/// # const SOCK_STREAM: u64 = libc::SOCK_STREAM as u64;
65/// # const AF_INET: u64 = libc::AF_INET as u64;
66/// // if syscall (specified elsewhere) is `read`, allow reading from stdin
67/// seccomp_arg_filter!(arg0 == 1);
68/// // if syscall is `socket`, allow IPV4 sockets only
69/// seccomp_arg_filter!(arg0 & AF_INET == AF_INET);
70/// // if syscall is `socket`, allow TCP sockets only
71/// seccomp_arg_filter!(arg0 & SOCK_STREAM == SOCK_STREAM);
72/// ```
73///
74/// You should use the [`seccomp_arg_filter!`] macros to create these.
75pub struct SeccompArgumentFilter {
76 /// Which syscall argument to filter. Starts at 0 for the first argument.
77 pub arg_idx: u8,
78 /// What operation should be used to compare to the user-provided value.
79 comparator: SeccompilerComparator,
80 /// The user-provided value to compare the argument against.
81 pub value: u64,
82 /// Whether the argument is 64 bits or 32 bits. See the docstring for why this is needed.
83 pub is_64bit: bool,
84}
85
86impl SeccompArgumentFilter {
87 #[must_use]
88 /// Create a new [`SeccompArgumentFilter`]. You should probably use the [`seccomp_arg_filter!`]
89 /// instead.
90 pub fn new(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter {
91 // TODO: add quirks mode file and check whether syscall's parameter at index `arg_idx` is
92 // 32 or 64 bit (and also I guess if it even has that many arguments)
93 SeccompArgumentFilter::new64(arg_idx, comparator, value)
94 }
95
96 #[must_use]
97 /// Create a new [`SeccompArgumentFilter`] that checks all 64 bits of the provided argument.
98 /// You should probably use the [`seccomp_arg_filter!`] instead.
99 pub fn new64(arg_idx: u8, comparator: SeccompilerComparator, value: u64) -> SeccompArgumentFilter {
100 SeccompArgumentFilter {
101 arg_idx,
102 comparator,
103 value,
104 is_64bit: true,
105 }
106 }
107
108 #[must_use]
109 /// Create a new [`SeccompArgumentFilter`] that checks 32 bits of the provided argument.
110 /// You should probably use the [`seccomp_arg_filter!`] instead. See the struct's documentation
111 /// for why this is needed.
112 pub fn new32(arg_idx: u8, comparator: SeccompilerComparator, value: u32) -> SeccompArgumentFilter {
113 // Note that it doesn't matter if we convert with or without sign extension here since the
114 // point is that we'll only compare the least significant 32 bits anyway.
115 let value = u64::from(value);
116 SeccompArgumentFilter {
117 arg_idx,
118 comparator,
119 value,
120 is_64bit: false,
121 }
122 }
123
124 pub(crate) fn into_seccompiler(self) -> Result<SeccompilerArgumentFilter, ExtraSafeError> {
125 use seccompiler::SeccompCmpArgLen;
126 let arg_len = if self.is_64bit { SeccompCmpArgLen::Qword } else { SeccompCmpArgLen::Dword };
127 Ok(SeccompilerArgumentFilter::new(self.arg_idx, arg_len,
128 self.comparator, self.value)?)
129 }
130}
131
132#[derive(Debug, Clone)]
133#[must_use]
134/// A seccomp rule.
135pub struct SeccompRule {
136 /// The syscall being filtered
137 pub syscall: syscalls::Sysno,
138 /// Filters on the syscall's arguments. The SeccompRule allows the syscall if all argument
139 /// filters evaluate to true.
140 pub argument_filters: Vec<SeccompArgumentFilter>,
141}
142
143impl SeccompRule {
144 /// Constructs a new [`SeccompRule`] that unconditionally allows the given syscall.
145 pub fn new(syscall: syscalls::Sysno) -> SeccompRule {
146 SeccompRule {
147 syscall,
148 argument_filters: Vec::new(),
149 }
150 }
151
152 /// Adds a condition to the [`SeccompRule`] which must evaluate to true in order for the syscall to be
153 /// allowed.
154 pub fn and_condition(mut self, argument_filter: SeccompArgumentFilter) -> SeccompRule {
155 self.argument_filters.push(argument_filter);
156
157 self
158 }
159
160 /// Convert an extrasafe `SeccompRule` to a seccompiler `SeccompilerRule`. Seccompiler's rules
161 /// require that at least one `ArgumentFilter`, so if we have a "simple rule" in extrasafe
162 /// terminology, we return `Option::None`.
163 pub(crate) fn into_seccompiler(self) -> Result<Option<SeccompilerRule>, ExtraSafeError> {
164 if self.argument_filters.is_empty() {
165 return Ok(None);
166 }
167
168 let argument_filters: Vec<SeccompilerArgumentFilter> = self.argument_filters.into_iter()
169 .map(SeccompArgumentFilter::into_seccompiler).collect::<Result::<_, ExtraSafeError>>()?;
170
171 Ok(Some(SeccompilerRule::new(argument_filters)?))
172 }
173}
174
175#[derive(Debug, Clone)]
176/// A [`SeccompRule`] labeled with the name of the [`RuleSet`] it originated from. Internal-only.
177struct LabeledSeccompRule(pub &'static str, pub SeccompRule);
178
179/// A [`RuleSet`] is a collection of [`SeccompRule`] and `LandlockRule` s that enable a
180/// functionality, such as opening files or starting threads.
181pub trait RuleSet {
182 /// A simple rule is a seccomp rule that just allows the syscall without restriction.
183 fn simple_rules(&self) -> Vec<crate::syscalls::Sysno>;
184
185 /// A conditional rule is a seccomp rule that uses a condition to restrict the syscall, e.g. only
186 /// specific flags as parameters.
187 fn conditional_rules(&self) -> HashMap<crate::syscalls::Sysno, Vec<SeccompRule>> {
188 HashMap::new()
189 }
190
191 /// The name of the profile.
192 fn name(&self) -> &'static str;
193
194 #[cfg(feature = "landlock")]
195 /// A landlock rule is a pair of an access control (e.g. read/write access, directory creation
196 /// access) and a directory or path.
197 fn landlock_rules(&self) -> Vec<LandlockRule> {
198 Vec::new()
199 }
200}
201
202impl<T: ?Sized + RuleSet> RuleSet for &T {
203 #[inline]
204 fn simple_rules(&self) -> Vec<syscalls::Sysno> {
205 T::simple_rules(self)
206 }
207
208 #[inline]
209 fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
210 T::conditional_rules(self)
211 }
212
213 #[inline]
214 fn name(&self) -> &'static str {
215 T::name(self)
216 }
217
218 #[cfg(feature = "landlock")]
219 #[inline]
220 fn landlock_rules(&self) -> Vec<LandlockRule> {
221 T::landlock_rules(self)
222 }
223}
224
225impl RuleSet for syscalls::Sysno {
226 fn simple_rules(&self) -> Vec<syscalls::Sysno> {
227 Vec::from([*self])
228 }
229
230 fn conditional_rules(&self) -> HashMap<syscalls::Sysno, Vec<SeccompRule>> {
231 HashMap::new()
232 }
233
234 fn name(&self) -> &'static str {
235 self.name()
236 }
237}
238
239#[must_use]
240/// A struct representing a set of rules to be loaded into a seccomp filter and applied to the
241/// current thread, or all threads in the current process.
242///
243/// Create with [`new()`](Self::new). Add [`RuleSet`]s with [`enable()`](Self::enable), and then use [`apply_to_current_thread()`](Self::apply_to_current_thread)
244/// to apply the filters to the current thread, or [`apply_to_all_threads()`](Self::apply_to_all_threads) to apply the filter to
245/// all threads in the process.
246#[derive(Debug)]
247pub struct SafetyContext {
248 /// A mapping from a syscall to either be a single simple rule or multiple conditional rules, but not both.
249 seccomp_rules: HashMap<syscalls::Sysno, Vec<LabeledSeccompRule>>,
250 #[cfg(feature = "landlock")]
251 /// A mapping from filesystem paths to [`LandlockRule`]s specifying files and directories with
252 /// the operations that can be performed on them.
253 landlock_rules: HashMap<PathBuf, LabeledLandlockRule>,
254 /// The errno returned when a syscall does not match one of the seccomp rules. Defaults to 1.
255 errno: u32,
256 /// Flag to apply seccomp to all threads rather than just the current thread. Defaults to
257 /// false (but the public apply functions always set it directly anyway)
258 all_threads: bool,
259 #[cfg(feature = "landlock")]
260 /// Flag to only use landlock filters and not enable seccomp filters at all. Defaults to false.
261 only_landlock: bool,
262}
263
264impl SafetyContext {
265 /// Create a new [`SafetyContext`]. The seccomp filters will not be loaded until either
266 /// [`apply_to_current_thread`](Self::apply_to_current_thread) or
267 /// [`apply_to_all_threads`](Self::apply_to_all_threads) is called.
268 pub fn new() -> SafetyContext {
269 SafetyContext {
270 seccomp_rules: HashMap::new(),
271 #[cfg(feature = "landlock")]
272 landlock_rules: HashMap::new(),
273 errno: 1,
274 all_threads: false,
275 #[cfg(feature = "landlock")]
276 only_landlock: false,
277 }
278 }
279
280 /// Set the errno to the provided value when a syscall does not match one of the seccomp rules
281 /// in this `SafetyContext`.
282 pub fn with_errno(mut self, errno: u32) -> SafetyContext {
283 self.errno = errno;
284 self
285 }
286
287 /// Gather unconditional and conditional seccomp rules to be provided to the seccomp context.
288 #[allow(clippy::needless_pass_by_value)]
289 fn gather_rules<R: RuleSet>(rules: R) -> Vec<SeccompRule> {
290 let base_syscalls = rules.simple_rules();
291 let mut rules = rules.conditional_rules();
292 for syscall in base_syscalls {
293 let rule = SeccompRule::new(syscall);
294 rules.entry(syscall)
295 .or_insert_with(Vec::new)
296 .push(rule);
297 }
298
299 rules.into_values().flatten()
300 .collect()
301 }
302
303 /// Enable the simple and conditional rules provided by the [`RuleSet`].
304 ///
305 /// # Errors
306 /// Will return [`ExtraSafeError::ConditionalNoEffectError`] if a conditional rule is enabled at
307 /// the same time as a simple rule for a syscall, which would override the conditional rule.
308 pub fn enable<R: RuleSet>(mut self, policy: R) -> Result<SafetyContext, ExtraSafeError> {
309 #[cfg(feature = "landlock")]
310 self.enable_landlock_rules(&policy)?;
311
312 self.enable_seccomp_rules(policy)?;
313
314 Ok(self)
315 }
316
317 #[cfg(feature = "landlock")]
318 fn enable_landlock_rules<R: RuleSet>(&mut self, policy: &R) -> Result<(), ExtraSafeError> {
319 let name = policy.name();
320 let rules = policy.landlock_rules().into_iter()
321 .map(|rule| (rule.path.clone(), LabeledLandlockRule(name, rule)));
322
323 for (path, labeled_rule) in rules {
324 if let Some(existing_rule) = self.landlock_rules.get(&path) {
325 return Err(ExtraSafeError::DuplicatePath(path.clone(), existing_rule.0, labeled_rule.0));
326 }
327 // value here is always none because we checked above that we're not inserting a path
328 // that already exists
329 let _always_none = self.landlock_rules.insert(path, labeled_rule);
330 }
331 Ok(())
332 }
333
334 fn enable_seccomp_rules<R: RuleSet>(&mut self, policy: R) -> Result<(), ExtraSafeError> {
335 let policy_name = policy.name();
336 let new_rules = SafetyContext::gather_rules(policy)
337 .into_iter()
338 .map(|rule| LabeledSeccompRule(policy_name, rule));
339
340 for labeled_new_rule in new_rules {
341 let new_rule = &labeled_new_rule.1;
342 let syscall = &new_rule.syscall;
343
344 if let Some(existing_rules) = self.seccomp_rules.get(syscall) {
345 for labeled_existing_rule in existing_rules {
346 let existing_rule = &labeled_existing_rule.1;
347
348 let new_is_simple = new_rule.argument_filters.is_empty();
349 let existing_is_simple = existing_rule.argument_filters.is_empty();
350
351 // if one rule is conditional and the other is simple, let the user know there
352 // would be a conflict and raise an error.
353 if new_is_simple && !existing_is_simple {
354 return Err(ExtraSafeError::ConditionalNoEffectError(
355 new_rule.syscall,
356 labeled_existing_rule.0,
357 labeled_new_rule.0,
358 ));
359 }
360 else if !new_is_simple && existing_is_simple {
361 return Err(ExtraSafeError::ConditionalNoEffectError(
362 new_rule.syscall,
363 labeled_new_rule.0,
364 labeled_existing_rule.0,
365 ));
366 }
367 // otherwise, they're either both conditional rules or both simple rules,
368 // in which case we continue to check the existing filters, and then add the
369 // rules to our filter as normal if all checks pass.
370 //
371 // In the end, the rules for a syscall must either be all simple (i.e.
372 // duplicates from different rulesets) or all conditional (e.g. multiple rules
373 // allowing read to be called on specific fds)
374 }
375 }
376
377 self.seccomp_rules
378 .entry(*syscall)
379 .or_insert_with(Vec::new)
380 .push(labeled_new_rule);
381 }
382
383 Ok(())
384 }
385
386 #[cfg(feature = "landlock")]
387 /// Do not use seccomp at all, and only enable landlock filters.
388 pub fn landlock_only(mut self) -> SafetyContext {
389 self.only_landlock = true;
390 self
391 }
392
393 // TODO: unused, need to figure out a good way to do this without clasing with the existing
394 // seccomp argument-filtered/not-filtered checks
395 // #[cfg(feature = "landlock")]
396 // /// If there are both landlock and seccomp rules, and the seccomp rules are argument-filtered
397 // /// such that they would conflict with the operation of the landlock rules, return the names of
398 // /// the rulesets that they come from. This only gets called once in `SafetyContext::apply`
399 // /// because it iterates over all seccomp rules.
400 // ///
401 // /// Returns (seccomp_ruleset_name, landlock_ruleset_name)
402 // fn landlock_seccomp_rule_conflict(&self) -> Option<(&'static str, &'static str)> {
403 // if let Some((_path, landlock_rule)) = self.landlock_rules.iter().next() {
404 // for syscall in Self::landlock_restricted_syscalls() {
405 // if let Some(existing_rules) = self.seccomp_rules.get(&syscall) {
406 // // the rules for a given syscall are either all argument-filtered rules or a single
407 // // unfiltered rule so either way this will stop after 1 iteration
408 // if let Some(labeled_rule) = existing_rules.iter().find(|rule| !rule.1.argument_filters.is_empty()) {
409 // return Some((labeled_rule.0, landlock_rule.0)); // return the names of the originating rulesets
410 // }
411 // }
412 // }
413 // }
414 // return None;
415 // }
416
417 // #[cfg(feature = "landlock")]
418 // // TODO: there doesn't seem to be anything in the official documentation about this?
419 // // this definitely isn't exhaustive
420 // fn landlock_restricted_syscalls() -> Vec<syscalls::Sysno> {
421 // let mut syscalls = Vec::new();
422 // syscalls.extend(builtins::systemio::IO_OPEN_SYSCALLS);
423 // syscalls.extend(builtins::systemio::IO_METADATA_SYSCALLS);
424
425 // syscalls
426 // }
427
428 /// Load the [`SafetyContext`]'s rules into a seccomp filter and apply the filter to the current
429 /// thread.
430 ///
431 /// If the landlock feature is enabled but no landlock rules are applied, landlock is not
432 /// enabled, unless `landlock_only()` is called. To enable landlock but allow no file access,
433 /// you can first apply a `landlock_only()` `SafetyContext`, and then apply a separate
434 /// `SafetyContext` with your seccomp rules.
435 ///
436 /// # Errors
437 /// May return an [`ExtraSafeError`].
438 ///
439 /// If no rulesets are enabled, returns an `ExtraSafeError::NoRulesEnabled` error. If you
440 /// really want to enable "nothing", try enabling the `builtins::BasicCapabilities` default
441 /// ruleset manually, or create your own with e.g. just the `exit` syscall.
442 pub fn apply_to_current_thread(mut self) -> Result<(), ExtraSafeError> {
443 self.all_threads = false;
444 self.apply()
445 }
446
447 /// Load the [`SafetyContext`]'s rules into a seccomp filter and apply the filter to all threads in
448 /// this process.
449 ///
450 /// If the landlock feature is enabled but no landlock rules are applied, landlock is not
451 /// enabled, unless `landlock_only()` is called. To enable landlock but allow no file access,
452 /// you can first apply a `landlock_only()` `SafetyContext`, and then apply a separate
453 /// `SafetyContext` with your seccomp rules.
454 ///
455 /// # Errors
456 /// May return an [`ExtraSafeError`].
457 ///
458 /// If no rulesets are enabled, returns an `ExtraSafeError::NoRulesEnabled` error. If you
459 /// really want to enable "nothing", try enabling the `builtins::BasicCapabilities` default
460 /// ruleset manually, or create your own with e.g. just the `exit` syscall.
461 pub fn apply_to_all_threads(mut self) -> Result<(), ExtraSafeError> {
462 #[cfg(feature = "landlock")]
463 if !self.landlock_rules.is_empty() {
464 return Err(ExtraSafeError::LandlockNoThreadSync);
465 }
466 self.all_threads = true;
467 self.apply()
468 }
469
470 /// Actually do the application of the rules. If `self.all_threads` is True, applies the rules to
471 /// all threads via seccomp tsync. If `self.only_landlock` is True, only applies landlock rules.
472 ///
473 /// If the landlock feature is enabled but no landlock rules are applied, landlock is not
474 /// enabled, unless `landlock_only()` is called. To enable landlock but allow no file access,
475 /// you can first apply a `landlock_only()` `SafetyContext`, and then apply a separate
476 /// `SafetyContext` with your seccomp rules.
477 ///
478 /// If no rulesets are enabled, returns an `ExtraSafeError::NoRulesEnabled` error. If you
479 /// really want to enable "nothing", try enabling the `builtins::BasicCapabilities` default
480 /// ruleset manually, or create your own with e.g. just the `exit` syscall.
481 fn apply(mut self) -> Result<(), ExtraSafeError> {
482 #[cfg(feature = "landlock")]
483 if self.seccomp_rules.is_empty() && self.landlock_rules.is_empty() {
484 return Err(ExtraSafeError::NoRulesEnabled);
485 }
486 #[cfg(not(feature = "landlock"))]
487 if self.seccomp_rules.is_empty() {
488 return Err(ExtraSafeError::NoRulesEnabled);
489 }
490
491 self = self.enable(builtins::BasicCapabilities)?;
492
493 #[cfg(feature = "landlock")]
494 if self.only_landlock {
495 return self.apply_landlock_rules();
496 }
497 // If no landlock rules, do not try to apply them since it would prevent all filesystem
498 // access.
499 else if self.landlock_rules.is_empty() {
500 return self.apply_seccomp_rules();
501 }
502
503 #[cfg(feature = "landlock")]
504 self.apply_landlock_rules()?;
505 self.apply_seccomp_rules()
506 }
507
508 fn apply_seccomp_rules(self) -> Result<(), ExtraSafeError> {
509 // Turn our internal HashMap into a BTreeMap for seccompiler, being careful to avoid
510 // https://github.com/rust-vmm/seccompiler/issues/42 i.e. don't use BTreeMap's collect impl
511 // because it will ignore duplicates.
512
513 let mut rules_map: BTreeMap<i64, Vec<SeccompilerRule>> = BTreeMap::new();
514
515 for (syscall, labeled_rules) in self.seccomp_rules {
516 let syscall = syscall.id().into();
517
518 let mut seccompiler_rules = Vec::new();
519 for LabeledSeccompRule(_origin, rule) in labeled_rules {
520 // If there are conditional rules, insert them to the vec
521 if let Some(seccompiler_rule) = rule.into_seccompiler()? {
522 seccompiler_rules.push(seccompiler_rule);
523 }
524 // otherwise, keep the vec empty, which indicates to seccompiler that the syscall
525 // should be allowed without restriction
526 }
527 let result = rules_map.insert(syscall, seccompiler_rules);
528 assert!(result.is_none(), "extrasafe logic error: somehow inserted the same syscall's rules twice");
529 }
530
531 #[cfg(not(target_os = "linux"))]
532 compile_error!("extrasafe is currently only supported on linux");
533
534 let seccompiler_filter = SeccompilerFilter::new(
535 rules_map,
536 SeccompAction::Errno(self.errno),
537 SeccompAction::Allow,
538 std::env::consts::ARCH.try_into().expect("invalid arches are prevented above"),
539 )?;
540
541 let bpf_filter: seccompiler::BpfProgram = seccompiler_filter.try_into()?;
542
543 if self.all_threads {
544 seccompiler::apply_filter_all_threads(&bpf_filter)?;
545 }
546 else {
547 seccompiler::apply_filter(&bpf_filter)?;
548 }
549
550 Ok(())
551 }
552
553 #[cfg(feature = "landlock")]
554 fn apply_landlock_rules(&self) -> Result<(), ExtraSafeError> {
555 let abi = ABI::V2;
556 let mut landlock_ruleset = Ruleset::default()
557 .set_compatibility(CompatLevel::HardRequirement)
558 .handle_access(AccessFs::from_all(abi))?
559 .create()?;
560
561 for LabeledLandlockRule(_policy_name, rule) in self.landlock_rules.values() {
562 // If path does not exist or is not accessible, just ignore it
563 if let Ok(fd) = PathFd::new(rule.path.clone()) {
564 let path_beneath = PathBeneath::new(fd, rule.access_rules);
565 landlock_ruleset = landlock_ruleset.add_rule(path_beneath)?;
566 }
567 }
568 let _status = landlock_ruleset.restrict_self();
569 Ok(())
570 }
571}