Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
311ad21
sandbox2 and deps licenses
valeriy42 Oct 28, 2025
7382f3b
build integration
valeriy42 Oct 28, 2025
2c5775e
Enhance Sandbox2 integration tests and policy handling
valeriy42 Oct 28, 2025
9b8da0f
Refactor Sandbox2 handling in CDetachedProcessSpawner_Linux
valeriy42 Oct 28, 2025
ac67539
Implement Sandbox2 syscall policy for ML processes
valeriy42 Oct 28, 2025
b43e999
formatting
valeriy42 Nov 5, 2025
5cd22f9
fix header
valeriy42 Nov 5, 2025
5c5a2a0
sandbox2 library correctly linked
valeriy42 Nov 6, 2025
0fe86f5
Refactor Sandbox2 policy handling in unit tests
valeriy42 Nov 6, 2025
9dadd42
test 1 passes without sandbox2
valeriy42 Nov 6, 2025
404c6b5
benign model passes. policy very permissive.
valeriy42 Nov 6, 2025
878fadf
evil model generator
valeriy42 Nov 6, 2025
d4adb62
restrict policies.
valeriy42 Nov 6, 2025
d60ba2f
Refactor syscall definitions in CDetachedProcessSpawner_Linux
valeriy42 Nov 6, 2025
fbc3c00
formatting
valeriy42 Nov 6, 2025
9d5d2ba
changelog
valeriy42 Nov 6, 2025
7506aab
patch SAPI cmake file to make Python3 dependency optional
valeriy42 Nov 6, 2025
6f49269
Merge branch 'enhancement/sandbox2' of https://github.com/valeriy42/m…
valeriy42 Nov 6, 2025
ac26ca6
fix python checks on CI
valeriy42 Nov 7, 2025
ec5cde2
Remove Sandbox2 security tests and add CDetachedProcessSpawner tests …
valeriy42 Nov 7, 2025
03b1ee4
Add syscall allowance for prlimit64 in sandbox policy
valeriy42 Nov 20, 2025
b93dfec
formatting
valeriy42 Nov 21, 2025
0594140
adjust for arm64
valeriy42 Nov 21, 2025
c77ebb6
python integration test simulation works
valeriy42 Nov 27, 2025
fa8f99d
java IT works again
valeriy42 Dec 3, 2025
8dc9905
formatting
valeriy42 Dec 9, 2025
d853920
Fix CI build failures
valeriy42 Dec 9, 2025
4f69cd9
Merge remote-tracking branch 'upstream/main' into enhancement/sandbox2
valeriy42 Jul 17, 2026
ec7d3ed
[ML] Allow legacy file-system syscalls in pytorch_inference sandbox p…
valeriy42 Jul 17, 2026
a6a7ecd
Update docs/changelog/2873.yaml
valeriy42 Jul 17, 2026
ee0bcb3
[ML] Disable unity build for vendored Abseil / Sandboxed API
valeriy42 Jul 17, 2026
f5d6fc2
[ML] Fix sandboxed pytorch_inference named-pipe setup on long CI paths
valeriy42 Jul 17, 2026
62ea65b
[ML] Add Sandbox2 diagnostics for pytorch_inference pipe-visibility f…
valeriy42 Jul 18, 2026
cce88ce
[ML] Fix diagnostics: stop redirecting the controller's stderr
valeriy42 Jul 18, 2026
e15a5e1
[ML] Log Sandbox2 AwaitResult for pytorch_inference diagnosis
valeriy42 Jul 18, 2026
27b8d73
[ML] TEMP: narrow multi-node ES ITs to *PyTorchModelIT for faster San…
valeriy42 Jul 18, 2026
e582cd9
[ML] TEMP: force minimal x86_64 PR pipeline for Sandbox2 debugging
valeriy42 Jul 18, 2026
57f00ed
[ML] Allow clone3 by number so pytorch_inference threads on CI
valeriy42 Jul 18, 2026
a190c3e
[ML] Remove Sandbox2 debugging scaffolding and restore full CI
valeriy42 Jul 18, 2026
0a56df5
[ML] Fix Sandbox2 spawner allowlist, PID tracking, and termination lo…
valeriy42 Jul 19, 2026
d9a856d
[ML] Allow glibc futex ops needed for concurrent pytorch_inference
valeriy42 Jul 20, 2026
569f8cc
[ML] Disarm Sandbox2 wall-time/CPU limits for the long-lived pytorch_…
valeriy42 Jul 21, 2026
0d89fc4
[ML] Fix clang-format violations in Sandbox2 spawner and test
valeriy42 Jul 21, 2026
facd362
formatting
valeriy42 Jul 21, 2026
39940cb
[ML] Fix Sandbox2 spawner tracking, seccomp gate, and attack tests
valeriy42 Jul 22, 2026
ff0349c
formatting
valeriy42 Jul 22, 2026
339d3f5
[ML] Remove Sandbox2 attack-defense test from Docker CI
valeriy42 Jul 22, 2026
69dea1b
[ML] Harden Sandbox2 spawner after code review follow-ups
valeriy42 Jul 22, 2026
786cdeb
[ML] Improve Sandbox2 production observability and logging
valeriy42 Jul 23, 2026
ef21959
[ML] Honour --disableSandbox kill switch in pytorch_inference spawner
valeriy42 Jul 23, 2026
63d8850
[ML] Correct Main.cc comment about pytorch_inference sandbox routing
valeriy42 Jul 23, 2026
634182f
[ML] Strip --disableSandbox on non-Linux spawners
valeriy42 Jul 23, 2026
f153159
[ML] Point Sandbox2 startup failures at sandbox_enabled kill switch
valeriy42 Jul 23, 2026
7c9623c
formatting
valeriy42 Jul 23, 2026
6fcdecd
[ML] Fix unit test failures from spawn failure reason and seccomp tes…
valeriy42 Jul 27, 2026
ab46fc8
Merge branch 'main' of https://github.com/elastic/ml-cpp into enhance…
valeriy42 Jul 28, 2026
3dc4a56
[ML] Relocate Sandbox2 to MlSandbox and address PR #2873 review
valeriy42 Aug 19, 2026
3d96e50
formatting
valeriy42 Aug 19, 2026
6f57661
[ML] Address Jan's PR #2873 review nits
valeriy42 Aug 19, 2026
bf83cda
formatting
valeriy42 Aug 19, 2026
f0bdcf6
[ML] Fix Sandbox2 CI: set ML_SANDBOX2_EXPECT, link zlib, add Windows …
valeriy42 Aug 19, 2026
eeb52ab
[ML] Fix Sandbox2 CI regressions from MlSandbox restructure
valeriy42 Aug 19, 2026
45c1dae
[ML] Fix Sandbox2 zlib link without touching sandbox2 ALIAS
valeriy42 Aug 19, 2026
730933d
[ML] Fix Sandbox2 CI: zlib link order, probe mount, test bundle
valeriy42 Aug 20, 2026
f8b0a53
[ML] Fix aarch64 host sandbox re-run missing Boost SONAMEs
valeriy42 Aug 20, 2026
f1c938a
[ML] Fix aarch64 host sandbox re-run missing GCC 13 libstdc++
valeriy42 Aug 20, 2026
92016a5
[ML] Remove dead build_and_test.sh CI script
valeriy42 Aug 20, 2026
90dd9ca
[ML] Run aarch64 enforced Sandbox2 tests inside build image
valeriy42 Aug 20, 2026
fec7e1e
[ML] Escalate aarch64 enforced Sandbox2 docker run to --privileged
valeriy42 Aug 20, 2026
b1dadfe
[ML] Bundle gcc133 sysroot for aarch64 host enforced Sandbox2
valeriy42 Aug 20, 2026
181caa9
[ML] Diagnose Sandbox2 userns denial and dispatch coverage on it
valeriy42 Sep 3, 2026
4439aba
[ML] TEMPORARY: trim PR CI to Linux aarch64 while debugging Sandbox2
valeriy42 Sep 3, 2026
50bacc2
[ML] Fix the userns probe: mount(proc) also needs CLONE_NEWPID
valeriy42 Sep 3, 2026
4e29545
[ML] Pin enforced Sandbox2 coverage to the aarch64 host runner
valeriy42 Sep 3, 2026
781c044
[ML] Set CPP_SRC_HOME for the aarch64 host sandbox re-run
valeriy42 Sep 3, 2026
e88a112
Revert "[ML] TEMPORARY: trim PR CI to Linux aarch64 while debugging S…
valeriy42 Sep 3, 2026
15db7e3
[ML] Keep the userns diagnostic as a regression canary
valeriy42 Sep 3, 2026
5e3f47c
[ML] Address remaining Sandbox2 review nits and de-duplicate extractA…
valeriy42 Sep 7, 2026
c0df11e
[ML] Pin both Sandbox2 coverage modes on the aarch64 host
valeriy42 Sep 7, 2026
91b324f
[ML] Stop testPolicyViolationDifferential passing for the wrong reason
valeriy42 Sep 7, 2026
e4ea5a0
[ML] Fix use-after-free between the Sandbox2 monitor thread and its s…
valeriy42 Sep 7, 2026
293d085
[ML] Stop scalar options polluting rejectedPipeArgs
valeriy42 Sep 7, 2026
528d482
[ML] Fix the spawn-timeout use-after-free and cover argument classifi…
valeriy42 Sep 7, 2026
e624f1b
[ML] Classify pytorch_inference arg dirs by option name
valeriy42 Sep 7, 2026
915457f
[ML] Generation-safe Sandbox2 registry with pidfd terminate
valeriy42 Sep 7, 2026
f88d624
[ML] Reset childPid and defer spawn success log on thread-start failure
valeriy42 Sep 7, 2026
dbae478
[ML] Fix enforced-mode coverage claim to syscall-policy differential
valeriy42 Sep 7, 2026
f52cd56
[ML] Close stale pidfd on registry overwrite and broaden thread-start…
valeriy42 Sep 7, 2026
060aa27
formatting
valeriy42 Sep 7, 2026
d2adbba
[ML] Correct the rejectedPipeArgs description after the name-based re…
valeriy42 Sep 7, 2026
338e1b3
[ML] Harden Sandbox2 spawn exception-safety and enforced-coverage gate
valeriy42 Sep 8, 2026
17ff410
formatting
valeriy42 Sep 8, 2026
db5b14a
[ML] Fix stale CPytorchInferenceSandboxPolicy_Linux.cc cross-references
valeriy42 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions .buildkite/pipelines/build_linux.json.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,25 @@ def main(args):
],
})
else:
# Regression canary for the enforced Sandbox2 coverage pinned in
# run_tests.sh: reports which user-namespace stage this agent
# denies, if any. Kept because that pin fails the build when the
# agent loses namespace support, and this step is what says which
# stage broke - the alternative is another round of guessing.
# Needs no build artifacts, hence no depends_on: it answers in
# ~1 minute rather than behind the 45 minute compile. soft_fail
# because it is instrumentation, not a gate.
pipeline_steps.append({
"label": f"Diagnose user namespaces for linux-{arch} :linux:",
"timeout_in_minutes": "15",
"agents": test_agents[arch],
"commands": [
".buildkite/scripts/steps/diagnose_userns.sh"
],
"key": f"diagnose_userns_linux-{arch}",
"soft_fail": True,
})

