ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
remote_gdb_process.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Linux process-identity proofs for the remote-GDB state broker."""
4
5from __future__ import annotations
6
7import contextlib
8import os
9import select
10import signal
11import stat
12from dataclasses import dataclass
13from pathlib import Path
14
15MAX_AUTHORITY_BYTES = 256 * 1024
16RUN_ARGS_MIN = 3
17RUN_ARGS_MAX = 6
18RUN_TAIL_MAX = 3
19PORT_ARG_COUNT = 2
20SCRIPT_ARG = 2
21
22
23class ProcessError(ValueError):
24 """A Linux process claim could not be bound to one live identity."""
25
26
27@dataclass(frozen=True)
28class ProcessProof:
29 """Authenticated identity of the live remote-GDB Bash process."""
30
31 start_ticks: int
32 argv: tuple[str, ...]
33
34
35def pid_alive(pid: int) -> bool:
36 """Return process existence without sending a state-changing signal."""
37 try:
38 os.kill(pid, 0)
39 except ProcessLookupError:
40 return False
41 except PermissionError:
42 return True
43 return True
44
45
46def pidfd_live(descriptor: int) -> bool:
47 """Require the retained Linux pidfd target to remain alive."""
48 poller = select.poll()
49 poller.register(descriptor, select.POLLIN | select.POLLHUP | select.POLLERR)
50 return not poller.poll(0)
51
52
53def signal_authority(parent_pid: int, platform: str) -> tuple[object, int | None]:
54 """Acquire the strongest stdlib parent-signal capability for the platform."""
55 if platform.startswith("linux"):
56 try:
57 descriptor = os.pidfd_open(parent_pid, 0)
58 except (AttributeError, OSError) as exc:
59 message = "Linux pidfd authority is unavailable"
60 raise ProcessError(message) from exc
61
62 def signal_pidfd(_pid: int) -> None:
63 if not pidfd_live(descriptor):
64 message = "remote-GDB parent exited before stop"
65 raise ProcessError(message)
66 signal.pidfd_send_signal(descriptor, signal.SIGTERM, None, 0)
67
68 return signal_pidfd, descriptor
69
70 def signal_parent(pid: int) -> None:
71 os.kill(pid, signal.SIGTERM)
72
73 return signal_parent, None
74
75
76def start_ticks(proc_root: Path, pid: int) -> int:
77 """Read Linux start ticks without splitting the comm field."""
78 try:
79 raw = (proc_root / str(pid) / "stat").read_text(encoding="ascii")
80 return int(raw[raw.rindex(")") + 2 :].split()[19])
81 except (OSError, ValueError, IndexError) as exc:
82 message = "cannot authenticate process start time"
83 raise ProcessError(message) from exc
84
85
86def process_uid(proc_root: Path, pid: int) -> int:
87 """Read the effective process owner from procfs."""
88 try:
89 lines = (proc_root / str(pid) / "status").read_text(encoding="ascii").splitlines()
90 uid_line = next(line for line in lines if line.startswith("Uid:"))
91 return int(uid_line.split()[1])
92 except (OSError, ValueError, IndexError, StopIteration) as exc:
93 message = "cannot authenticate process owner"
94 raise ProcessError(message) from exc
95
96
97def process_argv(proc_root: Path, pid: int) -> tuple[str, ...]:
98 """Read one strict NUL-delimited procfs argv."""
99 try:
100 fields = (proc_root / str(pid) / "cmdline").read_bytes().split(b"\0")
101 if fields and not fields[-1]:
102 fields.pop()
103 return tuple(field.decode("utf-8", "strict") for field in fields)
104 except (OSError, UnicodeError) as exc:
105 message = "cannot authenticate process argv"
106 raise ProcessError(message) from exc
107
108
109def _regular_identity(path: Path) -> os.stat_result:
110 flags = os.O_RDONLY | os.O_CLOEXEC | getattr(os, "O_NOFOLLOW", 0)
111 try:
112 descriptor = os.open(path, flags)
113 before = os.fstat(descriptor)
114 raw = os.read(descriptor, MAX_AUTHORITY_BYTES + 1)
115 after = os.fstat(descriptor)
116 current = path.lstat()
117 except OSError as exc:
118 message = "cannot authenticate canonical remote-GDB script"
119 raise ProcessError(message) from exc
120 finally:
121 if "descriptor" in locals():
122 os.close(descriptor)
123 if (
124 len(raw) > MAX_AUTHORITY_BYTES
125 or not stat.S_ISREG(before.st_mode)
126 or (before.st_dev, before.st_ino) != (after.st_dev, after.st_ino)
127 or (before.st_dev, before.st_ino) != (current.st_dev, current.st_ino)
128 ):
129 message = "canonical remote-GDB script is linked, replaced, or special"
130 raise ProcessError(message)
131 return before
132
133
134def _script_open(proc_root: Path, pid: int, identity: os.stat_result) -> bool:
135 try:
136 entries = tuple((proc_root / str(pid) / "fd").iterdir())
137 except OSError as exc:
138 message = "cannot authenticate process script descriptor"
139 raise ProcessError(message) from exc
140 for entry in entries:
141 with contextlib.suppress(OSError):
142 observed = entry.stat()
143 if (observed.st_dev, observed.st_ino) == (identity.st_dev, identity.st_ino):
144 return True
145 return False
146
147
148def _parent_paths(pid: int, root: Path, proc_root: Path) -> None:
149 if process_uid(proc_root, pid) != os.getuid():
150 message = "remote-GDB parent owner is invalid"
151 raise ProcessError(message)
152 try:
153 executable = (proc_root / str(pid) / "exe").resolve(strict=True)
154 cwd = (proc_root / str(pid) / "cwd").resolve(strict=True)
155 except OSError as exc:
156 message = "cannot authenticate parent executable or cwd"
157 raise ProcessError(message) from exc
158 if executable != Path("/bin/bash").resolve(strict=True) or cwd != root:
159 message = "remote-GDB parent executable or workspace is wrong"
160 raise ProcessError(message)
161
162
163def _parent_argv(pid: int, claim: tuple[Path, Path, str, str], proc_root: Path) -> tuple[str, ...]:
164 root, script, port, app_arg = claim
165 argv = process_argv(proc_root, pid)
166 if not RUN_ARGS_MIN <= len(argv) <= RUN_ARGS_MAX + 1 or argv[1] != "-p":
167 message = "remote-GDB parent argv is not privileged Bash"
168 raise ProcessError(message)
169 if argv[SCRIPT_ARG] == "--" and len(argv) == RUN_ARGS_MIN:
170 message = "remote-GDB parent argv omits its script"
171 raise ProcessError(message)
172 script_arg = SCRIPT_ARG + 1 if argv[SCRIPT_ARG] == "--" else SCRIPT_ARG
173 invoked = (
174 Path(argv[script_arg]) if Path(argv[script_arg]).is_absolute() else root / argv[script_arg]
175 )
176 if invoked.resolve(strict=True) != script:
177 message = "remote-GDB parent argv names another script"
178 raise ProcessError(message)
179 tail = argv[script_arg + 1 :]
180 if (tail and tail[0] != "run") or len(tail) > RUN_TAIL_MAX:
181 message = "remote-GDB parent action or argv count is invalid"
182 raise ProcessError(message)
183 actual_port = tail[1] if len(tail) >= PORT_ARG_COUNT else "2331"
184 actual_app = tail[2] if len(tail) == RUN_TAIL_MAX else ""
185 if actual_port != port or actual_app != app_arg:
186 message = "remote-GDB parent argv does not match requested state"
187 raise ProcessError(message)
188 if not _script_open(proc_root, pid, _regular_identity(script)):
189 message = "remote-GDB parent has no canonical script descriptor"
190 raise ProcessError(message)
191 return argv
192
193
194def parent_proof(
195 pid: int,
196 claim: tuple[Path, Path, str, str],
197 proc_root: Path,
198) -> ProcessProof:
199 """Bind Bash, cwd, argv, open script, and start time after pidfd acquisition."""
200 _parent_paths(pid, claim[0], proc_root)
201 return ProcessProof(start_ticks(proc_root, pid), _parent_argv(pid, claim, proc_root))