feat: securely prepare verification roots

This commit is contained in:
2026-10-08 05:29:59 +00:00
parent 6ae993887c
commit 6c27a9e07d
8 changed files with 644 additions and 13 deletions
+4 -2
View File
@@ -191,7 +191,9 @@ class Broker:
self.token = secrets.token_hex(32)
self.deadline = time.monotonic() + args.ttl_seconds
self.set = ManagedLockSet.for_issue(self.root, args.owner, args.repository, args.issue, timeout=args.acquire_timeout)
self.meta = {"owner": args.owner, "repository": args.repository, "issue": args.issue, "role": args.role, "run_id": args.run_id, "pid": os.getpid(), "deadline": self.deadline}
self.meta = {"root": str(self.root), "owner": args.owner, "repository": args.repository,
"issue": args.issue, "role": args.role, "run_id": args.run_id,
"pid": os.getpid(), "deadline": self.deadline}
self.state = {**self.meta, "socket": str(self.sock_path), "status": "held", "token": self.token}
self.server: socket.socket | None = None
self.socket_identity: tuple[int, int] | None = None
@@ -274,7 +276,7 @@ class Broker:
action = req.get("action")
if action == "status":
return {"status": self.state["status"], "deadline": self.deadline,
"repository_held": self.set.repository.held,
"repository_held": self.set.repository.held, "root": self.state["root"],
"owner": self.state["owner"], "repository": self.state["repository"],
"issue": self.state["issue"], "role": self.state["role"],
"pid": self.state["pid"]}
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Prepare canonical, private verification roots for an Issue run."""
from __future__ import annotations
import argparse
import json
import math
import os
import re
import stat
import sys
import time
from pathlib import Path
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from harness.lib import managed_lock_broker as broker
class VerificationError(ValueError):
"""The lease, identifiers, or cache layout is not safe to use."""
_PRIVATE_MODE = 0o700
_OWNER_REPOSITORY = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,254}")
_RUN_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]{0,127}")
_OPEN_DIR_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
def _identifier(value: object, label: str, pattern: re.Pattern[str]) -> str:
if not isinstance(value, str) or not pattern.fullmatch(value) or value in {".", ".."}:
raise VerificationError(f"invalid {label}")
return value
def _issue_number(value: object) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
raise VerificationError("issue must be positive")
return value
def _absolute(value: os.PathLike[str] | str, label: str) -> Path:
raw = os.fspath(value)
path = Path(raw)
if not isinstance(raw, str) or not path.is_absolute() or "\x00" in raw:
raise VerificationError(f"{label} must be an absolute path")
if any(part in {"", ".", ".."} for part in raw.split("/")[1:]):
raise VerificationError(f"invalid {label}")
return path
def _open_absolute_directory(path: Path, label: str) -> int:
"""Open every component without following symlinks."""
parts = path.parts
if not parts or parts[0] != "/":
raise VerificationError(f"invalid {label}")
try:
fd = os.open(parts[0], _OPEN_DIR_FLAGS)
for part in parts[1:]:
nxt = os.open(part, _OPEN_DIR_FLAGS, dir_fd=fd)
os.close(fd)
fd = nxt
return fd
except OSError as exc:
try:
os.close(fd)
except (OSError, UnboundLocalError):
pass
raise VerificationError(f"unsafe {label}") from exc
def _require_private_fd(fd: int, label: str) -> None:
st = os.fstat(fd)
if (
not stat.S_ISDIR(st.st_mode)
or st.st_uid != os.getuid()
or stat.S_IMODE(st.st_mode) != _PRIVATE_MODE
):
raise VerificationError(f"unsafe {label}")
def _open_private_child(parent_fd: int, name: str, label: str) -> int:
try:
fd = os.open(name, _OPEN_DIR_FLAGS, dir_fd=parent_fd)
except OSError as exc:
raise VerificationError(f"unsafe {label}") from exc
try:
_require_private_fd(fd, label)
return fd
except BaseException:
os.close(fd)
raise
def _same_inode(left: os.stat_result, right: os.stat_result) -> bool:
return (left.st_dev, left.st_ino) == (right.st_dev, right.st_ino)
def _remove_created_directory(parent_fd: int, name: str, created: os.stat_result) -> None:
"""Remove only the still-empty directory inode created by this call."""
try:
current = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
if not _same_inode(current, created):
return
os.rmdir(name, dir_fd=parent_fd)
except OSError:
pass
def _create_private_child(parent_fd: int, name: str, label: str) -> int:
try:
os.mkdir(name, _PRIVATE_MODE, dir_fd=parent_fd)
created = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
except FileExistsError as exc:
raise VerificationError(f"existing {label}") from exc
except OSError as exc:
raise VerificationError(f"cannot create {label}") from exc
try:
if not stat.S_ISDIR(created.st_mode):
raise VerificationError(f"changed {label}")
fd = _open_private_child(parent_fd, name, label)
# Bind both the opened descriptor and the current name to the inode
# observed immediately after mkdir. A cooperating same-UID process is
# excluded by the authenticated Issue lease; replacement is fail-closed.
named = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
opened = os.fstat(fd)
if not (_same_inode(created, opened) and _same_inode(created, named)):
os.close(fd)
raise VerificationError(f"changed {label}")
return fd
except BaseException:
# Never chmod, replace, or remove an entry substituted after creation.
_remove_created_directory(parent_fd, name, created)
raise
def _lease(handle: Path, root: Path, owner: str, repository: str, issue: int) -> None:
try:
metadata = broker.read_handle_metadata(handle)
response = broker.request_handle(handle, "status")
except Exception as exc:
raise VerificationError("broker lease unavailable") from exc
if not isinstance(metadata, dict) or not isinstance(response, dict):
raise VerificationError("invalid broker response")
if response.get("error") or response.get("status") not in {"held", "repository-held"}:
raise VerificationError("broker lease not held")
deadline = response.get("deadline")
if (
isinstance(deadline, bool)
or not isinstance(deadline, (int, float))
or not math.isfinite(deadline)
or deadline <= time.monotonic()
):
raise VerificationError("broker lease expired")
for source in (metadata, response):
if (
source.get("root") != str(root)
or source.get("owner") != owner
or source.get("repository") != repository
or source.get("issue") != issue
):
raise VerificationError("broker lease binding mismatch")
if metadata.get("role") != response.get("role") or response.get("role") not in broker.ROLES:
raise VerificationError("broker lease role mismatch")
def prepare(
root: os.PathLike[str] | str,
owner: str,
repository: str,
issue: int,
run_id: str,
handle: os.PathLike[str] | str,
) -> Path:
"""Authenticate a lease and no-clobber-create one private run directory."""
owner = _identifier(owner, "owner", _OWNER_REPOSITORY)
repository = _identifier(repository, "repository", _OWNER_REPOSITORY)
run_id = _identifier(run_id, "run_id", _RUN_ID)
issue = _issue_number(issue)
root = _absolute(root, "root")
handle = _absolute(handle, "handle")
_lease(handle, root, owner, repository, issue)
issue_path = root / "repositories" / owner / repository / "worktrees" / f"issue-{issue}"
issue_fd = _open_absolute_directory(issue_path, "Issue directory")
verification_fd = None
run_fd = None
created_verification_inode = None
try:
_require_private_fd(issue_fd, "Issue directory")
try:
verification_fd = _open_private_child(issue_fd, "verification", "verification directory")
except VerificationError as exc:
try:
os.stat("verification", dir_fd=issue_fd, follow_symlinks=False)
except FileNotFoundError:
verification_fd = _create_private_child(issue_fd, "verification", "verification directory")
created_verification_inode = os.fstat(verification_fd)
else:
raise exc
try:
os.stat(run_id, dir_fd=verification_fd, follow_symlinks=False)
except FileNotFoundError:
pass
except OSError as exc:
raise VerificationError("cannot inspect run directory") from exc
else:
raise VerificationError("existing run directory")
try:
run_fd = _create_private_child(verification_fd, run_id, "run directory")
except BaseException:
if created_verification_inode is not None:
_remove_created_directory(
issue_fd, "verification", created_verification_inode
)
raise
return issue_path / "verification" / run_id
finally:
for fd in (run_fd, verification_fd, issue_fd):
if fd is not None:
try:
os.close(fd)
except OSError:
pass
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="verification_root")
sub = parser.add_subparsers(dest="command", required=True)
command = sub.add_parser("prepare")
command.add_argument("--root", required=True)
command.add_argument("--owner", required=True)
command.add_argument("--repository", required=True)
command.add_argument("--issue", required=True, type=int)
command.add_argument("--run-id", required=True)
command.add_argument("--handle", required=True)
return parser
def main(argv: list[str] | None = None) -> int:
args = _parser().parse_args(argv)
try:
path = prepare(args.root, args.owner, args.repository, args.issue, args.run_id, args.handle)
print(json.dumps({"path": str(path), "verification_mode": "0700", "run_mode": "0700"}, separators=(",", ":")))
return 0
except (VerificationError, OSError, ValueError) as exc:
print(json.dumps({"error": str(exc)[:200]}, separators=(",", ":")), file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())