test(827): capture Gate0 runtime evidence
This commit is contained in:
180
docs/compaction-refresh/probes/p1_broker.py
Normal file
180
docs/compaction-refresh/probes/p1_broker.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user