#!/usr/bin/env python3 """Gate0 P1 broker prototype: peercred anchor minting and /proc ancestry checks.""" from __future__ import annotations import argparse import json import os import secrets import socket import stat import struct import sys from pathlib import Path from typing import Any def proc_node(pid: int) -> dict[str, Any]: text = Path(f"/proc/{pid}/stat").read_text() close = text.rfind(")") comm = text[text.find("(") + 1 : close] fields = text[close + 2 :].split() cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().split(b"\0") return { "pid": pid, "ppid": int(fields[1]), "starttime_ticks": int(fields[19]), "comm": comm, "exe": os.readlink(f"/proc/{pid}/exe"), "argv0": cmdline[0].decode(errors="replace") if cmdline and cmdline[0] else "", "argc": len([part for part in cmdline if part]), } def ancestry(peer_pid: int, anchor: dict[str, Any] | None) -> tuple[list[dict[str, Any]], bool, str]: chain: list[dict[str, Any]] = [] pid = peer_pid seen: set[int] = set() try: while pid > 0 and pid not in seen: seen.add(pid) node = proc_node(pid) chain.append(node) if anchor and pid == anchor["pid"]: if node["starttime_ticks"] != anchor["starttime_ticks"]: return chain, False, "anchor-starttime-mismatch" break pid = node["ppid"] else: return chain, False, "anchor-not-reached" if not anchor or chain[-1]["pid"] != anchor["pid"]: return chain, False, "anchor-not-reached" # Re-read every node after the walk. A disappearing PID or changed # starttime invalidates the complete chain (PID-reuse/race closure). for original in chain: again = proc_node(original["pid"]) if again["starttime_ticks"] != original["starttime_ticks"]: return chain, False, f"starttime-race:{original['pid']}" return chain, True, "ancestry-reaches-registered-anchor" except (FileNotFoundError, ProcessLookupError, PermissionError) as exc: return chain, False, f"proc-walk-failed:{type(exc).__name__}" def emit(log_file: Path, record: dict[str, Any]) -> None: line = json.dumps(record, sort_keys=True) with log_file.open("a", encoding="utf-8") as out: out.write(line + "\n") print(line, flush=True) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--socket", required=True) parser.add_argument("--log", required=True) parser.add_argument("--state", required=True) args = parser.parse_args() socket_path = Path(args.socket) log_file = Path(args.log) state_file = Path(args.state) socket_path.parent.mkdir(parents=True, exist_ok=True) os.chmod(socket_path.parent, 0o700) socket_path.unlink(missing_ok=True) log_file.unlink(missing_ok=True) state_file.unlink(missing_ok=True) server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) server.bind(str(socket_path)) os.chmod(socket_path, 0o600) server.listen(8) anchor: dict[str, Any] | None = None session_id: str | None = None emit( log_file, { "event": "broker-listen", "pid": os.getpid(), "socket": str(socket_path), "directory_mode": f"{stat.S_IMODE(socket_path.parent.stat().st_mode):04o}", "socket_mode": f"{stat.S_IMODE(socket_path.stat().st_mode):04o}", }, ) while True: conn, _ = server.accept() with conn: raw = conn.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12) peer_pid, peer_uid, peer_gid = struct.unpack("3i", raw) request = json.loads(conn.makefile("r", encoding="utf-8").readline()) action = request.get("action") if action == "register-anchor" and anchor is None: anchor = proc_node(peer_pid) session_id = secrets.token_hex(16) state = {"session_id": session_id, "anchor": anchor} state_file.write_text(json.dumps(state, sort_keys=True) + "\n") record = { "event": "anchor-minted", "decision": "ACCEPT", "peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid}, "anchor": anchor, "session_id": session_id, } emit(log_file, record) conn.sendall((json.dumps(record) + "\n").encode()) continue if action in {"resolve-hook", "claim-session"}: chain, reaches, reason = ancestry(peer_pid, anchor) claimed = request.get("session_id") claim_ok = action == "resolve-hook" or claimed == session_id accepted = bool(anchor and session_id and reaches and claim_ok) if action == "claim-session" and claimed != session_id: reason = "unknown-session-id" elif action == "claim-session" and claimed == session_id and not reaches: reason = "victim-id-known-but-ancestry-mismatch" record = { "event": action, "decision": "ACCEPT" if accepted else "REJECT", "reason": reason, "peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid}, "claimed_session_id": claimed, "resolved_session_id": session_id if accepted else None, "anchor": anchor, "ancestry": chain, "starttimes_rechecked": reaches, } emit(log_file, record) conn.sendall((json.dumps(record) + "\n").encode()) continue if action == "shutdown": record = { "event": "broker-shutdown", "peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid}, } emit(log_file, record) conn.sendall((json.dumps(record) + "\n").encode()) break record = { "event": "invalid-request", "decision": "REJECT", "peercred": {"pid": peer_pid, "uid": peer_uid, "gid": peer_gid}, } emit(log_file, record) conn.sendall((json.dumps(record) + "\n").encode()) server.close() socket_path.unlink(missing_ok=True) if __name__ == "__main__": try: main() except Exception as exc: print(f"P1 broker fatal: {type(exc).__name__}: {exc}", file=sys.stderr) raise