Skip to content

[ML] Generate syscall policies from one declaration; fail-closed degraded seccomp - #3182

Open
valeriy42 wants to merge 1 commit into
feature/sandbox2-pr-a-foundationfrom
feature/sandbox2-pr-b-seccomp-policy
Open

valeriy42 wants to merge 1 commit into
feature/sandbox2-pr-a-foundationfrom
feature/sandbox2-pr-b-seccomp-policy

Conversation

@valeriy42

@valeriy42 valeriy42 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Replace the hand-maintained BPF jump-offset table in CSystemCallFilter_Linux.cc with a program builder that derives every jump from the allowlist vector's own size/index. The applied program is generated from CPytorchInferenceSyscallAllowlist.h, a single machine-readable declaration, instead of a parallel hardcoded list.

CSystemCallFilter::installSystemCallFilter() returns a typed ESystemCallFilterInstallOutcome across all three platform implementations instead of void, and logs an ml.seccomp.installed readiness marker on success. pytorch_inference/Main.cc gains a decideDegradedModeAction() decision that would terminate before CIoManager::initIo() on any degraded-mode seccomp failure; that termination stays behind an internal switch defaulting to false until the controller has a typed way to know a degraded-mode launch was a deliberate operator choice rather than the only option available. Flipping it on today would terminate every launch on a host lacking seccomp BPF, with no operator fallback to select instead. The four non-PyTorch callers now make their unchanged log-and-continue policy explicit instead of silently discarding the result.

Also adds an explicit structured degradedModeAttestationMarker() so a controller/Elasticsearch observer can assert seccomp installation directly instead of inferring it from the absence of a fatal log line, and carries forward two pytorch_inference/libtorch compatibility fixes from #2873 into the shared declaration (clone3 allowed by literal syscall number, and prlimit64) with a named regression test, so a from-scratch rewrite doesn't silently drop them.

CSeccompFilterBuilderTest.cc decodes the actually-built BPF program to prove it matches the declaration and that jump offsets are derived, not hand-maintained, plus fault-injected coverage of decideDegradedModeAction() for every install-failure class.

A future Sandbox2 filesystem/network policy doesn't exist yet in this branch's history; this change establishes the single declaration for that policy to consume once it's written.

Verified on a real x86_64 host: ml_test_seccomp (7 test cases) and the real pytorch_inference/autodetect/categorize/normalize/data_frame_analyzer binaries all build, link, and run correctly against the new typed API; the generated BPF program installs via a real prctl(PR_SET_SECCOMP) call.

…aded seccomp

Replace the hand-maintained BPF jump-offset table in
CSystemCallFilter_Linux.cc with a program builder that derives every
jump from the allowlist vector's own size/index. The applied program
is generated from CPytorchInferenceSyscallAllowlist.h, a single
machine-readable declaration, instead of a parallel hardcoded list
(design.md V3/MG6).

