Skip to content

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

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

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

Conversation

@valeriy42

@valeriy42 valeriy42 commented Oct 28, 2025

Copy link
Copy Markdown
Contributor

PyTorch inference runs untrusted TorchScript models supplied by Elasticsearch users, so the native process must be treated as hostile. Today pytorch_inference relies largely on in-process seccomp and graph validation, but that still leaves a large syscall and filesystem surface exposed inside the same address space as libtorch. This PR hardens the Linux production path by spawning pytorch_inference inside Google Sandbox2 from the ML controller, so isolation, syscall policy, and filesystem access are enforced before the model binary starts executing.

The implementation lives primarily in CDetachedProcessSpawner_Linux: controller-spawned pytorch_inference processes get a Sandbox2 policy tailored to the real ES wire-up (named pipes, restore streams, libtorch threading, and long-lived daemon behaviour), while other controller children continue to use the existing posix_spawn path. Build support vendors Abseil and the Sandboxed API on Linux, wires them into CMake, and adds the required license files. Supporting changes cover controller/pytorch command-line handling, clearer IO setup errors, seccomp filter alignment for the legacy path, and a gated --skipModelValidation build option that stays off in distributed builds.

Validation adds Linux unit coverage for the spawner and Sandbox2 spawn path, extends the existing evil-model tests, and introduces an end-to-end attack-defense integration test run from the Docker test entrypoint. CI debugging during development surfaced several practical constraints—clone3 on older build headers, futex operations under load, FIFO visibility across mount namespaces, and Sandbox2’s default wall-time limits on a daemon process—and the policy and spawner logic were adjusted accordingly so sandboxed inference can start and stay up in real ES deployments.

Review sequence recommendation
Review in this order (foundation → integration → tests → docs):

  1. lib/core/CDetachedProcessSpawner_Linux.cc — the new 719-line core; everything else supports it.
  2. lib/core/CDetachedProcessSpawner.cc — the non-Linux baseline, to contrast dispatch/tracking behavior.
  3. lib/seccomp/CSystemCallFilter_Linux.cc — legacy (non-sandboxed) syscall path that must stay coherent with the Sandbox2 policy.
  4. bin/pytorch_inference/Main.cc + bin/pytorch_inference/CCmdLineParser.cc — how the child decides seccomp-vs-sandbox and parses args.
  5. bin/controller/* + lib/api/CIoManager.cc — command-line threading and IO error reporting.
  6. 3rd_party/CMakeLists.txt, cmake/variables.cmake, cmake/functions.cmake, lib/core/CMakeLists.txt — build wiring / SANDBOX2_AVAILABLE gating.
  7. Tests: lib/core/unittest/CDetachedProcessSpawnerTest_Linux.cc, test/test_sandbox2_attack_defense.py, test/evil_model_generator.py.
  8. Docs: docs/changelog/2873.yaml, docs/CHANGELOG.asciidoc, license files.

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

@valeriy42
valeriy42 requested a review from edsavage July 22, 2026 06:58
Keep sandboxed pytorch_inference PIDs killable by tracking them outside the
waitpid reaper, gate in-process seccomp on an ML_SANDBOXED runtime marker,
and fix the attack-defense restore wire format plus leftover debug scaffolding.
@valeriy42
valeriy42 force-pushed the enhancement/sandbox2 branch from 0b7aec4 to 39940cb Compare July 22, 2026 07:50
Keep the harness as a manual local smoke test; CI coverage remains in
CModelGraphValidatorTest and CDetachedProcessSpawnerTest_Linux.
@valeriy42

Copy link
Copy Markdown
Contributor Author

buildkite run_pytorch_tests

Extract the pytorch_inference Sandbox2 policy builder, serialize TMPDIR
env mutation, filter ML_SANDBOXED from non-sandbox spawns, and improve
logging, tests, and maintainer comments for seccomp BPF offsets.
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