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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ gvisor