ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
workspace_guard.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Secure lock and metadata boundary for the workspace lifecycle shell."""
5
6from __future__ import annotations
7
8import fcntl
9import os
10import signal
11import stat
12import subprocess
13import sys
14from pathlib import Path
15from typing import NoReturn
16
17SAFE_FILE_MODE_MASK = stat.S_IWGRP | stat.S_IWOTH
18EXPECTED_METADATA_FIELDS = {
19 "schema",
20 "name",
21 "created",
22 "by",
23 "ref",
24 "base_commit",
25 "path",
26 "branch",
27 "owner",
28}
29LOCK_ARGC_MIN = 5
30METADATA_ARGC = 5
31INHERIT_DESCRIPTOR = True
32
33
34class GuardError(Exception):
35 """One fail-closed workspace boundary violation."""
36
37
38def fail(message: str, cause: BaseException | None = None) -> NoReturn:
39 """Raise one consistently constructed boundary error."""
40 error = GuardError(message)
41 if cause is not None:
42 raise error from cause
43 raise error
44
45
46def lexical(path: str) -> Path:
47 """Return an absolute normalized path without following symlinks."""
48 if not path or not path.isascii() or not path.isprintable():
49 fail("path must be printable single-line ASCII")
50 expanded = Path(path).expanduser()
51 return Path(os.path.abspath(expanded)) # noqa: PTH100 -- resolve would follow symlinks
52
53
54def _components(path: Path) -> list[Path]:
55 """Return every absolute component from slash through ``path``."""
56 parts = path.parts
57 current = Path(parts[0])
58 result = [current]
59 for part in parts[1:]:
60 current /= part
61 result.append(current)
62 return result
63
64
65def reject_symlink_components(path: Path) -> None:
66 """Reject every existing symlink component in an absolute path."""
67 for component in _components(path):
68 try:
69 info = component.lstat()
70 except FileNotFoundError:
71 return
72 if stat.S_ISLNK(info.st_mode):
73 fail(f"symlink path component is forbidden: {component}")
74
75
76def _contains(parent: Path, child: Path) -> bool:
77 """Return whether ``parent`` is equal to or lexically contains ``child``."""
78 try:
79 child.relative_to(parent)
80 except ValueError:
81 return False
82 return True
83
84
85def validate_root(raw_root: str, raw_upstream: str) -> tuple[Path, Path]:
86 """Validate or safely create the narrowly scoped workspace root."""
87 root = lexical(raw_root)
88 upstream = lexical(raw_upstream)
89 home = lexical(str(Path.home()))
90 forbidden = (Path("/"), home, upstream)
91 if any(root == item for item in forbidden):
92 fail(f"workspace root is a forbidden broad path: {root}")
93 if _contains(root, home) or _contains(root, upstream):
94 fail(f"workspace root may not be an ancestor of HOME or the repository: {root}")
95 reject_symlink_components(root)
96 reject_symlink_components(upstream)
97 if not upstream.is_dir():
98 fail(f"upstream repository is not a directory: {upstream}")
99 if not root.exists():
100 parent = root.parent
101 if not parent.is_dir():
102 fail(f"workspace root parent does not exist: {parent}")
103 root.mkdir(mode=0o700)
104 info = root.lstat()
105 if not stat.S_ISDIR(info.st_mode) or stat.S_ISLNK(info.st_mode):
106 fail(f"workspace root is not a real directory: {root}")
107 if info.st_uid != os.geteuid():
108 fail(f"workspace root has foreign ownership: {root}")
109 if info.st_mode & SAFE_FILE_MODE_MASK:
110 fail(f"workspace root is group/world writable: {root}")
111 return root, upstream
112
113
114def open_lock(root: Path) -> int:
115 """Open and validate the non-following, non-truncating lifecycle lock."""
116 flags = os.O_CREAT | os.O_RDWR | os.O_CLOEXEC
117 flags |= getattr(os, "O_NOFOLLOW", 0)
118 path = root / ".workspace.lock"
119 try:
120 descriptor = os.open(path, flags, 0o600)
121 except OSError as exc:
122 fail(f"workspace lock could not be opened safely: {exc}", exc)
123 try:
124 info = os.fstat(descriptor)
125 checks = (
126 stat.S_ISREG(info.st_mode),
127 info.st_uid == os.geteuid(),
128 info.st_nlink == 1,
129 not bool(info.st_mode & SAFE_FILE_MODE_MASK),
130 )
131 if not all(checks):
132 fail("workspace lock must be a private, singly linked regular file")
133 fcntl.flock(descriptor, fcntl.LOCK_EX)
134 os.set_inheritable(descriptor, INHERIT_DESCRIPTOR)
135 except BaseException:
136 os.close(descriptor)
137 raise
138 else:
139 return descriptor
140
141
142def run_locked(root: str, upstream: str, script: str, argv: list[str]) -> int:
143 """Hold the validated lock while one shell lifecycle transaction runs."""
144 validated_root, validated_upstream = validate_root(root, upstream)
145 descriptor = open_lock(validated_root)
146 environment = os.environ.copy()
147 environment.update(
148 {
149 "RA8_WS_ROOT": str(validated_root),
150 "RA8_WS_UPSTREAM": str(validated_upstream),
151 "RA8_WS_LOCKED": "1",
152 "RA8_WS_LOCK_FD": str(descriptor),
153 }
154 )
155 bash = Path("/bin/bash")
156 if not bash.is_file() or not os.access(bash, os.X_OK):
157 fail("/bin/bash is required for workspace lifecycle transactions")
158 child = subprocess.Popen( # noqa: S603 -- resolved Bash, fixed script argv, no shell
159 [str(bash), "-p", script, *argv],
160 env=environment,
161 pass_fds=(descriptor,),
162 )
163
164 def forward(signum: int, _frame: object) -> None:
165 """Forward termination to the transaction that owns the lock."""
166 if child.poll() is None:
167 child.send_signal(signum)
168
169 signals = (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)
170 previous = {sig: signal.signal(sig, forward) for sig in signals}
171 try:
172 return child.wait()
173 finally:
174 for sig, handler in previous.items():
175 signal.signal(sig, handler)
176 os.close(descriptor)
177
178
179def _metadata_fields(path: Path) -> dict[str, str]:
180 """Read one private, regular schema-2 metadata record."""
181 info = path.lstat()
182 if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode):
183 fail("metadata is not a regular file")
184 if info.st_uid != os.geteuid() or info.st_nlink != 1 or info.st_mode & SAFE_FILE_MODE_MASK:
185 fail("metadata ownership, links, or mode are unsafe")
186 fields: dict[str, str] = {}
187 for line in path.read_text(encoding="ascii").splitlines():
188 key, separator, value = line.partition("=")
189 if not separator or key in fields or not value or not value.isprintable():
190 fail("metadata contains an invalid or duplicate field")
191 fields[key] = value
192 if set(fields) != EXPECTED_METADATA_FIELDS or fields.get("schema") != "2":
193 fail("metadata schema is unsupported")
194 return fields
195
196
197def validate_metadata(raw_path: str, raw_root: str, name: str, owner: str) -> int:
198 """Validate deletion authority for one exact metadata record."""
199 path = lexical(raw_path)
200 root = lexical(raw_root)
201 expected = root / ".meta" / name
202 if path != expected:
203 fail("metadata path is outside its exact reserved slot")
204 fields = _metadata_fields(path)
205 if fields["name"] != name or fields["path"] != str(root / name):
206 fail("metadata name/path binding is inconsistent")
207 actual_owner = fields["owner"]
208 if owner not in ("any", actual_owner):
209 fail(f"metadata owner is {actual_owner}, expected {owner}")
210 if actual_owner not in {"agent", "work"}:
211 fail("metadata owner is unsupported")
212 identifier = name.removeprefix("work-")
213 if actual_owner == "work":
214 if not name.startswith("work-") or fields["branch"] != f"work/{identifier}":
215 fail("work metadata violates reserved name/branch binding")
216 elif name.startswith("work-"):
217 fail("agent metadata may not occupy a reserved work-* name")
218 return 0
219
220
221def main(argv: list[str]) -> int:
222 """Dispatch the private lock wrapper or metadata validator."""
223 try:
224 if len(argv) >= LOCK_ARGC_MIN and argv[0] == "lock" and argv[4] == "--":
225 return run_locked(argv[1], argv[2], argv[3], argv[5:])
226 if len(argv) == METADATA_ARGC and argv[0] == "metadata":
227 return validate_metadata(argv[1], argv[2], argv[3], argv[4])
228 fail("invalid workspace guard invocation")
229 except (GuardError, OSError, UnicodeError) as exc:
230 print(f"workspace guard: {exc}", file=sys.stderr)
231 return 1
232
233
234if __name__ == "__main__":
235 raise SystemExit(main(sys.argv[1:]))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298