gvisor 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gvisor/__init__.py +18 -0
- gvisor/sandbox.py +309 -0
- gvisor-0.0.1.dist-info/METADATA +36 -0
- gvisor-0.0.1.dist-info/RECORD +6 -0
- gvisor-0.0.1.dist-info/WHEEL +5 -0
- gvisor-0.0.1.dist-info/top_level.txt +1 -0
gvisor/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copyright 2026 The gVisor Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""gVisor sandboxexec sandbox package."""
|
|
16
|
+
|
|
17
|
+
from .sandbox import Error
|
|
18
|
+
from .sandbox import Sandbox
|
gvisor/sandbox.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Copyright 2026 The gVisor Authors.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Python binding for gVisor sandbox.
|
|
16
|
+
|
|
17
|
+
This module provides a Python API for creating gVisor sandboxes and executing
|
|
18
|
+
commands inside them.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import random
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import tempfile
|
|
27
|
+
from typing import Optional, Tuple
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Error(Exception):
|
|
31
|
+
"""Base exception for Sandbox operations."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Sandbox:
|
|
35
|
+
"""Represents a running gVisor sandbox."""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
runtime_dir: Optional[str] = None,
|
|
40
|
+
sandbox_id: Optional[str] = None,
|
|
41
|
+
enable_networking: bool = True,
|
|
42
|
+
):
|
|
43
|
+
"""Initializes and starts a new sandbox.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
runtime_dir: Custom runtime directory where bundle and state files are
|
|
47
|
+
written. If not set, a temporary directory is created.
|
|
48
|
+
sandbox_id: Specific sandbox ID. If not set, a unique ID is generated
|
|
49
|
+
automatically.
|
|
50
|
+
enable_networking: Whether networking is enabled inside the sandbox.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
Error: If sandbox creation fails.
|
|
54
|
+
"""
|
|
55
|
+
self._enable_networking = enable_networking
|
|
56
|
+
self._runtime_dir = ""
|
|
57
|
+
self._owns_runtime_dir = False
|
|
58
|
+
self._id = ""
|
|
59
|
+
self._state_dir = ""
|
|
60
|
+
self._bundle_dir = ""
|
|
61
|
+
self._closed = False
|
|
62
|
+
self._runsc_path = self._find_runsc()
|
|
63
|
+
|
|
64
|
+
if runtime_dir is None:
|
|
65
|
+
try:
|
|
66
|
+
self._runtime_dir = tempfile.mkdtemp(prefix="gvisor-sandbox-")
|
|
67
|
+
except OSError as e:
|
|
68
|
+
raise Error(f"failed to create runtime directory: {e}") from e
|
|
69
|
+
self._owns_runtime_dir = True
|
|
70
|
+
else:
|
|
71
|
+
self._runtime_dir = runtime_dir
|
|
72
|
+
self._owns_runtime_dir = False
|
|
73
|
+
|
|
74
|
+
self._id = sandbox_id or self._generate_id()
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
if os.geteuid() != 0 and self._enable_networking:
|
|
78
|
+
raise Error("enabling networking requires running as root")
|
|
79
|
+
|
|
80
|
+
self._state_dir = os.path.join(self._runtime_dir, "state")
|
|
81
|
+
try:
|
|
82
|
+
os.makedirs(self._state_dir, mode=0o700, exist_ok=True)
|
|
83
|
+
except OSError as e:
|
|
84
|
+
raise Error(f"failed to create sandbox state directory: {e}") from e
|
|
85
|
+
|
|
86
|
+
# Verify permissions (mode might be different if directory already
|
|
87
|
+
# existed).
|
|
88
|
+
try:
|
|
89
|
+
stat_info = os.stat(self._state_dir)
|
|
90
|
+
if (stat_info.st_mode & 0o777) != 0o700:
|
|
91
|
+
os.chmod(self._state_dir, 0o700)
|
|
92
|
+
except OSError as e:
|
|
93
|
+
raise Error(
|
|
94
|
+
"sandbox state directory has incorrect permissions and failed to"
|
|
95
|
+
f" chmod: {e}"
|
|
96
|
+
) from e
|
|
97
|
+
|
|
98
|
+
self._bundle_dir = self._create_bundle()
|
|
99
|
+
|
|
100
|
+
# Launch the sandbox in detached mode.
|
|
101
|
+
args = ["--root", self._state_dir]
|
|
102
|
+
if os.geteuid() != 0:
|
|
103
|
+
args.append("--ignore-cgroups")
|
|
104
|
+
if not self._enable_networking:
|
|
105
|
+
args.append("--network=none")
|
|
106
|
+
args.extend(["run", "--bundle", self._bundle_dir, "--detach", self._id])
|
|
107
|
+
|
|
108
|
+
# We must use a file for stderr because runsc run with --detach spawns a
|
|
109
|
+
# grandchild process. If we use pipes (e.g. capture_output=True), Python's
|
|
110
|
+
# subprocess.run will hang waiting for the pipes to close. Writing to a
|
|
111
|
+
# file avoids this hang while still allowing us to capture errors.
|
|
112
|
+
stderr_path = os.path.join(self._runtime_dir, "runsc-stderr.log")
|
|
113
|
+
try:
|
|
114
|
+
with open(stderr_path, "w+b") as stderr_file:
|
|
115
|
+
subprocess.run(
|
|
116
|
+
[self._runsc_path] + args,
|
|
117
|
+
check=True,
|
|
118
|
+
stdout=subprocess.DEVNULL,
|
|
119
|
+
stderr=stderr_file,
|
|
120
|
+
timeout=30,
|
|
121
|
+
)
|
|
122
|
+
except subprocess.TimeoutExpired as e:
|
|
123
|
+
raise Error("sandbox creation timed out (runsc run hung)") from e
|
|
124
|
+
except subprocess.CalledProcessError as e:
|
|
125
|
+
stderr_content = ""
|
|
126
|
+
if os.path.exists(stderr_path):
|
|
127
|
+
try:
|
|
128
|
+
with open(stderr_path, "r", errors="replace") as f:
|
|
129
|
+
stderr_content = f.read()
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
raise Error(
|
|
133
|
+
"failed to create sandbox via subprocess: exit code"
|
|
134
|
+
f" {e.returncode}, stderr: {stderr_content}"
|
|
135
|
+
) from e
|
|
136
|
+
except Exception:
|
|
137
|
+
self.close()
|
|
138
|
+
raise
|
|
139
|
+
|
|
140
|
+
def __enter__(self) -> "Sandbox":
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
144
|
+
self.close()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def id(self) -> str:
|
|
148
|
+
"""Returns the sandbox ID."""
|
|
149
|
+
return self._id
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def bundle_dir(self) -> str:
|
|
153
|
+
"""Returns the path to the OCI bundle directory."""
|
|
154
|
+
return self._bundle_dir
|
|
155
|
+
|
|
156
|
+
def _generate_id(self) -> str:
|
|
157
|
+
return f"{random.getrandbits(128):032x}"
|
|
158
|
+
|
|
159
|
+
def _find_runsc(self) -> str:
|
|
160
|
+
if "RUNSC_PATH" in os.environ:
|
|
161
|
+
return os.environ["RUNSC_PATH"]
|
|
162
|
+
path = shutil.which("runsc")
|
|
163
|
+
if path is not None:
|
|
164
|
+
return path
|
|
165
|
+
raise Error("runsc binary is not found")
|
|
166
|
+
|
|
167
|
+
def _create_bundle(self) -> str:
|
|
168
|
+
"""Creates the OCI bundle directory and config.json.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The path to the created bundle directory.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
Error: If bundle creation fails.
|
|
175
|
+
"""
|
|
176
|
+
bundle_dir = os.path.join(self._runtime_dir, self._id)
|
|
177
|
+
rootfs_dir = os.path.join(bundle_dir, "rootfs")
|
|
178
|
+
try:
|
|
179
|
+
os.makedirs(rootfs_dir, mode=0o755, exist_ok=True)
|
|
180
|
+
except OSError as e:
|
|
181
|
+
raise Error(f"failed to create bundle directories: {e}") from e
|
|
182
|
+
|
|
183
|
+
namespaces = [
|
|
184
|
+
{"type": "pid"},
|
|
185
|
+
{"type": "mount"},
|
|
186
|
+
{"type": "uts"},
|
|
187
|
+
{"type": "ipc"},
|
|
188
|
+
]
|
|
189
|
+
if os.geteuid() != 0:
|
|
190
|
+
namespaces.append({"type": "user"})
|
|
191
|
+
if self._enable_networking:
|
|
192
|
+
namespaces.append({"type": "network"})
|
|
193
|
+
|
|
194
|
+
mounts = [
|
|
195
|
+
{"destination": "/proc", "type": "proc", "source": "proc"},
|
|
196
|
+
{"destination": "/dev", "type": "tmpfs", "source": "tmpfs"},
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for p in ["/bin", "/usr", "/lib", "/lib64", "/etc/alternatives"]:
|
|
200
|
+
if os.path.exists(p):
|
|
201
|
+
opts = ["rbind", "ro", "nosuid", "nodev"]
|
|
202
|
+
if p == "/etc/alternatives":
|
|
203
|
+
opts = ["rbind", "ro"]
|
|
204
|
+
mounts.append({
|
|
205
|
+
"destination": p,
|
|
206
|
+
"type": "bind",
|
|
207
|
+
"source": p,
|
|
208
|
+
"options": opts,
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
linux = {
|
|
212
|
+
"namespaces": namespaces,
|
|
213
|
+
}
|
|
214
|
+
if os.geteuid() != 0:
|
|
215
|
+
linux["uidMappings"] = [
|
|
216
|
+
{"containerID": 0, "hostID": os.geteuid(), "size": 1}
|
|
217
|
+
]
|
|
218
|
+
linux["gidMappings"] = [
|
|
219
|
+
{"containerID": 0, "hostID": os.getegid(), "size": 1}
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
spec = {
|
|
223
|
+
"ociVersion": "1.0.0",
|
|
224
|
+
"root": {
|
|
225
|
+
"path": "rootfs",
|
|
226
|
+
"readonly": True,
|
|
227
|
+
},
|
|
228
|
+
"process": {
|
|
229
|
+
"terminal": False,
|
|
230
|
+
"user": {"uid": 0, "gid": 0},
|
|
231
|
+
"args": ["sleep", "infinity"],
|
|
232
|
+
"cwd": "/",
|
|
233
|
+
"env": ["PATH=/bin:/usr/bin:/usr/local/bin"],
|
|
234
|
+
},
|
|
235
|
+
"mounts": mounts,
|
|
236
|
+
"linux": linux,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
config_path = os.path.join(bundle_dir, "config.json")
|
|
240
|
+
try:
|
|
241
|
+
with open(config_path, "w") as f:
|
|
242
|
+
json.dump(spec, f, indent=2)
|
|
243
|
+
except OSError as e:
|
|
244
|
+
raise Error(f"failed to create config.json: {e}") from e
|
|
245
|
+
|
|
246
|
+
return bundle_dir
|
|
247
|
+
|
|
248
|
+
def exec(
|
|
249
|
+
self, cmd: str, *args: str, timeout: Optional[float] = None
|
|
250
|
+
) -> Tuple[str, str]:
|
|
251
|
+
"""Runs the given command inside the running sandbox.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
cmd: The command to run.
|
|
255
|
+
*args: Arguments to the command.
|
|
256
|
+
timeout: Timeout in seconds.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
A tuple of (stdout, stderr) strings.
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
Error: If the command execution fails or times out.
|
|
263
|
+
"""
|
|
264
|
+
runsc_args = ["--root", self._state_dir, "exec", self._id, cmd] + list(args)
|
|
265
|
+
try:
|
|
266
|
+
result = subprocess.run(
|
|
267
|
+
[self._runsc_path] + runsc_args,
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
check=True,
|
|
271
|
+
timeout=timeout,
|
|
272
|
+
)
|
|
273
|
+
return result.stdout, result.stderr
|
|
274
|
+
except subprocess.TimeoutExpired as e:
|
|
275
|
+
raise Error(f"exec timed out after {timeout} seconds") from e
|
|
276
|
+
except subprocess.CalledProcessError as e:
|
|
277
|
+
raise Error(f"exec failed: {e.stderr}") from e
|
|
278
|
+
|
|
279
|
+
def close(self):
|
|
280
|
+
"""Kills the sandbox processes and cleans up directories."""
|
|
281
|
+
if self._closed:
|
|
282
|
+
return
|
|
283
|
+
self._closed = True
|
|
284
|
+
|
|
285
|
+
if self._state_dir:
|
|
286
|
+
kill_args = ["--root", self._state_dir, "kill", self._id, "SIGKILL"]
|
|
287
|
+
subprocess.run(
|
|
288
|
+
[self._runsc_path] + kill_args, capture_output=True, check=False
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
delete_args = ["--root", self._state_dir, "delete", "--force", self._id]
|
|
292
|
+
delete_result = subprocess.run(
|
|
293
|
+
[self._runsc_path] + delete_args, capture_output=True, check=False
|
|
294
|
+
)
|
|
295
|
+
if delete_result.returncode != 0:
|
|
296
|
+
# We might want to log this, but we continue cleanup anyway.
|
|
297
|
+
pass
|
|
298
|
+
|
|
299
|
+
if self._bundle_dir and os.path.exists(self._bundle_dir):
|
|
300
|
+
try:
|
|
301
|
+
shutil.rmtree(self._bundle_dir)
|
|
302
|
+
except OSError:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
if self._owns_runtime_dir and os.path.exists(self._runtime_dir):
|
|
306
|
+
try:
|
|
307
|
+
shutil.rmtree(self._runtime_dir)
|
|
308
|
+
except OSError:
|
|
309
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gvisor
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python bindings for gVisor.
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/google/gvisor
|
|
7
|
+
Project-URL: Bug-Tracker, https://github.com/google/gvisor/issues
|
|
8
|
+
Project-URL: Website, https://gvisor.dev
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: System :: Operating System Kernels
|
|
21
|
+
Requires-Python: >=3.7
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# gVisor Python Bindings
|
|
25
|
+
|
|
26
|
+
Python bindings for [gVisor](https://github.com/google/gvisor).
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
29
|
+
|
|
30
|
+
gVisor is an application kernel, written in Go, that implements a substantial portion of the Linux system surface. It includes an Open Container Initiative (OCI) runtime called `runsc` that provides an isolation boundary between the application and the host kernel. The `runsc` runtime delivers strong sandbox isolation while still allowing applications to behave as they would under standard runtimes.
|
|
31
|
+
|
|
32
|
+
These Python bindings provide a programmable interface to interact with gVisor.
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
Apache License 2.0
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
gvisor/__init__.py,sha256=uziV7Be4VFYl3e6HcSPNPXk5syPlxfF2twYyKJJpf3Q,684
|
|
2
|
+
gvisor/sandbox.py,sha256=FeDrhd5Qp2bTmqLj81QP0G_vBYf8cUd8RijTd2qkogg,9416
|
|
3
|
+
gvisor-0.0.1.dist-info/METADATA,sha256=pVJkY-_cpdactenZnOfuyi5JoQQWtG08V3gA2iJfRCo,1580
|
|
4
|
+
gvisor-0.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
gvisor-0.0.1.dist-info/top_level.txt,sha256=0JH2F4Xgt4ILnWpb12zgZQw_vs7v6dIdqXY9q4UP8JE,7
|
|
6
|
+
gvisor-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gvisor
|