Skip to content

[ML] Add Sandbox2 security integration for PyTorch inference#2873

Open
valeriy42 wants to merge 44 commits into
elastic:mainfrom
valeriy42:enhancement/sandbox2
Open

[ML] Add Sandbox2 security integration for PyTorch inference#2873
valeriy42 wants to merge 44 commits into
elastic:mainfrom
valeriy42:enhancement/sandbox2

Conversation

@valeriy42

Copy link
Copy Markdown
Contributor

Overview

This PR introduces Sandbox2 security integration to enhance the security posture of the PyTorch inference process in the ML-CPP codebase.

Changes

Security Enhancements

  • Sandbox2 Integration: Added comprehensive Sandbox2 security framework integration
  • Process Isolation: Implemented secure process spawning with privilege dropping
  • Security Testing: Added comprehensive test suite to validate security constraints

Build System Updates

  • CMake Integration: Updated build system to include Sandbox2 dependencies
  • Third-party Dependencies: Added Abseil and Sandbox2 as third-party dependencies
  • License Compliance: Added proper license files for all new dependencies

Key Components

  • Core implementation for secure process spawning
  • Comprehensive test suite validating security constraints
  • Updated CMakeLists.txt files across multiple components
  • Proper license attribution for all dependencies

Security Benefits

  • Process Isolation: PyTorch inference processes run in isolated sandboxes
  • Privilege Dropping: Processes run with minimal required privileges
  • Resource Constraints: Sandbox2 enforces memory, CPU, and system call limitations
  • Attack Surface Reduction: Malicious PyTorch models cannot escape sandbox constraints

Testing

  • Added comprehensive unit tests validating security constraints
  • Tests verify privilege dropping, resource limits, and sandbox enforcement
  • All existing tests continue to pass

Dependencies

  • Sandbox2: Google's sandboxing framework for Linux
  • Abseil: Google's C++ common libraries (required by Sandbox2)

This enhancement significantly improves the security posture of the ML-CPP system by ensuring that PyTorch inference processes cannot compromise the host system.

@prodsecmachine

prodsecmachine commented Oct 28, 2025

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@valeriy42 valeriy42 changed the title Add Sandbox2 security integration for PyTorch inference [ML] Add Sandbox2 security integration for PyTorch inference Oct 28, 2025
@valeriy42
valeriy42 marked this pull request as draft October 28, 2025 14:00
- Added new tests for Sandbox2 functionality, including privilege validation, filesystem isolation, and syscall filtering.
- Introduced a TestCleanup class for managing temporary files during tests.
- Updated the CDetachedProcessSpawner_Linux.cc to support new command line arguments for log handling and model path.
- Renamed modelDir to modelPath for clarity and adjusted policy building to accommodate file access.
- Improved overall test coverage for Sandbox2 features and ensured graceful degradation when Sandbox2 is not available.
- Improved error handling for cases when Sandbox2 is disabled or unavailable for pytorch_inference processes.
- Enhanced logging to provide clearer feedback on spawning failures with Sandbox2.
- Updated comments for clarity regarding the fallback to standard posix_spawn for non-pytorch_inference processes.
- Added a new function to apply standard ML syscall restrictions using Sandbox2's PolicyBuilder, ensuring consistent security across ML processes.
- Updated the CDetachedProcessSpawner_Linux to utilize the new syscall policy for pytorch_inference, eliminating the need for seccomp filtering in this context.
- Enhanced comments and documentation to outline future migration plans for other ML processes to Sandbox2.
- Noted the gradual transition from seccomp filters to Sandbox2 policies in the CSystemCallFilter_Linux implementation.
- Removed SetUserAndGroup from PolicyBuilder due to updates in the sandboxed-api.
- Updated AddTmpfs to include a size parameter for better resource management.
- Refactored Sandbox2 instantiation to use unique_ptr for the executor, improving memory management.
- Enhanced comments to clarify changes and provide context for future updates.
- Removed outdated syscall number definitions and replaced them with fallback definitions for newer syscalls, ensuring compatibility with RHEL8 headers.
- Updated the handling of input pipes in the sandbox policy to allow read and write access, improving functionality.
- Enhanced comments for better clarity on syscall handling and future maintenance.
@valeriy42 valeriy42 added the ci:run-qa-tests Run a subset of the QA tests label Nov 6, 2025
…for Linux

- Deleted Sandbox2SecurityTest.cc as it is no longer needed.
- Updated CMakeLists.txt to remove references to the deleted test file.
- Introduced CDetachedProcessSpawnerTest_Linux.cc, which includes tests for process spawning and integration with Sandbox2.
- Enhanced CMakeLists.txt to link against Sandbox2 libraries for the new tests.
@valeriy42
valeriy42 marked this pull request as ready for review July 17, 2026 16:21
@elasticsearchmachine

Copy link
Copy Markdown

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

@elasticsearchmachine

