Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 7 additions & 3 deletions sandboxexec/sandbox/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ go_test(

py_library(
name = "sandbox_py",
srcs = ["sandbox.py"],
srcs = [
"python/gvisor/__init__.py",
"python/gvisor/sandbox.py",
],
imports = ["python"],
visibility = ["//:__subpackages__"],
)

py_test(
name = "sandbox_py_test",
srcs = ["sandbox_test.py"],
srcs = ["python/tests/sandbox_test.py"],
data = ["//runsc"],
main = "sandbox_test.py",
main = "python/tests/sandbox_test.py",
tags = ["nogotsan"],
deps = [
":sandbox_py",
Expand Down
18 changes: 18 additions & 0 deletions sandboxexec/sandbox/python/gvisor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 The gVisor Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""gVisor sandboxexec sandbox package."""

from .sandbox import Error
from .sandbox import Sandbox
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from typing import Optional, Tuple


class SandboxError(Exception):
class Error(Exception):
"""Base exception for Sandbox operations."""


Expand All @@ -50,7 +50,7 @@ def __init__(
enable_networking: Whether networking is enabled inside the sandbox.

Raises:
SandboxError: If sandbox creation fails.
Error: If sandbox creation fails.
"""
self._enable_networking = enable_networking
self._runtime_dir = ""
Expand All @@ -65,7 +65,7 @@ def __init__(
try:
self._runtime_dir = tempfile.mkdtemp(prefix="gvisor-sandbox-")
except OSError as e:
raise SandboxError(f"failed to create runtime directory: {e}") from e
raise Error(f"failed to create runtime directory: {e}") from e
self._owns_runtime_dir = True
else:
self._runtime_dir = runtime_dir
Expand All @@ -75,23 +75,22 @@ def __init__(

try:
if os.geteuid() != 0 and self._enable_networking:
raise SandboxError("enabling networking requires running as root")
raise Error("enabling networking requires running as root")

self._state_dir = os.path.join(self._runtime_dir, "state")
try:
os.makedirs(self._state_dir, mode=0o700, exist_ok=True)
except OSError as e:
raise SandboxError(
f"failed to create sandbox state directory: {e}"
) from e
raise Error(f"failed to create sandbox state directory: {e}") from e

# Verify permissions (mode might be different if directory already existed).
# Verify permissions (mode might be different if directory already
# existed).
try:
stat_info = os.stat(self._state_dir)
if (stat_info.st_mode & 0o777) != 0o700:
os.chmod(self._state_dir, 0o700)
except OSError as e:
raise SandboxError(
raise Error(
"sandbox state directory has incorrect permissions and failed to"
f" chmod: {e}"
) from e
Expand Down Expand Up @@ -121,7 +120,7 @@ def __init__(
timeout=30,
)
except subprocess.TimeoutExpired as e:
raise SandboxError("sandbox creation timed out (runsc run hung)") from e
raise Error("sandbox creation timed out (runsc run hung)") from e
except subprocess.CalledProcessError as e:
stderr_content = ""
if os.path.exists(stderr_path):
Expand All @@ -130,9 +129,9 @@ def __init__(
stderr_content = f.read()
except OSError:
pass
raise SandboxError(
f"failed to create sandbox via subprocess: exit code {e.returncode},"
f" stderr: {stderr_content}"
raise Error(
"failed to create sandbox via subprocess: exit code"
f" {e.returncode}, stderr: {stderr_content}"
) from e
except Exception:
self.close()
Expand Down Expand Up @@ -163,7 +162,7 @@ def _find_runsc(self) -> str:
path = shutil.which("runsc")
if path is not None:
return path
raise SandboxError("runsc binary is not found")
raise Error("runsc binary is not found")

def _create_bundle(self) -> str:
"""Creates the OCI bundle directory and config.json.
Expand All @@ -172,14 +171,14 @@ def _create_bundle(self) -> str:
The path to the created bundle directory.

Raises:
SandboxError: If bundle creation fails.
Error: If bundle creation fails.
"""
bundle_dir = os.path.join(self._runtime_dir, self._id)
rootfs_dir = os.path.join(bundle_dir, "rootfs")
try:
os.makedirs(rootfs_dir, mode=0o755, exist_ok=True)
except OSError as e:
raise SandboxError(f"failed to create bundle directories: {e}") from e
raise Error(f"failed to create bundle directories: {e}") from e

namespaces = [
{"type": "pid"},
Expand Down Expand Up @@ -242,7 +241,7 @@ def _create_bundle(self) -> str:
with open(config_path, "w") as f:
json.dump(spec, f, indent=2)
except OSError as e:
raise SandboxError(f"failed to create config.json: {e}") from e
raise Error(f"failed to create config.json: {e}") from e

return bundle_dir

Expand All @@ -260,7 +259,7 @@ def exec(
A tuple of (stdout, stderr) strings.

Raises:
SandboxError: If the command execution fails or times out.
Error: If the command execution fails or times out.
"""
runsc_args = ["--root", self._state_dir, "exec", self._id, cmd] + list(args)
try:
Expand All @@ -273,9 +272,9 @@ def exec(
)
return result.stdout, result.stderr
except subprocess.TimeoutExpired as e:
raise SandboxError(f"exec timed out after {timeout} seconds") from e
raise Error(f"exec timed out after {timeout} seconds") from e
except subprocess.CalledProcessError as e:
raise SandboxError(f"exec failed: {e.stderr}") from e
raise Error(f"exec failed: {e.stderr}") from e

def close(self):
"""Kills the sandbox processes and cleans up directories."""
Expand Down
18 changes: 18 additions & 0 deletions sandboxexec/sandbox/python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "gvisor"
version = "0.1.0"
description = "Python bindings for gVisor."
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: POSIX :: Linux",
]
dependencies = []

[tool.setuptools]
packages = ["gvisor"]
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import tempfile
import unittest

from sandboxexec.sandbox import sandbox
from gvisor import sandbox


def find_runsc() -> str:
Expand Down Expand Up @@ -67,7 +67,7 @@ def test_exec_dmesg(self):
def test_exec_timeout(self):
enable_networking = os.geteuid() == 0
with sandbox.Sandbox(enable_networking=enable_networking) as sb:
with self.assertRaises(sandbox.SandboxError) as ctx:
with self.assertRaises(sandbox.Error) as ctx:
sb.exec("sleep", "10", timeout=1)
self.assertIn("exec timed out", str(ctx.exception))

Expand All @@ -80,15 +80,14 @@ def test_exec_with_args(self):
def test_exec_invalid_command_or_args(self):
enable_networking = os.geteuid() == 0
with sandbox.Sandbox(enable_networking=enable_networking) as sb:
with self.assertRaises(sandbox.SandboxError) as ctx:
with self.assertRaises(sandbox.Error) as ctx:
sb.exec("nonexistent_command_xyz123")
self.assertIn("exec failed", str(ctx.exception))

with self.assertRaises(sandbox.SandboxError) as ctx:
with self.assertRaises(sandbox.Error) as ctx:
sb.exec("ls", "--invalid-flag-xyz123")
self.assertIn("exec failed", str(ctx.exception))


def test_sandbox_options(self):
enable_networking = os.geteuid() == 0
with tempfile.TemporaryDirectory() as runtime_dir:
Expand All @@ -106,17 +105,18 @@ def test_non_root_networking_error(self):
self.skipTest("this test must be run as non-root")

before_tmp = set(os.listdir(tempfile.gettempdir()))
with self.assertRaises(sandbox.SandboxError) as ctx:
with self.assertRaises(sandbox.Error) as ctx:
sandbox.Sandbox(enable_networking=True)
after_tmp = set(os.listdir(tempfile.gettempdir()))

self.assertIn(
"enabling networking requires running as root", str(ctx.exception)
)
leaked = [f for f in after_tmp - before_tmp if f.startswith("gvisor-sandbox-")]
leaked = [
f for f in after_tmp - before_tmp if f.startswith("gvisor-sandbox-")
]
self.assertEqual(leaked, [])


def test_create_bundle(self):
# Test the internal _create_bundle method to verify config.json
for enable_networking in [False, True]:
Expand All @@ -133,7 +133,7 @@ def test_create_bundle(self):
sandbox_id=sandbox_id,
enable_networking=enable_networking,
)
except sandbox.SandboxError as e:
except sandbox.Error as e:
self.fail(f"Failed to create sandbox: {e}")

try:
Expand Down
Loading