# aarch64: split into build and test steps
build_key = f"build_test_linux-{arch}-{build_type}"

Expand Down Expand Up @@ -218,8 +237,12 @@ def main(args):
],
})

# Add debug build/test steps for PR builds to detect compilation errors with optimization disabled
if os.environ.get("BUILDKITE_PIPELINE_SLUG", "ml-cpp-pr-builds") != "ml-cpp-debug-build" and \
# Add debug build/test steps for PR builds to detect compilation errors with
# optimization disabled. These are x86_64 steps, so they only belong in the
# pipeline when x86_64 was actually requested - an aarch64-only build must
# not drag an x86_64 pair along with it.
if args.build_x86_64 and \
os.environ.get("BUILDKITE_PIPELINE_SLUG", "ml-cpp-pr-builds") != "ml-cpp-debug-build" and \
os.environ.get("BUILDKITE_PULL_REQUEST", "false") != "false" and \
not should_skip_version_bump_pr_ci():
debug_build_key = "build_test_linux-x86_64-RelWithDebInfo-debug"
Expand Down
29 changes: 27 additions & 2 deletions .buildkite/scripts/steps/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,31 @@ if [[ "$HARDWARE_ARCH" = aarch64 && -z "${CPP_CROSS_COMPILE:-}" && "$(uname)" =
' | tar xf -

docker run --rm --workdir=/ml-cpp $TEMP_TAG bash -c '
set -e
# Complete GCC133/Boost toolchain runtime for the *host* enforced
# sandbox re-run. Do not enumerate individual SONAMEs — copying the
# whole gcc133 lib{,64} tree is the point (libstdc++, libgcc_s,
# libgomp, all Boost SONAMEs, and anything else the toolchain ships).
# Flatten into one sysroot so a single LD_LIBRARY_PATH entry beats
# the AlmaLinux 8 agent /lib64.
mkdir -p cmake-build-docker/lib/sysroot
if [ -d /usr/local/gcc133/lib64 ]; then
cp -a /usr/local/gcc133/lib64/. cmake-build-docker/lib/sysroot/
fi
if [ -d /usr/local/gcc133/lib ]; then
cp -a /usr/local/gcc133/lib/. cmake-build-docker/lib/sysroot/
fi
if [ ! -d /usr/local/gcc133/lib64 ] && [ ! -d /usr/local/gcc133/lib ]; then
echo "error: /usr/local/gcc133/lib{,64} missing from build image" >&2
exit 1
fi
{
find cmake-build-docker/test -name "ml_test_*" -type f -executable 2>/dev/null
find cmake-build-docker/lib -name "*.so" 2>/dev/null
find build/distribution -name "*.so" -not -path "*.debug*" 2>/dev/null
find cmake-build-docker/lib \( -name "*.so" -o -name "*.so.*" \) 2>/dev/null
find build/distribution \( -name "*.so" -o -name "*.so.*" \) -not -path "*.debug*" 2>/dev/null
# Sandbox2 spawn tests need the installed pytorch_inference binary.
find build/distribution -type f -path "*/bin/pytorch_inference" 2>/dev/null
find cmake-build-docker/lib/sysroot 2>/dev/null
} | sort -u > /tmp/bundle-files.txt
echo "Files in bundle: $(wc -l < /tmp/bundle-files.txt)" >&2
tar czf - -T /tmp/bundle-files.txt
Expand Down Expand Up @@ -109,6 +130,8 @@ elif [[ "$(uname)" = "Linux" ]]; then
find ${BUILD_DIR}/lib -name "*.so" -o -name "*.dylib" 2>/dev/null
if [ -d "build/distribution" ]; then
find build/distribution -type f \( -name "*.so" -o -name "*.dylib" \) -not -path "*.dSYM*" 2>/dev/null
# Sandbox2 spawn tests need the installed pytorch_inference binary.
find build/distribution -type f -path "*/bin/pytorch_inference" 2>/dev/null
fi
} | sort -u > /tmp/test-bundle-files.txt

Expand Down Expand Up @@ -151,6 +174,8 @@ else
find ${BUILD_DIR}/lib -name "*.so" -o -name "*.dylib" 2>/dev/null
if [ -d "build/distribution" ]; then
find build/distribution -type f \( -name "*.so" -o -name "*.dylib" \) -not -path "*.dSYM*" 2>/dev/null
# Sandbox2 spawn tests need the installed pytorch_inference binary.
find build/distribution -type f -path "*/bin/pytorch_inference" 2>/dev/null
fi
} | sort -u > /tmp/test-bundle-files.txt

