ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
fleet_mutation_lock.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Serialize fleet mutations through an independent dev-host lock guardian."""
5
6from __future__ import annotations
7
8import argparse
9import fcntl
10import os
11import pwd
12import select
13import signal
14import socket
15import stat
16import subprocess
17import sys
18import tempfile
19import time
20from collections.abc import Iterator, Sequence
21from contextlib import contextmanager, suppress
22from dataclasses import dataclass
23from pathlib import Path
24from threading import Lock
25from typing import Any
26
27sys.path.insert(0, str(Path(__file__).resolve().parent))
28
29import fleet_model as fm
30import fleet_reach as fr
31
32LOCK_BUSY_STATUS = 75
33DIRECTORY_MODE = 0o700
34LOCK_MODE = 0o600
35REGISTER_FIELD_COUNT = 2
36CANCELLED_STATUS = 125
37CHILD_WAIT_TIMEOUT = 2
38GUARDIAN_ERROR_STATUS = 2
39LOCK_READY = b"RA8-FLEET-MUTATION-LOCKED\n"
40GUARDIAN_FD_ENV = "RA8_FLEET_MUTATION_GUARDIAN_FD"
41HOLDER_CONNECT_TIMEOUT = 15
42HOLDER_ALIVE_INTERVAL = 5
43HOLDER_ALIVE_COUNT = 3
44HOLDER_READY_TIMEOUT = 20
45GATED_EXEC = (
46 "import os,sys;"
47 "fd=int(sys.argv[1]);token=os.read(fd,1);os.close(fd);"
48 "sys.exit(125) if token != b'1' else os.execvp(sys.argv[2],sys.argv[2:])"
49)
50GUARDIAN_REPLY_TIMEOUT = 5
51REMOTE_HOLDER = (
52 "/bin/bash --noprofile --norc -p -c '"
53 "set -e; umask 077; "
54 'd="$HOME/.local/state/ra8-fleet-mutation"; lock="$d/mutation.lock"; '
55 '/usr/bin/mkdir -p -- "$d"; '
56 '[ ! -L "$d" ] && [ -d "$d" ]; '
57 'uid="$(/usr/bin/id -u)"; '
58 '[ "$(/usr/bin/stat -c %u -- "$d")" = "$uid" ]; '
59 '[ "$(/usr/bin/stat -c %a -- "$d")" = 700 ]; '
60 '[ ! -e "$lock" ] || { [ ! -L "$lock" ] && [ -f "$lock" ]; }; '
61 ': >>"$lock"; /usr/bin/chmod 600 -- "$lock"; '
62 '[ "$(/usr/bin/stat -c %u -- "$lock")" = "$uid" ]; '
63 '[ "$(/usr/bin/stat -c %a -- "$lock")" = 600 ]; '
64 'exec 9>>"$lock"; '
65 'path_meta="$(/usr/bin/stat -c %d:%i:%u:%a:%F -- "$lock")"; '
66 'fd_meta="$(/usr/bin/stat -Lc %d:%i:%u:%a:%F -- /proc/$$/fd/9)"; '
67 '[ "$path_meta" = "$fd_meta" ]; '
68 '/usr/bin/flock -n -E 75 9 || { rc=$?; [ "$rc" -eq 75 ] && exit 75; exit "$rc"; }; '
69 f'printf "{LOCK_READY.decode().rstrip()}\\n"; '
70 "/bin/cat >/dev/null'"
71)
72
73
74class MutationLockBusyError(RuntimeError):
75 """Another controller owns the dev-host fleet mutation lock."""
76
77
78class MutationLockError(RuntimeError):
79 """The dev-host fleet mutation authority could not be reached safely."""
80
81
82@dataclass
83class _CapabilityState:
84 """Mutable process-local capability state without module rebinding."""
85
86 active: socket.socket | None = None
87
88
89_CAPABILITY_STATE = _CapabilityState()
90_CAPABILITY_LOCK = Lock()
91
92
93def authority_host(data: dict[str, Any]) -> str:
94 """Return the unique declared dev control host."""
95 names = [name for name, host in data["hosts"].items() if host.get("class") == "dev_box"]
96 if len(names) != 1:
97 message = f"expected one dev_box mutation authority, found {len(names)}"
98 raise MutationLockError(message)
99 return names[0]
100
101
102def _local_lock_path() -> Path:
103 """Return a validated caller-owned local authority path."""
104 home = Path(pwd.getpwuid(os.getuid()).pw_dir)
105 directory = home / ".local/state/ra8-fleet-mutation"
106 try:
107 directory.mkdir(mode=DIRECTORY_MODE, parents=True, exist_ok=True)
108 except OSError as error:
109 message = "cannot create local mutation lock directory"
110 raise MutationLockError(message) from error
111 metadata = directory.lstat()
112 if (
113 stat.S_ISLNK(metadata.st_mode)
114 or not stat.S_ISDIR(metadata.st_mode)
115 or metadata.st_uid != os.getuid()
116 or stat.S_IMODE(metadata.st_mode) != DIRECTORY_MODE
117 ):
118 message = "local mutation lock directory must be real, caller-owned, mode 0700"
119 raise MutationLockError(message)
120 return directory / "mutation.lock"
121
122
123def _exclusive(path: Path) -> object:
124 """Open and acquire a validated nonblocking local lock."""
125 flags = os.O_RDWR | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0)
126 descriptor = os.open(path, flags, LOCK_MODE)
127 stream = os.fdopen(descriptor, "a+", encoding="ascii")
128 acquired = False
129 try:
130 metadata = os.fstat(descriptor)
131 path_metadata = path.lstat()
132 if (
133 not stat.S_ISREG(metadata.st_mode)
134 or stat.S_ISLNK(path_metadata.st_mode)
135 or metadata.st_uid != os.getuid()
136 or stat.S_IMODE(metadata.st_mode) != LOCK_MODE
137 or (metadata.st_dev, metadata.st_ino) != (path_metadata.st_dev, path_metadata.st_ino)
138 ):
139 message = "local mutation lock must be one caller-owned mode-0600 inode"
140 raise MutationLockError(message)
141 fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
142 acquired = True
143 return stream
144 finally:
145 if not acquired:
146 stream.close()
147
148
149def _holder_argv(data: dict[str, Any]) -> list[str]:
150 """Build the holder-only SSH transport with bounded liveness."""
151 target = fr.ssh_target(data, authority_host(data))
152 return [
153 target[0],
154 "-o",
155 f"ConnectTimeout={HOLDER_CONNECT_TIMEOUT}",
156 "-o",
157 f"ServerAliveInterval={HOLDER_ALIVE_INTERVAL}",
158 "-o",
159 f"ServerAliveCountMax={HOLDER_ALIVE_COUNT}",
160 *target[1:],
161 REMOTE_HOLDER,
162 ]
163
164
165def _terminate_and_reap(process: subprocess.Popen[bytes]) -> None:
166 """Bound termination and always reap a holder transport."""
167 if process.poll() is None:
168 process.terminate()
169 try:
170 process.wait(timeout=CHILD_WAIT_TIMEOUT)
171 except subprocess.TimeoutExpired:
172 process.kill()
173 process.wait()
174
175
176def _read_holder_ready(process: subprocess.Popen[bytes], timeout: float) -> bytes:
177 """Read the holder token without permitting a silent transport to hang."""
178 if process.stdout is None:
179 message = "dev-host lock transport has no output pipe"
180 raise MutationLockError(message)
181 readable, _, _ = select.select([process.stdout], [], [], timeout)
182 if not readable:
183 message = "dev-host mutation lock READY handshake timed out"
184 raise MutationLockError(message)
185 return process.stdout.readline()
186
187
188def _finish_holder_handshake(process: subprocess.Popen[bytes]) -> None:
189 """Validate READY or classify the exact pre-token holder failure."""
190 token = _read_holder_ready(process, HOLDER_READY_TIMEOUT)
191 if token == LOCK_READY:
192 return
193 try:
194 status = process.wait(timeout=CHILD_WAIT_TIMEOUT)
195 except subprocess.TimeoutExpired as error:
196 message = "dev-host lock transport was silent before READY"
197 raise MutationLockError(message) from error
198 if status == LOCK_BUSY_STATUS:
199 message = "another controller owns the dev-host mutation lock"
200 raise MutationLockBusyError(message)
201 message = f"dev-host mutation lock setup failed before READY (rc={status})"
202 raise MutationLockError(message)
203
204
205def _open_remote_holder(data: dict[str, Any]) -> subprocess.Popen[bytes]:
206 """Acquire the remote flock through a bounded token handshake."""
207 process = subprocess.Popen( # noqa: S603 -- fixed SSH executable and remote program
208 _holder_argv(data), stdin=subprocess.PIPE, stdout=subprocess.PIPE
209 )
210 try:
211 _finish_holder_handshake(process)
212 except (MutationLockError, OSError, subprocess.SubprocessError):
213 _terminate_and_reap(process)
214 raise
215 return process
216
217
218def _group_exists(process_group: int) -> bool:
219 """Return whether a protected group retains any live member."""
220 try:
221 entries = tuple(Path("/proc").iterdir())
222 except OSError:
223 return True
224 for entry in entries:
225 if not entry.name.isdigit():
226 continue
227 try:
228 raw = (entry / "stat").read_bytes()
229 except FileNotFoundError:
230 continue
231 except OSError:
232 return True
233 closing = raw.rfind(b")")
234 fields = raw[closing + 2 :].split() if closing >= 0 else []
235 try:
236 state, _parent, group, *_remaining = fields
237 except ValueError:
238 return True
239 if not group.isdigit():
240 return True
241 if int(group) == process_group and state != b"Z":
242 return True
243 return False
244
245
246def _signal_groups(groups: set[int], process_signal: int) -> None:
247 """Signal every process group registered with this guardian."""
248 for process_group in groups:
249 with suppress(ProcessLookupError):
250 os.killpg(process_group, process_signal)
251
252
253def _guardian_loop(capability: socket.socket, holder: object) -> int:
254 """Own the holder until capability closure and every mutation group exit."""
255 groups: set[int] = set()
256 released = False
257 remote = holder if isinstance(holder, subprocess.Popen) else None
258 capability.settimeout(0.0)
259 while True:
260 groups = {group for group in groups if _group_exists(group)}
261 if remote is not None and remote.poll() is not None:
262 _signal_groups(groups, signal.SIGTERM)
263 time.sleep(0.2)
264 _signal_groups(groups, signal.SIGKILL)
265 with suppress(OSError):
266 capability.send(b"LOST")
267 return 2
268 if released and not groups:
269 return 0
270 readable, _, _ = select.select([capability], [], [], 0.2)
271 if not readable:
272 continue
273 try:
274 request = capability.recv(128)
275 except BlockingIOError:
276 continue
277 if not request:
278 released = True
279 continue
280 if request == b"PING":
281 capability.send(b"ACK")
282 continue
283 fields = request.decode("ascii", errors="strict").split()
284 if len(fields) != REGISTER_FIELD_COUNT or fields[0] != "REGISTER":
285 capability.send(b"DENY")
286 continue
287 try:
288 process_group = int(fields[1])
289 except ValueError:
290 capability.send(b"DENY")
291 continue
292 if process_group <= 0:
293 capability.send(b"DENY")
294 continue
295 groups.add(process_group)
296 capability.send(b"ACK")
297
298
299def _guardian_main(
300 data: dict[str, Any], installed_local: bool, capability: socket.socket, status_fd: int
301) -> None:
302 """Acquire authority, publish READY, and supervise independently."""
303 holder: object | None = None
304 status = os.fdopen(status_fd, "wb", buffering=0)
305 try:
306 holder = _exclusive(_local_lock_path()) if installed_local else _open_remote_holder(data)
307 status.write(b"READY\n")
308 result = _guardian_loop(capability, holder)
309 status.write(f"EXIT {result}\n".encode("ascii"))
310 except MutationLockBusyError:
311 status.write(b"BUSY\n")
312 result = LOCK_BUSY_STATUS
313 except (
314 MutationLockError,
315 OSError,
316 ValueError,
317 UnicodeError,
318 subprocess.SubprocessError,
319 ) as error:
320 status.write(f"ERROR {type(error).__name__}: {error}\n".encode("utf-8", errors="replace"))
321 result = GUARDIAN_ERROR_STATUS
322 finally:
323 if isinstance(holder, subprocess.Popen):
324 if holder.stdin is not None:
325 with suppress(BrokenPipeError):
326 holder.stdin.close()
327 _terminate_and_reap(holder)
328 elif holder is not None:
329 holder.close()
330 capability.close()
331 status.close()
332 os._exit(result)
333
334
335def _read_status_line(descriptor: int, timeout: float) -> bytes:
336 """Read one bounded guardian status record."""
337 readable, _, _ = select.select([descriptor], [], [], timeout)
338 if not readable:
339 message = "mutation guardian startup timed out"
340 raise MutationLockError(message)
341 output = bytearray()
342 while not output.endswith(b"\n"):
343 chunk = os.read(descriptor, 1)
344 if not chunk:
345 break
346 output += chunk
347 return bytes(output)
348
349
350def _start_guardian(data: dict[str, Any], installed_local: bool) -> tuple[int, socket.socket, int]:
351 """Fork a session-independent guardian and await authoritative readiness."""
352 parent_socket, guardian_socket = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
353 status_read, status_write = os.pipe()
354 pid = os.fork()
355 if pid == 0:
356 parent_socket.close()
357 os.close(status_read)
358 with suppress(OSError):
359 os.setsid()
360 for process_signal in (signal.SIGTERM, signal.SIGHUP, signal.SIGINT, signal.SIGQUIT):
361 signal.signal(process_signal, signal.SIG_IGN)
362 _guardian_main(data, installed_local, guardian_socket, status_write)
363 guardian_socket.close()
364 os.close(status_write)
365 try:
366 record = _read_status_line(status_read, HOLDER_READY_TIMEOUT + 5)
367 except (MutationLockError, OSError):
368 parent_socket.close()
369 with suppress(ProcessLookupError):
370 os.kill(pid, signal.SIGKILL)
371 os.waitpid(pid, 0)
372 os.close(status_read)
373 raise
374 if record == b"READY\n":
375 return pid, parent_socket, status_read
376 parent_socket.close()
377 os.waitpid(pid, 0)
378 os.close(status_read)
379 if record == b"BUSY\n":
380 message = "another controller owns the dev-host mutation lock"
381 raise MutationLockBusyError(message)
382 message = record.decode("utf-8", errors="replace").strip()
383 if not message:
384 message = "guardian exited before READY"
385 raise MutationLockError(message)
386
387
388def _socket_from_descriptor(descriptor: int) -> socket.socket:
389 """Validate and adopt one inherited AF_UNIX capability descriptor."""
390 candidate = socket.socket(fileno=descriptor)
391 if candidate.family != socket.AF_UNIX:
392 candidate.close()
393 message = "guardian descriptor is not an AF_UNIX socket"
394 raise OSError(message)
395 return candidate
396
397
398def _capability_socket() -> socket.socket:
399 """Resolve the active inherited guardian capability, never an environment claim alone."""
400 if _CAPABILITY_STATE.active is not None:
401 return _CAPABILITY_STATE.active
402 raw = os.environ.get(GUARDIAN_FD_ENV)
403 if raw is None:
404 message = "mutating fleet command requires a live guardian capability"
405 raise MutationLockError(message)
406 try:
407 descriptor = int(raw)
408 _CAPABILITY_STATE.active = _socket_from_descriptor(descriptor)
409 except (ValueError, OSError) as error:
410 message = "inherited guardian capability is invalid"
411 raise MutationLockError(message) from error
412 return _CAPABILITY_STATE.active
413
414
415def require_guardian_capability() -> None:
416 """Validate live authority and register this mutation process group."""
417 capability = _capability_socket()
418 with _CAPABILITY_LOCK:
419 prior_timeout = capability.gettimeout()
420 try:
421 capability.settimeout(GUARDIAN_REPLY_TIMEOUT)
422 capability.send(f"REGISTER {os.getpgrp()}".encode("ascii"))
423 if capability.recv(16) != b"ACK":
424 message = "mutation guardian denied the process group"
425 raise MutationLockError(message)
426 except (OSError, TimeoutError) as error:
427 message = "mutation guardian lease is lost"
428 raise MutationLockError(message) from error
429 finally:
430 with suppress(OSError):
431 capability.settimeout(prior_timeout)
432
433
434def guardian_subprocess_kwargs() -> dict[str, object]:
435 """Prove live authority, then return the FD inheritance for a guarded spawn."""
436 capability = _capability_socket()
437 with _CAPABILITY_LOCK:
438 prior_timeout = capability.gettimeout()
439 try:
440 capability.settimeout(GUARDIAN_REPLY_TIMEOUT)
441 capability.send(b"PING")
442 if capability.recv(16) != b"ACK":
443 message = "mutation guardian denied child preparation"
444 raise MutationLockError(message)
445 except (OSError, TimeoutError) as error:
446 message = "mutation guardian was lost before child spawn"
447 raise MutationLockError(message) from error
448 finally:
449 with suppress(OSError):
450 capability.settimeout(prior_timeout)
451 descriptor = capability.fileno()
452 environment = os.environ.copy()
453 environment[GUARDIAN_FD_ENV] = str(descriptor)
454 return {"env": environment, "pass_fds": (descriptor,)}
455
456
457@contextmanager
458def mutation_lock(
459 data: dict[str, Any], *, installed_local: bool = False, on_loss: object | None = None
460) -> Iterator[None]:
461 """Expose one guardian capability while it owns the canonical flock."""
462 del on_loss
463 if _CAPABILITY_STATE.active is not None:
464 message = "nested mutation guardians are forbidden"
465 raise MutationLockError(message)
466 pid, capability, status_fd = _start_guardian(data, installed_local)
467 _CAPABILITY_STATE.active = capability
468 body_completed = False
469 try:
470 yield
471 body_completed = True
472 finally:
473 _CAPABILITY_STATE.active = None
474 capability.close()
475 _, wait_status = os.waitpid(pid, 0)
476 ready = select.select([status_fd], [], [], 0)[0]
477 final = _read_status_line(status_fd, 0) if ready else b""
478 os.close(status_fd)
479 failed = not os.WIFEXITED(wait_status) or os.WEXITSTATUS(wait_status) != 0
480 if body_completed and failed:
481 detail = final.decode("utf-8", errors="replace").strip()
482 message = detail or "mutation guardian lost authority"
483 raise MutationLockError(message)
484
485
486def _gated_argv(descriptor: int, argv: Sequence[str]) -> list[str]:
487 """Build an inert bootstrap that execs command code only after one token."""
488 return [sys.executable, "-I", "-c", GATED_EXEC, str(descriptor), *argv]
489
490
491def run_locked(data: dict[str, Any], argv: Sequence[str]) -> int:
492 """Run one complete process group under an independent guardian."""
493 child: subprocess.Popen[bytes] | None = None
494 pending_signal = 0
495
496 def forward(process_signal: int, _frame: object) -> None:
497 nonlocal pending_signal
498 pending_signal = process_signal
499 if child is not None:
500 with suppress(ProcessLookupError):
501 os.killpg(child.pid, process_signal)
502
503 handled = (signal.SIGTERM, signal.SIGHUP, signal.SIGINT, signal.SIGQUIT)
504 previous = {item: signal.signal(item, forward) for item in handled}
505 try:
506 with mutation_lock(data):
507 if pending_signal:
508 return 128 + pending_signal
509 gate_read, gate_write = os.pipe()
510 blocked = signal.pthread_sigmask(signal.SIG_BLOCK, handled)
511 try:
512 kwargs = guardian_subprocess_kwargs()
513 pass_fds = (*kwargs["pass_fds"], gate_read)
514 child = subprocess.Popen( # noqa: S603 -- caller supplies exact argv
515 _gated_argv(gate_read, argv),
516 start_new_session=True,
517 env=kwargs["env"],
518 pass_fds=pass_fds,
519 )
520 os.close(gate_read)
521 gate_read = -1
522 require_guardian_capability_for_group(child.pid)
523 queued = set(signal.sigpending()).intersection(handled)
524 cancellation = pending_signal or (min(queued) if queued else 0)
525 if cancellation:
526 with suppress(ProcessLookupError):
527 os.killpg(child.pid, cancellation)
528 else:
529 os.write(gate_write, b"1")
530 except BaseException:
531 if child is not None:
532 with suppress(ProcessLookupError):
533 os.killpg(child.pid, signal.SIGKILL)
534 child.wait()
535 raise
536 finally:
537 if gate_read >= 0:
538 os.close(gate_read)
539 os.close(gate_write)
540 signal.pthread_sigmask(signal.SIG_SETMASK, blocked)
541 return child.wait()
542 finally:
543 for process_signal, handler in previous.items():
544 signal.signal(process_signal, handler)
545
546
547def require_guardian_capability_for_group(process_group: int) -> None:
548 """Register a just-created, still-inert protected process group."""
549 capability = _capability_socket()
550 with _CAPABILITY_LOCK:
551 prior_timeout = capability.gettimeout()
552 try:
553 capability.settimeout(GUARDIAN_REPLY_TIMEOUT)
554 capability.send(f"REGISTER {process_group}".encode("ascii"))
555 if capability.recv(16) != b"ACK":
556 message = "mutation guardian refused child publication"
557 raise MutationLockError(message)
558 except (OSError, TimeoutError) as error:
559 message = "mutation guardian was lost before child publication"
560 raise MutationLockError(message) from error
561 finally:
562 with suppress(OSError):
563 capability.settimeout(prior_timeout)
564
565
566def _boundary_contract_errors(infra_text: str) -> list[str]:
567 """Require every direct wrapper mutation to enter the guardian."""
568 required = (
569 'MUTATION_LOCK="${ROOT}/scripts/dev/fleet_mutation_lock.py"\n',
570 ' "$PYTHON" -I "$MUTATION_LOCK" -- "$PYTHON" -I "$FLEET" "$@"\n',
571 ' fleet_mutation apply "$@"\n',
572 ' fleet_mutation register-runner "$@"\n',
573 ' fleet_mutation register-hil "$@"\n',
574 ' fleet_mutation remove "$@"\n',
575 ' fleet_mutation scale "$1" "$2"\n',
576 )
577 return (
578 []
579 if all(infra_text.count(item) == 1 for item in required)
580 else ["a supported direct mutation bypasses the independent guardian"]
581 )
582
583
584def _capability_selftest() -> list[str]:
585 """Prove an environment claim cannot manufacture mutation authority."""
586 failures: list[str] = []
587 prior = os.environ.get(GUARDIAN_FD_ENV)
588 os.environ[GUARDIAN_FD_ENV] = "999999"
589 try:
590 require_guardian_capability()
591 failures.append("an environment-only guardian claim was accepted")
592 except MutationLockError:
593 pass
594 finally:
595 if prior is None:
596 os.environ.pop(GUARDIAN_FD_ENV, None)
597 else:
598 os.environ[GUARDIAN_FD_ENV] = prior
599 return failures
600
601
602def _bench_reentry_selftest() -> list[str]:
603 """Prove validated authority remains inheritable by a nested mutation entry."""
604 parent, guardian = socket.socketpair(socket.AF_UNIX, socket.SOCK_SEQPACKET)
605 guardian_pid = os.fork()
606 if guardian_pid == 0:
607 parent.close()
608 for expected in (b"PING", b"REGISTER"):
609 request = guardian.recv(128)
610 if not request.startswith(expected):
611 os._exit(2)
612 guardian.send(b"ACK")
613 os._exit(0)
614 guardian.close()
615 descriptor = parent.fileno()
616 previous = os.environ.get(GUARDIAN_FD_ENV)
617 os.environ[GUARDIAN_FD_ENV] = str(descriptor)
618 _CAPABILITY_STATE.active = None
619 kwargs = guardian_subprocess_kwargs()
620 nested = os.fork()
621 if nested == 0:
622 _CAPABILITY_STATE.active.detach()
623 _CAPABILITY_STATE.active = None
624 os.environ.clear()
625 os.environ.update(kwargs["env"])
626 os.setsid()
627 require_guardian_capability()
628 os._exit(0)
629 _, nested_status = os.waitpid(nested, 0)
630 parent.close()
631 _CAPABILITY_STATE.active = None
632 _, guardian_status = os.waitpid(guardian_pid, 0)
633 if previous is None:
634 os.environ.pop(GUARDIAN_FD_ENV, None)
635 else:
636 os.environ[GUARDIAN_FD_ENV] = previous
637 inherited = kwargs["pass_fds"] == (descriptor,)
638 if not inherited or nested_status != 0 or guardian_status != 0:
639 return ["nested bench mutation did not inherit and register the live guardian"]
640 return []
641
642
643def _metadata_selftest() -> list[str]:
644 """Prove remote setup and holder transport retain their fail-closed clauses."""
645 failures: list[str] = []
646 clauses = ("set -e", "[ ! -L", "stat -c %u", "stat -c %a", "%d:%i", "exit 75")
647 if any(clause not in REMOTE_HOLDER for clause in clauses):
648 failures.append("remote holder setup metadata checks are incomplete")
649 ready_clause = f'printf "{LOCK_READY.decode().rstrip()}\\n"'
650 if ready_clause not in REMOTE_HOLDER:
651 failures.append("remote holder READY record is not newline-delimited")
652 fake = {"hosts": {"dev": {"class": "dev_box", "connect": {"address": "127.0.0.1"}}}}
653 argv = _holder_argv(fake)
654 options = ("ConnectTimeout=15", "ServerAliveInterval=5", "ServerAliveCountMax=3")
655 failures.extend(f"holder transport omits {option}" for option in options if option not in argv)
656 return failures
657
658
659def _two_controller_selftest(path: Path) -> list[str]:
660 """Prove one live kernel lock excludes a second controller."""
661 failures: list[str] = []
662 ready_read, ready_write = os.pipe()
663 release_read, release_write = os.pipe()
664 pid = os.fork()
665 if pid == 0:
666 os.close(ready_read)
667 os.close(release_write)
668 try:
669 with _exclusive(path):
670 os.write(ready_write, b"1")
671 os.read(release_read, 1)
672 finally:
673 os._exit(0)
674 os.close(ready_write)
675 os.close(release_read)
676 try:
677 if os.read(ready_read, 1) != b"1":
678 failures.append("first controller did not acquire the mutation lock")
679 try:
680 with _exclusive(path):
681 failures.append("second controller acquired the live mutation lock")
682 except BlockingIOError:
683 pass
684 finally:
685 os.write(release_write, b"1")
686 os.close(release_write)
687 os.close(ready_read)
688 os.waitpid(pid, 0)
689 with _exclusive(path):
690 pass
691 return failures
692
693
694def _silent_transport_selftest() -> list[str]:
695 """Prove a connected transport that emits no token is bounded and reaped."""
696 process = subprocess.Popen(["/bin/sleep", "60"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
697 try:
698 _read_holder_ready(process, 0.05)
699 except MutationLockError:
700 _terminate_and_reap(process)
701 else:
702 _terminate_and_reap(process)
703 return ["silent pre-token holder transport did not time out"]
704 if process.poll() is None:
705 return ["timed-out holder transport was not reaped"]
706 return []
707
708
709def _holder_loss_selftest() -> list[str]:
710 """Prove post-token holder death kills an already-published mutation group."""
711 parent_capability, guardian_capability = socket.socketpair(
712 socket.AF_UNIX, socket.SOCK_SEQPACKET
713 )
714 guardian_pid = os.fork()
715 if guardian_pid == 0:
716 parent_capability.close()
717 holder = subprocess.Popen(["/bin/sleep", "0.15"])
718 result = _guardian_loop(guardian_capability, holder)
719 _terminate_and_reap(holder)
720 os._exit(result)
721 guardian_capability.close()
722 child = subprocess.Popen(["/bin/sleep", "60"], start_new_session=True)
723 parent_capability.send(f"REGISTER {child.pid}".encode("ascii"))
724 acknowledged = parent_capability.recv(16) == b"ACK"
725 try:
726 child.wait(timeout=CHILD_WAIT_TIMEOUT)
727 except subprocess.TimeoutExpired:
728 os.killpg(child.pid, signal.SIGKILL)
729 child.wait()
730 failure = "holder death did not terminate the published mutation group"
731 else:
732 failure = ""
733 parent_capability.close()
734 os.waitpid(guardian_pid, 0)
735 failures = [] if acknowledged else ["guardian did not publish the mutation child"]
736 if failure:
737 failures.append(failure)
738 return failures
739
740
741def _cancelled_spawn_selftest(path: Path) -> list[str]:
742 """Prove cancellation closes the execution gate before command code runs."""
743 gate_read, gate_write = os.pipe()
744 child = os.fork()
745 if child == 0:
746 os.close(gate_write)
747 token = os.read(gate_read, 1)
748 os.close(gate_read)
749 if token != b"1":
750 os._exit(CANCELLED_STATUS)
751 path.touch()
752 os._exit(0)
753 os.close(gate_read)
754 os.close(gate_write)
755 _, wait_status = os.waitpid(child, 0)
756 status = os.waitstatus_to_exitcode(wait_status)
757 if status != CANCELLED_STATUS or path.exists():
758 return ["cancellation before publication allowed mutation code to execute"]
759 return []
760
761
762def _hard_parent_death_selftest(path: Path) -> list[str]:
763 """Prove controller SIGKILL cannot release authority before its child group."""
764 ready_read, ready_write = os.pipe()
765 controller = os.fork()
766 if controller == 0:
767 os.close(ready_read)
768 lock_read, lock_write = os.pipe()
769 parent_capability, guardian_capability = socket.socketpair(
770 socket.AF_UNIX, socket.SOCK_SEQPACKET
771 )
772 guardian = os.fork()
773 if guardian == 0:
774 parent_capability.close()
775 os.close(lock_read)
776 lock = _exclusive(path)
777 os.write(lock_write, b"L")
778 os.close(lock_write)
779 result = _guardian_loop(guardian_capability, lock)
780 lock.close()
781 os._exit(result)
782 guardian_capability.close()
783 os.close(lock_write)
784 if os.read(lock_read, 1) != b"L":
785 os._exit(2)
786 os.close(lock_read)
787 child = subprocess.Popen(
788 ["/bin/sleep", "0.4"],
789 start_new_session=True,
790 pass_fds=(parent_capability.fileno(),),
791 )
792 parent_capability.send(f"REGISTER {child.pid}".encode("ascii"))
793 if parent_capability.recv(16) != b"ACK":
794 os._exit(3)
795 os.write(ready_write, b"C")
796 signal.pause()
797 os._exit(4)
798 os.close(ready_write)
799 if os.read(ready_read, 1) != b"C":
800 os.kill(controller, signal.SIGKILL)
801 os.waitpid(controller, 0)
802 return ["hard-parent-death fixture did not publish its child"]
803 os.close(ready_read)
804 os.kill(controller, signal.SIGKILL)
805 os.waitpid(controller, 0)
806 try:
807 with _exclusive(path):
808 return ["controller SIGKILL released the flock while its child survived"]
809 except BlockingIOError:
810 pass
811 deadline = time.monotonic() + 2
812 while time.monotonic() < deadline:
813 try:
814 with _exclusive(path):
815 return []
816 except BlockingIOError:
817 time.sleep(0.05)
818 return ["guardian did not release after the complete child group exited"]
819
820
821def _isolated_import_selftest() -> list[str]:
822 """Prove the exact isolated interpreter entry can load local fleet modules."""
823 result = subprocess.run( # noqa: S603 -- fixed Python and current module
824 ["/usr/bin/python3", "-I", str(Path(__file__).resolve()), "--selftest-import"],
825 capture_output=True,
826 check=False,
827 text=True,
828 )
829 if result.returncode == 0:
830 return []
831 detail = result.stderr.strip() or f"exit {result.returncode}"
832 return [f"isolated mutation-lock entry failed: {detail}"]
833
834
835def run_selftest() -> list[str]:
836 """Run deterministic boundary, metadata, capability, and exclusion proofs."""
837 root = Path(__file__).resolve().parents[2]
838 infra_text = (root / "scripts/dev/infra.sh").read_text(encoding="ascii")
839 failures = (
840 _isolated_import_selftest()
841 + _boundary_contract_errors(infra_text)
842 + _capability_selftest()
843 + _metadata_selftest()
844 + _silent_transport_selftest()
845 + _holder_loss_selftest()
846 + _bench_reentry_selftest()
847 + _cancelled_spawn_selftest(Path(tempfile.gettempdir()) / f"ra8-cancel-{os.getpid()}")
848 )
849 with tempfile.TemporaryDirectory(prefix="ra8-fleet-mutation-lock-") as raw:
850 directory = Path(raw)
851 directory.chmod(DIRECTORY_MODE)
852 failures += _two_controller_selftest(directory / "mutation.lock")
853 failures += _hard_parent_death_selftest(directory / "parent-death.lock")
854 return failures
855
856
857def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
858 """Parse the offline selftest or one protected command."""
859 parser = argparse.ArgumentParser(description=__doc__)
860 parser.add_argument("--selftest", action="store_true")
861 parser.add_argument("--selftest-import", action="store_true", help=argparse.SUPPRESS)
862 parser.add_argument("command", nargs=argparse.REMAINDER)
863 return parser.parse_args(argv)
864
865
866def main(argv: Sequence[str] | None = None) -> int:
867 """Enter the lock selftest or execute one serialized fleet mutation."""
868 args = parse_args(argv)
869 if args.selftest_import:
870 return 0
871 if args.selftest:
872 failures = run_selftest()
873 for failure in failures:
874 print(f"fleet_mutation_lock.py --selftest: FAIL: {failure}", file=sys.stderr)
875 if not failures:
876 print("fleet_mutation_lock.py --selftest: PASS")
877 return int(bool(failures))
878 command = list(args.command)
879 if command[:1] == ["--"]:
880 command = command[1:]
881 if not command:
882 print("fleet-mutation-lock: a command is required", file=sys.stderr)
883 return 2
884 try:
885 return run_locked(fm.load(), command)
886 except MutationLockBusyError as error:
887 print(f"fleet-mutation-lock: {error}", file=sys.stderr)
888 return LOCK_BUSY_STATUS
889 except (MutationLockError, OSError, fm.FleetError) as error:
890 print(f"fleet-mutation-lock: FATAL: {error}", file=sys.stderr)
891 return 2
892
893
894if __name__ == "__main__":
895 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157