3"""Authenticated Linux process primitives for the image selftest supervisor."""
5from __future__
import annotations
14from contextlib
import suppress
15from dataclasses
import dataclass
16from pathlib
import Path
20PROCESS_LOAD_VERSION = 1
21PROCESS_GROUP_FIELD = 2
24PR_SET_CHILD_SUBREAPER = 36
25PR_GET_CHILD_SUBREAPER = 37
26CHILD_LIST_MAX_BYTES = 4096
27ENTRY_EXEC_DESCRIPTOR_MINIMUM = 64
28if globals().get(
"_RA8_SUPERVISOR_PROCESS_VERSION") != PROCESS_LOAD_VERSION:
29 message =
"supervisor process module is source-only"
30 raise RuntimeError(message)
31MAIN_API = vars(__main__)
32DEADLINE_SECONDS = MAIN_API[
"DEADLINE_SECONDS"]
33MANAGED_SIGNALS = MAIN_API[
"MANAGED_SIGNALS"]
34POLL_SECONDS = MAIN_API[
"POLL_SECONDS"]
35ControllerLaunch = MAIN_API[
"ControllerLaunch"]
36_anchored_root_descriptor = MAIN_API[
"_anchored_root_descriptor"]
37_entry_digest = MAIN_API[
"_entry_digest"]
38_entry_metadata_is_safe = MAIN_API[
"_entry_metadata_is_safe"]
39_open_entry_authority = MAIN_API[
"_open_entry_authority"]
40SUPERVISOR_PROGRAM = __main__.__file__
43@dataclass(frozen=True)
45 """Bind one live process against numeric PID reuse."""
53def _stat_identity(raw: bytes) -> tuple[int, int, bytes] |
None:
54 """Extract process authority without decoding the arbitrary comm field."""
55 closing = raw.rfind(b
")")
58 fields = raw[closing + 2 :].split()
59 required = (PROCESS_GROUP_FIELD, SESSION_FIELD, START_TIME_FIELD)
60 if len(fields) <= START_TIME_FIELD
or any(
not fields[index].isdigit()
for index
in required):
63 int(fields[PROCESS_GROUP_FIELD]),
64 int(fields[SESSION_FIELD]),
65 fields[START_TIME_FIELD],
69def _stat_group(raw: bytes) -> int |
None:
70 """Extract one process group without decoding the arbitrary comm field."""
71 identity = _stat_identity(raw)
72 return None if identity
is None else identity[0]
75def _stat_parent(raw: bytes) -> int |
None:
76 """Extract one parent PID without decoding the arbitrary comm field."""
77 closing = raw.rfind(b
")")
80 fields = raw[closing + 2 :].split()
81 return int(fields[1])
if len(fields) > 1
and fields[1].isdigit()
else None
84def _bind_process(pid: int) -> ProcessIdentity |
None:
85 """Bind one process identity while its numeric PID cannot be reused."""
87 identity = _stat_identity(Path(f
"/proc/{pid}/stat").read_bytes())
92 return ProcessIdentity(pid, identity[0], identity[1], identity[2])
95def _reserve_entry_descriptor(descriptor: int) -> int:
96 """Move an entry FD above the Bash helper descriptor namespace."""
98 reserved = fcntl.fcntl(
100 fcntl.F_DUPFD_CLOEXEC,
101 ENTRY_EXEC_DESCRIPTOR_MINIMUM,
106 descriptor_flags = fcntl.fcntl(reserved, fcntl.F_GETFD)
107 except BaseException:
110 if reserved < ENTRY_EXEC_DESCRIPTOR_MINIMUM
or not (descriptor_flags & fcntl.FD_CLOEXEC):
112 message =
"entry descriptor reservation returned an unsafe descriptor"
113 raise RuntimeError(message)
117def _group_members(group: int) -> set[int] |
None:
118 """Return process IDs in one Linux group without spawning another process."""
121 entries = tuple(Path(
"/proc").iterdir())
124 for entry
in entries:
125 if not entry.name.isdigit():
128 raw = (entry /
"stat").read_bytes()
129 except FileNotFoundError:
131 except (OSError, RuntimeError):
133 closing = raw.rfind(b
")")
134 fields = raw[closing + 2 :].split()
if closing >= 0
else []
137 if fields[0] == b
"Z":
139 process_group = _stat_group(raw)
140 if process_group
is None:
142 if process_group == group:
143 members.add(int(entry.name))
147def _direct_children() -> dict[int, ProcessIdentity] | None:
148 """Bind every kernel-parented direct child, failing closed on unknown state."""
150 value = Path(f
"/proc/self/task/{os.getpid()}/children").read_bytes()
153 if len(value) > CHILD_LIST_MAX_BYTES:
156 for token
in value.split():
157 if not token.isdigit():
161 raw = Path(f
"/proc/{pid}/stat").read_bytes()
164 parent = _stat_parent(raw)
165 identity = _stat_identity(raw)
166 if parent != os.getpid()
or identity
is None:
168 children[pid] = ProcessIdentity(pid, identity[0], identity[1], identity[2])
172def _child_table_is_empty() -> bool:
173 """Require both procfs and waitid to prove that this process has no children."""
174 if _direct_children() != {}:
177 os.waitid(os.P_ALL, 0, os.WEXITED | os.WNOHANG | os.WNOWAIT)
178 except ChildProcessError:
183def _enable_child_subreaper() -> bool:
184 """Enable and verify the Linux child-subreaper boundary before any spawn."""
185 if sys.platform !=
"linux":
187 libc = ctypes.CDLL(
None, use_errno=
True)
189 prctl.argtypes = (ctypes.c_int, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong)
190 prctl.restype = ctypes.c_int
191 if prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0:
193 state = ctypes.c_int()
194 result = prctl(PR_GET_CHILD_SUBREAPER, ctypes.addressof(state), 0, 0, 0)
195 return result == 0
and state.value == 1
199 """Own one unreaped process-group leader from spawn through cleanup."""
201 def __init__(self) -> None:
202 """Create an empty authority that cannot signal before a successful fork."""
203 self.pid: int |
None =
None
204 self.child: subprocess.Popen[bytes] |
None =
None
206 self.leader_terminal =
False
207 self.leader_identity: ProcessIdentity |
None =
None
208 self.authority_lost =
False
209 self.cleaning =
False
210 self.death_read: int |
None =
None
211 self.death_write: int |
None =
None
212 self.entry_descriptor: int |
None =
None
213 self.entry_path: str |
None =
None
214 self.entry_identity: tuple[int, int] |
None =
None
215 self.entry_digest: str |
None =
None
216 self.entry_integrity =
True
217 self.subreaper =
False
218 self.adopted: dict[int, ProcessIdentity] = {}
219 self.children_contained =
True
221 def enable_subreaper(self) -> bool:
222 """Establish an empty Linux subreaper boundary before the first spawn."""
223 self.subreaper = _enable_child_subreaper()
224 return self.subreaper
and _child_table_is_empty()
226 def spawn(self, source_descriptor: int, launch: ControllerLaunch) ->
None:
227 """Fork one isolated controller while managed signals remain blocked."""
228 if not self.subreaper:
229 message =
"child subreaper boundary is unavailable or not empty"
230 raise RuntimeError(message)
231 self.death_read, self.death_write = os.pipe2(os.O_CLOEXEC)
232 root_descriptor = _anchored_root_descriptor(launch.status)
233 root_metadata = os.fstat(root_descriptor)
234 root_identity = f
"{root_metadata.st_dev}:{root_metadata.st_ino}"
235 entry_authority =
"missing-entry-selftest"
236 if not launch.missing_entry_selftest:
237 descriptor, identity, digest = _open_entry_authority(
238 launch.entry, launch.pre_open_mutator
240 descriptor = _reserve_entry_descriptor(descriptor)
241 self.entry_descriptor = descriptor
242 self.entry_path = launch.entry
243 self.entry_identity = identity
244 self.entry_digest = digest
245 entry_authority = str(descriptor)
246 if launch.entry_mutator
is not None:
247 launch.entry_mutator()
257 str(self.death_read),
258 str(root_descriptor),
260 str(launch.watchdog_timeout),
262 inherited = [source_descriptor, self.death_read, root_descriptor]
263 if self.entry_descriptor
is not None:
264 inherited.append(self.entry_descriptor)
265 child = subprocess.Popen(
267 pass_fds=tuple(inherited),
268 start_new_session=
True,
270 self.bind_spawned_child(child)
271 os.close(self.death_read)
272 self.death_read =
None
274 def bind_spawned_child(self, child: subprocess.Popen[bytes]) ->
None:
275 """Bind a manually launched isolated child to the subreaper authority."""
276 if not self.subreaper
or self.child
is not None or self.pid
is not None:
277 message =
"manual child cannot be bound to this subreaper authority"
278 raise RuntimeError(message)
281 self.children_contained =
False
283 terminal = os.waitid(
286 os.WEXITED | os.WNOHANG | os.WNOWAIT,
288 except ChildProcessError:
290 self.leader_terminal =
True
291 self.authority_lost =
True
292 message =
"manual child was reaped before identity binding"
293 raise RuntimeError(message)
from None
294 self.leader_terminal = terminal
is not None
295 identity = _bind_process(child.pid)
296 self.leader_identity = identity
297 children = _direct_children()
300 and identity.pid == identity.group == identity.session
301 and children
is not None
302 and children.get(child.pid) == identity
305 message =
"manual child is not the exact isolated direct child"
306 raise RuntimeError(message)
308 def _reap(self) -> bool:
309 """Reap the exact leader once, after its process group is empty."""
310 if self.pid
is None or self.child
is None or self.reaped:
313 self.child.wait(timeout=DEADLINE_SECONDS)
314 except subprocess.TimeoutExpired:
319 def _close_death_pipe(self) -> None:
320 """Close each still-owned parent-death descriptor at most once."""
321 if self.death_read
is not None:
322 os.close(self.death_read)
323 self.death_read =
None
324 if self.death_write
is not None:
325 os.close(self.death_write)
326 self.death_write =
None
328 def _close_entry_authority(self) -> bool:
329 """Recheck and close the exact entry only after its controller is reaped."""
330 if self.entry_descriptor
is None:
331 return self.entry_integrity
333 metadata = os.fstat(self.entry_descriptor)
334 current_identity = (metadata.st_dev, metadata.st_ino)
335 current_digest = _entry_digest(self.entry_descriptor)
336 path_metadata = os.lstat(self.entry_path)
if self.entry_path
is not None else None
337 self.entry_integrity = (
338 _entry_metadata_is_safe(metadata)
339 and path_metadata
is not None
340 and _entry_metadata_is_safe(path_metadata)
341 and (path_metadata.st_dev, path_metadata.st_ino) == self.entry_identity
342 and current_identity == self.entry_identity
343 and current_digest == self.entry_digest
345 except (OSError, RuntimeError):
346 self.entry_integrity =
False
348 os.close(self.entry_descriptor)
349 self.entry_descriptor =
None
350 self.entry_path =
None
351 return self.entry_integrity
353 def _kill_and_reap_adopted(self, authority: ProcessIdentity) -> bool:
354 """Kill and reap one exact direct child without releasing PID authority early."""
355 if _bind_process(authority.pid) != authority:
357 with suppress(ProcessLookupError):
358 os.kill(authority.pid, signal.SIGKILL)
362 os.WEXITED | os.WNOHANG | os.WNOWAIT,
366 if _bind_process(authority.pid) != authority:
368 waited, _status = os.waitpid(authority.pid, 0)
369 return waited == authority.pid
371 def _bound_direct_children(self, excluded_pid: int |
None) -> dict[int, ProcessIdentity] |
None:
372 """Bind direct children while retaining one separately owned leader."""
373 children = _direct_children()
376 if excluded_pid
is not None:
377 leader = children.pop(excluded_pid,
None)
378 if leader != self.leader_identity:
382 def _cleanup_adopted_children(self, excluded_pid: int |
None =
None) -> bool:
383 """Drain adopted descendants without reaping a separately bound leader."""
384 if not self.subreaper:
386 deadline = time.monotonic() + DEADLINE_SECONDS
387 while time.monotonic() < deadline:
388 children = self._bound_direct_children(excluded_pid)
390 time.sleep(POLL_SECONDS)
392 for pid, authority
in children.items():
393 retained = self.adopted.setdefault(pid, authority)
394 if retained != authority:
396 pending = tuple(self.adopted.values())
397 for authority
in pending:
398 if self._kill_and_reap_adopted(authority):
399 self.adopted.pop(authority.pid,
None)
400 if not self.adopted
and not children:
401 if excluded_pid
is not None:
403 if _child_table_is_empty():
404 self.children_contained =
True
406 time.sleep(POLL_SECONDS)
409 def _wait_leader_terminal(self) -> bool:
410 """Retain exact leader authority through a WNOWAIT terminal proof."""
411 if self.pid
is None or self.leader_identity
is None:
413 deadline = time.monotonic() + DEADLINE_SECONDS
414 while time.monotonic() < deadline:
419 os.WEXITED | os.WNOHANG | os.WNOWAIT,
421 except ChildProcessError:
422 self.authority_lost =
True
425 time.sleep(POLL_SECONDS)
427 if result
is not None:
428 self.leader_terminal = _bind_process(self.pid) == self.leader_identity
429 return self.leader_terminal
430 time.sleep(POLL_SECONDS)
433 def leader_is_running(self) -> bool:
434 """Prove the exact bound controller remains live and non-zombie."""
435 if self.pid
is None or self.reaped:
438 raw = Path(f
"/proc/{self.pid}/stat").read_bytes()
441 closing = raw.rfind(b
")")
442 return closing >= 0
and raw[closing + 2 :].split()[0] != b
"Z"
444 def require_running(self, context: str) ->
None:
445 """Fail the supervision transaction when its exact leader is not live."""
446 if not self.leader_is_running():
447 message = f
"controller lost group authority {context}"
448 raise RuntimeError(message)
450 def _finish_terminal_leader(self) -> bool:
451 """Reap a WNOWAIT-bound terminal leader without signaling its numeric group."""
453 self.cleaning =
False
455 descendants_clean = self._cleanup_adopted_children()
456 return descendants_clean
and self._close_entry_authority()
458 def cleanup(self) -> bool:
459 """Kill the bound group before reaping its non-reusable leader."""
460 signal.pthread_sigmask(signal.SIG_BLOCK, MANAGED_SIGNALS)
461 if self.authority_lost:
463 if self.pid
is None or self.reaped:
464 self._close_death_pipe()
465 descendants_clean = self._cleanup_adopted_children()
466 return descendants_clean
and self._close_entry_authority()
470 for managed
in MANAGED_SIGNALS:
471 signal.signal(managed, signal.SIG_IGN)
473 self._close_death_pipe()
474 if self.leader_terminal:
475 return self._finish_terminal_leader()
476 with suppress(ProcessLookupError):
477 os.killpg(leader, signal.SIGKILL)
478 leader_terminal = self._wait_leader_terminal()
479 descendants_drained = leader_terminal
and self._cleanup_adopted_children(leader)
480 members = _group_members(leader)
if descendants_drained
else None
481 if members
is not None and members <= {leader}
and self._reap():
482 descendants_clean = self._cleanup_adopted_children()
483 return descendants_clean
and self._close_entry_authority()
484 self.cleaning =
False
487 def contain(self) -> bool:
488 """Remain the subreaper until every effected child is safely contained."""
489 cleaned = self.cleanup()
490 while not cleaned
and not self.children_contained:
491 time.sleep(POLL_SECONDS)
492 cleaned = self.cleanup()