Expand Down
131 changes: 0 additions & 131 deletions .buildkite/scripts/steps/build_and_test.sh

This file was deleted.

72 changes: 72 additions & 0 deletions .buildkite/scripts/steps/diagnose_userns.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/bin/bash
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
# or more contributor license agreements. Licensed under the Elastic License
# 2.0 and the following additional limitation. Functionality enabled by the
# files subject to the Elastic License 2.0 may only be used in production when
# invoked by an Elasticsearch process with a license key installed that permits
# use of machine learning features. You may not use this file except in
# compliance with the Elastic License 2.0 and the foregoing additional
# limitation.

# Reports which runner on this agent, if any, can create the user namespace
# Sandbox2 needs - and for the ones that cannot, the exact stage the kernel
# denies.
#
# Sandbox2 requires all of: unshare(CLONE_NEWUSER), a uid/gid mapping,
# unshare(CLONE_NEWNS), a private "/", and a fresh mount of procfs. Any one can
# be denied independently and the failures are indistinguishable from outside,
# which is why userns_probe.sh reports them stage by stage in each runner.
#
# Runs the same probe on the host and under several container configurations,
# because the blocker differs between them: Docker's default seccomp profile
# denies unshare(CLONE_NEWUSER) outright, while a mount covering /proc blocks
# only the final stage. The point is to find one runner where every stage passes,
# which is where ML_SANDBOX2_REQUIRE=enforced can then be set.
#
# Needs no build artifacts, so it runs with no depends_on and answers in about a
# minute instead of waiting behind a 45 minute compile. Never fails the step:
# this is instrumentation, not a gate.

