3"""Two-sided escaped-descendant proofs for the image supervisor."""
5from __future__
import annotations
12from pathlib
import Path
14from hil_convergence_safety_runtime_mutations
import (
17 RESIDUE_TIMEOUT_SECONDS,
28IDENTITY_FIELD_COUNT = 2
29PROCESS_GROUP_FIELD, SESSION_FIELD, START_TIME_FIELD = 2, 3, 19
32def _write_escape_fixture(root: Path) -> Path:
33 """Write a payload whose grandchild escapes its inherited session."""
34 payload = root /
"escape-payload.py"
36 "import os, pathlib, sys, time\n"
37 "root = pathlib.Path(sys.argv[1])\n"
42 " held = os.open(root, os.O_RDONLY | os.O_DIRECTORY)\n"
45 " process = os.getpid()\n"
46 " raw = pathlib.Path('/proc/self/stat').read_bytes()\n"
47 " fields = raw[raw.rfind(b')') + 2:].split()\n"
48 " if os.getpgrp() != process or os.getsid(0) != process:\n"
50 " identity = f'{process}:{int(fields[19])}\\n'.encode('ascii')\n"
51 " identity_fd = os.open(root / 'escape-child.identity', "
52 "os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)\n"
53 " os.write(identity_fd, identity)\n"
54 " os.close(identity_fd)\n"
55 " group_fd = os.open(root / 'escape-child.bound', "
56 "os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)\n"
57 " os.write(group_fd, f'{process}\\n'.encode('ascii'))\n"
58 " os.close(group_fd)\n"
62 "deadline = time.monotonic() + 5.0\n"
63 "while not (root / 'escape-child.bound').exists():\n"
64 " if time.monotonic() >= deadline:\n"
70 payload.chmod(stat.S_IRUSR | stat.S_IWUSR)
71 entry = root /
"escape-entry.sh"
74 '[[ "$1" == "--selftest" ]] || exit 64\n'
75 f
'exec /usr/bin/python3 -B -I -S "{payload}" "{root}"\n',
78 entry.chmod(PRIVATE_MODE)
82def _read_escape_identity(root: Path) -> tuple[int, int]:
83 """Read the exact PID/start-time identity published by the fixture."""
84 raw = (root /
"escape-child.identity").read_bytes().strip().split(b
":")
85 group = (root /
"escape-child.bound").read_bytes().strip()
87 len(raw) != IDENTITY_FIELD_COUNT
88 or not all(value.isdigit()
for value
in raw)
89 or not group.isdigit()
91 message =
"escaped-descendant identity receipt is malformed"
92 raise RuntimeMutationError(message)
93 identity = int(raw[0]), int(raw[1])
94 if identity[0] <= 0
or int(group) != identity[0]:
95 message =
"escaped-descendant group receipt is inconsistent"
96 raise RuntimeMutationError(message)
100def _live_process_identity(process: int) -> tuple[int, int, int] |
None:
101 """Return one live process's group, session, and start time."""
103 raw = Path(f
"/proc/{process}/stat").read_bytes()
104 except FileNotFoundError:
106 except OSError
as error:
107 message =
"escaped-descendant identity is unreadable"
108 raise RuntimeMutationError(message)
from error
109 closing = raw.rfind(b
")")
110 fields = raw[closing + 2 :].split()
if closing >= 0
else []
111 identity_fields = (PROCESS_GROUP_FIELD, SESSION_FIELD, START_TIME_FIELD)
112 if len(fields) <= START_TIME_FIELD
or not all(
113 fields[index].isdigit()
for index
in identity_fields
115 message =
"escaped-descendant process identity is malformed"
116 raise RuntimeMutationError(message)
117 if fields[0] == b
"Z":
119 return tuple(int(fields[index])
for index
in identity_fields)
122def _escape_is_live(identity: tuple[int, int]) -> bool:
123 """Prove the receipt still names its isolated escaped process."""
124 process, start_time = identity
125 return _live_process_identity(process) == (process, process, start_time)
128def _terminate_escape(identity: tuple[int, int]) -> bool:
129 """Kill only the exact still-live escaped group and wait for disappearance."""
130 process, _start_time = identity
131 if not _escape_is_live(identity):
133 os.killpg(process, signal.SIGKILL)
134 deadline = time.monotonic() + RESIDUE_TIMEOUT_SECONDS
135 while time.monotonic() < deadline:
136 current = _live_process_identity(process)
139 if current != (process, process, identity[1]):
140 message =
"escaped-descendant PID identity changed during cleanup"
141 raise RuntimeMutationError(message)
142 time.sleep(POLL_SECONDS)
146def _require_empty_diagnostics(stderr: bytes) ->
None:
147 """Reject any unexpected diagnostic from the public supervisor."""
149 message =
"escaped-descendant supervisor emitted diagnostics"
150 raise RuntimeMutationError(message)
154 supervisor: str, process_source: str, cases_source: str
155) -> tuple[Path, tuple[int, int], tuple[int, int], int |
None, bool]:
156 """Run the public supervisor against one isolated escaping payload."""
157 root, root_identity = _create_root()
158 escape_identity: tuple[int, int] |
None =
None
160 main_path, process_path, cases_path = _write_sources(
161 root, supervisor, process_source, cases_source
163 entry = _write_escape_fixture(root)
164 bound = root /
"escape-supervisor.bound"
165 bound.write_bytes(b
"")
166 bound.chmod(stat.S_IRUSR | stat.S_IWUSR)
167 status, clean, stderr = _run_supervisor(
174 str(root /
"escape-supervisor.outer"),
175 str(root /
"escape-supervisor.status"),
177 _identity_text(root),
180 escape_identity = _read_escape_identity(root)
181 _require_empty_diagnostics(stderr)
182 except (OSError, RuntimeMutationError, subprocess.TimeoutExpired, ValueError):
183 identity_receipt = root /
"escape-child.identity"
184 if escape_identity
is None and identity_receipt.exists():
185 escape_identity = _read_escape_identity(root)
187 escape_identity
is not None
188 and _escape_is_live(escape_identity)
189 and not _terminate_escape(escape_identity)
191 message =
"escaped-descendant failure cleanup did not converge"
192 raise RuntimeMutationError(message)
from None
193 _remove_root(root, root_identity)
196 return root, root_identity, escape_identity, status, clean
199def cases(inputs: dict[str, str]) -> list[tuple[str, bool]]:
200 """Prove subreaper cleanup and detect deleting its enablement."""
201 if not Path(
"/proc/self/stat").is_file():
202 return [(
"image supervisor escaped-descendant proof is Linux-only",
True)]
203 supervisor = inputs[
"devcontainer_image_selftest_supervisor"]
204 process_source = inputs[
"devcontainer_image_selftest_process"]
205 supervisor_cases = inputs[
"devcontainer_image_selftest_supervisor_cases"]
206 enable =
" self.subreaper = _enable_child_subreaper()\n"
207 if process_source.count(enable) != 1:
208 message =
"subreaper runtime mutation authority is not unique"
209 raise RuntimeMutationError(message)
210 mutant_process = process_source.replace(
212 " self.subreaper = True # runtime mutation: kernel subreaper disabled\n",
215 with _owned_root_scope():
216 roots: list[tuple[Path, tuple[int, int], tuple[int, int] |
None]] = []
218 base_root, base_root_identity, base_escape, base_status, base_clean = _run_escape(
219 supervisor, process_source, supervisor_cases
221 roots.append((base_root, base_root_identity, base_escape))
222 mutant_root, mutant_root_identity, mutant_escape, mutant_status, mutant_clean = (
223 _run_escape(supervisor, mutant_process, supervisor_cases)
225 roots.append((mutant_root, mutant_root_identity, mutant_escape))
226 base_absent =
not _escape_is_live(base_escape)
and _no_residue((base_root,))
227 mutant_live = _escape_is_live(mutant_escape)
and not _no_residue((mutant_root,))
228 deletion_refused =
False
230 _remove_root(mutant_root, mutant_root_identity)
231 except RuntimeMutationError:
232 deletion_refused = mutant_root.is_dir()
233 mutant_recovered = _terminate_escape(mutant_escape)
and _no_residue((mutant_root,))
236 "supervisor subreaper removes a setsid root-owning descendant",
237 base_status == 1
and base_clean
and base_absent,
240 "subreaper deletion mutation preserves live root and then recovers exactly",
245 and mutant_recovered,
249 for root, root_identity, escape_identity
in reversed(roots):
251 escape_identity
is not None
252 and _escape_is_live(escape_identity)
253 and not _terminate_escape(escape_identity)
255 message =
"escaped-descendant recovery did not converge"
256 raise RuntimeMutationError(message)
257 _remove_root(root, root_identity)