hpc-sandbox 0.3.0__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.
- hpc_sandbox/__init__.py +116 -0
- hpc_sandbox/__main__.py +5 -0
- hpc_sandbox/anchor.py +322 -0
- hpc_sandbox/cli.py +628 -0
- hpc_sandbox/enroot.py +1235 -0
- hpc_sandbox/holder.py +211 -0
- hpc_sandbox/images.py +506 -0
- hpc_sandbox/locks.py +76 -0
- hpc_sandbox/memory.py +449 -0
- hpc_sandbox/paths.py +57 -0
- hpc_sandbox/pods.py +1251 -0
- hpc_sandbox/process.py +252 -0
- hpc_sandbox/procfs.py +322 -0
- hpc_sandbox/py.typed +0 -0
- hpc_sandbox/runtime.py +153 -0
- hpc_sandbox/safefs.py +478 -0
- hpc_sandbox/sandbox.py +1284 -0
- hpc_sandbox/sessions.py +613 -0
- hpc_sandbox/singularity.py +994 -0
- hpc_sandbox/tools.py +151 -0
- hpc_sandbox/types.py +257 -0
- hpc_sandbox-0.3.0.dist-info/METADATA +202 -0
- hpc_sandbox-0.3.0.dist-info/RECORD +26 -0
- hpc_sandbox-0.3.0.dist-info/WHEEL +4 -0
- hpc_sandbox-0.3.0.dist-info/entry_points.txt +2 -0
- hpc_sandbox-0.3.0.dist-info/licenses/LICENSE +21 -0
hpc_sandbox/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Rootless sandboxes for HPC clusters, on Singularity/Apptainer or enroot.
|
|
2
|
+
|
|
3
|
+
The names in ``__all__`` are the public API. ``hpc_sandbox.__version__`` is the running
|
|
4
|
+
hpc-sandbox's version: a source checkout's ``pyproject.toml``, else the installed distribution's.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from .enroot import EnrootProvider, EnrootRuntime, EnrootSandbox, EnrootService, SquashfsImages
|
|
11
|
+
from .images import ImageFormat, ImageStore, ResolvedImage, SifImages
|
|
12
|
+
from .pods import PodService, SessionPod
|
|
13
|
+
from .runtime import ContainerRuntime
|
|
14
|
+
from .sessions import JanitorReport
|
|
15
|
+
from .singularity import SingularityProvider, SingularitySandbox, SingularityService
|
|
16
|
+
from .types import (
|
|
17
|
+
CWD_ERROR_EXIT_CODE,
|
|
18
|
+
CWD_ERROR_PREFIX,
|
|
19
|
+
CWD_ERROR_REASONS,
|
|
20
|
+
DownloadReport,
|
|
21
|
+
DroppedLink,
|
|
22
|
+
ExecResult,
|
|
23
|
+
HealthCheck,
|
|
24
|
+
Mount,
|
|
25
|
+
Pod,
|
|
26
|
+
PodNetwork,
|
|
27
|
+
PodProvider,
|
|
28
|
+
PodSpec,
|
|
29
|
+
Sandbox,
|
|
30
|
+
SandboxError,
|
|
31
|
+
SandboxLostError,
|
|
32
|
+
SandboxProvider,
|
|
33
|
+
SandboxSnapshot,
|
|
34
|
+
SandboxSpec,
|
|
35
|
+
SandboxStartError,
|
|
36
|
+
ServiceHealth,
|
|
37
|
+
ServiceSpec,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"CWD_ERROR_EXIT_CODE",
|
|
42
|
+
"CWD_ERROR_PREFIX",
|
|
43
|
+
"CWD_ERROR_REASONS",
|
|
44
|
+
"ContainerRuntime",
|
|
45
|
+
"DownloadReport",
|
|
46
|
+
"DroppedLink",
|
|
47
|
+
"EnrootProvider",
|
|
48
|
+
"EnrootRuntime",
|
|
49
|
+
"EnrootSandbox",
|
|
50
|
+
"EnrootService",
|
|
51
|
+
"ExecResult",
|
|
52
|
+
"HealthCheck",
|
|
53
|
+
"ImageFormat",
|
|
54
|
+
"ImageStore",
|
|
55
|
+
"JanitorReport",
|
|
56
|
+
"Mount",
|
|
57
|
+
"Pod",
|
|
58
|
+
"PodNetwork",
|
|
59
|
+
"PodProvider",
|
|
60
|
+
"PodService",
|
|
61
|
+
"PodSpec",
|
|
62
|
+
"ResolvedImage",
|
|
63
|
+
"Sandbox",
|
|
64
|
+
"SandboxError",
|
|
65
|
+
"SandboxLostError",
|
|
66
|
+
"SandboxProvider",
|
|
67
|
+
"SandboxSnapshot",
|
|
68
|
+
"SandboxSpec",
|
|
69
|
+
"SandboxStartError",
|
|
70
|
+
"ServiceHealth",
|
|
71
|
+
"ServiceSpec",
|
|
72
|
+
"SessionPod",
|
|
73
|
+
"SifImages",
|
|
74
|
+
"SingularityProvider",
|
|
75
|
+
"SingularitySandbox",
|
|
76
|
+
"SingularityService",
|
|
77
|
+
"SquashfsImages",
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
# Where a source checkout keeps its pyproject.toml: this file is src/hpc_sandbox/__init__.py.
|
|
81
|
+
# Not parents[2], which raises IndexError when the package sits right below / (/hpc_sandbox).
|
|
82
|
+
_CHECKOUT_PYPROJECT = Path(__file__).resolve().parent.parent.parent / "pyproject.toml"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _running_version(pyproject: Path = _CHECKOUT_PYPROJECT) -> str:
|
|
86
|
+
"""The version of the running hpc-sandbox: that of ``pyproject`` when it is hpc-sandbox's (a
|
|
87
|
+
source checkout, which may shadow an installed copy whose metadata is stale), else the installed
|
|
88
|
+
distribution's, else ``"0+unknown"``."""
|
|
89
|
+
import importlib.metadata
|
|
90
|
+
import tomllib
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
project = tomllib.loads(pyproject.read_text(encoding="utf-8")).get("project")
|
|
94
|
+
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError):
|
|
95
|
+
project = None
|
|
96
|
+
if (
|
|
97
|
+
isinstance(project, dict)
|
|
98
|
+
and project.get("name") == "hpc-sandbox"
|
|
99
|
+
and project.get("version")
|
|
100
|
+
):
|
|
101
|
+
return str(project["version"])
|
|
102
|
+
try:
|
|
103
|
+
return importlib.metadata.version("hpc-sandbox")
|
|
104
|
+
except importlib.metadata.PackageNotFoundError:
|
|
105
|
+
return "0+unknown"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def __getattr__(name: str) -> str:
|
|
109
|
+
if name == "__version__": # read on first use, then kept as a plain attribute
|
|
110
|
+
version = globals()["__version__"] = _running_version()
|
|
111
|
+
return version
|
|
112
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if TYPE_CHECKING:
|
|
116
|
+
__version__: str
|
hpc_sandbox/__main__.py
ADDED
hpc_sandbox/anchor.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Create a sandbox's namespaces and run its container as their PID 1.
|
|
2
|
+
|
|
3
|
+
:mod:`hpc_sandbox.enroot` runs this file as a script in a fresh interpreter::
|
|
4
|
+
|
|
5
|
+
python -I -S anchor.py PARENT_PID NETWORK -- COMMAND [ARG...]
|
|
6
|
+
python -I -S anchor.py PARENT_PID join HOLDER_PID [OPTION...] -- COMMAND [ARG...]
|
|
7
|
+
|
|
8
|
+
The first form starts a sandbox of its own. It
|
|
9
|
+
|
|
10
|
+
1. arranges to be SIGKILLed when its parent (the provider process) dies, and
|
|
11
|
+
exits at once if that already happened;
|
|
12
|
+
2. creates new user, mount, PID and IPC namespaces, mapping the calling user
|
|
13
|
+
and group to themselves, plus a network namespace whose only interface is
|
|
14
|
+
an up loopback when NETWORK is ``none``;
|
|
15
|
+
3. forks the PID namespace's init, which arranges to be SIGKILLed if this
|
|
16
|
+
process dies, mounts a fresh ``/proc`` and executes COMMAND (``enroot
|
|
17
|
+
start ...`` with a keep-alive loop as the container's command);
|
|
18
|
+
4. waits for it and exits with its status.
|
|
19
|
+
|
|
20
|
+
When the init process dies, the kernel kills every process in its PID
|
|
21
|
+
namespace, so killing it stops the whole sandbox. Only unprivileged user
|
|
22
|
+
namespaces are needed: the ``unshare`` command line is avoided because the
|
|
23
|
+
options this needs are missing from older util-linux releases.
|
|
24
|
+
|
|
25
|
+
The ``join`` form starts a service of a pod (see :mod:`hpc_sandbox.holder`):
|
|
26
|
+
it joins the user and network namespaces of the pod's holder, creates mount,
|
|
27
|
+
PID, IPC and UTS namespaces, and forks an init of its own as their PID 1. The
|
|
28
|
+
init sets the service's hostname (``--hostname NAME``), resource limits
|
|
29
|
+
(``--rlimit NAME=SOFT:HARD``, within the hard limits it has) and an optional
|
|
30
|
+
sized tmpfs (``--shm DIR:BYTES``, for the runtime to bind at ``/dev/shm``),
|
|
31
|
+
then runs COMMAND as its child, in a nested user namespace mapping root to
|
|
32
|
+
root with ``--userns``, with its output through a pipe this process copies to
|
|
33
|
+
its own (so the service cannot truncate the log, and no file size limit of the
|
|
34
|
+
service applies to it). Root in a nested user namespace holds no capability
|
|
35
|
+
over the pod's network. The init forwards SIGTERM, SIGINT and SIGHUP to the
|
|
36
|
+
child, reaps orphans, and exits with the child's status; the runtime is never
|
|
37
|
+
PID 1, so killing the init always ends the service.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
from __future__ import annotations
|
|
41
|
+
|
|
42
|
+
import ctypes
|
|
43
|
+
import fcntl
|
|
44
|
+
import os
|
|
45
|
+
import resource
|
|
46
|
+
import select
|
|
47
|
+
import signal
|
|
48
|
+
import socket
|
|
49
|
+
import struct
|
|
50
|
+
import sys
|
|
51
|
+
|
|
52
|
+
CLONE_NEWNS = 0x00020000
|
|
53
|
+
CLONE_NEWUTS = 0x04000000
|
|
54
|
+
CLONE_NEWIPC = 0x08000000
|
|
55
|
+
CLONE_NEWUSER = 0x10000000
|
|
56
|
+
CLONE_NEWPID = 0x20000000
|
|
57
|
+
CLONE_NEWNET = 0x40000000
|
|
58
|
+
MS_NOSUID = 0x2
|
|
59
|
+
MS_NODEV = 0x4
|
|
60
|
+
MS_NOEXEC = 0x8
|
|
61
|
+
MS_REC = 0x4000
|
|
62
|
+
MS_PRIVATE = 0x40000
|
|
63
|
+
PR_SET_PDEATHSIG = 1
|
|
64
|
+
SIOCGIFFLAGS = 0x8913
|
|
65
|
+
SIOCSIFFLAGS = 0x8914
|
|
66
|
+
IFF_UP = 0x1
|
|
67
|
+
|
|
68
|
+
_libc = ctypes.CDLL(None, use_errno=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _check(result: int, what: str) -> None:
|
|
72
|
+
if result != 0:
|
|
73
|
+
errno = ctypes.get_errno()
|
|
74
|
+
raise OSError(errno, f"{what}: {os.strerror(errno)}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _write(path: str, text: str) -> None:
|
|
78
|
+
with open(path, "w") as stream:
|
|
79
|
+
stream.write(text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _die_with_parent() -> None:
|
|
83
|
+
_check(_libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL, 0, 0, 0), "prctl(PR_SET_PDEATHSIG)")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _loopback_up() -> None:
|
|
87
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
|
88
|
+
request = struct.pack("16sh", b"lo", 0)
|
|
89
|
+
flags = struct.unpack("16sh", fcntl.ioctl(sock, SIOCGIFFLAGS, request))[1]
|
|
90
|
+
fcntl.ioctl(sock, SIOCSIFFLAGS, struct.pack("16sh", b"lo", flags | IFF_UP))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _set_rlimit(name: str, soft: int, hard: int) -> None:
|
|
94
|
+
"""Set a limit (-1: unlimited) within the hard limit this process already has."""
|
|
95
|
+
which = getattr(resource, f"RLIMIT_{name.upper()}", None)
|
|
96
|
+
if which is None:
|
|
97
|
+
raise OSError(22, f"unknown resource limit {name}")
|
|
98
|
+
infinity = resource.RLIM_INFINITY
|
|
99
|
+
soft, hard = (infinity if value < 0 else value for value in (soft, hard))
|
|
100
|
+
current = resource.getrlimit(which)[1]
|
|
101
|
+
if current != infinity and (hard == infinity or hard > current):
|
|
102
|
+
hard = current
|
|
103
|
+
if soft == infinity or (hard != infinity and soft > hard):
|
|
104
|
+
soft = hard
|
|
105
|
+
try:
|
|
106
|
+
resource.setrlimit(which, (soft, hard))
|
|
107
|
+
except ValueError as error:
|
|
108
|
+
raise OSError(22, f"limit {name}: {error}") from None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _setns(path: str, nstype: int) -> None:
|
|
112
|
+
fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC)
|
|
113
|
+
try:
|
|
114
|
+
_check(_libc.setns(fd, nstype), f"setns({path})")
|
|
115
|
+
finally:
|
|
116
|
+
os.close(fd)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _join(argv: list[str]) -> int:
|
|
120
|
+
"""The ``join`` form: a pod service below the holder ``argv[0]``."""
|
|
121
|
+
holder, options, command = argv[0], argv[1 : argv.index("--")], argv[argv.index("--") + 1 :]
|
|
122
|
+
hostname, rlimits, shm, nested = None, [], None, False
|
|
123
|
+
while options:
|
|
124
|
+
option = options.pop(0)
|
|
125
|
+
if option == "--hostname":
|
|
126
|
+
hostname = options.pop(0)
|
|
127
|
+
elif option == "--rlimit":
|
|
128
|
+
name, _, values = options.pop(0).partition("=")
|
|
129
|
+
soft, _, hard = values.partition(":")
|
|
130
|
+
rlimits.append((name, int(soft), int(hard)))
|
|
131
|
+
elif option == "--shm":
|
|
132
|
+
directory, _, size = options.pop(0).rpartition(":")
|
|
133
|
+
shm = (directory, int(size))
|
|
134
|
+
elif option == "--userns":
|
|
135
|
+
nested = True
|
|
136
|
+
else:
|
|
137
|
+
raise OSError(22, f"unknown option {option}")
|
|
138
|
+
# The user namespace first: it owns the network namespace.
|
|
139
|
+
_setns(f"/proc/{holder}/ns/user", CLONE_NEWUSER)
|
|
140
|
+
_setns(f"/proc/{holder}/ns/net", CLONE_NEWNET)
|
|
141
|
+
_check(_libc.unshare(CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWUTS), "unshare")
|
|
142
|
+
_check(_libc.mount(b"none", b"/", None, MS_REC | MS_PRIVATE, None), "make / private")
|
|
143
|
+
# The service writes to a pipe this process copies to its own output, as
|
|
144
|
+
# Docker's shim does: what the service opens as /dev/stdout is the pipe.
|
|
145
|
+
output_read, output_write = os.pipe()
|
|
146
|
+
alive_read, alive_write = os.pipe()
|
|
147
|
+
init = os.fork()
|
|
148
|
+
if init == 0:
|
|
149
|
+
os.close(alive_write)
|
|
150
|
+
os.close(output_read)
|
|
151
|
+
try:
|
|
152
|
+
_die_with_parent()
|
|
153
|
+
if select.select([alive_read], [], [], 0)[0]:
|
|
154
|
+
os._exit(1)
|
|
155
|
+
os.close(alive_read)
|
|
156
|
+
os.dup2(output_write, 1)
|
|
157
|
+
os.dup2(output_write, 2)
|
|
158
|
+
os.close(output_write)
|
|
159
|
+
os._exit(_service_init(hostname, rlimits, shm, nested, command))
|
|
160
|
+
except OSError as error:
|
|
161
|
+
print(f"hpc-sandbox: {error}", file=sys.stderr)
|
|
162
|
+
os._exit(1)
|
|
163
|
+
os.close(alive_read)
|
|
164
|
+
os.close(output_write)
|
|
165
|
+
# Until everything in the service is gone (no threads here: this process
|
|
166
|
+
# can no longer start any, its children being in another PID namespace).
|
|
167
|
+
_copy(output_read, 1)
|
|
168
|
+
return _wait(init)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _copy(source: int, target: int) -> None:
|
|
172
|
+
"""Copy ``source`` to ``target`` until its end, dropping what cannot be written."""
|
|
173
|
+
while chunk := os.read(source, 65536):
|
|
174
|
+
view = memoryview(chunk)
|
|
175
|
+
while view:
|
|
176
|
+
try:
|
|
177
|
+
written = os.write(target, view)
|
|
178
|
+
except OSError:
|
|
179
|
+
break
|
|
180
|
+
view = view[written:]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _service_init(
|
|
184
|
+
hostname: str | None,
|
|
185
|
+
rlimits: list[tuple[str, int, int]],
|
|
186
|
+
shm: tuple[str, int] | None,
|
|
187
|
+
nested: bool,
|
|
188
|
+
command: list[str],
|
|
189
|
+
) -> int:
|
|
190
|
+
"""PID 1 of a service: set it up, run COMMAND as a child, pass on signals, reap."""
|
|
191
|
+
_check(
|
|
192
|
+
_libc.mount(b"proc", b"/proc", b"proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, None),
|
|
193
|
+
"mount /proc",
|
|
194
|
+
)
|
|
195
|
+
if hostname:
|
|
196
|
+
name = hostname.encode()
|
|
197
|
+
_check(_libc.sethostname(name, len(name)), "sethostname")
|
|
198
|
+
for name, soft, hard in rlimits:
|
|
199
|
+
_set_rlimit(name, soft, hard)
|
|
200
|
+
if shm is not None:
|
|
201
|
+
directory, size = shm
|
|
202
|
+
options = f"size={size},mode=1777".encode()
|
|
203
|
+
_check(
|
|
204
|
+
_libc.mount(b"tmpfs", directory.encode(), b"tmpfs", MS_NOSUID | MS_NODEV, options),
|
|
205
|
+
f"mount tmpfs at {directory}",
|
|
206
|
+
)
|
|
207
|
+
child = os.fork()
|
|
208
|
+
if child == 0:
|
|
209
|
+
try:
|
|
210
|
+
_die_with_parent()
|
|
211
|
+
if nested:
|
|
212
|
+
_check(_libc.unshare(CLONE_NEWUSER), "unshare (nested user namespace)")
|
|
213
|
+
_write("/proc/self/setgroups", "deny")
|
|
214
|
+
_write("/proc/self/uid_map", "0 0 1")
|
|
215
|
+
_write("/proc/self/gid_map", "0 0 1")
|
|
216
|
+
for ignored in (signal.SIGPIPE, signal.SIGXFSZ):
|
|
217
|
+
signal.signal(ignored, signal.SIG_DFL)
|
|
218
|
+
os.execvp(command[0], command)
|
|
219
|
+
except OSError as error:
|
|
220
|
+
print(f"hpc-sandbox: cannot run {command[0]}: {error}", file=sys.stderr)
|
|
221
|
+
os._exit(127)
|
|
222
|
+
for forwarded in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
|
|
223
|
+
signal.signal(forwarded, lambda signum, _frame: _forward(child, signum))
|
|
224
|
+
status = 0
|
|
225
|
+
while True: # reap orphans until the command itself exits
|
|
226
|
+
try:
|
|
227
|
+
pid, raw = os.waitpid(-1, 0)
|
|
228
|
+
except InterruptedError:
|
|
229
|
+
continue
|
|
230
|
+
except ChildProcessError:
|
|
231
|
+
break
|
|
232
|
+
if pid == child:
|
|
233
|
+
status = 128 + os.WTERMSIG(raw) if os.WIFSIGNALED(raw) else os.WEXITSTATUS(raw)
|
|
234
|
+
break
|
|
235
|
+
return status
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _forward(child: int, signum: int) -> None:
|
|
239
|
+
try:
|
|
240
|
+
os.kill(child, signum)
|
|
241
|
+
except ProcessLookupError:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _wait(child: int) -> int:
|
|
246
|
+
while True:
|
|
247
|
+
try:
|
|
248
|
+
_, status = os.waitpid(child, 0)
|
|
249
|
+
break
|
|
250
|
+
except InterruptedError:
|
|
251
|
+
continue
|
|
252
|
+
if os.WIFSIGNALED(status):
|
|
253
|
+
return 128 + os.WTERMSIG(status)
|
|
254
|
+
return os.WEXITSTATUS(status)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def main(argv: list[str]) -> int:
|
|
258
|
+
if len(argv) >= 5 and argv[1] == "join" and "--" in argv[3:]:
|
|
259
|
+
_die_with_parent()
|
|
260
|
+
if os.getppid() != int(argv[0]):
|
|
261
|
+
return 1 # the parent died before PR_SET_PDEATHSIG took effect
|
|
262
|
+
return _join(argv[2:])
|
|
263
|
+
if len(argv) < 4 or argv[2] != "--" or argv[1] not in ("host", "none"):
|
|
264
|
+
print(
|
|
265
|
+
"usage: anchor.py PARENT_PID host|none -- COMMAND [ARG...]\n"
|
|
266
|
+
" anchor.py PARENT_PID join HOLDER_PID [OPTION...] -- COMMAND [ARG...]",
|
|
267
|
+
file=sys.stderr,
|
|
268
|
+
)
|
|
269
|
+
return 2
|
|
270
|
+
parent, network, command = int(argv[0]), argv[1], argv[3:]
|
|
271
|
+
_die_with_parent()
|
|
272
|
+
if os.getppid() != parent:
|
|
273
|
+
return 1 # the parent died before PR_SET_PDEATHSIG took effect
|
|
274
|
+
uid, gid = os.getuid(), os.getgid()
|
|
275
|
+
flags = CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWIPC
|
|
276
|
+
if network == "none":
|
|
277
|
+
flags |= CLONE_NEWNET
|
|
278
|
+
_check(_libc.unshare(flags), "unshare (are unprivileged user namespaces enabled?)")
|
|
279
|
+
_write("/proc/self/setgroups", "deny")
|
|
280
|
+
_write("/proc/self/uid_map", f"{uid} {uid} 1")
|
|
281
|
+
_write("/proc/self/gid_map", f"{gid} {gid} 1")
|
|
282
|
+
_check(_libc.mount(b"none", b"/", None, MS_REC | MS_PRIVATE, None), "make / private")
|
|
283
|
+
if network == "none":
|
|
284
|
+
_loopback_up()
|
|
285
|
+
# The init process watches this pipe: EOF means this process is already gone.
|
|
286
|
+
alive_read, alive_write = os.pipe()
|
|
287
|
+
child = os.fork()
|
|
288
|
+
if child == 0:
|
|
289
|
+
os.close(alive_write)
|
|
290
|
+
_die_with_parent()
|
|
291
|
+
if select.select([alive_read], [], [], 0)[0]:
|
|
292
|
+
os._exit(1)
|
|
293
|
+
os.close(alive_read)
|
|
294
|
+
_check(
|
|
295
|
+
_libc.mount(b"proc", b"/proc", b"proc", MS_NOSUID | MS_NODEV | MS_NOEXEC, None),
|
|
296
|
+
"mount /proc",
|
|
297
|
+
)
|
|
298
|
+
for ignored in (signal.SIGPIPE, signal.SIGXFSZ): # ignored by Python, not by containers
|
|
299
|
+
signal.signal(ignored, signal.SIG_DFL)
|
|
300
|
+
try:
|
|
301
|
+
os.execvp(command[0], command)
|
|
302
|
+
except OSError as error:
|
|
303
|
+
print(f"hpc-sandbox: cannot run {command[0]}: {error}", file=sys.stderr)
|
|
304
|
+
os._exit(127)
|
|
305
|
+
os.close(alive_read)
|
|
306
|
+
while True:
|
|
307
|
+
try:
|
|
308
|
+
_, status = os.waitpid(child, 0)
|
|
309
|
+
break
|
|
310
|
+
except InterruptedError:
|
|
311
|
+
continue
|
|
312
|
+
if os.WIFSIGNALED(status):
|
|
313
|
+
return 128 + os.WTERMSIG(status)
|
|
314
|
+
return os.WEXITSTATUS(status)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == "__main__":
|
|
318
|
+
try:
|
|
319
|
+
sys.exit(main(sys.argv[1:]))
|
|
320
|
+
except OSError as error:
|
|
321
|
+
print(f"hpc-sandbox: {error}", file=sys.stderr)
|
|
322
|
+
sys.exit(1)
|