set -uo pipefail

MY_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROBE="${MY_DIR}/userns_probe.sh"
BASE_IMAGE="${SANDBOX2_DIAGNOSE_IMAGE:-docker.elastic.co/ml-dev/ml-linux-aarch64-native-build:17}"

echo "--- Sandbox2 userns diagnosis: host"
bash "$PROBE" host

if ! command -v docker >/dev/null 2>&1; then
echo "warning: docker not on PATH; skipping container probes"
echo "--- Sandbox2 userns diagnosis: done"
exit 0
fi

if ! docker pull "$BASE_IMAGE" >/dev/null 2>&1; then
echo "warning: could not pull ${BASE_IMAGE}; skipping container probes"
echo "--- Sandbox2 userns diagnosis: done"
exit 0
fi

# label@@docker flags. Ordered least to most privileged: the cheapest
# configuration that clears every stage is the one to adopt, since each
# escalation widens what a compromised test process could reach.
RUNNERS=(
"docker@@"
"docker+seccomp-unconfined@@--security-opt seccomp=unconfined"
"docker+seccomp+systempaths@@--security-opt seccomp=unconfined --security-opt systempaths=unconfined"
"docker-privileged@@--privileged"
"docker-privileged+systempaths@@--privileged --security-opt systempaths=unconfined"
)

