From 95cdade9a69b0d337c44b26de77344c50357ab8c Mon Sep 17 00:00:00 2001 From: Egor Lazarchuk Date: Mon, 9 Dec 2024 16:30:07 +0000 Subject: [PATCH] feat: remove libseccomp rust dependency Replace `libseccomp` with in house binding. This way we don't need to add another dependency and allows us to have access the `seccomp_export_bpf_mem` method, not exposed in the `libseccomp` crate. This creates another issue though: the `seccomp_export_bpf_mem` function needs to be exposed by the libseccomp library. This is not an issue when `seccompiler` is build in the docker environment because we build it from source. But version provided by linux distribution might have this function not exposed. Signed-off-by: Egor Lazarchuk --- Cargo.lock | 25 ---- src/seccompiler/Cargo.toml | 1 - src/seccompiler/build.rs | 1 + src/seccompiler/src/bindings.rs | 174 +++++++++++++++++++++++++ src/seccompiler/src/lib.rs | 216 +++++++++++++++++++++----------- src/seccompiler/src/types.rs | 45 +++---- 6 files changed, 333 insertions(+), 129 deletions(-) create mode 100644 src/seccompiler/src/bindings.rs diff --git a/Cargo.lock b/Cargo.lock index 8d56cf5aa0f..f6db8d73d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -875,24 +875,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" -[[package]] -name = "libseccomp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21c57fd8981a80019807b7b68118618d29a87177c63d704fc96e6ecd003ae5b3" -dependencies = [ - "bitflags 1.3.2", - "libc", - "libseccomp-sys", - "pkg-config", -] - -[[package]] -name = "libseccomp-sys" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7cbbd4ad467251987c6e5b47d53b11a5a05add08f2447a9e2d70aef1e0d138" - [[package]] name = "linux-loader" version = "0.13.0" @@ -1044,12 +1026,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" -[[package]] -name = "pkg-config" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" - [[package]] name = "polyval" version = "0.6.2" @@ -1237,7 +1213,6 @@ dependencies = [ "clap", "displaydoc", "libc", - "libseccomp", "serde", "serde_json", "thiserror 2.0.3", diff --git a/src/seccompiler/Cargo.toml b/src/seccompiler/Cargo.toml index f3f1f34f53f..ace418f7e2a 100644 --- a/src/seccompiler/Cargo.toml +++ b/src/seccompiler/Cargo.toml @@ -20,7 +20,6 @@ bincode = "1.2.1" clap = { version = "4.5.21", features = ["derive", "string"] } displaydoc = "0.2.5" libc = "0.2.167" -libseccomp = "0.3.0" serde = { version = "1.0.215", features = ["derive"] } serde_json = "1.0.133" thiserror = "2.0.3" diff --git a/src/seccompiler/build.rs b/src/seccompiler/build.rs index 69878f1f31b..d0d2a30e39e 100644 --- a/src/seccompiler/build.rs +++ b/src/seccompiler/build.rs @@ -2,5 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 fn main() { + println!("cargo::rustc-link-search=/usr/local/lib"); println!("cargo::rustc-link-lib=seccomp"); } diff --git a/src/seccompiler/src/bindings.rs b/src/seccompiler/src/bindings.rs new file mode 100644 index 00000000000..34be3741d74 --- /dev/null +++ b/src/seccompiler/src/bindings.rs @@ -0,0 +1,174 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] + +//! Raw FFI bindings for libseccomp library + +use std::os::raw::*; + +pub const MINUS_EEXIST: i32 = -libc::EEXIST; + +/// Filter context/handle (`*mut`) +pub type scmp_filter_ctx = *mut c_void; +/// Filter context/handle (`*const`) +pub type const_scmp_filter_ctx = *const c_void; + +/// Comparison operators +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub enum scmp_compare { + _SCMP_CMP_MIN = 0, + /// not equal + SCMP_CMP_NE = 1, + /// less than + SCMP_CMP_LT = 2, + /// less than or equal + SCMP_CMP_LE = 3, + /// equal + SCMP_CMP_EQ = 4, + /// greater than or equal + SCMP_CMP_GE = 5, + /// greater than + SCMP_CMP_GT = 6, + /// masked equality + SCMP_CMP_MASKED_EQ = 7, + _SCMP_CMP_MAX, +} + +/// Argument datum +pub type scmp_datum_t = u64; + +/// Argument / Value comparison definition +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(C)] +pub struct scmp_arg_cmp { + /// argument number, starting at 0 + pub arg: c_uint, + /// the comparison op, e.g. `SCMP_CMP_*` + pub op: scmp_compare, + pub datum_a: scmp_datum_t, + pub datum_b: scmp_datum_t, +} + +pub const SCMP_ARCH_X86_64: u32 = 0xc000003e; +pub const SCMP_ARCH_AARCH64: u32 = 0xc00000b7; +/// Kill the process +pub const SCMP_ACT_KILL_PROCESS: u32 = 0x80000000; +/// Kill the thread +pub const SCMP_ACT_KILL_THREAD: u32 = 0x00000000; +/// Throw a `SIGSYS` signal +pub const SCMP_ACT_TRAP: u32 = 0x00030000; +/// Notifies userspace +pub const SCMP_ACT_ERRNO_MASK: u32 = 0x00050000; +/// Return the specified error code +#[must_use] +pub const fn SCMP_ACT_ERRNO(x: u16) -> u32 { + SCMP_ACT_ERRNO_MASK | x as u32 +} +pub const SCMP_ACT_TRACE_MASK: u32 = 0x7ff00000; +/// Notify a tracing process with the specified value +#[must_use] +pub const fn SCMP_ACT_TRACE(x: u16) -> u32 { + SCMP_ACT_TRACE_MASK | x as u32 +} +/// Allow the syscall to be executed after the action has been logged +pub const SCMP_ACT_LOG: u32 = 0x7ffc0000; +/// Allow the syscall to be executed +pub const SCMP_ACT_ALLOW: u32 = 0x7fff0000; + +#[link(name = "seccomp")] +extern "C" { + /// Initialize the filter state + /// + /// - `def_action`: the default filter action + /// + /// This function initializes the internal seccomp filter state and should + /// be called before any other functions in this library to ensure the filter + /// state is initialized. Returns a filter context on success, `ptr::null()` on failure. + pub fn seccomp_init(def_action: u32) -> scmp_filter_ctx; + + /// Adds an architecture to the filter + /// + /// - `ctx`: the filter context + /// - `arch_token`: the architecture token, e.g. `SCMP_ARCH_*` + /// + /// This function adds a new architecture to the given seccomp filter context. + /// Any new rules added after this function successfully returns will be added + /// to this architecture but existing rules will not be added to this + /// architecture. If the architecture token is [`SCMP_ARCH_NATIVE`] then the native + /// architecture will be assumed. Returns zero on success, `-libc::EEXIST` if + /// specified architecture is already present, other negative values on failure. + pub fn seccomp_arch_add(ctx: scmp_filter_ctx, arch_token: u32) -> c_int; + + /// Resolve a syscall name to a number + /// + /// - `name`: the syscall name + /// + /// Resolve the given syscall name to the syscall number. Returns the syscall + /// number on success, including negative pseudo syscall numbers (e.g. `__PNR_*`); + /// returns [`__NR_SCMP_ERROR`] on failure. + pub fn seccomp_syscall_resolve_name(name: *const c_char) -> c_int; + + /// Add a new rule to the filter + /// + /// - `ctx`: the filter context + /// - `action`: the filter action + /// - `syscall`: the syscall number + /// - `arg_cnt`: the number of argument filters in the argument filter chain + /// - `...`: [`scmp_arg_cmp`] structs + /// + /// This function adds a series of new argument/value checks to the seccomp + /// filter for the given syscall; multiple argument/value checks can be + /// specified and they will be chained together (AND'd together) in the filter. + /// If the specified rule needs to be adjusted due to architecture specifics it + /// will be adjusted without notification. Returns zero on success, negative + /// values on failure. + pub fn seccomp_rule_add( + ctx: scmp_filter_ctx, + action: u32, + syscall: c_int, + arg_cnt: c_uint, + ... + ) -> c_int; + + /// Add a new rule to the filter + /// + /// - `ctx`: the filter context + /// - `action`: the filter action + /// - `syscall`: the syscall number + /// - `arg_cnt`: the number of elements in the arg_array parameter + /// - `arg_array`: array of [`scmp_arg_cmp`] structs + /// + /// This function adds a series of new argument/value checks to the seccomp + /// filter for the given syscall; multiple argument/value checks can be + /// specified and they will be chained together (AND'd together) in the filter. + /// If the specified rule needs to be adjusted due to architecture specifics it + /// will be adjusted without notification. Returns zero on success, negative + /// values on failure. + pub fn seccomp_rule_add_array( + ctx: scmp_filter_ctx, + action: u32, + syscall: c_int, + arg_cnt: c_uint, + arg_array: *const scmp_arg_cmp, + ) -> c_int; + + /// Generate seccomp Berkeley Packet Filter (BPF) code and export it to a buffer + /// + /// - `ctx`: the filter context + /// - `buf`: the destination buffer + /// - `len`: on input the length of the buffer, on output the number of bytes in the program + /// + /// This function generates seccomp Berkeley Packer Filter (BPF) code and writes + /// it to the given buffer. Returns zero on success, negative values on failure. + pub fn seccomp_export_bpf_mem( + ctx: const_scmp_filter_ctx, + buf: *mut c_void, + len: *mut usize, + ) -> c_int; +} + +/// Negative pseudo syscall number returned by some functions in case of an error +pub const __NR_SCMP_ERROR: c_int = -1; diff --git a/src/seccompiler/src/lib.rs b/src/seccompiler/src/lib.rs index d3fa7aff242..97b6c5c05fa 100644 --- a/src/seccompiler/src/lib.rs +++ b/src/seccompiler/src/lib.rs @@ -3,14 +3,15 @@ use std::collections::HashMap; use std::fs::File; -use std::io::{Read, Seek}; -use std::os::fd::FromRawFd; -use std::os::unix::fs::MetadataExt; +use std::io::Read; use bincode::Error as BincodeError; -use libseccomp::*; + +mod bindings; +use bindings::*; pub mod types; +use libc::c_void; pub use types::*; /// Binary filter compilation errors. @@ -24,22 +25,16 @@ pub enum CompilationError { JsonDeserialize(serde_json::Error), /// Cannot parse arch: {0} ArchParse(String), - /// Cannot create libseccomp context: {0} - LibSeccompContext(libseccomp::error::SeccompError), - /// Cannot add libseccomp arch: {0} - LibSeccompArch(libseccomp::error::SeccompError), - /// Cannot add libseccomp syscall: {0} - LibSeccompSycall(libseccomp::error::SeccompError), - /// Cannot add libseccomp syscall rule: {0} - LibSeccompRule(libseccomp::error::SeccompError), - /// Cannot create memfd: {0} - MemfdCreate(i32), - /// Cannot resize memfd: {0} - MemfdResize(std::io::Error), - /// Cannot export libseccomp bpf: {0} - LibSeccompExport(libseccomp::error::SeccompError), - /// Cannot read from memfd: {0} - MemfdRead(std::io::Error), + /// Cannot create libseccomp context + LibSeccompContext, + /// Cannot add libseccomp arch + LibSeccompArch, + /// Cannot add libseccomp syscall + LibSeccompSycall, + /// Cannot add libseccomp syscall rule + LibSeccompRule, + /// Cannot export libseccomp bpf + LibSeccompExport, /// Cannot create output file: {0} OutputCreate(std::io::Error), /// Cannot serialize bfp: {0} @@ -62,39 +57,47 @@ pub fn compile_bpf( let arch: TargetArch = arch.try_into().map_err(CompilationError::ArchParse)?; - // SAFETY: Safe because the parameters are valid. - let memfd_fd = unsafe { libc::memfd_create("bpf\0".as_ptr().cast(), 0) }; - if memfd_fd < 0 { - return Err(CompilationError::MemfdCreate( - // SAFETY: Safe because there are no parameters. - unsafe { *libc::__errno_location() }, - )); - } - - // SAFETY: Safe because the parameters are valid. - let mut memfd = unsafe { File::from_raw_fd(memfd_fd) }; - let mut bpf_map: HashMap> = HashMap::new(); for (name, filter) in bpf_map_json.0.iter() { let default_action = filter.default_action.to_scmp_type(); let filter_action = filter.filter_action.to_scmp_type(); - let mut bpf_filter = ScmpFilterContext::new_filter(default_action) - .map_err(CompilationError::LibSeccompContext)?; - bpf_filter - .add_arch(arch.to_scmp_type()) - .map_err(CompilationError::LibSeccompArch)?; + // SAFETY: Safe as all args are correect. + let bpf_filter = unsafe { + let r = seccomp_init(default_action); + if r.is_null() { + return Err(CompilationError::LibSeccompContext); + } + r + }; + + // SAFETY: Safe as all args are correect. + unsafe { + let r = seccomp_arch_add(bpf_filter, arch.to_scmp_type()); + if r != 0 && r != MINUS_EEXIST { + return Err(CompilationError::LibSeccompArch); + } + } for rule in filter.filter.iter() { - let syscall = ScmpSyscall::from_name(&rule.syscall) - .map_err(CompilationError::LibSeccompSycall)?; + // SAFETY: Safe as all args are correect. + let syscall = unsafe { + let r = seccomp_syscall_resolve_name(rule.syscall.as_ptr()); + if r == __NR_SCMP_ERROR { + return Err(CompilationError::LibSeccompSycall); + } + r + }; // TODO remove when we drop deprecated "basic" arg from cli. // "basic" bpf means it ignores condition checks. if basic { - bpf_filter - .add_rule(filter_action, syscall) - .map_err(CompilationError::LibSeccompRule)?; + // SAFETY: Safe as all args are correect. + unsafe { + if seccomp_rule_add(bpf_filter, filter_action, syscall, 0) != 0 { + return Err(CompilationError::LibSeccompRule); + } + } } else if let Some(rules) = &rule.args { let comparators = rules .iter() @@ -107,45 +110,110 @@ pub fn compile_bpf( // For `ioctls` we need to mask upper bits as musl // sets them to 1, but libseccomp expilictly checks that they are 0. // with 0x00000000FFFFFFFF mask upper bits are always 0. - let op = if syscall == IOCTL { - let original_rule = rule.op.to_scmp_type(); - if original_rule == ScmpCompareOp::Equal { - ScmpCompareOp::MaskedEqual(0x00000000FFFFFFFF) - } else { - original_rule + match rule.op { + SeccompCmpOp::Eq => { + if syscall == IOCTL { + scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_MASKED_EQ, + datum_a: 0x00000000FFFFFFFF, + datum_b: rule.val, + } + } else { + scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_EQ, + datum_a: rule.val, + datum_b: 0, + } + } } - } else { - rule.op.to_scmp_type() - }; - ScmpArgCompare::new(rule.index as u32, op, rule.val) + SeccompCmpOp::Ge => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_GE, + datum_a: rule.val, + datum_b: 0, + }, + SeccompCmpOp::Gt => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_GT, + datum_a: rule.val, + datum_b: 0, + }, + SeccompCmpOp::Le => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_LE, + datum_a: rule.val, + datum_b: 0, + }, + SeccompCmpOp::Lt => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_LT, + datum_a: rule.val, + datum_b: 0, + }, + SeccompCmpOp::Ne => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_NE, + datum_a: rule.val, + datum_b: 0, + }, + + SeccompCmpOp::MaskedEq(m) => scmp_arg_cmp { + arg: rule.index as u32, + op: scmp_compare::SCMP_CMP_MASKED_EQ, + datum_a: m, + datum_b: rule.val, + }, + } }) - .collect::>(); - bpf_filter - .add_rule_conditional(filter_action, syscall, &comparators) - .map_err(CompilationError::LibSeccompRule)?; + .collect::>(); + + // SAFETY: Safe as all args are correect. + // We can assume noone will define u32::MAX + // filters for a syscall. + #[allow(clippy::cast_possible_truncation)] + unsafe { + if seccomp_rule_add_array( + bpf_filter, + filter_action, + syscall, + comparators.len() as u32, + comparators.as_ptr(), + ) != 0 + { + return Err(CompilationError::LibSeccompRule); + } + } } else { - bpf_filter - .add_rule(filter_action, syscall) - .map_err(CompilationError::LibSeccompRule)?; + // SAFETY: Safe as all args are correect. + unsafe { + if seccomp_rule_add(bpf_filter, filter_action, syscall, 0) != 0 { + return Err(CompilationError::LibSeccompRule); + } + } } } - memfd.rewind().unwrap(); - bpf_filter - .export_bpf(&mut memfd) - .map_err(CompilationError::LibSeccompExport)?; - memfd.rewind().unwrap(); - - // Usize == u64 - #[allow(clippy::cast_possible_truncation)] - let size = memfd.metadata().unwrap().size() as usize; - let instructions = size / std::mem::size_of::(); - let mut bpf = vec![0_u64; instructions]; - + // First we need to get a number of bytes we need to store the bpf. + let mut len: usize = 0; + // SAFETY: Safe as all args are correect. + unsafe { + if seccomp_export_bpf_mem(bpf_filter, std::ptr::null_mut::(), &mut len) != 0 { + return Err(CompilationError::LibSeccompExport); + } + } + // Bpf consists of instructions each 8 bytes long and 4 bytes aligned. + // We will use Vec to store bpf program to safisfy all the needs. + let len_u64 = len / std::mem::size_of::(); + let mut bpf = vec![0_u64; len_u64]; // SAFETY: Safe as u64 has bigger alignment and size is correct. - let bpf_u8: &mut [u8] = - unsafe { std::slice::from_raw_parts_mut(bpf.as_mut_ptr().cast(), size) }; - memfd.read_exact(bpf_u8).unwrap(); + unsafe { + if seccomp_export_bpf_mem(bpf_filter, bpf.as_mut_ptr().cast(), &mut len) != 0 { + return Err(CompilationError::LibSeccompExport); + } + } + bpf_map.insert(name.clone(), bpf); } diff --git a/src/seccompiler/src/types.rs b/src/seccompiler/src/types.rs index 99d5ef7c36a..70a0d9e512a 100644 --- a/src/seccompiler/src/types.rs +++ b/src/seccompiler/src/types.rs @@ -1,9 +1,10 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use std::collections::BTreeMap; +use std::{collections::BTreeMap, ffi::CString}; -use libseccomp::{ScmpAction, ScmpArch, ScmpCompareOp}; +// use libseccomp::{ScmpAction, ScmpArch, ScmpCompareOp}; +use crate::bindings::*; use serde::*; /// Comparison to perform when matching a condition. @@ -19,20 +20,6 @@ pub enum SeccompCmpOp { Ne, } -impl SeccompCmpOp { - pub fn to_scmp_type(&self) -> ScmpCompareOp { - match self { - SeccompCmpOp::Eq => ScmpCompareOp::Equal, - SeccompCmpOp::Ge => ScmpCompareOp::GreaterEqual, - SeccompCmpOp::Gt => ScmpCompareOp::Greater, - SeccompCmpOp::Le => ScmpCompareOp::LessOrEqual, - SeccompCmpOp::Lt => ScmpCompareOp::Less, - SeccompCmpOp::MaskedEq(me) => ScmpCompareOp::MaskedEqual(*me), - SeccompCmpOp::Ne => ScmpCompareOp::NotEqual, - } - } -} - /// Condition that syscall must match in order to satisfy a rule. #[derive(Debug, Deserialize)] pub struct SeccompCondition { @@ -46,7 +33,7 @@ pub struct SeccompCondition { #[serde(rename_all = "snake_case")] pub enum SeccompAction { Allow, - Errno(i32), + Errno(u16), KillThread, KillProcess, Log, @@ -55,15 +42,15 @@ pub enum SeccompAction { } impl SeccompAction { - pub fn to_scmp_type(&self) -> ScmpAction { + pub fn to_scmp_type(&self) -> u32 { match self { - SeccompAction::Allow => ScmpAction::Allow, - SeccompAction::Errno(e) => ScmpAction::Errno(*e), - SeccompAction::KillThread => ScmpAction::KillThread, - SeccompAction::KillProcess => ScmpAction::KillProcess, - SeccompAction::Log => ScmpAction::Log, - SeccompAction::Trace(t) => ScmpAction::Trace(*t), - SeccompAction::Trap => ScmpAction::Trap, + SeccompAction::Allow => SCMP_ACT_ALLOW, + SeccompAction::Errno(e) => SCMP_ACT_ERRNO(*e), + SeccompAction::KillThread => SCMP_ACT_KILL_THREAD, + SeccompAction::KillProcess => SCMP_ACT_KILL_PROCESS, + SeccompAction::Log => SCMP_ACT_LOG, + SeccompAction::Trace(t) => SCMP_ACT_TRACE(*t), + SeccompAction::Trap => SCMP_ACT_TRAP, } } } @@ -75,7 +62,7 @@ impl SeccompAction { /// If no rule matches the default action is applied. #[derive(Debug, Deserialize)] pub struct SyscallRule { - pub syscall: String, + pub syscall: CString, pub args: Option>, } @@ -104,10 +91,10 @@ pub enum TargetArch { } impl TargetArch { - pub fn to_scmp_type(&self) -> ScmpArch { + pub fn to_scmp_type(&self) -> u32 { match self { - TargetArch::X86_64 => ScmpArch::X8664, - TargetArch::Aarch64 => ScmpArch::Aarch64, + TargetArch::X86_64 => SCMP_ARCH_X86_64, + TargetArch::Aarch64 => SCMP_ARCH_AARCH64, } } }