ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
bench_lock_broker.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Own the bench flock without exposing its descriptor to Bash children."""
4
5from __future__ import annotations
6
7import argparse
8import base64
9import binascii
10import fcntl
11import json
12import os
13import re
14import select
15import signal
16import stat
17import subprocess
18import sys
19import tempfile
20import time
21from collections.abc import Callable
22from contextlib import suppress
23from dataclasses import dataclass
24from pathlib import Path
25
26DENIED = 11
27FAILED = 13
28MAX_FIELDS_BYTES = 8192
29FD_PROBE_SOURCE = (
30 'import sys\nsys.stdout.write("READY\\n")\nsys.stdout.flush()\n'
31 "raise SystemExit(0 if sys.stdin.buffer.read(1) == b'x' else 1)"
32)
33
34
35@dataclass(frozen=True)
36class HoldRequest:
37 """One authenticated broker request parsed from the fixed CLI."""
38
39 lock: Path
40 record: Path
41 wait_s: int
42 fields_b64: str
43 host_pid: int
44 host_ticks: int
45
46
47class BrokerError(ValueError):
48 """The lock request or its parent holder identity was unsafe."""
49
50
51def _start_ticks(pid: int) -> int:
52 """Read one Linux process start time without parsing its comm as fields."""
53 try:
54 raw = Path(f"/proc/{pid}/stat").read_text(encoding="ascii")
55 return int(raw[raw.rindex(")") + 2 :].split()[19])
56 except (OSError, ValueError, IndexError) as exc:
57 msg = f"cannot authenticate process {pid}"
58 raise BrokerError(msg) from exc
59
60
61def _fields(encoded: str) -> dict[str, str]:
62 """Decode the existing bounded key=value hold request."""
63 try:
64 raw = base64.b64decode(encoded, validate=True)
65 text = raw.decode("utf-8", "strict")
66 except (binascii.Error, UnicodeError) as exc:
67 msg = "hold fields are not canonical base64 UTF-8"
68 raise BrokerError(msg) from exc
69 if len(raw) > MAX_FIELDS_BYTES:
70 msg = "hold fields exceed their size bound"
71 raise BrokerError(msg)
72 result: dict[str, str] = {}
73 for line in text.splitlines():
74 key, separator, value = line.partition("=")
75 if not separator or key in result:
76 msg = "hold fields are malformed or duplicated"
77 raise BrokerError(msg)
78 result[key] = value
79 required = {
80 "break_glass",
81 "git_ref",
82 "hold_kind",
83 "holder_class",
84 "holder_name",
85 "intent",
86 "lock_id",
87 "max_hold_s",
88 "origin",
89 "resource",
90 }
91 if not required <= result.keys() or result["resource"] != "bench":
92 msg = "hold fields omit the bench identity"
93 raise BrokerError(msg)
94 return result
95
96
97def _close_error(descriptor: int, closer: Callable[[int], None]) -> OSError | None:
98 """Attempt one numeric close exactly once and return its error."""
99 try:
100 closer(descriptor)
101 except OSError as error:
102 return error
103 return None
104
105
106def _open_lock(
107 path: Path,
108 *,
109 opener: Callable[[Path, int, int], int] = os.open,
110 fd_stat: Callable[[int], os.stat_result] = os.fstat,
111 path_stat: Callable[[Path], os.stat_result] | None = None,
112 closer: Callable[[int], None] = os.close,
113) -> tuple[int, os.stat_result]:
114 """Open one stable non-linked regular lock inode with CLOEXEC."""
115 flags = os.O_RDWR | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0)
116 descriptor: int | None = None
117 try:
118 try:
119 descriptor = opener(path, flags, 0o666)
120 except FileNotFoundError:
121 # O_CREAT on an existing foreign-owned file in a sticky directory
122 # is denied when Linux fs.protected_regular=2, even with mode 0666.
123 # Create exclusively after absence, then reopen without O_CREAT if
124 # another legitimate actor wins that race.
125 try:
126 descriptor = opener(path, flags | os.O_CREAT | os.O_EXCL, 0o666)
127 except FileExistsError:
128 descriptor = opener(path, flags, 0o666)
129 observed = fd_stat(descriptor)
130 current = path.stat(follow_symlinks=False) if path_stat is None else path_stat(path)
131 inheritable = os.get_inheritable(descriptor)
132 except OSError as exc:
133 close_failure = None if descriptor is None else _close_error(descriptor, closer)
134 msg = f"cannot open canonical lock: {exc}"
135 error = BrokerError(msg)
136 if close_failure is not None:
137 error.add_note(f"lock descriptor close also failed: {close_failure}")
138 raise error from exc
139 if (
140 not stat.S_ISREG(observed.st_mode)
141 or observed.st_nlink != 1
142 or (observed.st_dev, observed.st_ino) != (current.st_dev, current.st_ino)
143 or inheritable
144 ):
145 close_failure = _close_error(descriptor, closer)
146 msg = "canonical lock is linked, replaced, non-regular, or inheritable"
147 error = BrokerError(msg)
148 if close_failure is not None:
149 error.add_note(f"lock descriptor close also failed: {close_failure}")
150 raise error
151 return descriptor, observed
152
153
154def _take(descriptor: int, wait_s: int) -> None:
155 """Take the flock within a monotonic bounded wait."""
156 deadline = time.monotonic() + wait_s
157 while not _try_take(descriptor):
158 if time.monotonic() >= deadline:
159 raise BlockingIOError
160 time.sleep(0.1)
161
162
163def _try_take(descriptor: int) -> bool:
164 """Attempt one nonblocking flock acquisition."""
165 try:
166 fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
167 except BlockingIOError:
168 return False
169 return True
170
171
172def _record(fields: dict[str, str], host_pid: int, host_ticks: int) -> dict[str, object]:
173 """Construct telemetry only after the broker owns the live kernel lock."""
174 now = int(time.time())
175 try:
176 boot = Path("/proc/sys/kernel/random/boot_id").read_text(encoding="ascii").strip()
177 broker_ticks = _start_ticks(os.getpid())
178 budget = int(fields["max_hold_s"])
179 except (OSError, ValueError) as exc:
180 msg = "cannot construct the live holder record"
181 raise BrokerError(msg) from exc
182 return {
183 "resource": "bench",
184 "lock_id": fields["lock_id"],
185 "holder_class": fields["holder_class"],
186 "holder_name": fields["holder_name"],
187 "pid": os.getpid(),
188 "pid_start_ticks": broker_ticks,
189 "host_pid": host_pid,
190 "host_start_ticks": host_ticks,
191 "boot_id": boot,
192 "origin": fields["origin"],
193 "intent": fields["intent"],
194 "git_ref": fields["git_ref"],
195 "acquired_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
196 "acquired_epoch": now,
197 "max_hold_s": budget,
198 "hold_kind": fields["hold_kind"],
199 "last_activity": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
200 "break_glass": fields["break_glass"] == "true",
201 }
202
203
204def _publish(path: Path, record: dict[str, object]) -> os.stat_result:
205 """Atomically publish and durably bind one holder-record inode."""
206 payload = (json.dumps(record, indent=2, separators=(",", ": ")) + "\n").encode()
207 temp = path.with_name(f".{path.name}.tmp.{os.getpid()}")
208 flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0)
209 descriptor = os.open(temp, flags, 0o666)
210 try:
211 os.fchmod(descriptor, 0o666)
212 remaining = memoryview(payload)
213 while remaining:
214 remaining = remaining[os.write(descriptor, remaining) :]
215 os.fsync(descriptor)
216 finally:
217 os.close(descriptor)
218 temp.replace(path)
219 directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
220 try:
221 os.fsync(directory)
222 finally:
223 os.close(directory)
224 return path.stat(follow_symlinks=False)
225
226
227def _cleanup(path: Path, identity: os.stat_result | None) -> None:
228 """Remove only the exact record inode this broker published."""
229 if identity is None:
230 return
231 try:
232 current = path.stat(follow_symlinks=False)
233 if (current.st_dev, current.st_ino) != (identity.st_dev, identity.st_ino):
234 return
235 path.unlink()
236 directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
237 try:
238 os.fsync(directory)
239 finally:
240 os.close(directory)
241 except FileNotFoundError:
242 return
243
244
245@dataclass(frozen=True)
246class ReleaseActions:
247 """Exact record, lock, and descriptor operations for one finalizer."""
248
249 cleanup: Callable[[Path, os.stat_result | None], None] = _cleanup
250 unlock: Callable[[int, int], None] = fcntl.flock
251 closer: Callable[[int], None] = os.close
252
253
254def _interrupt(_signum: int, _frame: object) -> None:
255 """Turn transport-loss signals into finally-based record/lock cleanup."""
256 raise InterruptedError
257
258
259def _release_hold(
260 record: tuple[Path, os.stat_result | None],
261 descriptor: int,
262 actions: ReleaseActions | None = None,
263) -> OSError | BrokerError | None:
264 """Attempt record cleanup, unlock, and close while retaining the first error."""
265 path, identity = record
266 active = actions or ReleaseActions()
267 first_error: OSError | BrokerError | None = None
268 try:
269 active.cleanup(path, identity)
270 except (OSError, BrokerError) as error:
271 first_error = error
272 try:
273 active.unlock(descriptor, fcntl.LOCK_UN)
274 except OSError as error:
275 if first_error is None:
276 first_error = error
277 close_error = _close_error(descriptor, active.closer)
278 if first_error is None:
279 first_error = close_error
280 return first_error
281
282
283def hold(request: HoldRequest) -> int:
284 """Acquire, publish the exact process pair, and hold until parent EOF."""
285 if os.getppid() != request.host_pid or _start_ticks(request.host_pid) != request.host_ticks:
286 msg = "broker parent is not the authenticated host process"
287 raise BrokerError(msg)
288 fields = _fields(request.fields_b64)
289 descriptor, _identity = _open_lock(request.lock)
290 published: os.stat_result | None = None
291 result = 0
292 primary: OSError | BrokerError | InterruptedError | None = None
293 try:
294 try:
295 _take(descriptor, request.wait_s)
296 except BlockingIOError:
297 result = DENIED
298 else:
299 published = _publish(
300 request.record,
301 _record(fields, request.host_pid, request.host_ticks),
302 )
303 print(f"ACQUIRED {os.getpid()} {_start_ticks(os.getpid())}", flush=True)
304 while os.read(0, 4096):
305 pass
306 except (OSError, BrokerError, InterruptedError) as error:
307 primary = error
308 cleanup_error = _release_hold((request.record, published), descriptor)
309 if primary is not None:
310 if cleanup_error is not None:
311 primary.add_note(f"broker finalizer also failed: {cleanup_error}")
312 raise primary
313 if cleanup_error is not None:
314 raise cleanup_error
315 return result
316
317
318def _selftest_fields() -> str:
319 """Return one valid wrapped-hold request for the broker selftest."""
320 fields = {
321 "resource": "bench",
322 "lock_id": "0123456789abcdef",
323 "holder_class": "agent",
324 "holder_name": "selftest",
325 "intent": "offline broker selftest",
326 "max_hold_s": "30",
327 "hold_kind": "wrapped",
328 "break_glass": "false",
329 "origin": "selftest",
330 "git_ref": "dev",
331 }
332 text = "".join(f"{key}={value}\n" for key, value in fields.items())
333 return base64.b64encode(text.encode()).decode("ascii")
334
335
336def _start_broker(lock: Path, record: Path, host_ticks: int) -> subprocess.Popen[bytes]:
337 """Start these exact source bytes as the synthetic broker child."""
338 source = Path(__file__).read_text(encoding="utf-8")
339 return subprocess.Popen( # noqa: S603 -- fixed interpreter and this exact in-memory source
340 [
341 "/usr/bin/python3",
342 "-B",
343 "-I",
344 "-S",
345 "-c",
346 source,
347 str(lock),
348 str(record),
349 "0",
350 _selftest_fields(),
351 str(os.getpid()),
352 str(host_ticks),
353 ],
354 stdin=subprocess.PIPE,
355 stdout=subprocess.PIPE,
356 stderr=subprocess.PIPE,
357 close_fds=True,
358 )
359
360
361def _lock_owners(lock: Path) -> set[int]:
362 """Return kernel FLOCK owner PIDs for one exact inode."""
363 observed = lock.stat()
364 expected = (os.major(observed.st_dev), os.minor(observed.st_dev), observed.st_ino)
365 owners: set[int] = set()
366 for line in Path("/proc/locks").read_text(encoding="ascii").splitlines():
367 fields = line.split()
368 for index, field in enumerate(fields):
369 match = re.fullmatch(r"([0-9a-fA-F]+):([0-9a-fA-F]+):(\d+)", field)
370 if match is None or "FLOCK" not in fields or index == 0:
371 continue
372 identity = (int(match.group(1), 16), int(match.group(2), 16), int(match.group(3)))
373 if identity == expected and fields[index - 1].isdigit():
374 owners.add(int(fields[index - 1]))
375 return owners
376
377
378def _wait_ack(process: subprocess.Popen[bytes]) -> str:
379 """Read the one flushed acquisition line with a fixed deadline."""
380 if process.stdout is None:
381 return ""
382 readable, _writable, _exceptional = select.select([process.stdout], [], [], 5)
383 return process.stdout.readline().decode("utf-8", "replace").strip() if readable else ""
384
385
386def _process_argv(pid: int) -> tuple[bytes, ...] | None:
387 """Return one live Linux argv only when its NUL framing is canonical."""
388 try:
389 raw = Path(f"/proc/{pid}/cmdline").read_bytes()
390 except OSError:
391 return None
392 if not raw or not raw.endswith(b"\0"):
393 return None
394 fields = tuple(raw[:-1].split(b"\0"))
395 return fields if fields and all(field for field in fields) else None
396
397
398def _start_fd_probe() -> subprocess.Popen[bytes]:
399 """Start one bounded child that stays live until its parent releases it."""
400 return subprocess.Popen( # noqa: S603 -- fixed interpreter and in-tree probe source
401 ["/usr/bin/python3", "-B", "-I", "-S", "-c", FD_PROBE_SOURCE],
402 stdin=subprocess.PIPE,
403 stdout=subprocess.PIPE,
404 stderr=subprocess.PIPE,
405 close_fds=True,
406 )
407
408
409def _close_process_streams(process: subprocess.Popen[bytes]) -> OSError | None:
410 """Attempt every parent-side stream close and retain the first failure."""
411 first_error: OSError | None = None
412 for stream in (process.stdin, process.stdout, process.stderr):
413 if stream is None:
414 continue
415 try:
416 stream.close()
417 except OSError as error:
418 if first_error is None:
419 first_error = error
420 return first_error
421
422
423def _finish_child(
424 process: subprocess.Popen[bytes], *, close_input: bool
425) -> tuple[int, OSError | None]:
426 """Bound, reap, and close one test child while retaining the first I/O error."""
427 first_error: OSError | None = None
428 if close_input and process.stdin is not None:
429 try:
430 process.stdin.close()
431 except OSError as error:
432 first_error = error
433 try:
434 try:
435 status = process.wait(timeout=5)
436 except subprocess.TimeoutExpired:
437 process.kill()
438 status = process.wait(timeout=5)
439 finally:
440 close_error = _close_process_streams(process)
441 if first_error is None:
442 first_error = close_error
443 return status, first_error
444
445
446def _stop_fd_probe(process: subprocess.Popen[bytes]) -> int:
447 """Release or forcibly reap the exact direct probe child within a deadline."""
448 if process.stdin is not None:
449 with suppress(BrokenPipeError):
450 process.stdin.write(b"x")
451 status, close_error = _finish_child(process, close_input=True)
452 if close_error is not None:
453 raise close_error
454 return status
455
456
457class _CloseProbe:
458 """Record a parent-side stream close and optionally refuse it."""
459
460 def __init__(self, *, fail: bool = False) -> None:
461 self.closed = False
462 self.fail = fail
463
464 def close(self) -> None:
465 """Record the exact attempt before raising the injected failure."""
466 self.closed = True
467 if self.fail:
468 message = "injected broker stream-close failure"
469 raise OSError(message)
470
471
472class _ProcessProbe:
473 """Expose the three stream fields consumed by exhaustive cleanup."""
474
475 def __init__(self) -> None:
476 self.stdin = _CloseProbe(fail=True)
477 self.stdout = _CloseProbe()
478 self.stderr = _CloseProbe()
479
480
481def _linked_lock_refusal(root: Path, lock: Path) -> str | None:
482 """Require a hard-linked lock refusal to close its opened descriptor."""
483 linked = root / "open-cleanup-linked.lock"
484 os.link(lock, linked)
485 before = {path.name for path in Path("/proc/self/fd").iterdir()}
486 try:
487 _open_lock(lock)
488 except BrokerError:
489 if {path.name for path in Path("/proc/self/fd").iterdir()} != before:
490 return "linked lock refusal leaked its opened descriptor"
491 else:
492 return "multiply-linked canonical lock inode was accepted"
493 finally:
494 linked.unlink()
495 return None
496
497
498def _existing_lock_open_selftest(root: Path) -> str | None:
499 """Model protected_regular and require a no-create open."""
500 lock = root / "open-existing.lock"
501 lock.write_bytes(b"")
502 calls: list[int] = []
503
504 def protected_opener(path: Path, flags: int, mode: int) -> int:
505 calls.append(flags)
506 if flags & os.O_CREAT:
507 message = "injected fs.protected_regular refusal"
508 raise PermissionError(message)
509 return os.open(path, flags, mode)
510
511 descriptor: int | None = None
512 try:
513 descriptor, _identity = _open_lock(lock, opener=protected_opener)
514 except BrokerError:
515 return "existing canonical lock failed under protected_regular semantics"
516 finally:
517 if descriptor is not None:
518 os.close(descriptor)
519 if len(calls) != 1 or calls[0] & (os.O_CREAT | os.O_EXCL):
520 return "existing canonical lock was reopened with creation flags"
521 return None
522
523
524def _missing_lock_creation_selftest(root: Path) -> str | None:
525 """Require one no-create open followed by one exclusive create."""
526 lock = root / "open-missing.lock"
527 calls: list[int] = []
528
529 def opener(path: Path, flags: int, mode: int) -> int:
530 calls.append(flags)
531 return os.open(path, flags, mode)
532
533 descriptor: int | None = None
534 try:
535 descriptor, _identity = _open_lock(lock, opener=opener)
536 except BrokerError:
537 return "missing canonical lock was not created"
538 finally:
539 if descriptor is not None:
540 os.close(descriptor)
541 create_flags = os.O_CREAT | os.O_EXCL
542 expected_calls = 2
543 if (
544 len(calls) != expected_calls
545 or calls[0] & create_flags
546 or calls[1] & create_flags != create_flags
547 ):
548 return "missing canonical lock did not use one exclusive-create retry"
549 return None
550
551
552def _concurrent_lock_creation_selftest(root: Path) -> str | None:
553 """Require a no-create reopen after another creator wins the race."""
554 lock = root / "open-collision.lock"
555 calls: list[int] = []
556 exclusive_call = 2
557
558 def opener(path: Path, flags: int, mode: int) -> int:
559 calls.append(flags)
560 if len(calls) == 1:
561 raise FileNotFoundError(path)
562 if len(calls) == exclusive_call:
563 path.write_bytes(b"")
564 raise FileExistsError(path)
565 return os.open(path, flags, mode)
566
567 descriptor: int | None = None
568 try:
569 descriptor, _identity = _open_lock(lock, opener=opener)
570 except BrokerError:
571 return "concurrent canonical lock creation was not accepted"
572 finally:
573 if descriptor is not None:
574 os.close(descriptor)
575 create_flags = os.O_CREAT | os.O_EXCL
576 expected_calls = 3
577 if (
578 len(calls) != expected_calls
579 or calls[0] & create_flags
580 or calls[1] & create_flags != create_flags
581 or calls[2] & create_flags
582 ):
583 return "concurrent canonical lock creation did not reopen safely"
584 return None
585
586
587def _open_lock_error_selftest(root: Path) -> list[str]:
588 """Require unexpected open and create failures to remain exact."""
589 failures: list[str] = []
590 denied_calls: list[int] = []
591
592 def denied_opener(_path: Path, flags: int, _mode: int) -> int:
593 denied_calls.append(flags)
594 message = "injected ordinary-open refusal"
595 raise PermissionError(message)
596
597 try:
598 _open_lock(root / "open-denied.lock", opener=denied_opener)
599 except BrokerError as error:
600 if not isinstance(error.__cause__, PermissionError) or len(denied_calls) != 1:
601 failures.append("unexpected ordinary-open failure was retried or obscured")
602 else:
603 failures.append("unexpected ordinary-open failure was accepted")
604
605 creation_calls: list[int] = []
606 creation_failure = OSError("injected exclusive-create failure")
607 expected_creation_calls = 2
608
609 def creation_opener(_path: Path, flags: int, _mode: int) -> int:
610 creation_calls.append(flags)
611 if len(creation_calls) == 1:
612 raise FileNotFoundError
613 raise creation_failure
614
615 try:
616 _open_lock(root / "open-create-error.lock", opener=creation_opener)
617 except BrokerError as error:
618 if (
619 error.__cause__ is not creation_failure
620 or len(creation_calls) != expected_creation_calls
621 ):
622 failures.append("unexpected exclusive-create failure was retried or obscured")
623 else:
624 failures.append("unexpected exclusive-create failure was accepted")
625 return failures
626
627
628def _open_lock_creation_selftest(root: Path) -> list[str]:
629 """Exercise existing, missing, raced, and failed canonical opens."""
630 failures = _open_lock_error_selftest(root)
631 for check in (
632 _existing_lock_open_selftest,
633 _missing_lock_creation_selftest,
634 _concurrent_lock_creation_selftest,
635 ):
636 failure = check(root)
637 if failure is not None:
638 failures.append(failure)
639 return failures
640
641
642def _open_lock_cleanup_selftest(root: Path) -> list[str]:
643 """Prove every post-open refusal releases exactly its owned lock FD."""
644 failures: list[str] = []
645 lock = root / "open-cleanup.lock"
646 lock.write_bytes(b"")
647 opened = -1
648 replacement = -1
649 close_attempts: list[int] = []
650
651 def opener(path: Path, flags: int, mode: int) -> int:
652 nonlocal opened
653 opened = os.open(path, flags, mode)
654 return opened
655
656 def deny_path(_path: Path) -> os.stat_result:
657 message = "injected canonical-path stat failure"
658 raise PermissionError(message)
659
660 def close_reuse(descriptor: int) -> None:
661 nonlocal replacement
662 close_attempts.append(descriptor)
663 os.close(descriptor)
664 replacement = os.open(os.devnull, os.O_RDONLY | os.O_CLOEXEC)
665 if replacement != descriptor:
666 message = "lock descriptor number was not reused"
667 raise BrokerError(message)
668 message = "injected ambiguous lock close failure"
669 raise OSError(message)
670
671 try:
672 _open_lock(lock, opener=opener, path_stat=deny_path, closer=close_reuse)
673 except BrokerError as error:
674 if (
675 error.__cause__ is None
676 or str(error.__cause__) != "injected canonical-path stat failure"
677 or close_attempts != [opened]
678 or replacement != opened
679 ):
680 failures.append("post-open stat refusal lost its primary error or retried close")
681 else:
682 try:
683 os.fstat(replacement)
684 except OSError:
685 failures.append("post-open stat refusal closed a reused unrelated descriptor")
686 else:
687 failures.append("post-open canonical-path stat failure was accepted")
688 finally:
689 with suppress(OSError):
690 os.close(replacement)
691
692 linked_failure = _linked_lock_refusal(root, lock)
693 if linked_failure is not None:
694 failures.append(linked_failure)
695 return failures
696
697
698def _finalizer_cleanup_selftest(root: Path) -> list[str]:
699 """Prove cleanup, unlock, close, and stream failures remain exhaustive."""
700 failures: list[str] = []
701 calls: list[str] = []
702
703 def fail_cleanup(_path: Path, _identity: os.stat_result | None) -> None:
704 calls.append("cleanup")
705 message = "injected record cleanup failure"
706 raise BrokerError(message)
707
708 def fail_unlock(_descriptor: int, _operation: int) -> None:
709 calls.append("unlock")
710 message = "injected unlock failure"
711 raise OSError(message)
712
713 def fail_close(_descriptor: int) -> None:
714 calls.append("close")
715 message = "injected close failure"
716 raise OSError(message)
717
718 error = _release_hold(
719 (root / "unused.json", None),
720 12345,
721 ReleaseActions(fail_cleanup, fail_unlock, fail_close),
722 )
723 if not isinstance(error, BrokerError) or calls != ["cleanup", "unlock", "close"]:
724 failures.append("hold finalizer did not attempt every action or retain its first error")
725 process = _ProcessProbe()
726 stream_error = _close_process_streams(process)
727 streams = (process.stdin, process.stdout, process.stderr)
728 if stream_error is None or not all(stream.closed for stream in streams):
729 failures.append("parent stream cleanup did not attempt every close after failure")
730 return failures
731
732
733def _fd_probe_selftest(lock: Path) -> list[str]:
734 """Prove a ready, parent-held child has not inherited the broker lock."""
735 failures = []
736 child = _start_fd_probe()
737 try:
738 if _wait_ack(child) != "READY" or child.poll() is not None:
739 failures.append("descriptor probe did not remain live after its readiness receipt")
740 else:
741 expected_argv = (
742 b"/usr/bin/python3",
743 b"-B",
744 b"-I",
745 b"-S",
746 b"-c",
747 FD_PROBE_SOURCE.encode("ascii"),
748 )
749 if _process_argv(child.pid) != expected_argv:
750 failures.append("descriptor probe did not retain its fixed reviewed argv")
751 lock_identity = lock.stat()
752 try:
753 descriptors = tuple(Path(f"/proc/{child.pid}/fd").iterdir())
754 for fd in descriptors:
755 observed = fd.stat()
756 if (observed.st_dev, observed.st_ino) == (
757 lock_identity.st_dev,
758 lock_identity.st_ino,
759 ):
760 failures.append("broker lock descriptor leaked to an unrelated child")
761 break
762 except OSError:
763 failures.append("live descriptor probe disappeared during its inode proof")
764 finally:
765 if _stop_fd_probe(child) != 0:
766 failures.append("descriptor probe did not exit cleanly after release")
767 return failures
768
769
770def _broker_lifecycle_selftest(root: Path) -> list[str]:
771 """Prove kernel PID, competitor exclusion, cleanup, and fd non-inheritance."""
772 failures: list[str] = []
773 lock = root / "board.lock"
774 record = root / "holder.json"
775 lock.write_bytes(b"")
776 host_ticks = _start_ticks(os.getpid())
777 process = _start_broker(lock, record, host_ticks)
778 try:
779 ack = _wait_ack(process)
780 source = Path(__file__).read_bytes()
781 expected_argv = (
782 b"/usr/bin/python3",
783 b"-B",
784 b"-I",
785 b"-S",
786 b"-c",
787 source,
788 os.fsencode(lock),
789 os.fsencode(record),
790 b"0",
791 _selftest_fields().encode("ascii"),
792 str(os.getpid()).encode("ascii"),
793 str(host_ticks).encode("ascii"),
794 )
795 argv = _process_argv(process.pid)
796 if argv != expected_argv:
797 failures.append("broker child did not retain its fixed reviewed argv")
798 if ack.split()[:2] != ["ACQUIRED", str(process.pid)] or _lock_owners(lock) != {process.pid}:
799 failures.append("broker acquisition lacks one matching kernel FLOCK owner")
800 competitor = os.open(lock, os.O_RDWR | os.O_CLOEXEC)
801 try:
802 try:
803 fcntl.flock(competitor, fcntl.LOCK_EX | fcntl.LOCK_NB)
804 except BlockingIOError:
805 pass
806 else:
807 failures.append("competing client acquired the broker-held inode")
808 finally:
809 os.close(competitor)
810 failures.extend(_fd_probe_selftest(lock))
811 finally:
812 status, close_error = _finish_child(process, close_input=True)
813 if status != 0 or record.exists() or _lock_owners(lock):
814 failures.append("stdin EOF did not release and clean the broker hold")
815 if close_error is not None:
816 failures.append("broker lifecycle did not close every parent-side stream")
817 return failures
818
819
820def _broker_failure_selftest(root: Path) -> list[str]:
821 """Prove signal, stale-parent, and replaced-record cleanup fail closed."""
822 failures: list[str] = []
823 lock = root / "signal.lock"
824 record = root / "signal.json"
825 lock.write_bytes(b"")
826 process = _start_broker(lock, record, _start_ticks(os.getpid()))
827 try:
828 if not _wait_ack(process):
829 failures.append("signal fixture did not acquire")
830 replacement = record.with_suffix(".replacement")
831 replacement.write_text("unowned\n", encoding="utf-8")
832 replacement.replace(record)
833 process.send_signal(signal.SIGTERM)
834 finally:
835 _status, close_error = _finish_child(process, close_input=False)
836 if record.read_text(encoding="utf-8") != "unowned\n":
837 failures.append("broker cleanup removed a replaced holder record")
838 if close_error is not None:
839 failures.append("signaled broker did not close every parent-side stream")
840 stale = _start_broker(root / "stale.lock", root / "stale.json", 1)
841 stale_status, stale_close_error = _finish_child(stale, close_input=True)
842 if stale_status == 0:
843 failures.append("stale parent start time was accepted")
844 if stale_close_error is not None:
845 failures.append("stale-parent broker did not close every parent-side stream")
846 return failures
847
848
849def run_selftest() -> list[str]:
850 """Exercise the real Linux broker process without a bench or network."""
851 if not Path("/proc/locks").is_file():
852 return ["bench lock broker requires Linux /proc/locks"]
853 with tempfile.TemporaryDirectory(prefix="ra8-lock-broker-") as raw:
854 root = Path(raw)
855 return (
856 _open_lock_creation_selftest(root)
857 + _open_lock_cleanup_selftest(root)
858 + _finalizer_cleanup_selftest(root)
859 + _broker_lifecycle_selftest(root)
860 + _broker_failure_selftest(root)
861 )
862
863
864def main() -> int:
865 """Parse one fixed broker request and fail closed with stable exit codes."""
866 if sys.argv[1:] == ["--selftest"]:
867 failures = run_selftest()
868 for failure in failures:
869 print(f"bench-lock-broker selftest: {failure}", file=sys.stderr)
870 return 1 if failures else 0
871 parser = argparse.ArgumentParser(description=__doc__)
872 parser.add_argument("lock", type=Path)
873 parser.add_argument("record", type=Path)
874 parser.add_argument("wait_s", type=int)
875 parser.add_argument("fields_b64")
876 parser.add_argument("host_pid", type=int)
877 parser.add_argument("host_start_ticks", type=int)
878 args = parser.parse_args()
879 for signum in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT):
880 signal.signal(signum, _interrupt)
881 try:
882 return hold(
883 HoldRequest(
884 args.lock,
885 args.record,
886 args.wait_s,
887 args.fields_b64,
888 args.host_pid,
889 args.host_start_ticks,
890 )
891 )
892 except InterruptedError:
893 return FAILED
894 except (BrokerError, OSError) as exc:
895 print(f"bench-lock-broker: {exc}", file=sys.stderr)
896 return FAILED
897
898
899if __name__ == "__main__":
900 raise SystemExit(main())
-copyright
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298