Conversation
7fb827f to
5a777b8
Compare
5a777b8 to
bdb9a9e
Compare
…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.
bdb9a9e to
f6f2fab
Compare
|
Pinging @elastic/ml-core (Team:ML) |
There was a problem hiding this comment.
🟡 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 coversmkstempsand temporary-file I/O failures. ReturningE_MechanismUnavailablemisclassifies a setup/install failure and makes the new typed outcome inaccurate for callers. Map this toE_FilterInstallFailedor 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_JEQsyscall rows. On x86 the builder also emits theBPF_JGTx32-ABI guard at line 78, whosejtis 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 ondenyIndex(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.
| 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); |
| //! 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 { | ||
|
|
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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");
Replace the hand-maintained BPF jump-offset table in
CSystemCallFilter_Linux.ccwith a program builder that derives every jump from the allowlist vector's own size/index. The applied program is generated fromCPytorchInferenceSyscallAllowlist.h, a single machine-readable declaration, instead of a parallel hardcoded list.CSystemCallFilter::installSystemCallFilter()returns a typedESystemCallFilterInstallOutcomeacross all three platform implementations instead ofvoid, and logs anml.seccomp.installedreadiness marker on success.pytorch_inference/Main.ccgains adecideDegradedModeAction()decision that would terminate beforeCIoManager::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 (clone3allowed by literal syscall number, andprlimit64) with a named regression test, so a from-scratch rewrite doesn't silently drop them.CSeccompFilterBuilderTest.ccdecodes the actually-built BPF program to prove it matches the declaration and that jump offsets are derived, not hand-maintained, plus fault-injected coverage ofdecideDegradedModeAction()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 realpytorch_inference/autodetect/categorize/normalize/data_frame_analyzerbinaries all build, link, and run correctly against the new typed API; the generated BPF program installs via a realprctl(PR_SET_SECCOMP)call.