3"""Linux process-identity proofs for the remote-GDB state broker."""
5from __future__
import annotations
12from dataclasses
import dataclass
13from pathlib
import Path
15MAX_AUTHORITY_BYTES = 256 * 1024
23class ProcessError(ValueError):
24 """A Linux process claim could not be bound to one live identity."""
27@dataclass(frozen=
True)
29 """Authenticated identity of the live remote-GDB Bash process."""
35def pid_alive(pid: int) -> bool:
36 """Return process existence without sending a state-changing signal."""
39 except ProcessLookupError:
41 except PermissionError:
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)
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"):
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
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)
68 return signal_pidfd, descriptor
70 def signal_parent(pid: int) ->
None:
71 os.kill(pid, signal.SIGTERM)
73 return signal_parent,
None
76def start_ticks(proc_root: Path, pid: int) -> int:
77 """Read Linux start ticks without splitting the comm field."""
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
86def process_uid(proc_root: Path, pid: int) -> int:
87 """Read the effective process owner from procfs."""
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
97def process_argv(proc_root: Path, pid: int) -> tuple[str, ...]:
98 """Read one strict NUL-delimited procfs argv."""
100 fields = (proc_root / str(pid) /
"cmdline").read_bytes().split(b
"\0")
101 if fields
and not fields[-1]:
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
109def _regular_identity(path: Path) -> os.stat_result:
110 flags = os.O_RDONLY | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
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
121 if "descriptor" in locals():
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)
129 message =
"canonical remote-GDB script is linked, replaced, or special"
130 raise ProcessError(message)
134def _script_open(proc_root: Path, pid: int, identity: os.stat_result) -> bool:
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):
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)
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)
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
174 Path(argv[script_arg])
if Path(argv[script_arg]).is_absolute()
else root / argv[script_arg]
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)
196 claim: tuple[Path, Path, str, str],
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))