diff --git a/.buildkite/custom-tests.json b/.buildkite/custom-tests.json new file mode 100644 index 0000000..afb78f9 --- /dev/null +++ b/.buildkite/custom-tests.json @@ -0,0 +1,27 @@ +{ + "tests": [ + { + "test_name": "build-gnu-json", + "command": "RUSTFLAGS=\"-D warnings\" cargo build --release --features=json", + "platform": [ + "x86_64", + "aarch64" + ] + }, + { + "test_name": "build-musl-json", + "command": "RUSTFLAGS=\"-D warnings\" cargo build --release --features=json --target {target_platform}-unknown-linux-musl", + "platform": [ + "x86_64", + "aarch64" + ] + }, + { + "test_name": "validate-syscall-tables", + "command": "tools/generate_syscall_tables.sh --test", + "platform": [ + "x86_64" + ] + } + ] +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4e1a6b9..23dea7f 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: cargo directory: "/" schedule: - interval: daily + interval: weekly open-pull-requests-limit: 10 - package-ecosystem: gitsubmodule directory: "/" diff --git a/Cargo.toml b/Cargo.toml index 23febb4..63f26c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,10 @@ keywords = ["seccomp", "jail", "sandbox"] license = "Apache-2.0 OR BSD-3-Clause" edition = "2018" +[features] +json = ["serde", "serde_json"] + [dependencies] -libc = ">=0.2.98" +libc = "^0.2.39" +serde = { version = "^1.0.27", features = ["derive"], optional = true} +serde_json = {version = "^1.0.9", optional = true} diff --git a/README.md b/README.md index eed5f03..31d4864 100644 --- a/README.md +++ b/README.md @@ -256,9 +256,12 @@ let filters: BpfMap = seccompiler::compile_from_json( categories to BPF programs. ```rust -pub type BpfMap = HashMap>; +pub type BpfMap = HashMap; ``` +Note that, in order to use the JSON functionality, you need to add the `json` +feature when importing the library. + For **Rust filters**, it’s enough to perform a `try_into()` cast, from a `SeccompFilter` to a `BpfProgram`: @@ -284,8 +287,6 @@ seccompiler::apply_filter(&bpf_prog)?; It’s interesting to note that installing the filter does not take ownership or invalidate the BPF program, thanks to the kernel which performs a `copy_from_user` on the program before installing it. -This is why `BpfMap` entries map to `Arc`, so that they can -be shared across threads of the same category, avoiding copies. ## Seccomp best practices diff --git a/coverage_config_aarch64.json b/coverage_config_aarch64.json index 1c4cd75..a28ea5a 100644 --- a/coverage_config_aarch64.json +++ b/coverage_config_aarch64.json @@ -1,5 +1,5 @@ { "coverage_score": 0, - "exclude_path": "tests/integration_tests.rs", - "crate_features": "" + "exclude_path": "tests/integration_tests.rs,tests/json.rs", + "crate_features": "json" } diff --git a/coverage_config_x86_64.json b/coverage_config_x86_64.json index 8d2d750..78d72a3 100644 --- a/coverage_config_x86_64.json +++ b/coverage_config_x86_64.json @@ -1,5 +1,5 @@ { - "coverage_score": 87.3, - "exclude_path": "tests/integration_tests.rs", - "crate_features": "" + "coverage_score": 93.3, + "exclude_path": "tests/integration_tests.rs,tests/json.rs", + "crate_features": "json" } diff --git a/rust-vmm-ci b/rust-vmm-ci index 8901e77..ae7db2d 160000 --- a/rust-vmm-ci +++ b/rust-vmm-ci @@ -1 +1 @@ -Subproject commit 8901e7752288ae1061e2ee888a104c083a451668 +Subproject commit ae7db2d98a071f52de3d60af9c937204b1f087a4 diff --git a/src/backend/filter.rs b/src/backend/filter.rs index f5eec1b..a5b5f8b 100644 --- a/src/backend/filter.rs +++ b/src/backend/filter.rs @@ -78,7 +78,7 @@ impl SeccompFilter { /// ``` /// /// [`SeccompRule`]: struct.SeccompRule.html - /// [`SeccompAction`]: struct.SeccompAction.html + /// [`SeccompAction`]: enum.SeccompAction.html pub fn new( rules: BTreeMap>, mismatch_action: SeccompAction, diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 17bcb64..c9f6ce8 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -13,6 +13,9 @@ pub use condition::SeccompCondition; pub use filter::SeccompFilter; pub use rule::SeccompRule; +#[cfg(feature = "json")] +use serde::Deserialize; + use core::fmt::Formatter; use std::convert::TryFrom; use std::fmt::Display; @@ -26,7 +29,7 @@ use bpf::{ pub use bpf::{sock_filter, BpfProgram, BpfProgramRef}; /// Backend Result type. -pub type Result = std::result::Result; +type Result = std::result::Result; /// Backend-related errors. #[derive(Debug, PartialEq)] @@ -43,6 +46,8 @@ pub enum Error { InvalidTargetArch(String), } +impl std::error::Error for Error {} + impl Display for Error { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { use self::Error::*; @@ -101,6 +106,11 @@ impl TryFrom<&str> for TargetArch { } /// Comparison to perform when matching a condition. +#[cfg_attr( + feature = "json", + derive(Deserialize), + serde(rename_all = "snake_case") +)] #[derive(Clone, Debug, PartialEq)] pub enum SeccompCmpOp { /// Argument value is equal to the specified value. @@ -120,6 +130,7 @@ pub enum SeccompCmpOp { } /// Seccomp argument value length. +#[cfg_attr(feature = "json", derive(Deserialize), serde(rename_all = "lowercase"))] #[derive(Clone, Debug, PartialEq)] pub enum SeccompCmpArgLen { /// Argument value length is 4 bytes. @@ -129,6 +140,11 @@ pub enum SeccompCmpArgLen { } /// Actions that a seccomp filter can return for a syscall. +#[cfg_attr( + feature = "json", + derive(Deserialize), + serde(rename_all = "snake_case") +)] #[derive(Clone, Debug, PartialEq)] pub enum SeccompAction { /// Allows syscall. @@ -154,7 +170,7 @@ impl From for u32 { /// /// * `action` - The [`SeccompAction`] that the kernel will take. /// - /// [`SeccompAction`]: struct.SeccompAction.html + /// [`SeccompAction`]: enum.SeccompAction.html fn from(action: SeccompAction) -> Self { match action { SeccompAction::Allow => SECCOMP_RET_ALLOW, diff --git a/src/backend/rule.rs b/src/backend/rule.rs index 23f34b2..6bd08a2 100644 --- a/src/backend/rule.rs +++ b/src/backend/rule.rs @@ -71,7 +71,7 @@ impl SeccompRule { ) { // Tries to detect whether prepending the current condition will produce an unjumpable // offset (since BPF conditional jumps are a maximum of 255 instructions, which is - // std::u8::MAX). + // u8::MAX). if offset.checked_add(CONDITION_MAX_LEN + 1).is_none() { // If that is the case, three additional helper jumps are prepended and the offset // is reset to 1. diff --git a/src/frontend/json.rs b/src/frontend/json.rs new file mode 100644 index 0000000..02f0bc2 --- /dev/null +++ b/src/frontend/json.rs @@ -0,0 +1,845 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +//! Module defining the logic for compiling the deserialized json file into +//! the IR. (Intermediate Representation) +//! +//! It also defines some of the objects that a JSON seccomp filter is deserialized into: +//! [`JsonFilter`](struct.JsonFilter.html), +//! [`JsonRule`](struct.JsonRule.html), +//! [`JsonCondition`](struct.JsonCondition.html). +// +//! The rest of objects are deserialized directly into the IR : +//! [`SeccompCondition`](struct.SeccompCondition.html), +//! [`SeccompAction`](enum.SeccompAction.html), +//! [`SeccompCmpOp`](enum.SeccompCmpOp.html), +//! [`SeccompCmpArgLen`](enum.SeccompCmpArgLen.html). + +use std::collections::{BTreeMap, HashMap}; +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::io::Read; +use std::result; + +use crate::backend::{ + Error as BackendError, SeccompAction, SeccompCmpArgLen, SeccompCmpOp, SeccompCondition, + SeccompFilter, SeccompRule, TargetArch, +}; +use crate::syscall_table::SyscallTable; +use serde::de::{self, Error as _, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; + +type Result = result::Result; + +/// Error compiling JSON into IR. +#[derive(Debug)] +pub enum Error { + /// Backend error creating the `SeccompFilter` IR. + Backend(BackendError), + /// Error deserializing JSON. + SerdeJson(serde_json::Error), + /// Invalid syscall name for the given arch. + SyscallName(String, TargetArch), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use self::Error::*; + + match self { + Backend(error) => Some(error), + SerdeJson(error) => Some(error), + _ => None, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Error::*; + + match self { + Backend(error) => write!(f, "{}", error), + SerdeJson(error) => { + write!(f, "Error parsing Json: {}", error) + } + SyscallName(syscall_name, arch) => write!( + f, + "Invalid syscall name: {} for given arch: {:?}.", + syscall_name, arch + ), + } + } +} + +/// Deserializable object that represents the top-level map of Json Filters. +// Need the 'newtype' pattern so that we can implement a custom deserializer. +pub(crate) struct JsonFilterMap(pub HashMap); + +// Implement a custom deserializer, that returns an error for duplicate thread keys. +impl<'de> Deserialize<'de> for JsonFilterMap { + fn deserialize(deserializer: D) -> result::Result + where + D: de::Deserializer<'de>, + { + struct JsonFilterMapVisitor; + + impl<'d> Visitor<'d> for JsonFilterMapVisitor { + type Value = HashMap; + + fn expecting(&self, f: &mut fmt::Formatter<'_>) -> result::Result<(), fmt::Error> { + f.write_str("a map of filters") + } + + fn visit_map(self, mut access: M) -> result::Result + where + M: MapAccess<'d>, + { + let mut values = Self::Value::with_capacity(access.size_hint().unwrap_or(0)); + + while let Some((key, value)) = access.next_entry()? { + if values.insert(key, value).is_some() { + return Err(M::Error::custom("duplicate filter key")); + }; + } + + Ok(values) + } + } + Ok(JsonFilterMap( + deserializer.deserialize_map(JsonFilterMapVisitor)?, + )) + } +} + +/// Dummy placeholder type for a JSON comment. Holds no value. +/// Used for adding comments in the JSON file, since the standard does not allow for native +/// comments. +/// This type declaration is needed so that we can implement a custom deserializer for it. +#[derive(PartialEq, Debug, Clone)] +struct JsonComment; + +// Implement a custom deserializer that only validates that the comment is a string and drops the +// value. +impl<'de> Deserialize<'de> for JsonComment { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + String::deserialize(deserializer)?; + + Ok(JsonComment {}) + } +} + +/// Condition that a syscall must match in order to satisfy a rule. +// Almost equivalent to the [`SeccompCondition`](struct.html.SeccompCondition), with the added +// optional json `comment` property. +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct JsonCondition { + /// Index of the argument that is to be compared. + #[serde(rename = "index")] + arg_index: u8, + /// Length of the argument value that is to be compared. + #[serde(rename = "type")] + arg_len: SeccompCmpArgLen, + /// Comparison operator to perform. + #[serde(rename = "op")] + operator: SeccompCmpOp, + /// The value that will be compared with the argument value of the syscall. + #[serde(rename = "val")] + value: u64, + /// Optional empty value, represents a `comment` property in the JSON file. + comment: Option, +} + +impl TryFrom for SeccompCondition { + type Error = Error; + + fn try_from(json_cond: JsonCondition) -> Result { + SeccompCondition::new( + json_cond.arg_index, + json_cond.arg_len, + json_cond.operator, + json_cond.value, + ) + .map_err(Error::Backend) + } +} + +/// Deserializable object representing a rule associated to a syscall. +#[derive(Debug, Deserialize, PartialEq, Clone)] +#[serde(deny_unknown_fields)] +pub(crate) struct JsonRule { + /// Name of the syscall. + syscall: String, + /// Rule conditions. + #[serde(rename = "args")] + conditions: Option>, + /// Optional empty value, represents a `comment` property in the JSON file. + comment: Option, +} + +/// Deserializable seccomp filter. +#[derive(Deserialize, PartialEq, Debug, Clone)] +#[serde(deny_unknown_fields)] +pub(crate) struct JsonFilter { + /// Default action if no rules match. e.g. `Kill` for an AllowList. + #[serde(alias = "default_action")] + mismatch_action: SeccompAction, + /// Default action if a rule matches. e.g. `Allow` for an AllowList. + #[serde(alias = "filter_action")] + match_action: SeccompAction, + /// The collection of `JsonRule`s. + #[serde(rename = "filter")] + rules: Vec, +} + +/// Object responsible for compiling [`JsonFilter`](struct.JsonFilter.html)s into +/// [`SeccompFilter`](../backend/struct.SeccompFilter.html)s, which represent the IR. +pub(crate) struct JsonCompiler { + /// Target architecture. Can be different from the current `target_arch`. + arch: TargetArch, + /// Target-specific syscall table. + syscall_table: SyscallTable, +} + +impl JsonCompiler { + /// Create a new `Compiler` instance, for the given target architecture. + pub fn new(arch: TargetArch) -> Self { + Self { + arch, + syscall_table: SyscallTable::new(arch), + } + } + + /// Main compilation function. + // This can easily be extracted to a Frontend trait if seccompiler will need to support + // multiple frontend types (YAML, etc.) + pub fn compile(&self, reader: R) -> Result> { + let filters: JsonFilterMap = serde_json::from_reader(reader).map_err(Error::SerdeJson)?; + let filters = filters.0; + let mut bpf_map: HashMap = HashMap::with_capacity(filters.len()); + + for (name, filter) in filters.into_iter() { + bpf_map.insert(name, self.make_seccomp_filter(filter)?); + } + Ok(bpf_map) + } + + /// Transforms the deserialized `JsonFilter` into a `SeccompFilter` (IR language). + fn make_seccomp_filter(&self, filter: JsonFilter) -> Result { + let mut rule_map: BTreeMap> = BTreeMap::new(); + + for json_rule in filter.rules { + let syscall_name = json_rule.syscall; + let syscall_nr = self + .syscall_table + .get_syscall_nr(&syscall_name) + .ok_or_else(|| Error::SyscallName(syscall_name.clone(), self.arch))?; + let rule_accumulator = rule_map.entry(syscall_nr).or_insert_with(Vec::new); + + if let Some(conditions) = json_rule.conditions { + let mut seccomp_conditions = Vec::with_capacity(conditions.len()); + for condition in conditions { + seccomp_conditions.push(condition.try_into()?); + } + rule_accumulator + .push(SeccompRule::new(seccomp_conditions).map_err(Error::Backend)?); + } + } + + SeccompFilter::new( + rule_map, + filter.mismatch_action, + filter.match_action, + self.arch, + ) + .map_err(Error::Backend) + } +} + +#[cfg(test)] +mod tests { + use super::{Error, JsonCompiler, JsonCondition, JsonFilter, JsonRule}; + use crate::backend::{ + Error as BackendError, SeccompAction, SeccompCmpArgLen, SeccompCmpArgLen::*, SeccompCmpOp, + SeccompCmpOp::*, SeccompCondition as Cond, SeccompFilter, SeccompRule, + }; + use std::collections::HashMap; + use std::convert::TryInto; + use std::env::consts::ARCH; + + impl JsonFilter { + pub fn new( + mismatch_action: SeccompAction, + match_action: SeccompAction, + rules: Vec, + ) -> JsonFilter { + JsonFilter { + mismatch_action, + match_action, + rules, + } + } + } + + impl JsonRule { + pub fn new(syscall: String, conditions: Option>) -> JsonRule { + JsonRule { + syscall, + conditions, + comment: None, + } + } + } + + impl JsonCondition { + pub fn new( + arg_index: u8, + arg_len: SeccompCmpArgLen, + operator: SeccompCmpOp, + value: u64, + ) -> Self { + Self { + arg_index, + arg_len, + operator, + value, + comment: None, + } + } + } + + #[test] + // Test the transformation of `JsonFilter` objects into `SeccompFilter` objects. (JSON to IR) + fn test_make_seccomp_filter() { + let compiler = JsonCompiler::new(ARCH.try_into().unwrap()); + + // Test with malformed filters. + let wrong_syscall_name_filter = JsonFilter::new( + SeccompAction::Trap, + SeccompAction::Allow, + vec![JsonRule::new("wrong_syscall".to_string(), None)], + ); + + assert!(matches!( + compiler + .make_seccomp_filter(wrong_syscall_name_filter) + .unwrap_err(), + Error::SyscallName(_, _) + )); + + // Test that `SeccompConditions` validations are triggered and caught by the compilation. + let wrong_arg_index_filter = JsonFilter::new( + SeccompAction::Allow, + SeccompAction::Trap, + vec![JsonRule::new( + "futex".to_string(), + Some(vec![JsonCondition::new(8, Dword, Le, 65)]), + )], + ); + + assert!(matches!( + compiler + .make_seccomp_filter(wrong_arg_index_filter) + .unwrap_err(), + Error::Backend(BackendError::InvalidArgumentNumber) + )); + + // Test that `SeccompRule` validations are triggered and caught by the compilation. + let empty_rule_filter = JsonFilter::new( + SeccompAction::Allow, + SeccompAction::Trap, + vec![JsonRule::new("read".to_string(), Some(vec![]))], + ); + + assert!(matches!( + compiler.make_seccomp_filter(empty_rule_filter).unwrap_err(), + Error::Backend(BackendError::EmptyRule) + )); + + // Test that `SeccompFilter` validations are triggered and caught by the compilation. + let wrong_syscall_name_filter = JsonFilter::new( + SeccompAction::Allow, + SeccompAction::Allow, + vec![JsonRule::new("read".to_string(), None)], + ); + + assert!(matches!( + compiler + .make_seccomp_filter(wrong_syscall_name_filter) + .unwrap_err(), + Error::Backend(BackendError::IdenticalActions) + )); + + // Test a well-formed filter. + let filter = JsonFilter::new( + SeccompAction::Trap, + SeccompAction::Allow, + vec![ + JsonRule::new("read".to_string(), None), + JsonRule::new( + "futex".to_string(), + Some(vec![ + JsonCondition::new(2, Dword, Le, 65), + JsonCondition::new(1, Qword, Ne, 80), + ]), + ), + JsonRule::new( + "futex".to_string(), + Some(vec![ + JsonCondition::new(3, Qword, Gt, 65), + JsonCondition::new(1, Qword, Lt, 80), + ]), + ), + JsonRule::new( + "futex".to_string(), + Some(vec![JsonCondition::new(3, Qword, Ge, 65)]), + ), + JsonRule::new( + "ioctl".to_string(), + Some(vec![JsonCondition::new(3, Dword, MaskedEq(100), 65)]), + ), + ], + ); + + // The expected IR. + let seccomp_filter = SeccompFilter::new( + vec![ + ( + compiler.syscall_table.get_syscall_nr("read").unwrap(), + vec![], + ), + ( + compiler.syscall_table.get_syscall_nr("futex").unwrap(), + vec![ + SeccompRule::new(vec![ + Cond::new(2, Dword, Le, 65).unwrap(), + Cond::new(1, Qword, Ne, 80).unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![ + Cond::new(3, Qword, Gt, 65).unwrap(), + Cond::new(1, Qword, Lt, 80).unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![Cond::new(3, Qword, Ge, 65).unwrap()]).unwrap(), + ], + ), + ( + compiler.syscall_table.get_syscall_nr("ioctl").unwrap(), + vec![ + SeccompRule::new(vec![Cond::new(3, Dword, MaskedEq(100), 65).unwrap()]) + .unwrap(), + ], + ), + ] + .into_iter() + .collect(), + SeccompAction::Trap, + SeccompAction::Allow, + ARCH.try_into().unwrap(), + ) + .unwrap(); + + assert_eq!( + compiler.make_seccomp_filter(filter).unwrap(), + seccomp_filter + ); + } + + #[allow(clippy::useless_asref)] + #[test] + fn test_compile() { + let compiler = JsonCompiler::new(ARCH.try_into().unwrap()); + // test with malformed JSON + { + // empty file + let json_input = ""; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // not json + let json_input = "hjkln"; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // top-level array + let json_input = "[]"; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // thread key must be a string + let json_input = "{1}"; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // empty Filter object + let json_input = r#"{"a": {}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // missing 'filter' field + let json_input = r#"{"a": {"match_action": "allow", "mismatch_action":"log"}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // wrong key 'filters' + let json_input = + r#"{"a": {"match_action": "allow", "mismatch_action":"log", "filters": []}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // wrong action 'logs' + let json_input = + r#"{"a": {"match_action": "allow", "mismatch_action":"logs", "filter": []}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // duplicate action fields using aliases + let json_input = r#"{ + "a": { + "match_action": "allow", + "mismatch_action":"log", + "filter_action": "trap", + "filter": [] + } + }"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // action that expects a value + let json_input = + r#"{"a": {"match_action": "allow", "mismatch_action":"errno", "filter": []}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // overflowing u64 value + let json_input = r#" + { + "thread_2": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [ + { + "syscall": "ioctl", + "args": [ + { + "index": 3, + "type": "qword", + "op": "eq", + "val": 18446744073709551616 + } + ] + } + ] + } + } + "#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // negative integer value + let json_input = r#" + { + "thread_2": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [ + { + "syscall": "ioctl", + "args": [ + { + "index": 3, + "type": "qword", + "op": "eq", + "val": -1846 + } + ] + } + ] + } + } + "#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // float value + let json_input = r#" + { + "thread_2": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [ + { + "syscall": "ioctl", + "args": [ + { + "index": 3, + "type": "qword", + "op": "eq", + "val": 1846.4 + } + ] + } + ] + } + } + "#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // invalid comment + let json_input = r#" + { + "thread_2": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [ + { + "syscall": "ioctl", + "args": [ + { + "index": 3, + "type": "qword", + "op": "eq", + "val": 14, + "comment": 15 + } + ] + } + ] + } + } + "#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + + // duplicate filter keys + let json_input = r#" + { + "thread_1": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [] + }, + "thread_1": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [] + } + } + "#; + assert!(compiler.compile(json_input.as_bytes()).is_err()); + } + + // test with correctly formed JSON + { + // empty JSON file + let json_input = "{}"; + + assert_eq!(compiler.compile(json_input.as_bytes()).unwrap().len(), 0); + + // empty Filter + let json_input = + r#"{"a": {"match_action": "allow", "mismatch_action":"log", "filter": []}}"#; + assert!(compiler.compile(json_input.as_bytes()).is_ok()); + + // action fields using aliases + let json_input = r#"{ + "a": { + "default_action":"log", + "filter_action": "allow", + "filter": [] + } + }"#; + let filter_with_aliases = compiler.compile(json_input.as_bytes()).unwrap(); + let json_input = r#"{ + "a": { + "mismatch_action":"log", + "match_action": "allow", + "filter": [] + } + }"#; + let filter_without_aliases = compiler.compile(json_input.as_bytes()).unwrap(); + assert_eq!( + filter_with_aliases.get("a").unwrap(), + filter_without_aliases.get("a").unwrap() + ); + + // action fields using combined action fields (with and without aliases) + let json_input = r#"{ + "a": { + "default_action":"log", + "filter_action": "allow", + "filter": [] + } + }"#; + let filter_without_aliases = compiler.compile(json_input.as_bytes()).unwrap(); + assert_eq!( + filter_with_aliases.get("a").unwrap(), + filter_without_aliases.get("a").unwrap() + ); + + // correctly formed JSON filter + let json_input = r#" + { + "thread_1": { + "mismatch_action": { + "errno": 12 + }, + "match_action": "allow", + "filter": [ + { + "syscall": "openat" + }, + { + "syscall": "close" + }, + { + "syscall": "read" + }, + { + "syscall": "futex", + "args": [ + { + "index": 2, + "type": "dword", + "op": "le", + "val": 65 + }, + { + "index": 1, + "type": "qword", + "op": "ne", + "val": 80 + } + ] + }, + { + "syscall": "futex", + "args": [ + { + "index": 3, + "type": "qword", + "op": "gt", + "val": 65 + }, + { + "index": 1, + "type": "qword", + "op": "lt", + "val": 80 + } + ] + }, + { + "syscall": "futex", + "args": [ + { + "index": 3, + "type": "qword", + "op": "ge", + "val": 65, + "comment": "dummy comment" + } + ] + }, + { + "syscall": "ioctl", + "args": [ + { + "index": 3, + "type": "dword", + "op": { + "masked_eq": 100 + }, + "val": 65 + } + ] + } + ] + }, + "thread_2": { + "mismatch_action": "trap", + "match_action": "allow", + "filter": [ + { + "syscall": "ioctl", + "comment": "dummy comment", + "args": [ + { + "index": 3, + "type": "dword", + "op": "eq", + "val": 65, + "comment": "dummy comment" + } + ] + } + ] + } + } + "#; + // safe because we know the string is UTF-8 + + let mut filters = HashMap::new(); + filters.insert( + "thread_1".to_string(), + SeccompFilter::new( + vec![ + (libc::SYS_openat, vec![]), + (libc::SYS_close, vec![]), + (libc::SYS_read, vec![]), + ( + libc::SYS_futex, + vec![ + SeccompRule::new(vec![ + Cond::new(2, Dword, Le, 65).unwrap(), + Cond::new(1, Qword, Ne, 80).unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![ + Cond::new(3, Qword, Gt, 65).unwrap(), + Cond::new(1, Qword, Lt, 80).unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![Cond::new(3, Qword, Ge, 65).unwrap()]) + .unwrap(), + ], + ), + ( + libc::SYS_ioctl, + vec![SeccompRule::new(vec![ + Cond::new(3, Dword, MaskedEq(100), 65).unwrap() + ]) + .unwrap()], + ), + ] + .into_iter() + .collect(), + SeccompAction::Errno(12), + SeccompAction::Allow, + ARCH.try_into().unwrap(), + ) + .unwrap(), + ); + + filters.insert( + "thread_2".to_string(), + SeccompFilter::new( + vec![( + libc::SYS_ioctl, + vec![SeccompRule::new(vec![Cond::new(3, Dword, Eq, 65).unwrap()]).unwrap()], + )] + .into_iter() + .collect(), + SeccompAction::Trap, + SeccompAction::Allow, + ARCH.try_into().unwrap(), + ) + .unwrap(), + ); + + // sort the HashMaps by key and transform into vectors, to make comparison possible + let mut v1: Vec<_> = filters.into_iter().collect(); + v1.sort_by(|x, y| x.0.cmp(&y.0)); + + let mut v2: Vec<_> = compiler + .compile(json_input.as_bytes()) + .unwrap() + .into_iter() + .collect(); + v2.sort_by(|x, y| x.0.cmp(&y.0)); + assert_eq!(v1, v2); + } + } +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs new file mode 100644 index 0000000..358c8b5 --- /dev/null +++ b/src/frontend/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +pub mod json; diff --git a/src/lib.rs b/src/lib.rs index 8d111b9..26b3010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,19 @@ //! Writing BPF programs by hand is difficult and error-prone. This crate provides high-level //! wrappers for working with system call filtering. //! +//! The core concept of the library is the filter. It is an abstraction that +//! models a collection of syscall-mapped rules, coupled with on-match and +//! default actions, that logically describes a policy for dispatching actions +//! (e.g. Allow, Trap, Errno) for incoming system calls. +//! +//! Seccompiler provides constructs for defining filters, compiling them into +//! loadable BPF programs and installing them in the kernel. +//! +//! Filters are defined either with a JSON file or using Rust code, with +//! library-defined structures. Both representations are semantically equivalent +//! and model the rules of the filter. Choosing one or the other depends on the use +//! case and preference. +//! //! # Supported platforms //! //! Due to the fact that seccomp is a Linux-specific feature, this crate is @@ -42,10 +55,10 @@ //! system call matches. A system call may also map to an empty rule vector, which //! means that the system call will match, regardless of the actual arguments. //! -//! # Example +//! # Examples //! -//! The following example defines and installs a simple filter, that sends SIGSYS for `accept4`, -//! `fcntl(any, F_SETFD, FD_CLOEXEC, ..)` and `fcntl(any, F_GETFD, ...)`. +//! The following example defines and installs a simple Rust filter, that sends SIGSYS for +//! `accept4`, `fcntl(any, F_SETFD, FD_CLOEXEC, ..)` and `fcntl(any, F_GETFD, ...)`. //! It allows any other syscalls. //! //! ``` @@ -94,17 +107,93 @@ //! seccompiler::apply_filter(&filter).unwrap(); //! ``` //! +//! +//! This second example defines and installs an equivalent JSON filter (uses the `json` feature): +//! +//! ``` +//! # #[cfg(feature = "json")] +//! # { +//! use seccompiler::BpfMap; +//! use std::convert::TryInto; +//! +//! let json_input = r#"{ +//! "main_thread": { +//! "mismatch_action": "allow", +//! "match_action": "trap", +//! "filter": [ +//! { +//! "syscall": "accept4" +//! }, +//! { +//! "syscall": "fcntl", +//! "args": [ +//! { +//! "index": 1, +//! "type": "dword", +//! "op": "eq", +//! "val": 2, +//! "comment": "F_SETFD" +//! }, +//! { +//! "index": 2, +//! "type": "dword", +//! "op": "eq", +//! "val": 1, +//! "comment": "FD_CLOEXEC" +//! } +//! ] +//! }, +//! { +//! "syscall": "fcntl", +//! "args": [ +//! { +//! "index": 1, +//! "type": "dword", +//! "op": "eq", +//! "val": 1, +//! "comment": "F_GETFD" +//! } +//! ] +//! } +//! ] +//! } +//! }"#; +//! +//! let filter_map: BpfMap = seccompiler::compile_from_json( +//! json_input.as_bytes(), +//! std::env::consts::ARCH.try_into().unwrap(), +//! ).unwrap(); +//! let filter = filter_map.get("main_thread").unwrap(); +//! +//! seccompiler::apply_filter(&filter).unwrap(); +//! +//! # } +//! ``` +//! //! [`SeccompFilter`]: struct.SeccompFilter.html //! [`SeccompCondition`]: struct.SeccompCondition.html //! [`SeccompRule`]: struct.SeccompRule.html -//! [`SeccompAction`]: struct.SeccompAction.html +//! [`SeccompAction`]: enum.SeccompAction.html //! mod backend; +#[cfg(feature = "json")] +mod frontend; +#[cfg(feature = "json")] +mod syscall_table; +#[cfg(feature = "json")] +use std::convert::TryInto; +#[cfg(feature = "json")] +use std::io::Read; + +use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::io; +#[cfg(feature = "json")] +use frontend::json::{Error as JsonFrontendError, JsonCompiler}; + // Re-export the IR public types. pub use backend::{ sock_filter, BpfProgram, BpfProgramRef, Error as BackendError, SeccompAction, SeccompCmpArgLen, @@ -122,6 +211,9 @@ struct sock_fprog { /// Library Result type. pub type Result = std::result::Result; +///`BpfMap` is another type exposed by the library, which maps thread categories to BPF programs. +pub type BpfMap = HashMap; + /// Library errors. #[derive(Debug)] pub enum Error { @@ -131,6 +223,23 @@ pub enum Error { EmptyFilter, /// System error related to calling `prctl`. Prctl(io::Error), + /// Json Frontend Error. + #[cfg(feature = "json")] + JsonFrontend(JsonFrontendError), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use self::Error::*; + + match self { + Backend(error) => Some(error), + Prctl(error) => Some(error), + #[cfg(feature = "json")] + JsonFrontend(error) => Some(error), + _ => None, + } + } } impl Display for Error { @@ -147,17 +256,21 @@ impl Display for Error { Prctl(errno) => { write!(f, "Error calling `prctl`: {}", errno) } + #[cfg(feature = "json")] + JsonFrontend(error) => { + write!(f, "Json Frontend error: {}", error) + } } } } /// Apply a BPF filter to the calling thread. /// -/// # Arguments +/// # Arguments /// /// * `bpf_filter` - A reference to the [`BpfProgram`] to be installed. /// -/// [`BpfProgram`]: struct.BpfProgram.html +/// [`BpfProgram`]: type.BpfProgram.html pub fn apply_filter(bpf_filter: BpfProgramRef) -> Result<()> { // If the program is empty, don't install the filter. if bpf_filter.is_empty() { @@ -191,3 +304,28 @@ pub fn apply_filter(bpf_filter: BpfProgramRef) -> Result<()> { Ok(()) } + +/// Compile [`BpfProgram`]s from JSON. +/// +/// # Arguments +/// +/// * `reader` - [`std::io::Read`] object containing the JSON data conforming to the +/// [JSON file format](https://github.com/rust-vmm/seccompiler/blob/master/docs/json_format.md). +/// * `arch` - target architecture of the filter. +/// +/// [`BpfProgram`]: type.BpfProgram.html +#[cfg(feature = "json")] +pub fn compile_from_json(reader: R, arch: TargetArch) -> Result { + // Run the frontend. + let seccomp_filters: HashMap = JsonCompiler::new(arch) + .compile(reader) + .map_err(Error::JsonFrontend)?; + + // Run the backend. + let mut bpf_data: BpfMap = BpfMap::with_capacity(seccomp_filters.len()); + for (name, seccomp_filter) in seccomp_filters { + bpf_data.insert(name, seccomp_filter.try_into().map_err(Error::Backend)?); + } + + Ok(bpf_data) +} diff --git a/src/syscall_table/aarch64.rs b/src/syscall_table/aarch64.rs new file mode 100644 index 0000000..88cbf13 --- /dev/null +++ b/src/syscall_table/aarch64.rs @@ -0,0 +1,312 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +// This file is auto-generated by `tools/generate_syscall_tables`. +// Do NOT manually edit! +// Generated on: Tue Sep 14 11:46:41 UTC 2021 +// Kernel version: 5.10 + +use std::collections::HashMap; + +pub(crate) fn make_syscall_table() -> HashMap<&'static str, i64> { + vec![ + ("accept", 202), + ("accept4", 242), + ("acct", 89), + ("add_key", 217), + ("adjtimex", 171), + ("bind", 200), + ("bpf", 280), + ("brk", 214), + ("capget", 90), + ("capset", 91), + ("chdir", 49), + ("chroot", 51), + ("clock_adjtime", 266), + ("clock_getres", 114), + ("clock_gettime", 113), + ("clock_nanosleep", 115), + ("clock_settime", 112), + ("clone", 220), + ("clone3", 435), + ("close", 57), + ("close_range", 436), + ("connect", 203), + ("copy_file_range", 285), + ("delete_module", 106), + ("dup", 23), + ("dup3", 24), + ("epoll_create1", 20), + ("epoll_ctl", 21), + ("epoll_pwait", 22), + ("eventfd2", 19), + ("execve", 221), + ("execveat", 281), + ("exit", 93), + ("exit_group", 94), + ("faccessat", 48), + ("faccessat2", 439), + ("fadvise64", 223), + ("fallocate", 47), + ("fanotify_init", 262), + ("fanotify_mark", 263), + ("fchdir", 50), + ("fchmod", 52), + ("fchmodat", 53), + ("fchown", 55), + ("fchownat", 54), + ("fcntl", 25), + ("fdatasync", 83), + ("fgetxattr", 10), + ("finit_module", 273), + ("flistxattr", 13), + ("flock", 32), + ("fremovexattr", 16), + ("fsconfig", 431), + ("fsetxattr", 7), + ("fsmount", 432), + ("fsopen", 430), + ("fspick", 433), + ("fstat", 80), + ("fstatfs", 44), + ("fsync", 82), + ("ftruncate", 46), + ("futex", 98), + ("getcpu", 168), + ("getcwd", 17), + ("getdents64", 61), + ("getegid", 177), + ("geteuid", 175), + ("getgid", 176), + ("getgroups", 158), + ("getitimer", 102), + ("get_mempolicy", 236), + ("getpeername", 205), + ("getpgid", 155), + ("getpid", 172), + ("getppid", 173), + ("getpriority", 141), + ("getrandom", 278), + ("getresgid", 150), + ("getresuid", 148), + ("getrlimit", 163), + ("get_robust_list", 100), + ("getrusage", 165), + ("getsid", 156), + ("getsockname", 204), + ("getsockopt", 209), + ("gettid", 178), + ("gettimeofday", 169), + ("getuid", 174), + ("getxattr", 8), + ("init_module", 105), + ("inotify_add_watch", 27), + ("inotify_init1", 26), + ("inotify_rm_watch", 28), + ("io_cancel", 3), + ("ioctl", 29), + ("io_destroy", 1), + ("io_getevents", 4), + ("io_pgetevents", 292), + ("ioprio_get", 31), + ("ioprio_set", 30), + ("io_setup", 0), + ("io_submit", 2), + ("io_uring_enter", 426), + ("io_uring_register", 427), + ("io_uring_setup", 425), + ("kcmp", 272), + ("kexec_file_load", 294), + ("kexec_load", 104), + ("keyctl", 219), + ("kill", 129), + ("lgetxattr", 9), + ("linkat", 37), + ("listen", 201), + ("listxattr", 11), + ("llistxattr", 12), + ("lookup_dcookie", 18), + ("lremovexattr", 15), + ("lseek", 62), + ("lsetxattr", 6), + ("madvise", 233), + ("mbind", 235), + ("membarrier", 283), + ("memfd_create", 279), + ("migrate_pages", 238), + ("mincore", 232), + ("mkdirat", 34), + ("mknodat", 33), + ("mlock", 228), + ("mlock2", 284), + ("mlockall", 230), + ("mmap", 222), + ("mount", 40), + ("move_mount", 429), + ("move_pages", 239), + ("mprotect", 226), + ("mq_getsetattr", 185), + ("mq_notify", 184), + ("mq_open", 180), + ("mq_timedreceive", 183), + ("mq_timedsend", 182), + ("mq_unlink", 181), + ("mremap", 216), + ("msgctl", 187), + ("msgget", 186), + ("msgrcv", 188), + ("msgsnd", 189), + ("msync", 227), + ("munlock", 229), + ("munlockall", 231), + ("munmap", 215), + ("name_to_handle_at", 264), + ("nanosleep", 101), + ("newfstatat", 79), + ("nfsservctl", 42), + ("openat", 56), + ("openat2", 437), + ("open_by_handle_at", 265), + ("open_tree", 428), + ("perf_event_open", 241), + ("personality", 92), + ("pidfd_getfd", 438), + ("pidfd_open", 434), + ("pidfd_send_signal", 424), + ("pipe2", 59), + ("pivot_root", 41), + ("pkey_alloc", 289), + ("pkey_free", 290), + ("pkey_mprotect", 288), + ("ppoll", 73), + ("prctl", 167), + ("pread64", 67), + ("preadv", 69), + ("preadv2", 286), + ("prlimit64", 261), + ("process_madvise", 440), + ("process_vm_readv", 270), + ("process_vm_writev", 271), + ("pselect6", 72), + ("ptrace", 117), + ("pwrite64", 68), + ("pwritev", 70), + ("pwritev2", 287), + ("quotactl", 60), + ("read", 63), + ("readahead", 213), + ("readlinkat", 78), + ("readv", 65), + ("reboot", 142), + ("recvfrom", 207), + ("recvmmsg", 243), + ("recvmsg", 212), + ("remap_file_pages", 234), + ("removexattr", 14), + ("renameat", 38), + ("renameat2", 276), + ("request_key", 218), + ("restart_syscall", 128), + ("rseq", 293), + ("rt_sigaction", 134), + ("rt_sigpending", 136), + ("rt_sigprocmask", 135), + ("rt_sigqueueinfo", 138), + ("rt_sigreturn", 139), + ("rt_sigsuspend", 133), + ("rt_sigtimedwait", 137), + ("rt_tgsigqueueinfo", 240), + ("sched_getaffinity", 123), + ("sched_getattr", 275), + ("sched_getparam", 121), + ("sched_get_priority_max", 125), + ("sched_get_priority_min", 126), + ("sched_getscheduler", 120), + ("sched_rr_get_interval", 127), + ("sched_setaffinity", 122), + ("sched_setattr", 274), + ("sched_setparam", 118), + ("sched_setscheduler", 119), + ("sched_yield", 124), + ("seccomp", 277), + ("semctl", 191), + ("semget", 190), + ("semop", 193), + ("semtimedop", 192), + ("sendfile", 71), + ("sendmmsg", 269), + ("sendmsg", 211), + ("sendto", 206), + ("setdomainname", 162), + ("setfsgid", 152), + ("setfsuid", 151), + ("setgid", 144), + ("setgroups", 159), + ("sethostname", 161), + ("setitimer", 103), + ("set_mempolicy", 237), + ("setns", 268), + ("setpgid", 154), + ("setpriority", 140), + ("setregid", 143), + ("setresgid", 149), + ("setresuid", 147), + ("setreuid", 145), + ("setrlimit", 164), + ("set_robust_list", 99), + ("setsid", 157), + ("setsockopt", 208), + ("set_tid_address", 96), + ("settimeofday", 170), + ("setuid", 146), + ("setxattr", 5), + ("shmat", 196), + ("shmctl", 195), + ("shmdt", 197), + ("shmget", 194), + ("shutdown", 210), + ("sigaltstack", 132), + ("signalfd4", 74), + ("socket", 198), + ("socketpair", 199), + ("splice", 76), + ("statfs", 43), + ("statx", 291), + ("swapoff", 225), + ("swapon", 224), + ("symlinkat", 36), + ("sync", 81), + ("sync_file_range", 84), + ("syncfs", 267), + ("sysinfo", 179), + ("syslog", 116), + ("tee", 77), + ("tgkill", 131), + ("timer_create", 107), + ("timer_delete", 111), + ("timerfd_create", 85), + ("timerfd_gettime", 87), + ("timerfd_settime", 86), + ("timer_getoverrun", 109), + ("timer_gettime", 108), + ("timer_settime", 110), + ("times", 153), + ("tkill", 130), + ("truncate", 45), + ("umask", 166), + ("umount2", 39), + ("uname", 160), + ("unlinkat", 35), + ("unshare", 97), + ("userfaultfd", 282), + ("utimensat", 88), + ("vhangup", 58), + ("vmsplice", 75), + ("wait4", 260), + ("waitid", 95), + ("write", 64), + ("writev", 66), + ] + .into_iter() + .collect() +} diff --git a/src/syscall_table/mod.rs b/src/syscall_table/mod.rs new file mode 100644 index 0000000..3d49f03 --- /dev/null +++ b/src/syscall_table/mod.rs @@ -0,0 +1,50 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +mod aarch64; +mod x86_64; + +use crate::backend::TargetArch; +use std::collections::HashMap; + +/// Creates and owns a mapping from the arch-specific syscall name to the right number. +#[derive(Debug)] +pub(crate) struct SyscallTable { + map: HashMap<&'static str, i64>, +} + +impl SyscallTable { + pub fn new(arch: TargetArch) -> Self { + Self { + map: match arch { + TargetArch::aarch64 => aarch64::make_syscall_table(), + TargetArch::x86_64 => x86_64::make_syscall_table(), + }, + } + } + + /// Returns the arch-specific syscall number based on the given name. + pub fn get_syscall_nr(&self, sys_name: &str) -> Option { + self.map.get(sys_name).copied() + } +} + +#[cfg(test)] +mod tests { + use super::SyscallTable; + use crate::backend::TargetArch; + + #[test] + fn test_get_syscall_nr() { + // get number for a valid syscall + let instance_x86_64 = SyscallTable::new(TargetArch::x86_64); + let instance_aarch64 = SyscallTable::new(TargetArch::aarch64); + + assert_eq!(instance_x86_64.get_syscall_nr("close").unwrap(), 3); + assert_eq!(instance_aarch64.get_syscall_nr("close").unwrap(), 57); + + // invalid syscall name + assert!(instance_x86_64.get_syscall_nr("nosyscall").is_none()); + assert!(instance_aarch64.get_syscall_nr("nosyscall").is_none()); + } +} diff --git a/src/syscall_table/x86_64.rs b/src/syscall_table/x86_64.rs new file mode 100644 index 0000000..c0ea59e --- /dev/null +++ b/src/syscall_table/x86_64.rs @@ -0,0 +1,368 @@ +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +// This file is auto-generated by `tools/generate_syscall_tables`. +// Do NOT manually edit! +// Generated on: Tue Sep 14 11:46:40 UTC 2021 +// Kernel version: 5.10 + +use std::collections::HashMap; + +pub(crate) fn make_syscall_table() -> HashMap<&'static str, i64> { + vec![ + ("accept", 43), + ("accept4", 288), + ("access", 21), + ("acct", 163), + ("add_key", 248), + ("adjtimex", 159), + ("afs_syscall", 183), + ("alarm", 37), + ("arch_prctl", 158), + ("bind", 49), + ("bpf", 321), + ("brk", 12), + ("capget", 125), + ("capset", 126), + ("chdir", 80), + ("chmod", 90), + ("chown", 92), + ("chroot", 161), + ("clock_adjtime", 305), + ("clock_getres", 229), + ("clock_gettime", 228), + ("clock_nanosleep", 230), + ("clock_settime", 227), + ("clone", 56), + ("clone3", 435), + ("close", 3), + ("close_range", 436), + ("connect", 42), + ("copy_file_range", 326), + ("creat", 85), + ("create_module", 174), + ("delete_module", 176), + ("dup", 32), + ("dup2", 33), + ("dup3", 292), + ("epoll_create", 213), + ("epoll_create1", 291), + ("epoll_ctl", 233), + ("epoll_ctl_old", 214), + ("epoll_pwait", 281), + ("epoll_wait", 232), + ("epoll_wait_old", 215), + ("eventfd", 284), + ("eventfd2", 290), + ("execve", 59), + ("execveat", 322), + ("exit", 60), + ("exit_group", 231), + ("faccessat", 269), + ("faccessat2", 439), + ("fadvise64", 221), + ("fallocate", 285), + ("fanotify_init", 300), + ("fanotify_mark", 301), + ("fchdir", 81), + ("fchmod", 91), + ("fchmodat", 268), + ("fchown", 93), + ("fchownat", 260), + ("fcntl", 72), + ("fdatasync", 75), + ("fgetxattr", 193), + ("finit_module", 313), + ("flistxattr", 196), + ("flock", 73), + ("fork", 57), + ("fremovexattr", 199), + ("fsconfig", 431), + ("fsetxattr", 190), + ("fsmount", 432), + ("fsopen", 430), + ("fspick", 433), + ("fstat", 5), + ("fstatfs", 138), + ("fsync", 74), + ("ftruncate", 77), + ("futex", 202), + ("futimesat", 261), + ("getcpu", 309), + ("getcwd", 79), + ("getdents", 78), + ("getdents64", 217), + ("getegid", 108), + ("geteuid", 107), + ("getgid", 104), + ("getgroups", 115), + ("getitimer", 36), + ("get_kernel_syms", 177), + ("get_mempolicy", 239), + ("getpeername", 52), + ("getpgid", 121), + ("getpgrp", 111), + ("getpid", 39), + ("getpmsg", 181), + ("getppid", 110), + ("getpriority", 140), + ("getrandom", 318), + ("getresgid", 120), + ("getresuid", 118), + ("getrlimit", 97), + ("get_robust_list", 274), + ("getrusage", 98), + ("getsid", 124), + ("getsockname", 51), + ("getsockopt", 55), + ("get_thread_area", 211), + ("gettid", 186), + ("gettimeofday", 96), + ("getuid", 102), + ("getxattr", 191), + ("init_module", 175), + ("inotify_add_watch", 254), + ("inotify_init", 253), + ("inotify_init1", 294), + ("inotify_rm_watch", 255), + ("io_cancel", 210), + ("ioctl", 16), + ("io_destroy", 207), + ("io_getevents", 208), + ("ioperm", 173), + ("io_pgetevents", 333), + ("iopl", 172), + ("ioprio_get", 252), + ("ioprio_set", 251), + ("io_setup", 206), + ("io_submit", 209), + ("io_uring_enter", 426), + ("io_uring_register", 427), + ("io_uring_setup", 425), + ("kcmp", 312), + ("kexec_file_load", 320), + ("kexec_load", 246), + ("keyctl", 250), + ("kill", 62), + ("lchown", 94), + ("lgetxattr", 192), + ("link", 86), + ("linkat", 265), + ("listen", 50), + ("listxattr", 194), + ("llistxattr", 195), + ("lookup_dcookie", 212), + ("lremovexattr", 198), + ("lseek", 8), + ("lsetxattr", 189), + ("lstat", 6), + ("madvise", 28), + ("mbind", 237), + ("membarrier", 324), + ("memfd_create", 319), + ("migrate_pages", 256), + ("mincore", 27), + ("mkdir", 83), + ("mkdirat", 258), + ("mknod", 133), + ("mknodat", 259), + ("mlock", 149), + ("mlock2", 325), + ("mlockall", 151), + ("mmap", 9), + ("modify_ldt", 154), + ("mount", 165), + ("move_mount", 429), + ("move_pages", 279), + ("mprotect", 10), + ("mq_getsetattr", 245), + ("mq_notify", 244), + ("mq_open", 240), + ("mq_timedreceive", 243), + ("mq_timedsend", 242), + ("mq_unlink", 241), + ("mremap", 25), + ("msgctl", 71), + ("msgget", 68), + ("msgrcv", 70), + ("msgsnd", 69), + ("msync", 26), + ("munlock", 150), + ("munlockall", 152), + ("munmap", 11), + ("name_to_handle_at", 303), + ("nanosleep", 35), + ("newfstatat", 262), + ("nfsservctl", 180), + ("open", 2), + ("openat", 257), + ("openat2", 437), + ("open_by_handle_at", 304), + ("open_tree", 428), + ("pause", 34), + ("perf_event_open", 298), + ("personality", 135), + ("pidfd_getfd", 438), + ("pidfd_open", 434), + ("pidfd_send_signal", 424), + ("pipe", 22), + ("pipe2", 293), + ("pivot_root", 155), + ("pkey_alloc", 330), + ("pkey_free", 331), + ("pkey_mprotect", 329), + ("poll", 7), + ("ppoll", 271), + ("prctl", 157), + ("pread64", 17), + ("preadv", 295), + ("preadv2", 327), + ("prlimit64", 302), + ("process_madvise", 440), + ("process_vm_readv", 310), + ("process_vm_writev", 311), + ("pselect6", 270), + ("ptrace", 101), + ("putpmsg", 182), + ("pwrite64", 18), + ("pwritev", 296), + ("pwritev2", 328), + ("query_module", 178), + ("quotactl", 179), + ("read", 0), + ("readahead", 187), + ("readlink", 89), + ("readlinkat", 267), + ("readv", 19), + ("reboot", 169), + ("recvfrom", 45), + ("recvmmsg", 299), + ("recvmsg", 47), + ("remap_file_pages", 216), + ("removexattr", 197), + ("rename", 82), + ("renameat", 264), + ("renameat2", 316), + ("request_key", 249), + ("restart_syscall", 219), + ("rmdir", 84), + ("rseq", 334), + ("rt_sigaction", 13), + ("rt_sigpending", 127), + ("rt_sigprocmask", 14), + ("rt_sigqueueinfo", 129), + ("rt_sigreturn", 15), + ("rt_sigsuspend", 130), + ("rt_sigtimedwait", 128), + ("rt_tgsigqueueinfo", 297), + ("sched_getaffinity", 204), + ("sched_getattr", 315), + ("sched_getparam", 143), + ("sched_get_priority_max", 146), + ("sched_get_priority_min", 147), + ("sched_getscheduler", 145), + ("sched_rr_get_interval", 148), + ("sched_setaffinity", 203), + ("sched_setattr", 314), + ("sched_setparam", 142), + ("sched_setscheduler", 144), + ("sched_yield", 24), + ("seccomp", 317), + ("security", 185), + ("select", 23), + ("semctl", 66), + ("semget", 64), + ("semop", 65), + ("semtimedop", 220), + ("sendfile", 40), + ("sendmmsg", 307), + ("sendmsg", 46), + ("sendto", 44), + ("setdomainname", 171), + ("setfsgid", 123), + ("setfsuid", 122), + ("setgid", 106), + ("setgroups", 116), + ("sethostname", 170), + ("setitimer", 38), + ("set_mempolicy", 238), + ("setns", 308), + ("setpgid", 109), + ("setpriority", 141), + ("setregid", 114), + ("setresgid", 119), + ("setresuid", 117), + ("setreuid", 113), + ("setrlimit", 160), + ("set_robust_list", 273), + ("setsid", 112), + ("setsockopt", 54), + ("set_thread_area", 205), + ("set_tid_address", 218), + ("settimeofday", 164), + ("setuid", 105), + ("setxattr", 188), + ("shmat", 30), + ("shmctl", 31), + ("shmdt", 67), + ("shmget", 29), + ("shutdown", 48), + ("sigaltstack", 131), + ("signalfd", 282), + ("signalfd4", 289), + ("socket", 41), + ("socketpair", 53), + ("splice", 275), + ("stat", 4), + ("statfs", 137), + ("statx", 332), + ("swapoff", 168), + ("swapon", 167), + ("symlink", 88), + ("symlinkat", 266), + ("sync", 162), + ("sync_file_range", 277), + ("syncfs", 306), + ("_sysctl", 156), + ("sysfs", 139), + ("sysinfo", 99), + ("syslog", 103), + ("tee", 276), + ("tgkill", 234), + ("time", 201), + ("timer_create", 222), + ("timer_delete", 226), + ("timerfd_create", 283), + ("timerfd_gettime", 287), + ("timerfd_settime", 286), + ("timer_getoverrun", 225), + ("timer_gettime", 224), + ("timer_settime", 223), + ("times", 100), + ("tkill", 200), + ("truncate", 76), + ("tuxcall", 184), + ("umask", 95), + ("umount2", 166), + ("uname", 63), + ("unlink", 87), + ("unlinkat", 263), + ("unshare", 272), + ("uselib", 134), + ("userfaultfd", 323), + ("ustat", 136), + ("utime", 132), + ("utimensat", 280), + ("utimes", 235), + ("vfork", 58), + ("vhangup", 153), + ("vmsplice", 278), + ("vserver", 236), + ("wait4", 61), + ("waitid", 247), + ("write", 1), + ("writev", 20), + ] + .into_iter() + .collect() +} diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 56624cd..4fcb65c 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -34,11 +34,13 @@ const EXTRA_SYSCALLS: [i64; 6] = [ libc::SYS_futex, ]; -fn validate_seccomp_filter( - rules: Vec<(i64, Vec)>, - validation_fn: fn(), - should_fail: Option, -) { +enum Errno { + Equals(i32), + NotEquals(i32), + None, +} + +fn validate_seccomp_filter(rules: Vec<(i64, Vec)>, validation_fn: fn(), errno: Errno) { let mut rule_map: BTreeMap> = rules.into_iter().collect(); // Make sure the extra needed syscalls are allowed @@ -59,7 +61,7 @@ fn validate_seccomp_filter( // We need to run the validation inside another thread in order to avoid setting // the seccomp filter for the entire unit tests process. - let errno = thread::spawn(move || { + let returned_errno = thread::spawn(move || { // Install the filter. apply_filter(&filter).unwrap(); @@ -72,14 +74,11 @@ fn validate_seccomp_filter( .join() .unwrap(); - // In case of a seccomp denial `errno` should be `FAILURE_CODE` - if let Some(should_fail) = should_fail { - if should_fail { - assert_eq!(errno, FAILURE_CODE); - } else { - assert_ne!(errno, FAILURE_CODE); - } - } + match errno { + Errno::Equals(no) => assert_eq!(returned_errno, no), + Errno::NotEquals(no) => assert_ne!(returned_errno, no), + Errno::None => {} + }; } #[test] @@ -99,9 +98,14 @@ fn test_empty_filter() { // This should allow any system calls. let pid = thread::spawn(move || { + let seccomp_level = unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + assert_eq!(seccomp_level, 0); // Install the filter. apply_filter(&prog).unwrap(); + let seccomp_level = unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + assert_eq!(seccomp_level, 2); + unsafe { libc::getpid() } }) .join() @@ -163,7 +167,7 @@ fn test_eq_operator() { || unsafe { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -171,7 +175,7 @@ fn test_eq_operator() { || unsafe { libc::ioctl(0, 0); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use cases for QWORD @@ -185,7 +189,7 @@ fn test_eq_operator() { || unsafe { libc::ioctl(0, 0, u64::MAX); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -193,7 +197,7 @@ fn test_eq_operator() { || unsafe { libc::ioctl(0, 0, 0); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -211,7 +215,7 @@ fn test_ge_operator() { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); libc::ioctl(0, (KVM_GET_PIT2 + 1) as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -219,25 +223,24 @@ fn test_ge_operator() { || unsafe { libc::ioctl(0, (KVM_GET_PIT2 - 1) as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD let rules = vec![( libc::SYS_ioctl, - vec![SeccompRule::new(vec![ - Cond::new(2, Qword, Ge, u64::from(std::u32::MAX)).unwrap() - ]) - .unwrap()], + vec![ + SeccompRule::new(vec![Cond::new(2, Qword, Ge, u64::from(u32::MAX)).unwrap()]).unwrap(), + ], )]; // check syscalls that are supposed to work validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX)); - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 1); + libc::ioctl(0, 0, u64::from(u32::MAX)); + libc::ioctl(0, 0, u64::from(u32::MAX) + 1); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -245,7 +248,7 @@ fn test_ge_operator() { || unsafe { libc::ioctl(0, 0, 1); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -262,7 +265,7 @@ fn test_gt_operator() { || unsafe { libc::ioctl(0, (KVM_GET_PIT2 + 1) as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -270,14 +273,14 @@ fn test_gt_operator() { || unsafe { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD let rules = vec![( libc::SYS_ioctl, vec![SeccompRule::new(vec![ - Cond::new(2, Qword, Gt, u64::from(std::u32::MAX) + 10).unwrap() + Cond::new(2, Qword, Gt, u64::from(u32::MAX) + 10).unwrap() ]) .unwrap()], )]; @@ -285,17 +288,17 @@ fn test_gt_operator() { validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 11); + libc::ioctl(0, 0, u64::from(u32::MAX) + 11); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( rules, || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 10); + libc::ioctl(0, 0, u64::from(u32::MAX) + 10); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -313,7 +316,7 @@ fn test_le_operator() { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); libc::ioctl(0, (KVM_GET_PIT2 - 1) as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -321,14 +324,14 @@ fn test_le_operator() { || unsafe { libc::ioctl(0, (KVM_GET_PIT2 + 1) as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD let rules = vec![( libc::SYS_ioctl, vec![SeccompRule::new(vec![ - Cond::new(2, Qword, Le, u64::from(std::u32::MAX) + 10).unwrap() + Cond::new(2, Qword, Le, u64::from(u32::MAX) + 10).unwrap() ]) .unwrap()], )]; @@ -336,18 +339,18 @@ fn test_le_operator() { validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 10); - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 9); + libc::ioctl(0, 0, u64::from(u32::MAX) + 10); + libc::ioctl(0, 0, u64::from(u32::MAX) + 9); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( rules, || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 11); + libc::ioctl(0, 0, u64::from(u32::MAX) + 11); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -364,7 +367,7 @@ fn test_lt_operator() { || unsafe { libc::ioctl(0, (KVM_GET_PIT2 - 1) as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -372,14 +375,14 @@ fn test_lt_operator() { || unsafe { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD let rules = vec![( libc::SYS_ioctl, vec![SeccompRule::new(vec![ - Cond::new(2, Qword, Lt, u64::from(std::u32::MAX) + 10).unwrap() + Cond::new(2, Qword, Lt, u64::from(u32::MAX) + 10).unwrap() ]) .unwrap()], )]; @@ -387,17 +390,17 @@ fn test_lt_operator() { validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 9); + libc::ioctl(0, 0, u64::from(u32::MAX) + 9); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( rules, || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX) + 10); + libc::ioctl(0, 0, u64::from(u32::MAX) + 10); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -422,7 +425,7 @@ fn test_masked_eq_operator() { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); libc::ioctl(0, KVM_GET_PIT2_MSB as IoctlRequest); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -430,7 +433,7 @@ fn test_masked_eq_operator() { || unsafe { libc::ioctl(0, KVM_GET_PIT2_LSB as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD @@ -439,7 +442,7 @@ fn test_masked_eq_operator() { vec![SeccompRule::new(vec![Cond::new( 2, Qword, - MaskedEq(u64::from(std::u32::MAX)), + MaskedEq(u64::from(u32::MAX)), u64::MAX, ) .unwrap()]) @@ -449,10 +452,10 @@ fn test_masked_eq_operator() { validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, u64::from(std::u32::MAX)); + libc::ioctl(0, 0, u64::from(u32::MAX)); libc::ioctl(0, 0, u64::MAX); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -460,7 +463,7 @@ fn test_masked_eq_operator() { || unsafe { libc::ioctl(0, 0, 0); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -477,7 +480,7 @@ fn test_ne_operator() { || unsafe { libc::ioctl(0, 0); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -485,7 +488,7 @@ fn test_ne_operator() { || unsafe { libc::ioctl(0, KVM_GET_PIT2 as IoctlRequest); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); // check use case for QWORD @@ -499,7 +502,7 @@ fn test_ne_operator() { || unsafe { libc::ioctl(0, 0, 0); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); // check syscalls that are not supposed to work validate_seccomp_filter( @@ -507,7 +510,7 @@ fn test_ne_operator() { || unsafe { libc::ioctl(0, 0, u64::MAX); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); } @@ -532,10 +535,8 @@ fn test_complex_filter() { Cond::new(2, Dword, Eq, 15).unwrap(), ]) .unwrap(), - SeccompRule::new(vec![ - Cond::new(2, Qword, Eq, std::u32::MAX as u64 + 41).unwrap() - ]) - .unwrap(), + SeccompRule::new(vec![Cond::new(2, Qword, Eq, u32::MAX as u64 + 41).unwrap()]) + .unwrap(), ], ), ( @@ -555,7 +556,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 12); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -563,7 +564,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 14); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -571,7 +572,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 21); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -579,7 +580,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 39); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -587,15 +588,15 @@ fn test_complex_filter() { || unsafe { libc::ioctl(1, 0, 15); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, std::u32::MAX as u64 + 41); + libc::ioctl(0, 0, u32::MAX as u64 + 41); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -603,7 +604,7 @@ fn test_complex_filter() { || unsafe { libc::madvise(std::ptr::null_mut(), 0, 0); }, - Some(false), + Errno::NotEquals(FAILURE_CODE), ); validate_seccomp_filter( @@ -611,7 +612,7 @@ fn test_complex_filter() { || unsafe { assert!(libc::getpid() > 0); }, - None, + Errno::None, ); } @@ -622,7 +623,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 13); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -630,7 +631,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 16); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -638,7 +639,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 17); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -646,7 +647,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 18); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -654,7 +655,7 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 19); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -662,15 +663,15 @@ fn test_complex_filter() { || unsafe { libc::ioctl(0, 0, 20); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( rules.clone(), || unsafe { - libc::ioctl(0, 0, std::u32::MAX as u64 + 42); + libc::ioctl(0, 0, u32::MAX as u64 + 42); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( @@ -678,15 +679,15 @@ fn test_complex_filter() { || unsafe { libc::madvise(std::ptr::null_mut(), 1, 0); }, - Some(true), + Errno::Equals(FAILURE_CODE), ); validate_seccomp_filter( - rules.clone(), + rules, || unsafe { assert_eq!(libc::getuid() as i32, -FAILURE_CODE); }, - None, + Errno::None, ); } } diff --git a/tests/json.rs b/tests/json.rs new file mode 100644 index 0000000..b80c381 --- /dev/null +++ b/tests/json.rs @@ -0,0 +1,408 @@ +#![cfg(feature = "json")] + +use seccompiler::{apply_filter, compile_from_json, BpfProgram}; +use std::convert::TryInto; +use std::env::consts::ARCH; +use std::io::Read; +use std::thread; + +const FAILURE_CODE: i32 = 1000; + +enum Errno { + Equals(i32), + NotEquals(i32), + None, +} + +fn validate_json_filter(reader: R, validation_fn: fn(), errno: Errno) { + let mut filters = compile_from_json(reader, ARCH.try_into().unwrap()).unwrap(); + let filter: BpfProgram = filters.remove("main_thread").unwrap(); + + // We need to run the validation inside another thread in order to avoid setting + // the seccomp filter for the entire unit tests process. + let returned_errno = thread::spawn(move || { + // Install the filter. + apply_filter(&filter).unwrap(); + + // Call the validation fn. + validation_fn(); + + // Return errno. + std::io::Error::last_os_error().raw_os_error().unwrap() + }) + .join() + .unwrap(); + + match errno { + Errno::Equals(no) => assert_eq!(returned_errno, no), + Errno::NotEquals(no) => assert_ne!(returned_errno, no), + Errno::None => {} + }; +} + +#[test] +fn test_empty_filter_allow_all() { + // An empty filter should always return the default action. + // For example, for an empty allowlist, it should always trap/kill, + // for an empty denylist, it should allow all system calls. + + let json_input = r#"{ + "main_thread": { + "mismatch_action": "allow", + "match_action": "trap", + "filter": [] + } + }"#; + + let mut filters = compile_from_json(json_input.as_bytes(), ARCH.try_into().unwrap()).unwrap(); + let filter = filters.remove("main_thread").unwrap(); + // This should allow any system calls. + let pid = thread::spawn(move || { + let seccomp_level = unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + assert_eq!(seccomp_level, 0); + // Install the filter. + apply_filter(&filter).unwrap(); + let seccomp_level = unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + assert_eq!(seccomp_level, 2); + unsafe { libc::getpid() } + }) + .join() + .unwrap(); + // Check that the getpid syscall returned successfully. + assert!(pid > 0); +} + +#[test] +fn test_empty_filter_deny_all() { + let json_input = r#"{ + "main_thread": { + "mismatch_action": "kill_process", + "match_action": "allow", + "filter": [] + } + }"#; + + let mut filters = compile_from_json(json_input.as_bytes(), ARCH.try_into().unwrap()).unwrap(); + let filter = filters.remove("main_thread").unwrap(); + + // We need to use `fork` instead of `thread::spawn` to prohibit cargo from failing the test + // due to the SIGSYS exit code. + let pid = unsafe { libc::fork() }; + + match pid { + 0 => { + let seccomp_level = unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + assert_eq!(seccomp_level, 0); + // Install the filter. + apply_filter(&filter).unwrap(); + // this syscall will fail + unsafe { libc::prctl(libc::PR_GET_SECCOMP) }; + } + child_pid => { + let mut child_status: i32 = -1; + let pid_done = unsafe { libc::waitpid(child_pid, &mut child_status, 0) }; + assert_eq!(pid_done, child_pid); + + assert!(libc::WIFSIGNALED(child_status)); + assert_eq!(libc::WTERMSIG(child_status), libc::SIGSYS); + } + } +} + +#[test] +fn test_invalid_architecture() { + // A filter compiled for another architecture should kill the process upon evaluation. + // The process will appear as if it received a SIGSYS. + let mut arch = "aarch64"; + + if ARCH == "aarch64" { + arch = "x86_64"; + } + + let json_input = r#"{ + "main_thread": { + "mismatch_action": "allow", + "match_action": "trap", + "filter": [] + } + }"#; + + let mut filters = compile_from_json(json_input.as_bytes(), arch.try_into().unwrap()).unwrap(); + let filter = filters.remove("main_thread").unwrap(); + + let pid = unsafe { libc::fork() }; + match pid { + 0 => { + apply_filter(&filter).unwrap(); + + unsafe { + libc::getpid(); + } + } + child_pid => { + let mut child_status: i32 = -1; + let pid_done = unsafe { libc::waitpid(child_pid, &mut child_status, 0) }; + assert_eq!(pid_done, child_pid); + + assert!(libc::WIFSIGNALED(child_status)); + assert_eq!(libc::WTERMSIG(child_status), libc::SIGSYS); + } + }; +} + +#[test] +fn test_complex_filter() { + let json_input = r#"{ + "main_thread": { + "mismatch_action": {"errno" : 1000}, + "match_action": "allow", + "filter": [ + { + "syscall": "rt_sigprocmask", + "comment": "extra syscalls needed by the test runtime" + }, + { + "syscall": "sigaltstack" + }, + { + "syscall": "munmap" + }, + { + "syscall": "exit" + }, + { + "syscall": "rt_sigreturn" + }, + { + "syscall": "futex" + }, + { + "syscall": "getpid", + "comment": "start of the actual filter we want to test." + }, + { + "syscall": "ioctl", + "args": [ + { + "index": 2, + "type": "dword", + "op": "le", + "val": 14 + }, + { + "index": 2, + "type": "dword", + "op": "ne", + "val": 13 + } + ] + }, + { + "syscall": "ioctl", + "args": [ + { + "index": 2, + "type": "dword", + "op": "gt", + "val": 20 + }, + { + "index": 2, + "type": "dword", + "op": "lt", + "val": 40 + } + ] + }, + { + "syscall": "ioctl", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 1 + }, + { + "index": 2, + "type": "dword", + "op": "eq", + "val": 15 + } + ] + }, + { + "syscall": "ioctl", + "args": [ + { + "index": 2, + "type": "qword", + "op": "eq", + "val": 4294967336, + "comment": "u32::MAX as u64 + 41" + } + ] + }, + { + "syscall": "madvise", + "args": [ + { + "index": 0, + "type": "dword", + "op": "eq", + "val": 0 + }, + { + "index": 1, + "type": "dword", + "op": "eq", + "val": 0 + } + ] + } + ] + } + }"#; + + // check syscalls that are supposed to work + { + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 12); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 14); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 21); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 39); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(1, 0, 15); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, u32::MAX as u64 + 41); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::madvise(std::ptr::null_mut(), 0, 0); + }, + Errno::NotEquals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + assert!(libc::getpid() > 0); + }, + Errno::None, + ); + } + + // check syscalls that are not supposed to work + { + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 13); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 16); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 17); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 18); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 19); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, 20); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::ioctl(0, 0, u32::MAX as u64 + 42); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + libc::madvise(std::ptr::null_mut(), 1, 0); + }, + Errno::Equals(FAILURE_CODE), + ); + + validate_json_filter( + json_input.as_bytes(), + || unsafe { + assert_eq!(libc::getuid() as i32, -FAILURE_CODE); + }, + Errno::None, + ); + } +} diff --git a/tools/generate_syscall_tables.sh b/tools/generate_syscall_tables.sh new file mode 100755 index 0000000..32f7e29 --- /dev/null +++ b/tools/generate_syscall_tables.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash +# Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +# This script generates the syscall tables for seccompiler. + +set -e + +# Full path to the seccompiler tools dir. +TOOLS_DIR=$(cd "$(dirname "$0")" && pwd) + +# Full path to the seccompiler sources dir. +ROOT_DIR=$(cd "${TOOLS_DIR}/.." && pwd) + +# Path to the temporary linux kernel directory. +KERNEL_DIR="${ROOT_DIR}/.kernel" + +test_mode=0 + +PATH_TO_X86_TABLE="$ROOT_DIR/src/syscall_table/x86_64.rs" +PATH_TO_AARCH64_TABLE="$ROOT_DIR/src/syscall_table/aarch64.rs" + +PATH_TO_X86_TEST_TABLE="$ROOT_DIR/src/syscall_table/test_x86_64.rs" +PATH_TO_AARCH64_TEST_TABLE="$ROOT_DIR/src/syscall_table/test_aarch64.rs" + +generate_syscall_list_x86_64() { + # the table for x86_64 is nicely formatted here: + # linux/arch/x86/entry/syscalls/syscall_64.tbl + echo $(cat linux/arch/x86/entry/syscalls/syscall_64.tbl | grep -v "^#" | \ + grep -v -e '^$' | awk '{print $2,$3,$1}' | grep -v "^x32" | \ + awk '{print "(\""$2"\", "$3"),"}' | \ + sort -d) +} + +generate_syscall_list_aarch64() { + # filter for substituting `#define`s that point to other macros; + # values taken from linux/include/uapi/asm-generic/unistd.h + replace+='s/__NR3264_fadvise64/223/;' + replace+='s/__NR3264_fcntl/25/;' + replace+='s/__NR3264_fstatat/79/;' + replace+='s/__NR3264_fstatfs/44/;' + replace+='s/__NR3264_fstat/80/;' + replace+='s/__NR3264_ftruncate/46/;' + replace+='s/__NR3264_lseek/62/;' + replace+='s/__NR3264_sendfile/71/;' + replace+='s/__NR3264_statfs/43/;' + replace+='s/__NR3264_truncate/45/;' + replace+='s/__NR3264_mmap/222/;' + + echo "$1" > $path_to_rust_file + + # the aarch64 syscall table is not located in a .tbl file, like x86; + # we run gcc's pre-processor to extract the numeric constants from header + # files. + echo $(gcc -Ilinux/include/uapi -E -dM -D__ARCH_WANT_RENAMEAT\ + -D__BITS_PER_LONG=64 linux/arch/arm64/include/uapi/asm/unistd.h |\ + grep "#define __NR_" | grep -v "__NR_syscalls" |\ + grep -v "__NR_arch_specific_syscall" | awk -F '__NR_' '{print $2}' |\ + sed $replace | awk '{ print "(\""$1"\", "$2")," }' | sort -d) +} + +write_rust_syscall_table() { + kernel_version=$1 + platform=$2 + path_to_rust_file=$3 + + if [ "$platform" == "x86_64" ]; then + syscall_list=$(generate_syscall_list_x86_64) + elif [ "$platform" == "aarch64" ]; then + syscall_list=$(generate_syscall_list_aarch64) + else + die "Invalid platform" + fi + + echo "$(get_rust_file_header "$kernel_version")" > $path_to_rust_file + + printf " + use std::collections::HashMap; + + pub(crate) fn make_syscall_table() -> HashMap<&'static str, i64> { + vec![%s].into_iter().collect() }" "${syscall_list}" >> $path_to_rust_file + + rustfmt $path_to_rust_file + + echo "Generated at: $path_to_rust_file" +} + +# Validate the user supplied kernel version number. +# It must be composed of 2 groups of integers separated by dot, with an +# optional third group. +validate_kernel_version() { + local version_regex="^([0-9]+.)[0-9]+(.[0-9]+)?$" + version="$1" + + if [ -z "$version" ]; then + die "Version cannot be empty." + elif [[ ! "$version" =~ $version_regex ]]; then + die "Invalid version number: $version (expected: \$Major.\$Minor.\$Patch(optional))." + fi + +} + +download_kernel() { + kernel_version=$1 + kernel_major=v$(echo ${kernel_version} | cut -d . -f 1).x + kernel_baseurl=https://www.kernel.org/pub/linux/kernel/${kernel_major} + kernel_archive=linux-${kernel_version}.tar.xz + + # Create the kernel clone directory + rm -rf "$KERNEL_DIR" + mkdir -p "$KERNEL_DIR" || die "Error: cannot create dir $dir" + [ -x "$KERNEL_DIR" ] && [ -w "$dir" ] || \ + { + chmod +x+w "$KERNEL_DIR" + } || \ + die "Error: wrong permissions for $KERNEL_DIR. Should be +x+w" + + cd "$KERNEL_DIR" + + echo "Fetching linux kernel..." + + # Get sha256 checksum. + curl -fsSLO ${kernel_baseurl}/sha256sums.asc + kernel_sha256=$(grep ${kernel_archive} sha256sums.asc | cut -d ' ' -f 1) + # Get kernel archive. + curl -fsSLO "$kernel_baseurl/$kernel_archive" + # Verify checksum. + echo "${kernel_sha256} ${kernel_archive}" | sha256sum -c - + # Decompress the kernel source. + xz -d "${kernel_archive}" + cat linux-${kernel_version}.tar | tar -x && \ + mv linux-${kernel_version} linux +} + +run_validation() { + # We want to regenerate the tables and compare them with the existing ones. + # This is to validate that the tables are actually correct and were not + # mistakenly or maliciously modified. + arch=$1 + kernel_version=$2 + + if [[ $arch == "x86_64" ]]; then + path_to_table=$PATH_TO_X86_TABLE + path_to_test_table=$PATH_TO_X86_TEST_TABLE + elif [[ $arch == "aarch64" ]]; then + path_to_table=$PATH_TO_AARCH64_TABLE + path_to_test_table=$PATH_TO_AARCH64_TEST_TABLE + else + die "Invalid platform" + fi + + download_kernel "$kernel_version" + + # Generate new tables to validate against. + write_rust_syscall_table \ + "$kernel_version" "$arch" "$path_to_test_table" + + # Perform comparison. Tables should be identical, except for the timestamp + # comment line. + diff -I "\/\/ Generated on:.*" $path_to_table $path_to_test_table || { + echo "" + echo "Syscall table validation failed." + echo "Make sure they haven't been mistakenly altered." + echo "" + + exit 1 + } + + echo "Validation successful." +} + +get_rust_file_header() { + echo "$(cat <<-END +// Copyright $(date +"%Y") Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause + +// This file is auto-generated by \`tools/generate_syscall_tables\`. +// Do NOT manually edit! +// Generated on: $(date) +// Kernel version: $1 +END + )" +} + +# Exit with an error message +die() { + echo -e "$1" + exit 1 +} + +help() { + echo "" + echo "Generates the syscall tables for seccompiler, according to a given kernel version." + echo "Release candidate (rc) linux versions are not allowed." + echo "Outputs a rust file for each supported arch: src/seccompiler/src/syscall_table/{arch}.rs" + echo "Supported architectures: x86_64 and aarch64." + echo "" + echo "If passed the --test flag, it will validate that the generated syscall tables" + echo "are correct by regenerating them and comparing the results." + echo "" +} + +cleanup () { + rm -rf $KERNEL_DIR + + if [[ $test_mode -eq 1 ]]; then + rm -rf $PATH_TO_X86_TEST_TABLE + rm -rf $PATH_TO_AARCH64_TEST_TABLE + fi +} + +parse_cmdline() { + # Parse command line args. + while [ $# -gt 0 ]; do + case "$1" in + "-h"|"--help") { help; exit 1; } ;; + "--test") { test_mode=1; break; } ;; + *) { kernel_version="$1"; } ;; + esac + shift + done +} + +test() { + # Run the validation for x86_64. + echo "Validating table for x86_64..." + + kernel_version_x86_64=$(cat $PATH_TO_X86_TABLE | \ + awk -F '// Kernel version:' '{print $2}' | xargs) + + validate_kernel_version "$kernel_version_x86_64" + + run_validation "x86_64" "$kernel_version_x86_64" + + # Run the validation for aarch64. + echo "Validating table for aarch64..." + + kernel_version_aarch64=$(cat $PATH_TO_AARCH64_TABLE | \ + awk -F '// Kernel version:' '{print $2}' | xargs) + + validate_kernel_version "$kernel_version_aarch64" + + run_validation "aarch64" "$kernel_version_aarch64" +} + +main() { + if [[ $test_mode -eq 1 ]]; then + # When in test mode, re-generate the tables according to the version + # from the rust files and validate that they are identical. + test + else + # When not in test mode, we only want to re-generate the tables. + + validate_kernel_version "$kernel_version" + download_kernel "$kernel_version" + + # generate syscall table for x86_64 + echo "Generating table for x86_64..." + write_rust_syscall_table \ + "$kernel_version" "x86_64" "$PATH_TO_X86_TABLE" + + # generate syscall table for aarch64 + echo "Generating table for aarch64..." + write_rust_syscall_table \ + "$kernel_version" "aarch64" "$PATH_TO_AARCH64_TABLE" + fi +} + +# Setup a cleanup trap on exit. +trap cleanup EXIT + +parse_cmdline $@ + +main