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}