ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_image_subreaper_policy.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Exact Linux child-subreaper policy for the image selftest supervisor."""
4
5from __future__ import annotations
6
7import ast
8
9SUBREAPER_TOKENS = {
10 "child subreaper capability constant changed": "PR_SET_CHILD_SUBREAPER = 36",
11 "child subreaper verification constant changed": "PR_GET_CHILD_SUBREAPER = 37",
12 "child subreaper prctl activation removed": (
13 "if prctl(PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0:"
14 ),
15 "child subreaper prctl verification removed": (
16 "result = prctl(PR_GET_CHILD_SUBREAPER, ctypes.addressof(state), 0, 0, 0)"
17 ),
18 "child subreaper verified-state proof removed": ("return result == 0 and state.value == 1"),
19 "child subreaper activation assignment removed": (
20 " self.subreaper = _enable_child_subreaper()"
21 ),
22 "child subreaper initial-empty proof removed": (
23 "return self.subreaper and _child_table_is_empty()"
24 ),
25 "manual child subreaper gate removed": (
26 " if not self.subreaper or self.child is not None or self.pid is not None:"
27 ),
28 "manual child containment binding removed": (
29 " self.child = child\n"
30 " self.pid = child.pid\n"
31 " self.children_contained = False"
32 ),
33 "manual child isolated identity proof removed": (
34 " and identity.pid == identity.group == identity.session"
35 ),
36 "manual child direct-parent identity proof removed": (
37 " and children.get(child.pid) == identity"
38 ),
39 "manual child terminal authority initialization removed": (
40 " self.leader_terminal = False"
41 ),
42 "manual child WNOWAIT observation removed": (
43 " terminal = os.waitid(\n"
44 " os.P_PID,\n"
45 " child.pid,\n"
46 " os.WEXITED | os.WNOHANG | os.WNOWAIT,\n"
47 " )"
48 ),
49 "manual child early-reap PID release removed": (
50 " except ChildProcessError:\n"
51 " self.reaped = True\n"
52 " self.leader_terminal = True"
53 ),
54 "manual child terminal state binding removed": (
55 " self.leader_terminal = terminal is not None"
56 ),
57 "manual terminal child signal bypass removed": (
58 " if self.leader_terminal:\n return self._finish_terminal_leader()"
59 ),
60 "child subreaper leader exclusion removed": (
61 " leader = children.pop(excluded_pid, None)\n"
62 " if leader != self.leader_identity:"
63 ),
64 "child subreaper adopted exclusion dispatch removed": (
65 " children = self._bound_direct_children(excluded_pid)"
66 ),
67 "child subreaper leader WNOWAIT retention removed": (
68 " result = os.waitid(\n"
69 " os.P_PID,\n"
70 " self.pid,\n"
71 " os.WEXITED | os.WNOHANG | os.WNOWAIT,\n"
72 " )"
73 ),
74 "child subreaper lost authority fail-stop removed": (
75 " if self.authority_lost:\n return False"
76 ),
77 "child subreaper pre-reap adopted drain removed": (
78 " descendants_drained = leader_terminal and self._cleanup_adopted_children(leader)"
79 ),
80 "child subreaper direct-parent binding removed": (
81 " if parent != os.getpid() or identity is None:\n return None"
82 ),
83 "child subreaper pre-signal identity proof removed": (
84 " if _bind_process(authority.pid) != authority:\n"
85 " return False\n"
86 " with suppress(ProcessLookupError):"
87 ),
88 "child subreaper exact kill removed": "os.kill(authority.pid, signal.SIGKILL)",
89 "child subreaper WNOWAIT proof removed": (
90 " result = os.waitid(\n"
91 " os.P_PID,\n"
92 " authority.pid,\n"
93 " os.WEXITED | os.WNOHANG | os.WNOWAIT,\n"
94 " )"
95 ),
96 "child subreaper pre-reap identity proof removed": (
97 " if _bind_process(authority.pid) != authority:\n"
98 " return False\n"
99 " waited, _status = os.waitpid(authority.pid, 0)"
100 ),
101 "child subreaper exact reap removed": "return waited == authority.pid",
102 "child subreaper ECHILD completion removed": (
103 " except ChildProcessError:\n return True\n return False"
104 ),
105 "child subreaper empty completion removed": (" if _child_table_is_empty():"),
106 "child subreaper enable call removed": " if not supervisor.enable_subreaper():",
107 "child subreaper blocking containment removed": (
108 " while not cleaned and not self.children_contained:"
109 ),
110 "child subreaper final containment call removed": " supervisor.contain()",
111}
112
113MOVED_PROCESS_TOKENS = {
114 "bound-exit parent-death pipe removed": (
115 "self.death_read, self.death_write = os.pipe2(os.O_CLOEXEC)"
116 ),
117 "bound-exit parent-death descriptor propagation removed": (
118 "inherited = [source_descriptor, self.death_read, root_descriptor]"
119 ),
120 "bound-exit controller interpreter changed": " sys.executable,",
121 "bound-exit controller no-bytecode isolation removed": ' "-B",',
122 "bound-exit controller isolated-mode flag removed": ' "-I",',
123 "bound-exit controller site-import refusal removed": ' "-S",',
124 "bound-exit controller immutable helper path removed": (
125 ' SUPERVISOR_PROGRAM,\n "--controller",'
126 ),
127 "process stat bytes parser replaced with text decoding": '(entry / "stat").read_bytes()',
128 "bound-exit group cleanup reduced to leader PID": "os.killpg(leader, signal.SIGKILL)",
129 "bound-exit cleanup signal block moved after authority checks": (
130 " signal.pthread_sigmask(signal.SIG_BLOCK, MANAGED_SIGNALS)\n"
131 " if self.authority_lost:"
132 ),
133}
134
135PROCESS_STATIC_TOKENS = (
136 "root_descriptor = _anchored_root_descriptor(launch.status)",
137 "members is not None and members <= {leader}",
138 " if not self._reap():\n self.cleaning = False\n return False",
139)
140
141PROCESS_LOADER_TOKENS = {
142 "process source metadata regular binding removed": (
143 "_read_process_source",
144 "stat.S_ISREG(metadata.st_mode)",
145 ),
146 "process source metadata link binding removed": (
147 "_read_process_source",
148 "metadata.st_nlink == 1",
149 ),
150 "process source metadata owner binding removed": (
151 "_read_process_source",
152 "metadata.st_uid == os.getuid()",
153 ),
154 "process source metadata group binding removed": (
155 "_read_process_source",
156 "metadata.st_gid == os.getgid()",
157 ),
158 "process source metadata mode binding removed": (
159 "_read_process_source",
160 "stat.S_IMODE(metadata.st_mode) == PROCESS_MODE",
161 ),
162 "process source metadata byte bound removed": (
163 "_read_process_source",
164 "0 < metadata.st_size <= PROCESS_MAX_BYTES",
165 ),
166 "process source read step bound removed": (
167 "_read_process_source",
168 "for _step in range(PROCESS_READ_STEPS):",
169 ),
170 "process source descriptor pread binding removed": (
171 "_read_process_source",
172 "chunk = os.pread(descriptor, 4096, offset)",
173 ),
174 "process source complete-size postcondition removed": (
175 "_read_process_source",
176 "if len(source) != metadata.st_size or len(source) > PROCESS_MAX_BYTES:",
177 ),
178 "process loader bound source read removed": (
179 "_load_process_api",
180 "source = _read_process_source(descriptor)",
181 ),
182 "process loader pre-exec digest binding removed": (
183 "_load_process_api",
184 "if digest != PROCESS_RAW_SHA256:",
185 ),
186 "process loader namespace name binding removed": (
187 "_load_process_api",
188 'module_name = "_ra8_supervisor_process"',
189 ),
190 "process loader private module construction removed": (
191 "_load_process_api",
192 "module = types.ModuleType(module_name)",
193 ),
194 "process loader private module namespace removed": (
195 "_load_process_api",
196 "namespace = module.__dict__",
197 ),
198 "process loader namespace filename binding removed": (
199 "_load_process_api",
200 'namespace["__file__"] = f"/proc/self/fd/{descriptor}"',
201 ),
202 "process loader grant version binding removed": (
203 "_load_process_api",
204 'namespace["_RA8_SUPERVISOR_PROCESS_VERSION"] = 1',
205 ),
206 "process loader authenticated exec removed": (
207 "_load_process_api",
208 "exec( # noqa: S102 -- exact digest-bound source-only FD",
209 ),
210 "process loader compile source binding removed": ("_load_process_api", "compile(source,"),
211 "process loader compile filename binding removed": (
212 "_load_process_api",
213 'namespace["__file__"],',
214 ),
215 "process loader compile exec-mode binding removed": ("_load_process_api", ', "exec")'),
216 "process loader grant exact consumption removed": (
217 "_load_process_api",
218 'grant = namespace.pop("_RA8_SUPERVISOR_PROCESS_VERSION", None)',
219 ),
220 "process loader grant absence postcondition removed": (
221 "_load_process_api",
222 'if grant != 1 or "_RA8_SUPERVISOR_PROCESS_VERSION" in namespace:',
223 ),
224 "process loader post-exec same-FD digest removed": (
225 "_load_process_api",
226 "if hashlib.sha256(_read_process_source(descriptor)).hexdigest() != digest:",
227 ),
228 "process loader validation delegation removed": (
229 "_load_process_api",
230 "return _validate_process_api(module_name, namespace, module)",
231 ),
232 "process loader class API binding removed": (
233 "_validate_process_api",
234 "if not all(isinstance(value, type) for value in classes) or not all(",
235 ),
236 "process loader function API binding removed": (
237 "_validate_process_api",
238 "isinstance(value, types.FunctionType) for value in functions",
239 ),
240 "process loader API namespace identity removed": (
241 "_validate_process_api",
242 "if escaped:",
243 ),
244 "process loader class namespace binding removed": (
245 "_validate_process_api",
246 'escaped = any(value.__module__ != namespace["__name__"] for value in classes)',
247 ),
248 "process loader function namespace binding removed": (
249 "_validate_process_api",
250 "escaped = escaped or any(value.__globals__ is not namespace for value in functions)",
251 ),
252 "process loader method namespace binding removed": (
253 "_validate_process_api",
254 "escaped = escaped or any(value.__globals__ is not namespace for value in methods)",
255 ),
256 "process loader preexisting module refusal removed": (
257 "_load_process_api",
258 "if module_name in sys.modules:",
259 ),
260 "process loader module registration removed": (
261 "_load_process_api",
262 "sys.modules[module_name] = module",
263 ),
264 "process loader module identity postcondition removed": (
265 "_validate_process_api",
266 "escaped = escaped or sys.modules.get(module_name) is not module",
267 ),
268 "process loader module residue cleanup removed": (
269 "_load_process_api",
270 "if module is not None and sys.modules.get(module_name) is module:\n"
271 " del sys.modules[module_name]",
272 ),
273 "process loader descriptor final-close removed": (
274 "_load_process_api",
275 "os.close(descriptor)",
276 ),
277 "process loader install residue refusal removed": (
278 "_install_process_api",
279 'if "_ra8_supervisor_process" in sys.modules:',
280 ),
281}
282
283PROCESS_LOADER_ORDER = (
284 'module_name = "_ra8_supervisor_process"',
285 "if module_name in sys.modules:",
286 "module = types.ModuleType(module_name)",
287 "namespace = module.__dict__",
288 'namespace["_RA8_SUPERVISOR_PROCESS_VERSION"] = 1',
289 "sys.modules[module_name] = module",
290 "exec( # noqa: S102 -- exact digest-bound source-only FD",
291 'grant = namespace.pop("_RA8_SUPERVISOR_PROCESS_VERSION", None)',
292 "if hashlib.sha256(_read_process_source(descriptor)).hexdigest() != digest:",
293 "return _validate_process_api(module_name, namespace, module)",
294 "if module is not None and sys.modules.get(module_name) is module:",
295 "del sys.modules[module_name]",
296 "os.close(descriptor)",
297)
298PROCESS_VALIDATION_ORDER = (
299 "api = tuple(namespace.get(name) for name in names)",
300 "classes, functions = api[:2], api[2:]",
301 "if not all(isinstance(value, type) for value in classes) or not all(",
302 "methods = tuple(",
303 'escaped = any(value.__module__ != namespace["__name__"] for value in classes)',
304 "escaped = escaped or sys.modules.get(module_name) is not module",
305 "return api",
306)
307PROCESS_LOADER_ORDER_DIAGNOSTIC = (
308 "devcontainer image supervisor: authenticated process-loader order drifted"
309)
310
311SUPERVISOR_SUBREAPER_LABELS = frozenset(
312 {"child subreaper enable call removed", "child subreaper final containment call removed"}
313)
314SUPERVISOR_SUBREAPER_OWNERS = {
315 "child subreaper enable call removed": "_supervise",
316 "child subreaper final containment call removed": "_supervise",
317}
318SUPERVISOR_SUBREAPER_ORDER = (
319 "if not supervisor.enable_subreaper():",
320 "old_mask = signal.pthread_sigmask(signal.SIG_BLOCK, MANAGED_SIGNALS)",
321 "supervisor.spawn(source_descriptor, launch)",
322 " supervisor.contain()",
323)
324PROCESS_SUBREAPER_ORDER = (
325 "os.killpg(leader, signal.SIGKILL)",
326 "leader_terminal = self._wait_leader_terminal()",
327 "self._cleanup_adopted_children(leader)",
328 "members = _group_members(leader) if descendants_drained else None",
329 "members is not None and members <= {leader} and self._reap()",
330 "descendants_clean = self._cleanup_adopted_children()",
331 "return descendants_clean and self._close_entry_authority()",
332)
333
334
335def semantic_findings(label: str) -> tuple[str, ...] | None:
336 """Return the exact focused finding for one child-subreaper mutation."""
337 loader = PROCESS_LOADER_TOKENS.get(label)
338 if loader is not None:
339 function, token = loader
340 return (
341 "devcontainer image supervisor: "
342 f"{function} process-loader token is not unique: {token}",
343 )
344 if label == "process loader authentication order changed":
345 return (PROCESS_LOADER_ORDER_DIAGNOSTIC,)
346 token = SUBREAPER_TOKENS.get(label, MOVED_PROCESS_TOKENS.get(label))
347 if token is None:
348 return None
349 authority = (
350 "devcontainer image supervisor"
351 if label in SUPERVISOR_SUBREAPER_LABELS
352 else "devcontainer image supervisor process"
353 )
354 finding = f"{authority}: required process-authority token is not unique: {token}"
355 findings = (finding,)
356 if label in {
357 "child subreaper enable call removed",
358 "child subreaper final containment call removed",
359 }:
360 findings += ("devcontainer image supervisor: subreaper cleanup order drifted",)
361 if label == "bound-exit group cleanup reduced to leader PID":
362 findings += ("devcontainer image supervisor: subreaper cleanup order drifted",)
363 if label == "bound-exit cleanup signal block moved after authority checks":
364 authority_token = SUBREAPER_TOKENS["child subreaper lost authority fail-stop removed"]
365 findings += (
366 "devcontainer image supervisor process: required process-authority token "
367 f"is not unique: {authority_token}",
368 )
369 if label == "child subreaper lost authority fail-stop removed":
370 moved = MOVED_PROCESS_TOKENS["bound-exit cleanup signal block moved after authority checks"]
371 findings += (
372 "devcontainer image supervisor process: required process-authority token "
373 f"is not unique: {moved}",
374 )
375 if label == "child subreaper pre-reap adopted drain removed":
376 findings += ("devcontainer image supervisor: subreaper cleanup order drifted",)
377 return findings
378
379
380def _function_source(source: str, name: str) -> str | None:
381 """Return one complete top-level function from parsed source."""
382 try:
383 module = ast.parse(source)
384 except SyntaxError:
385 return None
386 for node in module.body:
387 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
388 return ast.get_source_segment(source, node)
389 return None
390
391
392def _loader_errors(supervisor: str) -> list[str]:
393 """Bind every process-loader property inside its exact owning function."""
394 findings = []
395 for function, token in PROCESS_LOADER_TOKENS.values():
396 body = _function_source(supervisor, function)
397 if body is None or body.count(token) != 1:
398 findings.append(
399 "devcontainer image supervisor: "
400 f"{function} process-loader token is not unique: {token}"
401 )
402 order_specs = (
403 ("_load_process_api", PROCESS_LOADER_ORDER),
404 ("_validate_process_api", PROCESS_VALIDATION_ORDER),
405 )
406 for function, order in order_specs:
407 body = _function_source(supervisor, function)
408 if body is not None and all(body.count(token) == 1 for token in order):
409 positions = tuple(body.index(token) for token in order)
410 if positions != tuple(sorted(positions)):
411 findings.append(PROCESS_LOADER_ORDER_DIAGNOSTIC)
412 return findings
413
414
415def errors(supervisor: str, process_source: str) -> list[str]:
416 """Require every subreaper token once and its cleanup chain in order."""
417 findings = []
418 for label, token in SUBREAPER_TOKENS.items():
419 if label in SUPERVISOR_SUBREAPER_LABELS:
420 owner = SUPERVISOR_SUBREAPER_OWNERS[label]
421 source = _function_source(supervisor, owner) or ""
422 else:
423 source = process_source
424 authority = (
425 "devcontainer image supervisor"
426 if label in SUPERVISOR_SUBREAPER_LABELS
427 else "devcontainer image supervisor process"
428 )
429 if source.count(token) != 1:
430 findings.append(f"{authority}: required process-authority token is not unique: {token}")
431 findings.extend(
432 "devcontainer image supervisor process: required process-authority token is not unique: "
433 f"{token}"
434 for token in MOVED_PROCESS_TOKENS.values()
435 if process_source.count(token) != 1
436 )
437 findings.extend(
438 "devcontainer image supervisor process: required process-authority token is not unique: "
439 f"{token}"
440 for token in PROCESS_STATIC_TOKENS
441 if process_source.count(token) != 1
442 )
443 orders = (
444 (supervisor, SUPERVISOR_SUBREAPER_ORDER),
445 (process_source, PROCESS_SUBREAPER_ORDER),
446 )
447 for source, order in orders:
448 position = -1
449 for token in order:
450 position = source.find(token, position + 1)
451 if position < 0:
452 findings.append("devcontainer image supervisor: subreaper cleanup order drifted")
453 break
454 return findings + _loader_errors(supervisor)