From 7cf6ea12f25279834f39d57024cff545a8fd1f86 Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Tue, 21 Jul 2026 13:12:28 +1200 Subject: [PATCH 1/3] Reject non-native ABIs in ML seccomp BPF filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linux seccomp filter matched only on seccomp_data.nr, so an x86_64 process could issue int 0x80 (i386) and hit number collisions — notably i386 socketcall (102) vs allowlisted x86_64 getuid (102). That opened a socket surface the policy intended to block (elastic/security#12621). Require seccomp_data.arch to match the native ABI (AUDIT_ARCH_X86_64 / AUDIT_ARCH_AARCH64) with an immediate deny on mismatch, before any nr allowlist matching. The arch check is self-contained so existing relative jump offsets in the nr allowlist are unchanged. Co-authored-by: Cursor --- include/seccomp/CSystemCallFilter.h | 3 +++ lib/seccomp/CSystemCallFilter_Linux.cc | 17 ++++++++++++++++- lib/seccomp/unittest/CSystemCallFilterTest.cc | 6 ++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/include/seccomp/CSystemCallFilter.h b/include/seccomp/CSystemCallFilter.h index 17836bba9a..03c77086cd 100644 --- a/include/seccomp/CSystemCallFilter.h +++ b/include/seccomp/CSystemCallFilter.h @@ -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. diff --git a/lib/seccomp/CSystemCallFilter_Linux.cc b/lib/seccomp/CSystemCallFilter_Linux.cc index 9d53971007..fe1cccb066 100644 --- a/lib/seccomp/CSystemCallFilter_Linux.cc +++ b/lib/seccomp/CSystemCallFilter_Linux.cc @@ -30,10 +30,25 @@ 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 +// Offsets into struct seccomp_data (linux/seccomp.h). const std::uint32_t SECCOMP_DATA_NR_OFFSET = 0x00; +const std::uint32_t SECCOMP_DATA_ARCH_OFFSET = 0x04; 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, SECCOMP_DATA_ARCH_OFFSET), +#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), diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.cc b/lib/seccomp/unittest/CSystemCallFilterTest.cc index 56b6d66ade..db49d6f74f 100644 --- a/lib/seccomp/unittest/CSystemCallFilterTest.cc +++ b/lib/seccomp/unittest/CSystemCallFilterTest.cc @@ -188,6 +188,12 @@ BOOST_AUTO_TEST_CASE(testSystemCallFilter) { // Install the filter ml::seccomp::CSystemCallFilter::installSystemCallFilter(); + // Native allowlisted calls must still work. Compat-ABI collisions + // (e.g. i386 socketcall via int 0x80 matching x86_64 getuid=102) are + // rejected by the seccomp_data.arch check at the start of the filter; + // that path is not exercised here because it requires issuing a foreign + // ABI syscall from the test process. + BOOST_REQUIRE_MESSAGE(systemCall() == false, "Calling std::system should fail"); // Operations that must function after seccomp is initialised From c4bfda203c03eeeb370fbbbf565bf9741797f7d2 Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Tue, 21 Jul 2026 13:13:01 +1200 Subject: [PATCH 2/3] Add changelog for #3080 Co-authored-by: Cursor --- docs/changelog/3080.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/3080.yaml diff --git a/docs/changelog/3080.yaml b/docs/changelog/3080.yaml new file mode 100644 index 0000000000..f79dbf39d8 --- /dev/null +++ b/docs/changelog/3080.yaml @@ -0,0 +1,5 @@ +area: Machine Learning +issues: [] +pr: 3080 +summary: Reject non-native ABIs in ML seccomp filter (socketcall/getuid collision) +type: bug From fb1d22e182a75cecf39922167ea549c70d3bd00d Mon Sep 17 00:00:00 2001 From: Ed Savage Date: Tue, 21 Jul 2026 15:06:25 +1200 Subject: [PATCH 3/3] Address Copilot review on seccomp arch gate Use offsetof for seccomp_data field loads and add an x86_64-only int 0x80 denial test, soft-skipped when IA32 emulation is unavailable. Co-authored-by: Cursor --- lib/seccomp/CSystemCallFilter_Linux.cc | 9 +-- lib/seccomp/unittest/CSystemCallFilterTest.cc | 81 +++++++++++++++++-- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/lib/seccomp/CSystemCallFilter_Linux.cc b/lib/seccomp/CSystemCallFilter_Linux.cc index fe1cccb066..fd0f6192e0 100644 --- a/lib/seccomp/CSystemCallFilter_Linux.cc +++ b/lib/seccomp/CSystemCallFilter_Linux.cc @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -30,10 +31,6 @@ namespace { // The x64 ABI should fail these calls const std::uint32_t UPPER_NR_LIMIT = 0x3FFFFFFF; -// Offsets into struct seccomp_data (linux/seccomp.h). -const std::uint32_t SECCOMP_DATA_NR_OFFSET = 0x00; -const std::uint32_t SECCOMP_DATA_ARCH_OFFSET = 0x04; - 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 — @@ -41,7 +38,7 @@ const struct sock_filter FILTER[] = { // 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, SECCOMP_DATA_ARCH_OFFSET), + 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__) @@ -50,7 +47,7 @@ const struct sock_filter FILTER[] = { 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 diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.cc b/lib/seccomp/unittest/CSystemCallFilterTest.cc index db49d6f74f..10d0a6d5c5 100644 --- a/lib/seccomp/unittest/CSystemCallFilterTest.cc +++ b/lib/seccomp/unittest/CSystemCallFilterTest.cc @@ -28,9 +28,15 @@ #include #include +#include #include #include +#if defined(Linux) && defined(__x86_64__) +#include +#include +#endif + BOOST_AUTO_TEST_SUITE(CSystemCallFilterTest) namespace { @@ -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; + 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; +} +#endif // Linux && __x86_64__ } BOOST_AUTO_TEST_CASE(testSystemCallFilter) { @@ -185,14 +239,31 @@ 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(); - // Native allowlisted calls must still work. Compat-ABI collisions - // (e.g. i386 socketcall via int 0x80 matching x86_64 getuid=102) are - // rejected by the seccomp_data.arch check at the start of the filter; - // that path is not exercised here because it requires issuing a foreign - // ABI syscall from the test process. +#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");