Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion bin/autodetect/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
// Policy unchanged: 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_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/categorize/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
// Policy unchanged: 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_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/data_frame_analyzer/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
// Policy unchanged: 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_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/normalize/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
// Policy unchanged: 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_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
28 changes: 27 additions & 1 deletion bin/pytorch_inference/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
128 changes: 128 additions & 0 deletions include/seccomp/CPytorchInferenceSyscallAllowlist.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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_CPytorchInferenceSyscallAllowlist_h
#define INCLUDED_ml_seccomp_CPytorchInferenceSyscallAllowlist_h

#include <vector>

#ifdef __linux__
#include <sys/syscall.h>
#endif

namespace ml {
namespace seccomp {
namespace pytorch_inference {

@edsavage edsavage Sep 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that both the name and the pytorch_inference namespace are misleading here. The header comment notes this allowlist is shared by all five ML binaries (pytorch_inference, autodetect, categorize, normalize, data_frame_analyzer), but the name implies it is exclusive to pytorch_inference. A reader of autodetect/Main.cc has no way to know that installSystemCallFilter() internally calls pytorch_inference::legacyBpfAllowedSyscalls().

I get the forward-looking intention (that the Sandbox2 policy will eventually consume this declaration specifically for pytorch_inference), but that makes the naming more load-bearing, not less. When the other four binaries eventually get their own policies the name will actively mislead. Something like CMlLegacyBpfSyscallAllowlist.h with the function in ml::seccomp (not a pytorch_inference sub-namespace) would still be accurate today while keeping the door open for the future split.

#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 std::vector<int> legacyBpfAllowedSyscalls() {
std::vector<int> syscalls {
#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)
};
return syscalls;
}

#endif // __linux__

} // namespace pytorch_inference
} // namespace seccomp
} // namespace ml

#endif // INCLUDED_ml_seccomp_CPytorchInferenceSyscallAllowlist_h
39 changes: 39 additions & 0 deletions include/seccomp/CSeccompFilterBuilder.h
Original file line number Diff line number Diff line change
@@ -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 <linux/filter.h>

#include <vector>

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 CPytorchInferenceSyscallAllowlist.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<sock_filter> buildSyscallAllowlistProgram(const std::vector<int>& allowedSyscalls);
}
}

#endif // __linux__

#endif // INCLUDED_ml_seccomp_CSeccompFilterBuilder_h
80 changes: 79 additions & 1 deletion include/seccomp/CSystemCallFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

#include <core/CNonInstantiatable.h>

#include <string>

namespace ml {
namespace seccomp {

Expand Down Expand Up @@ -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: a failed degraded launch already exits
//! before initIo() (see decideDegradedModeAction()) and must never emit
//! this marker, since doing so would falsely attest a filter that isn't
//! there. Logged over the existing per-process log pipe; this is not a new
//! startup channel.
Comment on lines +107 to +111
inline std::string degradedModeAttestationMarker(ESystemCallFilterInstallOutcome outcome) {
if (outcome != ESystemCallFilterInstallOutcome::E_Installed) {
return std::string();
}
return "{\"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();
};
}
}
Expand Down
Loading