diff --git a/bin/autoconfig/Main.cc b/bin/autoconfig/Main.cc index d6d1e8eec2..d99b34a6e0 100644 --- a/bin/autoconfig/Main.cc +++ b/bin/autoconfig/Main.cc @@ -28,6 +28,8 @@ #include #include +#include + #include "CCmdLineParser.h" #include @@ -76,6 +78,8 @@ int main(int argc, char** argv) { ml::core::CProcessPriority::reducePriority(); + ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); return EXIT_FAILURE; diff --git a/bin/autodetect/Main.cc b/bin/autodetect/Main.cc index 2465393960..550500942e 100644 --- a/bin/autodetect/Main.cc +++ b/bin/autodetect/Main.cc @@ -42,6 +42,8 @@ #include #include +#include + #include "CCmdLineParser.h" #include @@ -120,6 +122,8 @@ int main(int argc, char** argv) { ml::core::CProcessPriority::reducePriority(); + ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); return EXIT_FAILURE; diff --git a/bin/categorize/Main.cc b/bin/categorize/Main.cc index 266aef3601..99d59daf9a 100644 --- a/bin/categorize/Main.cc +++ b/bin/categorize/Main.cc @@ -38,6 +38,8 @@ #include #include +#include + #include "CCmdLineParser.h" #include @@ -91,6 +93,8 @@ int main(int argc, char** argv) { ml::core::CProcessPriority::reducePriority(); + ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); return EXIT_FAILURE; diff --git a/bin/normalize/Main.cc b/bin/normalize/Main.cc index 7773cb97a0..2fb6e7ee40 100644 --- a/bin/normalize/Main.cc +++ b/bin/normalize/Main.cc @@ -28,6 +28,8 @@ #include #include +#include + #include "CCmdLineParser.h" #include @@ -78,6 +80,8 @@ int main(int argc, char** argv) { ml::core::CProcessPriority::reducePriority(); + ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + if (ioMgr.initIo() == false) { LOG_FATAL(<< "Failed to initialise IO"); return EXIT_FAILURE; diff --git a/docs/CHANGELOG.asciidoc b/docs/CHANGELOG.asciidoc index 7637fcc359..e6854d4f90 100644 --- a/docs/CHANGELOG.asciidoc +++ b/docs/CHANGELOG.asciidoc @@ -11,7 +11,7 @@ === Deprecations -=== New Features +=== New Features === Enhancements @@ -25,6 +25,10 @@ Reduce model memory by storing state for periodicity testing in a compressed for Forecasting of Machine Learning job time series is now supported for large jobs by temporarily storing model state on disk ({pull}89[#89]) +Secure the ML processes by preventing system calls such as fork and exec. The Linux implemenation uses +Seccomp BPF to intercept system calls and is available in kernels since 3.5. On Windows Job Objects prevent +new processes being created and macOS uses the sandbox functionality ({pull}98[#98]) + === Bug Fixes Age seasonal components in proportion to the fraction of values with which they're updated ({pull}88[#88]) diff --git a/include/seccomp/CSystemCallFilter.h b/include/seccomp/CSystemCallFilter.h new file mode 100644 index 0000000000..89da6d8add --- /dev/null +++ b/include/seccomp/CSystemCallFilter.h @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#ifndef INCLUDED_ml_seccomp_CSystemCallFilter_h +#define INCLUDED_ml_seccomp_CSystemCallFilter_h + +#include + +namespace ml { +namespace seccomp { + +//! \brief +//! Installs secure computing modes for Linux, macOs and Windows +//! +//! DESCRIPTION:\n +//! ML processes require a subset of system calls to function correctly. +//! These are create a named pipe, connect to a named pipe, read and write +//! no other system calls are necessary and should be resticted to prevent +//! malicious actions. +//! +//! IMPLEMENTATION DECISIONS:\n +//! Implementations are platform specific more details can be found in the +//! particular .cc files. +//! +//! Linux: +//! Seccomp BPF is used to restrict system calls on kernels since 3.5. +//! +//! macOs: +//! The sandbox facility is used to restict access to system resources. +//! +//! Windows: +//! Job Objects prevent the process spawning another. +//! +class CSystemCallFilter : private core::CNonInstantiatable { +public: + static void installSystemCallFilter(); +}; +} +} + +#endif // INCLUDED_ml_seccomp_CSystemCallFilter_h diff --git a/lib/Makefile b/lib/Makefile index 7b2027d089..95c7e998c2 100644 --- a/lib/Makefile +++ b/lib/Makefile @@ -18,6 +18,8 @@ COMPONENTS= \ test \ api \ config \ + seccomp \ + include $(CPP_SRC_HOME)/mk/toplevel.mk diff --git a/lib/api/CForecastRunner.cc b/lib/api/CForecastRunner.cc index 3da791f407..5927002974 100644 --- a/lib/api/CForecastRunner.cc +++ b/lib/api/CForecastRunner.cc @@ -350,6 +350,10 @@ bool CForecastRunner::pushForecastJob(const std::string& controlMessage, LOG_INFO(<< "Forecast of large model requested (requires " << std::to_string(1 + (totalMemoryUsage >> 20)) << " MB), using disk."); + // create a subdirectory using the unique forecast id + temporaryFolder /= forecastJob.s_ForecastId; + forecastJob.s_TemporaryFolder = temporaryFolder.string(); + boost::system::error_code errorCode; boost::filesystem::create_directories(temporaryFolder, errorCode); if (errorCode) { diff --git a/lib/seccomp/.objs/.gitignore b/lib/seccomp/.objs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/seccomp/CSystemCallFilter_Linux.cc b/lib/seccomp/CSystemCallFilter_Linux.cc new file mode 100644 index 0000000000..cdb287c018 --- /dev/null +++ b/lib/seccomp/CSystemCallFilter_Linux.cc @@ -0,0 +1,156 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include "seccomp/CSystemCallFilter.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace ml { +namespace seccomp { + +namespace { +// The old x32 ABI always has bit 30 set in the sys call numbers. +// The x64 architecture should fail these calls +const std::uint32_t UPPER_NR_LIMIT = 0x3FFFFFFF; + +// Offset to the nr field in struct seccomp_data +const std::uint32_t SECCOMP_DATA_NR_OFFSET = 0x00; +// Offset to the arch field in struct seccomp_data +const std::uint32_t SECCOMP_DATA_ARCH_OFFSET = 0x04; + +// Copied from seccomp.h +// seccomp.h cannot be included as it was added in Linux kernel 3.17 +// and this must build on older versions. +// TODO: remove on the minumum build kernel version supports seccomp +#define SECCOMP_MODE_FILTER 2 +#define SECCOMP_RET_ERRNO 0x00050000U +#define SECCOMP_RET_ALLOW 0x7fff0000U +#define SECCOMP_RET_DATA 0x0000ffffU + +// Added in Linux 3.5 +#ifndef PR_SET_NO_NEW_PRIVS +#define PR_SET_NO_NEW_PRIVS 38 +#endif + +const struct sock_filter FILTER[] = { + // Load architecture from 'seccomp_data' buffer into accumulator + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, SECCOMP_DATA_ARCH_OFFSET), + // Jump to disallow if architecture is not X86_64 + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 5), + // Load the system call number into accumulator + BPF_STMT(BPF_LD | BPF_W | BPF_ABS, SECCOMP_DATA_NR_OFFSET), + // 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, 35, 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 sys calls, jump to return allow on match + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 35, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 34, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_writev, 33, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_lseek, 32, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_lstat, 31, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_readlink, 30, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_stat, 29, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_fstat, 28, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 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_clone, 24, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_statfs, 23, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_dup2, 22, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdir, 21, 0), // for forecast temp storage + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_rmdir, 20, 0), // for forecast temp storage + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getdents, 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_futex, 14, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_madvise, 13, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_unlink, 12, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mknod, 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), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_munmap, 7, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap, 6, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_getuid, 5, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_exit_group, 4, 0), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_access, 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), + // Disallow call with error code EACCES + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | (EACCES & SECCOMP_RET_DATA)), + // Allow call + BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW)}; + +bool canUseSeccompBpf() { + // This call is expected to fail due to the nullptr argument + // but the failure mode informs us if the kernel was configured + // with CONFIG_SECCOMP_FILTER + // http://man7.org/linux/man-pages/man2/prctl.2.html + int result = prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, nullptr); + int configError = errno; + if (result != -1) { + LOG_ERROR(<< "prctl set seccomp with null argument should have failed"); + return false; + } + + // If the kernel is not configured with CONFIG_SECCOMP_FILTER + // or CONFIG_SECCOMP the error is EINVAL. EFAULT indicates the + // seccomp filters are enabled but the 3rd argument (nullptr) + // was invalid. + return configError == EFAULT; +} +} + +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 { + LOG_DEBUG(<< "Seccomp BPF not available"); + } +} +} +} diff --git a/lib/seccomp/CSystemCallFilter_MacOSX.cc b/lib/seccomp/CSystemCallFilter_MacOSX.cc new file mode 100644 index 0000000000..118cca8600 --- /dev/null +++ b/lib/seccomp/CSystemCallFilter_MacOSX.cc @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include "seccomp/CSystemCallFilter.h" + +#include + +#include +#include +#include + +#include +#include + +namespace ml { +namespace seccomp { + +namespace { +// The Sandbox rules deny all actions apart from creating fifos, +// opening files, reading and writing. +// (allow file-write*) is required for mkfifo and that permission +// can not be set using the more granular controls. +const std::string SANDBOX_RULES("\ + (version 1) \ + (deny default) \ + (allow file-read*) \ + (allow file-read-data) \ + (allow file-write*) \ + (allow file-write-data)"); + +// mkstemps will replace the Xs with random characters +const std::string FILE_NAME_TEMPLATE("ml.XXXXXX.sb"); +// The length of the suffix '.sb' +const int FILE_NAME_TEMPLATE_SUFFIX_LEN = 3; + +std::string getTempDir() { + // Prefer to use the temporary directory set by the Elasticsearch JVM + const char* tmpDir(::getenv("TMPDIR")); + + // If TMPDIR is not set use _PATH_VARTMP + std::string path((tmpDir == nullptr) ? _PATH_VARTMP : tmpDir); + // Make sure path ends with a slash so it's ready to have a file name appended + if (path[path.length() - 1] != '/') { + path += '/'; + } + return path; +} + +std::string writeTempRulesFile() { + std::string profileFilename = getTempDir() + FILE_NAME_TEMPLATE; + + // Create and open a temporary file with a random name + // profileFilename is updated with the new filename. + int fd = mkstemps(&profileFilename[0], FILE_NAME_TEMPLATE_SUFFIX_LEN); + if (fd == -1) { + LOG_ERROR(<< "Opening a temporary file with mkstemps failed: " + << std::strerror(errno)); + return std::string(); + } + write(fd, SANDBOX_RULES.c_str(), SANDBOX_RULES.size()); + close(fd); + + return profileFilename; +} +} + +void CSystemCallFilter::installSystemCallFilter() { + std::string profileFilename = writeTempRulesFile(); + if (profileFilename.empty()) { + LOG_WARN(<< "Cannot write sandbox rules. macOS sandbox will not be initialized"); + return; + } + + char* errorbuf = nullptr; + if (sandbox_init(profileFilename.c_str(), SANDBOX_NAMED, &errorbuf) != 0) { + std::string msg("Error initializing macOS sandbox"); + if (errorbuf != nullptr) { + msg += ": "; + msg += errorbuf; + sandbox_free_error(errorbuf); + } + LOG_ERROR(<< msg); + } else { + LOG_DEBUG(<< "macOS sandbox initialized"); + } + + std::remove(profileFilename.c_str()); +} +} +} diff --git a/lib/seccomp/CSystemCallFilter_Windows.cc b/lib/seccomp/CSystemCallFilter_Windows.cc new file mode 100644 index 0000000000..2242304803 --- /dev/null +++ b/lib/seccomp/CSystemCallFilter_Windows.cc @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include "seccomp/CSystemCallFilter.h" + +#include +#include +#include + +namespace ml { +namespace seccomp { + +namespace { + +struct SCheckedHandle { + SCheckedHandle(HANDLE handle) : s_Handle(handle) {} + ~SCheckedHandle() { CloseHandle(s_Handle); } + + HANDLE s_Handle; +}; +} + +void CSystemCallFilter::installSystemCallFilter() { + HANDLE job = CreateJobObject(nullptr, nullptr); + if (job == nullptr) { + LOG_ERROR(<< "Failed to create Job Object: " << ml::core::CWindowsError()); + return; + } + + // The job is not destroyed until the handle is closed + // and all processes have exited. + SCheckedHandle jobHandle(job); + + JOBOBJECT_BASIC_LIMIT_INFORMATION limits; + + // Get the current job information + if (QueryInformationJobObject(job, JobObjectBasicLimitInformation, &limits, + sizeof(limits), nullptr) == 0) { + LOG_ERROR(<< "Error querying Job Object information: " << ml::core::CWindowsError()); + return; + } + + // Limit the number of active processes to 1 and + // flag that the limit is set + limits.ActiveProcessLimit = uint32_t{1}; + limits.LimitFlags = limits.LimitFlags | JOB_OBJECT_LIMIT_ACTIVE_PROCESS; + if (SetInformationJobObject(job, JobObjectBasicLimitInformation, &limits, + sizeof(limits)) == 0) { + LOG_ERROR(<< "Error setting Job information: " << ml::core::CWindowsError()); + return; + } + + // Assign current process to the job + if (AssignProcessToJobObject(job, GetCurrentProcess()) == 0) { + LOG_ERROR(<< "Error assigning process to Job Object: " << ml::core::CWindowsError()); + return; + } + + LOG_DEBUG(<< "ActiveProcessLimit set to 1 for new Job Object"); +} +} +} diff --git a/lib/seccomp/Makefile b/lib/seccomp/Makefile new file mode 100644 index 0000000000..b84a1edabe --- /dev/null +++ b/lib/seccomp/Makefile @@ -0,0 +1,19 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# +include $(CPP_SRC_HOME)/mk/defines.mk + +TARGET=$(OBJS_DIR)/libMlSeccomp$(STATIC_LIB_EXT) + + +all: build + +PLATFORM_SRCS= \ +CSystemCallFilter.cc \ + +SRCS= \ +$(OS_SRCS) + +include $(CPP_SRC_HOME)/mk/staticlib.mk diff --git a/lib/seccomp/unittest/.objs/.gitignore b/lib/seccomp/unittest/.objs/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.cc b/lib/seccomp/unittest/CSystemCallFilterTest.cc new file mode 100644 index 0000000000..31606c5ddd --- /dev/null +++ b/lib/seccomp/unittest/CSystemCallFilterTest.cc @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include "CSystemCallFilterTest.h" + +#include +#include +#include +#include +#ifdef Linux +#include +#include +#endif + +#include + +#include + +#include +#include + +#include +#include + +namespace { + +const uint32_t SLEEP_TIME_MS = 100; +const size_t TEST_SIZE = 10000; +const size_t MAX_ATTEMPTS = 20; +const char TEST_CHAR = 'a'; +#ifdef Windows +const char* const TEST_READ_PIPE_NAME = "\\\\.\\pipe\\testreadpipe"; +const char* const TEST_WRITE_PIPE_NAME = "\\\\.\\pipe\\testwritepipe"; +#else +const char* const TEST_READ_PIPE_NAME = "testreadpipe"; +const char* const TEST_WRITE_PIPE_NAME = "testwritepipe"; +#endif + +class CNamedPipeWriter : public ml::core::CThread { +public: + CNamedPipeWriter(const std::string& fileName, size_t size) + : m_FileName(fileName), m_Size(size) {} + +protected: + virtual void run() { + // Wait for the file to exist + ml::core::CSleep::sleep(SLEEP_TIME_MS); + + std::ofstream strm(m_FileName.c_str()); + for (size_t i = 0; i < m_Size && strm.good(); ++i) { + strm << TEST_CHAR; + } + } + + virtual void shutdown() {} + +private: + std::string m_FileName; + size_t m_Size; +}; + +class CNamedPipeReader : public ml::core::CThread { +public: + CNamedPipeReader(const std::string& fileName) : m_FileName(fileName) {} + + const std::string& data() const { return m_Data; } + +protected: + virtual void run() { + m_Data.clear(); + + std::ifstream strm; + + // Try to open the file repeatedly to allow time for the other + // thread to create it + size_t attempt(1); + do { + CPPUNIT_ASSERT(attempt++ <= MAX_ATTEMPTS); + ml::core::CSleep::sleep(SLEEP_TIME_MS); + strm.open(m_FileName.c_str()); + } while (!strm.is_open()); + + static const std::streamsize BUF_SIZE = 512; + char buffer[BUF_SIZE]; + while (strm.good()) { + strm.read(buffer, BUF_SIZE); + CPPUNIT_ASSERT(!strm.bad()); + if (strm.gcount() > 0) { + // This code deals with the test character we write to + // detect the short-lived connection problem on Windows + const char* copyFrom = buffer; + size_t copyLen = static_cast(strm.gcount()); + if (m_Data.empty() && *buffer == ml::core::CNamedPipeFactory::TEST_CHAR) { + ++copyFrom; + --copyLen; + } + if (copyLen > 0) { + m_Data.append(copyFrom, copyLen); + } + } + } + } + + virtual void shutdown() {} + +private: + std::string m_FileName; + std::string m_Data; +}; + +bool systemCall() { + return std::system("hostname") == 0; +} + +#ifdef Linux +bool versionIsBefore3_5(std::int64_t major, std::int64_t minor) { + if (major < 3) { + return true; + } + if (major == 3 && minor < 5) { + return true; + } + return false; +} +#endif +} + +CppUnit::Test* CSystemCallFilterTest::suite() { + CppUnit::TestSuite* suiteOfTests = new CppUnit::TestSuite("CSystemCallFilterTest"); + + suiteOfTests->addTest(new CppUnit::TestCaller( + "CSystemCallFilterTest::testSystemCallFilter", + &CSystemCallFilterTest::testSystemCallFilter)); + + return suiteOfTests; +} + +void CSystemCallFilterTest::testSystemCallFilter() { +#ifdef Linux + std::string release{ml::core::CUname::release()}; + ml::core::CRegex semVersion; + CPPUNIT_ASSERT(semVersion.init("(\\d)\\.(\\d{1,2})\\.(\\d{1,2}).*")); + ml::core::CRegex::TStrVec tokens; + CPPUNIT_ASSERT(semVersion.tokenise(release, tokens)); + // Seccomp is available in kernels since 3.5 + + std::int64_t major = std::stoi(tokens[0]); + std::int64_t minor = std::stoi(tokens[1]); + if (versionIsBefore3_5(major, minor)) { + LOG_INFO(<< "Cannot test seccomp on linux kernels before 3.5"); + return; + } +#endif // Linux + + // Ensure actions are not prohibited before the + // system call filters are applied + CPPUNIT_ASSERT(systemCall()); + + // Install the filter + ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + + CPPUNIT_ASSERT_ASSERTION_FAIL_MESSAGE("Calling std::system should fail", + CPPUNIT_ASSERT(systemCall())); + +#ifdef Windows + const std::string readPipeName{TEST_READ_PIPE_NAME}; + const std::string writePipeName{TEST_WRITE_PIPE_NAME}; +#else + const std::string readPipeName{ml::test::CTestTmpDir::tmpDir() + "/" + TEST_READ_PIPE_NAME}; + const std::string writePipeName{ml::test::CTestTmpDir::tmpDir() + "/" + TEST_WRITE_PIPE_NAME}; +#endif + + // Operations that must function after seccomp is initialised + openPipeAndRead(readPipeName); + openPipeAndWrite(writePipeName); + + makeAndRemoveDirectory(ml::test::CTestTmpDir::tmpDir()); +} + +void CSystemCallFilterTest::openPipeAndRead(const std::string& filename) { + + CNamedPipeWriter threadWriter(filename, TEST_SIZE); + CPPUNIT_ASSERT(threadWriter.start()); + + ml::core::CNamedPipeFactory::TIStreamP strm = + ml::core::CNamedPipeFactory::openPipeStreamRead(filename); + CPPUNIT_ASSERT(strm); + + static const std::streamsize BUF_SIZE = 512; + std::string readData; + readData.reserve(TEST_SIZE); + char buffer[BUF_SIZE]; + do { + strm->read(buffer, BUF_SIZE); + CPPUNIT_ASSERT(!strm->bad()); + if (strm->gcount() > 0) { + readData.append(buffer, static_cast(strm->gcount())); + } + } while (!strm->eof()); + + CPPUNIT_ASSERT_EQUAL(TEST_SIZE, readData.length()); + CPPUNIT_ASSERT_EQUAL(std::string(TEST_SIZE, TEST_CHAR), readData); + + CPPUNIT_ASSERT(threadWriter.stop()); + + strm.reset(); +} + +void CSystemCallFilterTest::openPipeAndWrite(const std::string& filename) { + CNamedPipeReader threadReader(filename); + CPPUNIT_ASSERT(threadReader.start()); + + ml::core::CNamedPipeFactory::TOStreamP strm = + ml::core::CNamedPipeFactory::openPipeStreamWrite(filename); + CPPUNIT_ASSERT(strm); + + size_t charsLeft(TEST_SIZE); + size_t blockSize(7); + while (charsLeft > 0) { + if (blockSize > charsLeft) { + blockSize = charsLeft; + } + (*strm) << std::string(blockSize, TEST_CHAR); + CPPUNIT_ASSERT(!strm->bad()); + charsLeft -= blockSize; + } + + strm.reset(); + + CPPUNIT_ASSERT(threadReader.stop()); + + CPPUNIT_ASSERT_EQUAL(TEST_SIZE, threadReader.data().length()); + CPPUNIT_ASSERT_EQUAL(std::string(TEST_SIZE, TEST_CHAR), threadReader.data()); +} + +void CSystemCallFilterTest::makeAndRemoveDirectory(const std::string& dirname) { + + boost::filesystem::path temporaryFolder(dirname); + temporaryFolder /= "test-directory"; + + boost::system::error_code errorCode; + boost::filesystem::create_directories(temporaryFolder, errorCode); + CPPUNIT_ASSERT(errorCode == 0); + boost::filesystem::remove_all(temporaryFolder, errorCode); + CPPUNIT_ASSERT(errorCode == 0); +} diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.h b/lib/seccomp/unittest/CSystemCallFilterTest.h new file mode 100644 index 0000000000..b3182c06d4 --- /dev/null +++ b/lib/seccomp/unittest/CSystemCallFilterTest.h @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#ifndef INCLUDED_CSystemCallFilterTest_h +#define INCLUDED_CSystemCallFilterTest_h + +#include + +#include + +class CSystemCallFilterTest : public CppUnit::TestFixture { +public: + void testSystemCallFilter(); + + static CppUnit::Test* suite(); + +private: + void openPipeAndRead(const std::string& filename); + void openPipeAndWrite(const std::string& filename); + void makeAndRemoveDirectory(const std::string& dirname); +}; + +#endif // INCLUDED_CSystemCallFilterTest_h diff --git a/lib/seccomp/unittest/Main.cc b/lib/seccomp/unittest/Main.cc new file mode 100644 index 0000000000..1287b350c2 --- /dev/null +++ b/lib/seccomp/unittest/Main.cc @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +#include + +#include "CSystemCallFilterTest.h" + +int main(int argc, const char** argv) { + ml::test::CTestRunner runner(argc, argv); + + runner.addTest(CSystemCallFilterTest::suite()); + + return !runner.runTests(); +} diff --git a/lib/seccomp/unittest/Makefile b/lib/seccomp/unittest/Makefile new file mode 100644 index 0000000000..c38b2b1cb3 --- /dev/null +++ b/lib/seccomp/unittest/Makefile @@ -0,0 +1,22 @@ +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License; +# you may not use this file except in compliance with the Elastic License. +# +include $(CPP_SRC_HOME)/mk/defines.mk + +TARGET=ml_test$(EXE_EXT) + +USE_BOOST=1 +USE_BOOST_FILESYSTEM_LIBS=1 +LIBS:=$(LIB_ML_SECCOMP) +LDFLAGS:=$(ML_SECCOMP_LDFLAGS) + +all: build + +SRCS=\ + Main.cc \ + CSystemCallFilterTest.cc \ + +include $(CPP_SRC_HOME)/mk/stdcppunit.mk + diff --git a/mk/linux-musl.mk b/mk/linux-musl.mk index 7d4a644394..428390d6c3 100644 --- a/mk/linux-musl.mk +++ b/mk/linux-musl.mk @@ -28,8 +28,8 @@ CFLAGS=-g $(OPTCFLAGS) -msse4.2 -mfpmath=sse -fstack-protector -fno-math-errno - CXXFLAGS=$(CFLAGS) -Wno-ctor-dtor-privacy -Wno-deprecated-declarations -Wold-style-cast -fvisibility-inlines-hidden CPPFLAGS=-isystem $(CPP_SRC_HOME)/3rd_party/include -isystem /usr/local/include -D$(OS) -DLINUX=2 -D_REENTRANT $(OPTCPPFLAGS) CDEPFLAGS=-MM -COMP_OUT_FLAG=-o -LINK_OUT_FLAG=-o +COMP_OUT_FLAG=-o +LINK_OUT_FLAG=-o DEP_REFORMAT=sed 's,\($*\)\.o[ :]*,$(OBJS_DIR)\/\1.o $@ : ,g' LOCALLIBS=-lm -lpthread -ldl -lrt NETLIBS= @@ -68,6 +68,8 @@ LIB_ML_API=-lMlApi LIB_ML_MATHS=-lMlMaths LIB_ML_CONFIG=-lMlConfig LIB_ML_MODEL=-lMlModel +LIB_ML_SECCOMP=-lMlSeccomp +ML_SECCOMP_LDFLAGS=-L$(CPP_SRC_HOME)/lib/seccomp/.objs LIB_ML_TEST=-lMlTest LIB_PATH+=-L/usr/local/lib diff --git a/mk/linux.mk b/mk/linux.mk index b77709618f..aa51f789a1 100644 --- a/mk/linux.mk +++ b/mk/linux.mk @@ -29,8 +29,8 @@ CFLAGS=-g $(OPTCFLAGS) -msse4.2 -mfpmath=sse -fstack-protector -fno-math-errno - CXXFLAGS=$(CFLAGS) -Wno-ctor-dtor-privacy -Wno-deprecated-declarations -Wold-style-cast -fvisibility-inlines-hidden CPPFLAGS=-isystem $(CPP_SRC_HOME)/3rd_party/include -isystem /usr/local/gcc62/include -D$(OS) -DLINUX=2 -D_REENTRANT $(OPTCPPFLAGS) CDEPFLAGS=-MM -COMP_OUT_FLAG=-o -LINK_OUT_FLAG=-o +COMP_OUT_FLAG=-o +LINK_OUT_FLAG=-o DEP_REFORMAT=sed 's,\($*\)\.o[ :]*,$(OBJS_DIR)\/\1.o $@ : ,g' LOCALLIBS=-lm -lpthread -ldl -lrt NETLIBS=-lnsl @@ -72,6 +72,8 @@ LIB_ML_API=-lMlApi LIB_ML_MATHS=-lMlMaths LIB_ML_CONFIG=-lMlConfig LIB_ML_MODEL=-lMlModel +LIB_ML_SECCOMP=-lMlSeccomp +ML_SECCOMP_LDFLAGS=-L$(CPP_SRC_HOME)/lib/seccomp/.objs LIB_ML_TEST=-lMlTest LIB_PATH+=-L/usr/local/gcc62/lib diff --git a/mk/linux_crosscompile_macosx.mk b/mk/linux_crosscompile_macosx.mk index 7c52056391..674a5771f8 100644 --- a/mk/linux_crosscompile_macosx.mk +++ b/mk/linux_crosscompile_macosx.mk @@ -32,9 +32,9 @@ CXXFLAGS=$(CFLAGS) -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-exit-time-d CPPFLAGS=-isystem $(SYSROOT)/usr/include/c++/v1 -isystem $(CPP_SRC_HOME)/3rd_party/include -isystem $(SYSROOT)/usr/local/include -D$(OS) $(OPTCPPFLAGS) ANALYZEFLAGS=--analyze CDEPFLAGS=-MM -COMP_OUT_FLAG=-o -ANALYZE_OUT_FLAG=-o -LINK_OUT_FLAG=-o +COMP_OUT_FLAG=-o +ANALYZE_OUT_FLAG=-o +LINK_OUT_FLAG=-o DEP_REFORMAT=sed 's,\($*\)\.o[ :]*,$(OBJS_DIR)\/\1.o $@ : ,g' LOCALLIBS= NETLIBS= @@ -75,6 +75,8 @@ ML_VER_LDFLAGS=-L$(CPP_SRC_HOME)/lib/ver/.objs LIB_ML_MATHS=-lMlMaths LIB_ML_CONFIG=-lMlConfig LIB_ML_MODEL=-lMlModel +LIB_ML_SECCOMP=-lMlSeccomp +ML_SECCOMP_LDFLAGS=-L$(CPP_SRC_HOME)/lib/seccomp/.objs LIB_ML_TEST=-lMlTest LIB_PATH+=-L$(SYSROOT)/usr/local/lib diff --git a/mk/macosx.mk b/mk/macosx.mk index 4bd9d17005..bfe0dae31d 100644 --- a/mk/macosx.mk +++ b/mk/macosx.mk @@ -28,9 +28,9 @@ CXXFLAGS=$(CFLAGS) -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-exit-time-d CPPFLAGS=-isystem $(CPP_SRC_HOME)/3rd_party/include -isystem /usr/local/include -D$(OS) $(OPTCPPFLAGS) ANALYZEFLAGS=--analyze CDEPFLAGS=-MM -COMP_OUT_FLAG=-o -ANALYZE_OUT_FLAG=-o -LINK_OUT_FLAG=-o +COMP_OUT_FLAG=-o +ANALYZE_OUT_FLAG=-o +LINK_OUT_FLAG=-o DEP_REFORMAT=sed 's,\($*\)\.o[ :]*,$(OBJS_DIR)\/\1.o $@ : ,g' LOCALLIBS= NETLIBS= @@ -74,6 +74,8 @@ ML_VER_LDFLAGS=-L$(CPP_SRC_HOME)/lib/ver/.objs LIB_ML_MATHS=-lMlMaths LIB_ML_CONFIG=-lMlConfig LIB_ML_MODEL=-lMlModel +LIB_ML_SECCOMP=-lMlSeccomp +ML_SECCOMP_LDFLAGS=-L$(CPP_SRC_HOME)/lib/seccomp/.objs LIB_ML_TEST=-lMlTest LIB_PATH+=-L/usr/local/lib diff --git a/mk/stdapp.mk b/mk/stdapp.mk index c5f7e2f884..93cfc802b0 100644 --- a/mk/stdapp.mk +++ b/mk/stdapp.mk @@ -6,9 +6,9 @@ include $(CPP_SRC_HOME)/mk/stdmodule.mk -LDFLAGS:=$(EXELDFLAGS) $(LDFLAGS) $(LIB_PATH) $(ML_VER_LDFLAGS) +LDFLAGS:=$(EXELDFLAGS) $(LDFLAGS) $(LIB_PATH) $(ML_VER_LDFLAGS) $(ML_SECCOMP_LDFLAGS) PICFLAGS=$(PLATPIEFLAGS) -LIBS:=$(LOCALLIBS) $(LIB_ML_VER) $(LIBS) +LIBS:=$(LOCALLIBS) $(LIB_ML_VER) $(LIB_ML_SECCOMP) $(LIBS) ifndef INSTALL_DIR INSTALL_DIR=$(CPP_PLATFORM_HOME)/bin diff --git a/mk/windows.mk b/mk/windows.mk index a2710e81d6..a6d785e2f5 100644 --- a/mk/windows.mk +++ b/mk/windows.mk @@ -103,6 +103,8 @@ LIB_ML_API=libMlApi.lib LIB_ML_MATHS=libMlMaths.lib LIB_ML_CONFIG=libMlConfig.lib LIB_ML_MODEL=libMlModel.lib +LIB_ML_SECCOMP=libMlSeccomp.lib +ML_SECCOMP_LDFLAGS=-LIBPATH:$(CPP_SRC_HOME)/lib/seccomp/.objs LIB_ML_TEST=libMlTest.lib LIB_PATH+=-LIBPATH:$(LOCAL_DRIVE):/usr/local/lib $(VCLDFLAGS) $(WINSDKLDFLAGS)