3"""Validate remote-GDB transport fields and broker its local state."""
5from __future__
import annotations
22from collections.abc
import Callable
23from dataclasses
import dataclass
24from pathlib
import Path
28STATE_NAME =
"state.json"
29SOCKET_NAME =
"control.sock"
30MAX_RECORD_BYTES = 16 * 1024
31MAX_AUTHORITY_BYTES = 256 * 1024
32MAX_REQUEST_BYTES = 1024
33MAX_APP_ARG_BYTES = 256
40HEX_RE = re.compile(
r"[0-9a-f]{64}")
41ACCEPT_TIMEOUT = socket.timeout
44class GuardError(ValueError):
45 """A transport, broker, or process-state claim failed closed."""
48@dataclass(frozen=
True)
50 """One descriptor-bound state record and its content identity."""
52 value: dict[str, object]
57@dataclass(frozen=True)
59 """Canonical authorities supplied by the direct parent Bash process."""
66 runtime_base: Path |
None =
None
67 proc_root: Path = Path(
"/proc")
68 platform: str = sys.platform
71@dataclass(frozen=True)
73 """Injectable process hooks keep offline tests away from real signals."""
75 getppid: Callable[[], int] = os.getppid
76 signal_parent: Callable[[int],
None] |
None =
None
77 pid_alive: Callable[[int], bool] |
None =
None
80def _load_process_authority() -> dict[str, object]:
81 path = Path(__file__).resolve(strict=
True).with_name(
"remote_gdb_process.py")
83 observed = path.lstat()
84 except OSError
as exc:
85 msg =
"remote-GDB process authority is unavailable"
86 raise RuntimeError(msg)
from exc
87 if not stat.S_ISREG(observed.st_mode)
or path.is_symlink():
88 msg =
"remote-GDB process authority is linked or special"
89 raise RuntimeError(msg)
90 return runpy.run_path(str(path))
93_PROCESS = _load_process_authority()
94ProcessError = _PROCESS[
"ProcessError"]
95ProcessProof = _PROCESS[
"ProcessProof"]
98def _validate_port(value: str) -> int:
99 if not value.isascii()
or not value.isdecimal():
100 msg =
"port must be decimal"
101 raise GuardError(msg)
102 port = int(value, 10)
103 if not PORT_MIN <= port <= PORT_MAX:
104 msg =
"port must be between 1024 and 65535"
105 raise GuardError(msg)
109def _canonical_authorities(root_arg: str) -> tuple[Path, Path]:
110 root = Path(root_arg)
112 resolved_root = root.resolve(strict=
True)
113 script = resolved_root /
"scripts/dev/remote_gdb_server.sh"
114 resolved_script = script.resolve(strict=
True)
115 root_stat = root.lstat()
116 script_stat = script.lstat()
117 except OSError
as exc:
118 msg =
"workspace or script authority is unavailable"
119 raise GuardError(msg)
from exc
120 expected = resolved_root /
"scripts/dev/remote_gdb_server.sh"
121 if root != resolved_root
or script != resolved_script
or resolved_script != expected:
122 msg =
"workspace or script authority is not canonical"
123 raise GuardError(msg)
124 if not stat.S_ISDIR(root_stat.st_mode)
or not stat.S_ISREG(script_stat.st_mode):
125 msg =
"workspace or script authority has the wrong type"
126 raise GuardError(msg)
127 return resolved_root, resolved_script
130def _regular_identity(path: Path) -> tuple[os.stat_result, str]:
131 flags = os.O_RDONLY | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
133 descriptor = os.open(path, flags)
134 before = os.fstat(descriptor)
135 raw = os.read(descriptor, MAX_AUTHORITY_BYTES + 1)
136 after = os.fstat(descriptor)
137 current = path.lstat()
138 except OSError
as exc:
139 msg = f
"cannot authenticate regular authority {path}"
140 raise GuardError(msg)
from exc
142 if "descriptor" in locals():
145 len(raw) > MAX_AUTHORITY_BYTES
146 or not stat.S_ISREG(before.st_mode)
147 or (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino)
148 or (before.st_dev, before.st_ino) != (current.st_dev, current.st_ino)
150 msg = f
"regular authority {path} is linked, replaced, special, or oversized"
151 raise GuardError(msg)
152 return before, hashlib.sha256(raw).hexdigest()
155def _helper_digest() -> str:
156 helper = Path(__file__).resolve(strict=
True)
157 _guard_stat, guard_digest = _regular_identity(helper)
158 _process_stat, process_digest = _regular_identity(helper.with_name(
"remote_gdb_process.py"))
159 return hashlib.sha256(f
"{guard_digest}:{process_digest}".encode(
"ascii")).hexdigest()
162def _open_private_directory(path: Path, uid: int) -> int:
163 flags = os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
165 descriptor = os.open(path, flags)
166 observed = os.fstat(descriptor)
167 current = path.lstat()
168 except OSError
as exc:
169 msg = f
"unsafe runtime directory {path}"
170 raise GuardError(msg)
from exc
172 not stat.S_ISDIR(observed.st_mode)
173 or observed.st_uid != uid
174 or stat.S_IMODE(observed.st_mode) != DIR_MODE
175 or (observed.st_dev, observed.st_ino) != (current.st_dev, current.st_ino)
178 msg = f
"runtime directory {path} is not owned mode 0700"
179 raise GuardError(msg)
183def _secure_child(parent: Path, name: str, uid: int) -> Path:
184 parent_fd = _open_private_directory(parent, uid)
187 os.mkdir(name, DIR_MODE, dir_fd=parent_fd)
189 except FileExistsError:
193 child = parent / name
194 descriptor = _open_private_directory(child, uid)
199def _home_runtime(uid: int, home: Path |
None) -> Path:
200 account_home = Path(pwd.getpwuid(uid).pw_dir)
if home
is None else home
202 resolved = account_home.resolve(strict=
True)
203 observed = account_home.lstat()
204 except OSError
as exc:
205 msg =
"account home is unavailable"
206 raise GuardError(msg)
from exc
208 account_home != resolved
209 or not stat.S_ISDIR(observed.st_mode)
210 or observed.st_uid != uid
211 or stat.S_IMODE(observed.st_mode) & (stat.S_IWGRP | stat.S_IWOTH)
213 msg =
"account home is linked, foreign, special, or writable by others"
214 raise GuardError(msg)
215 name =
".ra8-runtime"
217 (account_home / name).mkdir(mode=DIR_MODE)
218 home_fd = os.open(account_home, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC)
223 except FileExistsError:
225 return _secure_child(account_home / name,
"remote-gdb", uid)
228def _runtime_directory(
231 uid: int |
None =
None,
232 platform: str = sys.platform,
233 base: Path |
None =
None,
235 owner = os.getuid()
if uid
is None else uid
236 linux_base = Path(
"/run/user") / str(owner)
238 descriptor = _open_private_directory(base, owner)
240 selected = _secure_child(base,
"ra8-remote-gdb", owner)
241 elif platform.startswith(
"linux")
and linux_base.exists():
242 descriptor = _open_private_directory(linux_base, owner)
244 selected = _secure_child(linux_base,
"ra8-remote-gdb", owner)
246 selected = _home_runtime(owner,
None)
247 key = hashlib.sha256(os.fsencode(root)).hexdigest()[:24]
248 return _secure_child(selected, key, owner)
251def _process_value(name: str, *arguments: object) -> object:
253 return _PROCESS[name](*arguments)
254 except ProcessError
as exc:
255 raise GuardError(str(exc))
from exc
258def _proc_start_ticks(proc_root: Path, pid: int) -> int:
259 return int(_process_value(
"start_ticks", proc_root, pid))
262def _proc_uid(proc_root: Path, pid: int) -> int:
263 return int(_process_value(
"process_uid", proc_root, pid))
266def _proc_argv(proc_root: Path, pid: int) -> tuple[str, ...]:
267 return tuple(_process_value(
"process_argv", proc_root, pid))
270def _process_proof(pid: int, request: BrokerRequest) -> ProcessProof:
271 return _process_value(
274 (request.root, request.script, request.port, request.app_arg),
279def _record_keys() -> set[str]:
285 "broker_start_ticks",
287 "parent_start_ticks",
305def _no_duplicate_json(pairs: list[tuple[str, object]]) -> dict[str, object]:
306 result: dict[str, object] = {}
307 for key, value
in pairs:
309 msg = f
"duplicate state field {key}"
310 raise GuardError(msg)
315def _validate_record(value: dict[str, object]) ->
None:
317 set(value) != _record_keys()
318 or type(value[
"version"])
is not int
319 or value[
"version"] != RECORD_VERSION
321 msg =
"state schema or version is invalid"
322 raise GuardError(msg)
326 "broker_start_ticks",
328 "parent_start_ticks",
336 if any(type(value[field])
is not int
or int(value[field]) < 0
for field
in integers):
337 msg =
"state integer field is invalid"
338 raise GuardError(msg)
349 if any(type(value[field])
is not str
for field
in strings):
350 msg =
"state string field is invalid"
351 raise GuardError(msg)
352 argv = value[
"parent_argv"]
353 if not isinstance(argv, list)
or any(type(field)
is not str
for field
in argv):
354 msg =
"state parent argv is invalid"
355 raise GuardError(msg)
357 HEX_RE.fullmatch(str(value[field]))
is None
358 for field
in (
"script_sha256",
"helper_sha256",
"nonce")
360 msg =
"state digest or nonce is invalid"
361 raise GuardError(msg)
362 _validate_port(str(value[
"port"]))
365def _publish_record(state_dir: Path, value: dict[str, object]) -> RecordProof:
366 payload = (json.dumps(value, sort_keys=
True, separators=(
",",
":")) +
"\n").encode(
"ascii")
367 directory = _open_private_directory(state_dir, os.getuid())
368 temporary = f
".{STATE_NAME}.tmp.{os.getpid()}.{secrets.token_hex(8)}"
369 flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
372 descriptor = os.open(temporary, flags, FILE_MODE, dir_fd=directory)
373 os.fchmod(descriptor, FILE_MODE)
374 remaining = memoryview(payload)
376 remaining = remaining[os.write(descriptor, remaining) :]
379 temporary, STATE_NAME, src_dir_fd=directory, dst_dir_fd=directory, follow_symlinks=
False
381 os.unlink(temporary, dir_fd=directory)
383 except FileExistsError
as exc:
384 msg =
"remote-GDB state already exists"
385 raise GuardError(msg)
from exc
389 with contextlib.suppress(FileNotFoundError):
390 os.unlink(temporary, dir_fd=directory)
392 return _read_record(state_dir)
395def _read_record(state_dir: Path) -> RecordProof:
396 directory = _open_private_directory(state_dir, os.getuid())
397 flags = os.O_RDONLY | os.O_NONBLOCK | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
399 descriptor = os.open(STATE_NAME, flags, dir_fd=directory)
400 before = os.fstat(descriptor)
401 raw = os.read(descriptor, MAX_RECORD_BYTES + 1)
402 after = os.fstat(descriptor)
403 current = os.stat(STATE_NAME, dir_fd=directory, follow_symlinks=
False)
404 except FileNotFoundError
as exc:
405 msg =
"remote-GDB state is absent"
406 raise GuardError(msg)
from exc
407 except OSError
as exc:
408 msg =
"remote-GDB state cannot be opened safely"
409 raise GuardError(msg)
from exc
411 if "descriptor" in locals():
415 len(raw) > MAX_RECORD_BYTES
416 or not stat.S_ISREG(before.st_mode)
417 or before.st_uid != os.getuid()
418 or stat.S_IMODE(before.st_mode) != FILE_MODE
419 or (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino)
420 or (before.st_dev, before.st_ino) != (current.st_dev, current.st_ino)
422 msg =
"remote-GDB state is linked, replaced, foreign, special, or oversized"
423 raise GuardError(msg)
425 value = json.loads(raw.decode(
"ascii",
"strict"), object_pairs_hook=_no_duplicate_json)
426 except (UnicodeError, json.JSONDecodeError)
as exc:
427 msg =
"remote-GDB state JSON is malformed"
428 raise GuardError(msg)
from exc
429 if not isinstance(value, dict):
430 msg =
"remote-GDB state is not an object"
431 raise GuardError(msg)
432 _validate_record(value)
433 return RecordProof(value, before, hashlib.sha256(raw).hexdigest())
436def _unlink_exact(state_dir: Path, expected: RecordProof) -> bool:
438 current = _read_record(state_dir)
441 if (current.stat.st_dev, current.stat.st_ino) != (
442 expected.stat.st_dev,
443 expected.stat.st_ino,
444 )
or current.digest != expected.digest:
446 directory = _open_private_directory(state_dir, os.getuid())
448 os.unlink(STATE_NAME, dir_fd=directory)
455def _peer_uid(connection: socket.socket) -> int:
456 if hasattr(connection,
"getpeereid"):
457 uid, _gid = connection.getpeereid()
459 if hasattr(socket,
"SO_PEERCRED"):
460 _pid, uid, _gid = struct.unpack(
461 "3i", connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12)
464 if hasattr(socket,
"LOCAL_PEERCRED"):
465 version, uid = struct.unpack(
467 connection.getsockopt(getattr(socket,
"SOL_LOCAL", 0), socket.LOCAL_PEERCRED, 8),
470 msg =
"Unix peer credential version is invalid"
471 raise GuardError(msg)
473 msg =
"Unix peer credentials are unavailable"
474 raise GuardError(msg)
477def _socket_identity(path: Path) -> os.stat_result:
479 observed = path.lstat()
480 except OSError
as exc:
481 msg =
"remote-GDB control socket is unavailable"
482 raise GuardError(msg)
from exc
484 not stat.S_ISSOCK(observed.st_mode)
485 or observed.st_uid != os.getuid()
486 or stat.S_IMODE(observed.st_mode) != FILE_MODE
488 msg =
"remote-GDB control path is not the owned socket"
489 raise GuardError(msg)
493def _request_bytes(action: str, nonce: str) -> bytes:
494 if action
not in {
"status",
"stop",
"release"}
or HEX_RE.fullmatch(nonce)
is None:
495 msg =
"control request fields are invalid"
496 raise GuardError(msg)
497 value = {
"action": action,
"nonce": nonce,
"version": PROTOCOL_VERSION}
498 return (json.dumps(value, sort_keys=
True, separators=(
",",
":")) +
"\n").encode(
"ascii")
501def _parse_request(raw: bytes, nonce: str) -> str:
502 if not raw.endswith(b
"\n")
or len(raw) > MAX_REQUEST_BYTES:
503 msg =
"control request is incomplete or oversized"
504 raise GuardError(msg)
506 value = json.loads(raw.decode(
"ascii",
"strict"), object_pairs_hook=_no_duplicate_json)
507 except (UnicodeError, json.JSONDecodeError)
as exc:
508 msg =
"control request JSON is malformed"
509 raise GuardError(msg)
from exc
510 if not isinstance(value, dict)
or set(value) != {
"action",
"nonce",
"version"}:
511 msg =
"control request schema is invalid"
512 raise GuardError(msg)
513 if value[
"version"] != PROTOCOL_VERSION
or type(value[
"action"])
is not str:
514 msg =
"control request version or action type is invalid"
515 raise GuardError(msg)
516 supplied = value[
"nonce"]
517 if type(supplied)
is not str
or not hmac.compare_digest(supplied, nonce):
518 msg =
"control request nonce is invalid"
519 raise GuardError(msg)
520 if value[
"action"]
not in {
"status",
"stop",
"release"}:
521 msg =
"control request action is invalid"
522 raise GuardError(msg)
523 return str(value[
"action"])
526def _static_record(request: BrokerRequest, record: RecordProof) ->
None:
528 root_stat = request.root.lstat()
529 script_stat, script_digest = _regular_identity(request.script)
530 helper_digest = _helper_digest()
533 "platform": request.platform,
534 "root": str(request.root),
535 "root_dev": root_stat.st_dev,
536 "root_ino": root_stat.st_ino,
537 "script": str(request.script),
538 "script_dev": script_stat.st_dev,
539 "script_ino": script_stat.st_ino,
540 "script_sha256": script_digest,
541 "helper_sha256": helper_digest,
542 "port": request.port,
544 if any(value[key] != wanted
for key, wanted
in expected.items()):
545 msg =
"remote-GDB state does not match current authorities"
546 raise GuardError(msg)
549def _broker_live(request: BrokerRequest, record: RecordProof) -> bool:
550 pid = int(record.value[
"broker_pid"])
551 if request.platform.startswith(
"linux"):
553 return _proc_uid(request.proc_root, pid) == os.getuid()
and _proc_start_ticks(
554 request.proc_root, pid
555 ) == int(record.value[
"broker_start_ticks"])
558 return _pid_alive(pid)
561def _live_broker_record(request: BrokerRequest, record: RecordProof) ->
None:
562 _static_record(request, record)
563 if not _broker_live(request, record):
564 msg =
"remote-GDB broker identity is stale"
565 raise GuardError(msg)
566 if not request.platform.startswith(
"linux"):
568 pid = int(record.value[
"broker_pid"])
570 executable = (request.proc_root / str(pid) /
"exe").resolve(strict=
True)
571 except OSError
as exc:
572 msg =
"remote-GDB broker executable is unavailable"
573 raise GuardError(msg)
from exc
574 helper = str(Path(__file__).resolve(strict=
True))
585 str(record.value[
"parent_pid"]),
587 str(record.value[
"app_arg"]),
590 executable != Path(
"/usr/bin/python3").resolve(strict=
True)
591 or _proc_argv(request.proc_root, pid) != expected
593 msg =
"remote-GDB broker executable or argv is invalid"
594 raise GuardError(msg)
601 request: BrokerRequest |
None =
None,
602 timeout: float = 2.0,
604 record = _read_record(state_dir)
605 if request
is not None:
606 _live_broker_record(request, record)
607 socket_path = state_dir / SOCKET_NAME
608 socket_stat = _socket_identity(socket_path)
609 if (socket_stat.st_dev, socket_stat.st_ino) != (
610 int(record.value[
"socket_dev"]),
611 int(record.value[
"socket_ino"]),
613 msg =
"control socket identity does not match state"
614 raise GuardError(msg)
615 client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
616 client.settimeout(timeout)
618 client.connect(str(socket_path))
619 client.sendall(_request_bytes(action, str(record.value[
"nonce"])))
620 response = client.recv(16)
621 except OSError
as exc:
622 msg =
"control broker is unavailable"
623 raise GuardError(msg)
from exc
626 if response != b
"OK\n":
627 msg =
"control broker returned an invalid response"
628 raise GuardError(msg)
632def _pid_alive(pid: int) -> bool:
633 return bool(_process_value(
"pid_alive", pid))
636def _pidfd_live(descriptor: int) -> bool:
637 return bool(_process_value(
"pidfd_live", descriptor))
640def _signal_authority(request: BrokerRequest) -> tuple[Callable[[int],
None], int |
None]:
641 raw_signal, descriptor = _process_value(
642 "signal_authority", request.parent_pid, request.platform
645 def checked_signal(pid: int) ->
None:
648 except ProcessError
as exc:
649 raise GuardError(str(exc))
from exc
651 return checked_signal, descriptor
654def _parent_authority(
655 request: BrokerRequest, hooks: BrokerHooks
656) -> tuple[Callable[[int],
None], int |
None, ProcessProof]:
657 signal_parent, pidfd = (
658 _signal_authority(request)
if hooks.signal_parent
is None else (hooks.signal_parent,
None)
662 _process_proof(request.parent_pid, request)
663 if request.platform.startswith(
"linux")
664 else ProcessProof(0, ())
666 except (GuardError, OSError):
667 if pidfd
is not None:
670 if request.platform.startswith(
"linux")
and (
671 pidfd
is None or not _pidfd_live(pidfd)
or hooks.getppid() != request.parent_pid
673 if pidfd
is not None:
675 msg =
"remote-GDB parent changed during pidfd authentication"
676 raise GuardError(msg)
677 return signal_parent, pidfd, parent
681 request: BrokerRequest,
682 socket_stat: os.stat_result,
683 parent: ProcessProof,
684) -> dict[str, object]:
685 root_stat = request.root.stat()
686 script_stat, script_digest = _regular_identity(request.script)
687 helper_digest = _helper_digest()
689 _proc_start_ticks(request.proc_root, os.getpid())
690 if request.platform.startswith(
"linux")
694 "version": RECORD_VERSION,
696 "platform": request.platform,
697 "broker_pid": os.getpid(),
698 "broker_start_ticks": broker_ticks,
699 "parent_pid": request.parent_pid,
700 "parent_start_ticks": parent.start_ticks,
701 "parent_argv": list(parent.argv),
702 "root": str(request.root),
703 "root_dev": root_stat.st_dev,
704 "root_ino": root_stat.st_ino,
705 "script": str(request.script),
706 "script_dev": script_stat.st_dev,
707 "script_ino": script_stat.st_ino,
708 "script_sha256": script_digest,
709 "helper_sha256": helper_digest,
710 "port": request.port,
711 "app_arg": request.app_arg,
712 "socket_dev": socket_stat.st_dev,
713 "socket_ino": socket_stat.st_ino,
714 "nonce": secrets.token_hex(32),
718def _cleanup_socket(state_dir: Path, socket_identity: os.stat_result) ->
None:
719 directory = _open_private_directory(state_dir, os.getuid())
721 with contextlib.suppress(FileNotFoundError):
722 current = os.stat(SOCKET_NAME, dir_fd=directory, follow_symlinks=
False)
723 if (current.st_dev, current.st_ino) == (
724 socket_identity.st_dev,
725 socket_identity.st_ino,
727 os.unlink(SOCKET_NAME, dir_fd=directory)
733def _recv_request(connection: socket.socket) -> bytes:
735 while b
"\n" not in raw
and len(raw) <= MAX_REQUEST_BYTES:
736 chunk = connection.recv(
min(256, MAX_REQUEST_BYTES + 1 - len(raw)))
740 if raw.count(b
"\n") != 1
or not raw.endswith(b
"\n"):
741 msg =
"control request has trailing, incomplete, or excessive data"
742 raise GuardError(msg)
746def _serve_connection(
747 connection: socket.socket,
749 request: BrokerRequest,
750 signal_parent: Callable[[int],
None],
751 getppid: Callable[[], int],
753 connection.settimeout(2.0)
754 if _peer_uid(connection) != os.getuid():
755 msg =
"control peer has the wrong UID"
756 raise GuardError(msg)
757 raw = _recv_request(connection)
758 action = _parse_request(raw, nonce)
759 if action ==
"status":
760 connection.sendall(b
"OK\n")
762 if getppid() != request.parent_pid:
763 msg =
"broker parent died before control action"
764 raise GuardError(msg)
766 signal_parent(request.parent_pid)
767 connection.sendall(b
"OK\n")
771def _state_dir(request: BrokerRequest) -> Path:
772 return _runtime_directory(
774 platform=request.platform,
775 base=request.runtime_base,
779def _prepare_state(request: BrokerRequest, hooks: BrokerHooks) -> Path:
780 state_dir = _state_dir(request)
781 socket_path = state_dir / SOCKET_NAME
783 record = _read_record(state_dir)
784 except GuardError
as exc:
785 if str(exc) !=
"remote-GDB state is absent":
787 if socket_path.exists()
or socket_path.is_symlink():
788 msg =
"control socket exists without authenticated state"
789 raise GuardError(msg)
from exc
791 _static_record(request, record)
793 hooks.pid_alive(int(record.value[
"broker_pid"]))
794 if hooks.pid_alive
is not None and not request.platform.startswith(
"linux")
795 else _broker_live(request, record)
798 msg =
"an authenticated remote-GDB broker is already live"
799 raise GuardError(msg)
800 if socket_path.exists()
or socket_path.is_symlink():
801 socket_stat = _socket_identity(socket_path)
802 if (socket_stat.st_dev, socket_stat.st_ino) != (
803 int(record.value[
"socket_dev"]),
804 int(record.value[
"socket_ino"]),
806 msg =
"stale control socket identity is ambiguous"
807 raise GuardError(msg)
808 _cleanup_socket(state_dir, socket_stat)
809 if not _unlink_exact(state_dir, record):
810 msg =
"stale remote-GDB state changed during cleanup"
811 raise GuardError(msg)
815def _run_broker(request: BrokerRequest, hooks: BrokerHooks |
None =
None) ->
None:
816 hooks = BrokerHooks()
if hooks
is None else hooks
817 if hooks.getppid() != request.parent_pid:
818 msg =
"broker caller is not its direct parent"
819 raise GuardError(msg)
820 state_dir = _prepare_state(request, hooks)
821 socket_path = state_dir / SOCKET_NAME
822 record: RecordProof |
None =
None
823 socket_stat: os.stat_result |
None =
None
824 signal_parent, pidfd, parent = _parent_authority(request, hooks)
825 server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
827 server.bind(str(socket_path))
828 socket_path.chmod(FILE_MODE)
829 socket_stat = _socket_identity(socket_path)
831 server.settimeout(0.25)
832 record = _publish_record(state_dir, _broker_record(request, socket_stat, parent))
834 while keep_running
and hooks.getppid() == request.parent_pid:
836 connection, _address = server.accept()
837 except ACCEPT_TIMEOUT:
841 keep_running = _serve_connection(
843 str(record.value[
"nonce"]),
848 except (GuardError, OSError):
849 with contextlib.suppress(OSError):
850 connection.sendall(b
"ERR\n")
853 if record
is not None:
854 _unlink_exact(state_dir, record)
855 if socket_stat
is not None:
856 _cleanup_socket(state_dir, socket_stat)
857 if pidfd
is not None:
861def _status_broker(state_dir: Path, request: BrokerRequest) -> RecordProof |
None:
863 return _request_broker(state_dir,
"status", request=request, timeout=0.25)
868def _await_broker(request: BrokerRequest, broker_pid: int, timeout: float = 5.0) ->
None:
869 state_dir = _state_dir(request)
870 deadline = time.monotonic() + timeout
871 while time.monotonic() < deadline:
872 record = _status_broker(state_dir, request)
873 if record
is not None and int(record.value[
"broker_pid"]) == broker_pid:
876 msg =
"remote-GDB broker did not become ready"
877 raise GuardError(msg)
880def _control_broker(request: BrokerRequest, action: str) ->
None:
881 state_dir = _state_dir(request)
883 _request_broker(state_dir, action, request=request)
884 except GuardError
as exc:
885 if str(exc) ==
"remote-GDB state is absent":
890def _request_from_args(args: argparse.Namespace) -> BrokerRequest:
891 root, script = _canonical_authorities(args.root)
892 _validate_port(args.port)
893 app_arg = getattr(args,
"app_arg",
"")
895 raw = app_arg.encode(
"ascii",
"strict")
896 except UnicodeEncodeError
as exc:
897 msg =
"application argument must be ASCII"
898 raise GuardError(msg)
from exc
900 len(raw) > MAX_APP_ARG_BYTES
or any(byte < ASCII_MIN
or byte > ASCII_MAX
for byte
in raw)
902 msg =
"application argument is unsafe or oversized"
903 raise GuardError(msg)
904 parent_pid = getattr(args,
"parent_pid", 0)
905 if parent_pid < 0
or (args.command
in {
"broker",
"await"}
and parent_pid <= 1):
906 msg =
"parent PID is invalid"
907 raise GuardError(msg)
908 return BrokerRequest(root, script, args.port, app_arg, parent_pid)
911def _parser() -> argparse.ArgumentParser:
912 parser = argparse.ArgumentParser(description=__doc__)
913 commands = parser.add_subparsers(dest=
"command", required=
True)
914 for name
in (
"broker",
"await",
"stop",
"release"):
915 command = commands.add_parser(name)
916 command.add_argument(
"--root", required=
True)
917 command.add_argument(
"--port", required=
True)
918 if name
in {
"broker",
"await"}:
919 command.add_argument(
"--parent-pid", required=
True, type=int)
921 command.add_argument(
"--app-arg", default=
"")
923 command.add_argument(
"--broker-pid", required=
True, type=int)
928 """Dispatch one broker operation."""
929 args = _parser().parse_args()
931 request = _request_from_args(args)
932 if args.command ==
"broker":
934 elif args.command ==
"await":
935 _await_broker(request, args.broker_pid)
937 _control_broker(request, args.command)
938 except GuardError
as exc:
939 print(f
"remote_gdb_guard: {exc}", file=sys.stderr)
944if __name__ ==
"__main__":
void main(void)
The application entry point Reset_Handler hands control to.
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.