CSystemCallFilter::installSystemCallFilter() returns a typed
ESystemCallFilterInstallOutcome across all three platform
implementations instead of void, and logs the ml.seccomp.installed
readiness marker on success. pytorch_inference/Main.cc gains a
decideDegradedModeAction() decision that would terminate before
CIoManager::initIo() on any degraded-mode seccomp failure; that
termination stays behind an internal switch defaulting to false until
ml-cpp PR E's typed controller routing can guarantee a degraded-mode
launch was deliberate (design.md MG8, "activation is deliberately
split across two slices"). The four non-PyTorch callers now make
their unchanged log-and-continue policy explicit instead of silently
discarding the result.

Adds CSeccompFilterBuilderTest.cc: decodes the actually-built BPF
program to prove it matches the declaration and that jump offsets are
derived, not hand-maintained; fault-injected coverage of
decideDegradedModeAction() for every install-failure class (V13); a
named regression test per design.md M4 carry-forward syscall (clone3
by number, prlimit64, x86_64 legacy filesystem syscalls) so a
blank-slate reconstruction cannot silently drop a hard-won
compatibility fix from frozen PR #2873; and an explicit structured
degradedModeAttestationMarker() (design.md M2) so a controller/ES
observer can assert seccomp installation directly instead of
inferring it from the absence of a fatal log line.

The Sandbox2-grants half of V3 (CPytorchInferenceSandboxPolicy.cc)
does not exist yet in this clean rebuild lineage (ml-cpp PR C); this
change establishes the single declaration for that PR to consume.
d9a856d (Sandbox2 AllowFutexOp arg-filtering) and
730933d/f8b0a534 (Buildkite run_tests.sh/build.sh packaging) are
likewise out of this file's scope - see the bd note on
elastic-workspace-3b59.3 for the explicit deferral to PR C/D/E.

Verified on a real x86_64 devbox: ml_test_seccomp (7 test cases) and
the real pytorch_inference/autodetect/categorize/normalize/
data_frame_analyzer binaries all build, link, and run correctly
against the new typed API; the generated BPF program installs via a
real prctl(PR_SET_SECCOMP) call.
@elasticsearchmachine

Copy link
Copy Markdown

Pinging @elastic/ml-core (Team:ML)

Copilot AI left a comment

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.

🟡 Changes recommended

Unresolved critical and moderate findings remain in the regression tests, BPF layout, and macOS outcome mapping.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This pull request centralizes syscall declarations, generates Linux seccomp BPF dynamically, and adds typed installation outcomes with degraded-mode handling.

Changes:

  • Generates BPF filters from the shared syscall allowlist.
  • Adds cross-platform outcomes and attestation markers.
  • Expands builder, installation, and degraded-mode tests.
File summaries
File Summary and final comments
lib/seccomp/unittest/CSystemCallFilterTest.cc Verifies successful installation outcomes.
lib/seccomp/unittest/CSeccompFilterBuilderTest.cc Adds builder and decision tests. Critical (3 votes): separate temporary vectors produce unrelated iterators and undefined behavior. Nit (1 vote): add coverage for the x86 BPF_JGT guard offset.
lib/seccomp/unittest/CMakeLists.txt Registers the new tests.
lib/seccomp/CSystemCallFilter_Windows.cc Returns typed Windows outcomes.
lib/seccomp/CSystemCallFilter_MacOSX.cc Returns typed macOS outcomes. Moderate (1 vote): empty temporary rules files can represent setup failures, not only unavailable mechanisms.
lib/seccomp/CSystemCallFilter_Linux.cc Generates and installs Linux BPF. Moderate (3 votes): 8-bit conditional-jump offsets wrap at 256 entries and must be rejected, limited, or laid out differently.
include/seccomp/CSystemCallFilter.h Defines outcomes and degraded-mode APIs. Nit (3 votes): document that exiting before initIo() is conditional on the currently disabled termination switch.
include/seccomp/CSeccompFilterBuilder.h Declares the BPF builder.
include/seccomp/CPytorchInferenceSyscallAllowlist.h Provides the shared syscall declaration.
bin/pytorch_inference/Main.cc Handles degraded-mode decisions and attestation.
bin/normalize/Main.cc Makes continuation policy explicit.
bin/data_frame_analyzer/Main.cc Makes continuation policy explicit.
bin/categorize/Main.cc Makes continuation policy explicit.
bin/autodetect/Main.cc Makes continuation policy explicit.
Review details

Suppressed comments (2)

lib/seccomp/CSystemCallFilter_MacOSX.cc:94

  • When writeTempRulesFile() returns empty, the macOS sandbox mechanism is not known to be unavailable; this path also covers mkstemps and temporary-file I/O failures. Returning E_MechanismUnavailable misclassifies a setup/install failure and makes the new typed outcome inaccurate for callers. Map this to E_FilterInstallFailed or introduce a distinct profile-setup outcome.
ESystemCallFilterInstallOutcome CSystemCallFilter::installSystemCallFilter() {
    std::string profileFilename{writeTempRulesFile()};
    if (profileFilename.empty()) {
        LOG_WARN(<< "Cannot write sandbox rules. macOS sandbox will not be initialized");
        return ESystemCallFilterInstallOutcome::E_MechanismUnavailable;

lib/seccomp/unittest/CSeccompFilterBuilderTest.cc:125

  • This offset check only visits BPF_JEQ syscall rows. On x86 the builder also emits the BPF_JGT x32-ABI guard at line 78, whose jt is the other size-derived offset; a stale guard offset could miss the deny row while these tests still pass. Add an x86 assertion that this guard lands on denyIndex (and ideally exercise an x32 syscall).
        if (sawNrLoad && program[i].code == (BPF_JMP | BPF_JEQ | BPF_K) &&
            program[i].jt > 0) {
            BOOST_REQUIRE_EQUAL(allowIndex, i + program[i].jt + 1);
            foundSyscalls.insert(static_cast<int>(program[i].k));
        }
  • Files reviewed: 9/14 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +159 to +161
const std::set<int> declared{
ml::seccomp::pytorch_inference::legacyBpfAllowedSyscalls().begin(),
ml::seccomp::pytorch_inference::legacyBpfAllowedSyscalls().end()};
// count, so adding, removing or reordering an entry in allowedSyscalls
// never requires touching any other row.
for (std::uint32_t i = 0; i < numSyscalls; ++i) {
const auto jumpToAllow = static_cast<std::uint8_t>(numSyscalls - i);
Comment on lines +107 to +111
//! 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.
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.

// checking any allowlisted syscall.
program.push_back(BPF_JUMP(BPF_JMP | BPF_JGT | BPF_K, UPPER_NR_LIMIT,
static_cast<std::uint8_t>(numSyscalls), 0));
#endif

@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.

Both here and in the per-syscall loop below (jumpToAllow), the cast to std::uint8_t will silently overflow if the allowlist ever grows past 255 entries. The current list is around 45 so this is fine in practice, but worth guarding explicitly with something like:

assert(numSyscalls <= std::numeric_limits<std::uint8_t>::max() &&
       "Syscall allowlist too large for BPF jump offset");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants