Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
bdb9a9e
[ML] Generate syscall policies from one declaration; fail-closed degr…
valeriy42 Sep 9, 2026
79d4173
[ML] Typed filesystem/network launch policy for Sandbox2 pytorch_infe…
valeriy42 Sep 9, 2026
51ddf27
[ML] Explicit lifecycle state enum + CAS timeout latch for sandboxed …
valeriy42 Sep 9, 2026
6ff755d
[ML] Fix doc-comment provenance for sandboxed process lifecycle enum
valeriy42 Sep 9, 2026
6b4d850
[ML] Kill-and-reap guard + injectable seams for spawn() skeleton
valeriy42 Sep 9, 2026
5ead403
[ML] Fix guard UAF and unguarded exception window in spawn()
valeriy42 Sep 9, 2026
c5ea3b5
[ML] pidfd outcome classification, ENOSYS->Sandbox2::Kill() routing, …
valeriy42 Sep 9, 2026
9971a80
[ML] terminateChild(): roll back s_State on failed termination attempt
valeriy42 Sep 9, 2026
282af8d
[ML] Add CSandboxedProcessSpawnerLifecycleTest_Linux (PR D Task 4)
valeriy42 Sep 9, 2026
e540295
[ML] Strengthen ENOSYS-fallback SIGKILL assertion and bound its wait
valeriy42 Sep 9, 2026
47a8dd6
[ML] Fix UAF on timeout-then-detach path in pidfd-ENOSYS lifecycle test
valeriy42 Sep 9, 2026
b42ee96
[ML] Fix pidfd recycled-descriptor race, generation-blind rollback, d…
valeriy42 Sep 9, 2026
31c72b4
[ML] Adjudicate final-review residuals: fix log PID, guard warm-up ha…
valeriy42 Sep 9, 2026
13cc526
[ML] Fix self-review round 2 findings: EXTERNAL_KILL assertion, gate …
valeriy42 Sep 9, 2026
92aafd8
fix(sandbox2-lifecycle-test): release monitor closure refs before fd …
valeriy42 Sep 9, 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
8 changes: 7 additions & 1 deletion bin/autodetect/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,13 @@ int main(int argc, char** argv) {
// Reduce memory priority before installing system call filters.
ml::core::CProcessPriority::reduceMemoryPriority();

ml::seccomp::CSystemCallFilter::installSystemCallFilter();
// Policy unchanged: log and continue on a degraded install; this
// binary does not process untrusted model input, unlike
// pytorch_inference.
if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() !=
ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) {
LOG_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/categorize/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,13 @@ int main(int argc, char** argv) {
// Reduce memory priority before installing system call filters.
ml::core::CProcessPriority::reduceMemoryPriority();

ml::seccomp::CSystemCallFilter::installSystemCallFilter();
// Policy unchanged: log and continue on a degraded install; this
// binary does not process untrusted model input, unlike
// pytorch_inference.
if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() !=
ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) {
LOG_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/data_frame_analyzer/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,13 @@ int main(int argc, char** argv) {
// Reduce memory priority before installing system call filters.
ml::core::CProcessPriority::reduceMemoryPriority();

ml::seccomp::CSystemCallFilter::installSystemCallFilter();
// Policy unchanged: log and continue on a degraded install; this
// binary does not process untrusted model input, unlike
// pytorch_inference.
if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() !=
ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) {
LOG_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
8 changes: 7 additions & 1 deletion bin/normalize/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ int main(int argc, char** argv) {
// Reduce memory priority before installing system call filters.
ml::core::CProcessPriority::reduceMemoryPriority();

ml::seccomp::CSystemCallFilter::installSystemCallFilter();
// Policy unchanged: log and continue on a degraded install; this
// binary does not process untrusted model input, unlike
// pytorch_inference.
if (ml::seccomp::CSystemCallFilter::installSystemCallFilter() !=
ml::seccomp::ESystemCallFilterInstallOutcome::E_Installed) {
LOG_WARN(<< "Continuing without full syscall filtering");
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
28 changes: 27 additions & 1 deletion bin/pytorch_inference/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,33 @@ int main(int argc, char** argv) {

// Reduce memory priority before installing system call filters.
ml::core::CProcessPriority::reduceMemoryPriority();
ml::seccomp::CSystemCallFilter::installSystemCallFilter();

// Internal switch, not an operator setting: it stays false until the
// controller can route around Sandbox2 explicitly and guarantee that a
// degraded-mode (no-Sandbox2) launch was a deliberate operator choice
// rather than the only option this process has. Flipping it on today
// would terminate every launch on a host lacking seccomp BPF, with no
// operator fallback to select instead.
constexpr bool TERMINATE_ON_DEGRADED_SECCOMP_FAILURE{false};

const ml::seccomp::ESystemCallFilterInstallOutcome seccompOutcome{
ml::seccomp::CSystemCallFilter::installSystemCallFilter()};

if (ml::seccomp::decideDegradedModeAction(seccompOutcome, TERMINATE_ON_DEGRADED_SECCOMP_FAILURE) ==
ml::seccomp::EDegradedModeAction::E_TerminateBeforeIo) {
LOG_FATAL(<< "Seccomp installation " << ml::seccomp::describe(seccompOutcome)
<< "; terminating before untrusted model processing");
return EXIT_FAILURE;
}

// Explicit structured attestation the controller/Elasticsearch can
// assert on directly, rather than inferring readiness from the absence
// of a fatal log line above.
const std::string degradedModeMarker{
ml::seccomp::degradedModeAttestationMarker(seccompOutcome)};
if (degradedModeMarker.empty() == false) {
LOG_INFO(<< degradedModeMarker);
}

if (ioMgr.initIo() == false) {
LOG_FATAL(<< "Failed to initialise IO");
Expand Down
144 changes: 144 additions & 0 deletions include/sandbox/CPytorchInferenceSandboxPolicy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/*
* 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.
*/
#ifndef INCLUDED_ml_sandbox_CPytorchInferenceSandboxPolicy_h
#define INCLUDED_ml_sandbox_CPytorchInferenceSandboxPolicy_h

#include <string>
#include <vector>

#ifdef SANDBOX2_AVAILABLE
#include <sandboxed_api/sandbox2/policybuilder.h>
#endif

namespace ml {
namespace sandbox {

//! Reasons a path-bearing launch argument fails typed validation against the
//! pinned child-root contract (docs/projects/mlcpp-sandbox2-pr2873/design.md
//! Sandbox2 clean rebuild plan, PR C, gate V16). Every value here must fail
//! *before* a policy is constructed; none of them widen a mount to recover.
enum class EChildIpcPathRejection {
E_UnrecognizedOption, //!< option name is not input/output/restore/logPipe.
E_NotAbsolute, //!< value does not start with '/'.
E_RootLevelPath, //!< value has no mountable parent directory below '/'.
E_ContainsDotDot, //!< value has a ".." path component.
E_CanonicalizationFailed, //!< realpath() could not resolve the parent directory.
E_OutsideTrustedBase, //!< canonical parent is not beneath the trusted $TMPDIR.
E_WrongDepth, //!< canonical parent is not exactly $TMPDIR/ml-child-ipc/<child-id>.
E_ChildIdMismatch, //!< two path options resolved to a different <child-id>.
E_MutableSymlinkOrAlias, //!< the literal and canonical parent directories diverge.
E_Duplicate //!< the same literal argument was supplied more than once.
};

//! One rejected path-bearing argument and why.
struct SRejectedChildIpcPath {
std::string s_Arg;
EChildIpcPathRejection s_Reason;
};

//! A typed, validated launch specification for a single sandboxed
//! pytorch_inference child, derived from its path-bearing launch options
//! (input, output, restore, logPipe). Replaces raw argument-directory
//! inference: every accepted path is provably beneath the one pinned
//! per-child IPC root, never inferred from arbitrary argv content.
struct SChildIpcLaunchSpec {
//! <child-id> path component shared by every accepted path option.
//! Empty iff no recognized path option was present in the command line.
std::string s_ChildId;
//! Canonical $TMPDIR/ml-child-ipc/<child-id> - the directory the native
//! controller creates (mode 0700) before policy construction, and the
//! only host directory CSandboxedProcessSpawner maps to
//! /run/elastic/ml-ipc. Empty iff s_ChildId is empty.
std::string s_ChildIpcRoot;
//! Canonical paths of every accepted path-bearing argument, always
//! s_ChildIpcRoot plus exactly one leaf component.
std::vector<std::string> s_PipePaths;
};

//! Result of validating a pytorch_inference launch command line against the
//! pinned child-root contract.
struct SChildIpcValidationResult {
//! True only when at least one path option was present and every
//! path option that was present was accepted. False means the caller
//! must fail the spawn - never fall back to a partially-built policy.
bool s_Ok = false;
SChildIpcLaunchSpec s_Spec;
std::vector<SRejectedChildIpcPath> s_Rejected;
};

//! Validate every input/output/restore/logPipe argument in args against the
//! pinned child-root contract: each must canonicalize to a parent directory
//! of exactly trustedTmpDir/ml-child-ipc/<child-id>, for one consistent
//! <child-id>, with no ".."; no relative, root, or out-of-root path; no
//! divergent literal/canonical parent; and no duplicate literal argument.
//! Scalar (non path-bearing) options are never inspected as candidate paths.
//! trustedTmpDir must already be the canonical form of the operator's
//! Environment.tmpDir(); this function does not itself decide what counts
//! as trusted.
SChildIpcValidationResult validateChildIpcLaunchSpec(const std::string& trustedTmpDir,
const std::vector<std::string>& args);

#ifdef SANDBOX2_AVAILABLE

//! What buildPytorchInferenceFilesystemPolicy does with one of the seven
//! historically bulk-mounted fixed directories
//! (/lib /lib64 /usr/lib /usr/lib64 /etc /proc /sys). See design.md
//! §Filesystem and IPC policy: whole /etc and a host /proc/sys bind are
//! non-conformant.
enum class EFixedMountAction {
E_MountReadOnlyDirectory, //!< the whole directory is demonstrated necessary read-only.
E_MountNamespacedProcfs, //!< Sandbox2 supplies this inside the sandbox's own PID/mount namespace; never bind the host directory.
E_Skip //!< not mapped at all; narrower entries (files) are added separately.
};

//! One fixed-mount decision plus the reason it is scoped that way.
struct SFixedMountDecision {
std::string s_Path;
EFixedMountAction s_Action;
std::string s_Reason;
};

//! The minimization decision PR C applies to each of the seven historically
//! bulk-mounted fixed directories, with its justification. /etc is Skip
//! (see allowlistedEtcFiles() for the narrower replacement); /proc and /sys
//! are the Sandbox2-namespaced procfs/sysfs, never a host bind (see
//! namespacedProcfsConformant() for the runtime assertion that this actually
//! held for a given launch). /lib, /lib64, /usr/lib, /usr/lib64 remain whole
//! read-only directories: the dynamic loader resolves libtorch/glibc shared
//! objects from them at runtime from an unbounded, platform-dependent set,
//! so per-file allowlisting would duplicate the loader's own search logic.
const std::vector<SFixedMountDecision>& fixedMountDecisions();

//! Individual /etc files pytorch_inference/libtorch are demonstrated to
//! need, replacing a whole-/etc bind. Extend only with a named consumer.
const std::vector<std::string>& allowlistedEtcFiles();

//! Builds the filesystem and network-shape portion of the pytorch_inference
//! Sandbox2 policy: minimized fixed mounts (fixedMountDecisions,
//! allowlistedEtcFiles), a private bounded tmpfs at /tmp, the one per-child
//! IPC root mapped to /run/elastic/ml-ipc, and the syscall allowlist shared
//! with the legacy BPF filter
//! (seccomp::pytorch_inference::legacyBpfAllowedSyscalls, kept in sync per
//! that header's own comment). Does not call TryBuild() - the caller owns
//! final policy construction so tests can inspect the builder before
//! commit. spec must already be s_Ok from validateChildIpcLaunchSpec; this
//! function does not re-validate it.
sandbox2::PolicyBuilder buildPytorchInferenceFilesystemPolicy(const std::string& binDir,
const std::string& libDir,
const SChildIpcLaunchSpec& spec,
std::size_t tmpfsSizeBytes);

#endif // SANDBOX2_AVAILABLE

} // namespace sandbox
} // namespace ml

#endif // INCLUDED_ml_sandbox_CPytorchInferenceSandboxPolicy_h
Loading