for runner in "${RUNNERS[@]}"; do
label="${runner%%@@*}"
flags="${runner#*@@}"
echo "--- Sandbox2 userns diagnosis: ${label}"
# shellcheck disable=SC2086 - flags must word-split into separate arguments.
docker run --rm -i $flags "$BASE_IMAGE" bash -s "$label" < "$PROBE" ||
echo "[${label}] container probe could not run (exit $?)"
done

echo "--- Sandbox2 userns diagnosis: done"
exit 0
71 changes: 71 additions & 0 deletions .buildkite/scripts/steps/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,71 @@ if [[ "$HARDWARE_ARCH" = aarch64 && -z "${CPP_CROSS_COMPILE:-}" && "$(uname)" =
# Seccomp tests run inside the Docker container which shares the host
# kernel, so the kernel's seccomp filters are exercised without needing
# a separate outside-Docker run.
#
# The container's sandbox tests are not load-bearing for coverage: both modes
# are pinned on the host below, so whichever mode the container happens to
# select is a bonus. Coverage therefore does not depend on Docker's default
# seccomp profile continuing to deny unshare(CLONE_NEWUSER).

# Both coverage modes run on the host, each pinned, so neither can quietly
# stop executing. Measured by diagnose_userns.sh on this agent
# (core-almalinux-8-aarch64, kernel 4.18):
#
# host all stages OK
# docker (default) denied at unshare(CLONE_NEWUSER)
# docker + seccomp=unconfined denied at mount(proc), masked /proc paths
# docker + seccomp + systempaths all stages OK
# docker --privileged all stages OK
#
# The host needs no privilege escalation for the enforced half. Runs against
# the bundled gcc133 sysroot so the binary does not resolve against
# AlmaLinux 8 /lib64.
if [[ $TEST_OUTCOME -eq 0 ]]; then
REPO_ROOT_ABS="$(pwd)"
SYSROOT="$(pwd)/${BUILD_DIR}/lib/sysroot"
LIB_DIRS=$(find "$(pwd)/${BUILD_DIR}/lib" "$(pwd)/build/distribution" \
\( -name "*.so" -o -name "*.so.*" \) \
-exec dirname {} \; 2>/dev/null | sort -u | tr '\n' ':')
SANDBOX_TEST_DIR="${REPO_ROOT_ABS}/${BUILD_DIR}/test/lib/sandbox/unittest"
# CPP_SRC_HOME must be set: CResourceLocator::cppRootDir() otherwise
# falls back to "../../.." on the assumption that the cwd is a source
# unittest directory, whereas this runs from the build tree - so the
# spawn test looked for pytorch_inference under cmake-build-docker/ and
# did not find it. set_env.sh exports it inside the container; nothing
# does on the host.
export CPP_SRC_HOME="${REPO_ROOT_ABS}"
export LD_LIBRARY_PATH="${SYSROOT}:${LIB_DIRS}"

echo "--- Re-running sandbox unit tests on host (enforced)"
# Hardcoded, not overridable: this re-run is the enforced-coverage gate.
# An env-var default (ML_SANDBOX2_HOST_REQUIRE:-enforced) would let a
# pipeline downgrade it to fail_closed and pass required CI without ever
# exercising a real sandbox. If a future agent genuinely cannot run
# enforced on the host, diagnose_userns.sh reports which stage it denies.
(cd "${SANDBOX_TEST_DIR}" && \
ML_SANDBOX2_REQUIRE=enforced \
./ml_test_sandbox) || TEST_OUTCOME=$?

# Fail-closed coverage - spawn refusal plus the kill-switch hint - is the
# security property that keeps an unsandboxed pytorch_inference from ever
# starting, so it must not depend on an external default either.
#
# user.max_user_namespaces is per-user-namespace, so setting it to 0
# inside a namespace we own denies every further user namespace in that
# subtree. That covers clone(CLONE_NEWUSER), which is what Sandbox2 uses,
# as well as unshare(CLONE_NEWUSER), which is what the test's probe uses;
# both then fail with ENOSPC. A seccomp profile denying only unshare(2)
# would desynchronise the two and make the probe report a capability
# Sandbox2 does not have. Verified on kernels 4.18 (this agent) and 7.0.
if [[ $TEST_OUTCOME -eq 0 ]]; then
echo "--- Re-running sandbox unit tests on host (fail_closed)"
unshare --user --map-root-user sh -c "
echo 0 > /proc/sys/user/max_user_namespaces || exit 1
cd '${SANDBOX_TEST_DIR}' || exit 1
ML_SANDBOX2_REQUIRE=fail_closed exec ./ml_test_sandbox
" || TEST_OUTCOME=$?
fi
fi

else
# --- Linux x86_64 / macOS: run tests directly ---
Expand All @@ -98,6 +163,12 @@ else
export DYLD_LIBRARY_PATH="${LIB_DIRS}${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
fi

# Linux x86_64 PR agents are Buildkite k8s pods: user namespaces often work
# but Sandbox2's mount("proc", "/proc", "proc", ...) returns EPERM, so the
# sandbox tests self-select fail-closed coverage here. Nothing is pinned -
# these pods' namespace support is a property of the k8s runtime, not of this
# repository. macOS has no CSandboxedProcessSpawnerTest_Linux.cc at all.

echo "--- Running tests"
cmake \
-DSOURCE_DIR="$(pwd)" \
Expand Down
Loading