ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
devcontainer_image_selftest_process.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Authenticated Linux process primitives for the image selftest supervisor."""
4
5from __future__ import annotations
6
7import ctypes
8import fcntl
9import os
10import signal
11import subprocess
12import sys
13import time
14from contextlib import suppress
15from dataclasses import dataclass
16from pathlib import Path
17
18import __main__
19
20PROCESS_LOAD_VERSION = 1
21PROCESS_GROUP_FIELD = 2
22SESSION_FIELD = 3
23START_TIME_FIELD = 19
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__
41
42
43@dataclass(frozen=True)
44class ProcessIdentity:
45 """Bind one live process against numeric PID reuse."""
46
47 pid: int
48 group: int
49 session: int
50 start_time: bytes
51
52
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")")
56 if closing < 0:
57 return None
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):
61 return None
62 return (
63 int(fields[PROCESS_GROUP_FIELD]),
64 int(fields[SESSION_FIELD]),
65 fields[START_TIME_FIELD],
66 )
67
68
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]
73
74
75def _stat_parent(raw: bytes) -> int | None:
76 """Extract one parent PID without decoding the arbitrary comm field."""
77 closing = raw.rfind(b")")
78 if closing < 0:
79 return None
80 fields = raw[closing + 2 :].split()
81 return int(fields[1]) if len(fields) > 1 and fields[1].isdigit() else None
82
83
84def _bind_process(pid: int) -> ProcessIdentity | None:
85 """Bind one process identity while its numeric PID cannot be reused."""
86 try:
87 identity = _stat_identity(Path(f"/proc/{pid}/stat").read_bytes())
88 except OSError:
89 return None
90 if identity is None:
91 return None
92 return ProcessIdentity(pid, identity[0], identity[1], identity[2])
93
94
95def _reserve_entry_descriptor(descriptor: int) -> int:
96 """Move an entry FD above the Bash helper descriptor namespace."""
97 try:
98 reserved = fcntl.fcntl(
99 descriptor,
100 fcntl.F_DUPFD_CLOEXEC,
101 ENTRY_EXEC_DESCRIPTOR_MINIMUM,
102 )
103 finally:
104 os.close(descriptor)
105 try:
106 descriptor_flags = fcntl.fcntl(reserved, fcntl.F_GETFD)
107 except BaseException:
108 os.close(reserved)
109 raise
110 if reserved < ENTRY_EXEC_DESCRIPTOR_MINIMUM or not (descriptor_flags & fcntl.FD_CLOEXEC):
111 os.close(reserved)
112 message = "entry descriptor reservation returned an unsafe descriptor"
113 raise RuntimeError(message)
114 return reserved
115
116
117def _group_members(group: int) -> set[int] | None:
118 """Return process IDs in one Linux group without spawning another process."""
119 members = set()
120 try:
121 entries = tuple(Path("/proc").iterdir())
122 except OSError:
123 return None
124 for entry in entries:
125 if not entry.name.isdigit():
126 continue
127 try:
128 raw = (entry / "stat").read_bytes()
129 except FileNotFoundError:
130 continue
131 except (OSError, RuntimeError):
132 return None
133 closing = raw.rfind(b")")
134 fields = raw[closing + 2 :].split() if closing >= 0 else []
135 if not fields:
136 return None
137 if fields[0] == b"Z":
138 continue
139 process_group = _stat_group(raw)
140 if process_group is None:
141 return None
142 if process_group == group:
143 members.add(int(entry.name))
144 return members
145
146
147def _direct_children() -> dict[int, ProcessIdentity] | None:
148 """Bind every kernel-parented direct child, failing closed on unknown state."""
149 try:
150 value = Path(f"/proc/self/task/{os.getpid()}/children").read_bytes()
151 except OSError:
152 return None
153 if len(value) > CHILD_LIST_MAX_BYTES:
154 return None
155 children = {}
156 for token in value.split():
157 if not token.isdigit():
158 return None
159 pid = int(token)
160 try:
161 raw = Path(f"/proc/{pid}/stat").read_bytes()
162 except OSError:
163 return None
164 parent = _stat_parent(raw)
165 identity = _stat_identity(raw)
166 if parent != os.getpid() or identity is None:
167 return None
168 children[pid] = ProcessIdentity(pid, identity[0], identity[1], identity[2])
169 return children
170
171
172def _child_table_is_empty() -> bool:
173 """Require both procfs and waitid to prove that this process has no children."""
174 if _direct_children() != {}:
175 return False
176 try:
177 os.waitid(os.P_ALL, 0, os.WEXITED | os.WNOHANG | os.WNOWAIT)
178 except ChildProcessError:
179 return True
180 return False
181
182
183def _enable_child_subreaper() -> bool:
184 """Enable and verify the Linux child-subreaper boundary before any spawn."""
185 if sys.platform != "linux":
186 return False
187 libc = ctypes.CDLL(None, use_errno=True)
188 prctl = libc.prctl
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:
192 return False
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
196
197
198class BoundGroup:
199 """Own one unreaped process-group leader from spawn through cleanup."""
200
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
205 self.reaped = False
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
220
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()
225
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
239 )
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()
248 argv = (
249 sys.executable,
250 "-B",
251 "-I",
252 "-S",
253 SUPERVISOR_PROGRAM,
254 "--controller",
255 entry_authority,
256 str(launch.status),
257 str(self.death_read),
258 str(root_descriptor),
259 root_identity,
260 str(launch.watchdog_timeout),
261 )
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( # noqa: S603 -- current pinned interpreter and helper
266 argv,
267 pass_fds=tuple(inherited),
268 start_new_session=True,
269 )
270 self.bind_spawned_child(child)
271 os.close(self.death_read)
272 self.death_read = None
273
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)
279 self.child = child
280 self.pid = child.pid
281 self.children_contained = False
282 try:
283 terminal = os.waitid(
284 os.P_PID,
285 child.pid,
286 os.WEXITED | os.WNOHANG | os.WNOWAIT,
287 )
288 except ChildProcessError:
289 self.reaped = True
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()
298 safe = (
299 identity is not None
300 and identity.pid == identity.group == identity.session
301 and children is not None
302 and children.get(child.pid) == identity
303 )
304 if not safe:
305 message = "manual child is not the exact isolated direct child"
306 raise RuntimeError(message)
307
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:
311 return True
312 try:
313 self.child.wait(timeout=DEADLINE_SECONDS)
314 except subprocess.TimeoutExpired:
315 return False
316 self.reaped = True
317 return True
318
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
327
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
332 try:
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
344 )
345 except (OSError, RuntimeError):
346 self.entry_integrity = False
347 finally:
348 os.close(self.entry_descriptor)
349 self.entry_descriptor = None
350 self.entry_path = None
351 return self.entry_integrity
352
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:
356 return False
357 with suppress(ProcessLookupError):
358 os.kill(authority.pid, signal.SIGKILL)
359 result = os.waitid(
360 os.P_PID,
361 authority.pid,
362 os.WEXITED | os.WNOHANG | os.WNOWAIT,
363 )
364 if result is None:
365 return False
366 if _bind_process(authority.pid) != authority:
367 return False
368 waited, _status = os.waitpid(authority.pid, 0)
369 return waited == authority.pid
370
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()
374 if children is None:
375 return None
376 if excluded_pid is not None:
377 leader = children.pop(excluded_pid, None)
378 if leader != self.leader_identity:
379 return None
380 return children
381
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:
385 return False
386 deadline = time.monotonic() + DEADLINE_SECONDS
387 while time.monotonic() < deadline:
388 children = self._bound_direct_children(excluded_pid)
389 if children is None:
390 time.sleep(POLL_SECONDS)
391 continue
392 for pid, authority in children.items():
393 retained = self.adopted.setdefault(pid, authority)
394 if retained != authority:
395 return False
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:
402 return True
403 if _child_table_is_empty():
404 self.children_contained = True
405 return True
406 time.sleep(POLL_SECONDS)
407 return False
408
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:
412 return False
413 deadline = time.monotonic() + DEADLINE_SECONDS
414 while time.monotonic() < deadline:
415 try:
416 result = os.waitid(
417 os.P_PID,
418 self.pid,
419 os.WEXITED | os.WNOHANG | os.WNOWAIT,
420 )
421 except ChildProcessError:
422 self.authority_lost = True
423 return False
424 except OSError:
425 time.sleep(POLL_SECONDS)
426 continue
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)
431 return False
432
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:
436 return False
437 try:
438 raw = Path(f"/proc/{self.pid}/stat").read_bytes()
439 except OSError:
440 return False
441 closing = raw.rfind(b")")
442 return closing >= 0 and raw[closing + 2 :].split()[0] != b"Z"
443
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)
449
450 def _finish_terminal_leader(self) -> bool:
451 """Reap a WNOWAIT-bound terminal leader without signaling its numeric group."""
452 if not self._reap():
453 self.cleaning = False
454 return False
455 descendants_clean = self._cleanup_adopted_children()
456 return descendants_clean and self._close_entry_authority()
457
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:
462 return False
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()
467 if self.cleaning:
468 return False
469 self.cleaning = True
470 for managed in MANAGED_SIGNALS:
471 signal.signal(managed, signal.SIG_IGN)
472 leader = self.pid
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
485 return False
486
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()
493 return cleaned