Copy link
Copy Markdown

Hi @valeriy42, I've created a changelog YAML for you.

CI configures the build with -DCMAKE_UNITY_BUILD=ON, which propagated into
the FetchContent'd Abseil and Sandboxed API sources. Abseil's
absl_time_zone target is not unity-build safe: kDigits is defined in an
anonymous namespace in both time_zone_fixed.cc and time_zone_posix.cc,
and these collide when merged into a single unity translation unit,
failing the x86_64 RelWithDebInfo (and debug) builds early in compilation.

Disable CMAKE_UNITY_BUILD around the third-party FetchContent block and
restore the caller's setting afterwards. ml-cpp's own sources remain
unity-built.
pytorch_inference runs inside a Sandbox2 mount namespace and creates its
named pipes with mkfifo(). On the x86_64 CI agents, whose build paths are
long enough to trigger the TMPDIR override, the log FIFO never became
visible at the host path Elasticsearch waits on, so the connection timed
out with FileNotFoundException while the sandboxee blocked in
open(O_WRONLY).

- Restore the controller's global TMPDIR on every exit path of the
  sandboxed spawn. Previously the override to /tmp (needed to keep the
  Sandbox2 forkserver socket under the 108-char limit) was never undone,
  so the second and subsequent spawns saw the short value, skipped the
  per-process TMPDIR restoration and launched pytorch_inference with the
  wrong TMPDIR.
- Bind-mount both the canonical (realpath) and the literal pipe directory
  when they differ, so a mkfifo() against the literal path the sandboxee
  uses is always backed by the host directory rather than the sandbox
  throw-away rootfs.
- Add a host-side diagnostic that reports whether/when the log FIFO
  appears, to distinguish mount-visibility from start-up latency in CI.
…ailure

The sandboxed pytorch_inference log FIFO never becomes visible on the host
directory Elasticsearch watches on CI (both the ml multi-node and inference
service suites), so ES fails to connect and reports FileNotFoundException.
The 30s appearance poll confirms this is a hard mount-visibility problem,
not start-up latency.

The controller cannot currently observe why: Sandbox2 owns and reaps the
sandboxee so the waitpid tracker never sees it, and neither the Sandbox2
forkserver/monitor stderr (mount warnings, seccomp violations) nor the
sandboxee's own early stderr is captured by Elasticsearch.

Add temporary diagnostics, surfaced through the ml-cpp logger so they reach
the ES node log:
 * capture the Sandbox2 forkserver/monitor stderr by redirecting the
   controller's fd 2 to a file before the global forkserver is started;
 * capture the sandboxee stdout/stderr via Executor::ipc()->MapFd();
 * dump both from the existing log-FIFO appearance diagnostic thread.

To be reverted once the root cause is identified.
The previous diagnostic commit redirected the controller's fd 2 to a file to
capture the Sandbox2 forkserver/monitor stderr. On CI the controller's log
stream is read from its stderr, so this silenced ALL controller logging after
the redirect (build 2783 shows controller output stopping right after the
"TMPDIR path too long" message), including the diagnostic dumps themselves.

Remove the fd 2 redirect and the forkserver-stderr capture. Keep the safe
sandboxee capture: map pytorch_inference's stdout/stderr with
Executor::ipc()->MapFd() and dump them via the ml-cpp logger from the
diagnostic thread, which will surface the mkfifo()/open() errno that explains
why the log FIFO never appears on the host.

Still diagnostic-only, to be reverted once the root cause is identified.
The sandboxed pytorch_inference produces no stdout/stderr on CI and its
log FIFO never appears on the host, but the controller's tracker never
reaps it (it is a child of the Sandbox2 forkserver), so we had no signal
for how it actually terminates. Move the sandbox instance into the
diagnostic thread so it owns the lifetime, wait on AwaitResult, and log
the final status (SETUP_ERROR / VIOLATION / SIGNALED / exit code) and
reason code alongside the captured sandboxee output. This distinguishes
a namespace/mount setup failure from a seccomp violation or a silent
block on the log pipe.
…dbox2 debugging

Short-circuit run_es_tests.sh to run only the native-multi-node
:x-pack:plugin:ml:qa:native-multi-node-tests:javaRestTest task filtered to
*PyTorchModelIT. The Sandbox2 pytorch_inference FIFO-visibility failure is
universal and fires on the first spawn, so a single deployment test class
reproduces it in minutes instead of running the full ~40+ min suite.

REVERT before merge.
Override the generated PR pipeline (regardless of labels/comments) to a single
Linux x86_64 build plus the multi-node ML integration tests only. Disables the
aarch64, macOS and Windows builds and their ES-test steps, and the secondary ES
inference-API test step, so each debugging round-trip is one build + one pytorch
test class instead of five platform builds and the full ES suites.

