diff --git a/bin/autodetect/Main.cc b/bin/autodetect/Main.cc index 4c328fa5e6..047cbe49ee 100644 --- a/bin/autodetect/Main.cc +++ b/bin/autodetect/Main.cc @@ -177,7 +177,13 @@ int main(int argc, char** argv) { // Reduce memory priority before installing system call filters. ml::core::CProcessPriority::reduceMemoryPriority(); - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + // Log and continue on a degraded install. This + // binary does not process untrusted model input, unlike + // pytorch_inference. + if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() != + ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) { + LOG_INFO(<< "Continuing without full syscall filtering"); + } if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); diff --git a/bin/categorize/Main.cc b/bin/categorize/Main.cc index aa4a1a4aaf..ae60e880d3 100644 --- a/bin/categorize/Main.cc +++ b/bin/categorize/Main.cc @@ -137,7 +137,13 @@ int main(int argc, char** argv) { // Reduce memory priority before installing system call filters. ml::core::CProcessPriority::reduceMemoryPriority(); - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + // Log and continue on a degraded install. This + // binary does not process untrusted model input, unlike + // pytorch_inference. + if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() != + ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) { + LOG_INFO(<< "Continuing without full syscall filtering"); + } if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); diff --git a/bin/data_frame_analyzer/Main.cc b/bin/data_frame_analyzer/Main.cc index 4b7b3d1ff1..78e172e438 100644 --- a/bin/data_frame_analyzer/Main.cc +++ b/bin/data_frame_analyzer/Main.cc @@ -160,7 +160,13 @@ int main(int argc, char** argv) { // Reduce memory priority before installing system call filters. ml::core::CProcessPriority::reduceMemoryPriority(); - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + // Log and continue on a degraded install. This + // binary does not process untrusted model input, unlike + // pytorch_inference. + if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() != + ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) { + LOG_INFO(<< "Continuing without full syscall filtering"); + } if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); diff --git a/bin/normalize/Main.cc b/bin/normalize/Main.cc index f6a79a7b65..b50723f0b3 100644 --- a/bin/normalize/Main.cc +++ b/bin/normalize/Main.cc @@ -115,7 +115,13 @@ int main(int argc, char** argv) { // Reduce memory priority before installing system call filters. ml::core::CProcessPriority::reduceMemoryPriority(); - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + // Log and continue on a degraded install. This + // binary does not process untrusted model input, unlike + // pytorch_inference. + if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() != + ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) { + LOG_INFO(<< "Continuing without full syscall filtering"); + } if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); diff --git a/bin/pytorch_inference/Main.cc b/bin/pytorch_inference/Main.cc index cb0e4393a7..800c7525b6 100644 --- a/bin/pytorch_inference/Main.cc +++ b/bin/pytorch_inference/Main.cc @@ -295,7 +295,33 @@ int main(int argc, char** argv) { // Reduce memory priority before installing system call filters. ml::core::CProcessPriority::reduceMemoryPriority(); - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + + // Internal switch, not an operator setting: it stays false until the + // controller can route around Sandbox2 explicitly and guarantee that a + // degraded-mode (no-Sandbox2) launch was a deliberate operator choice + // rather than the only option this process has. Flipping it on today + // would terminate every launch on a host lacking seccomp BPF, with no + // operator fallback to select instead. + constexpr bool TERMINATE_ON_DEGRADED_SECCOMP_FAILURE{false}; + + const ml::seccomp::ESystemCallFilterInstallOutcome seccompOutcome{ + ml::seccomp::CSystemCallFilter::installSystemCallFilter()}; + + if (ml::seccomp::decideDegradedModeAction(seccompOutcome, TERMINATE_ON_DEGRADED_SECCOMP_FAILURE) == + ml::seccomp::EDegradedModeAction::E_TerminateBeforeIo) { + LOG_FATAL(<< "Seccomp installation " << ml::seccomp::describe(seccompOutcome) + << "; terminating before untrusted model processing"); + return EXIT_FAILURE; + } + + // Explicit structured attestation the controller/Elasticsearch can + // assert on directly, rather than inferring readiness from the absence + // of a fatal log line above. + const std::string degradedModeMarker{ + ml::seccomp::degradedModeAttestationMarker(seccompOutcome)}; + if (degradedModeMarker.empty() == false) { + LOG_INFO(<< degradedModeMarker); + } if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); diff --git a/include/seccomp/CMlLegacyBpfSyscallAllowlist.h b/include/seccomp/CMlLegacyBpfSyscallAllowlist.h new file mode 100644 index 0000000000..f15b5f277d --- /dev/null +++ b/include/seccomp/CMlLegacyBpfSyscallAllowlist.h @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the following additional limitation. Functionality enabled by the + * files subject to the Elastic License 2.0 may only be used in production when + * invoked by an Elasticsearch process with a license key installed that permits + * use of machine learning features. You may not use this file except in + * compliance with the Elastic License 2.0 and the foregoing additional + * limitation. + */ +#ifndef INCLUDED_ml_seccomp_CMlLegacyBpfSyscallAllowlist_h +#define INCLUDED_ml_seccomp_CMlLegacyBpfSyscallAllowlist_h + +#include +#include + +#ifdef __linux__ +#include +#endif + +namespace ml { +namespace seccomp { + +#ifdef __linux__ + +// statx, rseq and clone3 won't be defined on a RHEL/CentOS 7 build machine, +// but might exist on the kernel we run on, so fall back to the raw numbers. +#if defined(__x86_64__) +#ifndef __NR_statx +#define ML_NR_statx 332 +#else +#define ML_NR_statx __NR_statx +#endif +#ifndef __NR_rseq +#define ML_NR_rseq 334 +#else +#define ML_NR_rseq __NR_rseq +#endif +#elif defined(__aarch64__) +#ifndef __NR_statx +#define ML_NR_statx 291 +#else +#define ML_NR_statx __NR_statx +#endif +#ifndef __NR_rseq +#define ML_NR_rseq 293 +#else +#define ML_NR_rseq __NR_rseq +#endif +#endif +#ifndef __NR_clone3 +#define ML_NR_clone3 435 +#else +#define ML_NR_clone3 __NR_clone3 +#endif + +//! Syscalls permitted by the legacy in-process BPF filter +//! (CSystemCallFilter_Linux.cc) for every process that installs it, currently +//! shared by pytorch_inference, autodetect, categorize, normalize and +//! data_frame_analyzer. This is the single machine-readable declaration that +//! the applied BPF program is generated from: CSystemCallFilter_Linux.cc +//! contains no independent syscall list and no manually maintained jump +//! offsets. A future Sandbox2 policy is expected to consume the same +//! declaration for its explicit grants, so both mechanisms stay in sync. +//! +//! Carry-forward note: PR #2873 fixed several pytorch_inference/libtorch +//! compatibility gaps the hard way, and this declaration is a rewrite from +//! scratch rather than a copy of that work, so it deliberately keeps two of +//! them. ML_NR_clone3 (see 57f00ed1b) and __NR_prlimit64 (see 03b1ee4a) are +//! carried into this shared declaration so a future Sandbox2 policy +//! inherits them automatically instead of rediscovering them the same way; +//! CSeccompFilterBuilderTest.cc asserts both stay present. The x86_64 +//! legacy filesystem syscalls below (see ec7d3ed85) were already part of +//! this filter's syscall set prior to this declaration and remain +//! unchanged. PR #2873's futex-op broadening (see d9a856d5f) and CI +//! link-order/test-bundle packaging fixes (see 730933db, f8b0a534) apply to +//! the Sandbox2 policy and its Buildkite pipeline respectively, not to this +//! file — carry those forward when that code is written instead of +//! rediscovering them. +inline constexpr int kLegacyBpfAllowedSyscalls[] { +#if defined(__x86_64__) + __NR_access, __NR_open, __NR_dup2, __NR_unlink, __NR_stat, __NR_lstat, + __NR_time, __NR_readlink, __NR_getdents, // for forecast temp storage + __NR_rmdir, // for forecast temp storage + __NR_mkdir, // for forecast temp storage + __NR_mknod, +#elif defined(__aarch64__) + __NR_faccessat, +#endif + __NR_fcntl, // for fdopendir + __NR_getrusage, + __NR_getpid, // for pthread_kill + ML_NR_statx, // for create_directories + __NR_getrandom, // for unique_path + __NR_mknodat, __NR_newfstatat, __NR_readlinkat, __NR_dup3, + __NR_getpriority, // for nice + __NR_setpriority, // for nice + __NR_read, __NR_write, __NR_writev, __NR_lseek, __NR_clock_gettime, + __NR_gettimeofday, __NR_fstat, __NR_close, __NR_connect, ML_NR_clone3, + __NR_clone, __NR_statfs, + __NR_mkdirat, // for forecast temp storage + __NR_unlinkat, // for forecast temp storage + __NR_getdents64, // for forecast temp storage + __NR_openat, // for forecast temp storage + __NR_tgkill, // for the crash handler + __NR_rt_sigaction, // for the crash handler + __NR_rt_sigreturn, + __NR_rt_sigprocmask, // for recent pthread_create + ML_NR_rseq, // for recent pthread_create + __NR_futex, __NR_madvise, __NR_nanosleep, __NR_set_robust_list, + __NR_mprotect, // for malloc arenas and pthread stacks + __NR_mremap, // for malloc arenas + __NR_munmap, // for malloc arenas + __NR_mmap, // for malloc arenas + __NR_getuid, __NR_exit_group, __NR_brk, __NR_exit, + __NR_prlimit64, // libtorch/Sandbox2-monitor query rlimits under load (03b1ee4a) +}; + +static_assert(std::size(kLegacyBpfAllowedSyscalls) <= 255, + "legacy BPF allowlist exceeds classic BPF jt (8-bit)"); + +inline std::vector legacyBpfAllowedSyscalls() { + return {kLegacyBpfAllowedSyscalls, + kLegacyBpfAllowedSyscalls + std::size(kLegacyBpfAllowedSyscalls)}; +} + +#endif // __linux__ + +} // namespace seccomp +} // namespace ml + +#endif // INCLUDED_ml_seccomp_CMlLegacyBpfSyscallAllowlist_h diff --git a/include/seccomp/CSeccompFilterBuilder.h b/include/seccomp/CSeccompFilterBuilder.h new file mode 100644 index 0000000000..bb0d7379e8 --- /dev/null +++ b/include/seccomp/CSeccompFilterBuilder.h @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the following additional limitation. Functionality enabled by the + * files subject to the Elastic License 2.0 may only be used in production when + * invoked by an Elasticsearch process with a license key installed that permits + * use of machine learning features. You may not use this file except in + * compliance with the Elastic License 2.0 and the foregoing additional + * limitation. + */ +#ifndef INCLUDED_ml_seccomp_CSeccompFilterBuilder_h +#define INCLUDED_ml_seccomp_CSeccompFilterBuilder_h + +#ifdef __linux__ + +#include + +#include + +namespace ml { +namespace seccomp { + +//! Builds a seccomp BPF program that allows exactly allowedSyscalls, on the +//! native architecture only, and denies everything else with EACCES. +//! +//! The caller supplies allowedSyscalls in any order: every generated jump +//! offset is derived from the vector's size and the row's own index, so +//! adding, removing or reordering a syscall never requires updating any +//! other row. This is the mechanism that lets CSystemCallFilter_Linux.cc +//! apply CMlLegacyBpfSyscallAllowlist.h's declaration directly, instead +//! of maintaining a second, hand-written BPF program with manual jump +//! offsets that can silently drift from the declaration. +std::vector buildSyscallAllowlistProgram(const std::vector& allowedSyscalls); +} +} + +#endif // __linux__ + +#endif // INCLUDED_ml_seccomp_CSeccompFilterBuilder_h diff --git a/include/seccomp/CSystemCallFilter.h b/include/seccomp/CSystemCallFilter.h index 9855d27002..7d98e7ecca 100644 --- a/include/seccomp/CSystemCallFilter.h +++ b/include/seccomp/CSystemCallFilter.h @@ -13,6 +13,8 @@ #include +#include + namespace ml { namespace seccomp { @@ -41,9 +43,85 @@ namespace seccomp { //! Windows: //! Job Objects prevent the process spawning another. //! +enum class ESystemCallFilterInstallOutcome { + E_Installed, + //! The platform mechanism itself is unavailable (e.g. kernel not built + //! with CONFIG_SECCOMP_FILTER). + E_MechanismUnavailable, + //! The mechanism is available but a required privilege-restriction step + //! failed (e.g. PR_SET_NO_NEW_PRIVS on Linux). + E_PrivilegeRestrictionFailed, + //! The mechanism is available but installing the filter/profile itself + //! failed. + E_FilterInstallFailed +}; + +//! Human-readable description of an install outcome, for diagnostics only; +//! not a stable machine-parsed value. +inline const char* describe(ESystemCallFilterInstallOutcome outcome) { + switch (outcome) { + case ESystemCallFilterInstallOutcome::E_Installed: + return "installed"; + case ESystemCallFilterInstallOutcome::E_MechanismUnavailable: + return "mechanism unavailable"; + case ESystemCallFilterInstallOutcome::E_PrivilegeRestrictionFailed: + return "privilege restriction failed"; + case ESystemCallFilterInstallOutcome::E_FilterInstallFailed: + return "filter install failed"; + } + return "unknown"; +} + +//! What a caller should do, given an install outcome and whether hard +//! termination is currently enabled at that call site. +enum class EDegradedModeAction { + E_ContinueDespiteFailure, + E_TerminateBeforeIo +}; + +//! Pure decision function: does this install outcome require terminating +//! before untrusted IO/model processing? +//! +//! terminateOnFailure is an internal switch, not an operator setting. Every +//! degraded-mode seccomp failure should eventually terminate before +//! processing, but flipping that on for every call site before the +//! ml-cpp/Elasticsearch controller protocol can guarantee a degraded-mode +//! launch was a deliberate operator choice would fail every launch on a +//! host lacking seccomp BPF, with no operator fallback setting to select +//! instead. Callers pass false today; a later change wires the real route +//! decision through this parameter once that guarantee exists. +inline EDegradedModeAction decideDegradedModeAction(ESystemCallFilterInstallOutcome outcome, + bool terminateOnFailure) { + if (outcome == ESystemCallFilterInstallOutcome::E_Installed || !terminateOnFailure) { + return EDegradedModeAction::E_ContinueDespiteFailure; + } + return EDegradedModeAction::E_TerminateBeforeIo; +} + +//! Structured signal a controller/Elasticsearch observer asserts to confirm +//! that a legacy/degraded-mode pytorch_inference launch actually installed +//! its in-process seccomp filter before processing untrusted model input. +//! Replaces attesting readiness by inference — "no fatal log line appeared +//! before initIo() ran" — with an explicit signal a test or observer can +//! assert on directly. Returns empty when installation did not succeed so +//! this marker can never falsely attest a filter that isn't there. +//! Terminate-before-initIo() applies only when decideDegradedModeAction() +//! is called with terminateOnFailure true (not today's production default). +//! Logged over the existing per-process log pipe; this is not a new startup +//! channel. +inline std::string degradedModeAttestationMarker(ESystemCallFilterInstallOutcome outcome) { + if (outcome != ESystemCallFilterInstallOutcome::E_Installed) { + return std::string(); + } + return R"({"ml_sandbox2_route":"legacy","event":"seccomp_installed"})"; +} + class CSystemCallFilter : private core::CNonInstantiatable { public: - static void installSystemCallFilter(); + //! Installs the platform syscall filter. Returns the typed outcome so a + //! caller can decide whether to continue or terminate; callers must not + //! silently discard the result (see decideDegradedModeAction()). + [[nodiscard]] static ESystemCallFilterInstallOutcome installSystemCallFilter(); }; } } diff --git a/lib/seccomp/CSystemCallFilter_Linux.cc b/lib/seccomp/CSystemCallFilter_Linux.cc index 466b58cd41..147ab94d40 100644 --- a/lib/seccomp/CSystemCallFilter_Linux.cc +++ b/lib/seccomp/CSystemCallFilter_Linux.cc @@ -8,10 +8,22 @@ * compliance with the Elastic License 2.0 and the foregoing additional * limitation. */ + +/* + * NOTE: This seccomp filter is being gradually replaced by Sandbox2 policies + * for processes that are spawned via CDetachedProcessSpawner. The allowed + * syscall set lives in CMlLegacyBpfSyscallAllowlist.h, the single + * machine-readable declaration this filter is generated from; a future + * Sandbox2 policy is expected to consume the same declaration for its + * explicit grants. + */ #include #include +#include +#include + #include #include #include @@ -30,125 +42,64 @@ namespace { // The old x32 ABI always has bit 30 set in the sys call numbers. // The x64 ABI should fail these calls const std::uint32_t UPPER_NR_LIMIT = 0x3FFFFFFF; +} + +std::vector buildSyscallAllowlistProgram(const std::vector& allowedSyscalls) { + const auto numSyscalls = static_cast(allowedSyscalls.size()); + + std::vector program; + program.reserve(numSyscalls + 6); -const struct sock_filter FILTER[] = { // Reject non-native ABIs before matching syscall numbers. Without this, // an x86_64 process can issue int 0x80 (i386) and hit number collisions — - // e.g. i386 socketcall (102) matches the allowlisted x86_64 getuid (102). - // Hardening in response to a privately reported ML seccomp-bypass finding. - // This prefix is self-contained (immediate RET on mismatch) so the relative - // jump offsets in the nr allowlist below are unchanged. - BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch)), + // e.g. i386 socketcall (102) matches an allowlisted x86_64 syscall with + // the same number. Hardening in response to a privately reported ML + // seccomp-bypass finding. This prefix is self-contained (immediate RET + // on mismatch), so it never affects the jump offsets below. + program.push_back( + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, arch))); #ifdef __x86_64__ - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0), + program.push_back(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 1, 0)); #elif defined(__aarch64__) - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0), + program.push_back(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_AARCH64, 1, 0)); +#else +#error Unsupported hardware architecture #endif - BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)), + program.push_back(BPF_STMT(BPF_RET | BPF_K, + SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA))); // Load the system call number into accumulator - BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), + program.push_back(BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr))); #ifdef __x86_64__ -// The statx, rseq and clone3 syscalls won't be defined on a RHEL/CentOS 7 build -// machine, but might exist on the kernel we run on -#ifndef __NR_statx -#define __NR_statx 332 -#endif -#ifndef __NR_rseq -#define __NR_rseq 334 -#endif -#ifndef __NR_clone3 -#define __NR_clone3 435 -#endif - // Only applies to x86_64 arch. Jump to disallow for calls using the x32 ABI - BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, UPPER_NR_LIMIT, 56, 0), - // If any sys call filters are added or removed then the jump - // destination for each statement including the one above must - // be updated accordingly - - // Allowed architecture-specific sys calls, jump to return allow on match - // Some of these are not used in latest glibc, and not supported in Linux - // kernels for recent architectures, but in a few cases different sys calls - // are used on different architectures - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_access, 56, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 55, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_dup2, 54, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_unlink, 53, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_stat, 52, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_lstat, 51, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_time, 50, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_readlink, 49, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getdents, 48, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rmdir, 47, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdir, 46, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mknod, 45, 0), -#elif defined(__aarch64__) -// The statx, rseq and clone3 syscalls won't be defined on a RHEL/CentOS 7 build -// machine, but might exist on the kernel we run on -#ifndef __NR_statx -#define __NR_statx 291 -#endif -#ifndef __NR_rseq -#define __NR_rseq 293 -#endif -#ifndef __NR_clone3 -#define __NR_clone3 435 -#endif - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_faccessat, 45, 0), -#else -#error Unsupported hardware architecture + // Jump to the deny row (immediately after the last syscall row below, + // i.e. numSyscalls rows ahead) for calls using the x32 ABI, without + // checking any allowlisted syscall. + program.push_back(BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, UPPER_NR_LIMIT, + static_cast(numSyscalls), 0)); #endif - // Allowed sys calls for all architectures, jump to return allow on match - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fcntl, 44, 0), // for fdopendir - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getrusage, 43, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpid, 42, 0), // for pthread_kill - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_statx, 41, 0), // for create_directories - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getrandom, 40, 0), // for unique_path - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mknodat, 39, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_newfstatat, 38, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_readlinkat, 37, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_dup3, 36, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getpriority, 35, 0), // for nice - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_setpriority, 34, 0), // for nice - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 33, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 32, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_writev, 31, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_lseek, 30, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clock_gettime, 29, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_gettimeofday, 28, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fstat, 27, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_close, 26, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_connect, 25, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone3, 24, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_clone, 23, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_statfs, 22, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdirat, 21, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_unlinkat, 20, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getdents64, 19, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 18, 0), // for forecast temp storage - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_tgkill, 17, 0), // for the crash handler - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigaction, 16, 0), // for the crash handler - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigreturn, 15, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rt_sigprocmask, 14, 0), // for recent pthread_create - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rseq, 13, 0), // for recent pthread_create - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_futex, 12, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_madvise, 11, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_nanosleep, 10, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_set_robust_list, 9, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mprotect, 8, 0), // for malloc arenas and pthread stacks - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mremap, 7, 0), // for malloc arenas - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_munmap, 6, 0), // for malloc arenas - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap, 5, 0), // for malloc arenas - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getuid, 4, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 3, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_brk, 2, 0), - BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit, 1, 0), + // Every syscall row jumps to the terminal SECCOMP_RET_ALLOW row on match. + // The jump distance is derived from the row's own index and the total + // count, so adding, removing or reordering an entry in allowedSyscalls + // never requires touching any other row. + for (std::uint32_t i = 0; i < numSyscalls; ++i) { + const auto jumpToAllow = static_cast(numSyscalls - i); + program.push_back(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, + static_cast(allowedSyscalls[i]), + jumpToAllow, 0)); + } + // Disallow call with error code EACCES - BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)), + program.push_back(BPF_STMT(BPF_RET | BPF_K, + SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA))); // Allow call - BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)}; + program.push_back(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)); + + return program; +} + +namespace { bool canUseSeccompBpf() { // This call is expected to fail due to the nullptr argument @@ -170,40 +121,44 @@ bool canUseSeccompBpf() { } } -void CSystemCallFilter::installSystemCallFilter() { - if (canUseSeccompBpf()) { - LOG_DEBUG(<< "Seccomp BPF filters available"); - - // Ensure more permissive privileges cannot be set in future. - // This must be set before installing the filter. - // PR_SET_NO_NEW_PRIVS was aded in kernel 3.5 - if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { - LOG_ERROR(<< "prctl PR_SET_NO_NEW_PRIVS failed: " << std::strerror(errno)); - return; - } - - struct sock_fprog prog = { - .len = static_cast(sizeof(FILTER) / sizeof(FILTER[0])), - .filter = const_cast(FILTER)}; - - // Install the filter. - // prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter) was introduced - // in kernel 3.5. This is functionally equivalent to - // seccomp(SECCOMP_SET_MODE_FILTER, 0, filter) which was added in - // kernel 3.17. We choose the older more compatible function. - // Note this precludes the use of calling seccomp() with the - // SECCOMP_FILTER_FLAG_TSYNC which is acceptable if the filter - // is installed by the main thread before any other threads are - // spawned. - if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { - LOG_ERROR(<< "Unable to install Seccomp BPF: " << std::strerror(errno)); - } else { - LOG_DEBUG(<< "Seccomp BPF installed"); - } - - } else { +ESystemCallFilterInstallOutcome CSystemCallFilter::installSystemCallFilter() { + if (canUseSeccompBpf() == false) { LOG_DEBUG(<< "Seccomp BPF not available"); + return ESystemCallFilterInstallOutcome::E_MechanismUnavailable; + } + LOG_DEBUG(<< "Seccomp BPF filters available"); + + // Ensure more permissive privileges cannot be set in future. + // This must be set before installing the filter. + // PR_SET_NO_NEW_PRIVS was added in kernel 3.5 + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { + LOG_ERROR(<< "prctl PR_SET_NO_NEW_PRIVS failed: " << std::strerror(errno)); + return ESystemCallFilterInstallOutcome::E_PrivilegeRestrictionFailed; } + + const std::vector program{ + buildSyscallAllowlistProgram(legacyBpfAllowedSyscalls())}; + + struct sock_fprog prog = {.len = static_cast(program.size()), + .filter = const_cast(program.data())}; + + // Install the filter. + // prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, filter) was introduced + // in kernel 3.5. This is functionally equivalent to + // seccomp(SECCOMP_SET_MODE_FILTER, 0, filter) which was added in + // kernel 3.17. We choose the older more compatible function. + // Note this precludes the use of calling seccomp() with the + // SECCOMP_FILTER_FLAG_TSYNC which is acceptable if the filter + // is installed by the main thread before any other threads are + // spawned. + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) { + LOG_ERROR(<< "Unable to install Seccomp BPF: " << std::strerror(errno)); + return ESystemCallFilterInstallOutcome::E_FilterInstallFailed; + } + + LOG_DEBUG(<< "Seccomp BPF installed"); + LOG_INFO(<< "ml.seccomp.installed"); + return ESystemCallFilterInstallOutcome::E_Installed; } } } diff --git a/lib/seccomp/CSystemCallFilter_MacOSX.cc b/lib/seccomp/CSystemCallFilter_MacOSX.cc index 3756875d06..92b5874764 100644 --- a/lib/seccomp/CSystemCallFilter_MacOSX.cc +++ b/lib/seccomp/CSystemCallFilter_MacOSX.cc @@ -87,13 +87,14 @@ std::string writeTempRulesFile() { } } -void CSystemCallFilter::installSystemCallFilter() { +ESystemCallFilterInstallOutcome CSystemCallFilter::installSystemCallFilter() { std::string profileFilename{writeTempRulesFile()}; if (profileFilename.empty()) { LOG_WARN(<< "Cannot write sandbox rules. macOS sandbox will not be initialized"); - return; + return ESystemCallFilterInstallOutcome::E_FilterInstallFailed; } + ESystemCallFilterInstallOutcome outcome{ESystemCallFilterInstallOutcome::E_Installed}; char* errorbuf{nullptr}; if (::sandbox_init(profileFilename.c_str(), SANDBOX_NAMED, &errorbuf) != 0) { std::string msg("Error initializing macOS sandbox"); @@ -103,11 +104,14 @@ void CSystemCallFilter::installSystemCallFilter() { ::sandbox_free_error(errorbuf); } LOG_ERROR(<< msg); + outcome = ESystemCallFilterInstallOutcome::E_FilterInstallFailed; } else { LOG_DEBUG(<< "macOS sandbox initialized"); + LOG_INFO(<< "ml.seccomp.installed"); } std::remove(profileFilename.c_str()); + return outcome; } } } diff --git a/lib/seccomp/CSystemCallFilter_Windows.cc b/lib/seccomp/CSystemCallFilter_Windows.cc index ce4924c629..16a3928f10 100644 --- a/lib/seccomp/CSystemCallFilter_Windows.cc +++ b/lib/seccomp/CSystemCallFilter_Windows.cc @@ -27,11 +27,11 @@ struct SCheckedHandle { }; } -void CSystemCallFilter::installSystemCallFilter() { +ESystemCallFilterInstallOutcome CSystemCallFilter::installSystemCallFilter() { HANDLE job = CreateJobObject(nullptr, nullptr); if (job == nullptr) { LOG_ERROR(<< "Failed to create Job Object: " << ml::core::CWindowsError()); - return; + return ESystemCallFilterInstallOutcome::E_MechanismUnavailable; } // The job is not destroyed until the handle is closed @@ -44,7 +44,7 @@ void CSystemCallFilter::installSystemCallFilter() { if (QueryInformationJobObject(job, JobObjectBasicLimitInformation, &limits, sizeof(limits), nullptr) == 0) { LOG_ERROR(<< "Error querying Job Object information: " << ml::core::CWindowsError()); - return; + return ESystemCallFilterInstallOutcome::E_FilterInstallFailed; } // Limit the number of active processes to 1 and @@ -54,16 +54,18 @@ void CSystemCallFilter::installSystemCallFilter() { if (SetInformationJobObject(job, JobObjectBasicLimitInformation, &limits, sizeof(limits)) == 0) { LOG_ERROR(<< "Error setting Job information: " << ml::core::CWindowsError()); - return; + return ESystemCallFilterInstallOutcome::E_FilterInstallFailed; } // Assign current process to the job if (AssignProcessToJobObject(job, GetCurrentProcess()) == 0) { LOG_ERROR(<< "Error assigning process to Job Object: " << ml::core::CWindowsError()); - return; + return ESystemCallFilterInstallOutcome::E_FilterInstallFailed; } LOG_DEBUG(<< "ActiveProcessLimit set to 1 for new Job Object"); + LOG_INFO(<< "ml.seccomp.installed"); + return ESystemCallFilterInstallOutcome::E_Installed; } } } diff --git a/lib/seccomp/unittest/CMakeLists.txt b/lib/seccomp/unittest/CMakeLists.txt index 7af78c795c..2656170e85 100644 --- a/lib/seccomp/unittest/CMakeLists.txt +++ b/lib/seccomp/unittest/CMakeLists.txt @@ -13,6 +13,7 @@ project("ML Seccomp unit tests") set (SRCS Main.cc + CSeccompFilterBuilderTest.cc CSystemCallFilterTest.cc ) diff --git a/lib/seccomp/unittest/CSeccompFilterBuilderTest.cc b/lib/seccomp/unittest/CSeccompFilterBuilderTest.cc new file mode 100644 index 0000000000..6be2e8e27d --- /dev/null +++ b/lib/seccomp/unittest/CSeccompFilterBuilderTest.cc @@ -0,0 +1,262 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the following additional limitation. Functionality enabled by the + * files subject to the Elastic License 2.0 may only be used in production when + * invoked by an Elasticsearch process with a license key installed that permits + * use of machine learning features. You may not use this file except in + * compliance with the Elastic License 2.0 and the foregoing additional + * limitation. + */ + +#include + +#include + +#include + +#ifdef __linux__ + +// These must be included before BOOST_AUTO_TEST_SUITE() opens a namespace: +// BOOST_AUTO_TEST_SUITE(name) expands to `namespace name { ... }`, so any +// #include placed after it would get its declarations nested inside that +// namespace instead of at global scope, shadowing ::ml::seccomp with an +// incomplete duplicate. +#include +#include + +#include +#include +#include +#include +#include + +#endif // __linux__ + +BOOST_AUTO_TEST_SUITE(CSeccompFilterBuilderTest) + +#ifdef __linux__ + +namespace { + +//! Decodes the syscall numbers this builder actually applies, by walking the +//! generated program rather than re-reading the declaration it was built +//! from. This is the proof that the applied program matches the +//! declaration, not a comparison of two independently maintained lists. +//! +//! Rows before the syscall-number load (the arch load/check prefix) also use +//! BPF_JMP|BPF_JEQ|BPF_K, so decoding starts only once that load is seen. +std::set decodeAppliedSyscalls(const std::vector& program) { + std::set applied; + bool sawNrLoad{false}; + for (const auto& instr : program) { + if (instr.code == (BPF_LD | BPF_W | BPF_ABS) && + instr.k == offsetof(struct seccomp_data, nr)) { + sawNrLoad = true; + continue; + } + if (sawNrLoad && instr.code == (BPF_JMP | BPF_JEQ | BPF_K) && instr.jt > 0) { + applied.insert(static_cast(instr.k)); + } + } + return applied; +} + +} // namespace + +BOOST_AUTO_TEST_CASE(testAppliedProgramMatchesDeclaration) { + const std::vector declared{ml::seccomp::legacyBpfAllowedSyscalls()}; + const std::vector program{ml::seccomp::buildSyscallAllowlistProgram(declared)}; + + const std::set declaredSet{declared.begin(), declared.end()}; + BOOST_REQUIRE_EQUAL(declaredSet.size(), declared.size()); // declaration has no duplicates + const std::set appliedSet{decodeAppliedSyscalls(program)}; + BOOST_REQUIRE_EQUAL_COLLECTIONS(declaredSet.begin(), declaredSet.end(), + appliedSet.begin(), appliedSet.end()); + + // Structural invariants that must hold regardless of declaration content: + // native-arch load/check, syscall-number load, and a final deny/allow + // pair. No index into this vector is hand-maintained anywhere in + // production code. + BOOST_TEST_REQUIRE(program.size() >= declared.size() + 4); + BOOST_REQUIRE_EQUAL(static_cast(BPF_RET | BPF_K), + static_cast(program.back().code)); + BOOST_REQUIRE_EQUAL(static_cast(SECCOMP_RET_ALLOW), + program.back().k); + const auto& denyRow = program[program.size() - 2]; + BOOST_REQUIRE_EQUAL(static_cast(BPF_RET | BPF_K), + static_cast(denyRow.code)); + BOOST_TEST_REQUIRE(denyRow.k != SECCOMP_RET_ALLOW); +} + +BOOST_AUTO_TEST_CASE(testJumpOffsetsAreDerivedNotHandMaintained) { + // An arbitrary, deliberately unordered and out-of-production-order list. + // If any jump offset were hand-maintained rather than derived from the + // vector's size/index, reordering or resizing this list would desync it + // from the generated rows; this test would fail with a stale allowlist + // but pass immediately once regenerated, which is exactly the property + // "no manual BPF jump offsets remain" requires. + const std::vector arbitrarySyscalls{200, 1, 57, 9, 300}; + const std::vector program{ + ml::seccomp::buildSyscallAllowlistProgram(arbitrarySyscalls)}; + + const std::size_t allowIndex{program.size() - 1}; + const std::size_t denyIndex{program.size() - 2}; + BOOST_REQUIRE_EQUAL(static_cast(SECCOMP_RET_ALLOW), + program[allowIndex].k); + BOOST_TEST_REQUIRE(program[denyIndex].k != SECCOMP_RET_ALLOW); + + // Every syscall row's own jt must land exactly on the allow row: for a + // row at absolute index i, i + jt + 1 == allowIndex. The arch load/check + // prefix also uses BPF_JMP|BPF_JEQ|BPF_K but targets the nr-load + // instruction, not the allow row, so decoding starts only after the + // syscall-number load is seen (mirrors decodeAppliedSyscalls() above). + std::set foundSyscalls; + bool sawNrLoad{false}; + for (std::size_t i = 0; i < program.size(); ++i) { + if (program[i].code == (BPF_LD | BPF_W | BPF_ABS) && + program[i].k == offsetof(struct seccomp_data, nr)) { + sawNrLoad = true; + continue; + } + if (sawNrLoad && program[i].code == (BPF_JMP | BPF_JEQ | BPF_K) && + program[i].jt > 0) { + BOOST_REQUIRE_EQUAL(allowIndex, i + program[i].jt + 1); + foundSyscalls.insert(static_cast(program[i].k)); + } + } + const std::set expected{arbitrarySyscalls.begin(), arbitrarySyscalls.end()}; + BOOST_REQUIRE_EQUAL_COLLECTIONS(expected.begin(), expected.end(), + foundSyscalls.begin(), foundSyscalls.end()); + +#ifdef __x86_64__ + // The x32-ABI guard must jump to the deny row (numSyscalls rows ahead of + // the JGT instruction), not hand-maintained like the old static FILTER[]. + constexpr std::uint32_t upperNrLimit{0x3FFFFFFF}; + bool sawX32Guard{false}; + sawNrLoad = false; + for (std::size_t i = 0; i < program.size(); ++i) { + if (program[i].code == (BPF_LD | BPF_W | BPF_ABS) && + program[i].k == offsetof(struct seccomp_data, nr)) { + sawNrLoad = true; + continue; + } + if (sawNrLoad && program[i].code == (BPF_JMP | BPF_JGT | BPF_K)) { + BOOST_REQUIRE_EQUAL(upperNrLimit, program[i].k); + BOOST_REQUIRE_EQUAL(denyIndex, i + program[i].jt + 1); + sawX32Guard = true; + break; + } + } + BOOST_TEST_REQUIRE(sawX32Guard); +#endif +} + +BOOST_AUTO_TEST_CASE(testArchGuardRejectsNonNativeAbi) { + const std::vector program{ + ml::seccomp::buildSyscallAllowlistProgram(std::vector{1})}; + + BOOST_TEST_REQUIRE(program.size() >= 3); + BOOST_REQUIRE_EQUAL(static_cast(BPF_LD | BPF_W | BPF_ABS), + static_cast(program[0].code)); + BOOST_REQUIRE_EQUAL(static_cast(offsetof(struct seccomp_data, arch)), + program[0].k); + BOOST_REQUIRE_EQUAL(static_cast(BPF_JMP | BPF_JEQ | BPF_K), + static_cast(program[1].code)); +#ifdef __x86_64__ + BOOST_REQUIRE_EQUAL(static_cast(AUDIT_ARCH_X86_64), program[1].k); +#elif defined(__aarch64__) + BOOST_REQUIRE_EQUAL(static_cast(AUDIT_ARCH_AARCH64), + program[1].k); +#endif + BOOST_REQUIRE_EQUAL(static_cast(BPF_RET | BPF_K), + static_cast(program[2].code)); + BOOST_TEST_REQUIRE(program[2].k != SECCOMP_RET_ALLOW); +} + +BOOST_AUTO_TEST_CASE(testCarryForwardSyscallsPresent) { + // This declaration must + // not silently drop pytorch_inference/libtorch compatibility fixes. + // Each assertion below is a named regression test for one carried-forward + // fix within this file's scope. + const std::vector syscalls{ml::seccomp::legacyBpfAllowedSyscalls()}; + const std::set declared{syscalls.begin(), syscalls.end()}; + + // 57f00ed1b: clone3 must be allowed by its literal syscall number (435 on + // both x86_64 and aarch64), not only via __NR_clone3, because some build + // images' kernel headers predate clone3 while the runtime glibc uses it. + BOOST_TEST_REQUIRE(declared.count(435) == 1); + + // 03b1ee4a: prlimit64, queried by libtorch/the Sandbox2 monitor under + // sustained load. + BOOST_TEST_REQUIRE(declared.count(__NR_prlimit64) == 1); + +#ifdef __x86_64__ + // ec7d3ed85: glibc's x86_64 file-system wrappers issue these legacy + // syscalls (not their *at equivalents) when pytorch_inference creates + // and tears down its named pipes. + const int legacyFsSyscalls[]{__NR_mknod, __NR_unlink, __NR_rmdir, + __NR_mkdir, __NR_readlink, __NR_access, + __NR_dup2}; + for (int nr : legacyFsSyscalls) { + BOOST_TEST_REQUIRE(declared.count(nr) == 1); + } +#endif +} + +#endif // __linux__ + +BOOST_AUTO_TEST_CASE(testDegradedModeAttestationMarker) { + using ml::seccomp::ESystemCallFilterInstallOutcome; + using ml::seccomp::degradedModeAttestationMarker; + + // The marker must be present and exact on success - this is what a + // controller/Elasticsearch observer asserts, replacing "no fatal log + // line appeared" as an implicit readiness signal. + BOOST_REQUIRE_EQUAL( + std::string("{\"ml_sandbox2_route\":\"legacy\",\"event\":\"seccomp_installed\"}"), + degradedModeAttestationMarker(ESystemCallFilterInstallOutcome::E_Installed)); + + // Every failure class must attest nothing - a caller that logged this + // marker on a failed install would falsely claim protection that isn't + // there. + BOOST_TEST_REQUIRE(degradedModeAttestationMarker(ESystemCallFilterInstallOutcome::E_MechanismUnavailable) + .empty()); + BOOST_TEST_REQUIRE(degradedModeAttestationMarker(ESystemCallFilterInstallOutcome::E_PrivilegeRestrictionFailed) + .empty()); + BOOST_TEST_REQUIRE(degradedModeAttestationMarker(ESystemCallFilterInstallOutcome::E_FilterInstallFailed) + .empty()); +} + +BOOST_AUTO_TEST_CASE(testDecideDegradedModeActionFaultInjection) { + using ml::seccomp::EDegradedModeAction; + using ml::seccomp::ESystemCallFilterInstallOutcome; + using ml::seccomp::decideDegradedModeAction; + + // Successful installation never terminates, regardless of the switch. + BOOST_REQUIRE_EQUAL(static_cast(EDegradedModeAction::E_ContinueDespiteFailure), + static_cast(decideDegradedModeAction( + ESystemCallFilterInstallOutcome::E_Installed, false))); + BOOST_REQUIRE_EQUAL(static_cast(EDegradedModeAction::E_ContinueDespiteFailure), + static_cast(decideDegradedModeAction( + ESystemCallFilterInstallOutcome::E_Installed, true))); + + // Every fault-injected failure class - capability probe failure, + // PR_SET_NO_NEW_PRIVS, and filter installation - with the internal + // switch off (today's production default), every call site continues; + // with it on (the behaviour a later change activates), every one + // terminates. + const ESystemCallFilterInstallOutcome failureModes[]{ + ESystemCallFilterInstallOutcome::E_MechanismUnavailable, + ESystemCallFilterInstallOutcome::E_PrivilegeRestrictionFailed, + ESystemCallFilterInstallOutcome::E_FilterInstallFailed}; + + for (const auto outcome : failureModes) { + BOOST_REQUIRE_EQUAL(static_cast(EDegradedModeAction::E_ContinueDespiteFailure), + static_cast(decideDegradedModeAction(outcome, false))); + BOOST_REQUIRE_EQUAL(static_cast(EDegradedModeAction::E_TerminateBeforeIo), + static_cast(decideDegradedModeAction(outcome, true))); + } +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.cc b/lib/seccomp/unittest/CSystemCallFilterTest.cc index a9024673d1..dd3983571f 100644 --- a/lib/seccomp/unittest/CSystemCallFilterTest.cc +++ b/lib/seccomp/unittest/CSystemCallFilterTest.cc @@ -275,7 +275,9 @@ BOOST_AUTO_TEST_CASE(testSystemCallFilter) { #endif // Install the filter - ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + BOOST_REQUIRE_EQUAL( + static_cast(ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed), + static_cast(ml::seccomp::CSystemCallFilter::installSystemCallFilter())); #if defined(Linux) && defined(__x86_64__) if (i386CompatUsable) {