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 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..fd0f6192e0 100644 --- a/lib/seccomp/CSystemCallFilter_Linux.cc +++ b/lib/seccomp/CSystemCallFilter_Linux.cc @@ -13,6 +13,7 @@ #include #include +#include #include #include @@ -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 diff --git a/lib/seccomp/unittest/CSystemCallFilterTest.cc b/lib/seccomp/unittest/CSystemCallFilterTest.cc index 56b6d66ade..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,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