REVERT before merge.
The AwaitResult diagnostic revealed that the sandboxed pytorch_inference
is killed by a seccomp violation on clone3 (syscall 435) the moment
libtorch spawns a thread, before it can create its log FIFO - which is
why the FIFO never appeared on the host and the sandboxee produced no
output. The policy did guard clone3 with '#ifdef __NR_clone3', but the
CentOS 7 based CI build image has kernel headers that predate clone3, so
__NR_clone3 is undefined there and the rule was compiled out, while the
newer glibc on the CI runtime uses clone3 for thread creation. aarch64
was unaffected because its glibc still uses clone. Allow clone3 by its
literal number (435 on both x86_64 and aarch64) so the policy no longer
depends on the build image's header version.
Now that the root cause (clone3 seccomp violation) is fixed and CI is
green, remove the temporary diagnostics and debugging shortcuts:

- CDetachedProcessSpawner_Linux.cc: drop the FIFO-appearance poll, the
  Executor MapFd stdout/stderr capture, logFileTail and the g_DiagSeq
  counter. Replace the diagnostic thread and the unused g_SandboxMap with
  a slim monitor thread that owns each sandbox instance for the lifetime
  of pytorch_inference and logs only genuinely abnormal termination
  (seccomp VIOLATION or sandbox SETUP_ERROR); normal SIGTERM shutdown is
  no longer reported as an error.
- .buildkite/pipeline.json.py, dev-tools/run_es_tests.sh, build.gradle:
  revert the temporary single-x86_64 pipeline, the *PyTorchModelIT test
  narrowing and the strip-skipping so the full build/test matrix runs.
- CLogger.cc: revert the stderr log-pipe fallback back to main.
- CIoManager.cc: keep the per-stream open-failure error messages but drop
  the verbose step-by-step debug narration.
- Remove the local controller-only ingest reimplementation test; keep the
  Sandbox2 attack-defense security test and its model generator.
…gging

Enforce the exact permitted-path allowlist before Sandbox2 dispatch so a
path merely containing pytorch_inference cannot bypass authorization. Add
CTrackerThread::removePid and call it from the sandbox monitor thread after
AwaitResult so sandboxed PIDs do not leak in the live-process tracker. Mirror
checkForDeadChildren termination logging for sandboxed pytorch_inference,
including SIGKILL/OOM diagnostics, while keeping normal SIGTERM at INFO.
@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_serverless_tests

@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_qa_tests

@valeriy42 valeriy42 removed ci:run-qa-tests Run a subset of the QA tests ci:run-serverless-tests labels Jul 19, 2026
@cla-checker-service

cla-checker-service Bot commented Jul 20, 2026

Copy link
Copy Markdown

💚 CLA has been signed

@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_qa_tests

Broaden the Sandbox2 futex policy beyond WAIT/WAKE so timed condition
variable waits and requeue paths used under sustained inference load are
not SIGSYS-killed. Also raise rlimit_nofile above Sandbox2's default.
…inference daemon

QA reproduction (appex-qa build 883) captured the real Sandbox2 diagnostic
that earlier CI runs lacked: pytorch_inference was being killed with
'Process TIMEOUT' by Sandbox2's default 120s wall-time limit (and 1024s
CPU-time limit), which are designed for run-to-completion sandboxees, not
a daemon that stays up for the lifetime of a deployed model. This surfaces
in Elasticsearch as 'inference native process died unexpectedly ... Unexpected
end of file'. Disarm both limits for pytorch_inference.

Also, while investigating:
- Rework checkForDeadChildren() in both CDetachedProcessSpawner and its
  Linux/Sandbox2 specialisation to waitpid() only tracked PIDs individually,
  rather than waitpid(-1, ...), avoiding interference with the async
  AwaitResult monitor thread used for sandboxed pytorch_inference.
- Restrict /dev mounts in the Sandbox2 policy to the specific device nodes
  needed (null/urandom/random) instead of the whole directory.
- Gate --skipModelValidation behind a new ML_ALLOW_SKIP_MODEL_VALIDATION
  build option (default OFF), so production/distributed builds cannot
  disable model graph validation.
- Make Sandbox2 a hard requirement on Linux (3rd_party/CMakeLists.txt)
  instead of silently disabling it if unavailable.
- Replace the placeholder Sandbox2Test suite (which only checked host
  filesystem permissions) with tests that actually spawn pytorch_inference
  under Sandbox2 and confirm it starts, runs, and terminates cleanly.
- Run the Sandbox2 attack-defense integration test from the Docker test
  entrypoint on Linux.
@valeriy42
valeriy42 force-pushed the enhancement/sandbox2 branch from 714f36a to 0d89fc4 Compare July 21, 2026 10:34
@valeriy42
valeriy42 force-pushed the enhancement/sandbox2 branch from af53f6c to facd362 Compare July 21, 2026 11:11
@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_qa_tests QAF_TESTS_TO_RUN=pytorch_tests

@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_qa_tests

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.

3 participants