ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
hil_convergence_safety_image_lock_digest.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Bind privileged image-lock shell surfaces to exact reviewed raw bytes."""
4
5from __future__ import annotations
6
7import ast
8import hashlib
9import os
10import re
11import stat
12from collections.abc import Callable, Mapping
13from dataclasses import dataclass
14from pathlib import Path, PurePosixPath
15
16import hil_convergence_safety_raw_digest_controls as raw_digest_controls
17
18# 2026-08-28: These privileged runtime surfaces and their implementation-control
19# policy own cross-user image serialization and its failure cleanup. Structural
20# enumeration cannot prove an unaudited byte harmless, so each surface is
21# review-bound; intentional edits must update these pins separately.
22DEVCONTAINER_IMAGE_PATH = "scripts/ci/devcontainer_image.sh"
23DEVCONTAINER_IMAGE_RAW_SHA256 = "6cc453374569d45c31351bac2ffd1b99b4dc36af45f5464979cd59ac5e7c5d5f"
24DEVCONTAINER_IMAGE_LOCK_RECEIPTS_PATH = "scripts/ci/devcontainer_image_lock_receipts.bash"
25DEVCONTAINER_IMAGE_LOCK_RECEIPTS_RAW_SHA256 = (
26 "854cfd1163d3d49eda0b05d8a14c5b32385b4b96e7d08de20b03da5cbd1ee727"
27)
28DEVCONTAINER_IMAGE_LOCK_SELFTEST_PATH = "scripts/ci/devcontainer_image_lock_selftest.bash"
29DEVCONTAINER_IMAGE_LOCK_SELFTEST_RAW_SHA256 = (
30 "ecd07f920afd67996299c55a824a951a336e00f5e7ad4522c5a8cd99b885f3f5"
31)
32DEVCONTAINER_IMAGE_SELFTEST_PATH = "scripts/ci/devcontainer_image_selftest.bash"
33DEVCONTAINER_IMAGE_SELFTEST_RAW_SHA256 = (
34 "1d84ebe964ea5085a4145db610cb1d37ad61c02ea6d1e3220f8befdb2de1fcd7"
35)
36DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_PATH = (
37 "scripts/ci/devcontainer_image_bound_exit_selftest.bash"
38)
39DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_RAW_SHA256 = (
40 "79a1a39638b961ecb2daededdd363f04a7f765991b860a629cfbdc589df8de4d"
41)
42DEVCONTAINER_IMAGE_SELFTEST_CASES_PATH = "scripts/ci/devcontainer_image_selftest_cases.bash"
43DEVCONTAINER_IMAGE_SELFTEST_CASES_RAW_SHA256 = (
44 "77284974e5886b4e7796999275145e229d42a6c4fc38c45d1786cba1c0878395"
45)
46DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_PATH = "scripts/ci/devcontainer_image_signal_selftest.bash"
47DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_RAW_SHA256 = (
48 "37890007bdfe343848b8d42f2f58367e6018b0503b3117df6011ad599754ea21"
49)
50DEVCONTAINER_IMAGE_SELFTEST_PROCESS_PATH = "scripts/ci/devcontainer_image_selftest_process.py"
51DEVCONTAINER_IMAGE_SELFTEST_PROCESS_RAW_SHA256 = (
52 "7dbb7b6fa4c477d8baea6ed43f2f1bb87a1555015dda5984fed1efb614b182f8"
53)
54DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_PATH = "scripts/ci/devcontainer_image_selftest_supervisor.py"
55DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_RAW_SHA256 = (
56 "2e52c9d4a2a1f0d1adad083a00611445ad9eff5015d765caef244317846c7990"
57)
58DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_PATH = (
59 "scripts/ci/devcontainer_image_selftest_supervisor_cases.py"
60)
61DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_RAW_SHA256 = (
62 "897a5be60eec486f9f9615fead84db22f8526dba189df305f561bc1c7b5e49e7"
63)
64RAW_DIGEST_CONTROLS_PATH = "scripts/checks/hil_convergence_safety_raw_digest_controls.py"
65RAW_DIGEST_CONTROLS_RAW_SHA256 = "62787d69cb8d4069facfcfb6b97aa1a0d7268d7609ec929f8f38d46fb2057a8b"
66
67_DIRECTORY_MODE = 0o755
68# Checkout roots may be private or read-only. Owner read/search is mandatory;
69# group/other write and every special mode bit are forbidden. Shared setgid
70# worktrees therefore fail closed and need a separately reviewed policy.
71_ROOT_MODE_REQUIRED = 0o500
72_ROOT_MODE_ALLOWED = 0o755
73_EXECUTABLE_MODE = 0o755
74_SOURCE_MODE = 0o644
75_MAX_AUTHORITY_BYTES = 1_048_576
76_READ_STEP_BYTES = 65_536
77_FORBIDDEN_RELATIVE_PARTS = frozenset({"", ".", ".."})
78_FIXED_PATH_BY_PIN = {
79 "DEVCONTAINER_IMAGE_RAW_SHA256": DEVCONTAINER_IMAGE_PATH,
80 "DEVCONTAINER_IMAGE_LOCK_RECEIPTS_RAW_SHA256": DEVCONTAINER_IMAGE_LOCK_RECEIPTS_PATH,
81 "DEVCONTAINER_IMAGE_LOCK_SELFTEST_RAW_SHA256": DEVCONTAINER_IMAGE_LOCK_SELFTEST_PATH,
82 "DEVCONTAINER_IMAGE_SELFTEST_RAW_SHA256": DEVCONTAINER_IMAGE_SELFTEST_PATH,
83 "DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_RAW_SHA256": (
84 DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_PATH
85 ),
86 "DEVCONTAINER_IMAGE_SELFTEST_CASES_RAW_SHA256": DEVCONTAINER_IMAGE_SELFTEST_CASES_PATH,
87 "DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_RAW_SHA256": DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_PATH,
88 "DEVCONTAINER_IMAGE_SELFTEST_PROCESS_RAW_SHA256": DEVCONTAINER_IMAGE_SELFTEST_PROCESS_PATH,
89 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_RAW_SHA256": (
90 DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_PATH
91 ),
92 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_RAW_SHA256": (
93 DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_PATH
94 ),
95 "RAW_DIGEST_CONTROLS_RAW_SHA256": RAW_DIGEST_CONTROLS_PATH,
96}
97
98Target = tuple[str, str, str, int]
99Identity = tuple[int, int, int, int, int, int, int, int, int, int]
100
101
102@dataclass(frozen=True)
103class RawReadAuthority:
104 """Portable numeric ownership derived from the checked repository root.
105
106 Hosted CI, containers, and worktrees use different IDs, so no numeric ID is
107 pinned in source. The retained root descriptor establishes both IDs and all
108 audited directories and files must match. Explicit fields exist so the
109 production owner/group branches have unprivileged two-sided tests.
110 """
111
112 root_uid: int
113 root_gid: int
114 file_uid: int
115 file_gid: int
116
117
118@dataclass(frozen=True)
119class RawReadTestHooks:
120 """Deterministic race and failure hooks used by the two-sided selftest."""
121
122 missing_capability: str | None = None
123 allow_alternate_fixed_paths: bool = False
124 after_root_open: Callable[[Path, int], None] | None = None
125 after_component_open: Callable[[str, int, int], None] | None = None
126 after_file_open: Callable[[str, int], None] | None = None
127 after_read: Callable[[str, int], None] | None = None
128 before_post_rewalk: Callable[[str], None] | None = None
129 before_root_postcheck: Callable[[Path, int], None] | None = None
130 after_close_fd: Callable[[int], None] | None = None
131
132
133@dataclass(frozen=True)
134class _AuditContext:
135 """Shared authority, flags, and test hooks for one retained-root audit."""
136
137 authority: RawReadAuthority
138 flags: Mapping[str, int]
139 hooks: RawReadTestHooks | None
140
141
142@dataclass(frozen=True)
143class _MetadataPolicy:
144 """Exact metadata policy for one directory or regular file."""
145
146 kind: str
147 uid: int
148 gid: int
149 mode: int
150 single_link: bool
151 label: str
152
153
154@dataclass(frozen=True)
155class _DirectoryRequest:
156 """One component-wise openat request."""
157
158 parent_fd: int
159 component: str
160 relative: str
161 index: int
162
163
164@dataclass
165class _OpenedTarget:
166 """Descriptor-backed state retained across one bounded file read."""
167
168 fd: int
169 before: os.stat_result
170 directory_identities: tuple[Identity, ...]
171 ledger: _DescriptorLedger
172
173
174def _shell_targets(authority: Mapping[str, object]) -> tuple[Target, ...]:
175 """Return each raw-bound privileged shell surface."""
176 return (
177 (
178 str(authority.get("DEVCONTAINER_IMAGE_PATH", "")),
179 "DEVCONTAINER_IMAGE_RAW_SHA256",
180 "devcontainer image authority",
181 _EXECUTABLE_MODE,
182 ),
183 (
184 str(authority.get("DEVCONTAINER_IMAGE_LOCK_RECEIPTS_PATH", "")),
185 "DEVCONTAINER_IMAGE_LOCK_RECEIPTS_RAW_SHA256",
186 "devcontainer image-lock receipt authority",
187 _SOURCE_MODE,
188 ),
189 (
190 str(authority.get("DEVCONTAINER_IMAGE_LOCK_SELFTEST_PATH", "")),
191 "DEVCONTAINER_IMAGE_LOCK_SELFTEST_RAW_SHA256",
192 "devcontainer image-lock selftest authority",
193 _SOURCE_MODE,
194 ),
195 (
196 str(authority.get("DEVCONTAINER_IMAGE_SELFTEST_PATH", "")),
197 "DEVCONTAINER_IMAGE_SELFTEST_RAW_SHA256",
198 "devcontainer image selftest lifecycle authority",
199 _SOURCE_MODE,
200 ),
201 (
202 str(authority.get("DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_PATH", "")),
203 "DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_RAW_SHA256",
204 "devcontainer image bound-exit selftest authority",
205 _SOURCE_MODE,
206 ),
207 (
208 str(authority.get("DEVCONTAINER_IMAGE_SELFTEST_CASES_PATH", "")),
209 "DEVCONTAINER_IMAGE_SELFTEST_CASES_RAW_SHA256",
210 "devcontainer image selftest cases authority",
211 _SOURCE_MODE,
212 ),
213 (
214 str(authority.get("DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_PATH", "")),
215 "DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_RAW_SHA256",
216 "devcontainer image signal selftest authority",
217 _SOURCE_MODE,
218 ),
219 )
220
221
222def _python_targets(authority: Mapping[str, object]) -> tuple[Target, ...]:
223 """Return each raw-bound Python process and control surface."""
224 return (
225 (
226 str(authority.get("DEVCONTAINER_IMAGE_SELFTEST_PROCESS_PATH", "")),
227 "DEVCONTAINER_IMAGE_SELFTEST_PROCESS_RAW_SHA256",
228 "devcontainer image selftest process authority",
229 _SOURCE_MODE,
230 ),
231 (
232 str(authority.get("DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_PATH", "")),
233 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_RAW_SHA256",
234 "devcontainer image selftest supervisor authority",
235 _SOURCE_MODE,
236 ),
237 (
238 str(authority.get("DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_PATH", "")),
239 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_RAW_SHA256",
240 "devcontainer image selftest supervisor cases authority",
241 _SOURCE_MODE,
242 ),
243 (
244 str(authority.get("RAW_DIGEST_CONTROLS_PATH", "")),
245 "RAW_DIGEST_CONTROLS_RAW_SHA256",
246 "raw digest implementation-control authority",
247 _SOURCE_MODE,
248 ),
249 )
250
251
252def _targets(values: Mapping[str, object] | None = None) -> tuple[Target, ...]:
253 """Return path, pin, label, and exact file mode for each bound surface."""
254 authority = globals() if values is None else values
255 return _shell_targets(authority) + _python_targets(authority)
256
257
258def pin_names() -> tuple[str, ...]:
259 """Return the exact mutable names used by the production wrong-pin tests."""
260 return tuple(pin_name for _path, pin_name, _label, _mode in _targets())
261
262
263def _source_names() -> frozenset[str]:
264 """Return every direct source authority name parsed without execution."""
265 return frozenset(
266 {
267 "DEVCONTAINER_IMAGE_PATH",
268 "DEVCONTAINER_IMAGE_RAW_SHA256",
269 "DEVCONTAINER_IMAGE_LOCK_RECEIPTS_PATH",
270 "DEVCONTAINER_IMAGE_LOCK_RECEIPTS_RAW_SHA256",
271 "DEVCONTAINER_IMAGE_LOCK_SELFTEST_PATH",
272 "DEVCONTAINER_IMAGE_LOCK_SELFTEST_RAW_SHA256",
273 "DEVCONTAINER_IMAGE_SELFTEST_PATH",
274 "DEVCONTAINER_IMAGE_SELFTEST_RAW_SHA256",
275 "DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_PATH",
276 "DEVCONTAINER_IMAGE_BOUND_EXIT_SELFTEST_RAW_SHA256",
277 "DEVCONTAINER_IMAGE_SELFTEST_CASES_PATH",
278 "DEVCONTAINER_IMAGE_SELFTEST_CASES_RAW_SHA256",
279 "DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_PATH",
280 "DEVCONTAINER_IMAGE_SIGNAL_SELFTEST_RAW_SHA256",
281 "DEVCONTAINER_IMAGE_SELFTEST_PROCESS_PATH",
282 "DEVCONTAINER_IMAGE_SELFTEST_PROCESS_RAW_SHA256",
283 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_PATH",
284 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_RAW_SHA256",
285 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_PATH",
286 "DEVCONTAINER_IMAGE_SELFTEST_SUPERVISOR_CASES_RAW_SHA256",
287 "RAW_DIGEST_CONTROLS_PATH",
288 "RAW_DIGEST_CONTROLS_RAW_SHA256",
289 }
290 )
291
292
293def authority_values() -> dict[str, object]:
294 """Return a detached copy of the direct path and digest authorities."""
295 return {name: globals()[name] for name in _source_names()}
296
297
298def target_specs(values: Mapping[str, object] | None = None) -> tuple[Target, ...]:
299 """Return immutable target specifications for the runtime selftest."""
300 return _targets(values)
301
302
303def maximum_authority_bytes() -> int:
304 """Return the exact bounded-read ceiling exercised by the selftest."""
305 return _MAX_AUTHORITY_BYTES
306
307
308def _expected_digest(
309 pin_name: str, label: str, values: Mapping[str, object] | None = None
310) -> tuple[str | None, str | None]:
311 """Return one exact pin or one diagnostic for that authority."""
312 authority = globals() if values is None else values
313 expected = authority.get(pin_name)
314 if not isinstance(expected, str) or re.fullmatch(r"[0-9a-f]{64}", expected) is None:
315 return None, f"{label}: exact raw SHA-256 pin is missing or malformed"
316 return expected, None
317
318
319def _digest_error(payload: bytes, expected: str, label: str) -> str | None:
320 """Return one raw-byte mismatch diagnostic, if any."""
321 if hashlib.sha256(payload).hexdigest() != expected:
322 return f"{label}: raw bytes differ from exact audited digest"
323 return None
324
325
326def _raw_target_error(
327 target: Target,
328 files: Mapping[str, bytes],
329 values: Mapping[str, object] | None,
330) -> str | None:
331 """Return one trusted local path, pin, presence, or digest error."""
332 path, pin_name, label, _mode = target
333 path_error = _target_path_error(path, pin_name, label)
334 if path_error is not None:
335 return path_error
336 expected, error = _expected_digest(pin_name, label, values)
337 if error is not None:
338 return error
339 payload = files.get(path)
340 if payload is None:
341 return f"{label}: bound file is missing"
342 return _digest_error(payload, expected, label)
343
344
345def raw_errors(files: Mapping[str, bytes], values: Mapping[str, object] | None = None) -> list[str]:
346 """Return errors for missing pins/files or raw-byte digest mismatches."""
347 return [
348 error
349 for target in _targets(values)
350 if (error := _raw_target_error(target, files, values)) is not None
351 ]
352
353
354def _control_target(values: Mapping[str, object]) -> Target:
355 """Return the one raw-bound implementation-control policy target."""
356 matches = [
357 target for target in _targets(values) if target[1] == "RAW_DIGEST_CONTROLS_RAW_SHA256"
358 ]
359 if len(matches) != 1:
360 return "", "RAW_DIGEST_CONTROLS_RAW_SHA256", "raw digest control target", _SOURCE_MODE
361 return matches[0]
362
363
364def _source_values(source: str) -> dict[str, object]:
365 """Parse the direct module authorities without executing candidate code."""
366 try:
367 tree = ast.parse(source)
368 except SyntaxError:
369 return {}
370 values: dict[str, object] = {}
371 for node in tree.body:
372 if not isinstance(node, ast.Assign) or len(node.targets) != 1:
373 continue
374 target = node.targets[0]
375 if isinstance(target, ast.Name) and target.id in _source_names():
376 try:
377 values[target.id] = ast.literal_eval(node.value)
378 except (ValueError, TypeError):
379 continue
380 return values
381
382
383def source_errors(
384 sources: tuple[str, ...],
385 authority_source: str,
386) -> list[str]:
387 """Apply the production digest path to mutation-selftest source bytes."""
388 values = _source_values(authority_source)
389 files = {
390 path: source.encode("utf-8")
391 for (path, _pin, _label, _mode), source in zip(_targets(values), sources, strict=True)
392 }
393 control_error = _raw_target_error(_control_target(values), files, values)
394 if control_error is not None:
395 return [control_error]
396 errors = implementation_errors(authority_source)
397 return errors + raw_errors(files, values)
398
399
400def implementation_errors(source: str) -> list[str]:
401 """Reject removal or duplication of every load-bearing reader control."""
402 return raw_digest_controls.implementation_errors(source)
403
404
405def implementation_controls() -> tuple[raw_digest_controls.ImplementationControl, ...]:
406 """Return every exact reader control from its focused policy module."""
407 return raw_digest_controls.controls()
408
409
410def implementation_mutations(source: str) -> tuple[tuple[str, str, str], ...]:
411 """Return exact one-control mutants and their required diagnostics."""
412 return raw_digest_controls.implementation_mutations(source)
413
414
415def _identity(value: os.stat_result) -> Identity:
416 """Return every security-relevant field required to remain exact."""
417 return (
418 value.st_dev,
419 value.st_ino,
420 value.st_mode,
421 value.st_uid,
422 value.st_gid,
423 value.st_nlink,
424 value.st_size,
425 value.st_mtime_ns,
426 value.st_ctime_ns,
427 stat.S_IFMT(value.st_mode),
428 )
429
430
431def _fixed_relative_parts(path: str) -> tuple[str, ...] | None:
432 """Return canonical POSIX components, rejecting absolute or parent paths."""
433 if re.fullmatch(r"[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)*", path) is None:
434 return None
435 pure = PurePosixPath(path)
436 if pure.is_absolute() or any(part in _FORBIDDEN_RELATIVE_PARTS for part in pure.parts):
437 return None
438 if pure.as_posix() != path:
439 return None
440 return pure.parts
441
442
443def _target_path_error(path: str, pin_name: str, label: str) -> str | None:
444 """Require one normalized path and its immutable repository location."""
445 if _fixed_relative_parts(path) is None:
446 return f"{label}: bound path must be a fixed normalized relative path"
447 if path != _FIXED_PATH_BY_PIN[pin_name]:
448 return f"{label}: bound path differs from fixed repository authority"
449 return None
450
451
452def _required_flags(hooks: RawReadTestHooks | None) -> tuple[dict[str, int] | None, str | None]:
453 """Resolve every required no-follow/nonblocking platform capability."""
454 required = ("O_DIRECTORY", "O_NOFOLLOW", "O_CLOEXEC", "O_NONBLOCK")
455 flags: dict[str, int] = {}
456 for name in required:
457 value = getattr(os, name, None)
458 if hooks is not None and hooks.missing_capability == name:
459 value = None
460 if not isinstance(value, int) or value == 0:
461 return None, (
462 f"raw digest authority: required platform capability is unavailable: {name}"
463 )
464 flags[name] = value
465 predicates = (
466 ("open_dir_fd", os.open in os.supports_dir_fd),
467 ("stat_dir_fd", os.stat in os.supports_dir_fd),
468 ("stat_follow_symlinks", os.stat in os.supports_follow_symlinks),
469 ("pread", callable(getattr(os, "pread", None))),
470 )
471 for name, available in predicates:
472 is_available = available and not (hooks is not None and hooks.missing_capability == name)
473 if not is_available:
474 return None, (
475 f"raw digest authority: required platform capability is unavailable: {name}"
476 )
477 return flags, None
478
479
480def _metadata_error(value: os.stat_result, policy: _MetadataPolicy) -> str | None:
481 """Return an exact type, owner, group, mode, or link diagnostic."""
482 kind_ok = (
483 stat.S_ISDIR(value.st_mode) if policy.kind == "directory" else stat.S_ISREG(value.st_mode)
484 )
485 if not kind_ok or (policy.single_link and value.st_nlink != 1):
486 return f"{policy.label}: bound path is absent, linked, or non-regular"
487 if value.st_uid != policy.uid:
488 return f"{policy.label}: bound path owner differs from repository authority"
489 if value.st_gid != policy.gid:
490 return f"{policy.label}: bound path group differs from repository authority"
491 if stat.S_IMODE(value.st_mode) != policy.mode:
492 return f"{policy.label}: bound path mode differs from exact audited mode"
493 return None
494
495
496class _DescriptorLedger:
497 """Own descriptors until their one exhaustive reverse-order close."""
498
499 def __init__(self, hooks: RawReadTestHooks | None) -> None:
500 self._fds: list[int] = []
501 self._hooks = hooks
502
503 def retain(self, fd: int) -> int:
504 """Record and return one newly opened descriptor."""
505 self._fds.append(fd)
506 return fd
507
508 def close_all(self, label: str) -> list[str]:
509 """Release ownership, close once, then invoke an after-close hook."""
510 errors = []
511 while self._fds:
512 fd = self._fds.pop()
513 errors.extend(_close_released_descriptor(fd, label, self._hooks))
514 return errors
515
516
517def _close_released_descriptor(
518 fd: int,
519 label: str,
520 hooks: RawReadTestHooks | None,
521) -> list[str]:
522 """Close one released number once, then notify an untrusted test hook."""
523 try:
524 os.close(fd)
525 except OSError as exc:
526 return [f"{label}: cannot close audited descriptor safely: {exc.errno}"]
527 hook = None if hooks is None else hooks.after_close_fd
528 if hook is not None:
529 try:
530 hook(fd)
531 except (OSError, RuntimeError):
532 return [f"{label}: descriptor-close hook failed"]
533 return []
534
535
536def _open_directory(
537 request: _DirectoryRequest,
538 context: _AuditContext,
539 ledger: _DescriptorLedger,
540) -> tuple[int | None, Identity | None, str | None]:
541 """No-follow open and bind one exact path component."""
542 label = f"raw digest authority directory: {request.relative}"
543 try:
544 before = os.stat(request.component, dir_fd=request.parent_fd, follow_symlinks=False)
545 error = _metadata_error(
546 before,
547 _MetadataPolicy(
548 kind="directory",
549 uid=context.authority.root_uid,
550 gid=context.authority.root_gid,
551 mode=_DIRECTORY_MODE,
552 single_link=False,
553 label=label,
554 ),
555 )
556 if error is not None:
557 return None, None, error
558 descriptor = ledger.retain(
559 os.open(
560 request.component,
561 os.O_RDONLY
562 | context.flags["O_DIRECTORY"]
563 | context.flags["O_NOFOLLOW"]
564 | context.flags["O_CLOEXEC"]
565 | context.flags["O_NONBLOCK"],
566 dir_fd=request.parent_fd,
567 )
568 )
569 opened = os.fstat(descriptor)
570 if _identity(before) != _identity(opened):
571 return None, None, f"{label}: component changed before its no-follow open"
572 hooks = context.hooks
573 if hooks is not None and hooks.after_component_open is not None:
574 hooks.after_component_open(request.relative, request.index, descriptor)
575 except OSError as exc:
576 return None, None, f"{label}: cannot inspect or open component safely: {exc.errno}"
577 return descriptor, _identity(opened), None
578
579
580def _read_exact_payload(
581 fd: int, before: os.stat_result, relative: str, hooks: RawReadTestHooks | None
582) -> tuple[bytes | None, str | None]:
583 """Use bounded positional reads and prove exact EOF at the pre-open size."""
584 label = f"raw digest authority file: {relative}"
585 if before.st_size > _MAX_AUTHORITY_BYTES:
586 return None, f"{label}: bound file exceeds maximum audited size"
587 chunks = []
588 offset = 0
589 try:
590 while offset < before.st_size:
591 requested = min(_READ_STEP_BYTES, before.st_size - offset)
592 chunk = os.pread(fd, requested, offset)
593 if len(chunk) != requested:
594 return None, f"{label}: bound file shrank during bounded raw-byte read"
595 chunks.append(chunk)
596 offset += requested
597 if os.pread(fd, 1, before.st_size) != b"" or os.pread(fd, 1, before.st_size + 1) != b"":
598 return None, f"{label}: bound file has bytes beyond its audited pre-read size"
599 if hooks is not None and hooks.after_read is not None:
600 hooks.after_read(relative, fd)
601 except OSError as exc:
602 return None, f"{label}: cannot read bound bytes safely: {exc.errno}"
603 return b"".join(chunks), None
604
605
606def _post_rewalk(
607 root_fd: int,
608 parts: tuple[str, ...],
609 expected_directories: tuple[Identity, ...],
610 expected_file: Identity,
611 context: _AuditContext,
612) -> str | None:
613 """Rewalk every component from the retained root and compare identities."""
614 relative = "/".join(parts)
615 ledger = _DescriptorLedger(context.hooks)
616 parent_fd = root_fd
617 error = None
618 try:
619 for index, component in enumerate(parts[:-1]):
620 request = _DirectoryRequest(parent_fd, component, relative, index)
621 descriptor, identity, error = _open_directory(
622 request,
623 _AuditContext(context.authority, context.flags, None),
624 ledger,
625 )
626 if error is not None:
627 break
628 if identity != expected_directories[index]:
629 error = (
630 f"raw digest authority file: {relative}: parent changed during raw-byte read"
631 )
632 break
633 parent_fd = descriptor
634 if error is None:
635 after_path = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False)
636 if _identity(after_path) != expected_file:
637 error = f"raw digest authority file: {relative}: path changed during raw-byte read"
638 except OSError as exc:
639 error = (
640 f"raw digest authority file: {relative}: cannot rewalk bound path safely: {exc.errno}"
641 )
642 close_errors = ledger.close_all(f"raw digest authority file: {relative}")
643 return error if error is not None else (close_errors[0] if close_errors else None)
644
645
646def _open_target(
647 root_fd: int,
648 target: Target,
649 context: _AuditContext,
650) -> tuple[_OpenedTarget | None, tuple[str, ...] | None, str | None]:
651 """Open and bind one target and every directory leading to it."""
652 path, _pin, label, _expected_mode = target
653 parts = _fixed_relative_parts(path)
654 if parts is None:
655 return None, None, f"{label}: bound path must be a fixed normalized relative path"
656 hooks = context.hooks
657 if path != _FIXED_PATH_BY_PIN[target[1]] and (
658 hooks is None or not hooks.allow_alternate_fixed_paths
659 ):
660 return None, None, f"{label}: bound path differs from fixed repository authority"
661 ledger = _DescriptorLedger(context.hooks)
662 parent_fd = root_fd
663 directory_identities: list[Identity] = []
664 error = None
665 completed = False
666 close_errors: list[str] = []
667 try:
668 for index, component in enumerate(parts[:-1]):
669 request = _DirectoryRequest(parent_fd, component, path, index)
670 parent_fd, identity, error = _open_directory(
671 request,
672 context,
673 ledger,
674 )
675 if error is not None:
676 break
677 directory_identities.append(identity)
678 if error is None:
679 fd, before, error = _open_final(parent_fd, parts, target, context, ledger)
680 completed = error is None
681 except (OSError, RuntimeError) as exc:
682 detail = exc.errno if isinstance(exc, OSError) else "hook"
683 error = f"{label}: cannot inspect or open bound bytes safely: {detail}"
684 finally:
685 if not completed:
686 close_errors = ledger.close_all(label)
687 if error is not None:
688 return None, None, error if not close_errors else close_errors[0]
689 state = _OpenedTarget(fd, before, tuple(directory_identities), ledger)
690 return state, parts, None
691
692
693def _open_final(
694 parent_fd: int,
695 parts: tuple[str, ...],
696 target: Target,
697 context: _AuditContext,
698 ledger: _DescriptorLedger,
699) -> tuple[int | None, os.stat_result | None, str | None]:
700 """No-follow open and bind the single-link regular final component."""
701 path, _pin, label, expected_mode = target
702 before = os.stat(parts[-1], dir_fd=parent_fd, follow_symlinks=False)
703 error = _metadata_error(
704 before,
705 _MetadataPolicy(
706 kind="file",
707 uid=context.authority.file_uid,
708 gid=context.authority.file_gid,
709 mode=expected_mode,
710 single_link=True,
711 label=label,
712 ),
713 )
714 if error is not None:
715 return None, None, error
716 fd = ledger.retain(
717 os.open(
718 parts[-1],
719 os.O_RDONLY
720 | context.flags["O_NOFOLLOW"]
721 | context.flags["O_CLOEXEC"]
722 | context.flags["O_NONBLOCK"],
723 dir_fd=parent_fd,
724 )
725 )
726 opened = os.fstat(fd)
727 if _identity(before) != _identity(opened):
728 return None, None, f"{label}: bound path changed before its no-follow open"
729 hooks = context.hooks
730 if hooks is not None and hooks.after_file_open is not None:
731 hooks.after_file_open(path, fd)
732 return fd, before, None
733
734
735def _audit_target(
736 root_fd: int,
737 target: Target,
738 expected: str,
739 context: _AuditContext,
740) -> list[str]:
741 """Read, bind, and digest one fixed target below the retained root."""
742 path, _pin, label, _expected_mode = target
743 state, parts, error = _open_target(root_fd, target, context)
744 if error is not None:
745 return [error]
746 payload = None
747 errors: list[str] = []
748 try:
749 hooks = context.hooks
750 if error is None:
751 payload, error = _read_exact_payload(state.fd, state.before, path, hooks)
752 if error is None:
753 after_fd = os.fstat(state.fd)
754 if _identity(after_fd) != _identity(state.before):
755 error = f"{label}: bound file metadata changed during bounded raw-byte read"
756 if error is None and hooks is not None and hooks.before_post_rewalk is not None:
757 hooks.before_post_rewalk(path)
758 if error is None:
759 error = _post_rewalk(
760 root_fd,
761 parts,
762 state.directory_identities,
763 _identity(state.before),
764 context,
765 )
766 if error is None:
767 error = _digest_error(payload, expected, label)
768 except (OSError, RuntimeError) as exc:
769 detail = exc.errno if isinstance(exc, OSError) else "hook"
770 error = f"{label}: cannot inspect or open bound bytes safely: {detail}"
771 finally:
772 errors.extend(state.ledger.close_all(label))
773 if error is not None:
774 errors.insert(0, error)
775 return errors
776
777
778def _root_metadata_error(value: os.stat_result, authority: RawReadAuthority | None) -> str | None:
779 """Validate repository-root type, ownership, and safe portable mode."""
780 label = "raw digest authority root"
781 if not stat.S_ISDIR(value.st_mode):
782 return f"{label}: repository root is not a directory"
783 mode = stat.S_IMODE(value.st_mode)
784 if (mode & _ROOT_MODE_REQUIRED) != _ROOT_MODE_REQUIRED or mode & ~_ROOT_MODE_ALLOWED:
785 return f"{label}: repository root mode is not safe for the audited authority"
786 if authority is not None and value.st_uid != authority.root_uid:
787 return f"{label}: repository root owner differs from configured authority"
788 if authority is not None and value.st_gid != authority.root_gid:
789 return f"{label}: repository root group differs from configured authority"
790 return None
791
792
793def _audit_all_targets(
794 root_fd: int,
795 values: Mapping[str, object],
796 context: _AuditContext,
797) -> list[str]:
798 """Audit all fixed targets below one retained repository root."""
799 errors = []
800 for target in _targets(values):
801 expected, error = _expected_digest(target[1], target[2], values)
802 if error is None:
803 errors.extend(_audit_target(root_fd, target, expected, context))
804 else:
805 errors.append(error)
806 return errors
807
808
809def audit_live_errors(
810 root: Path,
811 values: Mapping[str, object],
812 *,
813 authority: RawReadAuthority | None = None,
814 hooks: RawReadTestHooks | None = None,
815) -> list[str]:
816 """Audit exact live bytes through one retained no-follow root descriptor."""
817 flags, error = _required_flags(hooks)
818 if error is not None:
819 return [error]
820 ledger = _DescriptorLedger(hooks)
821 root_path = Path(root)
822 errors = []
823 try:
824 before = os.lstat(root_path)
825 error = _root_metadata_error(before, authority)
826 if error is None:
827 root_fd = ledger.retain(
828 os.open(
829 root_path,
830 os.O_RDONLY
831 | flags["O_DIRECTORY"]
832 | flags["O_NOFOLLOW"]
833 | flags["O_CLOEXEC"]
834 | flags["O_NONBLOCK"],
835 )
836 )
837 opened = os.fstat(root_fd)
838 if _identity(before) != _identity(opened):
839 error = "raw digest authority root: path changed before its no-follow open"
840 if error is None:
841 configured = authority or RawReadAuthority(
842 root_uid=opened.st_uid,
843 root_gid=opened.st_gid,
844 file_uid=opened.st_uid,
845 file_gid=opened.st_gid,
846 )
847 context = _AuditContext(configured, flags, hooks)
848 if hooks is not None and hooks.after_root_open is not None:
849 hooks.after_root_open(root_path, root_fd)
850 errors.extend(_audit_all_targets(root_fd, values, context))
851 if hooks is not None and hooks.before_root_postcheck is not None:
852 hooks.before_root_postcheck(root_path, root_fd)
853 after_fd = os.fstat(root_fd)
854 after_path = os.lstat(root_path)
855 if _identity(before) != _identity(after_fd) or _identity(before) != _identity(
856 after_path
857 ):
858 errors.append("raw digest authority root: path changed during retained-root audit")
859 else:
860 errors.append(error)
861 except (OSError, RuntimeError) as exc:
862 detail = exc.errno if isinstance(exc, OSError) else "hook"
863 errors.append(f"raw digest authority root: cannot inspect or open safely: {detail}")
864 finally:
865 errors.extend(ledger.close_all("raw digest authority root"))
866 return errors
867
868
869def live_errors(root: Path) -> list[str]:
870 """Read and validate each exact authority with fail-closed diagnostics."""
871 return audit_live_errors(root, authority_values())
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157