Skip to content
Draft
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
5 changes: 5 additions & 0 deletions docs/changelog/3080.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
area: Machine Learning
issues: []
pr: 3080
summary: Reject non-native ABIs in ML seccomp filter (socketcall/getuid collision)
type: bug
3 changes: 3 additions & 0 deletions include/seccomp/CSystemCallFilter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ namespace seccomp {
//!
//! Linux:
//! Seccomp BPF is used to restrict system calls on kernels since 3.5.
//! The filter first requires seccomp_data.arch to match the native ABI
//! (rejecting compat ABIs such as i386 int 0x80 on x86_64, which would
//! otherwise collide with allowlisted syscall numbers).
//!
//! macOs:
//! The sandbox facility is used to restict access to system resources.
Comment on lines 38 to 39
Expand Down
20 changes: 16 additions & 4 deletions lib/seccomp/CSystemCallFilter_Linux.cc
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <core/CLogger.h>

#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstring>

Expand All @@ -30,12 +31,23 @@ namespace {
// The x64 ABI 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;

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).
// See elastic/security#12621 / HackerOne report on ML seccomp bypass.
// 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)),
#ifdef __x86_64__
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),
#endif
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, SECCOMP_DATA_NR_OFFSET),
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
Expand Down
77 changes: 77 additions & 0 deletions lib/seccomp/unittest/CSystemCallFilterTest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@
#include <boost/test/unit_test.hpp>

#include <atomic>
#include <cerrno>
#include <cstdlib>
#include <string>

#if defined(Linux) && defined(__x86_64__)
#include <csetjmp>
#include <csignal>
#endif

BOOST_AUTO_TEST_SUITE(CSystemCallFilterTest)

namespace {
Expand Down Expand Up @@ -162,6 +168,54 @@ bool versionIsBefore3_5(int major, int minor) {
return false;
}
#endif

#if defined(Linux) && defined(__x86_64__)
// i386 ABI numbers from asm/unistd_32.h. Not available as macros when
// compiling a pure x86_64 translation unit, so hard-code the ones we need.
constexpr long I386_NR_GETUID{24};
// Collides with allowlisted x86_64 getuid (102) — the HackerOne #12621 case.
constexpr long I386_NR_SOCKETCALL{102};

sigjmp_buf g_i386ProbeJmpBuf;

void i386ProbeSignalHandler(int /*sig*/) {
siglongjmp(g_i386ProbeJmpBuf, 1);
}

//! Issue a 32-bit ABI syscall via int $0x80. Returns false if the kernel
//! lacks IA32 emulation (fault), otherwise writes the syscall result to \p result.
bool tryI386Syscall(long nr, long& result) {
struct sigaction faultHandler {};
faultHandler.sa_handler = &i386ProbeSignalHandler;
sigemptyset(&faultHandler.sa_mask);
faultHandler.sa_flags = 0;

struct sigaction previousSegv {};
struct sigaction previousIll {};
BOOST_TEST_REQUIRE(sigaction(SIGSEGV, &faultHandler, &previousSegv) == 0);
BOOST_TEST_REQUIRE(sigaction(SIGILL, &faultHandler, &previousIll) == 0);

bool available{false};
if (sigsetjmp(g_i386ProbeJmpBuf, 1) == 0) {
long ret;
// NOLINTNEXTLINE(hicpp-no-assembler)
asm volatile("int $0x80" : "=a"(ret) : "a"(nr) : "memory");
result = ret;
Comment on lines +201 to +203
available = true;
}

BOOST_TEST_REQUIRE(sigaction(SIGSEGV, &previousSegv, nullptr) == 0);
BOOST_TEST_REQUIRE(sigaction(SIGILL, &previousIll, nullptr) == 0);
return available;
}

long i386Syscall(long nr) {
long ret;
// NOLINTNEXTLINE(hicpp-no-assembler)
asm volatile("int $0x80" : "=a"(ret) : "a"(nr) : "memory");
return ret;
Comment on lines +214 to +216
}
#endif // Linux && __x86_64__
}

BOOST_AUTO_TEST_CASE(testSystemCallFilter) {
Expand All @@ -185,9 +239,32 @@ BOOST_AUTO_TEST_CASE(testSystemCallFilter) {
// system call filters are applied
BOOST_TEST_REQUIRE(systemCall());

#if defined(Linux) && defined(__x86_64__)
// Soft-detect IA32 emulation: kernels without CONFIG_IA32_EMULATION
// (or with it disabled at runtime) fault on int $0x80 from a 64-bit
// process. Skip the compat-ABI assertion in that case rather than fail CI.
long i386GetuidResult{0};
const bool i386EntryAvailable{tryI386Syscall(I386_NR_GETUID, i386GetuidResult)};
if (i386EntryAvailable) {
BOOST_TEST_REQUIRE(i386GetuidResult >= 0);
LOG_INFO(<< "i386 syscall entry available; will assert arch denial after filter install");
} else {
LOG_INFO(<< "i386 syscall entry unavailable; skipping compat-ABI seccomp assertion");
}
#endif

// Install the filter
ml::seccomp::CSystemCallFilter::installSystemCallFilter();

#if defined(Linux) && defined(__x86_64__)
if (i386EntryAvailable) {
// Without the seccomp_data.arch gate, i386 socketcall (102) matches
// allowlisted x86_64 getuid (102) and is wrongly permitted.
// Without the arch gate this would be wrongly allowed as x86_64 getuid.
BOOST_REQUIRE_EQUAL(-EACCES, i386Syscall(I386_NR_SOCKETCALL));
}
#endif

BOOST_REQUIRE_MESSAGE(systemCall() == false, "Calling std::system should fail");

// Operations that must function after seccomp is initialised
Expand Down